Skip to content

Instantly share code, notes, and snippets.

@nikolaskhodov
Created June 11, 2022 19:27
Show Gist options
  • Select an option

  • Save nikolaskhodov/c6b39d67e0bdaea6b1a7e1f3c497a7ed to your computer and use it in GitHub Desktop.

Select an option

Save nikolaskhodov/c6b39d67e0bdaea6b1a7e1f3c497a7ed to your computer and use it in GitHub Desktop.
Run an async function in web workers
import { v4 } from 'uuid';
interface IProgressMessage<T = any> {
id: string;
type: 'progress';
progress: T;
}
interface IDoneMessage<T = any> {
id: string;
type: 'done';
result: T;
}
interface IErrorMessage {
id: string;
type: 'error';
error: Error;
}
interface IStartMessage<T = any> {
id: string;
type: 'start';
args: T;
}
interface ITriggerFunctionConfig<P = any> {
onProgress?: (progress: P) => void;
}
function isStartMessage(arg: any): arg is IStartMessage {
return arg?.id !== undefined && arg?.type === 'start';
}
function isProgressMessage(arg: any): arg is IProgressMessage<any> {
return arg?.id !== undefined && arg?.type === 'progress';
}
function isDoneMessage(arg: any): arg is IDoneMessage<any> {
return arg?.id !== undefined && arg?.type === 'done';
}
function isErrorMessage(arg: any): arg is IErrorMessage {
return arg?.id !== undefined && arg?.type === 'error';
}
export type WorkerFunction<T, U, P> = (
args: T,
callback: (result: U | Error) => void,
progress?: (progress: P) => void
) => void;
export type TriggerFunction<T, U, P> = (
args: T,
workerConfig?: ITriggerFunctionConfig<P>
) => Promise<U>;
function webWorkerHandler(event: MessageEvent<any>): void {
// @ts-ignore
let func: WorkerFunction<any, any, any> = _func;
const data = event.data;
if (isStartMessage(data)) {
const id = data.id;
func(
data.args,
function(result) {
if (result instanceof Error) {
const errorMessage: IErrorMessage = {
id,
type: 'error',
error: result,
};
postMessage(errorMessage);
} else {
const doneMessage: IDoneMessage = {
id,
type: 'done',
result,
};
postMessage(doneMessage);
}
},
function(progress) {
const progressMessage: IProgressMessage = {
id,
type: 'progress',
progress,
};
postMessage(progressMessage);
}
);
}
}
export function runFunctionInWebWorker<T, U, P = any>(
func: WorkerFunction<T, U, P>,
config?: {
timeout?: number;
keepAlive?: boolean;
}
): TriggerFunction<T, U, P> {
if (!window.URL || !window.URL.createObjectURL) {
return;
}
const script = new Blob(
[
`
const _func = ${func.toString()};
${isStartMessage.toString()}
self.onmessage = ${webWorkerHandler.toString()};
`,
],
{
type: 'text/javascript',
}
);
const blob = window.URL.createObjectURL(script);
const permanentWorker: Worker | undefined =
config?.keepAlive === true ? new window.Worker(blob) : undefined;
return (args: T, workerConfig?: ITriggerFunctionConfig<P>): Promise<U> => {
return new Promise<U>((resolve, reject) => {
const worker =
config?.keepAlive === true ? permanentWorker : new window.Worker(blob);
const id = v4();
const cleanup = () => {
if (config?.keepAlive !== true) {
worker.terminate();
}
worker.removeEventListener('message', messageHandler);
clearTimeout(timeoutTimer);
};
const timeout = config?.timeout || 1000;
const timeoutTimer = setTimeout(() => {
reject(new Error('Timed out'));
cleanup();
}, timeout);
const messageHandler = (event: MessageEvent) => {
const response = event.data;
if (response?.id === id) {
if (isDoneMessage(response)) {
resolve(response.result);
cleanup();
} else if (isErrorMessage(response)) {
reject(response.error);
cleanup();
} else if (isProgressMessage(response)) {
workerConfig?.onProgress &&
workerConfig.onProgress(response.progress);
}
}
};
worker.addEventListener('message', messageHandler);
try {
const startMessage: IStartMessage = {
id,
type: 'start',
args,
};
worker.postMessage(startMessage);
} catch (err) {
reject(err);
cleanup();
}
});
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment