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