@xylabs/threads
Version:
Web workers & worker threads as simple as a function call
300 lines (293 loc) • 10.1 kB
JavaScript
// 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