@offmain/workerkit 0.8.9 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,31 +1,169 @@
1
- import { CollectOptions, CollectedResult, MainWorkerFactoryOptions, WorkerFunction, WorkerResult } from './types.ts';
1
+ import { CollectOptions, CollectedResult, PipelineStep, WorkerConfig, WorkerConfigMap, WorkerDataParam, WorkerFunction, WorkerReturnType, TypedSettledResults } from './types.ts';
2
2
  /**
3
3
  * Recursively collects all Transferable objects from a value.
4
4
  * Transferables (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas)
5
5
  * are zero-copy — they are moved to the worker instead of cloned.
6
6
  */
7
7
  export declare function extractTransferables(value: unknown, seen?: Set<object>): Transferable[];
8
- declare class MainWorkerFactory {
8
+ /**
9
+ * Central orchestrator for running typed Web Workers in parallel.
10
+ *
11
+ * `MainWorkerFactory` manages a registry of named worker configurations and
12
+ * handles the full lifecycle of each worker: spawning, partitioning input
13
+ * data across threads, retrying on failure, and collecting results.
14
+ *
15
+ * @typeParam TConfigs - A readonly tuple of {@link WorkerConfig} objects that
16
+ * defines the set of available workers and their typed signatures.
17
+ *
18
+ * @example
19
+ * const foreman = new MainWorkerFactory({
20
+ * workers: [
21
+ * { name: 'sum', role: 'compute', func: sumWorker, partition: true },
22
+ * ],
23
+ * });
24
+ *
25
+ * const settled = await foreman.runWorker('sum', { srcData: [1, 2, 3, 4] });
26
+ * const { data } = await foreman.collectResults(settled);
27
+ */
28
+ declare class MainWorkerFactory<TConfigs extends readonly WorkerConfig<WorkerFunction<any, any>>[]> {
9
29
  private readonly _workers;
10
30
  private readonly _threads;
11
- constructor(_initiator: WorkerFunction, options: MainWorkerFactoryOptions);
31
+ /**
32
+ * Creates a new `MainWorkerFactory`.
33
+ *
34
+ * @param options - Configuration object containing the `workers` registry.
35
+ */
36
+ constructor(options: {
37
+ workers: TConfigs;
38
+ });
39
+ /**
40
+ * Instantiates a {@link WorkerFactory} for the given worker function.
41
+ *
42
+ * @param workerFunction - The function to run inside the worker thread.
43
+ * @returns A new `WorkerFactory` wrapping the worker.
44
+ */
12
45
  private initWorker;
13
46
  /**
14
- * Partitions an array into up to numChunks evenly-sized chunks.
47
+ * Splits an array into up to `numChunks` evenly-sized sub-arrays.
48
+ *
49
+ * When the array length is not evenly divisible, the first `remainder`
50
+ * chunks receive one extra element so no data is lost.
51
+ *
52
+ * @param array - The source array to partition.
53
+ * @param numChunks - Maximum number of chunks to produce.
54
+ * Clamped to `array.length` so you never get empty chunks.
55
+ * @returns An array of sub-arrays. Returns `[]` when `array` is empty.
56
+ * @throws {Error} When `numChunks` is not a positive integer.
57
+ *
58
+ * @example
59
+ * partitionArray([1, 2, 3, 4, 5], 3);
60
+ * // → [[1, 2], [3, 4], [5]]
15
61
  */
16
62
  partitionArray<T>(array: T[], numChunks: number): T[][];
63
+ /**
64
+ * Looks up a registered worker configuration by name.
65
+ *
66
+ * @param name - The `name` field of the target {@link WorkerConfig}.
67
+ * @returns The matching config, or `undefined` if not found.
68
+ */
17
69
  private findWorkerByName;
18
- runWorker(workerName: string, { srcData, ...otherParams }: {
19
- srcData: unknown;
20
- } & Record<string, unknown>): Promise<PromiseSettledResult<WorkerResult>[]>;
70
+ /**
71
+ * Runs a named worker against the provided data, distributing work across
72
+ * threads when the worker is configured for partitioning.
73
+ *
74
+ * When `config.partition` is `true` and `srcData` is an array with more
75
+ * than one element, the array is split into up to `maxConcurrency` (or
76
+ * `navigator.hardwareConcurrency`) shards and each shard is processed by
77
+ * a separate worker thread in parallel.
78
+ *
79
+ * All threads are awaited with `Promise.allSettled`, so a failure in one
80
+ * shard does not cancel the others. Use {@link collectResults} to merge
81
+ * the settled output.
82
+ *
83
+ * @typeParam TName - The literal name of the worker to run (inferred from
84
+ * the registered `workers` tuple).
85
+ *
86
+ * @param workerName - Name of the worker as declared in the `workers` config.
87
+ * @param params - Object containing `srcData` (the payload) plus any
88
+ * additional key/value pairs forwarded to the worker verbatim.
89
+ *
90
+ * @returns A {@link TypedSettledResults} wrapping the settled promises from
91
+ * all spawned worker threads.
92
+ *
93
+ * @example
94
+ * const settled = await foreman.runWorker('sum', { srcData: [1, 2, 3] });
95
+ */
96
+ runWorker<TName extends keyof WorkerConfigMap<TConfigs> & string>(workerName: TName, { srcData, ...otherParams }: {
97
+ srcData: WorkerDataParam<WorkerConfigMap<TConfigs>[TName]>;
98
+ } & Record<string, unknown>): Promise<TypedSettledResults<WorkerReturnType<WorkerConfigMap<TConfigs>[TName]>>>;
99
+ /**
100
+ * Builds the array of per-thread worker promises for a single `runWorker`
101
+ * call.
102
+ *
103
+ * When `isPartitioned` is `true`, each promise receives its own slice of
104
+ * `srcData`; otherwise every thread receives the full payload.
105
+ *
106
+ * @param config - The resolved {@link WorkerConfig} for this run.
107
+ * @param workerName - Name used in error/retry logging.
108
+ * @param srcWorkerData - Combined `{ data, ...otherParams }` payload.
109
+ * @param threadCount - Number of parallel worker threads to spawn.
110
+ * @param isPartitioned - Whether `data` is a pre-split array of shards.
111
+ * @returns An array of promises, one per thread.
112
+ */
21
113
  private createWorkerPromises;
114
+ /**
115
+ * Runs a single worker instance, retrying on failure up to `retryCount`
116
+ * times before re-throwing the last error.
117
+ *
118
+ * Each retry is logged to `console.error` with the remaining attempt count
119
+ * so failures are visible during development.
120
+ *
121
+ * @param instanceConfig - Full configuration for the worker instance.
122
+ * @param retryCount - Remaining retry attempts (default `2`).
123
+ * @returns The successful {@link WorkerResult} once the worker resolves.
124
+ * @throws The last caught error when all retries are exhausted.
125
+ */
22
126
  private runWorkerWithRetry;
127
+ /**
128
+ * Spawns a single worker thread, posts the payload, and resolves or rejects
129
+ * based on the message the worker sends back.
130
+ *
131
+ * The worker is expected to respond with either:
132
+ * - `{ ok: true, data: T }` — success; resolves with a {@link WorkerResult}.
133
+ * - `{ ok: false, error: string }` — logical failure; rejects with a
134
+ * structured error object.
135
+ *
136
+ * Any transferable objects found in the payload are moved (not copied) to
137
+ * the worker via the `transfer` list of `postMessage`.
138
+ *
139
+ * The underlying `Worker` is always terminated after the first message,
140
+ * whether it succeeded or failed.
141
+ *
142
+ * @param instanceConfig - Worker function, name, shard index, and data.
143
+ * @returns A promise that resolves with the worker's result.
144
+ */
23
145
  private initiateWorker;
24
146
  /**
25
- * Collects and merges the settled results from `runWorker` — off the main thread.
147
+ * Collects and merges the settled results from {@link runWorker} — off the
148
+ * main thread.
149
+ *
150
+ * Fulfilled shards are extracted and passed to the `reducer` function, which
151
+ * runs inside a dedicated inline worker so the merge itself never blocks the
152
+ * main thread. Failed shards are counted and their raw rejection reasons are
153
+ * preserved in `errors`.
26
154
  *
27
- * @param settled The `PromiseSettledResult[]` returned by `runWorker`
28
- * @param options Optional `reducer` function (must be self-contained)
155
+ * @typeParam T - The per-shard data type (inferred from `settled`).
156
+ * @typeParam R - The final merged output type (defaults to a flat array of
157
+ * `T` items when no custom reducer is provided).
158
+ *
159
+ * @param settled - The {@link TypedSettledResults} returned by `runWorker`.
160
+ * @param options - Optional {@link CollectOptions}. Supply a `reducer` to
161
+ * control how shards are merged. The reducer **must be self-contained**
162
+ * (no closures over external variables) because it is serialised and run
163
+ * inside a worker.
164
+ *
165
+ * @returns A {@link CollectedResult} with the merged `data`, counts of
166
+ * `succeeded`/`failed` shards, and the raw `errors` array.
29
167
  *
30
168
  * @example
31
169
  * // default: flat array of all shard data
@@ -37,6 +175,28 @@ declare class MainWorkerFactory {
37
175
  * reducer: (shards) => shards.flat().reduce((a, b) => a + b, 0),
38
176
  * });
39
177
  */
40
- collectResults<T = unknown, R = T[]>(settled: PromiseSettledResult<WorkerResult>[], options?: CollectOptions<T, R>): Promise<CollectedResult<R>>;
178
+ collectResults<T = unknown, R = T extends (infer Item)[] ? Item[] : T[]>(settled: TypedSettledResults<T>, options?: CollectOptions<T, R>): Promise<CollectedResult<R>>;
179
+ /**
180
+ * Runs a chain of workers where each step's output feeds directly into the
181
+ * next step — **without passing through the main thread**.
182
+ *
183
+ * Internally, adjacent workers are connected via `MessageChannel` ports.
184
+ * Only the final result is sent back to the main thread, minimising
185
+ * serialisation overhead for large intermediate data.
186
+ *
187
+ * @param steps - An ordered array of pipeline steps. The first step must
188
+ * include `srcData`; subsequent steps receive the previous step's output.
189
+ *
190
+ * @returns A promise that resolves with the final step's output.
191
+ *
192
+ * @example
193
+ * const result = await foreman.pipeline([
194
+ * { worker: 'fetchPosts', srcData: { url: '/api/posts' } },
195
+ * { worker: 'transformPosts' },
196
+ * { worker: 'filterPosts' },
197
+ * ]);
198
+ * console.log(result); // final transformed + filtered data
199
+ */
200
+ pipeline<TResult = unknown>(steps: PipelineStep[]): Promise<TResult>;
41
201
  }
42
202
  export default MainWorkerFactory;
@@ -1,35 +1,128 @@
1
1
  import { WorkerFactory } from '../worker-factory';
2
+ /** A unique string identifier for a worker, matching its `name` field. */
2
3
  export type WorkerName = string;
4
+ /** A descriptive label for the worker's role (e.g. `'compute'`, `'io'`). */
3
5
  export type WorkerRole = string;
4
- export type WorkerFunction = (...params: unknown[]) => void;
5
- export interface WorkerConfig {
6
+ /**
7
+ * The shape of a function that runs inside a Web Worker.
8
+ *
9
+ * Workers receive a single `params` argument posted from the main thread and
10
+ * return (or resolve) a result that is posted back.
11
+ *
12
+ * @typeParam TParams - The type of the message payload sent to the worker.
13
+ * @typeParam TResult - The type of the value the worker posts back.
14
+ */
15
+ export type WorkerFunction<TParams = unknown, TResult = unknown> = (params: TParams) => TResult;
16
+ /**
17
+ * Configuration object that registers a named worker with the factory.
18
+ *
19
+ * @typeParam TFunc - The concrete {@link WorkerFunction} type for this worker.
20
+ */
21
+ export interface WorkerConfig<TFunc extends WorkerFunction = WorkerFunction> {
22
+ /** Unique name used to look up this worker via `runWorker`. */
6
23
  name: WorkerName;
24
+ /** Human-readable role label (e.g. `'compute'`, `'transform'`). */
7
25
  role: WorkerRole;
8
- func: WorkerFunction;
26
+ /** The worker function that will be serialised and run in a thread. */
27
+ func: TFunc;
28
+ /**
29
+ * Maximum number of parallel threads to spawn for this worker.
30
+ * Defaults to `navigator.hardwareConcurrency` when omitted.
31
+ */
9
32
  maxConcurrency?: number;
33
+ /**
34
+ * Number of times a failed worker thread is retried before the shard is
35
+ * marked as rejected. Defaults to `2`.
36
+ */
10
37
  retries?: number;
38
+ /**
39
+ * When `true`, an array `srcData` is split into per-thread shards before
40
+ * being dispatched. Each thread receives one shard instead of the full
41
+ * array.
42
+ */
11
43
  partition?: boolean;
12
44
  }
45
+ /**
46
+ * Derives a `name → function` map from a readonly tuple of
47
+ * {@link WorkerConfig} objects.
48
+ *
49
+ * Used internally to give `runWorker` a fully-typed `workerName` parameter
50
+ * and to infer the correct `srcData` type for each worker.
51
+ *
52
+ * @typeParam T - The readonly tuple of `WorkerConfig` values.
53
+ */
54
+ export type WorkerConfigMap<T extends readonly WorkerConfig<WorkerFunction<any, any>>[]> = {
55
+ [K in T[number]['name']]: Extract<T[number], {
56
+ name: K;
57
+ }>['func'];
58
+ };
59
+ /**
60
+ * Extracts the `params` type from a {@link WorkerFunction}.
61
+ *
62
+ * @typeParam TFunc - The worker function to inspect.
63
+ */
64
+ export type WorkerParams<TFunc extends WorkerFunction> = TFunc extends WorkerFunction<infer P, unknown> ? P : never;
65
+ /**
66
+ * Extracts the `data` field type from a worker function's params.
67
+ * Workers receive `{ data: T, index: number, ...otherParams }` — this
68
+ * pulls out just `T` so callers only need to supply the payload.
69
+ *
70
+ * Always allows `D | D[]` so partitioned workers can receive an array
71
+ * that the framework splits into per-shard items.
72
+ */
73
+ export type WorkerDataParam<TFunc extends WorkerFunction> = WorkerParams<TFunc> extends {
74
+ data: infer D;
75
+ } ? D extends (infer Item)[] ? Item[] : D | D[] : WorkerParams<TFunc>;
76
+ /** Extracts the return type from a {@link WorkerFunction}, unwrapping `Promise<T>` → `T`. */
77
+ export type WorkerReturnType<TFunc extends WorkerFunction> = TFunc extends WorkerFunction<any, infer R> ? R extends Promise<infer Resolved> ? Resolved : R : never;
78
+ /** Options passed to the `MainWorkerFactory` constructor. */
13
79
  export interface MainWorkerFactoryOptions {
14
80
  workers: WorkerConfig[];
15
81
  }
82
+ /** Internal representation of a worker config that has been instantiated. */
16
83
  export interface MainWorkerFactoryWorker extends WorkerConfig {
17
84
  worker: WorkerFactory;
18
85
  }
19
- export interface WorkerInstanceConfig {
86
+ /**
87
+ * Runtime configuration for a single worker thread instance.
88
+ *
89
+ * @typeParam TFunc - The worker function type for this instance.
90
+ */
91
+ export interface WorkerInstanceConfig<TFunc extends WorkerFunction = WorkerFunction> {
92
+ /** Name of the parent worker config, used in logs and error objects. */
20
93
  workerName: WorkerName;
21
- workerFunc: WorkerFunction;
94
+ /** The function serialised and executed inside the thread. */
95
+ workerFunc: TFunc;
96
+ /** Zero-based shard index assigned to this thread. */
22
97
  index: number;
98
+ /** The data payload (full or partitioned shard) sent to the thread. */
23
99
  data: unknown;
24
100
  }
101
+ /** The raw `MessageEvent` received when a worker thread succeeds. */
25
102
  export type WorkerSuccessResult = MessageEvent;
103
+ /** The raw `MessageEvent` (or `ErrorEvent`) received when a worker thread fails. */
26
104
  export type WorkerFailedResult = MessageEvent;
105
+ /** Structured result returned by a single worker thread, whether it succeeded or failed. */
27
106
  export interface WorkerResult {
107
+ /** Zero-based shard index of the thread that produced this result. */
28
108
  index: number;
109
+ /** The full instance config used to spawn this thread. */
29
110
  workerConfigs: WorkerInstanceConfig;
111
+ /** Present when the thread resolved successfully. */
30
112
  successResult?: WorkerSuccessResult;
113
+ /** Present when the thread rejected or posted `{ ok: false }`. */
31
114
  failedResult?: WorkerFailedResult;
32
115
  }
116
+ /**
117
+ * Typed wrapper around the settled results from `runWorker`.
118
+ * Carries `T` (the worker's return type) so `collectResults` can infer it.
119
+ */
120
+ export declare class TypedSettledResults<T> {
121
+ readonly results: PromiseSettledResult<WorkerResult>[];
122
+ constructor(results: PromiseSettledResult<WorkerResult>[]);
123
+ /** Never actually exists at runtime — used only for type inference. */
124
+ readonly __type: T;
125
+ }
33
126
  /** Options for collectResults */
34
127
  export interface CollectOptions<T, R = T[]> {
35
128
  /**
@@ -54,3 +147,10 @@ export interface CollectedResult<R> {
54
147
  /** Raw rejected results, if any */
55
148
  errors: PromiseRejectedResult[];
56
149
  }
150
+ /** A single step in a worker pipeline */
151
+ export interface PipelineStep {
152
+ /** Name of the registered worker to run */
153
+ worker: string;
154
+ /** Input data for the first step (subsequent steps receive previous output) */
155
+ srcData?: unknown;
156
+ }
@@ -1,7 +1,40 @@
1
1
  import { WorkerFunction } from '../main-worker-factory/types';
2
+ export interface WorkerFactoryOptions {
3
+ /** When true, generates a pipeline-aware worker that supports MessagePort forwarding */
4
+ pipeline?: boolean;
5
+ }
6
+ /**
7
+ * Low-level factory that serialises a {@link WorkerFunction} into a Blob URL
8
+ * and spawns a native `Worker` from it.
9
+ *
10
+ * `WorkerFactory` is an internal building block used by `MainWorkerFactory`.
11
+ * It handles the mechanics of turning a plain TypeScript function into a
12
+ * runnable worker thread — you rarely need to use it directly.
13
+ *
14
+ * The worker script is generated by {@link workerTemplate}, which wraps the
15
+ * function with a message listener and transferable-extraction logic.
16
+ */
2
17
  declare class WorkerFactory {
3
18
  readonly _worker: Worker;
4
- constructor(workerFunction: WorkerFunction);
19
+ /**
20
+ * Creates a new `Worker` from the given function.
21
+ *
22
+ * The function is stringified, embedded into a self-contained worker script,
23
+ * converted to a `Blob` URL, and passed to the `Worker` constructor.
24
+ *
25
+ * @param workerFunction - The function to run inside the worker thread.
26
+ * Must be self-contained — it cannot reference variables from the outer
27
+ * scope because it is serialised via `.toString()`.
28
+ * @param options - Optional configuration. Set `pipeline: true` for
29
+ * pipeline-aware workers that support MessagePort forwarding.
30
+ */
31
+ constructor(workerFunction: WorkerFunction, options?: WorkerFactoryOptions);
32
+ /**
33
+ * Returns the underlying native `Worker` instance.
34
+ *
35
+ * Use this to attach `onmessage` / `onerror` handlers and call
36
+ * `postMessage` / `terminate` directly.
37
+ */
5
38
  get getWorker(): Worker;
6
39
  }
7
40
  export default WorkerFactory;
@@ -1,2 +1,18 @@
1
+ /**o with
2
+ * Default initiator — a minimal echo worker used as a no-op placeholder.
3
+ *
4
+ * When no custom initiator is passed to `MainWorkerFactory`, this function
5
+ * is used. It simply reflects every incoming message back to the sender,
6
+ * which is useful for testing the messaging pipeline without any real
7
+ * computation.
8
+ *
9
+ * @example
10
+ * // Automatically used as the default:
11
+ * new MainWorkerFactory({ workers: [...] });
12
+ *
13
+ * // Equivalent explicit usage:
14
+ * import defaultInitiator from './initiator';
15
+ * new MainWorkerFactory({ workers: [...] }, defaultInitiator);
16
+ */
1
17
  declare const _default: () => void;
2
18
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@offmain/workerkit",
3
- "version": "0.8.9",
3
+ "version": "0.10.0",
4
4
  "description": "A lightweight manager for running functions in Web Workers with partitioning, retries, and concurrency control",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -28,11 +28,11 @@
28
28
  "lint": "eslint --fix .",
29
29
  "test": "vitest run",
30
30
  "test:watch": "vitest",
31
- "release:patch": "npm version patch && npm run release:publish",
32
- "release:minor": "npm version minor && npm run release:publish",
33
- "release:major": "npm version major && npm run release:publish",
34
- "release:publish": "npm run build:lib && npm publish",
35
- "prepublishOnly": "npm run build:lib",
31
+ "release": "release-it --ci",
32
+ "release:patch": "release-it --increment patch --ci",
33
+ "release:minor": "release-it --increment minor --ci",
34
+ "release:major": "release-it --increment major --ci",
35
+ "release:dry": "release-it --dry-run",
36
36
  "lint-staged": "lint-staged",
37
37
  "prepare": "husky"
38
38
  },
@@ -59,6 +59,7 @@
59
59
  ]
60
60
  },
61
61
  "devDependencies": {
62
+ "@release-it/conventional-changelog": "11.0.0",
62
63
  "@typescript-eslint/eslint-plugin": "^8.58.2",
63
64
  "@typescript-eslint/parser": "^8.58.2",
64
65
  "date-fns": "^4.1.0",
@@ -71,6 +72,7 @@
71
72
  "jsdom": "^24.0.0",
72
73
  "lint-staged": "^16.4.0",
73
74
  "prettier": "^3.4.2",
75
+ "release-it": "19.0.3",
74
76
  "typescript": "^5.7.2",
75
77
  "vite": "^6.3.3",
76
78
  "vite-plugin-dts": "^4.5.4",
@@ -1 +0,0 @@
1
- export {};