From a00b452df7b9b0ad8d8402b7b5c507a2e77cf5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Tue, 5 Oct 2021 14:23:40 +0300 Subject: [PATCH 01/50] chore: Add a stub for shared worker code --- src/get-expose.ts | 273 +++++++++++++++++++++++++++ src/index.ts | 16 +- src/master/implementation.browser.ts | 1 + src/master/index.ts | 3 + src/shared-worker/bundle-entry.ts | 9 + src/shared-worker/implementation.ts | 50 +++++ src/shared-worker/index.ts | 12 ++ src/types/master.ts | 1 + src/worker/index.ts | 229 +--------------------- 9 files changed, 367 insertions(+), 227 deletions(-) create mode 100644 src/get-expose.ts create mode 100644 src/shared-worker/bundle-entry.ts create mode 100644 src/shared-worker/implementation.ts create mode 100644 src/shared-worker/index.ts diff --git a/src/get-expose.ts b/src/get-expose.ts new file mode 100644 index 00000000..c454978f --- /dev/null +++ b/src/get-expose.ts @@ -0,0 +1,273 @@ +import isSomeObservable from "is-observable"; +import { Observable, Subscription } from "observable-fns"; +import { deserialize, serialize } from "./common"; +import { isTransferDescriptor, TransferDescriptor } from "./transferable"; +import { + MasterJobCancelMessage, + MasterJobRunMessage, + MasterMessageType, + SerializedError, + WorkerInitMessage, + WorkerJobErrorMessage, + WorkerJobResultMessage, + WorkerJobStartMessage, + WorkerMessageType, + WorkerUncaughtErrorMessage, +} from "./types/messages"; +import { + AbstractedWorkerAPI, + WorkerFunction, + WorkerModule, +} from "./types/worker"; + +function getExpose(Implementation: AbstractedWorkerAPI) { + let exposeCalled = false; + + const activeSubscriptions = new Map>(); + + const isMasterJobCancelMessage = ( + thing: any + ): thing is MasterJobCancelMessage => + thing && thing.type === MasterMessageType.cancel; + const isMasterJobRunMessage = (thing: any): thing is MasterJobRunMessage => + thing && thing.type === MasterMessageType.run; + + /** + * There are issues with `is-observable` not recognizing zen-observable's instances. + * We are using `observable-fns`, but it's based on zen-observable, too. + */ + const isObservable = (thing: any): thing is Observable => + isSomeObservable(thing) || isZenObservable(thing); + + function isZenObservable(thing: any): thing is Observable { + return ( + thing && + typeof thing === "object" && + typeof thing.subscribe === "function" + ); + } + + function deconstructTransfer(thing: any) { + return isTransferDescriptor(thing) + ? { payload: thing.send, transferables: thing.transferables } + : { payload: thing, transferables: undefined }; + } + + function postFunctionInitMessage() { + const initMessage: WorkerInitMessage = { + type: WorkerMessageType.init, + exposed: { + type: "function", + }, + }; + Implementation.postMessageToMaster(initMessage); + } + + function postModuleInitMessage(methodNames: string[]) { + const initMessage: WorkerInitMessage = { + type: WorkerMessageType.init, + exposed: { + type: "module", + methods: methodNames, + }, + }; + Implementation.postMessageToMaster(initMessage); + } + + function postJobErrorMessage( + uid: number, + rawError: Error | TransferDescriptor + ) { + const { payload: error, transferables } = deconstructTransfer(rawError); + const errorMessage: WorkerJobErrorMessage = { + type: WorkerMessageType.error, + uid, + error: serialize(error) as any as SerializedError, + }; + Implementation.postMessageToMaster(errorMessage, transferables); + } + + function postJobResultMessage( + uid: number, + completed: boolean, + resultValue?: any + ) { + const { payload, transferables } = deconstructTransfer(resultValue); + const resultMessage: WorkerJobResultMessage = { + type: WorkerMessageType.result, + uid, + complete: completed ? true : undefined, + payload, + }; + Implementation.postMessageToMaster(resultMessage, transferables); + } + + function postJobStartMessage( + uid: number, + resultType: WorkerJobStartMessage["resultType"] + ) { + const startMessage: WorkerJobStartMessage = { + type: WorkerMessageType.running, + uid, + resultType, + }; + Implementation.postMessageToMaster(startMessage); + } + + function postUncaughtErrorMessage(error: Error) { + try { + const errorMessage: WorkerUncaughtErrorMessage = { + type: WorkerMessageType.uncaughtError, + error: serialize(error) as any as SerializedError, + }; + Implementation.postMessageToMaster(errorMessage); + } catch (subError) { + // tslint:disable-next-line no-console + 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: number, fn: WorkerFunction, args: any[]) { + let syncResult: any; + + try { + syncResult = fn(...args); + } catch (error) { + 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) as any); + 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) as any); + } + } + } + + /** + * Expose a function or a module (an object whose values are functions) + * to the main thread. Must be called exactly once in every worker thread + * to signal its API to the main thread. + * + * @param exposed Function or object whose values are functions + */ + function expose(exposed: WorkerFunction | WorkerModule) { + if (!Implementation.isWorkerRuntime()) { + throw Error("expose() called in the master thread."); + } + if (exposeCalled) { + throw 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 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 self !== "undefined" && + typeof self.addEventListener === "function" && + Implementation.isWorkerRuntime() + ) { + self.addEventListener("error", (event) => { + // Post with some delay, so the master had some time to subscribe to messages + setTimeout(() => postUncaughtErrorMessage(event.error || event), 250); + }); + self.addEventListener("unhandledrejection", (event) => { + const error = (event as any).reason; + if (error && typeof (error as any).message === "string") { + // Post with some delay, so the master had some time to subscribe to messages + setTimeout(() => postUncaughtErrorMessage(error), 250); + } + }); + } + + if ( + typeof process !== "undefined" && + typeof process.on === "function" && + Implementation.isWorkerRuntime() + ) { + process.on("uncaughtException", (error) => { + // Post with some delay, so the master had some time to subscribe to messages + setTimeout(() => postUncaughtErrorMessage(error), 250); + }); + process.on("unhandledRejection", (error) => { + if (error && typeof (error as any).message === "string") { + // Post with some delay, so the master had some time to subscribe to messages + setTimeout(() => postUncaughtErrorMessage(error as any), 250); + } + }); + } + + return expose; +} + +export default getExpose; diff --git a/src/index.ts b/src/index.ts index 8daf5286..e061a362 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,11 @@ -export { registerSerializer } from "./common" -export * from "./master/index" -export { expose } from "./worker/index" -export { DefaultSerializer, JsonSerializable, Serializer, SerializerImplementation } from "./serializers" -export { Transfer, TransferDescriptor } from "./transferable" +export { registerSerializer } from "./common"; +export * from "./master/index"; +export { expose } from "./worker/index"; +export { expose as exposeShared } from "./shared-worker/index"; +export { + DefaultSerializer, + JsonSerializable, + Serializer, + SerializerImplementation, +} from "./serializers"; +export { Transfer, TransferDescriptor } from "./transferable"; diff --git a/src/master/implementation.browser.ts b/src/master/implementation.browser.ts index 87846b97..c8c58134 100644 --- a/src/master/implementation.browser.ts +++ b/src/master/implementation.browser.ts @@ -63,6 +63,7 @@ function selectWorkerImplementation(): ImplementationExport { return { blob: BlobWorker, + shared: SharedWorker, default: WebWorker } } diff --git a/src/master/index.ts b/src/master/index.ts index ed1b2da1..82034da2 100644 --- a/src/master/index.ts +++ b/src/master/index.ts @@ -17,3 +17,6 @@ export const BlobWorker = getWorkerImplementation().blob /** Worker implementation. Either web worker or a node.js Worker class. */ export const Worker = getWorkerImplementation().default + +/** Shared Worker implementation. Available only in the web. */ +export const SharedWorker = getWorkerImplementation().shared diff --git a/src/shared-worker/bundle-entry.ts b/src/shared-worker/bundle-entry.ts new file mode 100644 index 00000000..1cbb6ed6 --- /dev/null +++ b/src/shared-worker/bundle-entry.ts @@ -0,0 +1,9 @@ +import { expose } from "./index" +export * from "./index" + +if (typeof global !== "undefined") { + (global as any).expose = expose +} +if (typeof self !== "undefined") { + (self as any).expose = expose +} diff --git a/src/shared-worker/implementation.ts b/src/shared-worker/implementation.ts new file mode 100644 index 00000000..ea669655 --- /dev/null +++ b/src/shared-worker/implementation.ts @@ -0,0 +1,50 @@ +/// +// tslint:disable no-shadowed-variable + +// TODO: Adapt this module to use a shared worker +import { AbstractedWorkerAPI } from "../types/worker"; + +interface WorkerGlobalScope { + addEventListener(eventName: string, listener: (event: Event) => void): void; + postMessage(message: any, transferables?: any[]): void; + removeEventListener( + eventName: string, + listener: (event: Event) => void + ): void; +} + +declare const self: WorkerGlobalScope; + +const isWorkerRuntime: AbstractedWorkerAPI["isWorkerRuntime"] = + function isWorkerRuntime() { + const isWindowContext = + typeof self !== "undefined" && + typeof Window !== "undefined" && + self instanceof Window; + return typeof self !== "undefined" && self.postMessage && !isWindowContext + ? true + : false; + }; + +const postMessageToMaster: AbstractedWorkerAPI["postMessageToMaster"] = + function postMessageToMaster(data, transferList?) { + self.postMessage(data, transferList); + }; + +const subscribeToMasterMessages: AbstractedWorkerAPI["subscribeToMasterMessages"] = + function subscribeToMasterMessages(onMessage) { + const messageHandler = (messageEvent: MessageEvent) => { + onMessage(messageEvent.data); + }; + const unsubscribe = () => { + self.removeEventListener("message", messageHandler as EventListener); + }; + self.addEventListener("message", messageHandler as EventListener); + return unsubscribe; + }; + +export default { + isWorkerRuntime, + postMessageToMaster, + subscribeToMasterMessages, +}; diff --git a/src/shared-worker/index.ts b/src/shared-worker/index.ts new file mode 100644 index 00000000..99e4fd75 --- /dev/null +++ b/src/shared-worker/index.ts @@ -0,0 +1,12 @@ +import getExpose from "../get-expose"; +import Implementation from "./implementation"; + +export { registerSerializer } from "../common"; +export { Transfer } from "../transferable"; + +/** Returns `true` if this code is currently running in a worker. */ +export const isWorkerRuntime = Implementation.isWorkerRuntime; + +const expose = getExpose(Implementation); + +export { expose }; diff --git a/src/types/master.ts b/src/types/master.ts index 604e7fa9..03f77870 100644 --- a/src/types/master.ts +++ b/src/types/master.ts @@ -113,6 +113,7 @@ export declare class BlobWorker extends WorkerImplementation { export interface ImplementationExport { blob: typeof BlobWorker + shared: typeof SharedWorker default: typeof WorkerImplementation } diff --git a/src/worker/index.ts b/src/worker/index.ts index 79bbf0be..99e4fd75 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -1,227 +1,12 @@ -import isSomeObservable from "is-observable" -import { Observable, Subscription } from "observable-fns" -import { deserialize, serialize } from "../common" -import { isTransferDescriptor, TransferDescriptor } from "../transferable" -import { - MasterJobCancelMessage, - MasterJobRunMessage, - MasterMessageType, - SerializedError, - WorkerInitMessage, - WorkerJobErrorMessage, - WorkerJobResultMessage, - WorkerJobStartMessage, - WorkerMessageType, - WorkerUncaughtErrorMessage -} from "../types/messages" -import { WorkerFunction, WorkerModule } from "../types/worker" -import Implementation from "./implementation" +import getExpose from "../get-expose"; +import Implementation from "./implementation"; -export { registerSerializer } from "../common" -export { Transfer } from "../transferable" +export { registerSerializer } from "../common"; +export { Transfer } from "../transferable"; /** Returns `true` if this code is currently running in a worker. */ -export const isWorkerRuntime = Implementation.isWorkerRuntime +export const isWorkerRuntime = Implementation.isWorkerRuntime; -let exposeCalled = false +const expose = getExpose(Implementation); -const activeSubscriptions = new Map>() - -const isMasterJobCancelMessage = (thing: any): thing is MasterJobCancelMessage => thing && thing.type === MasterMessageType.cancel -const isMasterJobRunMessage = (thing: any): thing is MasterJobRunMessage => thing && thing.type === MasterMessageType.run - -/** - * There are issues with `is-observable` not recognizing zen-observable's instances. - * We are using `observable-fns`, but it's based on zen-observable, too. - */ -const isObservable = (thing: any): thing is Observable => isSomeObservable(thing) || isZenObservable(thing) - -function isZenObservable(thing: any): thing is Observable { - return thing && typeof thing === "object" && typeof thing.subscribe === "function" -} - -function deconstructTransfer(thing: any) { - return isTransferDescriptor(thing) - ? { payload: thing.send, transferables: thing.transferables } - : { payload: thing, transferables: undefined } -} - -function postFunctionInitMessage() { - const initMessage: WorkerInitMessage = { - type: WorkerMessageType.init, - exposed: { - type: "function" - } - } - Implementation.postMessageToMaster(initMessage) -} - -function postModuleInitMessage(methodNames: string[]) { - const initMessage: WorkerInitMessage = { - type: WorkerMessageType.init, - exposed: { - type: "module", - methods: methodNames - } - } - Implementation.postMessageToMaster(initMessage) -} - -function postJobErrorMessage(uid: number, rawError: Error | TransferDescriptor) { - const { payload: error, transferables } = deconstructTransfer(rawError) - const errorMessage: WorkerJobErrorMessage = { - type: WorkerMessageType.error, - uid, - error: serialize(error) as any as SerializedError - } - Implementation.postMessageToMaster(errorMessage, transferables) -} - -function postJobResultMessage(uid: number, completed: boolean, resultValue?: any) { - const { payload, transferables } = deconstructTransfer(resultValue) - const resultMessage: WorkerJobResultMessage = { - type: WorkerMessageType.result, - uid, - complete: completed ? true : undefined, - payload - } - Implementation.postMessageToMaster(resultMessage, transferables) -} - -function postJobStartMessage(uid: number, resultType: WorkerJobStartMessage["resultType"]) { - const startMessage: WorkerJobStartMessage = { - type: WorkerMessageType.running, - uid, - resultType - } - Implementation.postMessageToMaster(startMessage) -} - -function postUncaughtErrorMessage(error: Error) { - try { - const errorMessage: WorkerUncaughtErrorMessage = { - type: WorkerMessageType.uncaughtError, - error: serialize(error) as any as SerializedError - } - Implementation.postMessageToMaster(errorMessage) - } catch (subError) { - // tslint:disable-next-line no-console - 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: number, fn: WorkerFunction, args: any[]) { - let syncResult: any - - try { - syncResult = fn(...args) - } catch (error) { - 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) as any) - 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) as any) - } - } -} - -/** - * Expose a function or a module (an object whose values are functions) - * to the main thread. Must be called exactly once in every worker thread - * to signal its API to the main thread. - * - * @param exposed Function or object whose values are functions - */ -export function expose(exposed: WorkerFunction | WorkerModule) { - if (!Implementation.isWorkerRuntime()) { - throw Error("expose() called in the master thread.") - } - if (exposeCalled) { - throw 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 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 self !== "undefined" && typeof self.addEventListener === "function" && Implementation.isWorkerRuntime()) { - self.addEventListener("error", event => { - // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(event.error || event), 250) - }) - self.addEventListener("unhandledrejection", event => { - const error = (event as any).reason - if (error && typeof (error as any).message === "string") { - // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(error), 250) - } - }) -} - -if (typeof process !== "undefined" && typeof process.on === "function" && Implementation.isWorkerRuntime()) { - process.on("uncaughtException", (error) => { - // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(error), 250) - }) - process.on("unhandledRejection", (error) => { - if (error && typeof (error as any).message === "string") { - // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(error as any), 250) - } - }) -} +export { expose }; From 9dea04c9a6c52b53d7068f32309e02aaa23db39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Tue, 5 Oct 2021 14:31:28 +0300 Subject: [PATCH 02/50] chore: Refine shared worker implementation --- src/shared-worker/implementation.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/shared-worker/implementation.ts b/src/shared-worker/implementation.ts index ea669655..47e3d696 100644 --- a/src/shared-worker/implementation.ts +++ b/src/shared-worker/implementation.ts @@ -1,16 +1,19 @@ /// // tslint:disable no-shadowed-variable -// TODO: Adapt this module to use a shared worker import { AbstractedWorkerAPI } from "../types/worker"; +// https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker interface WorkerGlobalScope { - addEventListener(eventName: string, listener: (event: Event) => void): void; - postMessage(message: any, transferables?: any[]): void; - removeEventListener( - eventName: string, - listener: (event: Event) => void - ): void; + port: { + addEventListener(eventName: string, listener: (event: Event) => void): void; + removeEventListener( + eventName: string, + listener: (event: Event) => void + ): void; + postMessage(message: any, transferables?: any[]): void; + start(): void; + }; } declare const self: WorkerGlobalScope; @@ -21,14 +24,16 @@ const isWorkerRuntime: AbstractedWorkerAPI["isWorkerRuntime"] = typeof self !== "undefined" && typeof Window !== "undefined" && self instanceof Window; - return typeof self !== "undefined" && self.postMessage && !isWindowContext + return typeof self !== "undefined" && + self.port.postMessage && + !isWindowContext ? true : false; }; const postMessageToMaster: AbstractedWorkerAPI["postMessageToMaster"] = function postMessageToMaster(data, transferList?) { - self.postMessage(data, transferList); + self.port.postMessage(data, transferList); }; const subscribeToMasterMessages: AbstractedWorkerAPI["subscribeToMasterMessages"] = @@ -37,9 +42,10 @@ const subscribeToMasterMessages: AbstractedWorkerAPI["subscribeToMasterMessages" onMessage(messageEvent.data); }; const unsubscribe = () => { - self.removeEventListener("message", messageHandler as EventListener); + self.port.removeEventListener("message", messageHandler as EventListener); }; - self.addEventListener("message", messageHandler as EventListener); + self.port.addEventListener("message", messageHandler as EventListener); + self.port.start(); return unsubscribe; }; From 98555a8d76dace413e1213dcfc703ebe8e5ec50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Tue, 5 Oct 2021 14:37:37 +0300 Subject: [PATCH 03/50] chore: Add initial docs --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 047eb756..1b5b25a9 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ Uses web workers in the browser, `worker_threads` in node 12+ and [`tiny-worker` ### Features -* First-class support for **async functions** & **observables** -* Write code once, run it **on all platforms** -* Manage bulk task executions with **thread pools** -* Use **require()** and **import**/**export** in workers -* Works great with **webpack** +- First-class support for **async functions** & **observables** +- Write code once, run it **on all platforms** +- Manage bulk task executions with **thread pools** +- Use **require()** and **import**/**export** in workers +- Works great with **webpack** ### Version 0.x @@ -31,7 +31,7 @@ You can find the old version 0.12 of threads.js on the [`v0` branch](https://git npm install threads tiny-worker ``` -*You only need to install the `tiny-worker` package to support node.js < 12. It's an optional dependency and used as a fallback if `worker_threads` are not available.* +_You only need to install the `tiny-worker` package to support node.js < 12. It's an optional dependency and used as a fallback if `worker_threads` are not available._ ## Platform support @@ -147,26 +147,26 @@ Everything else should work out of the box. ```js // master.js -import { spawn, Thread, Worker } from "threads" +import { spawn, Thread, Worker } from "threads"; -const auth = await spawn(new Worker("./workers/auth")) -const hashed = await auth.hashPassword("Super secret password", "1234") +const auth = await spawn(new Worker("./workers/auth")); +const hashed = await auth.hashPassword("Super secret password", "1234"); -console.log("Hashed password:", hashed) +console.log("Hashed password:", hashed); -await Thread.terminate(auth) +await Thread.terminate(auth); ``` ```js // workers/auth.js -import sha256 from "js-sha256" -import { expose } from "threads/worker" +import sha256 from "js-sha256"; +import { expose } from "threads/worker"; expose({ hashPassword(password, salt) { - return sha256(password + salt) - } -}) + return sha256(password + salt); + }, +}); ``` ### spawn() @@ -181,6 +181,40 @@ Use `expose()` to make a function or an object containing methods callable from In case of exposing an object, `spawn()` will asynchronously return an object exposing all the object's functions. If you `expose()` a function, `spawn` will also return a callable function, not an object. +## Shared Workers + +[Shared Workers](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker) can be accessed from multiple browsing contexts (windows, iframes, other workers) making them useful for sharing tasks such as synchronization and avoiding redundant work. + +In threads.js, the functionality is exposed as follows: + +```js +// master.js +import { spawn, Thread, SharedWorker } from "threads"; + +const auth = await spawn(new SharedWorker("./workers/auth")); +const hashed = await auth.hashPassword("Super secret password", "1234"); + +console.log("Hashed password:", hashed); + +await Thread.terminate(auth); +``` + +```js +// workers/auth.js +import sha256 from "js-sha256"; +import { expose } from "threads/shared-worker"; + +exposeShared({ + hashPassword(password, salt) { + return sha256(password + salt); + }, +}); +``` + +As you might notice, compared to the original example, only the imports (`Worker` -> `SharedWorker` and `expose` path) have changed. + +Note that as the functionality makes sense only in the browser, it's available only there. Based on [caniuse](https://caniuse.com/sharedworkers), the functionality is widely supported [Safari being a notable exception](https://bugs.webkit.org/show_bug.cgi?id=149850). + ## Usage

From f22497251ad391fc87e862bfe46b514f42dd50b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Tue, 5 Oct 2021 14:51:34 +0300 Subject: [PATCH 04/50] test: Add initial tests --- package.json | 3 ++- test/shared-workers/hello.ts | 5 +++++ test/shared-workers/increment.ts | 8 ++++++++ test/shared.ts | 33 ++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 test/shared-workers/hello.ts create mode 100644 test/shared-workers/increment.ts create mode 100644 test/shared.ts diff --git a/package.json b/package.json index c4ef1233..90c9bf83 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,11 @@ "build:es": "tsc -p tsconfig-esm.json", "postbuild": "npm run bundle", "bundle": "rollup -c -f umd --file=bundle/worker.js --name=threads --silent -- dist-esm/worker/bundle-entry.js", - "test": "npm run test:library && npm run test:tooling && npm run test:puppeteer:basic && npm run test:puppeteer:webpack", + "test": "npm run test:library && npm run test:tooling && npm run test:puppeteer:basic && test:puppeteer:shared && npm run test:puppeteer:webpack", "test:library": "cross-env TS_NODE_FILES=true ava ./test/**/*.test.ts", "test:tooling": "cross-env TS_NODE_FILES=true ava ./test-tooling/**/*.test.ts", "test:puppeteer:basic": "puppet-run --plugin=mocha --bundle=./test/workers/:workers/ --serve=./bundle/worker.js:/worker.js ./test/*.chromium*.ts", + "test:puppeteer:shared": "puppet-run --plugin=mocha --bundle=./test/shared-workers/:workers/ --serve=./bundle/worker.js:/worker.js ./test/shared.ts", "test:puppeteer:webpack": "puppet-run --serve ./test-tooling/webpack/dist/app.web/0.worker.js --serve ./test-tooling/webpack/dist/app.web/1.worker.js --plugin=mocha ./test-tooling/webpack/webpack.chromium.mocha.ts", "posttest": "tslint --project .", "prepare": "npm run build" diff --git a/test/shared-workers/hello.ts b/test/shared-workers/hello.ts new file mode 100644 index 00000000..2df3ddc5 --- /dev/null +++ b/test/shared-workers/hello.ts @@ -0,0 +1,5 @@ +import { expose } from "../../src/shared-worker"; + +expose(function helloWorld() { + return "Hello World"; +}); diff --git a/test/shared-workers/increment.ts b/test/shared-workers/increment.ts new file mode 100644 index 00000000..4a7b19fa --- /dev/null +++ b/test/shared-workers/increment.ts @@ -0,0 +1,8 @@ +import { expose } from "../../src/shared-worker"; + +let counter = 0; + +expose(function increment(by: number = 1) { + counter += by; + return counter; +}); diff --git a/test/shared.ts b/test/shared.ts new file mode 100644 index 00000000..289f3514 --- /dev/null +++ b/test/shared.ts @@ -0,0 +1,33 @@ +/* + * This code here will be run in a headless Chromium browser using `puppet-run`. + * Check the package.json scripts `test:puppeteer:*`. + */ + +import { expect } from "chai"; +import { spawn, Thread } from "../"; + +// We need this as a work-around to make our threads Worker global, since +// the bundler would otherwise not recognize `new Worker()` as a web worker +// import "../src/master/register"; + +describe("threads in browser", function () { + it("can spawn and terminate a thread", async function () { + const helloWorld = await spawn<() => string>( + // @ts-ignore TODO: Figure out how to type this + new SharedWorker("./shared-workers/hello-world.js") + ); + expect(await helloWorld()).to.equal("Hello World"); + await Thread.terminate(helloWorld); + }); + + it("can call a function thread more than once", async function () { + const increment = await spawn<() => number>( + // @ts-ignore TODO: Figure out how to type this + new SharedWorker("./shared-workers/increment.js") + ); + expect(await increment()).to.equal(1); + expect(await increment()).to.equal(2); + expect(await increment()).to.equal(3); + await Thread.terminate(increment); + }); +}); From 2f5d1c08d4f33a6b37379fbbe99b8708972a7880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 6 Oct 2021 11:56:48 +0300 Subject: [PATCH 05/50] chore: Sketch out spawn for shared workers It looks like shared workers don't have a terminate method at all. The biggest open question is how to do a runtime check against worker. This is needed in order to adapt to the API. --- package-lock.json | 6 + package.json | 1 + src/master/invocation-proxy.ts | 150 ++++++++++------- src/master/spawn.ts | 244 +++++++++++++++++----------- src/shared-worker/implementation.ts | 18 +- src/types/master.ts | 157 ++++++++++-------- test/shared.ts | 2 - 7 files changed, 339 insertions(+), 239 deletions(-) diff --git a/package-lock.json b/package-lock.json index 767dfe93..e3ab46b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1400,6 +1400,12 @@ "@types/node": "*" } }, + "@types/sharedworker": { + "version": "0.0.54", + "resolved": "https://registry.npmjs.org/@types/sharedworker/-/sharedworker-0.0.54.tgz", + "integrity": "sha512-/559RfifJHLMRCVkUcLNwYqk6MnozbDJIwrssxRk2ICvB3mwWaDMjwPiawxlHpptZAQBNhin2h4R/YMTtoGvZQ==", + "dev": true + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", diff --git a/package.json b/package.json index 90c9bf83..649ceee8 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@types/execa": "^2.0.0", "@types/mocha": "^8.0.3", "@types/node": "^14.14.5", + "@types/sharedworker": "0.0.54", "@types/webpack": "^4.41.23", "ava": "^3.13.0", "chai": "^4.2.0", diff --git a/src/master/invocation-proxy.ts b/src/master/invocation-proxy.ts index 231eb1d2..56a4e173 100644 --- a/src/master/invocation-proxy.ts +++ b/src/master/invocation-proxy.ts @@ -5,17 +5,17 @@ * Keep in mind that this code can make or break the program's performance! Need to optimize more… */ -import DebugLogger from "debug" -import { multicast, Observable } from "observable-fns" -import { deserialize, serialize } from "../common" -import { ObservablePromise } from "../observable-promise" -import { isTransferDescriptor } from "../transferable" +import DebugLogger from "debug"; +import { multicast, Observable } from "observable-fns"; +import { deserialize, serialize } from "../common"; +import { ObservablePromise } from "../observable-promise"; +import { isTransferDescriptor } from "../transferable"; import { ModuleMethods, ModuleProxy, ProxyableFunction, - Worker as WorkerType -} from "../types/master" + Worker as TWorker, +} from "../types/master"; import { MasterJobCancelMessage, MasterJobRunMessage, @@ -23,130 +23,158 @@ import { WorkerJobErrorMessage, WorkerJobResultMessage, WorkerJobStartMessage, - WorkerMessageType -} from "../types/messages" + WorkerMessageType, +} from "../types/messages"; -const debugMessages = DebugLogger("threads:master:messages") +type WorkerType = SharedWorker | TWorker; -let nextJobUID = 1 +const debugMessages = DebugLogger("threads:master:messages"); -const dedupe = (array: T[]): T[] => Array.from(new Set(array)) +let nextJobUID = 1; -const isJobErrorMessage = (data: any): data is WorkerJobErrorMessage => data && data.type === WorkerMessageType.error -const isJobResultMessage = (data: any): data is WorkerJobResultMessage => data && data.type === WorkerMessageType.result -const isJobStartMessage = (data: any): data is WorkerJobStartMessage => data && data.type === WorkerMessageType.running +const dedupe = (array: T[]): T[] => Array.from(new Set(array)); -function createObservableForJob(worker: WorkerType, jobUID: number): Observable { - return new Observable(observer => { - let asyncType: "observable" | "promise" | undefined +const isJobErrorMessage = (data: any): data is WorkerJobErrorMessage => + data && data.type === WorkerMessageType.error; +const isJobResultMessage = (data: any): data is WorkerJobResultMessage => + data && data.type === WorkerMessageType.result; +const isJobStartMessage = (data: any): data is WorkerJobStartMessage => + data && data.type === WorkerMessageType.running; + +function createObservableForJob( + worker: WorkerType, + jobUID: number +): Observable { + return new Observable((observer) => { + let asyncType: "observable" | "promise" | undefined; const messageHandler = ((event: MessageEvent) => { - debugMessages("Message from worker:", event.data) - if (!event.data || event.data.uid !== jobUID) return + debugMessages("Message from worker:", event.data); + if (!event.data || event.data.uid !== jobUID) return; if (isJobStartMessage(event.data)) { - asyncType = event.data.resultType + asyncType = event.data.resultType; } else if (isJobResultMessage(event.data)) { if (asyncType === "promise") { if (typeof event.data.payload !== "undefined") { - observer.next(deserialize(event.data.payload)) + observer.next(deserialize(event.data.payload)); } - observer.complete() - worker.removeEventListener("message", messageHandler) + observer.complete(); + worker.removeEventListener("message", messageHandler); } else { if (event.data.payload) { - observer.next(deserialize(event.data.payload)) + observer.next(deserialize(event.data.payload)); } if (event.data.complete) { - observer.complete() - worker.removeEventListener("message", messageHandler) + observer.complete(); + worker.removeEventListener("message", messageHandler); } } } else if (isJobErrorMessage(event.data)) { - const error = deserialize(event.data.error as any) + const error = deserialize(event.data.error as any); if (asyncType === "promise" || !asyncType) { - observer.error(error) + observer.error(error); } else { - observer.error(error) + observer.error(error); } - worker.removeEventListener("message", messageHandler) + worker.removeEventListener("message", messageHandler); } - }) as EventListener + }) as EventListener; - worker.addEventListener("message", messageHandler) + worker.addEventListener("message", messageHandler); return () => { if (asyncType === "observable" || !asyncType) { const cancelMessage: MasterJobCancelMessage = { type: MasterMessageType.cancel, - uid: jobUID + uid: jobUID, + }; + + // TODO: How to check which type of worker we have? + if (worker.port) { + worker.port.postMessage(cancelMessage); + } else { + worker.postMessage(cancelMessage); } - worker.postMessage(cancelMessage) } - worker.removeEventListener("message", messageHandler) - } - }) + worker.removeEventListener("message", messageHandler); + }; + }); } -function prepareArguments(rawArgs: any[]): { args: any[], transferables: Transferable[] } { +function prepareArguments(rawArgs: any[]): { + args: any[]; + transferables: Transferable[]; +} { if (rawArgs.length === 0) { // Exit early if possible return { args: [], - transferables: [] - } + transferables: [], + }; } - const args: any[] = [] - const transferables: Transferable[] = [] + const args: any[] = []; + const transferables: Transferable[] = []; for (const arg of rawArgs) { if (isTransferDescriptor(arg)) { - args.push(serialize(arg.send)) - transferables.push(...arg.transferables) + args.push(serialize(arg.send)); + transferables.push(...arg.transferables); } else { - args.push(serialize(arg)) + args.push(serialize(arg)); } } return { args, - transferables: transferables.length === 0 ? transferables : dedupe(transferables) - } + transferables: + transferables.length === 0 ? transferables : dedupe(transferables), + }; } -export function createProxyFunction(worker: WorkerType, method?: string) { +export function createProxyFunction( + worker: WorkerType, + method?: string +) { return ((...rawArgs: Args) => { - const uid = nextJobUID++ - const { args, transferables } = prepareArguments(rawArgs) + const uid = nextJobUID++; + const { args, transferables } = prepareArguments(rawArgs); const runMessage: MasterJobRunMessage = { type: MasterMessageType.run, uid, method, - args - } + args, + }; - debugMessages("Sending command to run function to worker:", runMessage) + debugMessages("Sending command to run function to worker:", runMessage); try { - worker.postMessage(runMessage, transferables) + // TODO: How to check which type of worker we have? + if (worker.port) { + worker.port.postMessage(runMessage, transferables); + } else { + worker.postMessage(runMessage, transferables); + } } catch (error) { - return ObservablePromise.from(Promise.reject(error)) + return ObservablePromise.from(Promise.reject(error)); } - return ObservablePromise.from(multicast(createObservableForJob(worker, uid))) - }) as any as ProxyableFunction + return ObservablePromise.from( + multicast(createObservableForJob(worker, uid)) + ); + }) as any as ProxyableFunction; } export function createProxyModule( worker: WorkerType, methodNames: string[] ): ModuleProxy { - const proxy: any = {} + const proxy: any = {}; for (const methodName of methodNames) { - proxy[methodName] = createProxyFunction(worker, methodName) + proxy[methodName] = createProxyFunction(worker, methodName); } - return proxy + return proxy; } diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 2843bca0..3a73f948 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -1,134 +1,164 @@ -import DebugLogger from "debug" -import { Observable } from "observable-fns" -import { deserialize } from "../common" -import { createPromiseWithResolver } from "../promise" -import { $errors, $events, $terminate, $worker } from "../symbols" +import DebugLogger from "debug"; +import { Observable } from "observable-fns"; +import { deserialize } from "../common"; +import { createPromiseWithResolver } from "../promise"; +import { $errors, $events, $terminate, $worker } from "../symbols"; import { FunctionThread, ModuleThread, PrivateThreadProps, StripAsync, - Worker as WorkerType, + Worker as TWorker, WorkerEvent, WorkerEventType, WorkerInternalErrorEvent, WorkerMessageEvent, - WorkerTerminationEvent -} from "../types/master" -import { WorkerInitMessage, WorkerUncaughtErrorMessage } from "../types/messages" -import { WorkerFunction, WorkerModule } from "../types/worker" -import { createProxyFunction, createProxyModule } from "./invocation-proxy" + WorkerTerminationEvent, +} from "../types/master"; +import { + WorkerInitMessage, + WorkerUncaughtErrorMessage, +} from "../types/messages"; +import { WorkerFunction, WorkerModule } from "../types/worker"; +import { createProxyFunction, createProxyModule } from "./invocation-proxy"; + +type WorkerType = SharedWorker | TWorker; -type ArbitraryWorkerInterface = WorkerFunction & WorkerModule & { somekeythatisneverusedinproductioncode123: "magicmarker123" } -type ArbitraryThreadType = FunctionThread & ModuleThread +type ArbitraryWorkerInterface = WorkerFunction & + WorkerModule & { + somekeythatisneverusedinproductioncode123: "magicmarker123"; + }; +type ArbitraryThreadType = FunctionThread & ModuleThread; type ExposedToThreadType> = Exposed extends ArbitraryWorkerInterface - ? ArbitraryThreadType - : Exposed extends WorkerFunction - ? FunctionThread, StripAsync>> - : Exposed extends WorkerModule - ? ModuleThread - : never - - -const debugMessages = DebugLogger("threads:master:messages") -const debugSpawn = DebugLogger("threads:master:spawn") -const debugThreadUtils = DebugLogger("threads:master:thread-utils") - -const isInitMessage = (data: any): data is WorkerInitMessage => data && data.type === ("init" as const) -const isUncaughtErrorMessage = (data: any): data is WorkerUncaughtErrorMessage => data && data.type === ("uncaughtError" as const) - -const initMessageTimeout = typeof process !== "undefined" && process.env.THREADS_WORKER_INIT_TIMEOUT - ? Number.parseInt(process.env.THREADS_WORKER_INIT_TIMEOUT, 10) - : 10000 - -async function withTimeout(promise: Promise, timeoutInMs: number, errorMessage: string): Promise { - let timeoutHandle: any + ? ArbitraryThreadType + : Exposed extends WorkerFunction + ? FunctionThread, StripAsync>> + : Exposed extends WorkerModule + ? ModuleThread + : never; + +const debugMessages = DebugLogger("threads:master:messages"); +const debugSpawn = DebugLogger("threads:master:spawn"); +const debugThreadUtils = DebugLogger("threads:master:thread-utils"); + +const isInitMessage = (data: any): data is WorkerInitMessage => + data && data.type === ("init" as const); +const isUncaughtErrorMessage = ( + data: any +): data is WorkerUncaughtErrorMessage => + data && data.type === ("uncaughtError" as const); + +const initMessageTimeout = + typeof process !== "undefined" && process.env.THREADS_WORKER_INIT_TIMEOUT + ? Number.parseInt(process.env.THREADS_WORKER_INIT_TIMEOUT, 10) + : 10000; + +async function withTimeout( + promise: Promise, + timeoutInMs: number, + errorMessage: string +): Promise { + let timeoutHandle: any; const timeout = new Promise((resolve, reject) => { - timeoutHandle = setTimeout(() => reject(Error(errorMessage)), timeoutInMs) - }) - const result = await Promise.race([ - promise, - timeout - ]) - - clearTimeout(timeoutHandle) - return result + timeoutHandle = setTimeout(() => reject(Error(errorMessage)), timeoutInMs); + }); + const result = await Promise.race([promise, timeout]); + + clearTimeout(timeoutHandle); + return result; } function receiveInitMessage(worker: WorkerType): Promise { return new Promise((resolve, reject) => { const messageHandler = ((event: MessageEvent) => { - debugMessages("Message from worker before finishing initialization:", event.data) + debugMessages( + "Message from worker before finishing initialization:", + event.data + ); if (isInitMessage(event.data)) { - worker.removeEventListener("message", messageHandler) - resolve(event.data) + worker.removeEventListener("message", messageHandler); + resolve(event.data); } else if (isUncaughtErrorMessage(event.data)) { - worker.removeEventListener("message", messageHandler) - reject(deserialize(event.data.error)) + worker.removeEventListener("message", messageHandler); + reject(deserialize(event.data.error)); } - }) as EventListener - worker.addEventListener("message", messageHandler) - }) + }) as EventListener; + worker.addEventListener("message", messageHandler); + }); } -function createEventObservable(worker: WorkerType, workerTermination: Promise): Observable { - return new Observable(observer => { +function createEventObservable( + worker: WorkerType, + workerTermination: Promise +): Observable { + return new Observable((observer) => { const messageHandler = ((messageEvent: MessageEvent) => { const workerEvent: WorkerMessageEvent = { type: WorkerEventType.message, - data: messageEvent.data - } - observer.next(workerEvent) - }) as EventListener + data: messageEvent.data, + }; + observer.next(workerEvent); + }) as EventListener; const rejectionHandler = ((errorEvent: PromiseRejectionEvent) => { - debugThreadUtils("Unhandled promise rejection event in thread:", errorEvent) + debugThreadUtils( + "Unhandled promise rejection event in thread:", + errorEvent + ); const workerEvent: WorkerInternalErrorEvent = { type: WorkerEventType.internalError, - error: Error(errorEvent.reason) - } - observer.next(workerEvent) - }) as EventListener - worker.addEventListener("message", messageHandler) - worker.addEventListener("unhandledrejection", rejectionHandler) + error: Error(errorEvent.reason), + }; + observer.next(workerEvent); + }) as EventListener; + worker.addEventListener("message", messageHandler); + worker.addEventListener("unhandledrejection", rejectionHandler); workerTermination.then(() => { const terminationEvent: WorkerTerminationEvent = { - type: WorkerEventType.termination - } - worker.removeEventListener("message", messageHandler) - worker.removeEventListener("unhandledrejection", rejectionHandler) - observer.next(terminationEvent) - observer.complete() - }) - }) + type: WorkerEventType.termination, + }; + worker.removeEventListener("message", messageHandler); + worker.removeEventListener("unhandledrejection", rejectionHandler); + observer.next(terminationEvent); + observer.complete(); + }); + }); } -function createTerminator(worker: WorkerType): { termination: Promise, terminate: () => Promise } { - const [termination, resolver] = createPromiseWithResolver() +function createTerminator(worker: TWorker): { + termination: Promise; + terminate: () => Promise; +} { + const [termination, resolver] = createPromiseWithResolver(); const terminate = async () => { - debugThreadUtils("Terminating worker") + debugThreadUtils("Terminating worker"); // Newer versions of worker_threads workers return a promise - await worker.terminate() - resolver() - } - return { terminate, termination } + await worker.terminate(); + resolver(); + }; + return { terminate, termination }; } -function setPrivateThreadProps(raw: T, worker: WorkerType, workerEvents: Observable, terminate: () => Promise): T & PrivateThreadProps { +function setPrivateThreadProps( + raw: T, + worker: TWorker, + workerEvents: Observable, + terminate: () => Promise +): T & PrivateThreadProps { const workerErrors = workerEvents - .filter(event => event.type === WorkerEventType.internalError) - .map(errorEvent => (errorEvent as WorkerInternalErrorEvent).error) + .filter((event) => event.type === WorkerEventType.internalError) + .map((errorEvent) => (errorEvent as WorkerInternalErrorEvent).error); // tslint:disable-next-line prefer-object-spread return Object.assign(raw, { [$errors]: workerErrors, [$events]: workerEvents, [$terminate]: terminate, - [$worker]: worker - }) + [$worker]: worker, + }); } /** @@ -140,27 +170,47 @@ function setPrivateThreadProps(raw: T, worker: WorkerType, workerEvents: Obse * @param [options] * @param [options.timeout] Init message timeout. Default: 10000 or set by environment variable. */ -export async function spawn = ArbitraryWorkerInterface>( +export async function spawn< + Exposed extends WorkerFunction | WorkerModule = ArbitraryWorkerInterface +>( worker: WorkerType, options?: { timeout?: number } ): Promise> { - debugSpawn("Initializing new thread") + 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 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) + // TODO: Shared workers don't have terminate! + const { termination, terminate } = createTerminator(worker); + const events = createEventObservable(worker, termination); if (exposed.type === "function") { - const proxy = createProxyFunction(worker) - return setPrivateThreadProps(proxy, worker, events, terminate) as ExposedToThreadType + const proxy = createProxyFunction(worker); + return setPrivateThreadProps( + proxy, + worker, + events, + terminate + ) as ExposedToThreadType; } else if (exposed.type === "module") { - const proxy = createProxyModule(worker, exposed.methods) - return setPrivateThreadProps(proxy, worker, events, terminate) as ExposedToThreadType + const proxy = createProxyModule(worker, exposed.methods); + return setPrivateThreadProps( + proxy, + worker, + events, + terminate + ) as ExposedToThreadType; } else { - const type = (exposed as WorkerInitMessage["exposed"]).type - throw Error(`Worker init message states unexpected type of expose(): ${type}`) + const type = (exposed as WorkerInitMessage["exposed"]).type; + throw Error( + `Worker init message states unexpected type of expose(): ${type}` + ); } } diff --git a/src/shared-worker/implementation.ts b/src/shared-worker/implementation.ts index 47e3d696..1871d267 100644 --- a/src/shared-worker/implementation.ts +++ b/src/shared-worker/implementation.ts @@ -3,20 +3,7 @@ import { AbstractedWorkerAPI } from "../types/worker"; -// https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker -interface WorkerGlobalScope { - port: { - addEventListener(eventName: string, listener: (event: Event) => void): void; - removeEventListener( - eventName: string, - listener: (event: Event) => void - ): void; - postMessage(message: any, transferables?: any[]): void; - start(): void; - }; -} - -declare const self: WorkerGlobalScope; +declare const self: SharedWorker; const isWorkerRuntime: AbstractedWorkerAPI["isWorkerRuntime"] = function isWorkerRuntime() { @@ -33,7 +20,8 @@ const isWorkerRuntime: AbstractedWorkerAPI["isWorkerRuntime"] = const postMessageToMaster: AbstractedWorkerAPI["postMessageToMaster"] = function postMessageToMaster(data, transferList?) { - self.port.postMessage(data, transferList); + // TODO: Check if this cast is true for shared workers + self.port.postMessage(data, transferList as PostMessageOptions); }; const subscribeToMasterMessages: AbstractedWorkerAPI["subscribeToMasterMessages"] = diff --git a/src/types/master.ts b/src/types/master.ts index 03f77870..cc0a5381 100644 --- a/src/types/master.ts +++ b/src/types/master.ts @@ -3,64 +3,82 @@ // Cannot use `compilerOptions.esModuleInterop` and default import syntax // See -import { Observable } from "observable-fns" -import { ObservablePromise } from "../observable-promise" -import { $errors, $events, $terminate, $worker } from "../symbols" -import { TransferDescriptor } from "../transferable" +import { Observable } from "observable-fns"; +import { ObservablePromise } from "../observable-promise"; +import { $errors, $events, $terminate, $worker } from "../symbols"; +import { TransferDescriptor } from "../transferable"; interface ObservableLikeSubscription { - unsubscribe(): any + unsubscribe(): any; } interface ObservableLike { - subscribe(onNext: (value: T) => any, onError?: (error: any) => any, onComplete?: () => any): ObservableLikeSubscription + subscribe( + onNext: (value: T) => any, + onError?: (error: any) => any, + onComplete?: () => any + ): ObservableLikeSubscription; subscribe(listeners: { - next?(value: T): any, - error?(error: any): any, - complete?(): any, - }): ObservableLikeSubscription + next?(value: T): any; + error?(error: any): any; + complete?(): any; + }): ObservableLikeSubscription; } -export type StripAsync = - Type extends Promise +export type StripAsync = Type extends Promise ? PromiseBaseType : Type extends ObservableLike ? ObservableBaseType - : Type + : Type; -export type StripTransfer = - Type extends TransferDescriptor +export type StripTransfer = Type extends TransferDescriptor< + infer BaseType +> ? BaseType - : Type - -export type ModuleMethods = { [methodName: string]: (...args: any) => any } - -export type ProxyableArgs = Args extends [arg0: infer Arg0, ...rest: infer RestArgs] - ? [Arg0 extends Transferable ? Arg0 | TransferDescriptor : Arg0, ...RestArgs] - : Args - -export type ProxyableFunction = - Args extends [] - ? () => ObservablePromise>> - : (...args: ProxyableArgs) => ObservablePromise>> + : Type; + +export type ModuleMethods = { [methodName: string]: (...args: any) => any }; + +export type ProxyableArgs = Args extends [ + arg0: infer Arg0, + ...rest: infer RestArgs +] + ? [ + Arg0 extends Transferable ? Arg0 | TransferDescriptor : Arg0, + ...RestArgs + ] + : Args; + +export type ProxyableFunction = Args extends [] + ? () => ObservablePromise>> + : ( + ...args: ProxyableArgs + ) => ObservablePromise>>; export type ModuleProxy = { - [method in keyof Methods]: ProxyableFunction, ReturnType> -} + [method in keyof Methods]: ProxyableFunction< + Parameters, + ReturnType + >; +}; export interface PrivateThreadProps { - [$errors]: Observable - [$events]: Observable - [$terminate]: () => Promise - [$worker]: Worker + [$errors]: Observable; + [$events]: Observable; + [$terminate]: () => Promise; + [$worker]: Worker; } -export type FunctionThread = ProxyableFunction & PrivateThreadProps -export type ModuleThread = ModuleProxy & PrivateThreadProps +export type FunctionThread< + Args extends any[] = any[], + ReturnType = any +> = ProxyableFunction & PrivateThreadProps; +export type ModuleThread = + ModuleProxy & PrivateThreadProps; // We have those extra interfaces to keep the general non-specific `Thread` type // as an interface, so it's displayed concisely in any TypeScript compiler output. interface AnyFunctionThread extends PrivateThreadProps { - (...args: any[]): ObservablePromise + (...args: any[]): ObservablePromise; } // tslint:disable-next-line no-empty-interface @@ -69,73 +87,84 @@ interface AnyModuleThread extends PrivateThreadProps { } /** Worker thread. Either a `FunctionThread` or a `ModuleThread`. */ -export type Thread = AnyFunctionThread | AnyModuleThread +export type Thread = AnyFunctionThread | AnyModuleThread; -export type TransferList = Transferable[] +export type TransferList = Transferable[]; /** Worker instance. Either a web worker or a node.js Worker provided by `worker_threads` or `tiny-worker`. */ export interface Worker extends EventTarget { - postMessage(value: any, transferList?: TransferList): void - /** In nodejs 10+ return type is Promise while with tiny-worker and in browser return type is void */ - terminate(callback?: (error?: Error, exitCode?: number) => void): void | Promise + postMessage(value: any, transferList?: TransferList): void; + /** In nodejs 10+ return type is Promise while with tiny-worker and in browser return type is void */ + terminate( + callback?: (error?: Error, exitCode?: number) => void + ): void | Promise; } export interface ThreadsWorkerOptions extends WorkerOptions { /** Prefix for the path passed to the Worker constructor. Web worker only. */ - _baseURL?: string + _baseURL?: string; /** Resource limits passed on to Node worker_threads */ resourceLimits?: { /** The maximum size of the main heap in MB. */ - maxOldGenerationSizeMb?: number + maxOldGenerationSizeMb?: number; /** The maximum size of a heap space for recently created objects. */ - maxYoungGenerationSizeMb?: number + maxYoungGenerationSizeMb?: number; /** The size of a pre-allocated memory range used for generated code. */ - codeRangeSizeMb?: number - } + codeRangeSizeMb?: number; + }; /** Data passed on to node.js worker_threads. */ - workerData?: any + workerData?: any; /** Whether to apply CORS protection workaround. Defaults to true. */ - CORSWorkaround?: boolean + CORSWorkaround?: boolean; } /** Worker implementation. Either web worker or a node.js Worker class. */ -export declare class WorkerImplementation extends EventTarget implements Worker { - constructor(path: string, options?: ThreadsWorkerOptions) - public postMessage(value: any, transferList?: TransferList): void - public terminate(): void | Promise +export declare class WorkerImplementation + extends EventTarget + implements Worker +{ + constructor(path: string, options?: ThreadsWorkerOptions); + public postMessage(value: any, transferList?: TransferList): void; + public terminate(): void | Promise; } /** Class to spawn workers from a blob or source string. */ export declare class BlobWorker extends WorkerImplementation { - constructor(blob: Blob, options?: ThreadsWorkerOptions) - public static fromText(source: string, options?: ThreadsWorkerOptions): WorkerImplementation + constructor(blob: Blob, options?: ThreadsWorkerOptions); + public static fromText( + source: string, + options?: ThreadsWorkerOptions + ): WorkerImplementation; } export interface ImplementationExport { - blob: typeof BlobWorker - shared: typeof SharedWorker - default: typeof WorkerImplementation + blob: typeof BlobWorker; + shared: typeof SharedWorker; + default: typeof WorkerImplementation; } /** Event as emitted by worker thread. Subscribe to using `Thread.events(thread)`. */ export enum WorkerEventType { internalError = "internalError", message = "message", - termination = "termination" + termination = "termination", } export interface WorkerInternalErrorEvent { - type: WorkerEventType.internalError - error: Error + type: WorkerEventType.internalError; + error: Error; } export interface WorkerMessageEvent { - type: WorkerEventType.message, - data: Data + type: WorkerEventType.message; + data: Data; } export interface WorkerTerminationEvent { - type: WorkerEventType.termination + type: WorkerEventType.termination; } -export type WorkerEvent = WorkerInternalErrorEvent | WorkerMessageEvent | WorkerTerminationEvent +export type WorkerEvent = + | WorkerInternalErrorEvent + | WorkerMessageEvent + | WorkerTerminationEvent; diff --git a/test/shared.ts b/test/shared.ts index 289f3514..0ea596ec 100644 --- a/test/shared.ts +++ b/test/shared.ts @@ -13,7 +13,6 @@ import { spawn, Thread } from "../"; describe("threads in browser", function () { it("can spawn and terminate a thread", async function () { const helloWorld = await spawn<() => string>( - // @ts-ignore TODO: Figure out how to type this new SharedWorker("./shared-workers/hello-world.js") ); expect(await helloWorld()).to.equal("Hello World"); @@ -22,7 +21,6 @@ describe("threads in browser", function () { it("can call a function thread more than once", async function () { const increment = await spawn<() => number>( - // @ts-ignore TODO: Figure out how to type this new SharedWorker("./shared-workers/increment.js") ); expect(await increment()).to.equal(1); From 3e74bd8dce8fcee2de398fb1f20dec7faa38394d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 6 Oct 2021 12:08:12 +0300 Subject: [PATCH 06/50] chore: Refine type check --- src/master/invocation-proxy.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/master/invocation-proxy.ts b/src/master/invocation-proxy.ts index 56a4e173..994b286d 100644 --- a/src/master/invocation-proxy.ts +++ b/src/master/invocation-proxy.ts @@ -90,8 +90,7 @@ function createObservableForJob( uid: jobUID, }; - // TODO: How to check which type of worker we have? - if (worker.port) { + if (worker.constructor.name === "SharedWorker") { worker.port.postMessage(cancelMessage); } else { worker.postMessage(cancelMessage); @@ -150,8 +149,8 @@ export function createProxyFunction( debugMessages("Sending command to run function to worker:", runMessage); try { - // TODO: How to check which type of worker we have? - if (worker.port) { + // TODO: How to check which type of worker we have so that TS understands it? + if (worker.constructor.name === "SharedWorker") { worker.port.postMessage(runMessage, transferables); } else { worker.postMessage(runMessage, transferables); From e807f5be1fb53b8594d9aedf73645c0da777342b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 6 Oct 2021 12:09:21 +0300 Subject: [PATCH 07/50] fix: Use instanceof for shared worker checks --- src/master/invocation-proxy.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/master/invocation-proxy.ts b/src/master/invocation-proxy.ts index 994b286d..06b74285 100644 --- a/src/master/invocation-proxy.ts +++ b/src/master/invocation-proxy.ts @@ -90,7 +90,7 @@ function createObservableForJob( uid: jobUID, }; - if (worker.constructor.name === "SharedWorker") { + if (worker instanceof SharedWorker) { worker.port.postMessage(cancelMessage); } else { worker.postMessage(cancelMessage); @@ -149,8 +149,7 @@ export function createProxyFunction( debugMessages("Sending command to run function to worker:", runMessage); try { - // TODO: How to check which type of worker we have so that TS understands it? - if (worker.constructor.name === "SharedWorker") { + if (worker instanceof SharedWorker) { worker.port.postMessage(runMessage, transferables); } else { worker.postMessage(runMessage, transferables); From 9a0c7ab9821413a7dfdf17728bdfae204f8af494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 6 Oct 2021 12:11:11 +0300 Subject: [PATCH 08/50] fix: Start shared worker when events are added --- src/master/invocation-proxy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/master/invocation-proxy.ts b/src/master/invocation-proxy.ts index 06b74285..7dfd3ba1 100644 --- a/src/master/invocation-proxy.ts +++ b/src/master/invocation-proxy.ts @@ -83,6 +83,10 @@ function createObservableForJob( worker.addEventListener("message", messageHandler); + if (worker instanceof SharedWorker) { + worker.port.start(); + } + return () => { if (asyncType === "observable" || !asyncType) { const cancelMessage: MasterJobCancelMessage = { From 66d9e2dd67365f579a20decae7522dea8c4306e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 6 Oct 2021 12:13:28 +0300 Subject: [PATCH 09/50] chore: Add a todo --- src/master/spawn.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 3a73f948..0ce59296 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -166,7 +166,7 @@ function setPrivateThreadProps( * abstraction layer to provide the transparent API and verifies that * the worker has initialized successfully. * - * @param worker Instance of `Worker`. Either a web worker, `worker_threads` worker or `tiny-worker` worker. + * @param worker Instance of `Worker` or `SharedWorker`. Either a web worker, `worker_threads` worker or `tiny-worker` worker. * @param [options] * @param [options.timeout] Init message timeout. Default: 10000 or set by environment variable. */ @@ -187,7 +187,12 @@ export async function spawn< ); const exposed = initMessage.exposed; - // TODO: Shared workers don't have terminate! + if (worker instanceof SharedWorker) { + // @ts-ignore TODO: What to do in this case? Shared workers don't have + // terminate for example. + return Promise.resolve(worker); + } + const { termination, terminate } = createTerminator(worker); const events = createEventObservable(worker, termination); From dc8f1dfd2a2172bc30257e806a794eaffb25934b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 11 Oct 2021 17:35:46 +0300 Subject: [PATCH 10/50] fix: Make shared optional --- src/types/master.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/master.ts b/src/types/master.ts index cc0a5381..dc622806 100644 --- a/src/types/master.ts +++ b/src/types/master.ts @@ -139,7 +139,7 @@ export declare class BlobWorker extends WorkerImplementation { export interface ImplementationExport { blob: typeof BlobWorker; - shared: typeof SharedWorker; + shared?: typeof SharedWorker; default: typeof WorkerImplementation; } From c7421732e05d61c3cb992a989c82c27149b20d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 11 Oct 2021 19:17:41 +0300 Subject: [PATCH 11/50] fix: Add a check against possibly missing port --- src/shared-worker/implementation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared-worker/implementation.ts b/src/shared-worker/implementation.ts index 1871d267..3ff320e6 100644 --- a/src/shared-worker/implementation.ts +++ b/src/shared-worker/implementation.ts @@ -12,7 +12,7 @@ const isWorkerRuntime: AbstractedWorkerAPI["isWorkerRuntime"] = typeof Window !== "undefined" && self instanceof Window; return typeof self !== "undefined" && - self.port.postMessage && + self.port?.postMessage && !isWindowContext ? true : false; From bebb1af54e52e55f384300bcecd95c7e6929ee3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 11 Oct 2021 19:19:46 +0300 Subject: [PATCH 12/50] fix: Include shared worker to ts --- tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tsconfig.json b/tsconfig.json index a855de20..4577001d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,8 +13,10 @@ }, "include": [ "./src/index.ts", + "./src/get-expose.ts", "./src/observable.ts", "./src/master/*", + "./src/shared-worker/*", "./src/worker/*", "./types/tiny-worker.d.ts", "./types/is-observable.d.ts" From 5a9b2a92646a1a44ed44f17ebd16d7904bb6d63b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Tue, 12 Oct 2021 16:23:14 +0300 Subject: [PATCH 13/50] fix: Add a missing browser field --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 649ceee8..d960e94c 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "./dist/worker/implementation.js": "./dist/worker/implementation.browser.js", "./dist/worker/implementation.tiny-worker.js": false, "./dist/worker/implementation.worker_threads.js": false, + "./dist/shared-worker/implementation.js": "./dist/shared-worker/implementation.js", "./dist-esm/master/implementation.js": "./dist-esm/master/implementation.browser.js", "./dist-esm/master/implementation.node.js": false, "./dist-esm/worker/implementation.js": "./dist-esm/worker/implementation.browser.js", From aed0dda61027470f0dc769acb2ba280fa094c827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Tue, 12 Oct 2021 16:26:48 +0300 Subject: [PATCH 14/50] fix: Drop @types/sharedworker It looks like that eliminates type errors. I wonder why it's not needed. --- package-lock.json | 6 ------ package.json | 1 - 2 files changed, 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index e3ab46b6..767dfe93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1400,12 +1400,6 @@ "@types/node": "*" } }, - "@types/sharedworker": { - "version": "0.0.54", - "resolved": "https://registry.npmjs.org/@types/sharedworker/-/sharedworker-0.0.54.tgz", - "integrity": "sha512-/559RfifJHLMRCVkUcLNwYqk6MnozbDJIwrssxRk2ICvB3mwWaDMjwPiawxlHpptZAQBNhin2h4R/YMTtoGvZQ==", - "dev": true - }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", diff --git a/package.json b/package.json index d960e94c..b579c6db 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "@types/execa": "^2.0.0", "@types/mocha": "^8.0.3", "@types/node": "^14.14.5", - "@types/sharedworker": "0.0.54", "@types/webpack": "^4.41.23", "ava": "^3.13.0", "chai": "^4.2.0", From ad356b17fde473874f2f3c4626bcc4faaf4e0412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Tue, 12 Oct 2021 16:49:43 +0300 Subject: [PATCH 15/50] fix: Fix test paths --- package.json | 2 +- test/shared.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b579c6db..17668ac0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test:library": "cross-env TS_NODE_FILES=true ava ./test/**/*.test.ts", "test:tooling": "cross-env TS_NODE_FILES=true ava ./test-tooling/**/*.test.ts", "test:puppeteer:basic": "puppet-run --plugin=mocha --bundle=./test/workers/:workers/ --serve=./bundle/worker.js:/worker.js ./test/*.chromium*.ts", - "test:puppeteer:shared": "puppet-run --plugin=mocha --bundle=./test/shared-workers/:workers/ --serve=./bundle/worker.js:/worker.js ./test/shared.ts", + "test:puppeteer:shared": "puppet-run --plugin=mocha --bundle=./test/shared-workers/:shared-workers/ --serve=./bundle/worker.js:/worker.js ./test/shared.ts", "test:puppeteer:webpack": "puppet-run --serve ./test-tooling/webpack/dist/app.web/0.worker.js --serve ./test-tooling/webpack/dist/app.web/1.worker.js --plugin=mocha ./test-tooling/webpack/webpack.chromium.mocha.ts", "posttest": "tslint --project .", "prepare": "npm run build" diff --git a/test/shared.ts b/test/shared.ts index 0ea596ec..b447ee29 100644 --- a/test/shared.ts +++ b/test/shared.ts @@ -13,7 +13,7 @@ import { spawn, Thread } from "../"; describe("threads in browser", function () { it("can spawn and terminate a thread", async function () { const helloWorld = await spawn<() => string>( - new SharedWorker("./shared-workers/hello-world.js") + new SharedWorker("./shared-workers/hello.js") ); expect(await helloWorld()).to.equal("Hello World"); await Thread.terminate(helloWorld); From 6a70a09ac9d74ecd32e5b8e61cd9a3144c191457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Tue, 12 Oct 2021 16:53:32 +0300 Subject: [PATCH 16/50] chore: Add a todo --- test/shared.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/shared.ts b/test/shared.ts index b447ee29..97fb56d6 100644 --- a/test/shared.ts +++ b/test/shared.ts @@ -6,23 +6,23 @@ import { expect } from "chai"; import { spawn, Thread } from "../"; -// We need this as a work-around to make our threads Worker global, since -// the bundler would otherwise not recognize `new Worker()` as a web worker -// import "../src/master/register"; - describe("threads in browser", function () { it("can spawn and terminate a thread", async function () { - const helloWorld = await spawn<() => string>( - new SharedWorker("./shared-workers/hello.js") - ); + const sharedWorker = new SharedWorker("./shared-workers/hello.js"); + + // TODO: Why does not spawn complete for shared workers? + const helloWorld = await spawn<() => string>(sharedWorker); + + console.log("hello world fn", helloWorld); + expect(await helloWorld()).to.equal("Hello World"); await Thread.terminate(helloWorld); }); it("can call a function thread more than once", async function () { - const increment = await spawn<() => number>( - new SharedWorker("./shared-workers/increment.js") - ); + const sharedWorker = new SharedWorker("./shared-workers/increment.js"); + + const increment = await spawn<() => number>(sharedWorker); expect(await increment()).to.equal(1); expect(await increment()).to.equal(2); expect(await increment()).to.equal(3); From cf06ee9de20af44cc36c2d3877a4a2d08724a831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Tue, 12 Oct 2021 18:20:32 +0300 Subject: [PATCH 17/50] fix: Implement terminate for shared workers --- src/master/spawn.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 0ce59296..454a1d74 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -142,6 +142,19 @@ function createTerminator(worker: TWorker): { return { terminate, termination }; } +function createSharedWorkerTerminator(worker: SharedWorker): { + termination: Promise; + terminate: () => Promise; +} { + const [termination, resolver] = createPromiseWithResolver(); + const terminate = async () => { + debugThreadUtils("Terminating shared worker"); + await worker.port.close(); + resolver(); + }; + return { terminate, termination }; +} + function setPrivateThreadProps( raw: T, worker: TWorker, @@ -186,20 +199,26 @@ export async function spawn< `Timeout: Did not receive an init message from worker after ${timeout}ms. Make sure the worker calls expose().` ); const exposed = initMessage.exposed; + let termination, terminate; if (worker instanceof SharedWorker) { - // @ts-ignore TODO: What to do in this case? Shared workers don't have - // terminate for example. - return Promise.resolve(worker); + const o = createSharedWorkerTerminator(worker); + + termination = o.termination; + terminate = o.terminate; + } else { + const o = createTerminator(worker); + termination = o.termination; + terminate = o.terminate; } - const { termination, terminate } = createTerminator(worker); const events = createEventObservable(worker, termination); if (exposed.type === "function") { const proxy = createProxyFunction(worker); return setPrivateThreadProps( proxy, + // @ts-ignore TODO: How to handle this for shared workers? worker, events, terminate @@ -208,6 +227,7 @@ export async function spawn< const proxy = createProxyModule(worker, exposed.methods); return setPrivateThreadProps( proxy, + // @ts-ignore TODO: How to handle this for shared workers? worker, events, terminate From ebecb9e2012ff1caba44377498fd64500bb4108e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Tue, 12 Oct 2021 18:20:46 +0300 Subject: [PATCH 18/50] chore: Improve spacing --- src/master/spawn.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 454a1d74..ec9f0bc3 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -208,6 +208,7 @@ export async function spawn< terminate = o.terminate; } else { const o = createTerminator(worker); + termination = o.termination; terminate = o.terminate; } From e373b2047240f62d1e033b2678163f43b1b7acf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 18 Oct 2021 19:59:13 +0300 Subject: [PATCH 19/50] chore: Clean up formatting --- src/master/invocation-proxy.ts | 116 +++++++++++----------- src/master/spawn.ts | 172 ++++++++++++++++----------------- src/types/master.ts | 98 +++++++++---------- src/worker/index.ts | 14 +-- 4 files changed, 200 insertions(+), 200 deletions(-) diff --git a/src/master/invocation-proxy.ts b/src/master/invocation-proxy.ts index 7dfd3ba1..052c192a 100644 --- a/src/master/invocation-proxy.ts +++ b/src/master/invocation-proxy.ts @@ -5,17 +5,17 @@ * Keep in mind that this code can make or break the program's performance! Need to optimize more… */ -import DebugLogger from "debug"; -import { multicast, Observable } from "observable-fns"; -import { deserialize, serialize } from "../common"; -import { ObservablePromise } from "../observable-promise"; -import { isTransferDescriptor } from "../transferable"; +import DebugLogger from "debug" +import { multicast, Observable } from "observable-fns" +import { deserialize, serialize } from "../common" +import { ObservablePromise } from "../observable-promise" +import { isTransferDescriptor } from "../transferable" import { ModuleMethods, ModuleProxy, ProxyableFunction, Worker as TWorker, -} from "../types/master"; +} from "../types/master" import { MasterJobCancelMessage, MasterJobRunMessage, @@ -24,67 +24,67 @@ import { WorkerJobResultMessage, WorkerJobStartMessage, WorkerMessageType, -} from "../types/messages"; +} from "../types/messages" -type WorkerType = SharedWorker | TWorker; +type WorkerType = SharedWorker | TWorker -const debugMessages = DebugLogger("threads:master:messages"); +const debugMessages = DebugLogger("threads:master:messages") -let nextJobUID = 1; +let nextJobUID = 1 -const dedupe = (array: T[]): T[] => Array.from(new Set(array)); +const dedupe = (array: T[]): T[] => Array.from(new Set(array)) const isJobErrorMessage = (data: any): data is WorkerJobErrorMessage => - data && data.type === WorkerMessageType.error; + data && data.type === WorkerMessageType.error const isJobResultMessage = (data: any): data is WorkerJobResultMessage => - data && data.type === WorkerMessageType.result; + data && data.type === WorkerMessageType.result const isJobStartMessage = (data: any): data is WorkerJobStartMessage => - data && data.type === WorkerMessageType.running; + data && data.type === WorkerMessageType.running function createObservableForJob( worker: WorkerType, jobUID: number ): Observable { return new Observable((observer) => { - let asyncType: "observable" | "promise" | undefined; + let asyncType: "observable" | "promise" | undefined const messageHandler = ((event: MessageEvent) => { - debugMessages("Message from worker:", event.data); - if (!event.data || event.data.uid !== jobUID) return; + debugMessages("Message from worker:", event.data) + if (!event.data || event.data.uid !== jobUID) return if (isJobStartMessage(event.data)) { - asyncType = event.data.resultType; + asyncType = event.data.resultType } else if (isJobResultMessage(event.data)) { if (asyncType === "promise") { if (typeof event.data.payload !== "undefined") { - observer.next(deserialize(event.data.payload)); + observer.next(deserialize(event.data.payload)) } - observer.complete(); - worker.removeEventListener("message", messageHandler); + observer.complete() + worker.removeEventListener("message", messageHandler) } else { if (event.data.payload) { - observer.next(deserialize(event.data.payload)); + observer.next(deserialize(event.data.payload)) } if (event.data.complete) { - observer.complete(); - worker.removeEventListener("message", messageHandler); + observer.complete() + worker.removeEventListener("message", messageHandler) } } } else if (isJobErrorMessage(event.data)) { - const error = deserialize(event.data.error as any); + const error = deserialize(event.data.error as any) if (asyncType === "promise" || !asyncType) { - observer.error(error); + observer.error(error) } else { - observer.error(error); + observer.error(error) } - worker.removeEventListener("message", messageHandler); + worker.removeEventListener("message", messageHandler) } - }) as EventListener; + }) as EventListener - worker.addEventListener("message", messageHandler); + worker.addEventListener("message", messageHandler) if (worker instanceof SharedWorker) { - worker.port.start(); + worker.port.start() } return () => { @@ -92,40 +92,40 @@ function createObservableForJob( const cancelMessage: MasterJobCancelMessage = { type: MasterMessageType.cancel, uid: jobUID, - }; + } if (worker instanceof SharedWorker) { - worker.port.postMessage(cancelMessage); + worker.port.postMessage(cancelMessage) } else { - worker.postMessage(cancelMessage); + worker.postMessage(cancelMessage) } } - worker.removeEventListener("message", messageHandler); - }; - }); + worker.removeEventListener("message", messageHandler) + } + }) } function prepareArguments(rawArgs: any[]): { - args: any[]; - transferables: Transferable[]; + args: any[] + transferables: Transferable[] } { if (rawArgs.length === 0) { // Exit early if possible return { args: [], transferables: [], - }; + } } - const args: any[] = []; - const transferables: Transferable[] = []; + const args: any[] = [] + const transferables: Transferable[] = [] for (const arg of rawArgs) { if (isTransferDescriptor(arg)) { - args.push(serialize(arg.send)); - transferables.push(...arg.transferables); + args.push(serialize(arg.send)) + transferables.push(...arg.transferables) } else { - args.push(serialize(arg)); + args.push(serialize(arg)) } } @@ -133,7 +133,7 @@ function prepareArguments(rawArgs: any[]): { args, transferables: transferables.length === 0 ? transferables : dedupe(transferables), - }; + } } export function createProxyFunction( @@ -141,42 +141,42 @@ export function createProxyFunction( method?: string ) { return ((...rawArgs: Args) => { - const uid = nextJobUID++; - const { args, transferables } = prepareArguments(rawArgs); + const uid = nextJobUID++ + const { args, transferables } = prepareArguments(rawArgs) const runMessage: MasterJobRunMessage = { type: MasterMessageType.run, uid, method, args, - }; + } - debugMessages("Sending command to run function to worker:", runMessage); + debugMessages("Sending command to run function to worker:", runMessage) try { if (worker instanceof SharedWorker) { - worker.port.postMessage(runMessage, transferables); + worker.port.postMessage(runMessage, transferables) } else { - worker.postMessage(runMessage, transferables); + worker.postMessage(runMessage, transferables) } } catch (error) { - return ObservablePromise.from(Promise.reject(error)); + return ObservablePromise.from(Promise.reject(error)) } return ObservablePromise.from( multicast(createObservableForJob(worker, uid)) - ); - }) as any as ProxyableFunction; + ) + }) as any as ProxyableFunction } export function createProxyModule( worker: WorkerType, methodNames: string[] ): ModuleProxy { - const proxy: any = {}; + const proxy: any = {} for (const methodName of methodNames) { - proxy[methodName] = createProxyFunction(worker, methodName); + proxy[methodName] = createProxyFunction(worker, methodName) } - return proxy; + return proxy } diff --git a/src/master/spawn.ts b/src/master/spawn.ts index ec9f0bc3..ea4ac8b7 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -1,8 +1,8 @@ -import DebugLogger from "debug"; -import { Observable } from "observable-fns"; -import { deserialize } from "../common"; -import { createPromiseWithResolver } from "../promise"; -import { $errors, $events, $terminate, $worker } from "../symbols"; +import DebugLogger from "debug" +import { Observable } from "observable-fns" +import { deserialize } from "../common" +import { createPromiseWithResolver } from "../promise" +import { $errors, $events, $terminate, $worker } from "../symbols" import { FunctionThread, ModuleThread, @@ -14,21 +14,21 @@ import { WorkerInternalErrorEvent, WorkerMessageEvent, WorkerTerminationEvent, -} from "../types/master"; +} from "../types/master" import { WorkerInitMessage, WorkerUncaughtErrorMessage, -} from "../types/messages"; -import { WorkerFunction, WorkerModule } from "../types/worker"; -import { createProxyFunction, createProxyModule } from "./invocation-proxy"; +} from "../types/messages" +import { WorkerFunction, WorkerModule } from "../types/worker" +import { createProxyFunction, createProxyModule } from "./invocation-proxy" -type WorkerType = SharedWorker | TWorker; +type WorkerType = SharedWorker | TWorker type ArbitraryWorkerInterface = WorkerFunction & WorkerModule & { - somekeythatisneverusedinproductioncode123: "magicmarker123"; - }; -type ArbitraryThreadType = FunctionThread & ModuleThread; + somekeythatisneverusedinproductioncode123: "magicmarker123" + } +type ArbitraryThreadType = FunctionThread & ModuleThread type ExposedToThreadType> = Exposed extends ArbitraryWorkerInterface @@ -37,38 +37,38 @@ type ExposedToThreadType> = ? FunctionThread, StripAsync>> : Exposed extends WorkerModule ? ModuleThread - : never; + : never -const debugMessages = DebugLogger("threads:master:messages"); -const debugSpawn = DebugLogger("threads:master:spawn"); -const debugThreadUtils = DebugLogger("threads:master:thread-utils"); +const debugMessages = DebugLogger("threads:master:messages") +const debugSpawn = DebugLogger("threads:master:spawn") +const debugThreadUtils = DebugLogger("threads:master:thread-utils") const isInitMessage = (data: any): data is WorkerInitMessage => - data && data.type === ("init" as const); + data && data.type === ("init" as const) const isUncaughtErrorMessage = ( data: any ): data is WorkerUncaughtErrorMessage => - data && data.type === ("uncaughtError" as const); + data && data.type === ("uncaughtError" as const) const initMessageTimeout = typeof process !== "undefined" && process.env.THREADS_WORKER_INIT_TIMEOUT ? Number.parseInt(process.env.THREADS_WORKER_INIT_TIMEOUT, 10) - : 10000; + : 10000 async function withTimeout( promise: Promise, timeoutInMs: number, errorMessage: string ): Promise { - let timeoutHandle: any; + let timeoutHandle: any const timeout = new Promise((resolve, reject) => { - timeoutHandle = setTimeout(() => reject(Error(errorMessage)), timeoutInMs); - }); - const result = await Promise.race([promise, timeout]); + timeoutHandle = setTimeout(() => reject(Error(errorMessage)), timeoutInMs) + }) + const result = await Promise.race([promise, timeout]) - clearTimeout(timeoutHandle); - return result; + clearTimeout(timeoutHandle) + return result } function receiveInitMessage(worker: WorkerType): Promise { @@ -77,17 +77,17 @@ function receiveInitMessage(worker: WorkerType): Promise { debugMessages( "Message from worker before finishing initialization:", event.data - ); + ) if (isInitMessage(event.data)) { - worker.removeEventListener("message", messageHandler); - resolve(event.data); + worker.removeEventListener("message", messageHandler) + resolve(event.data) } else if (isUncaughtErrorMessage(event.data)) { - worker.removeEventListener("message", messageHandler); - reject(deserialize(event.data.error)); + worker.removeEventListener("message", messageHandler) + reject(deserialize(event.data.error)) } - }) as EventListener; - worker.addEventListener("message", messageHandler); - }); + }) as EventListener + worker.addEventListener("message", messageHandler) + }) } function createEventObservable( @@ -99,60 +99,60 @@ function createEventObservable( const workerEvent: WorkerMessageEvent = { type: WorkerEventType.message, data: messageEvent.data, - }; - observer.next(workerEvent); - }) as EventListener; + } + observer.next(workerEvent) + }) as EventListener const rejectionHandler = ((errorEvent: PromiseRejectionEvent) => { debugThreadUtils( "Unhandled promise rejection event in thread:", errorEvent - ); + ) const workerEvent: WorkerInternalErrorEvent = { type: WorkerEventType.internalError, error: Error(errorEvent.reason), - }; - observer.next(workerEvent); - }) as EventListener; - worker.addEventListener("message", messageHandler); - worker.addEventListener("unhandledrejection", rejectionHandler); + } + observer.next(workerEvent) + }) as EventListener + worker.addEventListener("message", messageHandler) + worker.addEventListener("unhandledrejection", rejectionHandler) workerTermination.then(() => { const terminationEvent: WorkerTerminationEvent = { type: WorkerEventType.termination, - }; - worker.removeEventListener("message", messageHandler); - worker.removeEventListener("unhandledrejection", rejectionHandler); - observer.next(terminationEvent); - observer.complete(); - }); - }); + } + worker.removeEventListener("message", messageHandler) + worker.removeEventListener("unhandledrejection", rejectionHandler) + observer.next(terminationEvent) + observer.complete() + }) + }) } function createTerminator(worker: TWorker): { - termination: Promise; - terminate: () => Promise; + termination: Promise + terminate: () => Promise } { - const [termination, resolver] = createPromiseWithResolver(); + const [termination, resolver] = createPromiseWithResolver() const terminate = async () => { - debugThreadUtils("Terminating worker"); + debugThreadUtils("Terminating worker") // Newer versions of worker_threads workers return a promise - await worker.terminate(); - resolver(); - }; - return { terminate, termination }; + await worker.terminate() + resolver() + } + return { terminate, termination } } function createSharedWorkerTerminator(worker: SharedWorker): { - termination: Promise; - terminate: () => Promise; + termination: Promise + terminate: () => Promise } { - const [termination, resolver] = createPromiseWithResolver(); + const [termination, resolver] = createPromiseWithResolver() const terminate = async () => { - debugThreadUtils("Terminating shared worker"); - await worker.port.close(); - resolver(); - }; - return { terminate, termination }; + debugThreadUtils("Terminating shared worker") + await worker.port.close() + resolver() + } + return { terminate, termination } } function setPrivateThreadProps( @@ -163,7 +163,7 @@ function setPrivateThreadProps( ): T & PrivateThreadProps { const workerErrors = workerEvents .filter((event) => event.type === WorkerEventType.internalError) - .map((errorEvent) => (errorEvent as WorkerInternalErrorEvent).error); + .map((errorEvent) => (errorEvent as WorkerInternalErrorEvent).error) // tslint:disable-next-line prefer-object-spread return Object.assign(raw, { @@ -171,7 +171,7 @@ function setPrivateThreadProps( [$events]: workerEvents, [$terminate]: terminate, [$worker]: worker, - }); + }) } /** @@ -189,54 +189,54 @@ export async function spawn< worker: WorkerType, options?: { timeout?: number } ): Promise> { - debugSpawn("Initializing new thread"); + debugSpawn("Initializing new thread") const timeout = - options && options.timeout ? options.timeout : initMessageTimeout; + 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; - let termination, terminate; + ) + const exposed = initMessage.exposed + let termination, terminate if (worker instanceof SharedWorker) { - const o = createSharedWorkerTerminator(worker); + const o = createSharedWorkerTerminator(worker) - termination = o.termination; - terminate = o.terminate; + termination = o.termination + terminate = o.terminate } else { - const o = createTerminator(worker); + const o = createTerminator(worker) - termination = o.termination; - terminate = o.terminate; + termination = o.termination + terminate = o.terminate } - const events = createEventObservable(worker, termination); + const events = createEventObservable(worker, termination) if (exposed.type === "function") { - const proxy = createProxyFunction(worker); + const proxy = createProxyFunction(worker) return setPrivateThreadProps( proxy, // @ts-ignore TODO: How to handle this for shared workers? worker, events, terminate - ) as ExposedToThreadType; + ) as ExposedToThreadType } else if (exposed.type === "module") { - const proxy = createProxyModule(worker, exposed.methods); + const proxy = createProxyModule(worker, exposed.methods) return setPrivateThreadProps( proxy, // @ts-ignore TODO: How to handle this for shared workers? worker, events, terminate - ) as ExposedToThreadType; + ) as ExposedToThreadType } else { - const type = (exposed as WorkerInitMessage["exposed"]).type; + const type = (exposed as WorkerInitMessage["exposed"]).type throw Error( `Worker init message states unexpected type of expose(): ${type}` - ); + ) } } diff --git a/src/types/master.ts b/src/types/master.ts index dc622806..c5da09ab 100644 --- a/src/types/master.ts +++ b/src/types/master.ts @@ -3,40 +3,40 @@ // Cannot use `compilerOptions.esModuleInterop` and default import syntax // See -import { Observable } from "observable-fns"; -import { ObservablePromise } from "../observable-promise"; -import { $errors, $events, $terminate, $worker } from "../symbols"; -import { TransferDescriptor } from "../transferable"; +import { Observable } from "observable-fns" +import { ObservablePromise } from "../observable-promise" +import { $errors, $events, $terminate, $worker } from "../symbols" +import { TransferDescriptor } from "../transferable" interface ObservableLikeSubscription { - unsubscribe(): any; + unsubscribe(): any } interface ObservableLike { subscribe( onNext: (value: T) => any, onError?: (error: any) => any, onComplete?: () => any - ): ObservableLikeSubscription; + ): ObservableLikeSubscription subscribe(listeners: { - next?(value: T): any; - error?(error: any): any; - complete?(): any; - }): ObservableLikeSubscription; + next?(value: T): any + error?(error: any): any + complete?(): any + }): ObservableLikeSubscription } export type StripAsync = Type extends Promise ? PromiseBaseType : Type extends ObservableLike ? ObservableBaseType - : Type; + : Type export type StripTransfer = Type extends TransferDescriptor< infer BaseType > ? BaseType - : Type; + : Type -export type ModuleMethods = { [methodName: string]: (...args: any) => any }; +export type ModuleMethods = { [methodName: string]: (...args: any) => any } export type ProxyableArgs = Args extends [ arg0: infer Arg0, @@ -46,39 +46,39 @@ export type ProxyableArgs = Args extends [ Arg0 extends Transferable ? Arg0 | TransferDescriptor : Arg0, ...RestArgs ] - : Args; + : Args export type ProxyableFunction = Args extends [] ? () => ObservablePromise>> : ( ...args: ProxyableArgs - ) => ObservablePromise>>; + ) => ObservablePromise>> export type ModuleProxy = { [method in keyof Methods]: ProxyableFunction< Parameters, ReturnType - >; -}; + > +} export interface PrivateThreadProps { - [$errors]: Observable; - [$events]: Observable; - [$terminate]: () => Promise; - [$worker]: Worker; + [$errors]: Observable + [$events]: Observable + [$terminate]: () => Promise + [$worker]: Worker } export type FunctionThread< Args extends any[] = any[], ReturnType = any -> = ProxyableFunction & PrivateThreadProps; +> = ProxyableFunction & PrivateThreadProps export type ModuleThread = - ModuleProxy & PrivateThreadProps; + ModuleProxy & PrivateThreadProps // We have those extra interfaces to keep the general non-specific `Thread` type // as an interface, so it's displayed concisely in any TypeScript compiler output. interface AnyFunctionThread extends PrivateThreadProps { - (...args: any[]): ObservablePromise; + (...args: any[]): ObservablePromise } // tslint:disable-next-line no-empty-interface @@ -87,35 +87,35 @@ interface AnyModuleThread extends PrivateThreadProps { } /** Worker thread. Either a `FunctionThread` or a `ModuleThread`. */ -export type Thread = AnyFunctionThread | AnyModuleThread; +export type Thread = AnyFunctionThread | AnyModuleThread -export type TransferList = Transferable[]; +export type TransferList = Transferable[] /** Worker instance. Either a web worker or a node.js Worker provided by `worker_threads` or `tiny-worker`. */ export interface Worker extends EventTarget { - postMessage(value: any, transferList?: TransferList): void; + postMessage(value: any, transferList?: TransferList): void /** In nodejs 10+ return type is Promise while with tiny-worker and in browser return type is void */ terminate( callback?: (error?: Error, exitCode?: number) => void - ): void | Promise; + ): void | Promise } export interface ThreadsWorkerOptions extends WorkerOptions { /** Prefix for the path passed to the Worker constructor. Web worker only. */ - _baseURL?: string; + _baseURL?: string /** Resource limits passed on to Node worker_threads */ resourceLimits?: { /** The maximum size of the main heap in MB. */ - maxOldGenerationSizeMb?: number; + maxOldGenerationSizeMb?: number /** The maximum size of a heap space for recently created objects. */ - maxYoungGenerationSizeMb?: number; + maxYoungGenerationSizeMb?: number /** The size of a pre-allocated memory range used for generated code. */ - codeRangeSizeMb?: number; - }; + codeRangeSizeMb?: number + } /** Data passed on to node.js worker_threads. */ - workerData?: any; + workerData?: any /** Whether to apply CORS protection workaround. Defaults to true. */ - CORSWorkaround?: boolean; + CORSWorkaround?: boolean } /** Worker implementation. Either web worker or a node.js Worker class. */ @@ -123,24 +123,24 @@ export declare class WorkerImplementation extends EventTarget implements Worker { - constructor(path: string, options?: ThreadsWorkerOptions); - public postMessage(value: any, transferList?: TransferList): void; - public terminate(): void | Promise; + constructor(path: string, options?: ThreadsWorkerOptions) + public postMessage(value: any, transferList?: TransferList): void + public terminate(): void | Promise } /** Class to spawn workers from a blob or source string. */ export declare class BlobWorker extends WorkerImplementation { - constructor(blob: Blob, options?: ThreadsWorkerOptions); + constructor(blob: Blob, options?: ThreadsWorkerOptions) public static fromText( source: string, options?: ThreadsWorkerOptions - ): WorkerImplementation; + ): WorkerImplementation } export interface ImplementationExport { - blob: typeof BlobWorker; - shared?: typeof SharedWorker; - default: typeof WorkerImplementation; + blob: typeof BlobWorker + shared?: typeof SharedWorker + default: typeof WorkerImplementation } /** Event as emitted by worker thread. Subscribe to using `Thread.events(thread)`. */ @@ -151,20 +151,20 @@ export enum WorkerEventType { } export interface WorkerInternalErrorEvent { - type: WorkerEventType.internalError; - error: Error; + type: WorkerEventType.internalError + error: Error } export interface WorkerMessageEvent { - type: WorkerEventType.message; - data: Data; + type: WorkerEventType.message + data: Data } export interface WorkerTerminationEvent { - type: WorkerEventType.termination; + type: WorkerEventType.termination } export type WorkerEvent = | WorkerInternalErrorEvent | WorkerMessageEvent - | WorkerTerminationEvent; + | WorkerTerminationEvent diff --git a/src/worker/index.ts b/src/worker/index.ts index 99e4fd75..30a6080c 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -1,12 +1,12 @@ -import getExpose from "../get-expose"; -import Implementation from "./implementation"; +import getExpose from "../get-expose" +import Implementation from "./implementation" -export { registerSerializer } from "../common"; -export { Transfer } from "../transferable"; +export { registerSerializer } from "../common" +export { Transfer } from "../transferable" /** Returns `true` if this code is currently running in a worker. */ -export const isWorkerRuntime = Implementation.isWorkerRuntime; +export const isWorkerRuntime = Implementation.isWorkerRuntime -const expose = getExpose(Implementation); +const expose = getExpose(Implementation) -export { expose }; +export { expose } From b661764085a7bccc9f75d4475918eecba32b7231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 18 Oct 2021 20:27:22 +0300 Subject: [PATCH 20/50] chore: Remove unnecessary formatting from readme --- README.md | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 1b5b25a9..0b134671 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ Uses web workers in the browser, `worker_threads` in node 12+ and [`tiny-worker` ### Features -- First-class support for **async functions** & **observables** -- Write code once, run it **on all platforms** -- Manage bulk task executions with **thread pools** -- Use **require()** and **import**/**export** in workers -- Works great with **webpack** +* First-class support for **async functions** & **observables** +* Write code once, run it **on all platforms** +* Manage bulk task executions with **thread pools** +* Use **require()** and **import**/**export** in workers +* Works great with **webpack** ### Version 0.x @@ -31,7 +31,7 @@ You can find the old version 0.12 of threads.js on the [`v0` branch](https://git npm install threads tiny-worker ``` -_You only need to install the `tiny-worker` package to support node.js < 12. It's an optional dependency and used as a fallback if `worker_threads` are not available._ +*You only need to install the `tiny-worker` package to support node.js < 12. It's an optional dependency and used as a fallback if `worker_threads` are not available.* ## Platform support @@ -62,7 +62,7 @@ Use with the [`threads-plugin`](https://github.com/andywer/threads-plugin). It w Then add it to your `webpack.config.js`: ```diff -+ const ThreadsPlugin = require('threads-plugin'); ++ const ThreadsPlugin = require('threads-plugin') module.exports = { // ... @@ -147,26 +147,26 @@ Everything else should work out of the box. ```js // master.js -import { spawn, Thread, Worker } from "threads"; +import { spawn, Thread, Worker } from "threads" -const auth = await spawn(new Worker("./workers/auth")); -const hashed = await auth.hashPassword("Super secret password", "1234"); +const auth = await spawn(new Worker("./workers/auth")) +const hashed = await auth.hashPassword("Super secret password", "1234") -console.log("Hashed password:", hashed); +console.log("Hashed password:", hashed) -await Thread.terminate(auth); +await Thread.terminate(auth) ``` ```js // workers/auth.js -import sha256 from "js-sha256"; -import { expose } from "threads/worker"; +import sha256 from "js-sha256" +import { expose } from "threads/worker" expose({ hashPassword(password, salt) { - return sha256(password + salt); + return sha256(password + salt) }, -}); +}) ``` ### spawn() @@ -189,26 +189,26 @@ In threads.js, the functionality is exposed as follows: ```js // master.js -import { spawn, Thread, SharedWorker } from "threads"; +import { spawn, Thread, SharedWorker } from "threads" -const auth = await spawn(new SharedWorker("./workers/auth")); -const hashed = await auth.hashPassword("Super secret password", "1234"); +const auth = await spawn(new SharedWorker("./workers/auth")) +const hashed = await auth.hashPassword("Super secret password", "1234") -console.log("Hashed password:", hashed); +console.log("Hashed password:", hashed) -await Thread.terminate(auth); +await Thread.terminate(auth) ``` ```js // workers/auth.js -import sha256 from "js-sha256"; -import { expose } from "threads/shared-worker"; +import sha256 from "js-sha256" +import { expose } from "threads/shared-worker" exposeShared({ hashPassword(password, salt) { - return sha256(password + salt); + return sha256(password + salt) }, -}); +}) ``` As you might notice, compared to the original example, only the imports (`Worker` -> `SharedWorker` and `expose` path) have changed. From 1c22039b55a524426a3580e5b6733ecd15aee85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 18 Oct 2021 20:27:52 +0300 Subject: [PATCH 21/50] chore: Drop ;'s --- src/get-expose.ts | 138 +++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/src/get-expose.ts b/src/get-expose.ts index c454978f..03cffc0b 100644 --- a/src/get-expose.ts +++ b/src/get-expose.ts @@ -1,7 +1,7 @@ -import isSomeObservable from "is-observable"; -import { Observable, Subscription } from "observable-fns"; -import { deserialize, serialize } from "./common"; -import { isTransferDescriptor, TransferDescriptor } from "./transferable"; +import isSomeObservable from "is-observable" +import { Observable, Subscription } from "observable-fns" +import { deserialize, serialize } from "./common" +import { isTransferDescriptor, TransferDescriptor } from "./transferable" import { MasterJobCancelMessage, MasterJobRunMessage, @@ -13,44 +13,44 @@ import { WorkerJobStartMessage, WorkerMessageType, WorkerUncaughtErrorMessage, -} from "./types/messages"; +} from "./types/messages" import { AbstractedWorkerAPI, WorkerFunction, WorkerModule, -} from "./types/worker"; +} from "./types/worker" function getExpose(Implementation: AbstractedWorkerAPI) { - let exposeCalled = false; + let exposeCalled = false - const activeSubscriptions = new Map>(); + const activeSubscriptions = new Map>() const isMasterJobCancelMessage = ( thing: any ): thing is MasterJobCancelMessage => - thing && thing.type === MasterMessageType.cancel; + thing && thing.type === MasterMessageType.cancel const isMasterJobRunMessage = (thing: any): thing is MasterJobRunMessage => - thing && thing.type === MasterMessageType.run; + thing && thing.type === MasterMessageType.run /** * There are issues with `is-observable` not recognizing zen-observable's instances. * We are using `observable-fns`, but it's based on zen-observable, too. */ const isObservable = (thing: any): thing is Observable => - isSomeObservable(thing) || isZenObservable(thing); + isSomeObservable(thing) || isZenObservable(thing) function isZenObservable(thing: any): thing is Observable { return ( thing && typeof thing === "object" && typeof thing.subscribe === "function" - ); + ) } function deconstructTransfer(thing: any) { return isTransferDescriptor(thing) ? { payload: thing.send, transferables: thing.transferables } - : { payload: thing, transferables: undefined }; + : { payload: thing, transferables: undefined } } function postFunctionInitMessage() { @@ -59,8 +59,8 @@ function getExpose(Implementation: AbstractedWorkerAPI) { exposed: { type: "function", }, - }; - Implementation.postMessageToMaster(initMessage); + } + Implementation.postMessageToMaster(initMessage) } function postModuleInitMessage(methodNames: string[]) { @@ -70,21 +70,21 @@ function getExpose(Implementation: AbstractedWorkerAPI) { type: "module", methods: methodNames, }, - }; - Implementation.postMessageToMaster(initMessage); + } + Implementation.postMessageToMaster(initMessage) } function postJobErrorMessage( uid: number, rawError: Error | TransferDescriptor ) { - const { payload: error, transferables } = deconstructTransfer(rawError); + const { payload: error, transferables } = deconstructTransfer(rawError) const errorMessage: WorkerJobErrorMessage = { type: WorkerMessageType.error, uid, error: serialize(error) as any as SerializedError, - }; - Implementation.postMessageToMaster(errorMessage, transferables); + } + Implementation.postMessageToMaster(errorMessage, transferables) } function postJobResultMessage( @@ -92,14 +92,14 @@ function getExpose(Implementation: AbstractedWorkerAPI) { completed: boolean, resultValue?: any ) { - const { payload, transferables } = deconstructTransfer(resultValue); + const { payload, transferables } = deconstructTransfer(resultValue) const resultMessage: WorkerJobResultMessage = { type: WorkerMessageType.result, uid, complete: completed ? true : undefined, payload, - }; - Implementation.postMessageToMaster(resultMessage, transferables); + } + Implementation.postMessageToMaster(resultMessage, transferables) } function postJobStartMessage( @@ -110,8 +110,8 @@ function getExpose(Implementation: AbstractedWorkerAPI) { type: WorkerMessageType.running, uid, resultType, - }; - Implementation.postMessageToMaster(startMessage); + } + Implementation.postMessageToMaster(startMessage) } function postUncaughtErrorMessage(error: Error) { @@ -119,8 +119,8 @@ function getExpose(Implementation: AbstractedWorkerAPI) { const errorMessage: WorkerUncaughtErrorMessage = { type: WorkerMessageType.uncaughtError, error: serialize(error) as any as SerializedError, - }; - Implementation.postMessageToMaster(errorMessage); + } + Implementation.postMessageToMaster(errorMessage) } catch (subError) { // tslint:disable-next-line no-console console.error( @@ -130,41 +130,41 @@ function getExpose(Implementation: AbstractedWorkerAPI) { subError, "\nOriginal error:", error - ); + ) } } async function runFunction(jobUID: number, fn: WorkerFunction, args: any[]) { - let syncResult: any; + let syncResult: any try { - syncResult = fn(...args); + syncResult = fn(...args) } catch (error) { - return postJobErrorMessage(jobUID, error); + return postJobErrorMessage(jobUID, error) } - const resultType = isObservable(syncResult) ? "observable" : "promise"; - postJobStartMessage(jobUID, resultType); + 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) as any); - activeSubscriptions.delete(jobUID); + postJobErrorMessage(jobUID, serialize(error) as any) + activeSubscriptions.delete(jobUID) }, () => { - postJobResultMessage(jobUID, true); - activeSubscriptions.delete(jobUID); + postJobResultMessage(jobUID, true) + activeSubscriptions.delete(jobUID) } - ); - activeSubscriptions.set(jobUID, subscription); + ) + activeSubscriptions.set(jobUID, subscription) } else { try { - const result = await syncResult; - postJobResultMessage(jobUID, true, serialize(result)); + const result = await syncResult + postJobResultMessage(jobUID, true, serialize(result)) } catch (error) { - postJobErrorMessage(jobUID, serialize(error) as any); + postJobErrorMessage(jobUID, serialize(error) as any) } } } @@ -178,14 +178,14 @@ function getExpose(Implementation: AbstractedWorkerAPI) { */ function expose(exposed: WorkerFunction | WorkerModule) { if (!Implementation.isWorkerRuntime()) { - throw Error("expose() called in the master thread."); + throw Error("expose() called in the master thread.") } if (exposeCalled) { throw Error( "expose() called more than once. This is not possible. Pass an object to expose() if you want to expose multiple functions." - ); + ) } - exposeCalled = true; + exposeCalled = true if (typeof exposed === "function") { Implementation.subscribeToMasterMessages((messageData) => { @@ -194,10 +194,10 @@ function getExpose(Implementation: AbstractedWorkerAPI) { messageData.uid, exposed, messageData.args.map(deserialize) - ); + ) } - }); - postFunctionInitMessage(); + }) + postFunctionInitMessage() } else if (typeof exposed === "object" && exposed) { Implementation.subscribeToMasterMessages((messageData) => { if (isMasterJobRunMessage(messageData) && messageData.method) { @@ -205,31 +205,31 @@ function getExpose(Implementation: AbstractedWorkerAPI) { messageData.uid, exposed[messageData.method], messageData.args.map(deserialize) - ); + ) } - }); + }) const methodNames = Object.keys(exposed).filter( (key) => typeof exposed[key] === "function" - ); - postModuleInitMessage(methodNames); + ) + postModuleInitMessage(methodNames) } else { throw 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); + const jobUID = messageData.uid + const subscription = activeSubscriptions.get(jobUID) if (subscription) { - subscription.unsubscribe(); - activeSubscriptions.delete(jobUID); + subscription.unsubscribe() + activeSubscriptions.delete(jobUID) } } - }); + }) } if ( @@ -239,15 +239,15 @@ function getExpose(Implementation: AbstractedWorkerAPI) { ) { self.addEventListener("error", (event) => { // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(event.error || event), 250); - }); + setTimeout(() => postUncaughtErrorMessage(event.error || event), 250) + }) self.addEventListener("unhandledrejection", (event) => { - const error = (event as any).reason; + const error = (event as any).reason if (error && typeof (error as any).message === "string") { // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(error), 250); + setTimeout(() => postUncaughtErrorMessage(error), 250) } - }); + }) } if ( @@ -257,17 +257,17 @@ function getExpose(Implementation: AbstractedWorkerAPI) { ) { process.on("uncaughtException", (error) => { // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(error), 250); - }); + setTimeout(() => postUncaughtErrorMessage(error), 250) + }) process.on("unhandledRejection", (error) => { if (error && typeof (error as any).message === "string") { // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(error as any), 250); + setTimeout(() => postUncaughtErrorMessage(error as any), 250) } - }); + }) } - return expose; + return expose } -export default getExpose; +export default getExpose From 9828320d18c31691fc3fcb02bdad76fdbb8562dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 18 Oct 2021 20:28:26 +0300 Subject: [PATCH 22/50] chore: Drop ;'s --- src/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index e061a362..8014986a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ -export { registerSerializer } from "./common"; -export * from "./master/index"; -export { expose } from "./worker/index"; -export { expose as exposeShared } from "./shared-worker/index"; +export { registerSerializer } from "./common" +export * from "./master/index" +export { expose } from "./worker/index" +export { expose as exposeShared } from "./shared-worker/index" export { DefaultSerializer, JsonSerializable, Serializer, SerializerImplementation, -} from "./serializers"; -export { Transfer, TransferDescriptor } from "./transferable"; +} from "./serializers" +export { Transfer, TransferDescriptor } from "./transferable" From f5944666ad315617248faf65f2851f4205a97d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 18 Oct 2021 20:29:11 +0300 Subject: [PATCH 23/50] chore: Remove unnecessary formatting from readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0b134671..5e267a05 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Use with the [`threads-plugin`](https://github.com/andywer/threads-plugin). It w Then add it to your `webpack.config.js`: ```diff -+ const ThreadsPlugin = require('threads-plugin') ++ const ThreadsPlugin = require('threads-plugin'); module.exports = { // ... @@ -165,7 +165,7 @@ import { expose } from "threads/worker" expose({ hashPassword(password, salt) { return sha256(password + salt) - }, + } }) ``` From bdee01a4d0e2434a2e809ddb4341f0f41fc02f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 18 Oct 2021 20:31:07 +0300 Subject: [PATCH 24/50] chore: Drop ,'s --- src/master/spawn.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index ea4ac8b7..8d5d37ab 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -98,7 +98,7 @@ function createEventObservable( const messageHandler = ((messageEvent: MessageEvent) => { const workerEvent: WorkerMessageEvent = { type: WorkerEventType.message, - data: messageEvent.data, + data: messageEvent.data } observer.next(workerEvent) }) as EventListener @@ -109,7 +109,7 @@ function createEventObservable( ) const workerEvent: WorkerInternalErrorEvent = { type: WorkerEventType.internalError, - error: Error(errorEvent.reason), + error: Error(errorEvent.reason) } observer.next(workerEvent) }) as EventListener @@ -118,7 +118,7 @@ function createEventObservable( workerTermination.then(() => { const terminationEvent: WorkerTerminationEvent = { - type: WorkerEventType.termination, + type: WorkerEventType.termination } worker.removeEventListener("message", messageHandler) worker.removeEventListener("unhandledrejection", rejectionHandler) @@ -170,7 +170,7 @@ function setPrivateThreadProps( [$errors]: workerErrors, [$events]: workerEvents, [$terminate]: terminate, - [$worker]: worker, + [$worker]: worker }) } From 8073c52c22396138a818e9f68d4a48f080ebb5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 18 Oct 2021 20:36:14 +0300 Subject: [PATCH 25/50] chore: Revert formatting --- src/types/master.ts | 64 ++++++++++++++------------------------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/src/types/master.ts b/src/types/master.ts index c5da09ab..eb1516cf 100644 --- a/src/types/master.ts +++ b/src/types/master.ts @@ -18,47 +18,36 @@ interface ObservableLike { onComplete?: () => any ): ObservableLikeSubscription subscribe(listeners: { - next?(value: T): any - error?(error: any): any - complete?(): any + next?(value: T): any, + error?(error: any): any, + complete?(): any, }): ObservableLikeSubscription } -export type StripAsync = Type extends Promise +export type StripAsync = + Type extends Promise ? PromiseBaseType : Type extends ObservableLike ? ObservableBaseType : Type -export type StripTransfer = Type extends TransferDescriptor< - infer BaseType -> +export type StripTransfer = Type extends TransferDescriptor ? BaseType : Type export type ModuleMethods = { [methodName: string]: (...args: any) => any } -export type ProxyableArgs = Args extends [ - arg0: infer Arg0, - ...rest: infer RestArgs -] - ? [ - Arg0 extends Transferable ? Arg0 | TransferDescriptor : Arg0, - ...RestArgs - ] +export type ProxyableArgs = Args extends [arg0: infer Arg0, ...rest: infer RestArgs] + ? [Arg0 extends Transferable ? Arg0 | TransferDescriptor : Arg0, ...RestArgs] : Args -export type ProxyableFunction = Args extends [] - ? () => ObservablePromise>> - : ( - ...args: ProxyableArgs - ) => ObservablePromise>> +export type ProxyableFunction = + Args extends [] + ? () => ObservablePromise>> + : (...args: ProxyableArgs) => ObservablePromise>> export type ModuleProxy = { - [method in keyof Methods]: ProxyableFunction< - Parameters, - ReturnType - > + [method in keyof Methods]: ProxyableFunction, ReturnType> } export interface PrivateThreadProps { @@ -68,12 +57,8 @@ export interface PrivateThreadProps { [$worker]: Worker } -export type FunctionThread< - Args extends any[] = any[], - ReturnType = any -> = ProxyableFunction & PrivateThreadProps -export type ModuleThread = - ModuleProxy & PrivateThreadProps +export type FunctionThread = ProxyableFunction & PrivateThreadProps +export type ModuleThread = ModuleProxy & PrivateThreadProps // We have those extra interfaces to keep the general non-specific `Thread` type // as an interface, so it's displayed concisely in any TypeScript compiler output. @@ -95,9 +80,7 @@ export type TransferList = Transferable[] export interface Worker extends EventTarget { postMessage(value: any, transferList?: TransferList): void /** In nodejs 10+ return type is Promise while with tiny-worker and in browser return type is void */ - terminate( - callback?: (error?: Error, exitCode?: number) => void - ): void | Promise + terminate(callback?: (error?: Error, exitCode?: number) => void): void | Promise } export interface ThreadsWorkerOptions extends WorkerOptions { /** Prefix for the path passed to the Worker constructor. Web worker only. */ @@ -119,10 +102,7 @@ export interface ThreadsWorkerOptions extends WorkerOptions { } /** Worker implementation. Either web worker or a node.js Worker class. */ -export declare class WorkerImplementation - extends EventTarget - implements Worker -{ +export declare class WorkerImplementation extends EventTarget implements Worker { constructor(path: string, options?: ThreadsWorkerOptions) public postMessage(value: any, transferList?: TransferList): void public terminate(): void | Promise @@ -131,10 +111,7 @@ export declare class WorkerImplementation /** Class to spawn workers from a blob or source string. */ export declare class BlobWorker extends WorkerImplementation { constructor(blob: Blob, options?: ThreadsWorkerOptions) - public static fromText( - source: string, - options?: ThreadsWorkerOptions - ): WorkerImplementation + public static fromText(source: string, options?: ThreadsWorkerOptions): WorkerImplementation } export interface ImplementationExport { @@ -164,7 +141,4 @@ export interface WorkerTerminationEvent { type: WorkerEventType.termination } -export type WorkerEvent = - | WorkerInternalErrorEvent - | WorkerMessageEvent - | WorkerTerminationEvent +export type WorkerEvent = WorkerInternalErrorEvent | WorkerMessageEvent | WorkerTerminationEvent From 2d004abdaf9bb1945543110040b15047d36388f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 18 Oct 2021 20:38:49 +0300 Subject: [PATCH 26/50] chore: Revert formatting --- src/master/invocation-proxy.ts | 35 ++++++++++------------------------ 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/src/master/invocation-proxy.ts b/src/master/invocation-proxy.ts index 052c192a..7c660318 100644 --- a/src/master/invocation-proxy.ts +++ b/src/master/invocation-proxy.ts @@ -34,17 +34,11 @@ let nextJobUID = 1 const dedupe = (array: T[]): T[] => Array.from(new Set(array)) -const isJobErrorMessage = (data: any): data is WorkerJobErrorMessage => - data && data.type === WorkerMessageType.error -const isJobResultMessage = (data: any): data is WorkerJobResultMessage => - data && data.type === WorkerMessageType.result -const isJobStartMessage = (data: any): data is WorkerJobStartMessage => - data && data.type === WorkerMessageType.running - -function createObservableForJob( - worker: WorkerType, - jobUID: number -): Observable { +const isJobErrorMessage = (data: any): data is WorkerJobErrorMessage => data && data.type === WorkerMessageType.error +const isJobResultMessage = (data: any): data is WorkerJobResultMessage => data && data.type === WorkerMessageType.result +const isJobStartMessage = (data: any): data is WorkerJobStartMessage => data && data.type === WorkerMessageType.running + +function createObservableForJob(worker: WorkerType, jobUID: number): Observable { return new Observable((observer) => { let asyncType: "observable" | "promise" | undefined @@ -105,15 +99,12 @@ function createObservableForJob( }) } -function prepareArguments(rawArgs: any[]): { - args: any[] - transferables: Transferable[] -} { +function prepareArguments(rawArgs: any[]): { args: any[], transferables: Transferable[] } { if (rawArgs.length === 0) { // Exit early if possible return { args: [], - transferables: [], + transferables: [] } } @@ -131,15 +122,11 @@ function prepareArguments(rawArgs: any[]): { return { args, - transferables: - transferables.length === 0 ? transferables : dedupe(transferables), + transferables: transferables.length === 0 ? transferables : dedupe(transferables) } } -export function createProxyFunction( - worker: WorkerType, - method?: string -) { +export function createProxyFunction(worker: WorkerType, method?: string) { return ((...rawArgs: Args) => { const uid = nextJobUID++ const { args, transferables } = prepareArguments(rawArgs) @@ -162,9 +149,7 @@ export function createProxyFunction( return ObservablePromise.from(Promise.reject(error)) } - return ObservablePromise.from( - multicast(createObservableForJob(worker, uid)) - ) + return ObservablePromise.from(multicast(createObservableForJob(worker, uid))) }) as any as ProxyableFunction } From 5c1f8ace422480fe9c6709ee93c8897bd7f1b505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 18 Oct 2021 20:39:52 +0300 Subject: [PATCH 27/50] chore: Revert formatting --- src/master/invocation-proxy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/master/invocation-proxy.ts b/src/master/invocation-proxy.ts index 7c660318..461c2857 100644 --- a/src/master/invocation-proxy.ts +++ b/src/master/invocation-proxy.ts @@ -23,7 +23,7 @@ import { WorkerJobErrorMessage, WorkerJobResultMessage, WorkerJobStartMessage, - WorkerMessageType, + WorkerMessageType } from "../types/messages" type WorkerType = SharedWorker | TWorker @@ -39,7 +39,7 @@ const isJobResultMessage = (data: any): data is WorkerJobResultMessage => data & const isJobStartMessage = (data: any): data is WorkerJobStartMessage => data && data.type === WorkerMessageType.running function createObservableForJob(worker: WorkerType, jobUID: number): Observable { - return new Observable((observer) => { + return new Observable(observer => { let asyncType: "observable" | "promise" | undefined const messageHandler = ((event: MessageEvent) => { @@ -134,7 +134,7 @@ export function createProxyFunction(worker: Work type: MasterMessageType.run, uid, method, - args, + args } debugMessages("Sending command to run function to worker:", runMessage) From e232ee6c7f49eebaba52ac88a7e198228c012d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 18 Oct 2021 20:42:51 +0300 Subject: [PATCH 28/50] chore: Revert formatting --- src/master/spawn.ts | 47 +++++++++++++++++---------------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 8d5d37ab..f85f1277 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -32,28 +32,23 @@ type ArbitraryThreadType = FunctionThread & ModuleThread type ExposedToThreadType> = Exposed extends ArbitraryWorkerInterface - ? ArbitraryThreadType - : Exposed extends WorkerFunction - ? FunctionThread, StripAsync>> - : Exposed extends WorkerModule - ? ModuleThread - : never + ? ArbitraryThreadType + : Exposed extends WorkerFunction + ? FunctionThread, StripAsync>> + : Exposed extends WorkerModule + ? ModuleThread + : never const debugMessages = DebugLogger("threads:master:messages") const debugSpawn = DebugLogger("threads:master:spawn") const debugThreadUtils = DebugLogger("threads:master:thread-utils") -const isInitMessage = (data: any): data is WorkerInitMessage => - data && data.type === ("init" as const) -const isUncaughtErrorMessage = ( - data: any -): data is WorkerUncaughtErrorMessage => - data && data.type === ("uncaughtError" as const) +const isInitMessage = (data: any): data is WorkerInitMessage => data && data.type === ("init" as const) +const isUncaughtErrorMessage = (data: any): data is WorkerUncaughtErrorMessage => data && data.type === ("uncaughtError" as const) -const initMessageTimeout = - typeof process !== "undefined" && process.env.THREADS_WORKER_INIT_TIMEOUT - ? Number.parseInt(process.env.THREADS_WORKER_INIT_TIMEOUT, 10) - : 10000 +const initMessageTimeout = typeof process !== "undefined" && process.env.THREADS_WORKER_INIT_TIMEOUT + ? Number.parseInt(process.env.THREADS_WORKER_INIT_TIMEOUT, 10) + : 10000 async function withTimeout( promise: Promise, @@ -65,7 +60,10 @@ async function withTimeout( const timeout = new Promise((resolve, reject) => { timeoutHandle = setTimeout(() => reject(Error(errorMessage)), timeoutInMs) }) - const result = await Promise.race([promise, timeout]) + const result = await Promise.race([ + promise, + timeout + ]) clearTimeout(timeoutHandle) return result @@ -74,10 +72,7 @@ async function withTimeout( function receiveInitMessage(worker: WorkerType): Promise { return new Promise((resolve, reject) => { const messageHandler = ((event: MessageEvent) => { - debugMessages( - "Message from worker before finishing initialization:", - event.data - ) + debugMessages("Message from worker before finishing initialization:", event.data) if (isInitMessage(event.data)) { worker.removeEventListener("message", messageHandler) resolve(event.data) @@ -90,10 +85,7 @@ function receiveInitMessage(worker: WorkerType): Promise { }) } -function createEventObservable( - worker: WorkerType, - workerTermination: Promise -): Observable { +function createEventObservable(worker: WorkerType, workerTermination: Promise): Observable { return new Observable((observer) => { const messageHandler = ((messageEvent: MessageEvent) => { const workerEvent: WorkerMessageEvent = { @@ -128,10 +120,7 @@ function createEventObservable( }) } -function createTerminator(worker: TWorker): { - termination: Promise - terminate: () => Promise -} { +function createTerminator(worker: TWorker): {termination: Promise, terminate: () => Promise } { const [termination, resolver] = createPromiseWithResolver() const terminate = async () => { debugThreadUtils("Terminating worker") From 1594e2c82de7c8d2e96daa390fe229098df237df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:10:45 +0300 Subject: [PATCH 29/50] chore: Drop semicolons --- test/shared-workers/hello.ts | 4 ++-- test/shared-workers/increment.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/shared-workers/hello.ts b/test/shared-workers/hello.ts index 2df3ddc5..4330c29a 100644 --- a/test/shared-workers/hello.ts +++ b/test/shared-workers/hello.ts @@ -1,5 +1,5 @@ -import { expose } from "../../src/shared-worker"; +import { expose } from "../../src/shared-worker" expose(function helloWorld() { - return "Hello World"; + return "Hello World" }); diff --git a/test/shared-workers/increment.ts b/test/shared-workers/increment.ts index 4a7b19fa..d3e6ee4f 100644 --- a/test/shared-workers/increment.ts +++ b/test/shared-workers/increment.ts @@ -1,8 +1,8 @@ -import { expose } from "../../src/shared-worker"; +import { expose } from "../../src/shared-worker" -let counter = 0; +let counter = 0 expose(function increment(by: number = 1) { - counter += by; - return counter; + counter += by + return counter }); From 127c55ddf448eeca94575f10143ced013a782f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:13:53 +0300 Subject: [PATCH 30/50] chore: Simplify code --- src/master/invocation-proxy.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/master/invocation-proxy.ts b/src/master/invocation-proxy.ts index 461c2857..b0fd6abf 100644 --- a/src/master/invocation-proxy.ts +++ b/src/master/invocation-proxy.ts @@ -87,12 +87,9 @@ function createObservableForJob(worker: WorkerType, jobUID: number): type: MasterMessageType.cancel, uid: jobUID, } + const port = worker instanceof SharedWorker ? worker.port : worker; - if (worker instanceof SharedWorker) { - worker.port.postMessage(cancelMessage) - } else { - worker.postMessage(cancelMessage) - } + port.postMessage(cancelMessage, []); } worker.removeEventListener("message", messageHandler) } @@ -136,15 +133,12 @@ export function createProxyFunction(worker: Work method, args } + const port = worker instanceof SharedWorker ? worker.port : worker; debugMessages("Sending command to run function to worker:", runMessage) try { - if (worker instanceof SharedWorker) { - worker.port.postMessage(runMessage, transferables) - } else { - worker.postMessage(runMessage, transferables) - } + port.postMessage(runMessage, transferables) } catch (error) { return ObservablePromise.from(Promise.reject(error)) } From 2b7d881b56ba5a13e337821bcc61c72016dbd79d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:28:36 +0300 Subject: [PATCH 31/50] refactor: Simplify implementation I figured out `worker` code can contain a bit of branching to take shared workers into account. This eliminates both implementation and test code. Note that tests don't pass at the moment! --- README.md | 4 +- package.json | 3 +- src/get-expose.ts | 273 --------------------------- src/index.ts | 1 - src/shared-worker/bundle-entry.ts | 9 - src/shared-worker/implementation.ts | 44 ----- src/shared-worker/index.ts | 12 -- src/worker/implementation.browser.ts | 19 +- src/worker/index.ts | 221 +++++++++++++++++++++- test/shared-workers/hello.ts | 5 - test/shared-workers/increment.ts | 8 - test/shared.ts | 4 +- tsconfig.json | 1 - 13 files changed, 238 insertions(+), 366 deletions(-) delete mode 100644 src/get-expose.ts delete mode 100644 src/shared-worker/bundle-entry.ts delete mode 100644 src/shared-worker/implementation.ts delete mode 100644 src/shared-worker/index.ts delete mode 100644 test/shared-workers/hello.ts delete mode 100644 test/shared-workers/increment.ts diff --git a/README.md b/README.md index 5e267a05..b46e9bdd 100644 --- a/README.md +++ b/README.md @@ -202,9 +202,9 @@ await Thread.terminate(auth) ```js // workers/auth.js import sha256 from "js-sha256" -import { expose } from "threads/shared-worker" +import { expose } from "threads/worker" -exposeShared({ +expose({ hashPassword(password, salt) { return sha256(password + salt) }, diff --git a/package.json b/package.json index 17668ac0..132c26de 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test:library": "cross-env TS_NODE_FILES=true ava ./test/**/*.test.ts", "test:tooling": "cross-env TS_NODE_FILES=true ava ./test-tooling/**/*.test.ts", "test:puppeteer:basic": "puppet-run --plugin=mocha --bundle=./test/workers/:workers/ --serve=./bundle/worker.js:/worker.js ./test/*.chromium*.ts", - "test:puppeteer:shared": "puppet-run --plugin=mocha --bundle=./test/shared-workers/:shared-workers/ --serve=./bundle/worker.js:/worker.js ./test/shared.ts", + "test:puppeteer:shared": "puppet-run --plugin=mocha --bundle=./test/workers/:workers/ --serve=./bundle/worker.js:/worker.js ./test/shared.ts", "test:puppeteer:webpack": "puppet-run --serve ./test-tooling/webpack/dist/app.web/0.worker.js --serve ./test-tooling/webpack/dist/app.web/1.worker.js --plugin=mocha ./test-tooling/webpack/webpack.chromium.mocha.ts", "posttest": "tslint --project .", "prepare": "npm run build" @@ -129,7 +129,6 @@ "./dist/worker/implementation.js": "./dist/worker/implementation.browser.js", "./dist/worker/implementation.tiny-worker.js": false, "./dist/worker/implementation.worker_threads.js": false, - "./dist/shared-worker/implementation.js": "./dist/shared-worker/implementation.js", "./dist-esm/master/implementation.js": "./dist-esm/master/implementation.browser.js", "./dist-esm/master/implementation.node.js": false, "./dist-esm/worker/implementation.js": "./dist-esm/worker/implementation.browser.js", diff --git a/src/get-expose.ts b/src/get-expose.ts deleted file mode 100644 index 03cffc0b..00000000 --- a/src/get-expose.ts +++ /dev/null @@ -1,273 +0,0 @@ -import isSomeObservable from "is-observable" -import { Observable, Subscription } from "observable-fns" -import { deserialize, serialize } from "./common" -import { isTransferDescriptor, TransferDescriptor } from "./transferable" -import { - MasterJobCancelMessage, - MasterJobRunMessage, - MasterMessageType, - SerializedError, - WorkerInitMessage, - WorkerJobErrorMessage, - WorkerJobResultMessage, - WorkerJobStartMessage, - WorkerMessageType, - WorkerUncaughtErrorMessage, -} from "./types/messages" -import { - AbstractedWorkerAPI, - WorkerFunction, - WorkerModule, -} from "./types/worker" - -function getExpose(Implementation: AbstractedWorkerAPI) { - let exposeCalled = false - - const activeSubscriptions = new Map>() - - const isMasterJobCancelMessage = ( - thing: any - ): thing is MasterJobCancelMessage => - thing && thing.type === MasterMessageType.cancel - const isMasterJobRunMessage = (thing: any): thing is MasterJobRunMessage => - thing && thing.type === MasterMessageType.run - - /** - * There are issues with `is-observable` not recognizing zen-observable's instances. - * We are using `observable-fns`, but it's based on zen-observable, too. - */ - const isObservable = (thing: any): thing is Observable => - isSomeObservable(thing) || isZenObservable(thing) - - function isZenObservable(thing: any): thing is Observable { - return ( - thing && - typeof thing === "object" && - typeof thing.subscribe === "function" - ) - } - - function deconstructTransfer(thing: any) { - return isTransferDescriptor(thing) - ? { payload: thing.send, transferables: thing.transferables } - : { payload: thing, transferables: undefined } - } - - function postFunctionInitMessage() { - const initMessage: WorkerInitMessage = { - type: WorkerMessageType.init, - exposed: { - type: "function", - }, - } - Implementation.postMessageToMaster(initMessage) - } - - function postModuleInitMessage(methodNames: string[]) { - const initMessage: WorkerInitMessage = { - type: WorkerMessageType.init, - exposed: { - type: "module", - methods: methodNames, - }, - } - Implementation.postMessageToMaster(initMessage) - } - - function postJobErrorMessage( - uid: number, - rawError: Error | TransferDescriptor - ) { - const { payload: error, transferables } = deconstructTransfer(rawError) - const errorMessage: WorkerJobErrorMessage = { - type: WorkerMessageType.error, - uid, - error: serialize(error) as any as SerializedError, - } - Implementation.postMessageToMaster(errorMessage, transferables) - } - - function postJobResultMessage( - uid: number, - completed: boolean, - resultValue?: any - ) { - const { payload, transferables } = deconstructTransfer(resultValue) - const resultMessage: WorkerJobResultMessage = { - type: WorkerMessageType.result, - uid, - complete: completed ? true : undefined, - payload, - } - Implementation.postMessageToMaster(resultMessage, transferables) - } - - function postJobStartMessage( - uid: number, - resultType: WorkerJobStartMessage["resultType"] - ) { - const startMessage: WorkerJobStartMessage = { - type: WorkerMessageType.running, - uid, - resultType, - } - Implementation.postMessageToMaster(startMessage) - } - - function postUncaughtErrorMessage(error: Error) { - try { - const errorMessage: WorkerUncaughtErrorMessage = { - type: WorkerMessageType.uncaughtError, - error: serialize(error) as any as SerializedError, - } - Implementation.postMessageToMaster(errorMessage) - } catch (subError) { - // tslint:disable-next-line no-console - 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: number, fn: WorkerFunction, args: any[]) { - let syncResult: any - - try { - syncResult = fn(...args) - } catch (error) { - 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) as any) - 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) as any) - } - } - } - - /** - * Expose a function or a module (an object whose values are functions) - * to the main thread. Must be called exactly once in every worker thread - * to signal its API to the main thread. - * - * @param exposed Function or object whose values are functions - */ - function expose(exposed: WorkerFunction | WorkerModule) { - if (!Implementation.isWorkerRuntime()) { - throw Error("expose() called in the master thread.") - } - if (exposeCalled) { - throw 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 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 self !== "undefined" && - typeof self.addEventListener === "function" && - Implementation.isWorkerRuntime() - ) { - self.addEventListener("error", (event) => { - // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(event.error || event), 250) - }) - self.addEventListener("unhandledrejection", (event) => { - const error = (event as any).reason - if (error && typeof (error as any).message === "string") { - // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(error), 250) - } - }) - } - - if ( - typeof process !== "undefined" && - typeof process.on === "function" && - Implementation.isWorkerRuntime() - ) { - process.on("uncaughtException", (error) => { - // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(error), 250) - }) - process.on("unhandledRejection", (error) => { - if (error && typeof (error as any).message === "string") { - // Post with some delay, so the master had some time to subscribe to messages - setTimeout(() => postUncaughtErrorMessage(error as any), 250) - } - }) - } - - return expose -} - -export default getExpose diff --git a/src/index.ts b/src/index.ts index 8014986a..aa7cf80f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ export { registerSerializer } from "./common" export * from "./master/index" export { expose } from "./worker/index" -export { expose as exposeShared } from "./shared-worker/index" export { DefaultSerializer, JsonSerializable, diff --git a/src/shared-worker/bundle-entry.ts b/src/shared-worker/bundle-entry.ts deleted file mode 100644 index 1cbb6ed6..00000000 --- a/src/shared-worker/bundle-entry.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { expose } from "./index" -export * from "./index" - -if (typeof global !== "undefined") { - (global as any).expose = expose -} -if (typeof self !== "undefined") { - (self as any).expose = expose -} diff --git a/src/shared-worker/implementation.ts b/src/shared-worker/implementation.ts deleted file mode 100644 index 3ff320e6..00000000 --- a/src/shared-worker/implementation.ts +++ /dev/null @@ -1,44 +0,0 @@ -/// -// tslint:disable no-shadowed-variable - -import { AbstractedWorkerAPI } from "../types/worker"; - -declare const self: SharedWorker; - -const isWorkerRuntime: AbstractedWorkerAPI["isWorkerRuntime"] = - function isWorkerRuntime() { - const isWindowContext = - typeof self !== "undefined" && - typeof Window !== "undefined" && - self instanceof Window; - return typeof self !== "undefined" && - self.port?.postMessage && - !isWindowContext - ? true - : false; - }; - -const postMessageToMaster: AbstractedWorkerAPI["postMessageToMaster"] = - function postMessageToMaster(data, transferList?) { - // TODO: Check if this cast is true for shared workers - self.port.postMessage(data, transferList as PostMessageOptions); - }; - -const subscribeToMasterMessages: AbstractedWorkerAPI["subscribeToMasterMessages"] = - function subscribeToMasterMessages(onMessage) { - const messageHandler = (messageEvent: MessageEvent) => { - onMessage(messageEvent.data); - }; - const unsubscribe = () => { - self.port.removeEventListener("message", messageHandler as EventListener); - }; - self.port.addEventListener("message", messageHandler as EventListener); - self.port.start(); - return unsubscribe; - }; - -export default { - isWorkerRuntime, - postMessageToMaster, - subscribeToMasterMessages, -}; diff --git a/src/shared-worker/index.ts b/src/shared-worker/index.ts deleted file mode 100644 index 99e4fd75..00000000 --- a/src/shared-worker/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import getExpose from "../get-expose"; -import Implementation from "./implementation"; - -export { registerSerializer } from "../common"; -export { Transfer } from "../transferable"; - -/** Returns `true` if this code is currently running in a worker. */ -export const isWorkerRuntime = Implementation.isWorkerRuntime; - -const expose = getExpose(Implementation); - -export { expose }; diff --git a/src/worker/implementation.browser.ts b/src/worker/implementation.browser.ts index 1988ad9c..3fa2fc91 100644 --- a/src/worker/implementation.browser.ts +++ b/src/worker/implementation.browser.ts @@ -13,21 +13,32 @@ declare const self: WorkerGlobalScope const isWorkerRuntime: AbstractedWorkerAPI["isWorkerRuntime"] = function isWorkerRuntime() { const isWindowContext = typeof self !== "undefined" && typeof Window !== "undefined" && self instanceof Window - return typeof self !== "undefined" && self.postMessage && !isWindowContext ? true : false + const port = self instanceof SharedWorker ? self.port : self; + + return typeof self !== "undefined" && port.postMessage && !isWindowContext ? true : false } const postMessageToMaster: AbstractedWorkerAPI["postMessageToMaster"] = function postMessageToMaster(data, transferList?) { - self.postMessage(data, transferList) + const port = self instanceof SharedWorker ? self.port : self; + + port.postMessage(data, transferList || []) } const subscribeToMasterMessages: AbstractedWorkerAPI["subscribeToMasterMessages"] = function subscribeToMasterMessages(onMessage) { + const isSharedWorker = self instanceof SharedWorker; + const port = isSharedWorker ? self.port : self; const messageHandler = (messageEvent: MessageEvent) => { onMessage(messageEvent.data) } const unsubscribe = () => { - self.removeEventListener("message", messageHandler as EventListener) + port.removeEventListener("message", messageHandler as EventListener) } - self.addEventListener("message", messageHandler as EventListener) + port.addEventListener("message", messageHandler as EventListener) + + if (isSharedWorker) { + self.port.start(); + } + return unsubscribe } diff --git a/src/worker/index.ts b/src/worker/index.ts index 30a6080c..7d2385b2 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -1,4 +1,20 @@ -import getExpose from "../get-expose" +import isSomeObservable from "is-observable" +import { Observable, Subscription } from "observable-fns" +import { deserialize, serialize } from "../common" +import { isTransferDescriptor, TransferDescriptor } from "../transferable" +import { + MasterJobCancelMessage, + MasterJobRunMessage, + MasterMessageType, + SerializedError, + WorkerInitMessage, + WorkerJobErrorMessage, + WorkerJobResultMessage, + WorkerJobStartMessage, + WorkerMessageType, + WorkerUncaughtErrorMessage +} from "../types/messages" +import { WorkerFunction, WorkerModule } from "../types/worker" import Implementation from "./implementation" export { registerSerializer } from "../common" @@ -7,6 +23,205 @@ export { Transfer } from "../transferable" /** Returns `true` if this code is currently running in a worker. */ export const isWorkerRuntime = Implementation.isWorkerRuntime -const expose = getExpose(Implementation) +let exposeCalled = false -export { expose } +const activeSubscriptions = new Map>() + +const isMasterJobCancelMessage = (thing: any): thing is MasterJobCancelMessage => thing && thing.type === MasterMessageType.cancel +const isMasterJobRunMessage = (thing: any): thing is MasterJobRunMessage => thing && thing.type === MasterMessageType.run + +/** + * There are issues with `is-observable` not recognizing zen-observable's instances. + * We are using `observable-fns`, but it's based on zen-observable, too. + */ +const isObservable = (thing: any): thing is Observable => isSomeObservable(thing) || isZenObservable(thing) + +function isZenObservable(thing: any): thing is Observable { + return thing && typeof thing === "object" && typeof thing.subscribe === "function" +} + +function deconstructTransfer(thing: any) { + return isTransferDescriptor(thing) + ? { payload: thing.send, transferables: thing.transferables } + : { payload: thing, transferables: undefined } +} + +function postFunctionInitMessage() { + const initMessage: WorkerInitMessage = { + type: WorkerMessageType.init, + exposed: { + type: "function" + } + } + Implementation.postMessageToMaster(initMessage) +} + +function postModuleInitMessage(methodNames: string[]) { + const initMessage: WorkerInitMessage = { + type: WorkerMessageType.init, + exposed: { + type: "module", + methods: methodNames + } + } + Implementation.postMessageToMaster(initMessage) +} + +function postJobErrorMessage(uid: number, rawError: Error | TransferDescriptor) { + const { payload: error, transferables } = deconstructTransfer(rawError) + const errorMessage: WorkerJobErrorMessage = { + type: WorkerMessageType.error, + uid, + error: serialize(error) as any as SerializedError + } + Implementation.postMessageToMaster(errorMessage, transferables) +} + +function postJobResultMessage(uid: number, completed: boolean, resultValue?: any) { + const { payload, transferables } = deconstructTransfer(resultValue) + const resultMessage: WorkerJobResultMessage = { + type: WorkerMessageType.result, + uid, + complete: completed ? true : undefined, + payload + } + Implementation.postMessageToMaster(resultMessage, transferables) +} + +function postJobStartMessage(uid: number, resultType: WorkerJobStartMessage["resultType"]) { + const startMessage: WorkerJobStartMessage = { + type: WorkerMessageType.running, + uid, + resultType + } + Implementation.postMessageToMaster(startMessage) +} + +function postUncaughtErrorMessage(error: Error) { + try { + const errorMessage: WorkerUncaughtErrorMessage = { + type: WorkerMessageType.uncaughtError, + error: serialize(error) as any as SerializedError + } + Implementation.postMessageToMaster(errorMessage) + } catch (subError) { + // tslint:disable-next-line no-console + 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: number, fn: WorkerFunction, args: any[]) { + let syncResult: any + + try { + syncResult = fn(...args) + } catch (error) { + 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) as any) + 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) as any) + } + } +} + +/** + * Expose a function or a module (an object whose values are functions) + * to the main thread. Must be called exactly once in every worker thread + * to signal its API to the main thread. + * + * @param exposed Function or object whose values are functions + */ +export function expose(exposed: WorkerFunction | WorkerModule) { + if (!Implementation.isWorkerRuntime()) { + throw Error("expose() called in the master thread.") + } + if (exposeCalled) { + throw 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 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 self !== "undefined" && typeof self.addEventListener === "function" && Implementation.isWorkerRuntime()) { + self.addEventListener("error", event => { + // Post with some delay, so the master had some time to subscribe to messages + setTimeout(() => postUncaughtErrorMessage(event.error || event), 250) + }) + self.addEventListener("unhandledrejection", event => { + const error = (event as any).reason + if (error && typeof (error as any).message === "string") { + // Post with some delay, so the master had some time to subscribe to messages + setTimeout(() => postUncaughtErrorMessage(error), 250) + } + }) +} + +if (typeof process !== "undefined" && typeof process.on === "function" && Implementation.isWorkerRuntime()) { + process.on("uncaughtException", (error) => { + // Post with some delay, so the master had some time to subscribe to messages + setTimeout(() => postUncaughtErrorMessage(error), 250) + }) + process.on("unhandledRejection", (error) => { + if (error && typeof (error as any).message === "string") { + // Post with some delay, so the master had some time to subscribe to messages + setTimeout(() => postUncaughtErrorMessage(error as any), 250) + } + }) +} \ No newline at end of file diff --git a/test/shared-workers/hello.ts b/test/shared-workers/hello.ts deleted file mode 100644 index 4330c29a..00000000 --- a/test/shared-workers/hello.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { expose } from "../../src/shared-worker" - -expose(function helloWorld() { - return "Hello World" -}); diff --git a/test/shared-workers/increment.ts b/test/shared-workers/increment.ts deleted file mode 100644 index d3e6ee4f..00000000 --- a/test/shared-workers/increment.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { expose } from "../../src/shared-worker" - -let counter = 0 - -expose(function increment(by: number = 1) { - counter += by - return counter -}); diff --git a/test/shared.ts b/test/shared.ts index 97fb56d6..a56ac8de 100644 --- a/test/shared.ts +++ b/test/shared.ts @@ -8,7 +8,7 @@ import { spawn, Thread } from "../"; describe("threads in browser", function () { it("can spawn and terminate a thread", async function () { - const sharedWorker = new SharedWorker("./shared-workers/hello.js"); + const sharedWorker = new SharedWorker("./workers/hello-world.js"); // TODO: Why does not spawn complete for shared workers? const helloWorld = await spawn<() => string>(sharedWorker); @@ -20,7 +20,7 @@ describe("threads in browser", function () { }); it("can call a function thread more than once", async function () { - const sharedWorker = new SharedWorker("./shared-workers/increment.js"); + const sharedWorker = new SharedWorker("./workers/increment.js"); const increment = await spawn<() => number>(sharedWorker); expect(await increment()).to.equal(1); diff --git a/tsconfig.json b/tsconfig.json index 4577001d..579f4f58 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,6 @@ "./src/get-expose.ts", "./src/observable.ts", "./src/master/*", - "./src/shared-worker/*", "./src/worker/*", "./types/tiny-worker.d.ts", "./types/is-observable.d.ts" From 04554a82e409a8586714f1164a6203a43c9ad7be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:32:30 +0300 Subject: [PATCH 32/50] chore: Drop formatting --- src/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index aa7cf80f..8daf5286 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,5 @@ export { registerSerializer } from "./common" export * from "./master/index" export { expose } from "./worker/index" -export { - DefaultSerializer, - JsonSerializable, - Serializer, - SerializerImplementation, -} from "./serializers" +export { DefaultSerializer, JsonSerializable, Serializer, SerializerImplementation } from "./serializers" export { Transfer, TransferDescriptor } from "./transferable" From a906d7762efd35485c7ec2e00874b97013625149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:33:04 +0300 Subject: [PATCH 33/50] chore: Drop formatting --- src/master/invocation-proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/master/invocation-proxy.ts b/src/master/invocation-proxy.ts index b0fd6abf..002529cd 100644 --- a/src/master/invocation-proxy.ts +++ b/src/master/invocation-proxy.ts @@ -85,7 +85,7 @@ function createObservableForJob(worker: WorkerType, jobUID: number): if (asyncType === "observable" || !asyncType) { const cancelMessage: MasterJobCancelMessage = { type: MasterMessageType.cancel, - uid: jobUID, + uid: jobUID } const port = worker instanceof SharedWorker ? worker.port : worker; From 42d15bf1b71845343a89f9a47567248b14664901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:34:06 +0300 Subject: [PATCH 34/50] chore: Drop formatting --- src/master/spawn.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index f85f1277..045c7295 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -50,11 +50,7 @@ const initMessageTimeout = typeof process !== "undefined" && process.env.THREADS ? Number.parseInt(process.env.THREADS_WORKER_INIT_TIMEOUT, 10) : 10000 -async function withTimeout( - promise: Promise, - timeoutInMs: number, - errorMessage: string -): Promise { +async function withTimeout(promise: Promise, timeoutInMs: number, errorMessage: string): Promise { let timeoutHandle: any const timeout = new Promise((resolve, reject) => { @@ -86,7 +82,7 @@ function receiveInitMessage(worker: WorkerType): Promise { } function createEventObservable(worker: WorkerType, workerTermination: Promise): Observable { - return new Observable((observer) => { + return new Observable(observer => { const messageHandler = ((messageEvent: MessageEvent) => { const workerEvent: WorkerMessageEvent = { type: WorkerEventType.message, From 9138fd50776dff332bcf0e48462a210c2bb02de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:34:39 +0300 Subject: [PATCH 35/50] chore: Drop formatting --- src/master/spawn.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 045c7295..76c7b908 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -24,10 +24,7 @@ import { createProxyFunction, createProxyModule } from "./invocation-proxy" type WorkerType = SharedWorker | TWorker -type ArbitraryWorkerInterface = WorkerFunction & - WorkerModule & { - somekeythatisneverusedinproductioncode123: "magicmarker123" - } +type ArbitraryWorkerInterface = WorkerFunction & WorkerModule & { somekeythatisneverusedinproductioncode123: "magicmarker123" } type ArbitraryThreadType = FunctionThread & ModuleThread type ExposedToThreadType> = From fdf90a32cf1df0b5be743027be4c12e472bb096c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:35:03 +0300 Subject: [PATCH 36/50] chore: Drop formatting --- src/master/spawn.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 76c7b908..d3553599 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -15,10 +15,7 @@ import { WorkerMessageEvent, WorkerTerminationEvent, } from "../types/master" -import { - WorkerInitMessage, - WorkerUncaughtErrorMessage, -} from "../types/messages" +import { WorkerInitMessage, WorkerUncaughtErrorMessage } from "../types/messages" import { WorkerFunction, WorkerModule } from "../types/worker" import { createProxyFunction, createProxyModule } from "./invocation-proxy" From a1b837b76cb133660446544251171de41b25605a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:35:15 +0300 Subject: [PATCH 37/50] chore: Drop formatting --- src/master/spawn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index d3553599..9abba869 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -13,7 +13,7 @@ import { WorkerEventType, WorkerInternalErrorEvent, WorkerMessageEvent, - WorkerTerminationEvent, + WorkerTerminationEvent } from "../types/master" import { WorkerInitMessage, WorkerUncaughtErrorMessage } from "../types/messages" import { WorkerFunction, WorkerModule } from "../types/worker" From 460697739b61e4cdeeb7e82ef9071eb8bf1f01c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:35:29 +0300 Subject: [PATCH 38/50] chore: Drop formatting --- src/master/spawn.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 9abba869..e40d04da 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -33,6 +33,7 @@ type ExposedToThreadType> = ? ModuleThread : never + const debugMessages = DebugLogger("threads:master:messages") const debugSpawn = DebugLogger("threads:master:spawn") const debugThreadUtils = DebugLogger("threads:master:thread-utils") From 7f1a625698cc6777db4ef41008326b0c6d71bac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:35:52 +0300 Subject: [PATCH 39/50] chore: Drop formatting --- src/master/spawn.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index e40d04da..7f2c3f63 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -86,10 +86,7 @@ function createEventObservable(worker: WorkerType, workerTermination: Promise { - debugThreadUtils( - "Unhandled promise rejection event in thread:", - errorEvent - ) + debugThreadUtils("Unhandled promise rejection event in thread:", errorEvent) const workerEvent: WorkerInternalErrorEvent = { type: WorkerEventType.internalError, error: Error(errorEvent.reason) From 8663a6d5142e067ce96e49368bdcd90598d8a23d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:37:05 +0300 Subject: [PATCH 40/50] chore: Drop formatting --- src/master/spawn.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 7f2c3f63..4a010113 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -139,8 +139,8 @@ function setPrivateThreadProps( terminate: () => Promise ): T & PrivateThreadProps { const workerErrors = workerEvents - .filter((event) => event.type === WorkerEventType.internalError) - .map((errorEvent) => (errorEvent as WorkerInternalErrorEvent).error) + .filter(event => event.type === WorkerEventType.internalError) + .map(errorEvent => (errorEvent as WorkerInternalErrorEvent).error) // tslint:disable-next-line prefer-object-spread return Object.assign(raw, { @@ -160,21 +160,14 @@ function setPrivateThreadProps( * @param [options] * @param [options.timeout] Init message timeout. Default: 10000 or set by environment variable. */ -export async function spawn< - Exposed extends WorkerFunction | WorkerModule = ArbitraryWorkerInterface ->( +export async function spawn = ArbitraryWorkerInterface>( worker: WorkerType, options?: { timeout?: number } ): Promise> { 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 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 let termination, terminate From cf5cf41710fef46f8c6132888120ee97ee210e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:42:05 +0300 Subject: [PATCH 41/50] chore: Mark a todo --- src/master/spawn.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 4a010113..0f1e9c7b 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -134,7 +134,7 @@ function createSharedWorkerTerminator(worker: SharedWorker): { function setPrivateThreadProps( raw: T, - worker: TWorker, + worker: WorkerType, workerEvents: Observable, terminate: () => Promise ): T & PrivateThreadProps { @@ -142,6 +142,7 @@ function setPrivateThreadProps( .filter(event => event.type === WorkerEventType.internalError) .map(errorEvent => (errorEvent as WorkerInternalErrorEvent).error) + // TODO: See here, how to make a SharedWorker proxy as expected? // tslint:disable-next-line prefer-object-spread return Object.assign(raw, { [$errors]: workerErrors, @@ -189,7 +190,6 @@ export async function spawn = const proxy = createProxyFunction(worker) return setPrivateThreadProps( proxy, - // @ts-ignore TODO: How to handle this for shared workers? worker, events, terminate @@ -198,7 +198,6 @@ export async function spawn = const proxy = createProxyModule(worker, exposed.methods) return setPrivateThreadProps( proxy, - // @ts-ignore TODO: How to handle this for shared workers? worker, events, terminate From 2ea1d5544aa04ae4f286a92d6a1eb7d9d11e5cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:42:33 +0300 Subject: [PATCH 42/50] chore: Drop a reference --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 579f4f58..a855de20 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,6 @@ }, "include": [ "./src/index.ts", - "./src/get-expose.ts", "./src/observable.ts", "./src/master/*", "./src/worker/*", From 4c9d072047cf2fc667382bd2e994f3ddc45a964c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:43:05 +0300 Subject: [PATCH 43/50] chore: Drop formatting --- src/worker/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker/index.ts b/src/worker/index.ts index 7d2385b2..79bbf0be 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -224,4 +224,4 @@ if (typeof process !== "undefined" && typeof process.on === "function" && Implem setTimeout(() => postUncaughtErrorMessage(error as any), 250) } }) -} \ No newline at end of file +} From 5aef0bdf26c67553d76192ece73cfa111ee246ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:46:16 +0300 Subject: [PATCH 44/50] chore: Drop formatting --- src/master/spawn.ts | 18 +++--------------- src/types/master.ts | 17 ++++++----------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 0f1e9c7b..4c65fe6c 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -188,24 +188,12 @@ export async function spawn = if (exposed.type === "function") { const proxy = createProxyFunction(worker) - return setPrivateThreadProps( - proxy, - worker, - events, - terminate - ) as ExposedToThreadType + return setPrivateThreadProps(proxy, worker, events, terminate) as ExposedToThreadType } else if (exposed.type === "module") { const proxy = createProxyModule(worker, exposed.methods) - return setPrivateThreadProps( - proxy, - worker, - events, - terminate - ) as ExposedToThreadType + return setPrivateThreadProps(proxy, worker, events, terminate) as ExposedToThreadType } else { const type = (exposed as WorkerInitMessage["exposed"]).type - throw Error( - `Worker init message states unexpected type of expose(): ${type}` - ) + throw Error(`Worker init message states unexpected type of expose(): ${type}`) } } diff --git a/src/types/master.ts b/src/types/master.ts index eb1516cf..0214d89b 100644 --- a/src/types/master.ts +++ b/src/types/master.ts @@ -12,11 +12,7 @@ interface ObservableLikeSubscription { unsubscribe(): any } interface ObservableLike { - subscribe( - onNext: (value: T) => any, - onError?: (error: any) => any, - onComplete?: () => any - ): ObservableLikeSubscription + subscribe(onNext: (value: T) => any, onError?: (error: any) => any, onComplete?: () => any): ObservableLikeSubscription subscribe(listeners: { next?(value: T): any, error?(error: any): any, @@ -31,9 +27,8 @@ export type StripAsync = ? ObservableBaseType : Type -export type StripTransfer = Type extends TransferDescriptor - ? BaseType - : Type +export type StripTransfer = + Type extends TransferDescriptor ? BaseType : Type export type ModuleMethods = { [methodName: string]: (...args: any) => any } @@ -79,7 +74,7 @@ export type TransferList = Transferable[] /** Worker instance. Either a web worker or a node.js Worker provided by `worker_threads` or `tiny-worker`. */ export interface Worker extends EventTarget { postMessage(value: any, transferList?: TransferList): void - /** In nodejs 10+ return type is Promise while with tiny-worker and in browser return type is void */ + /** In nodejs 10+ return type is Promise while with tiny-worker and in browser return type is void */ terminate(callback?: (error?: Error, exitCode?: number) => void): void | Promise } export interface ThreadsWorkerOptions extends WorkerOptions { @@ -124,7 +119,7 @@ export interface ImplementationExport { export enum WorkerEventType { internalError = "internalError", message = "message", - termination = "termination", + termination = "termination" } export interface WorkerInternalErrorEvent { @@ -133,7 +128,7 @@ export interface WorkerInternalErrorEvent { } export interface WorkerMessageEvent { - type: WorkerEventType.message + type: WorkerEventType.message, data: Data } From 3c0049049f8735b440f89311b18314fe346ff314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Wed, 20 Oct 2021 14:47:03 +0300 Subject: [PATCH 45/50] chore: Drop formatting --- src/types/master.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types/master.ts b/src/types/master.ts index 0214d89b..420d63cd 100644 --- a/src/types/master.ts +++ b/src/types/master.ts @@ -28,7 +28,9 @@ export type StripAsync = : Type export type StripTransfer = - Type extends TransferDescriptor ? BaseType : Type + Type extends TransferDescriptor + ? BaseType + : Type export type ModuleMethods = { [methodName: string]: (...args: any) => any } From 8e008d5bc391b727a09323215a1fa23b1737d010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Fri, 22 Oct 2021 16:45:47 +0300 Subject: [PATCH 46/50] refactor: Merge shared worker tests with spawn tests A bit simpler this way. --- README.md | 2 +- package.json | 1 - test/shared.ts | 31 ------------------------------- test/spawn.chromium.mocha.ts | 24 +++++++++++++++++++++++- 4 files changed, 24 insertions(+), 34 deletions(-) delete mode 100644 test/shared.ts diff --git a/README.md b/README.md index b46e9bdd..fec1f469 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ In threads.js, the functionality is exposed as follows: ```js // master.js -import { spawn, Thread, SharedWorker } from "threads" +import { spawn, SharedWorker, Thread } from "threads" const auth = await spawn(new SharedWorker("./workers/auth")) const hashed = await auth.hashPassword("Super secret password", "1234") diff --git a/package.json b/package.json index 132c26de..9f887d86 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "test:library": "cross-env TS_NODE_FILES=true ava ./test/**/*.test.ts", "test:tooling": "cross-env TS_NODE_FILES=true ava ./test-tooling/**/*.test.ts", "test:puppeteer:basic": "puppet-run --plugin=mocha --bundle=./test/workers/:workers/ --serve=./bundle/worker.js:/worker.js ./test/*.chromium*.ts", - "test:puppeteer:shared": "puppet-run --plugin=mocha --bundle=./test/workers/:workers/ --serve=./bundle/worker.js:/worker.js ./test/shared.ts", "test:puppeteer:webpack": "puppet-run --serve ./test-tooling/webpack/dist/app.web/0.worker.js --serve ./test-tooling/webpack/dist/app.web/1.worker.js --plugin=mocha ./test-tooling/webpack/webpack.chromium.mocha.ts", "posttest": "tslint --project .", "prepare": "npm run build" diff --git a/test/shared.ts b/test/shared.ts deleted file mode 100644 index a56ac8de..00000000 --- a/test/shared.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * This code here will be run in a headless Chromium browser using `puppet-run`. - * Check the package.json scripts `test:puppeteer:*`. - */ - -import { expect } from "chai"; -import { spawn, Thread } from "../"; - -describe("threads in browser", function () { - it("can spawn and terminate a thread", async function () { - const sharedWorker = new SharedWorker("./workers/hello-world.js"); - - // TODO: Why does not spawn complete for shared workers? - const helloWorld = await spawn<() => string>(sharedWorker); - - console.log("hello world fn", helloWorld); - - expect(await helloWorld()).to.equal("Hello World"); - await Thread.terminate(helloWorld); - }); - - it("can call a function thread more than once", async function () { - const sharedWorker = new SharedWorker("./workers/increment.js"); - - const increment = await spawn<() => number>(sharedWorker); - expect(await increment()).to.equal(1); - expect(await increment()).to.equal(2); - expect(await increment()).to.equal(3); - await Thread.terminate(increment); - }); -}); diff --git a/test/spawn.chromium.mocha.ts b/test/spawn.chromium.mocha.ts index 6a5cb350..b64061a2 100644 --- a/test/spawn.chromium.mocha.ts +++ b/test/spawn.chromium.mocha.ts @@ -4,7 +4,7 @@ */ import { expect } from "chai" -import { spawn, BlobWorker, Thread } from "../" +import { spawn, BlobWorker, SharedWorker, Thread } from "../" // We need this as a work-around to make our threads Worker global, since // the bundler would otherwise not recognize `new Worker()` as a web worker @@ -43,4 +43,26 @@ describe("threads in browser", function() { expect(await increment()).to.equal(3) await Thread.terminate(increment) }) + + it("can spawn and terminate a thread with a shared worker", async function () { + const sharedWorker = new SharedWorker("./workers/hello-world.js"); + + // TODO: Why does not spawn complete for shared workers? + const helloWorld = await spawn<() => string>(sharedWorker); + + console.log("hello world fn", helloWorld); + + expect(await helloWorld()).to.equal("Hello World"); + await Thread.terminate(helloWorld); + }) + + it("can call a function thread with a shared worker more than once", async function () { + const sharedWorker = new SharedWorker("./workers/increment.js"); + + const increment = await spawn<() => number>(sharedWorker); + expect(await increment()).to.equal(1); + expect(await increment()).to.equal(2); + expect(await increment()).to.equal(3); + await Thread.terminate(increment); + }) }) From de025a1d7c6d1ef2cf508f3776a9c90f3cbea41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Fri, 22 Oct 2021 16:50:58 +0300 Subject: [PATCH 47/50] fix: Solve the proxy type issue --- src/master/spawn.ts | 2 -- src/types/master.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index 4c65fe6c..fa18bbd3 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -142,8 +142,6 @@ function setPrivateThreadProps( .filter(event => event.type === WorkerEventType.internalError) .map(errorEvent => (errorEvent as WorkerInternalErrorEvent).error) - // TODO: See here, how to make a SharedWorker proxy as expected? - // tslint:disable-next-line prefer-object-spread return Object.assign(raw, { [$errors]: workerErrors, [$events]: workerEvents, diff --git a/src/types/master.ts b/src/types/master.ts index 420d63cd..ab081427 100644 --- a/src/types/master.ts +++ b/src/types/master.ts @@ -51,7 +51,7 @@ export interface PrivateThreadProps { [$errors]: Observable [$events]: Observable [$terminate]: () => Promise - [$worker]: Worker + [$worker]: SharedWorker | Worker } export type FunctionThread = ProxyableFunction & PrivateThreadProps From b3dfead7e00e13d53ab1c7775d8cf70425269947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 25 Oct 2021 16:42:38 +0300 Subject: [PATCH 48/50] fix: Fix shared worker checks within a worker --- src/worker/implementation.browser.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/worker/implementation.browser.ts b/src/worker/implementation.browser.ts index 3fa2fc91..144205fb 100644 --- a/src/worker/implementation.browser.ts +++ b/src/worker/implementation.browser.ts @@ -7,26 +7,26 @@ interface WorkerGlobalScope { addEventListener(eventName: string, listener: (event: Event) => void): void postMessage(message: any, transferables?: any[]): void removeEventListener(eventName: string, listener: (event: Event) => void): void + port?: WorkerGlobalScope & { start: () => void } } declare const self: WorkerGlobalScope const isWorkerRuntime: AbstractedWorkerAPI["isWorkerRuntime"] = function isWorkerRuntime() { const isWindowContext = typeof self !== "undefined" && typeof Window !== "undefined" && self instanceof Window - const port = self instanceof SharedWorker ? self.port : self; + const port = self.port || self; return typeof self !== "undefined" && port.postMessage && !isWindowContext ? true : false } const postMessageToMaster: AbstractedWorkerAPI["postMessageToMaster"] = function postMessageToMaster(data, transferList?) { - const port = self instanceof SharedWorker ? self.port : self; + const port = self.port || self; port.postMessage(data, transferList || []) } const subscribeToMasterMessages: AbstractedWorkerAPI["subscribeToMasterMessages"] = function subscribeToMasterMessages(onMessage) { - const isSharedWorker = self instanceof SharedWorker; - const port = isSharedWorker ? self.port : self; + const port = self.port || self; const messageHandler = (messageEvent: MessageEvent) => { onMessage(messageEvent.data) } @@ -35,7 +35,7 @@ const subscribeToMasterMessages: AbstractedWorkerAPI["subscribeToMasterMessages" } port.addEventListener("message", messageHandler as EventListener) - if (isSharedWorker) { + if (self.port) { self.port.start(); } From aceed4bcdaa1257bbe080142a481374f45f283fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 25 Oct 2021 16:45:47 +0300 Subject: [PATCH 49/50] chore: Add a debug print --- test/spawn.chromium.mocha.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/spawn.chromium.mocha.ts b/test/spawn.chromium.mocha.ts index b64061a2..82f79ae3 100644 --- a/test/spawn.chromium.mocha.ts +++ b/test/spawn.chromium.mocha.ts @@ -47,7 +47,8 @@ describe("threads in browser", function() { it("can spawn and terminate a thread with a shared worker", async function () { const sharedWorker = new SharedWorker("./workers/hello-world.js"); - // TODO: Why does not spawn complete for shared workers? + console.log('hello before spawn'); + const helloWorld = await spawn<() => string>(sharedWorker); console.log("hello world fn", helloWorld); From cffac589a62c86675c38405c9fdb0c64af6d7237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20Veps=C3=A4l=C3=A4inen?= Date: Mon, 25 Oct 2021 17:18:46 +0300 Subject: [PATCH 50/50] chore: Sketch out onconnect --- src/master/spawn.ts | 15 +++++++++++++++ src/worker/implementation.browser.ts | 24 ++++++++++++++++++++---- src/worker/index.ts | 6 ++++++ test/spawn.chromium.mocha.ts | 2 ++ test/workers/hello-world.ts | 8 ++++++++ 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/master/spawn.ts b/src/master/spawn.ts index fa18bbd3..3ba526ce 100644 --- a/src/master/spawn.ts +++ b/src/master/spawn.ts @@ -33,6 +33,7 @@ type ExposedToThreadType> = ? ModuleThread : never +console.log('hello from spawn') const debugMessages = DebugLogger("threads:master:messages") const debugSpawn = DebugLogger("threads:master:spawn") @@ -165,14 +166,26 @@ export async function spawn = ): Promise> { debugSpawn("Initializing new thread") +console.log('0000'); + const timeout = options && options.timeout ? options.timeout : initMessageTimeout + + console.log('000') + 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().`) + + console.log('00') + const exposed = initMessage.exposed let termination, terminate +console.log('a') + if (worker instanceof SharedWorker) { const o = createSharedWorkerTerminator(worker) +console.log('b') + termination = o.termination terminate = o.terminate } else { @@ -182,6 +195,8 @@ export async function spawn = terminate = o.terminate } +console.log('c', exposed.type) + const events = createEventObservable(worker, termination) if (exposed.type === "function") { diff --git a/src/worker/implementation.browser.ts b/src/worker/implementation.browser.ts index 144205fb..14804c1a 100644 --- a/src/worker/implementation.browser.ts +++ b/src/worker/implementation.browser.ts @@ -30,15 +30,31 @@ const subscribeToMasterMessages: AbstractedWorkerAPI["subscribeToMasterMessages" const messageHandler = (messageEvent: MessageEvent) => { onMessage(messageEvent.data) } + + // TODO: Handle onconnect here somehow! + if (self.port) { + // @ts-ignore TODO: Testing for now + const connectHandler = (e) => { + const port = e.ports[0]; + + port.addEventListener('message', (messageEvent: MessageEvent) => { + port.onMessage(messageEvent.data) + }); + + port.start(); + } + + self.port.addEventListener('connect', connectHandler) + + // TODO: Does this need unsubscription too? + return () => {}; + } + const unsubscribe = () => { port.removeEventListener("message", messageHandler as EventListener) } port.addEventListener("message", messageHandler as EventListener) - if (self.port) { - self.port.start(); - } - return unsubscribe } diff --git a/src/worker/index.ts b/src/worker/index.ts index 79bbf0be..c40b099d 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -150,6 +150,8 @@ async function runFunction(jobUID: number, fn: WorkerFunction, args: any[]) { } } +console.log('hello from worker index') + /** * Expose a function or a module (an object whose values are functions) * to the main thread. Must be called exactly once in every worker thread @@ -158,6 +160,8 @@ async function runFunction(jobUID: number, fn: WorkerFunction, args: any[]) { * @param exposed Function or object whose values are functions */ export function expose(exposed: WorkerFunction | WorkerModule) { + console.log('at expose', Implementation.isWorkerRuntime(), exposeCalled) + if (!Implementation.isWorkerRuntime()) { throw Error("expose() called in the master thread.") } @@ -166,6 +170,8 @@ export function expose(exposed: WorkerFunction | WorkerModule) { } exposeCalled = true + console.log('at expose, continuing', exposed) + if (typeof exposed === "function") { Implementation.subscribeToMasterMessages(messageData => { if (isMasterJobRunMessage(messageData) && !messageData.method) { diff --git a/test/spawn.chromium.mocha.ts b/test/spawn.chromium.mocha.ts index 82f79ae3..e985fb96 100644 --- a/test/spawn.chromium.mocha.ts +++ b/test/spawn.chromium.mocha.ts @@ -11,6 +11,7 @@ import { spawn, BlobWorker, SharedWorker, Thread } from "../" import "../src/master/register" describe("threads in browser", function() { +/* it("can spawn and terminate a thread", async function() { const helloWorld = await spawn<() => string>(new Worker("./workers/hello-world.js")) expect(await helloWorld()).to.equal("Hello World") @@ -43,6 +44,7 @@ describe("threads in browser", function() { expect(await increment()).to.equal(3) await Thread.terminate(increment) }) +*/ it("can spawn and terminate a thread with a shared worker", async function () { const sharedWorker = new SharedWorker("./workers/hello-world.js"); diff --git a/test/workers/hello-world.ts b/test/workers/hello-world.ts index f573eb44..69768b58 100644 --- a/test/workers/hello-world.ts +++ b/test/workers/hello-world.ts @@ -1,5 +1,13 @@ import { expose } from "../../src/worker" +onconnect = () => { + expose(function helloWorld() { + return "Hello World" + }) +} + +console.log('hello from worker') + expose(function helloWorld() { return "Hello World" })