diff --git a/createWorker.d.ts b/createWorker.d.ts new file mode 100644 index 00000000..58beaffd --- /dev/null +++ b/createWorker.d.ts @@ -0,0 +1 @@ +export { createWorker } from "./dist-esm/createWorker" diff --git a/createWorker.js b/createWorker.js new file mode 100644 index 00000000..ac8419d7 --- /dev/null +++ b/createWorker.js @@ -0,0 +1 @@ +module.exports = require("./dist/createWorker") diff --git a/createWorker.mjs b/createWorker.mjs new file mode 100644 index 00000000..58beaffd --- /dev/null +++ b/createWorker.mjs @@ -0,0 +1 @@ +export { createWorker } from "./dist-esm/createWorker" diff --git a/docs/usage.md b/docs/usage.md index 32b58459..61954992 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -166,6 +166,33 @@ const worker = await spawn(BlobWorker.fromText(MyWorker)) Bundle this module and you will obtain a stand-alone bundle that has its worker inlined. This is particularly useful for libraries using threads.js. +### createWorker - select worker backend +`createWorker` allows selecting the worker backend (among web, node, and tiny), and also if you want a blob worker. + +The second required argument to the `createWorker` is a string that specifies `backend: 'web' | 'node' | 'tiny'`. The third optional argument is an object that can be used to specify `blob: boolean` or other `WorkerOptions`. + +`createWorker` uses dynamic imports to only import the needed implementation, so you can import the needed functions directly to reduce the bundle size. + +```js +import { createWorker } from "threads/createWorker" +import { spawn, Thread } from "threads" + +async function run() { + const worker = await createWorker("./worker.js", "node") + const add = await spawn(worker) + const result = await add(2, 3) + await Thread.terminate(add) + return result +} + +run().then(result => { + console.log(`Result: ${result}`) +}).catch(error => { + console.error(error) +}) +``` + + ## TypeScript ### Type-safe workers diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 00000000..bdd85a3d --- /dev/null +++ b/index.d.ts @@ -0,0 +1 @@ +export { registerSerializer, spawn, DefaultSerializer, Pool, Thread, Transfer, Worker } from "./src/index.js" diff --git a/package.json b/package.json index 3101aca2..7c09d68f 100644 --- a/package.json +++ b/package.json @@ -124,12 +124,12 @@ "ts-node/register": false, "worker_threads": false, "./dist/master/implementation.js": "./dist/master/implementation.browser.js", - "./dist/master/implementation.node.js": false, + "./dist/master/implementation-node.js": false, "./dist/worker/implementation.js": "./dist/worker/implementation.browser.js", "./dist/worker/implementation.tiny-worker.js": false, "./dist/worker/implementation.worker_threads.js": false, "./dist-esm/master/implementation.js": "./dist-esm/master/implementation.browser.js", - "./dist-esm/master/implementation.node.js": false, + "./dist-esm/master/implementation-node.js": false, "./dist-esm/worker/implementation.js": "./dist-esm/worker/implementation.browser.js", "./dist-esm/worker/implementation.tiny-worker.js": false, "./dist-esm/worker/implementation.worker_threads.js": false @@ -139,6 +139,7 @@ "dist-esm/**", "*.js", "*.mjs", - "*.ts" + "*.ts", + "src" ] } diff --git a/src/createWorker.ts b/src/createWorker.ts new file mode 100644 index 00000000..5eb67aa2 --- /dev/null +++ b/src/createWorker.ts @@ -0,0 +1,35 @@ +import { + BlobWorker, + CreateWorkerOptions, + WorkerImplementation, +} from "./types/master" + +/** + * async function to creat a webworker. This function uses dynamic imports to only import the required implementation + * @param workerPath the path or Blob to the worker code + * @param backend backend for the threads + * @param {CreateWorkerOptions} options an object that can be used to specify `blob: boolean` or other {WorkerOptions}. Defaults to `{}`. + * @returns A web worker, a node.js worker, or a tiny worker class. + */ +export async function createWorker(workerPath: string | Blob, backend: "web" | "node" | "tiny", options: CreateWorkerOptions = {}) { + let WorkerConstructor: typeof WorkerImplementation | typeof BlobWorker + if (backend === "web") { + const { getWorkerImplementation } = await import("./master/implementation.browser") + WorkerConstructor = options.blob ? + getWorkerImplementation().blob : + getWorkerImplementation().default + } else if (backend === "node") { + const { initWorkerThreadsWorker } = await import("./master/implementation-node") + WorkerConstructor = options.blob ? + initWorkerThreadsWorker().blob : + initWorkerThreadsWorker().default + } else if (backend === "tiny") { + const { initTinyWorker } = await import("./master/implementation-node") + WorkerConstructor = options.blob ? + initTinyWorker().blob : + initTinyWorker().default + } else { + throw new Error("The worker backend is not supported.") + } + return new WorkerConstructor(workerPath as string & Blob, options) +} diff --git a/src/master/implementation.node.ts b/src/master/implementation-node.ts similarity index 91% rename from src/master/implementation.node.ts rename to src/master/implementation-node.ts index 68878c4a..799a5717 100644 --- a/src/master/implementation.node.ts +++ b/src/master/implementation-node.ts @@ -88,7 +88,8 @@ function resolveScriptPath(scriptPath: string, baseURL?: string | undefined) { return workerFilePath } -function initWorkerThreadsWorker(): ImplementationExport { +export function initWorkerThreadsWorker(): ImplementationExport { + isTinyWorker = false // Webpack hack const NativeWorker = typeof __non_webpack_require__ === "function" ? __non_webpack_require__("worker_threads").Worker @@ -164,7 +165,8 @@ function initWorkerThreadsWorker(): ImplementationExport { } } -function initTinyWorker(): ImplementationExport { +export function initTinyWorker(): ImplementationExport { + isTinyWorker = true const TinyWorker = require("tiny-worker") let allWorkers: Array = [] @@ -248,21 +250,31 @@ function initTinyWorker(): ImplementationExport { let implementation: ImplementationExport let isTinyWorker: boolean -function selectWorkerImplementation(): ImplementationExport { - try { - isTinyWorker = false +function selectWorkerImplementation(selection?: string): ImplementationExport { + if (!selection) { + + // automatic version based selection + try { + return initWorkerThreadsWorker() + } catch(error) { + // tslint:disable-next-line no-console + console.debug("Node worker_threads not available. Trying to fall back to tiny-worker polyfill...") + return initTinyWorker() + } + + // manual selection + } else if (selection === "node") { return initWorkerThreadsWorker() - } catch(error) { - // tslint:disable-next-line no-console - console.debug("Node worker_threads not available. Trying to fall back to tiny-worker polyfill...") - isTinyWorker = true + } else if (selection === "tiny") { return initTinyWorker() + } else { + throw new Error("selection is not supported" + selection) } } -export function getWorkerImplementation(): ImplementationExport { +export function getWorkerImplementation(selection?: string): ImplementationExport { if (!implementation) { - implementation = selectWorkerImplementation() + implementation = selectWorkerImplementation(selection) } return implementation } diff --git a/src/master/implementation.ts b/src/master/implementation.ts index b6839478..ba2e5ffa 100644 --- a/src/master/implementation.ts +++ b/src/master/implementation.ts @@ -5,9 +5,8 @@ // We alias `src/master/implementation` to `src/master/implementation.browser` for web // browsers already in the package.json, so if get here, it's safe to pass-through the // node implementation - +import * as NodeImplementation from "./implementation-node" import * as BrowserImplementation from "./implementation.browser" -import * as NodeImplementation from "./implementation.node" const runningInNode = typeof process !== 'undefined' && process.arch !== 'browser' && 'pid' in process const implementation = runningInNode ? NodeImplementation : BrowserImplementation diff --git a/src/types/master.ts b/src/types/master.ts index 8e614166..c308cd8b 100644 --- a/src/types/master.ts +++ b/src/types/master.ts @@ -98,6 +98,11 @@ export interface ThreadsWorkerOptions extends WorkerOptions { CORSWorkaround?: boolean } +export interface CreateWorkerOptions extends ThreadsWorkerOptions { + /** flag to return a BlobWorker */ + blob?: 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) diff --git a/test-tooling/rollup/app-createWorker.js b/test-tooling/rollup/app-createWorker.js new file mode 100644 index 00000000..c3fd9fce --- /dev/null +++ b/test-tooling/rollup/app-createWorker.js @@ -0,0 +1,17 @@ +import { createWorker } from "../../createWorker.mjs" +import { spawn, Thread } from "../../" + +async function run() { + const add = await spawn(await createWorker("./worker.js", "node")) + const result = await add(2, 3) + await Thread.terminate(add) + return result +} + +run().then(result => { + console.log(`Result: ${result}`) + puppet.exit(0) +}).catch(error => { + console.error(error) + puppet.exit(1) +}) diff --git a/test-tooling/rollup/rollup.test.ts b/test-tooling/rollup/rollup.test.ts index 42e6253d..39ea6b96 100644 --- a/test-tooling/rollup/rollup.test.ts +++ b/test-tooling/rollup/rollup.test.ts @@ -12,6 +12,11 @@ test("can be bundled using rollup", async t => { ...config }) + const appCreateWorkerBundleP = rollup({ + input: path.resolve(__dirname, "app-createWorker.js"), + ...config + }) + const workerBundleP = rollup({ input: path.resolve(__dirname, "worker.js"), ...config @@ -22,12 +27,17 @@ test("can be bundled using rollup", async t => { format: "iife" }) + const appCreateWorkerBundleWriteP = (await appCreateWorkerBundleP).write({ + dir: path.resolve(__dirname, "dist"), + format: "es" + }) + const workerBundleWriteP = (await workerBundleP).write({ dir: path.resolve(__dirname, "dist"), format: "iife" }) - await Promise.all([appBundleWriteP, workerBundleWriteP]) + await Promise.all([appBundleWriteP, workerBundleWriteP, appCreateWorkerBundleWriteP]) if (process.platform === "win32") { // Quick-fix for weird Windows issue in CI diff --git a/test/createWorker.test.ts b/test/createWorker.test.ts new file mode 100644 index 00000000..952d2a0e --- /dev/null +++ b/test/createWorker.test.ts @@ -0,0 +1,27 @@ +import test from "ava" +import { createWorker } from '../src/createWorker' +import { spawn, Thread } from "../src/index" + +test("createWorker web", async t => { + const worker = await createWorker("./workers/hello-world", "web") + const helloWorld = await spawn(worker) + t.is(await helloWorld(), "Hello World") + await Thread.terminate(helloWorld) + t.pass() +}) + +test("createWorker node", async t => { + const worker = await createWorker("./workers/hello-world", "node") + const helloWorld = await spawn(worker) + t.is(await helloWorld(), "Hello World") + await Thread.terminate(helloWorld) + t.pass() +}) + +test("createWorker tiny", async t => { + const worker = await createWorker("./workers/hello-world", "tiny") + const helloWorld = await spawn(worker) + t.is(await helloWorld(), "Hello World") + await Thread.terminate(helloWorld) + t.pass() +}) diff --git a/tsconfig.json b/tsconfig.json index a855de20..a3161ad0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ }, "include": [ "./src/index.ts", + "./src/createWorker.ts", "./src/observable.ts", "./src/master/*", "./src/worker/*",