@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
186 lines (185 loc) • 7.37 kB
TypeScript
/**
* 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;