UNPKG

make-asynchronous

Version:

Make a synchronous function asynchronous by running it in a worker

257 lines (204 loc) 5.97 kB
import {pEvent} from 'p-event'; const isNode = Boolean(globalThis.process?.versions?.node); // Assembled at runtime so bundlers don't statically detect the import and try to bundle `node:worker_threads` for browsers. const workerThreadsSpecifier = ['node:', 'worker_threads'].join(''); const makeBlob = content => new globalThis.Blob([content], {type: 'text/javascript'}); // A worker_threads message arrives as the raw value, while a Web Worker wraps it in a `MessageEvent`. const getMessageData = message => isNode ? message : message.data; /* On Node.js, the worker source is executed directly via `eval`, and this preamble shims the Web Worker globals (`postMessage`/`onmessage`) that the worker body uses onto `worker_threads`'s `parentPort`. This lets the exact same worker body run in both Node.js and browsers. */ const nodeWorkerPreamble = String.raw` import {parentPort, workerData} from 'node:worker_threads'; import {registerHooks} from 'node:module'; const isBareSpecifier = specifier => !specifier.startsWith('.') && !specifier.startsWith('/') && !/^[a-z\d+.-]+:/i.test(specifier); if (workerData.baseUrl) { registerHooks({ resolve(specifier, context, nextResolve) { if (context.parentURL === import.meta.url && isBareSpecifier(specifier)) { return nextResolve(specifier, {...context, parentURL: workerData.baseUrl}); } return nextResolve(specifier, context); }, }); } globalThis.self = globalThis; globalThis.postMessage = data => parentPort.postMessage(data); parentPort.on('message', data => globalThis.onmessage({data})); `; async function createWorker(content, {baseUrl} = {}) { let url; let worker; const cleanup = () => { if (url) { URL.revokeObjectURL(url); } worker?.terminate(); }; if (isNode) { const {Worker: NodeWorker} = await import(workerThreadsSpecifier); worker = new NodeWorker(nodeWorkerPreamble + content, { eval: true, workerData: { baseUrl: baseUrl ? String(baseUrl) : undefined, }, }); } else { url = URL.createObjectURL(makeBlob(content)); worker = new globalThis.Worker(url, {type: 'module'}); } return { worker, cleanup, }; } const makeContent = function_ => ` globalThis.onmessage = async ({data: arguments_}) => { try { const output = await (${function_.toString()})(...arguments_); globalThis.postMessage({output}); } catch (error) { globalThis.postMessage({error}); } }; `; export default function makeAsynchronous(function_, options) { const content = makeContent(function_); const setup = () => createWorker(content, options); async function run({worker, arguments_}) { const promise = pEvent(worker, 'message', { rejectionEvents: ['error', 'messageerror'], }); worker.postMessage(arguments_); const {output, error} = getMessageData(await promise); if (error) { throw error; } return output; } const fn = async (...arguments_) => { const {worker, cleanup} = await setup(); try { return await run({arguments_, worker}); } finally { cleanup(); } }; fn.withSignal = signal => async (...arguments_) => { signal.throwIfAborted(); let cleanup; try { const {worker, cleanup: cleanup_} = await setup(); cleanup = cleanup_; signal.throwIfAborted(); const abortPromise = pEvent(signal, [], { rejectionEvents: ['abort'], }); try { return await Promise.race([ run({arguments_, worker}), abortPromise, ]); } finally { abortPromise.cancel(); } } catch (error) { signal.throwIfAborted(); throw error; } finally { cleanup?.(); } }; return fn; } const makeIterableContent = function_ => ` const nothing = Symbol('nothing'); let iterator = nothing; globalThis.onmessage = async ({data: arguments_}) => { try { if (iterator === nothing) { iterator = await (${function_.toString()})(...arguments_); } const output = await iterator.next(); globalThis.postMessage({output}); } catch (error) { globalThis.postMessage({error}); } }; `; export function makeAsynchronousIterable(function_, options) { const content = makeIterableContent(function_); const setup = () => createWorker(content, options); const fn = (...arguments_) => ({ async * [Symbol.asyncIterator]() { const {worker, cleanup} = await setup(); try { let isFirstMessage = true; while (true) { const promise = pEvent(worker, 'message', { rejectionEvents: ['error', 'messageerror'], }); worker.postMessage(isFirstMessage ? arguments_ : undefined); isFirstMessage = false; const {output, error} = getMessageData(await promise); // eslint-disable-line no-await-in-loop if (error) { throw error; } const {value, done} = output; if (done) { break; } yield value; } } finally { cleanup(); } }, }); fn.withSignal = signal => (...arguments_) => ({ async * [Symbol.asyncIterator]() { signal.throwIfAborted(); let cleanup; try { const {worker, cleanup: cleanup_} = await setup(); cleanup = cleanup_; signal.throwIfAborted(); const abortPromise = pEvent(signal, [], { rejectionEvents: ['abort'], }); let isFirstMessage = true; try { while (true) { const promise = Promise.race([ pEvent(worker, 'message', { rejectionEvents: ['error', 'messageerror'], }), abortPromise, ]); worker.postMessage(isFirstMessage ? arguments_ : undefined); isFirstMessage = false; const {output, error} = getMessageData(await promise); // eslint-disable-line no-await-in-loop if (error) { throw error; } const {value, done} = output; if (done) { break; } yield value; } } finally { abortPromise.cancel(); } } catch (error) { signal.throwIfAborted(); throw error; } finally { cleanup?.(); } }, }); return fn; }