未验证 提交 d4ee10eb 编写于 作者: P Peter Pan 提交者: GitHub

fix browser compatibility (#903)

* feat: check browser compatibility

* fix: fix browser compatibility of esmodule in web worker
上级 44834335
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<script nomodule src="%PUBLIC_URL%/_dist_/assets/js/compatibility.js"></script>
<script type="module" src="%PUBLIC_URL%/_dist_/index.js"></script> <script type="module" src="%PUBLIC_URL%/_dist_/index.js"></script>
</body> </body>
</html> </html>
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
document.body.innerHTML =
'<p style="text-align: center;margin: 20px auto;">Your browser is not supported by VisualDL. <br /> Please refer to <a href="https://github.com/PaddlePaddle/VisualDL" target="_blank" rel="noreferrer noopener">VisualDL Document</a> for more information.</p>';
window.addEventListener('error', e => e.preventDefault());
...@@ -16,8 +16,7 @@ ...@@ -16,8 +16,7 @@
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import type {InitializeData} from '~/worker'; import WebWorker from '~/worker';
import {WebWorker} from '~/worker';
const BASE_URI: string = import.meta.env.SNOWPACK_PUBLIC_BASE_URI; const BASE_URI: string = import.meta.env.SNOWPACK_PUBLIC_BASE_URI;
...@@ -31,8 +30,7 @@ const useWorker = <D, P = unknown, E extends Error = Error>(name: string, params ...@@ -31,8 +30,7 @@ const useWorker = <D, P = unknown, E extends Error = Error>(name: string, params
const [result, setResult] = useState<WorkerResult<D, E>>({}); const [result, setResult] = useState<WorkerResult<D, E>>({});
useEffect(() => { useEffect(() => {
const worker = new WebWorker(`${BASE_URI}/_dist_/worker/${name}.js`, {type: 'module'}); const worker = new WebWorker(`${BASE_URI}/_dist_/worker/${name}.js`);
worker.emit<InitializeData>('INITIALIZE', {env: import.meta.env});
worker.on('INITIALIZED', () => { worker.on('INITIALIZED', () => {
setResult({worker}); setResult({worker});
worker.emit('RUN', params); worker.emit('RUN', params);
......
...@@ -26,7 +26,7 @@ import type { ...@@ -26,7 +26,7 @@ import type {
} from '~/resource/high-dimensional'; } from '~/resource/high-dimensional';
import {PCA, UMAP, tSNE} from '~/resource/high-dimensional'; import {PCA, UMAP, tSNE} from '~/resource/high-dimensional';
import {WorkerSelf} from '~/worker'; import type {Runner} from '~/worker/types';
import type {tSNEOptions} from '~/resource/high-dimensional/tsne'; import type {tSNEOptions} from '~/resource/high-dimensional/tsne';
type InfoStepData = { type InfoStepData = {
...@@ -41,86 +41,90 @@ type InfoParamsData = { ...@@ -41,86 +41,90 @@ type InfoParamsData = {
}; };
export type InfoData = InfoStepData | InfoResetData | InfoParamsData; export type InfoData = InfoStepData | InfoResetData | InfoParamsData;
const workerSelf = new WorkerSelf(); const runner: Runner = worker => {
workerSelf.emit('INITIALIZED'); const pca = (data: PCAParams) => {
workerSelf.on<CalculateParams>('RUN', ({reduction, params}) => { const {vectors, variance, totalVariance} = PCA(data.input, data.dim, data.n);
switch (reduction) { worker.emit<PCAResult>('RESULT', {
case 'pca': vectors: vectors as Vectors,
return pca(params as PCAParams); variance,
case 'tsne': totalVariance
return tsne(params as TSNEParams);
case 'umap':
return umap(params as UMAPParams);
default:
return null as never;
}
});
function pca(data: PCAParams) {
const {vectors, variance, totalVariance} = PCA(data.input, data.dim, data.n);
workerSelf.emit<PCAResult>('RESULT', {
vectors: vectors as Vectors,
variance,
totalVariance
});
}
function tsne(data: TSNEParams) {
const t_sne = new tSNE({
dimension: data.n,
perplexity: data.perplexity,
epsilon: data.epsilon
});
const reset = () => {
t_sne.setData(data.input, data.dim);
workerSelf.emit<TSNEResult>('RESULT', {
vectors: t_sne.solution as [number, number, number][],
step: t_sne.step
}); });
}; };
reset(); const tsne = (data: TSNEParams) => {
const t_sne = new tSNE({
dimension: data.n,
perplexity: data.perplexity,
epsilon: data.epsilon
});
workerSelf.on<InfoData>('INFO', infoData => { const reset = () => {
const type = infoData.type; t_sne.setData(data.input, data.dim);
switch (type) { worker.emit<TSNEResult>('RESULT', {
case 'step': { vectors: t_sne.solution as [number, number, number][],
t_sne.run(); step: t_sne.step
return workerSelf.emit<TSNEResult>('RESULT', { });
vectors: t_sne.solution as [number, number, number][], };
step: t_sne.step
}); reset();
}
case 'reset': { worker.on<InfoData>('INFO', infoData => {
return reset(); const type = infoData.type;
} switch (type) {
case 'params': { case 'step': {
const data = (infoData as InfoParamsData).data; t_sne.run();
if (data?.perplexity != null) { return worker.emit<TSNEResult>('RESULT', {
t_sne.setPerplexity(data.perplexity); vectors: t_sne.solution as [number, number, number][],
step: t_sne.step
});
}
case 'reset': {
return reset();
} }
if (data?.epsilon != null) { case 'params': {
t_sne.setEpsilon(data.epsilon); const data = (infoData as InfoParamsData).data;
if (data?.perplexity != null) {
t_sne.setPerplexity(data.perplexity);
}
if (data?.epsilon != null) {
t_sne.setEpsilon(data.epsilon);
}
return;
} }
return; default:
return null as never;
} }
});
};
const umap = (data: UMAPParams) => {
const result = UMAP(data.n, data.neighbors, data.input, data.dim);
if (result) {
worker.emit<UMAPResult>('RESULT', {
vectors: result.embedding as [number, number, number][],
epoch: result.nEpochs,
nEpochs: result.nEpochs
});
}
worker.on('INFO', () => {
worker.emit('INITIALIZED');
});
};
worker.on<CalculateParams>('RUN', ({reduction, params}) => {
switch (reduction) {
case 'pca':
return pca(params as PCAParams);
case 'tsne':
return tsne(params as TSNEParams);
case 'umap':
return umap(params as UMAPParams);
default: default:
return null as never; return null as never;
} }
}); });
}
function umap(data: UMAPParams) { worker.emit('INITIALIZED');
const result = UMAP(data.n, data.neighbors, data.input, data.dim); };
if (result) {
workerSelf.emit<UMAPResult>('RESULT', { export default runner;
vectors: result.embedding as [number, number, number][],
epoch: result.nEpochs,
nEpochs: result.nEpochs
});
}
workerSelf.on('INFO', () => {
workerSelf.emit('INITIALIZED');
});
}
...@@ -17,18 +17,21 @@ ...@@ -17,18 +17,21 @@
import {parseFromBlob, parseFromString} from '~/resource/high-dimensional'; import {parseFromBlob, parseFromString} from '~/resource/high-dimensional';
import type {ParseParams} from '~/resource/high-dimensional'; import type {ParseParams} from '~/resource/high-dimensional';
import {WorkerSelf} from '~/worker'; import type {Runner} from '~/worker/types';
const workerSelf = new WorkerSelf(); const runner: Runner = worker => {
workerSelf.emit('INITIALIZED'); worker.on<ParseParams>('RUN', async data => {
workerSelf.on<ParseParams>('RUN', async data => { if (data) {
if (data) { if (data.from === 'string') {
if (data.from === 'string') { return worker.emit('RESULT', parseFromString(data.params));
return workerSelf.emit('RESULT', parseFromString(data.params)); }
if (data.from === 'blob') {
return worker.emit('RESULT', await parseFromBlob(data.params));
}
} }
if (data.from === 'blob') { worker.emit('RESULT', null);
return workerSelf.emit('RESULT', await parseFromBlob(data.params)); });
} worker.emit('INITIALIZED');
} };
workerSelf.emit('RESULT', null);
}); export default runner;
...@@ -15,68 +15,50 @@ ...@@ -15,68 +15,50 @@
*/ */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type {InitializeData, WorkerMessage, WorkerMessageEvent} from '~/worker/types'; import type {
Handler,
export type {InitializeData}; IWorker,
InitializeData,
type WorkerMessageType<T> = WorkerMessage<T>['type']; Listeners,
type Handler<T> = (data: T) => unknown; WorkerMessage,
type Listeners = Partial<Record<WorkerMessageType<any>, Handler<any>[]>>; WorkerMessageEvent,
WorkerMessageType
} from '~/worker/types';
import {
callListener,
checkWorkerModuleSupport,
pushFunctionToListener,
removeFunctionFromListener,
runner
} from '~/worker/utils';
import EventEmitter from 'eventemitter3';
const BASE_URI: string = import.meta.env.SNOWPACK_PUBLIC_BASE_URI;
export default class WebWorker implements IWorker {
private listeners: Listeners = {};
private onceListeners: Listeners = {};
private worker: Worker | null = null;
private emitter: EventEmitter | null = null;
env = import.meta.env;
interface IWorker { constructor(name: string) {
emit<T>(type: WorkerMessageType<T>, data?: T): void; const workerPath = `${BASE_URI}/_dist_/worker`;
on<T>(type: WorkerMessageType<T>, handler: Handler<T>): void;
off<T>(type: WorkerMessageType<T>, handler?: Handler<T>): void;
once<T>(type: WorkerMessageType<T>, handler: Handler<T>): void;
}
function handlerInListener<T>(listeners: Listeners, type: WorkerMessageType<T>, handler: Handler<T>) { if (checkWorkerModuleSupport()) {
return listeners[type]!.findIndex(f => Object.is(f, handler)); this.worker = new Worker(`${workerPath}/worker.js`, {type: 'module'});
} this.worker.addEventListener('message', this.listener.bind(this));
function pushFunctionToListener<T>(listeners: Listeners, type: WorkerMessageType<T>, handler: Handler<T>) { this.emit<InitializeData>('INITIALIZE', {name, env: import.meta.env});
if (!listeners[type]) {
listeners[type] = [];
}
if (handlerInListener(listeners, type, handler) !== -1) {
return;
}
listeners[type]!.push(handler);
}
function removeFunctionFromListener<T>(listeners: Listeners, type: WorkerMessageType<T>, handler?: Handler<T>) {
if (listeners[type]) {
if (!handler) {
delete listeners[type];
} else { } else {
const index = handlerInListener(listeners, type, handler); this.emitter = new EventEmitter();
if (index !== -1) { this.emitter.addListener('message', this.listener.bind(this));
listeners[type]!.splice(index, 1); window.setTimeout(() => {
} runner(name, this);
}, 200);
} }
} }
}
function callListener<T>(this: IWorker, listeners: Listeners, data: WorkerMessage<T>) {
listeners[data.type]?.forEach(handler => {
try {
handler(data.data);
} catch (e) {
const error = e instanceof Error ? e : new Error(e);
this.emit('ERROR', error);
}
});
}
export class WorkerSelf implements IWorker {
private listeners: Listeners = {};
private onceListeners: Listeners = {};
constructor() {
self.addEventListener('message', this.listener.bind(this));
this.on<InitializeData>('INITIALIZE', ({env}) => {
self.__snowpack_env__ = env;
});
}
private listener<T>(e: WorkerMessageEvent<T>) { private listener<T>(e: WorkerMessageEvent<T>) {
callListener.call(this, this.onceListeners, e.data); callListener.call(this, this.onceListeners, e.data);
...@@ -85,10 +67,14 @@ export class WorkerSelf implements IWorker { ...@@ -85,10 +67,14 @@ export class WorkerSelf implements IWorker {
} }
emit<T>(type: WorkerMessageType<T>, data?: T) { emit<T>(type: WorkerMessageType<T>, data?: T) {
self.postMessage({ if (this.worker) {
type, this.worker.postMessage({
data type,
} as WorkerMessage<T>); data
} as WorkerMessage<T>);
} else if (this.emitter) {
this.emitter.emit('message', {data: {type, data}});
}
} }
on<T>(type: WorkerMessageType<T>, handler: Handler<T>) { on<T>(type: WorkerMessageType<T>, handler: Handler<T>) {
...@@ -103,40 +89,10 @@ export class WorkerSelf implements IWorker { ...@@ -103,40 +89,10 @@ export class WorkerSelf implements IWorker {
once<T>(type: WorkerMessageType<T>, handler: Handler<T>) { once<T>(type: WorkerMessageType<T>, handler: Handler<T>) {
pushFunctionToListener(this.onceListeners, type, handler); pushFunctionToListener(this.onceListeners, type, handler);
} }
}
export class WebWorker extends Worker implements IWorker { terminate() {
private listeners: Listeners = {}; if (this.worker) {
private onceListeners: Listeners = {}; this.worker.terminate();
}
constructor(...args: ConstructorParameters<typeof Worker>) {
super(...args);
this.addEventListener('message', this.listener.bind(this));
}
private listener<T>(e: WorkerMessageEvent<T>) {
callListener.call(this, this.onceListeners, e.data);
callListener.call(this, this.listeners, e.data);
delete this.onceListeners[e.data.type];
}
emit<T>(type: WorkerMessageType<T>, data?: T) {
this.postMessage({
type,
data
} as WorkerMessage<T>);
}
on<T>(type: WorkerMessageType<T>, handler: Handler<T>) {
pushFunctionToListener(this.listeners, type, handler);
}
off<T>(type: WorkerMessageType<T>, handler?: Handler<T>) {
removeFunctionFromListener(this.listeners, type, handler);
removeFunctionFromListener(this.onceListeners, type, handler);
}
once<T>(type: WorkerMessageType<T>, handler: Handler<T>) {
pushFunctionToListener(this.onceListeners, type, handler);
} }
} }
...@@ -14,21 +14,25 @@ ...@@ -14,21 +14,25 @@
* limitations under the License. * limitations under the License.
*/ */
type WorkerMessageType<T extends string, D = void> = { /* eslint-disable @typescript-eslint/no-explicit-any */
type: T;
data: D;
};
type Env = Record<string, string>;
export type InitializeData = { export type InitializeData = {
env: Record<string, string>; name: string;
env: Env;
};
type WorkerMessageData<T extends string, D = void> = {
type: T;
data: D;
}; };
type InitializeMessage = WorkerMessageType<'INITIALIZE', InitializeData>; type InitializeMessage = WorkerMessageData<'INITIALIZE', InitializeData>;
type InitializedMessage = WorkerMessageType<'INITIALIZED'>; type InitializedMessage = WorkerMessageData<'INITIALIZED'>;
type RunMessage<T> = WorkerMessageType<'RUN', T>; type RunMessage<T> = WorkerMessageData<'RUN', T>;
type ResultMessage<T> = WorkerMessageType<'RESULT', T>; type ResultMessage<T> = WorkerMessageData<'RESULT', T>;
type InfoMessage<T> = WorkerMessageType<'INFO', T>; type InfoMessage<T> = WorkerMessageData<'INFO', T>;
type ErrorMessage<E extends Error = Error> = WorkerMessageType<'ERROR', E>; type ErrorMessage<E extends Error = Error> = WorkerMessageData<'ERROR', E>;
export type WorkerMessage<T> = export type WorkerMessage<T> =
| InitializeMessage | InitializeMessage
...@@ -39,3 +43,17 @@ export type WorkerMessage<T> = ...@@ -39,3 +43,17 @@ export type WorkerMessage<T> =
| ErrorMessage; | ErrorMessage;
export type WorkerMessageEvent<T> = MessageEvent<WorkerMessage<T>>; export type WorkerMessageEvent<T> = MessageEvent<WorkerMessage<T>>;
export type WorkerMessageType<T> = WorkerMessage<T>['type'];
export type Handler<T> = (data: T) => unknown;
export type Listeners = Partial<Record<WorkerMessageType<any>, Handler<any>[]>>;
export interface IWorker {
env: Env;
emit<T>(type: WorkerMessageType<T>, data?: T): void;
on<T>(type: WorkerMessageType<T>, handler: Handler<T>): void;
off<T>(type: WorkerMessageType<T>, handler?: Handler<T>): void;
once<T>(type: WorkerMessageType<T>, handler: Handler<T>): void;
}
export type Runner = (worker: IWorker) => void;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type {Handler, IWorker, Listeners, Runner, WorkerMessage, WorkerMessageType} from '~/worker/types';
export function checkWorkerModuleSupport() {
let supports = false;
const tester = {
get type() {
// it's been called, it's supported
supports = true;
return 'module' as const;
}
};
try {
// We use "blob://" as url to avoid an useless network request.
// This will either throw in Chrome
// either fire an error event in Firefox
// which is perfect since
// we don't need the worker to actually start,
// checking for the type of the script is done before trying to load it.
new Worker('blob://', tester);
} finally {
return supports;
}
}
export function handlerInListener<T>(listeners: Listeners, type: WorkerMessageType<T>, handler: Handler<T>) {
return listeners[type]!.findIndex(f => Object.is(f, handler));
}
export function pushFunctionToListener<T>(listeners: Listeners, type: WorkerMessageType<T>, handler: Handler<T>) {
if (!listeners[type]) {
listeners[type] = [];
}
if (handlerInListener(listeners, type, handler) !== -1) {
return;
}
listeners[type]!.push(handler);
}
export function removeFunctionFromListener<T>(listeners: Listeners, type: WorkerMessageType<T>, handler?: Handler<T>) {
if (listeners[type]) {
if (!handler) {
delete listeners[type];
} else {
const index = handlerInListener(listeners, type, handler);
if (index !== -1) {
listeners[type]!.splice(index, 1);
}
}
}
}
export function callListener<T>(this: IWorker, listeners: Listeners, data: WorkerMessage<T>) {
listeners[data.type]?.forEach(handler => {
try {
handler(data.data);
} catch (e) {
const error = e instanceof Error ? e : new Error(e);
this.emit('ERROR', error);
}
});
}
export async function runner(name: string, worker: IWorker) {
const {default: runner}: {default: Runner} = await import(name);
runner(worker);
}
...@@ -16,33 +16,29 @@ ...@@ -16,33 +16,29 @@
import * as funcs from '@visualdl/wasm'; import * as funcs from '@visualdl/wasm';
import type {InitializeData} from '~/worker'; import type {Runner} from '~/worker/types';
import {WorkerSelf} from '~/worker';
import initWasm from '@visualdl/wasm'; import initWasm from '@visualdl/wasm';
const workerSelf = new WorkerSelf();
type FuncNames = Exclude<keyof typeof funcs, 'default'>; type FuncNames = Exclude<keyof typeof funcs, 'default'>;
async function init(env: Record<string, string>) { const runner: Runner = async worker => {
const PUBLIC_PATH = env.SNOWPACK_PUBLIC_PATH; const PUBLIC_PATH = worker.env.SNOWPACK_PUBLIC_PATH;
await initWasm(`${PUBLIC_PATH}/wasm/visualdl.wasm`); await initWasm(`${PUBLIC_PATH}/wasm/visualdl.wasm`);
workerSelf.emit('INITIALIZED'); worker.on<{name: FuncNames; params: unknown[]}>('RUN', ({name, params}) => {
workerSelf.on<{name: FuncNames; params: unknown[]}>('RUN', ({name, params}) => {
try { try {
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
const result = (funcs[name] as Function)(...params); const result = (funcs[name] as Function)(...params);
workerSelf.emit('RESULT', result); worker.emit('RESULT', result);
} catch (e) { } catch (e) {
if (e.message !== 'unreachable') { if (e.message !== 'unreachable') {
throw e; throw e;
} }
} }
}); });
}
workerSelf.on<InitializeData>('INITIALIZE', ({env}) => { worker.emit('INITIALIZED');
init(env); };
});
export default runner;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {
Handler,
IWorker,
InitializeData,
Listeners,
WorkerMessage,
WorkerMessageEvent,
WorkerMessageType
} from '~/worker/types';
import {callListener, pushFunctionToListener, removeFunctionFromListener, runner} from '~/worker/utils';
class WorkerSelf implements IWorker {
private listeners: Listeners = {};
private onceListeners: Listeners = {};
env = {};
constructor() {
self.addEventListener('message', this.listener.bind(this));
}
private listener<T>(e: WorkerMessageEvent<T>) {
callListener.call(this, this.onceListeners, e.data);
callListener.call(this, this.listeners, e.data);
delete this.onceListeners[e.data.type];
}
emit<T>(type: WorkerMessageType<T>, data?: T) {
self.postMessage({
type,
data
} as WorkerMessage<T>);
}
on<T>(type: WorkerMessageType<T>, handler: Handler<T>) {
pushFunctionToListener(this.listeners, type, handler);
}
off<T>(type: WorkerMessageType<T>, handler?: Handler<T>) {
removeFunctionFromListener(this.listeners, type, handler);
removeFunctionFromListener(this.onceListeners, type, handler);
}
once<T>(type: WorkerMessageType<T>, handler: Handler<T>) {
pushFunctionToListener(this.onceListeners, type, handler);
}
}
const worker = new WorkerSelf();
worker.once<InitializeData>('INITIALIZE', ({name, env}) => {
worker.env = env;
runner(name, worker);
});
...@@ -19,7 +19,6 @@ declare global { ...@@ -19,7 +19,6 @@ declare global {
webkitAudioContext: AudioContext; webkitAudioContext: AudioContext;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
_hmt: any[]; _hmt: any[];
__snowpack_env__: Record<string, string>;
} }
} }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册