UNPKG

@ayonli/jsext

Version:

A JavaScript extension package for building strong and modern applications.

186 lines (185 loc) 7.37 kB
/** * This module provides JavaScript the ability to run functions in parallel * threads and take advantage of multi-core CPUs, inspired by Golang. * @module */ export type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; }[keyof T]; export type FunctionProperties<T> = Readonly<Pick<T, FunctionPropertyNames<T>>>; export type ThreadedFunctions<M, T extends FunctionProperties<M> = FunctionProperties<M>> = { [K in keyof T]: T[K] extends (...args: infer A) => AsyncGenerator<infer T, infer R, infer N> ? (...args: A) => AsyncGenerator<T, R, N> : T[K] extends (...args: infer A) => Generator<infer T, infer R, infer N> ? (...args: A) => AsyncGenerator<T, R, N> : T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<Awaited<R>> : T[K]; }; /** * Wraps a module so its functions will be run in worker threads. * * In Node.js and Bun, the `module` can be either an ES module or a CommonJS * module, **node_modules** and built-in modules are also supported. * * In browsers and Deno, the `module` can only be an ES module. * * Data are cloned and transferred between threads via **Structured Clone** * **Algorithm**. * * Apart from the standard data types supported by the algorithm, {@link Channel} * can also be used to transfer data between threads. To do so, just passed a * channel instance to the threaded function. But be aware, channel can only be * used as a parameter, return a channel from the threaded function is not * allowed. Once passed, the data can only be transferred into and out-from the * function. * * The difference between using a channel and a generator function for streaming * processing is, for a generator function, `next(value)` is coupled with a * `yield value`, the process is blocked between **next** calls, channel doesn't * have this limit, we can use it to stream all the data into the function * before processing and receiving any result. * * The threaded function also supports `ArrayBuffer`s as transferable objects. * If an array buffer is presented as an argument or the direct property of an * argument (assume it's a plain object), or the array buffer is the return * value or the direct property of the return value (assume it's a plain object), * it automatically becomes a transferrable object and will be transferred to * the other thread instead of being cloned. This strategy allows us to easily * compose objects like `Request` and `Response` instances into plain objects * and pass them between threads without overhead. * * **NOTE:** * If the current module is already in a worker thread, use this function won't * create another worker thread. * * **NOTE:** * Cloning and transferring data between the main thread and worker threads are * very heavy and slow, worker threads are only intended to run CPU-intensive * tasks or divide tasks among multiple threads, they have no advantage when * performing IO-intensive tasks such as handling HTTP requests, always prefer * `cluster` module for that kind of purpose. * * **NOTE:** * For error instances, only the following types are guaranteed to be sent and * received properly between threads. * * - `Error` * - `EvalError` * - `RangeError` * - `ReferenceError` * - `SyntaxError` * - `TypeError` * - `URIError` * - `AggregateError` (as arguments, return values, thrown values, or shallow * object properties) * - `Exception` (as arguments, return values, thrown values, or shallow object * properties) * - `DOMException` (as arguments, return values, thrown values, or shallow * object properties) * * In order to handle errors properly between threads, throw well-known error * types or use `Exception` (or `DOMException`) with error names in the threaded * function. * * @example * ```ts * // regular or async function * import parallel from "@ayonli/jsext/parallel"; * const { greet } = parallel(() => import("./examples/worker.mjs")); * * console.log(await greet("World")); // Hi, World * ``` * * @example * ```ts * // generator or async generator function * import parallel from "@ayonli/jsext/parallel"; * const { sequence } = parallel(() => import("./examples/worker.mjs")); * * for await (const word of sequence(["foo", "bar"])) { * console.log(word); * } * // output: * // foo * // bar * ``` * * @example * ```ts * // use channel * import chan from "@ayonli/jsext/chan"; * import { range } from "@ayonli/jsext/number"; * import readAll from "@ayonli/jsext/readAll"; * import parallel from "@ayonli/jsext/parallel"; * const { twoTimesValues } = parallel(() => import("./examples/worker.mjs")); * * const channel = chan<{ value: number; done: boolean; }>(); * const length = twoTimesValues(channel); * * for (const value of range(0, 9)) { * await channel.push({ value, done: value === 9 }); * } * * const results = (await readAll(channel)).map(item => item.value); * console.log(results); // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] * console.log(await length); // 10 * ``` * * @example * ```ts * // use transferrable * import parallel from "@ayonli/jsext/parallel"; * const { transfer } = parallel(() => import("./examples/worker.mjs")); * * const arr = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); * const length = await transfer(arr.buffer); * * console.log(length); // 10 * console.log(arr.length); // 0 * ``` * * **Compatibility List:** * * The following environments are guaranteed to work: * * - [x] Node.js v12+ * - [x] Deno v1.0+ * - [x] Bun v1.0+ * - [x] Modern browsers * * **Use with Vite:** * * In order to use parallel threads with Vite, we need to adjust a little bit, * please check [this document](https://github.com/ayonli/jsext/blob/main/parallel/README.md#use-with-vite). * * **Warn about TSX (the runtime):** * * For users who use `tsx` to run TypeScript directly in Node.js, the runtime is * unable to use TypeScript directly in worker threads at the moment, so this * function won't work in such a case. * See [this issue](https://github.com/privatenumber/tsx/issues/354) for more * information. */ declare function parallel<M extends { [x: string]: any; }>(module: string | (() => Promise<M>)): ThreadedFunctions<M>; declare namespace parallel { /** * The maximum number of workers allowed to exist at the same time. If not * set, the program by default uses CPU core numbers as the limit. */ var maxWorkers: number | undefined; /** * In browsers, by default, the program loads the worker entry directly from * GitHub, which could be slow due to poor internet connection, we can copy * the entry file `bundle/worker.mjs` to a local path of our website and set * this option to that path so that it can be loaded locally. * * Or, if the code is bundled, the program won't be able to automatically * locate the entry file in the file system, in such case, we can also copy * the entry file (`bundle/worker.mjs` for Bun, Deno and the browser, * `bundle/worker-node.mjs` for Node.js) to a local directory and supply * this option instead. */ var workerEntry: string | undefined; /** * Indicates whether the current thread is the main thread. */ const isMainThread: boolean; } export default parallel;