UNPKG

@xylabs/threads

Version:

Web workers & worker threads as simple as a function call

465 lines (453 loc) 15.7 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/master/spawn.ts import DebugLogger2 from "debug"; import { Observable as Observable3 } from "observable-fns"; // src/serializers.ts var DefaultErrorSerializer = { deserialize(message) { return Object.assign(new Error(message.message), { name: message.name, stack: message.stack }); }, serialize(error) { return { __error_marker: "$$error", message: error.message, name: error.name, stack: error.stack }; } }; var isSerializedError = /* @__PURE__ */ __name((thing) => thing && typeof thing === "object" && "__error_marker" in thing && thing.__error_marker === "$$error", "isSerializedError"); var DefaultSerializer = { deserialize(message) { return isSerializedError(message) ? DefaultErrorSerializer.deserialize(message) : message; }, serialize(input) { return input instanceof Error ? DefaultErrorSerializer.serialize(input) : input; } }; // src/common.ts globalThis.registeredSerializer = globalThis.registeredSerializer ?? DefaultSerializer; function deserialize(message) { return globalThis.registeredSerializer.deserialize(message); } __name(deserialize, "deserialize"); function serialize(input) { return globalThis.registeredSerializer.serialize(input); } __name(serialize, "serialize"); // src/promise.ts var doNothing = /* @__PURE__ */ __name(() => void 0, "doNothing"); function createPromiseWithResolver() { let alreadyResolved = false; let resolvedTo; let resolver = doNothing; const promise = new Promise((resolve) => { if (alreadyResolved) { resolve(resolvedTo); } else { resolver = resolve; } }); const exposedResolver = /* @__PURE__ */ __name((value) => { alreadyResolved = true; resolvedTo = value; resolver(resolvedTo); }, "exposedResolver"); return [ promise, exposedResolver ]; } __name(createPromiseWithResolver, "createPromiseWithResolver"); // src/symbols.ts var $errors = Symbol("thread.errors"); var $events = Symbol("thread.events"); var $terminate = Symbol("thread.terminate"); var $transferable = Symbol("thread.transferable"); var $worker = Symbol("thread.worker"); // src/types/master.ts var WorkerEventType = /* @__PURE__ */ function(WorkerEventType2) { WorkerEventType2["internalError"] = "internalError"; WorkerEventType2["message"] = "message"; WorkerEventType2["termination"] = "termination"; return WorkerEventType2; }({}); // src/master/invocation-proxy.ts import DebugLogger from "debug"; import { multicast, Observable as Observable2 } from "observable-fns"; // src/observable-promise.ts import { Observable } from "observable-fns"; var doNothing2 = /* @__PURE__ */ __name(() => { }, "doNothing"); var returnInput = /* @__PURE__ */ __name((input) => input, "returnInput"); var runDeferred = /* @__PURE__ */ __name((fn) => Promise.resolve().then(fn), "runDeferred"); function fail(error) { throw error; } __name(fail, "fail"); function isThenable(thing) { return thing && typeof thing.then === "function"; } __name(isThenable, "isThenable"); var ObservablePromise = class _ObservablePromise extends Observable { static { __name(this, "ObservablePromise"); } [Symbol.toStringTag] = "[object ObservablePromise]"; initHasRun = false; fulfillmentCallbacks = []; rejectionCallbacks = []; firstValue; firstValueSet = false; rejection; state = "pending"; constructor(init) { super((originalObserver) => { const self = this; const observer = { ...originalObserver, complete() { originalObserver.complete(); self.onCompletion(); }, error(error) { originalObserver.error(error); self.onError(error); }, next(value) { originalObserver.next(value); self.onNext(value); } }; try { this.initHasRun = true; return init(observer); } catch (error) { observer.error(error); } }); } onNext(value) { if (!this.firstValueSet) { this.firstValue = value; this.firstValueSet = true; } } onError(error) { this.state = "rejected"; this.rejection = error; for (const onRejected of this.rejectionCallbacks) { runDeferred(() => onRejected(error)); } } onCompletion() { this.state = "fulfilled"; for (const onFulfilled of this.fulfillmentCallbacks) { runDeferred(() => onFulfilled(this.firstValue)); } } then(onFulfilledRaw, onRejectedRaw) { const onFulfilled = onFulfilledRaw || returnInput; const onRejected = onRejectedRaw || fail; let onRejectedCalled = false; return new Promise((resolve, reject) => { const rejectionCallback = /* @__PURE__ */ __name((error) => { if (onRejectedCalled) return; onRejectedCalled = true; try { resolve(onRejected(error)); } catch (anotherError) { reject(anotherError); } }, "rejectionCallback"); const fulfillmentCallback = /* @__PURE__ */ __name((value) => { try { resolve(onFulfilled(value)); } catch (ex) { const error = ex; rejectionCallback(error); } }, "fulfillmentCallback"); if (!this.initHasRun) { this.subscribe({ error: rejectionCallback }); } if (this.state === "fulfilled") { return resolve(onFulfilled(this.firstValue)); } if (this.state === "rejected") { onRejectedCalled = true; return resolve(onRejected(this.rejection)); } this.fulfillmentCallbacks.push(fulfillmentCallback); this.rejectionCallbacks.push(rejectionCallback); }); } catch(onRejected) { return this.then(void 0, onRejected); } finally(onCompleted) { const handler = onCompleted || doNothing2; return this.then((value) => { handler(); return value; }, () => handler()); } static from(thing) { return isThenable(thing) ? new _ObservablePromise((observer) => { const onFulfilled = /* @__PURE__ */ __name((value) => { observer.next(value); observer.complete(); }, "onFulfilled"); const onRejected = /* @__PURE__ */ __name((error) => { observer.error(error); }, "onRejected"); thing.then(onFulfilled, onRejected); }) : super.from(thing); } }; // src/transferable.ts function isTransferDescriptor(thing) { return thing && typeof thing === "object" && thing[$transferable]; } __name(isTransferDescriptor, "isTransferDescriptor"); // src/types/messages.ts var MasterMessageType = /* @__PURE__ */ function(MasterMessageType2) { MasterMessageType2["cancel"] = "cancel"; MasterMessageType2["run"] = "run"; return MasterMessageType2; }({}); var WorkerMessageType = /* @__PURE__ */ function(WorkerMessageType2) { WorkerMessageType2["error"] = "error"; WorkerMessageType2["init"] = "init"; WorkerMessageType2["result"] = "result"; WorkerMessageType2["running"] = "running"; WorkerMessageType2["uncaughtError"] = "uncaughtError"; return WorkerMessageType2; }({}); // src/master/invocation-proxy.ts var debugMessages = DebugLogger("threads:master:messages"); var nextJobUID = 1; var dedupe = /* @__PURE__ */ __name((array) => [ ...new Set(array) ], "dedupe"); var isJobErrorMessage = /* @__PURE__ */ __name((data) => data && data.type === WorkerMessageType.error, "isJobErrorMessage"); var isJobResultMessage = /* @__PURE__ */ __name((data) => data && data.type === WorkerMessageType.result, "isJobResultMessage"); var isJobStartMessage = /* @__PURE__ */ __name((data) => data && data.type === WorkerMessageType.running, "isJobStartMessage"); function createObservableForJob(worker, jobUID) { return new Observable2((observer) => { let asyncType; const messageHandler = /* @__PURE__ */ __name((event) => { debugMessages("Message from worker:", event.data); if (!event.data || event.data.uid !== jobUID) return; if (isJobStartMessage(event.data)) { asyncType = event.data.resultType; } else if (isJobResultMessage(event.data)) { if (asyncType === "promise") { if (event.data.payload !== void 0) { observer.next(deserialize(event.data.payload)); } observer.complete(); worker.removeEventListener("message", messageHandler); } else { if (event.data.payload) { observer.next(deserialize(event.data.payload)); } if (event.data.complete) { observer.complete(); worker.removeEventListener("message", messageHandler); } } } else if (isJobErrorMessage(event.data)) { const error = deserialize(event.data.error); if (asyncType === "promise" || !asyncType) { observer.error(error); } else { observer.error(error); } worker.removeEventListener("message", messageHandler); } }, "messageHandler"); worker.addEventListener("message", messageHandler); return () => { if (asyncType === "observable" || !asyncType) { const cancelMessage = { type: MasterMessageType.cancel, uid: jobUID }; worker.postMessage(cancelMessage); } worker.removeEventListener("message", messageHandler); }; }); } __name(createObservableForJob, "createObservableForJob"); function prepareArguments(rawArgs) { if (rawArgs.length === 0) { return { args: [], transferables: [] }; } const args = []; const transferables = []; for (const arg of rawArgs) { if (isTransferDescriptor(arg)) { args.push(serialize(arg.send)); transferables.push(...arg.transferables); } else { args.push(serialize(arg)); } } return { args, transferables: transferables.length === 0 ? transferables : dedupe(transferables) }; } __name(prepareArguments, "prepareArguments"); function createProxyFunction(worker, method) { return (...rawArgs) => { const uid = nextJobUID++; const { args, transferables } = prepareArguments(rawArgs); const runMessage = { args, method, type: MasterMessageType.run, uid }; debugMessages("Sending command to run function to worker:", runMessage); try { worker.postMessage(runMessage, transferables); } catch (error) { return ObservablePromise.from(Promise.reject(error)); } return ObservablePromise.from(multicast(createObservableForJob(worker, uid))); }; } __name(createProxyFunction, "createProxyFunction"); function createProxyModule(worker, methodNames) { const proxy = {}; for (const methodName of methodNames) { proxy[methodName] = createProxyFunction(worker, methodName); } return proxy; } __name(createProxyModule, "createProxyModule"); // src/master/spawn.ts var debugMessages2 = DebugLogger2("threads:master:messages"); var debugSpawn = DebugLogger2("threads:master:spawn"); var debugThreadUtils = DebugLogger2("threads:master:thread-utils"); var isInitMessage = /* @__PURE__ */ __name((data) => data && data.type === "init", "isInitMessage"); var isUncaughtErrorMessage = /* @__PURE__ */ __name((data) => data && data.type === "uncaughtError", "isUncaughtErrorMessage"); var initMessageTimeout = typeof process !== "undefined" && process.env !== void 0 && process.env.THREADS_WORKER_INIT_TIMEOUT ? Number.parseInt(process.env.THREADS_WORKER_INIT_TIMEOUT, 10) : 1e4; async function withTimeout(promise, timeoutInMs, errorMessage) { let timeoutHandle; const timeout = new Promise((resolve, reject) => { timeoutHandle = setTimeout(() => reject(new Error(errorMessage)), timeoutInMs); }); const result = await Promise.race([ promise, timeout ]); clearTimeout(timeoutHandle); return result; } __name(withTimeout, "withTimeout"); function receiveInitMessage(worker) { return new Promise((resolve, reject) => { const messageHandler = /* @__PURE__ */ __name((event) => { debugMessages2("Message from worker before finishing initialization:", event.data); if (isInitMessage(event.data)) { worker.removeEventListener("message", messageHandler); resolve(event.data); } else if (isUncaughtErrorMessage(event.data)) { worker.removeEventListener("message", messageHandler); reject(deserialize(event.data.error)); } }, "messageHandler"); worker.addEventListener("message", messageHandler); }); } __name(receiveInitMessage, "receiveInitMessage"); function createEventObservable(worker, workerTermination) { return new Observable3((observer) => { const messageHandler = /* @__PURE__ */ __name((messageEvent) => { const workerEvent = { data: messageEvent.data, type: WorkerEventType.message }; observer.next(workerEvent); }, "messageHandler"); const rejectionHandler = /* @__PURE__ */ __name((errorEvent) => { debugThreadUtils("Unhandled promise rejection event in thread:", errorEvent); const workerEvent = { error: new Error(errorEvent.reason), type: WorkerEventType.internalError }; observer.next(workerEvent); }, "rejectionHandler"); worker.addEventListener("message", messageHandler); worker.addEventListener("unhandledrejection", rejectionHandler); workerTermination.then(() => { const terminationEvent = { type: WorkerEventType.termination }; worker.removeEventListener("message", messageHandler); worker.removeEventListener("unhandledrejection", rejectionHandler); observer.next(terminationEvent); observer.complete(); }); }); } __name(createEventObservable, "createEventObservable"); function createTerminator(worker) { const [termination, resolver] = createPromiseWithResolver(); const terminate = /* @__PURE__ */ __name(async () => { debugThreadUtils("Terminating worker"); await worker.terminate(); resolver(); }, "terminate"); return { terminate, termination }; } __name(createTerminator, "createTerminator"); function setPrivateThreadProps(raw, worker, workerEvents, terminate) { const workerErrors = workerEvents.filter((event) => event.type === WorkerEventType.internalError).map((errorEvent) => errorEvent.error); return Object.assign(raw, { [$errors]: workerErrors, [$events]: workerEvents, [$terminate]: terminate, [$worker]: worker }); } __name(setPrivateThreadProps, "setPrivateThreadProps"); async function spawn(worker, options) { debugSpawn("Initializing new thread"); const timeout = options && options.timeout ? options.timeout : initMessageTimeout; const initMessage = await withTimeout(receiveInitMessage(worker), timeout, `Timeout: Did not receive an init message from worker after ${timeout}ms. Make sure the worker calls expose().`); const exposed = initMessage.exposed; const { termination, terminate } = createTerminator(worker); const events = createEventObservable(worker, termination); if (exposed.type === "function") { const proxy = createProxyFunction(worker); return setPrivateThreadProps(proxy, worker, events, terminate); } else if (exposed.type === "module") { const proxy = createProxyModule(worker, exposed.methods); return setPrivateThreadProps(proxy, worker, events, terminate); } else { const type = exposed.type; throw new Error(`Worker init message states unexpected type of expose(): ${type}`); } } __name(spawn, "spawn"); export { spawn }; //# sourceMappingURL=spawn.mjs.map