Created
August 7, 2018 14:12
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const a = 'hello'; | |
export const f = (a: any, b: any, c: any) => { | |
return { | |
a, b, c | |
}; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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