Skip to content

Instantly share code, notes, and snippets.

@justinfagnani
Created August 7, 2018 14:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justinfagnani/639f2fe13625e6b1b2a00f888f2403cf to your computer and use it in GitHub Desktop.
Save justinfagnani/639f2fe13625e6b1b2a00f888f2403cf to your computer and use it in GitHub Desktop.
ModuleWorker - Easily access and call exports of a JS module in a Worker
export const a = 'hello';
export const f = (a: any, b: any, c: any) => {
return {
a, b, c
};
};
import {ModuleWorker} from './module-worker.js';
(async () => {
const worker = new ModuleWorker(new URL('./example-worker.js', import.meta.url));
let module = await worker.module;
console.log(await module.a);
console.log(await module.call('f', 1, 2, 3));
})();
let module: any;
let url: string;
addEventListener('message', async (e: MessageEvent) => {
if (e.data == null) {
return;
}
const type = e.data.type;
if (type === 'import-module') {
let exportNames, error;
try {
if (url !== undefined) {
throw new Error('Module already loaded for ModuleWorker');
}
url = e.data.url;
module = await import(url);
exportNames = Array.from(Object.keys(module));
} catch (e) {
error = e;
}
postMessage({
type: 'module-record',
exportNames,
error,
});
} else if (type === 'get-export') {
let value, error;
try {
value = await module[e.data.name];
} catch (e) {
error = e;
}
postMessage({
type: 'get-export-reply',
value,
error,
messageId: e.data.messageId,
});
} else if (type === 'call-export') {
let value, error;
try {
value = await module[e.data.name](...e.data.args);
} catch (e) {
error = e;
}
postMessage({
type: 'call-export-reply',
value,
error,
messageId: e.data.messageId,
});
}
});
declare function postMessage(message: any, transfer?: any[] | undefined): void
/**
* A proxy to a module running in a Worker.
*
* For every export in the module, there is a Promise-valued property on the
* proxy. Exported functions can be invoked with the `call()` method.
*/
export type WorkerModuleProxy<M = any> = {
/**
* Call the exported function `name` of the worker module.
*/
call(name: string, ...args: any[]): Promise<any>;
} & {
/**
* Get an exported variable of the worker module.
*/
[P in keyof M]: M[P] extends Promise<any> ? M[P] : Promise<M[P]>;
};
/**
* A worker that loads a JS module and provides a convenient way to access and
* invoke its exports.
*/
export class ModuleWorker<M = any> {
private _worker: Worker;
/**
* Resolves to a proxy for the module in a worker.
*/
module: Promise<WorkerModuleProxy<M>>;
constructor(url: string|URL) {
let resolveModule: (module: any) => void;
let rejectModule;
this.module = new Promise((resolve, reject) => {
resolveModule = resolve;
rejectModule = reject;
});
this._worker = new Worker('./module-worker-host.js');
this._worker.postMessage({
type: 'import-module',
url: url.toString(),
});
let currentMessageId = 0;
const pendingRequests = new Map<number, Deferred<any>>();
this._worker.addEventListener('message', (e: MessageEvent) => {
if (e.data == null) {
return;
}
const type = e.data.type;
if (type === 'module-record') {
const module = {
call: (name: string, ...args: any[]) => {
const messageId = currentMessageId++;
const deferred = new Deferred<M>();
pendingRequests.set(messageId, deferred);
this._worker.postMessage({
type: 'call-export',
messageId,
name,
args,
});
return deferred.promise;
}
};
for (const name of e.data.exportNames) {
Object.defineProperty(module, name, {
get: () => {
const messageId = currentMessageId++;
const deferred = new Deferred();
pendingRequests.set(messageId, deferred);
this._worker.postMessage({
type: 'get-export',
messageId,
name,
});
return deferred.promise;
},
});
}
resolveModule(module);
} else if (type === 'get-export-reply' || type === 'call-export-reply') {
const messageId = e.data.messageId;
const deferred = pendingRequests.get(messageId);
if (deferred == null) {
throw new Error(`Invalid message id: ${messageId}`);
}
pendingRequests.delete(messageId);
deferred.resolve(e.data.value);
}
});
}
}
export class Deferred<T> {
promise: Promise<T>;
resolve!: (o?: T) => void;
reject!: (e?: Error) => void;
constructor() {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment