@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.
- package/README.md +83 -19
- package/dist/index.cjs +83 -8
- package/dist/index.js +395 -89
- package/dist/types/tools/main-worker-factory/index.d.ts +1 -1
- package/dist/types/tools/main-worker-factory/main-worker-factory.d.ts +171 -11
- package/dist/types/tools/main-worker-factory/types.d.ts +105 -5
- package/dist/types/tools/worker-factory/worker-factory.d.ts +34 -1
- package/dist/types/workers/initiator.d.ts +16 -0
- package/package.json +8 -6
- package/dist/types/workers/initiator.test.d.ts +0 -1
package/README.md
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight TypeScript library for running functions in Web Workers with support for partitioning, retries, and concurrency control — all without the boilerplate.
|
|
4
4
|
|
|
5
|
-
Instead of manually creating worker scripts and wiring up `postMessage` / `onmessage`, you write plain exported functions and hand them to `MainWorkerFactory`. The library
|
|
5
|
+
Instead of manually creating worker scripts and wiring up `postMessage` / `onmessage`, you write plain exported functions and hand them to `MainWorkerFactory`. The library serializes them into Blob workers, manages threads per function, handles retries on failure, and merges results back on the main thread.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install workerkit
|
|
12
|
+
npm install @offmain/workerkit
|
|
13
13
|
# or
|
|
14
|
-
pnpm add workerkit
|
|
14
|
+
pnpm add @offmain/workerkit
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
---
|
|
@@ -32,10 +32,10 @@ export function sum({ data }: { data: number[] }): number {
|
|
|
32
32
|
### 2. Register and run it
|
|
33
33
|
|
|
34
34
|
```ts
|
|
35
|
-
import { MainWorkerFactory } from 'workerkit';
|
|
35
|
+
import { MainWorkerFactory } from '@offmain/workerkit';
|
|
36
36
|
import { sum } from './sum.worker.ts';
|
|
37
37
|
|
|
38
|
-
const factory = new MainWorkerFactory(
|
|
38
|
+
const factory = new MainWorkerFactory({
|
|
39
39
|
workers: [
|
|
40
40
|
{
|
|
41
41
|
name: 'sum',
|
|
@@ -47,10 +47,10 @@ const factory = new MainWorkerFactory(initiator, {
|
|
|
47
47
|
],
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
const { data } = await factory.collectResults(
|
|
50
|
+
const settled = await factory.runWorker('sum', { srcData: [1, 2, 3, 4, 5] });
|
|
51
|
+
const { data } = await factory.collectResults(settled);
|
|
52
52
|
|
|
53
|
-
console.log(data); // 15
|
|
53
|
+
console.log(data); // [15]
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
---
|
|
@@ -63,7 +63,7 @@ console.log(data); // 15
|
|
|
63
63
|
| `role` | `string` | — | Logical grouping label |
|
|
64
64
|
| `func` | `Function` | — | The exported worker function to run |
|
|
65
65
|
| `maxConcurrency` | `number` | `navigator.hardwareConcurrency` | Max parallel worker instances — defaults to the number of logical CPU cores reported by the browser |
|
|
66
|
-
| `retries` | `number` | `0` | How many times to retry a failed shard
|
|
66
|
+
| `retries` | `number` | `0` | How many times to retry a failed shard before marking it as rejected |
|
|
67
67
|
| `partition` | `boolean` | `false` | Split array input across multiple workers automatically |
|
|
68
68
|
|
|
69
69
|
---
|
|
@@ -73,21 +73,85 @@ console.log(data); // 15
|
|
|
73
73
|
When `partition: true`, an array passed as `srcData` is automatically split across worker instances and results are merged back.
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
|
-
const
|
|
76
|
+
const settled = await factory.runWorker('sum', {
|
|
77
77
|
srcData: largeArray, // split across workers
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
const { data, succeeded, failed } = await factory.collectResults(
|
|
80
|
+
const { data, succeeded, failed } = await factory.collectResults(settled);
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
You can also provide a custom reducer to control how shard results are merged:
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
|
-
const { data } = await factory.collectResults(
|
|
86
|
+
const { data } = await factory.collectResults(settled, {
|
|
87
87
|
reducer: (shards) => shards.flat().sort((a, b) => b.score - a.score),
|
|
88
88
|
});
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
> **Note:** The reducer runs inside a worker thread and must be self-contained — it cannot reference variables from the outer scope.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Pipeline
|
|
96
|
+
|
|
97
|
+
Chain multiple workers together so data flows directly between them via `MessageChannel` — without passing through the main thread between steps.
|
|
98
|
+
|
|
99
|
+
### Why use a pipeline?
|
|
100
|
+
|
|
101
|
+
In a traditional multi-step workflow, intermediate data is serialized back to the main thread after each step:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
Main → Worker A → Main → Worker B → Main → Worker C → Main
|
|
105
|
+
↑ serialize ↑ serialize ↑ serialize
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
With large datasets (100k+ records), each serialization round-trip adds significant overhead — both in time and memory pressure on the main thread. The pipeline eliminates this:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
Main → Worker A → Worker B → Worker C → Main
|
|
112
|
+
↑ MessageChannel ↑ only final result
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Only the final result crosses back to the main thread. If your pipeline generates 20 MB of intermediate data but produces a 1 KB summary, you save ~40 MB of serialization (two round-trips avoided).
|
|
116
|
+
|
|
117
|
+
### Usage
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { MainWorkerFactory } from '@offmain/workerkit';
|
|
121
|
+
import { fetchData, transform, aggregate } from './workers.ts';
|
|
122
|
+
|
|
123
|
+
const factory = new MainWorkerFactory({
|
|
124
|
+
workers: [
|
|
125
|
+
{ name: 'fetchData', role: 'io', func: fetchData },
|
|
126
|
+
{ name: 'transform', role: 'compute', func: transform },
|
|
127
|
+
{ name: 'aggregate', role: 'compute', func: aggregate },
|
|
128
|
+
] as const,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = await factory.pipeline<AggregateResult>([
|
|
132
|
+
{ worker: 'fetchData', srcData: { url: '/api/records' } },
|
|
133
|
+
{ worker: 'transform' }, // receives fetchData output directly
|
|
134
|
+
{ worker: 'aggregate' }, // receives transform output directly
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
console.log(result); // only this small result crossed to main thread
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### How each step receives data
|
|
141
|
+
|
|
142
|
+
- The first step receives `srcData` as `{ data: srcData, index: 0 }` — same as `runWorker`.
|
|
143
|
+
- Each subsequent step receives the previous step's output as `{ data: previousOutput, index: 0 }`.
|
|
144
|
+
- Worker functions don't need any special handling — they use the same `{ data }` parameter signature as regular workers.
|
|
145
|
+
|
|
146
|
+
### When to use pipeline vs runWorker
|
|
147
|
+
|
|
148
|
+
| Scenario | Use |
|
|
149
|
+
| ---------------------------------------------------- | ----------------------- |
|
|
150
|
+
| Single step, or steps that need partitioning/retries | `runWorker` |
|
|
151
|
+
| Multi-step chain where intermediate data is large | `pipeline` |
|
|
152
|
+
| Steps that are independent (not sequential) | `runWorker` in parallel |
|
|
153
|
+
| Steps where only the final result matters to the UI | `pipeline` |
|
|
154
|
+
|
|
91
155
|
---
|
|
92
156
|
|
|
93
157
|
## ESLint Plugin
|
|
@@ -98,7 +162,7 @@ The package ships with two ESLint rules to catch common worker mistakes at lint
|
|
|
98
162
|
|
|
99
163
|
```js
|
|
100
164
|
// eslint.config.js
|
|
101
|
-
import workerPlugin from 'workerkit/eslint-plugin';
|
|
165
|
+
import workerPlugin from '@offmain/workerkit/eslint-plugin';
|
|
102
166
|
|
|
103
167
|
export default [...workerPlugin.configs.recommended];
|
|
104
168
|
```
|
|
@@ -113,16 +177,16 @@ Flags usage of browser main-thread-only APIs that are unavailable inside Web Wor
|
|
|
113
177
|
|
|
114
178
|
```ts
|
|
115
179
|
// sum.worker.ts ❌ — will be flagged
|
|
116
|
-
export function sum({ data }:
|
|
180
|
+
export function sum({ data }: { data: number[] }) {
|
|
117
181
|
document.title = 'working...'; // Error: 'document' is not available inside Web Workers
|
|
118
|
-
return data.reduce((a
|
|
182
|
+
return data.reduce((a, b) => a + b, 0);
|
|
119
183
|
}
|
|
120
184
|
```
|
|
121
185
|
|
|
122
186
|
```ts
|
|
123
187
|
// sum.worker.ts ✅
|
|
124
|
-
export function sum({ data }:
|
|
125
|
-
return data.reduce((a
|
|
188
|
+
export function sum({ data }: { data: number[] }) {
|
|
189
|
+
return data.reduce((a, b) => a + b, 0);
|
|
126
190
|
}
|
|
127
191
|
```
|
|
128
192
|
|
|
@@ -150,8 +214,8 @@ You can also import rules individually if you don't want the full recommended co
|
|
|
150
214
|
|
|
151
215
|
```js
|
|
152
216
|
// eslint.config.js
|
|
153
|
-
import noDomInWorker from 'workerkit/eslint-rules/no-dom-in-worker';
|
|
154
|
-
import workerExportable from 'workerkit/eslint-rules/worker-exportable';
|
|
217
|
+
import noDomInWorker from '@offmain/workerkit/eslint-rules/no-dom-in-worker';
|
|
218
|
+
import workerExportable from '@offmain/workerkit/eslint-rules/worker-exportable';
|
|
155
219
|
|
|
156
220
|
export default [
|
|
157
221
|
{
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var w=Object.defineProperty;var y=(o,e,t)=>e in o?w(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var h=(o,e,t)=>y(o,typeof e!="symbol"?e+"":e,t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const m=o=>`
|
|
2
2
|
const extractTransferables = (value, seen = new Set()) => {
|
|
3
3
|
if (value === null || typeof value !== 'object') return [];
|
|
4
4
|
if (seen.has(value)) return [];
|
|
@@ -14,12 +14,87 @@ const extractTransferables = (value, seen = new Set()) => {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
self.addEventListener('message', async (event) => {
|
|
17
|
-
|
|
18
|
-
const output = await ${
|
|
19
|
-
self.postMessage(output, extractTransferables(output));
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
try {
|
|
18
|
+
const output = await ${o}(event.data);
|
|
19
|
+
self.postMessage({ ok: true, data: output }, extractTransferables(output));
|
|
20
|
+
} catch (err) {
|
|
21
|
+
self.postMessage({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
`,v=o=>`
|
|
25
|
+
const extractTransferables = (value, seen = new Set()) => {
|
|
26
|
+
if (value === null || typeof value !== 'object') return [];
|
|
27
|
+
if (seen.has(value)) return [];
|
|
28
|
+
seen.add(value);
|
|
29
|
+
if (value instanceof ArrayBuffer || value instanceof MessagePort ||
|
|
30
|
+
(typeof ImageBitmap !== 'undefined' && value instanceof ImageBitmap) ||
|
|
31
|
+
(typeof OffscreenCanvas !== 'undefined' && value instanceof OffscreenCanvas)) {
|
|
32
|
+
return [value];
|
|
33
|
+
}
|
|
34
|
+
if (ArrayBuffer.isView(value)) return [value.buffer];
|
|
35
|
+
if (Array.isArray(value)) return value.flatMap(i => extractTransferables(i, seen));
|
|
36
|
+
return Object.values(value).flatMap(v => extractTransferables(v, seen));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const workerFn = ${o};
|
|
40
|
+
let outputPort = null;
|
|
41
|
+
let inputPort = null;
|
|
42
|
+
let pendingData = null;
|
|
43
|
+
|
|
44
|
+
async function processData(data) {
|
|
45
|
+
try {
|
|
46
|
+
const output = await workerFn(data);
|
|
47
|
+
const result = { ok: true, data: output };
|
|
48
|
+
const transfers = extractTransferables(output);
|
|
49
|
+
if (outputPort) {
|
|
50
|
+
outputPort.postMessage(result, transfers);
|
|
51
|
+
} else {
|
|
52
|
+
self.postMessage(result, transfers);
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const result = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
56
|
+
if (outputPort) {
|
|
57
|
+
outputPort.postMessage(result);
|
|
58
|
+
} else {
|
|
59
|
+
self.postMessage(result);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
self.addEventListener('message', (event) => {
|
|
65
|
+
if (event.data && event.data.__pipeline_ports__) {
|
|
66
|
+
if (event.data.outputPort) {
|
|
67
|
+
outputPort = event.data.outputPort;
|
|
68
|
+
}
|
|
69
|
+
if (event.data.inputPort) {
|
|
70
|
+
inputPort = event.data.inputPort;
|
|
71
|
+
inputPort.onmessage = (e) => {
|
|
72
|
+
if (e.data && e.data.ok === false) {
|
|
73
|
+
// Propagate errors through the pipeline
|
|
74
|
+
if (outputPort) outputPort.postMessage(e.data);
|
|
75
|
+
else self.postMessage(e.data);
|
|
76
|
+
} else {
|
|
77
|
+
processData({ data: e.data.data, index: 0 });
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// If we already received data before ports, process it now
|
|
82
|
+
if (pendingData !== null) {
|
|
83
|
+
processData(pendingData);
|
|
84
|
+
pendingData = null;
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// First worker in pipeline or standalone — process directly
|
|
89
|
+
if (!inputPort) {
|
|
90
|
+
processData(event.data);
|
|
91
|
+
} else {
|
|
92
|
+
// Store data until ports are configured
|
|
93
|
+
pendingData = event.data;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
`;class k{constructor(e,t){h(this,"_worker");const a=(t!=null&&t.pipeline?v:m)(e.toString()),u=new Blob([a],{type:"application/javascript"});this._worker=new Worker(URL.createObjectURL(u))}get getWorker(){return this._worker}}class P{constructor(e){this.results=e}}function g(o,e=new Set){return o===null||typeof o!="object"?[]:e.has(o)?[]:(e.add(o),o instanceof ArrayBuffer||o instanceof MessagePort||typeof ImageBitmap<"u"&&o instanceof ImageBitmap||typeof OffscreenCanvas<"u"&&o instanceof OffscreenCanvas?[o]:ArrayBuffer.isView(o)?[o.buffer]:Array.isArray(o)?o.flatMap(t=>g(t,e)):Object.values(o).flatMap(t=>g(t,e)))}class W{constructor(e){h(this,"_workers");h(this,"_threads");this._workers=e.workers,this._threads=navigator.hardwareConcurrency}initWorker(e){return new k(e)}partitionArray(e,t){if(!e.length)return[];if(t<=0)throw new Error("numChunks must be positive");const n=Math.min(t,e.length),a=Math.floor(e.length/n),u=e.length%n,l=[];let f=0;for(let r=0;r<n;r++){const s=a+(r<u?1:0);l.push(e.slice(f,f+s)),f+=s}return l}findWorkerByName(e){return this._workers.find(t=>t.name===e)}async runWorker(e,{srcData:t,...n}){const a=this.findWorkerByName(e);if(!a)return Promise.reject(new Error(`Worker "${e}" not found`));const u=a.maxConcurrency??this._threads,l=!!(Array.isArray(t)&&t.length>1&&a.partition),f=l?this.partitionArray(t,u):t,r=this.createWorkerPromises(a,e,{data:f,...n},u,l),s=await Promise.allSettled(r);return new P(s)}createWorkerPromises(e,t,n,a,u){const{data:l,...f}=n;return Array.from({length:a},(r,s)=>{const i=u&&Array.isArray(l)?l[s]:l;return this.runWorkerWithRetry({workerFunc:e.func,workerName:t,index:s,data:{data:i,...f}},e.retries)})}async runWorkerWithRetry(e,t=2){try{return await this.initiateWorker(e)}catch(n){if(t>0)return console.error(`Worker ${e.index} failed, retrying (${t} left):`,n),this.runWorkerWithRetry(e,t-1);throw console.error("Worker failed after all retries:",n),n}}initiateWorker({workerFunc:e,workerName:t,index:n,data:a}){return new Promise((u,l)=>{const r=this.initWorker(e).getWorker;r.onerror=i=>{r.terminate(),l({index:n,workerConfigs:{workerFunc:e,workerName:t,index:n,data:a},failedResult:i})},r.onmessage=i=>{var c,p;if(((c=i.data)==null?void 0:c.ok)===!1){r.terminate(),l({index:n,workerConfigs:{workerFunc:e,workerName:t,index:n,data:a},failedResult:new ErrorEvent("error",{message:i.data.error})});return}u({index:n,workerConfigs:{workerFunc:e,workerName:t,index:n,data:a},successResult:new MessageEvent("message",{data:(p=i.data)==null?void 0:p.data})}),r.terminate()};const s={index:n,...Array.isArray(a)?{data:a}:a};r.postMessage(s,g(s))})}async collectResults(e,t={}){const n=e.results.filter(r=>r.status==="fulfilled"),a=e.results.filter(r=>r.status==="rejected"),u=n.map(r=>r.value.successResult.data),l=t.reducer?t.reducer.toString():"(shards) => shards.flat()";return{data:await new Promise((r,s)=>{const i=`
|
|
97
|
+
const reducer = ${l};
|
|
23
98
|
self.addEventListener('message', (event) => {
|
|
24
99
|
try {
|
|
25
100
|
const result = reducer(event.data);
|
|
@@ -28,4 +103,4 @@ self.addEventListener('message', async (event) => {
|
|
|
28
103
|
self.postMessage({ ok: false, error: String(err) });
|
|
29
104
|
}
|
|
30
105
|
});
|
|
31
|
-
`,
|
|
106
|
+
`,c=new Blob([i],{type:"application/javascript"}),p=new Worker(URL.createObjectURL(c));p.onmessage=d=>{p.terminate(),d.data.ok?r(d.data.data):s(new Error(d.data.error))},p.onerror=d=>{p.terminate(),s(d)},p.postMessage(u)}),succeeded:n.length,failed:a.length,errors:a}}async pipeline(e){if(e.length===0)throw new Error("Pipeline requires at least one step");if(e.length===1){const t=e[0],n=this.findWorkerByName(t.worker);if(!n)throw new Error(`Worker "${t.worker}" not found`);const u=this.initWorker(n.func).getWorker;return new Promise((l,f)=>{u.onmessage=s=>{var i,c;u.terminate(),((i=s.data)==null?void 0:i.ok)===!1?f(new Error(s.data.error)):l((c=s.data)==null?void 0:c.data)},u.onerror=s=>{u.terminate(),f(s)};const r=t.srcData??{};u.postMessage({data:r,index:0},g(r))})}return new Promise((t,n)=>{const a=[],u=[];for(const r of e){const s=this.findWorkerByName(r.worker);if(!s){n(new Error(`Worker "${r.worker}" not found`));return}const i=new k(s.func,{pipeline:!0});a.push(i.getWorker)}for(let r=0;r<a.length-1;r++)u.push(new MessageChannel);for(let r=0;r<a.length;r++){const s=[],i={};r>0&&(i.inputPort=u[r-1].port1,s.push(i.inputPort)),r<a.length-1&&(i.outputPort=u[r].port2,s.push(i.outputPort)),a[r].postMessage({__pipeline_ports__:!0,...i},s)}const l=a[a.length-1];l.onmessage=r=>{var s,i;a.forEach(c=>c.terminate()),((s=r.data)==null?void 0:s.ok)===!1?n(new Error(r.data.error)):t((i=r.data)==null?void 0:i.data)},l.onerror=r=>{a.forEach(s=>s.terminate()),n(r)};const f=e[0].srcData??{};a[0].postMessage({data:f,index:0},g(f))})}}exports.MainWorkerFactory=W;exports.WorkerFactory=k;
|