UNPKG

@xylabs/threads

Version:

Web workers & worker threads as simple as a function call

300 lines (293 loc) 10.1 kB
// src/worker/worker.node.ts import { parentPort as optionalParentPort } from "worker_threads"; import { assertEx } from "@xylabs/assert"; // src/worker/expose.ts import isSomeObservable from "is-observable-2-1-0"; // src/serializers.ts function extendSerializer(extend, implementation) { const fallbackDeserializer = extend.deserialize.bind(extend); const fallbackSerializer = extend.serialize.bind(extend); return { deserialize(message) { return implementation.deserialize(message, fallbackDeserializer); }, serialize(input) { return implementation.serialize(input, fallbackSerializer); } }; } 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 = (thing) => thing && typeof thing === "object" && "__error_marker" in thing && thing.__error_marker === "$$error"; 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 registerSerializer(serializer) { globalThis.registeredSerializer = extendSerializer(globalThis.registeredSerializer, serializer); } function deserialize(message) { return globalThis.registeredSerializer.deserialize(message); } function serialize(input) { return globalThis.registeredSerializer.serialize(input); } // src/symbols.ts var $transferable = /* @__PURE__ */ Symbol("thread.transferable"); // src/transferable.ts function isTransferable(thing) { if (!thing || typeof thing !== "object") return false; return true; } function isTransferDescriptor(thing) { return thing && typeof thing === "object" && thing[$transferable]; } function Transfer(payload, transferables) { console.log("Transfer"); if (!transferables) { if (!isTransferable(payload)) throw new Error("Not transferable"); transferables = [payload]; } return { [$transferable]: true, send: payload, transferables }; } // src/worker/expose.ts var isErrorEvent = (value) => value && value.error; function createExpose(implementation, self) { let exposeCalled = false; const activeSubscriptions = /* @__PURE__ */ new Map(); const isMasterJobCancelMessage = (thing) => thing && thing.type === "cancel" /* cancel */; const isMasterJobRunMessage = (thing) => thing && thing.type === "run" /* run */; const isObservable = (thing) => isSomeObservable(thing) || isZenObservable(thing); function isZenObservable(thing) { return thing && typeof thing === "object" && typeof thing.subscribe === "function"; } function deconstructTransfer(thing) { return isTransferDescriptor(thing) ? { payload: thing.send, transferables: thing.transferables } : { payload: thing, transferables: void 0 }; } function postFunctionInitMessage() { const initMessage = { exposed: { type: "function" }, type: "init" /* init */ }; implementation.postMessageToMaster(initMessage); } function postModuleInitMessage(methodNames) { const initMessage = { exposed: { methods: methodNames, type: "module" }, type: "init" /* init */ }; implementation.postMessageToMaster(initMessage); } function postJobErrorMessage(uid, rawError) { const { payload: error, transferables } = deconstructTransfer(rawError); const errorMessage = { error: serialize(error), type: "error" /* error */, uid }; implementation.postMessageToMaster(errorMessage, transferables); } function postJobResultMessage(uid, completed, resultValue) { const { payload, transferables } = deconstructTransfer(resultValue); const resultMessage = { complete: completed ? true : void 0, payload, type: "result" /* result */, uid }; implementation.postMessageToMaster(resultMessage, transferables); } function postJobStartMessage(uid, resultType) { const startMessage = { resultType, type: "running" /* running */, uid }; implementation.postMessageToMaster(startMessage); } function postUncaughtErrorMessage(error) { try { const errorMessage = { error: serialize(error), type: "uncaughtError" /* uncaughtError */ }; implementation.postMessageToMaster(errorMessage); } catch (subError) { console.error( "Not reporting uncaught error back to master thread as it occured while reporting an uncaught error already.\nLatest error:", subError, "\nOriginal error:", error ); } } async function runFunction(jobUID, fn, args) { let syncResult; try { syncResult = fn(...args); } catch (ex) { const error = ex; return postJobErrorMessage(jobUID, error); } const resultType = isObservable(syncResult) ? "observable" : "promise"; postJobStartMessage(jobUID, resultType); if (isObservable(syncResult)) { const subscription = syncResult.subscribe( (value) => postJobResultMessage(jobUID, false, serialize(value)), (error) => { postJobErrorMessage(jobUID, serialize(error)); activeSubscriptions.delete(jobUID); }, () => { postJobResultMessage(jobUID, true); activeSubscriptions.delete(jobUID); } ); activeSubscriptions.set(jobUID, subscription); } else { try { const result = await syncResult; postJobResultMessage(jobUID, true, serialize(result)); } catch (error) { postJobErrorMessage(jobUID, serialize(error)); } } } const expose2 = (exposed) => { if (!implementation.isWorkerRuntime()) { throw new Error("expose() called in the master thread."); } if (exposeCalled) { throw new Error("expose() called more than once. This is not possible. Pass an object to expose() if you want to expose multiple functions."); } exposeCalled = true; if (typeof exposed === "function") { implementation.subscribeToMasterMessages((messageData) => { if (isMasterJobRunMessage(messageData) && !messageData.method) { runFunction(messageData.uid, exposed, messageData.args.map(deserialize)); } }); postFunctionInitMessage(); } else if (typeof exposed === "object" && exposed) { implementation.subscribeToMasterMessages((messageData) => { if (isMasterJobRunMessage(messageData) && messageData.method) { runFunction(messageData.uid, exposed[messageData.method], messageData.args.map(deserialize)); } }); const methodNames = Object.keys(exposed).filter((key) => typeof exposed[key] === "function"); postModuleInitMessage(methodNames); } else { throw new Error(`Invalid argument passed to expose(). Expected a function or an object, got: ${exposed}`); } implementation.subscribeToMasterMessages((messageData) => { if (isMasterJobCancelMessage(messageData)) { const jobUID = messageData.uid; const subscription = activeSubscriptions.get(jobUID); if (subscription) { subscription.unsubscribe(); activeSubscriptions.delete(jobUID); } } }); }; if (typeof globalThis !== "undefined" && typeof self.addEventListener === "function" && implementation.isWorkerRuntime()) { self.addEventListener("error", (event) => { setTimeout(() => postUncaughtErrorMessage(isErrorEvent(event) ? event.error : event), 250); }); self.addEventListener("unhandledrejection", (event) => { const error = event.reason; if (error && typeof error.message === "string") { setTimeout(() => postUncaughtErrorMessage(error), 250); } }); } if (typeof process !== "undefined" && typeof process.on === "function" && implementation.isWorkerRuntime()) { process.on("uncaughtException", (error) => { setTimeout(() => postUncaughtErrorMessage(error), 250); }); process.on("unhandledRejection", (error) => { if (error && typeof error.message === "string") { setTimeout(() => postUncaughtErrorMessage(error), 250); } }); } return expose2; } // src/worker/worker.node.ts var parentPort = assertEx(optionalParentPort, () => "Invariant violation: MessagePort to parent is not available."); function assertMessagePort(port) { if (!port) { throw new Error("Invariant violation: MessagePort to parent is not available."); } return port; } var isWorkerRuntime = function isWorkerRuntime2() { return true; }; var postMessageToMaster = function postMessageToMaster2(data, transferList) { assertMessagePort(parentPort).postMessage(data, transferList); }; var subscribeToMasterMessages = function subscribeToMasterMessages2(onMessage) { if (!parentPort) { throw new Error("Invariant violation: MessagePort to parent is not available."); } const messageHandler = (message) => { onMessage(message); }; const unsubscribe = () => { assertMessagePort(parentPort).off("message", messageHandler); }; assertMessagePort(parentPort).on("message", messageHandler); return unsubscribe; }; var addEventListener = parentPort?.on.bind(parentPort); var postMessage = parentPort?.postMessage.bind(parentPort); var removeEventListener = parentPort?.off.bind(parentPort); var expose = createExpose({ isWorkerRuntime, postMessageToMaster, subscribeToMasterMessages }, { addEventListener, postMessage, removeEventListener }); export { Transfer, addEventListener, expose, isWorkerRuntime, postMessage, postMessageToMaster, registerSerializer, removeEventListener, subscribeToMasterMessages }; //# sourceMappingURL=worker.node.mjs.map