@offmain/workerkit 0.9.2 → 0.11.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 +166 -0
- package/dist/index.cjs +133 -5
- package/dist/index.js +385 -98
- package/dist/types/tools/main-worker-factory/index.d.ts +1 -1
- package/dist/types/tools/main-worker-factory/main-worker-factory.d.ts +85 -2
- package/dist/types/tools/main-worker-factory/types.d.ts +7 -0
- package/dist/types/tools/worker-factory/worker-factory.d.ts +24 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,172 @@ const { data } = await factory.collectResults(settled, {
|
|
|
92
92
|
|
|
93
93
|
---
|
|
94
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
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Persistent Workers
|
|
158
|
+
|
|
159
|
+
Keep a worker alive with a cached dataset, then re-run it with different configs without re-sending the data.
|
|
160
|
+
|
|
161
|
+
### Why use persistent workers?
|
|
162
|
+
|
|
163
|
+
In a typical workflow where you apply multiple transformations to the same dataset, the standard `runWorker` approach re-serializes the entire dataset on every call:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
Call 1: Main ──[200k items]──→ Worker → Main
|
|
167
|
+
Call 2: Main ──[200k items]──→ Worker → Main ← same data, different config
|
|
168
|
+
Call 3: Main ──[200k items]──→ Worker → Main ← same data again
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
With 5 config variations on a 1.6 MB dataset, that's ~8 MB of redundant serialization. Persistent workers eliminate this by caching the dataset inside the worker:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
Call 1: Main ──[200k items + config]──→ Worker → Main ← dataset cached
|
|
175
|
+
Call 2: Main ──[config only]──────────→ Worker → Main ← reuses cache
|
|
176
|
+
Call 3: Main ──[config only]──────────→ Worker → Main ← reuses cache
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Only the first call transfers the dataset. Subsequent calls send just the config object (typically a few bytes), saving both serialization time and memory pressure.
|
|
180
|
+
|
|
181
|
+
### Usage
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { MainWorkerFactory } from '@offmain/workerkit';
|
|
185
|
+
import { transformArray } from './transform.worker.ts';
|
|
186
|
+
|
|
187
|
+
const factory = new MainWorkerFactory({
|
|
188
|
+
workers: [
|
|
189
|
+
{ name: 'transform', role: 'computation', func: transformArray },
|
|
190
|
+
] as const,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// First call: send dataset + config (dataset gets cached in worker memory)
|
|
194
|
+
const r1 = await factory.runPersistent('transform', {
|
|
195
|
+
dataset: largeArray,
|
|
196
|
+
config: { multiplier: 2, filter: 'even' },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Subsequent calls: only config — dataset is reused from cache
|
|
200
|
+
const r2 = await factory.runPersistent('transform', {
|
|
201
|
+
config: { multiplier: 5, filter: 'odd' },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const r3 = await factory.runPersistent('transform', {
|
|
205
|
+
config: { multiplier: 1, filter: 'none', limit: 1000 },
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Update the dataset when it changes
|
|
209
|
+
const r4 = await factory.runPersistent('transform', {
|
|
210
|
+
dataset: newArray, // replaces cached dataset
|
|
211
|
+
config: { multiplier: 3, filter: 'even' },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Release the worker when done — frees memory
|
|
215
|
+
factory.release('transform');
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### How the worker function receives data
|
|
219
|
+
|
|
220
|
+
The worker function signature stays the same as a regular worker — it receives `{ data, config }`:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
// transform.worker.ts
|
|
224
|
+
export function transformArray({
|
|
225
|
+
data,
|
|
226
|
+
config,
|
|
227
|
+
}: {
|
|
228
|
+
data: number[];
|
|
229
|
+
config: { multiplier: number; filter: string };
|
|
230
|
+
}) {
|
|
231
|
+
return data
|
|
232
|
+
.filter((n) => /* apply filter */)
|
|
233
|
+
.map((n) => n * config.multiplier);
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The framework handles the caching transparently — your function always receives the full `data` (from cache or freshly provided) plus the current `config`.
|
|
238
|
+
|
|
239
|
+
### When to use persistent vs runWorker
|
|
240
|
+
|
|
241
|
+
| Scenario | Use |
|
|
242
|
+
| ------------------------------------------------------ | --------------- |
|
|
243
|
+
| One-off computation | `runWorker` |
|
|
244
|
+
| Same dataset, multiple config variations | `runPersistent` |
|
|
245
|
+
| Interactive UI where user tweaks params on static data | `runPersistent` |
|
|
246
|
+
| Dataset changes frequently | `runWorker` |
|
|
247
|
+
| Need partitioning across multiple threads | `runWorker` |
|
|
248
|
+
|
|
249
|
+
### Memory management
|
|
250
|
+
|
|
251
|
+
The cached dataset lives in worker memory until `release()` is called. For large datasets, always call `release()` when you're done to free the memory:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
factory.release('transform');
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
After releasing, the next `runPersistent` call will create a fresh worker instance (requiring a new dataset).
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
95
261
|
## ESLint Plugin
|
|
96
262
|
|
|
97
263
|
The package ships with two ESLint rules to catch common worker mistakes at lint time.
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";var y=Object.defineProperty;var m=(
|
|
1
|
+
"use strict";var y=Object.defineProperty;var m=(s,e,t)=>e in s?y(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t;var h=(s,e,t)=>m(s,typeof e!="symbol"?e+"":e,t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const w=s=>`
|
|
2
2
|
const extractTransferables = (value, seen = new Set()) => {
|
|
3
3
|
if (value === null || typeof value !== 'object') return [];
|
|
4
4
|
if (seen.has(value)) return [];
|
|
@@ -15,14 +15,142 @@ const extractTransferables = (value, seen = new Set()) => {
|
|
|
15
15
|
|
|
16
16
|
self.addEventListener('message', async (event) => {
|
|
17
17
|
try {
|
|
18
|
-
const output = await ${
|
|
18
|
+
const output = await ${s}(event.data);
|
|
19
19
|
self.postMessage({ ok: true, data: output }, extractTransferables(output));
|
|
20
20
|
} catch (err) {
|
|
21
21
|
self.postMessage({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
22
22
|
}
|
|
23
23
|
})
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
`,P=s=>`
|
|
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 = ${s};
|
|
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
|
+
`,M=s=>`
|
|
97
|
+
const extractTransferables = (value, seen = new Set()) => {
|
|
98
|
+
if (value === null || typeof value !== 'object') return [];
|
|
99
|
+
if (seen.has(value)) return [];
|
|
100
|
+
seen.add(value);
|
|
101
|
+
if (value instanceof ArrayBuffer || value instanceof MessagePort ||
|
|
102
|
+
(typeof ImageBitmap !== 'undefined' && value instanceof ImageBitmap) ||
|
|
103
|
+
(typeof OffscreenCanvas !== 'undefined' && value instanceof OffscreenCanvas)) {
|
|
104
|
+
return [value];
|
|
105
|
+
}
|
|
106
|
+
if (ArrayBuffer.isView(value)) return [value.buffer];
|
|
107
|
+
if (Array.isArray(value)) return value.flatMap(i => extractTransferables(i, seen));
|
|
108
|
+
return Object.values(value).flatMap(v => extractTransferables(v, seen));
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const workerFn = ${s};
|
|
112
|
+
let cachedDataset = null;
|
|
113
|
+
|
|
114
|
+
self.addEventListener('message', async (event) => {
|
|
115
|
+
const msg = event.data;
|
|
116
|
+
|
|
117
|
+
if (msg && msg.type === 'release') {
|
|
118
|
+
cachedDataset = null;
|
|
119
|
+
self.postMessage({ ok: true, data: null, type: 'released' });
|
|
120
|
+
self.close();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (msg && msg.type === 'run') {
|
|
125
|
+
// Update cache if new dataset provided
|
|
126
|
+
if (msg.dataset !== undefined) {
|
|
127
|
+
cachedDataset = msg.dataset;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (cachedDataset === null) {
|
|
131
|
+
self.postMessage({ ok: false, error: 'No dataset cached. Provide a dataset on the first call.' });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const output = await workerFn({ data: cachedDataset, config: msg.config });
|
|
137
|
+
self.postMessage({ ok: true, data: output }, extractTransferables(output));
|
|
138
|
+
} catch (err) {
|
|
139
|
+
self.postMessage({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fallback: treat as a regular one-shot call for backwards compat
|
|
145
|
+
try {
|
|
146
|
+
const output = await workerFn(msg);
|
|
147
|
+
self.postMessage({ ok: true, data: output }, extractTransferables(output));
|
|
148
|
+
} catch (err) {
|
|
149
|
+
self.postMessage({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
`;var v=(s=>(s.Default="default",s.Pipeline="pipeline",s.Persistent="persistent",s))(v||{});const W=Object.freeze({persistent:M,pipeline:P,default:w});class k{constructor(e,t){h(this,"_worker");const n=(t==null?void 0:t.mode)??"default",a=W[n](e.toString()),u=new Blob([a],{type:"application/javascript"});this._worker=new Worker(URL.createObjectURL(u))}get getWorker(){return this._worker}}class b{constructor(e){this.results=e}}function p(s,e=new Set){return s===null||typeof s!="object"?[]:e.has(s)?[]:(e.add(s),s instanceof ArrayBuffer||s instanceof MessagePort||typeof ImageBitmap<"u"&&s instanceof ImageBitmap||typeof OffscreenCanvas<"u"&&s instanceof OffscreenCanvas?[s]:ArrayBuffer.isView(s)?[s.buffer]:Array.isArray(s)?s.flatMap(t=>p(t,e)):Object.values(s).flatMap(t=>p(t,e)))}class A{constructor(e){h(this,"_workers");h(this,"_threads");h(this,"_persistentWorkers",new Map);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,f=[];let l=0;for(let r=0;r<n;r++){const o=a+(r<u?1:0);f.push(e.slice(l,l+o)),l+=o}return f}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,f=!!(Array.isArray(t)&&t.length>1&&a.partition),l=f?this.partitionArray(t,u):t,r=this.createWorkerPromises(a,e,{data:l,...n},u,f),o=await Promise.allSettled(r);return new b(o)}createWorkerPromises(e,t,n,a,u){const{data:f,...l}=n;return Array.from({length:a},(r,o)=>{const i=u&&Array.isArray(f)?f[o]:f;return this.runWorkerWithRetry({workerFunc:e.func,workerName:t,index:o,data:{data:i,...l}},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,f)=>{const r=this.initWorker(e).getWorker;r.onerror=i=>{r.terminate(),f({index:n,workerConfigs:{workerFunc:e,workerName:t,index:n,data:a},failedResult:i})},r.onmessage=i=>{var c,d;if(((c=i.data)==null?void 0:c.ok)===!1){r.terminate(),f({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:(d=i.data)==null?void 0:d.data})}),r.terminate()};const o={index:n,...Array.isArray(a)?{data:a}:a};r.postMessage(o,p(o))})}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),f=t.reducer?t.reducer.toString():"(shards) => shards.flat()";return{data:await new Promise((r,o)=>{const i=`
|
|
153
|
+
const reducer = ${f};
|
|
26
154
|
self.addEventListener('message', (event) => {
|
|
27
155
|
try {
|
|
28
156
|
const result = reducer(event.data);
|
|
@@ -31,4 +159,4 @@ self.addEventListener('message', async (event) => {
|
|
|
31
159
|
self.postMessage({ ok: false, error: String(err) });
|
|
32
160
|
}
|
|
33
161
|
});
|
|
34
|
-
`,
|
|
162
|
+
`,c=new Blob([i],{type:"application/javascript"}),d=new Worker(URL.createObjectURL(c));d.onmessage=g=>{d.terminate(),g.data.ok?r(g.data.data):o(new Error(g.data.error))},d.onerror=g=>{d.terminate(),o(g)},d.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((f,l)=>{u.onmessage=o=>{var i,c;u.terminate(),((i=o.data)==null?void 0:i.ok)===!1?l(new Error(o.data.error)):f((c=o.data)==null?void 0:c.data)},u.onerror=o=>{u.terminate(),l(o)};const r=t.srcData??{};u.postMessage({data:r,index:0},p(r))})}return new Promise((t,n)=>{const a=[],u=[];for(const r of e){const o=this.findWorkerByName(r.worker);if(!o){n(new Error(`Worker "${r.worker}" not found`));return}const i=new k(o.func,{mode:v.Pipeline});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 o=[],i={};r>0&&(i.inputPort=u[r-1].port1,o.push(i.inputPort)),r<a.length-1&&(i.outputPort=u[r].port2,o.push(i.outputPort)),a[r].postMessage({__pipeline_ports__:!0,...i},o)}const f=a[a.length-1];f.onmessage=r=>{var o,i;a.forEach(c=>c.terminate()),((o=r.data)==null?void 0:o.ok)===!1?n(new Error(r.data.error)):t((i=r.data)==null?void 0:i.data)},f.onerror=r=>{a.forEach(o=>o.terminate()),n(r)};const l=e[0].srcData??{};a[0].postMessage({data:l,index:0},p(l))})}async runPersistent(e,t){const n=this.findWorkerByName(e);if(!n)throw new Error(`Worker "${e}" not found`);let a=this._persistentWorkers.get(e);return a||(a=new k(n.func,{mode:v.Persistent}).getWorker,this._persistentWorkers.set(e,a)),new Promise((u,f)=>{a.onmessage=r=>{var o,i;((o=r.data)==null?void 0:o.ok)===!1?f(new Error(r.data.error)):u((i=r.data)==null?void 0:i.data)},a.onerror=r=>{f(r)};const l={type:"run",config:t.config};t.dataset!==void 0&&(l.dataset=t.dataset),a.postMessage(l,p(l))})}release(e){const t=this._persistentWorkers.get(e);t&&(t.postMessage({type:"release"}),t.terminate(),this._persistentWorkers.delete(e))}}exports.MainWorkerFactory=A;exports.WorkerFactory=k;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
var
|
|
2
|
-
var
|
|
3
|
-
var
|
|
4
|
-
const
|
|
1
|
+
var w = Object.defineProperty;
|
|
2
|
+
var m = (s, e, t) => e in s ? w(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t;
|
|
3
|
+
var h = (s, e, t) => m(s, typeof e != "symbol" ? e + "" : e, t);
|
|
4
|
+
const y = (s) => `
|
|
5
5
|
const extractTransferables = (value, seen = new Set()) => {
|
|
6
6
|
if (value === null || typeof value !== 'object') return [];
|
|
7
7
|
if (seen.has(value)) return [];
|
|
@@ -18,14 +18,148 @@ const extractTransferables = (value, seen = new Set()) => {
|
|
|
18
18
|
|
|
19
19
|
self.addEventListener('message', async (event) => {
|
|
20
20
|
try {
|
|
21
|
-
const output = await ${
|
|
21
|
+
const output = await ${s}(event.data);
|
|
22
22
|
self.postMessage({ ok: true, data: output }, extractTransferables(output));
|
|
23
23
|
} catch (err) {
|
|
24
24
|
self.postMessage({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
25
25
|
}
|
|
26
26
|
})
|
|
27
|
+
`, P = (s) => `
|
|
28
|
+
const extractTransferables = (value, seen = new Set()) => {
|
|
29
|
+
if (value === null || typeof value !== 'object') return [];
|
|
30
|
+
if (seen.has(value)) return [];
|
|
31
|
+
seen.add(value);
|
|
32
|
+
if (value instanceof ArrayBuffer || value instanceof MessagePort ||
|
|
33
|
+
(typeof ImageBitmap !== 'undefined' && value instanceof ImageBitmap) ||
|
|
34
|
+
(typeof OffscreenCanvas !== 'undefined' && value instanceof OffscreenCanvas)) {
|
|
35
|
+
return [value];
|
|
36
|
+
}
|
|
37
|
+
if (ArrayBuffer.isView(value)) return [value.buffer];
|
|
38
|
+
if (Array.isArray(value)) return value.flatMap(i => extractTransferables(i, seen));
|
|
39
|
+
return Object.values(value).flatMap(v => extractTransferables(v, seen));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const workerFn = ${s};
|
|
43
|
+
let outputPort = null;
|
|
44
|
+
let inputPort = null;
|
|
45
|
+
let pendingData = null;
|
|
46
|
+
|
|
47
|
+
async function processData(data) {
|
|
48
|
+
try {
|
|
49
|
+
const output = await workerFn(data);
|
|
50
|
+
const result = { ok: true, data: output };
|
|
51
|
+
const transfers = extractTransferables(output);
|
|
52
|
+
if (outputPort) {
|
|
53
|
+
outputPort.postMessage(result, transfers);
|
|
54
|
+
} else {
|
|
55
|
+
self.postMessage(result, transfers);
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const result = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
59
|
+
if (outputPort) {
|
|
60
|
+
outputPort.postMessage(result);
|
|
61
|
+
} else {
|
|
62
|
+
self.postMessage(result);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
self.addEventListener('message', (event) => {
|
|
68
|
+
if (event.data && event.data.__pipeline_ports__) {
|
|
69
|
+
if (event.data.outputPort) {
|
|
70
|
+
outputPort = event.data.outputPort;
|
|
71
|
+
}
|
|
72
|
+
if (event.data.inputPort) {
|
|
73
|
+
inputPort = event.data.inputPort;
|
|
74
|
+
inputPort.onmessage = (e) => {
|
|
75
|
+
if (e.data && e.data.ok === false) {
|
|
76
|
+
// Propagate errors through the pipeline
|
|
77
|
+
if (outputPort) outputPort.postMessage(e.data);
|
|
78
|
+
else self.postMessage(e.data);
|
|
79
|
+
} else {
|
|
80
|
+
processData({ data: e.data.data, index: 0 });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// If we already received data before ports, process it now
|
|
85
|
+
if (pendingData !== null) {
|
|
86
|
+
processData(pendingData);
|
|
87
|
+
pendingData = null;
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// First worker in pipeline or standalone — process directly
|
|
92
|
+
if (!inputPort) {
|
|
93
|
+
processData(event.data);
|
|
94
|
+
} else {
|
|
95
|
+
// Store data until ports are configured
|
|
96
|
+
pendingData = event.data;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
`, M = (s) => `
|
|
100
|
+
const extractTransferables = (value, seen = new Set()) => {
|
|
101
|
+
if (value === null || typeof value !== 'object') return [];
|
|
102
|
+
if (seen.has(value)) return [];
|
|
103
|
+
seen.add(value);
|
|
104
|
+
if (value instanceof ArrayBuffer || value instanceof MessagePort ||
|
|
105
|
+
(typeof ImageBitmap !== 'undefined' && value instanceof ImageBitmap) ||
|
|
106
|
+
(typeof OffscreenCanvas !== 'undefined' && value instanceof OffscreenCanvas)) {
|
|
107
|
+
return [value];
|
|
108
|
+
}
|
|
109
|
+
if (ArrayBuffer.isView(value)) return [value.buffer];
|
|
110
|
+
if (Array.isArray(value)) return value.flatMap(i => extractTransferables(i, seen));
|
|
111
|
+
return Object.values(value).flatMap(v => extractTransferables(v, seen));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const workerFn = ${s};
|
|
115
|
+
let cachedDataset = null;
|
|
116
|
+
|
|
117
|
+
self.addEventListener('message', async (event) => {
|
|
118
|
+
const msg = event.data;
|
|
119
|
+
|
|
120
|
+
if (msg && msg.type === 'release') {
|
|
121
|
+
cachedDataset = null;
|
|
122
|
+
self.postMessage({ ok: true, data: null, type: 'released' });
|
|
123
|
+
self.close();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (msg && msg.type === 'run') {
|
|
128
|
+
// Update cache if new dataset provided
|
|
129
|
+
if (msg.dataset !== undefined) {
|
|
130
|
+
cachedDataset = msg.dataset;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (cachedDataset === null) {
|
|
134
|
+
self.postMessage({ ok: false, error: 'No dataset cached. Provide a dataset on the first call.' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const output = await workerFn({ data: cachedDataset, config: msg.config });
|
|
140
|
+
self.postMessage({ ok: true, data: output }, extractTransferables(output));
|
|
141
|
+
} catch (err) {
|
|
142
|
+
self.postMessage({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Fallback: treat as a regular one-shot call for backwards compat
|
|
148
|
+
try {
|
|
149
|
+
const output = await workerFn(msg);
|
|
150
|
+
self.postMessage({ ok: true, data: output }, extractTransferables(output));
|
|
151
|
+
} catch (err) {
|
|
152
|
+
self.postMessage({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
27
155
|
`;
|
|
28
|
-
|
|
156
|
+
var v = /* @__PURE__ */ ((s) => (s.Default = "default", s.Pipeline = "pipeline", s.Persistent = "persistent", s))(v || {});
|
|
157
|
+
const W = Object.freeze({
|
|
158
|
+
persistent: M,
|
|
159
|
+
pipeline: P,
|
|
160
|
+
default: y
|
|
161
|
+
});
|
|
162
|
+
class k {
|
|
29
163
|
/**
|
|
30
164
|
* Creates a new `Worker` from the given function.
|
|
31
165
|
*
|
|
@@ -35,13 +169,15 @@ class w {
|
|
|
35
169
|
* @param workerFunction - The function to run inside the worker thread.
|
|
36
170
|
* Must be self-contained — it cannot reference variables from the outer
|
|
37
171
|
* scope because it is serialised via `.toString()`.
|
|
172
|
+
* @param options - Optional configuration. Set `mode` to control the
|
|
173
|
+
* worker execution mode (default, pipeline, or persistent).
|
|
38
174
|
*/
|
|
39
|
-
constructor(e) {
|
|
40
|
-
|
|
41
|
-
const
|
|
175
|
+
constructor(e, t) {
|
|
176
|
+
h(this, "_worker");
|
|
177
|
+
const n = (t == null ? void 0 : t.mode) ?? "default", a = W[n](e.toString()), f = new Blob([a], {
|
|
42
178
|
type: "application/javascript"
|
|
43
179
|
});
|
|
44
|
-
this._worker = new Worker(URL.createObjectURL(
|
|
180
|
+
this._worker = new Worker(URL.createObjectURL(f));
|
|
45
181
|
}
|
|
46
182
|
/**
|
|
47
183
|
* Returns the underlying native `Worker` instance.
|
|
@@ -53,25 +189,26 @@ class w {
|
|
|
53
189
|
return this._worker;
|
|
54
190
|
}
|
|
55
191
|
}
|
|
56
|
-
class
|
|
192
|
+
class b {
|
|
57
193
|
constructor(e) {
|
|
58
194
|
this.results = e;
|
|
59
195
|
}
|
|
60
196
|
}
|
|
61
|
-
function
|
|
62
|
-
return
|
|
63
|
-
(
|
|
197
|
+
function p(s, e = /* @__PURE__ */ new Set()) {
|
|
198
|
+
return s === null || typeof s != "object" ? [] : e.has(s) ? [] : (e.add(s), s instanceof ArrayBuffer || s instanceof MessagePort || typeof ImageBitmap < "u" && s instanceof ImageBitmap || typeof OffscreenCanvas < "u" && s instanceof OffscreenCanvas ? [s] : ArrayBuffer.isView(s) ? [s.buffer] : Array.isArray(s) ? s.flatMap((t) => p(t, e)) : Object.values(s).flatMap(
|
|
199
|
+
(t) => p(t, e)
|
|
64
200
|
));
|
|
65
201
|
}
|
|
66
|
-
class
|
|
202
|
+
class B {
|
|
67
203
|
/**
|
|
68
204
|
* Creates a new `MainWorkerFactory`.
|
|
69
205
|
*
|
|
70
206
|
* @param options - Configuration object containing the `workers` registry.
|
|
71
207
|
*/
|
|
72
208
|
constructor(e) {
|
|
73
|
-
|
|
74
|
-
|
|
209
|
+
h(this, "_workers");
|
|
210
|
+
h(this, "_threads");
|
|
211
|
+
h(this, "_persistentWorkers", /* @__PURE__ */ new Map());
|
|
75
212
|
this._workers = e.workers, this._threads = navigator.hardwareConcurrency;
|
|
76
213
|
}
|
|
77
214
|
/**
|
|
@@ -81,7 +218,7 @@ class b {
|
|
|
81
218
|
* @returns A new `WorkerFactory` wrapping the worker.
|
|
82
219
|
*/
|
|
83
220
|
initWorker(e) {
|
|
84
|
-
return new
|
|
221
|
+
return new k(e);
|
|
85
222
|
}
|
|
86
223
|
/**
|
|
87
224
|
* Splits an array into up to `numChunks` evenly-sized sub-arrays.
|
|
@@ -99,16 +236,16 @@ class b {
|
|
|
99
236
|
* partitionArray([1, 2, 3, 4, 5], 3);
|
|
100
237
|
* // → [[1, 2], [3, 4], [5]]
|
|
101
238
|
*/
|
|
102
|
-
partitionArray(e,
|
|
239
|
+
partitionArray(e, t) {
|
|
103
240
|
if (!e.length) return [];
|
|
104
|
-
if (
|
|
105
|
-
const
|
|
106
|
-
let
|
|
107
|
-
for (let
|
|
108
|
-
const
|
|
109
|
-
|
|
241
|
+
if (t <= 0) throw new Error("numChunks must be positive");
|
|
242
|
+
const n = Math.min(t, e.length), a = Math.floor(e.length / n), f = e.length % n, u = [];
|
|
243
|
+
let l = 0;
|
|
244
|
+
for (let r = 0; r < n; r++) {
|
|
245
|
+
const o = a + (r < f ? 1 : 0);
|
|
246
|
+
u.push(e.slice(l, l + o)), l += o;
|
|
110
247
|
}
|
|
111
|
-
return
|
|
248
|
+
return u;
|
|
112
249
|
}
|
|
113
250
|
/**
|
|
114
251
|
* Looks up a registered worker configuration by name.
|
|
@@ -117,7 +254,7 @@ class b {
|
|
|
117
254
|
* @returns The matching config, or `undefined` if not found.
|
|
118
255
|
*/
|
|
119
256
|
findWorkerByName(e) {
|
|
120
|
-
return this._workers.find((
|
|
257
|
+
return this._workers.find((t) => t.name === e);
|
|
121
258
|
}
|
|
122
259
|
/**
|
|
123
260
|
* Runs a named worker against the provided data, distributing work across
|
|
@@ -146,20 +283,20 @@ class b {
|
|
|
146
283
|
* const settled = await foreman.runWorker('sum', { srcData: [1, 2, 3] });
|
|
147
284
|
*/
|
|
148
285
|
async runWorker(e, {
|
|
149
|
-
srcData:
|
|
150
|
-
...
|
|
286
|
+
srcData: t,
|
|
287
|
+
...n
|
|
151
288
|
}) {
|
|
152
|
-
const
|
|
153
|
-
if (!
|
|
289
|
+
const a = this.findWorkerByName(e);
|
|
290
|
+
if (!a)
|
|
154
291
|
return Promise.reject(new Error(`Worker "${e}" not found`));
|
|
155
|
-
const
|
|
156
|
-
|
|
292
|
+
const f = a.maxConcurrency ?? this._threads, u = !!(Array.isArray(t) && t.length > 1 && a.partition), l = u ? this.partitionArray(t, f) : t, r = this.createWorkerPromises(
|
|
293
|
+
a,
|
|
157
294
|
e,
|
|
158
|
-
{ data:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
),
|
|
162
|
-
return new
|
|
295
|
+
{ data: l, ...n },
|
|
296
|
+
f,
|
|
297
|
+
u
|
|
298
|
+
), o = await Promise.allSettled(r);
|
|
299
|
+
return new b(o);
|
|
163
300
|
}
|
|
164
301
|
/**
|
|
165
302
|
* Builds the array of per-thread worker promises for a single `runWorker`
|
|
@@ -175,16 +312,16 @@ class b {
|
|
|
175
312
|
* @param isPartitioned - Whether `data` is a pre-split array of shards.
|
|
176
313
|
* @returns An array of promises, one per thread.
|
|
177
314
|
*/
|
|
178
|
-
createWorkerPromises(e,
|
|
179
|
-
const { data:
|
|
180
|
-
return Array.from({ length:
|
|
181
|
-
const
|
|
315
|
+
createWorkerPromises(e, t, n, a, f) {
|
|
316
|
+
const { data: u, ...l } = n;
|
|
317
|
+
return Array.from({ length: a }, (r, o) => {
|
|
318
|
+
const i = f && Array.isArray(u) ? u[o] : u;
|
|
182
319
|
return this.runWorkerWithRetry(
|
|
183
320
|
{
|
|
184
321
|
workerFunc: e.func,
|
|
185
|
-
workerName:
|
|
186
|
-
index:
|
|
187
|
-
data: { data:
|
|
322
|
+
workerName: t,
|
|
323
|
+
index: o,
|
|
324
|
+
data: { data: i, ...l }
|
|
188
325
|
},
|
|
189
326
|
e.retries
|
|
190
327
|
);
|
|
@@ -202,16 +339,16 @@ class b {
|
|
|
202
339
|
* @returns The successful {@link WorkerResult} once the worker resolves.
|
|
203
340
|
* @throws The last caught error when all retries are exhausted.
|
|
204
341
|
*/
|
|
205
|
-
async runWorkerWithRetry(e,
|
|
342
|
+
async runWorkerWithRetry(e, t = 2) {
|
|
206
343
|
try {
|
|
207
344
|
return await this.initiateWorker(e);
|
|
208
|
-
} catch (
|
|
209
|
-
if (
|
|
345
|
+
} catch (n) {
|
|
346
|
+
if (t > 0)
|
|
210
347
|
return console.error(
|
|
211
|
-
`Worker ${e.index} failed, retrying (${
|
|
212
|
-
|
|
213
|
-
), this.runWorkerWithRetry(e,
|
|
214
|
-
throw console.error("Worker failed after all retries:",
|
|
348
|
+
`Worker ${e.index} failed, retrying (${t} left):`,
|
|
349
|
+
n
|
|
350
|
+
), this.runWorkerWithRetry(e, t - 1);
|
|
351
|
+
throw console.error("Worker failed after all retries:", n), n;
|
|
215
352
|
}
|
|
216
353
|
}
|
|
217
354
|
/**
|
|
@@ -234,43 +371,43 @@ class b {
|
|
|
234
371
|
*/
|
|
235
372
|
initiateWorker({
|
|
236
373
|
workerFunc: e,
|
|
237
|
-
workerName:
|
|
238
|
-
index:
|
|
239
|
-
data:
|
|
374
|
+
workerName: t,
|
|
375
|
+
index: n,
|
|
376
|
+
data: a
|
|
240
377
|
}) {
|
|
241
|
-
return new Promise((
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
index:
|
|
246
|
-
workerConfigs: { workerFunc: e, workerName:
|
|
247
|
-
failedResult:
|
|
378
|
+
return new Promise((f, u) => {
|
|
379
|
+
const r = this.initWorker(e).getWorker;
|
|
380
|
+
r.onerror = (i) => {
|
|
381
|
+
r.terminate(), u({
|
|
382
|
+
index: n,
|
|
383
|
+
workerConfigs: { workerFunc: e, workerName: t, index: n, data: a },
|
|
384
|
+
failedResult: i
|
|
248
385
|
});
|
|
249
|
-
},
|
|
250
|
-
var
|
|
251
|
-
if (((
|
|
252
|
-
|
|
253
|
-
index:
|
|
254
|
-
workerConfigs: { workerFunc: e, workerName:
|
|
386
|
+
}, r.onmessage = (i) => {
|
|
387
|
+
var c, d;
|
|
388
|
+
if (((c = i.data) == null ? void 0 : c.ok) === !1) {
|
|
389
|
+
r.terminate(), u({
|
|
390
|
+
index: n,
|
|
391
|
+
workerConfigs: { workerFunc: e, workerName: t, index: n, data: a },
|
|
255
392
|
failedResult: new ErrorEvent("error", {
|
|
256
|
-
message:
|
|
393
|
+
message: i.data.error
|
|
257
394
|
})
|
|
258
395
|
});
|
|
259
396
|
return;
|
|
260
397
|
}
|
|
261
|
-
|
|
262
|
-
index:
|
|
263
|
-
workerConfigs: { workerFunc: e, workerName:
|
|
398
|
+
f({
|
|
399
|
+
index: n,
|
|
400
|
+
workerConfigs: { workerFunc: e, workerName: t, index: n, data: a },
|
|
264
401
|
successResult: new MessageEvent("message", {
|
|
265
|
-
data: (
|
|
402
|
+
data: (d = i.data) == null ? void 0 : d.data
|
|
266
403
|
})
|
|
267
|
-
}),
|
|
404
|
+
}), r.terminate();
|
|
268
405
|
};
|
|
269
|
-
const
|
|
270
|
-
index:
|
|
271
|
-
...Array.isArray(
|
|
406
|
+
const o = {
|
|
407
|
+
index: n,
|
|
408
|
+
...Array.isArray(a) ? { data: a } : a
|
|
272
409
|
};
|
|
273
|
-
|
|
410
|
+
r.postMessage(o, p(o));
|
|
274
411
|
});
|
|
275
412
|
}
|
|
276
413
|
/**
|
|
@@ -305,16 +442,16 @@ class b {
|
|
|
305
442
|
* reducer: (shards) => shards.flat().reduce((a, b) => a + b, 0),
|
|
306
443
|
* });
|
|
307
444
|
*/
|
|
308
|
-
async collectResults(e,
|
|
309
|
-
const
|
|
310
|
-
(
|
|
311
|
-
),
|
|
312
|
-
(
|
|
313
|
-
),
|
|
445
|
+
async collectResults(e, t = {}) {
|
|
446
|
+
const n = e.results.filter(
|
|
447
|
+
(r) => r.status === "fulfilled"
|
|
448
|
+
), a = e.results.filter(
|
|
449
|
+
(r) => r.status === "rejected"
|
|
450
|
+
), f = n.map((r) => r.value.successResult.data), u = t.reducer ? t.reducer.toString() : "(shards) => shards.flat()";
|
|
314
451
|
return {
|
|
315
|
-
data: await new Promise((
|
|
316
|
-
const
|
|
317
|
-
const reducer = ${
|
|
452
|
+
data: await new Promise((r, o) => {
|
|
453
|
+
const i = `
|
|
454
|
+
const reducer = ${u};
|
|
318
455
|
self.addEventListener('message', (event) => {
|
|
319
456
|
try {
|
|
320
457
|
const result = reducer(event.data);
|
|
@@ -323,20 +460,170 @@ class b {
|
|
|
323
460
|
self.postMessage({ ok: false, error: String(err) });
|
|
324
461
|
}
|
|
325
462
|
});
|
|
326
|
-
`,
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
},
|
|
330
|
-
|
|
331
|
-
},
|
|
463
|
+
`, c = new Blob([i], { type: "application/javascript" }), d = new Worker(URL.createObjectURL(c));
|
|
464
|
+
d.onmessage = (g) => {
|
|
465
|
+
d.terminate(), g.data.ok ? r(g.data.data) : o(new Error(g.data.error));
|
|
466
|
+
}, d.onerror = (g) => {
|
|
467
|
+
d.terminate(), o(g);
|
|
468
|
+
}, d.postMessage(f);
|
|
332
469
|
}),
|
|
333
|
-
succeeded:
|
|
334
|
-
failed:
|
|
335
|
-
errors:
|
|
470
|
+
succeeded: n.length,
|
|
471
|
+
failed: a.length,
|
|
472
|
+
errors: a
|
|
336
473
|
};
|
|
337
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* Runs a chain of workers where each step's output feeds directly into the
|
|
477
|
+
* next step — **without passing through the main thread**.
|
|
478
|
+
*
|
|
479
|
+
* Internally, adjacent workers are connected via `MessageChannel` ports.
|
|
480
|
+
* Only the final result is sent back to the main thread, minimising
|
|
481
|
+
* serialisation overhead for large intermediate data.
|
|
482
|
+
*
|
|
483
|
+
* @typeParam TResult - The expected type of the final pipeline output.
|
|
484
|
+
* Defaults to `unknown` if not specified.
|
|
485
|
+
*
|
|
486
|
+
* @param steps - An ordered array of {@link PipelineStep} objects. The first
|
|
487
|
+
* step must include `srcData`; subsequent steps receive the previous
|
|
488
|
+
* step's output as `{ data: previousOutput, index: 0 }`.
|
|
489
|
+
*
|
|
490
|
+
* @returns A promise that resolves with the final step's output.
|
|
491
|
+
* @throws {Error} When `steps` is empty or a worker name is not found.
|
|
492
|
+
*
|
|
493
|
+
* @example
|
|
494
|
+
* const result = await foreman.pipeline<FilteredPost[]>([
|
|
495
|
+
* { worker: 'fetchPosts', srcData: { url: '/api/posts' } },
|
|
496
|
+
* { worker: 'transformPosts' },
|
|
497
|
+
* { worker: 'filterPosts' },
|
|
498
|
+
* ]);
|
|
499
|
+
* console.log(result); // final transformed + filtered data
|
|
500
|
+
*/
|
|
501
|
+
async pipeline(e) {
|
|
502
|
+
if (e.length === 0)
|
|
503
|
+
throw new Error("Pipeline requires at least one step");
|
|
504
|
+
if (e.length === 1) {
|
|
505
|
+
const t = e[0], n = this.findWorkerByName(t.worker);
|
|
506
|
+
if (!n) throw new Error(`Worker "${t.worker}" not found`);
|
|
507
|
+
const f = this.initWorker(n.func).getWorker;
|
|
508
|
+
return new Promise((u, l) => {
|
|
509
|
+
f.onmessage = (o) => {
|
|
510
|
+
var i, c;
|
|
511
|
+
f.terminate(), ((i = o.data) == null ? void 0 : i.ok) === !1 ? l(new Error(o.data.error)) : u((c = o.data) == null ? void 0 : c.data);
|
|
512
|
+
}, f.onerror = (o) => {
|
|
513
|
+
f.terminate(), l(o);
|
|
514
|
+
};
|
|
515
|
+
const r = t.srcData ?? {};
|
|
516
|
+
f.postMessage(
|
|
517
|
+
{ data: r, index: 0 },
|
|
518
|
+
p(r)
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return new Promise((t, n) => {
|
|
523
|
+
const a = [], f = [];
|
|
524
|
+
for (const r of e) {
|
|
525
|
+
const o = this.findWorkerByName(r.worker);
|
|
526
|
+
if (!o) {
|
|
527
|
+
n(new Error(`Worker "${r.worker}" not found`));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const i = new k(o.func, {
|
|
531
|
+
mode: v.Pipeline
|
|
532
|
+
});
|
|
533
|
+
a.push(i.getWorker);
|
|
534
|
+
}
|
|
535
|
+
for (let r = 0; r < a.length - 1; r++)
|
|
536
|
+
f.push(new MessageChannel());
|
|
537
|
+
for (let r = 0; r < a.length; r++) {
|
|
538
|
+
const o = [], i = {};
|
|
539
|
+
r > 0 && (i.inputPort = f[r - 1].port1, o.push(i.inputPort)), r < a.length - 1 && (i.outputPort = f[r].port2, o.push(i.outputPort)), a[r].postMessage(
|
|
540
|
+
{ __pipeline_ports__: !0, ...i },
|
|
541
|
+
o
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
const u = a[a.length - 1];
|
|
545
|
+
u.onmessage = (r) => {
|
|
546
|
+
var o, i;
|
|
547
|
+
a.forEach((c) => c.terminate()), ((o = r.data) == null ? void 0 : o.ok) === !1 ? n(new Error(r.data.error)) : t((i = r.data) == null ? void 0 : i.data);
|
|
548
|
+
}, u.onerror = (r) => {
|
|
549
|
+
a.forEach((o) => o.terminate()), n(r);
|
|
550
|
+
};
|
|
551
|
+
const l = e[0].srcData ?? {};
|
|
552
|
+
a[0].postMessage(
|
|
553
|
+
{ data: l, index: 0 },
|
|
554
|
+
p(l)
|
|
555
|
+
);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Runs a persistent worker that caches its dataset between calls.
|
|
560
|
+
*
|
|
561
|
+
* On the first call, provide both `dataset` and `config`. The worker stores
|
|
562
|
+
* the dataset in memory. On subsequent calls, only `config` is needed — the
|
|
563
|
+
* worker reuses the cached dataset and reprocesses it with the new config.
|
|
564
|
+
*
|
|
565
|
+
* The worker stays alive until {@link release} is called.
|
|
566
|
+
*
|
|
567
|
+
* @param workerName - Name of the registered worker.
|
|
568
|
+
* @param params - Object with optional `dataset` and required `config`.
|
|
569
|
+
* @returns The worker function's return value.
|
|
570
|
+
*
|
|
571
|
+
* @example
|
|
572
|
+
* // First call: send dataset + config
|
|
573
|
+
* const r1 = await factory.runPersistent('transform', {
|
|
574
|
+
* dataset: largeArray,
|
|
575
|
+
* config: { multiplier: 2 },
|
|
576
|
+
* });
|
|
577
|
+
*
|
|
578
|
+
* // Subsequent calls: only config, dataset is cached
|
|
579
|
+
* const r2 = await factory.runPersistent('transform', {
|
|
580
|
+
* config: { multiplier: 5 },
|
|
581
|
+
* });
|
|
582
|
+
*
|
|
583
|
+
* // Update dataset when needed
|
|
584
|
+
* const r3 = await factory.runPersistent('transform', {
|
|
585
|
+
* dataset: newArray,
|
|
586
|
+
* config: { multiplier: 3 },
|
|
587
|
+
* });
|
|
588
|
+
*
|
|
589
|
+
* // Release when done
|
|
590
|
+
* factory.release('transform');
|
|
591
|
+
*/
|
|
592
|
+
async runPersistent(e, t) {
|
|
593
|
+
const n = this.findWorkerByName(e);
|
|
594
|
+
if (!n) throw new Error(`Worker "${e}" not found`);
|
|
595
|
+
let a = this._persistentWorkers.get(e);
|
|
596
|
+
return a || (a = new k(n.func, {
|
|
597
|
+
mode: v.Persistent
|
|
598
|
+
}).getWorker, this._persistentWorkers.set(e, a)), new Promise((f, u) => {
|
|
599
|
+
a.onmessage = (r) => {
|
|
600
|
+
var o, i;
|
|
601
|
+
((o = r.data) == null ? void 0 : o.ok) === !1 ? u(new Error(r.data.error)) : f((i = r.data) == null ? void 0 : i.data);
|
|
602
|
+
}, a.onerror = (r) => {
|
|
603
|
+
u(r);
|
|
604
|
+
};
|
|
605
|
+
const l = {
|
|
606
|
+
type: "run",
|
|
607
|
+
config: t.config
|
|
608
|
+
};
|
|
609
|
+
t.dataset !== void 0 && (l.dataset = t.dataset), a.postMessage(l, p(l));
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Releases a persistent worker, freeing its cached dataset and terminating
|
|
614
|
+
* the thread.
|
|
615
|
+
*
|
|
616
|
+
* After calling `release`, subsequent `runPersistent` calls for this worker
|
|
617
|
+
* will create a fresh instance (requiring a new dataset).
|
|
618
|
+
*
|
|
619
|
+
* @param workerName - Name of the persistent worker to release.
|
|
620
|
+
*/
|
|
621
|
+
release(e) {
|
|
622
|
+
const t = this._persistentWorkers.get(e);
|
|
623
|
+
t && (t.postMessage({ type: "release" }), t.terminate(), this._persistentWorkers.delete(e));
|
|
624
|
+
}
|
|
338
625
|
}
|
|
339
626
|
export {
|
|
340
|
-
|
|
341
|
-
|
|
627
|
+
B as MainWorkerFactory,
|
|
628
|
+
k as WorkerFactory
|
|
342
629
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as MainWorkerFactory } from './main-worker-factory.ts';
|
|
2
|
-
export type { WorkerFunction, MainWorkerFactoryWorker, MainWorkerFactoryOptions, WorkerConfig, WorkerName, WorkerRole, } from './types.ts';
|
|
2
|
+
export type { WorkerFunction, MainWorkerFactoryWorker, MainWorkerFactoryOptions, PipelineStep, WorkerConfig, WorkerName, WorkerRole, } from './types.ts';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CollectOptions, CollectedResult, WorkerConfig, WorkerConfigMap, WorkerDataParam, WorkerFunction, WorkerReturnType, TypedSettledResults } 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)
|
|
@@ -12,6 +12,13 @@ export declare function extractTransferables(value: unknown, seen?: Set<object>)
|
|
|
12
12
|
* handles the full lifecycle of each worker: spawning, partitioning input
|
|
13
13
|
* data across threads, retrying on failure, and collecting results.
|
|
14
14
|
*
|
|
15
|
+
* Also supports:
|
|
16
|
+
* - **Pipelines** — chain workers via `MessageChannel` so intermediate data
|
|
17
|
+
* never crosses back to the main thread ({@link pipeline}).
|
|
18
|
+
* - **Persistent workers** — keep a worker alive with a cached dataset,
|
|
19
|
+
* re-running it with different configs without re-sending the data
|
|
20
|
+
* ({@link runPersistent}, {@link release}).
|
|
21
|
+
*
|
|
15
22
|
* @typeParam TConfigs - A readonly tuple of {@link WorkerConfig} objects that
|
|
16
23
|
* defines the set of available workers and their typed signatures.
|
|
17
24
|
*
|
|
@@ -19,7 +26,7 @@ export declare function extractTransferables(value: unknown, seen?: Set<object>)
|
|
|
19
26
|
* const foreman = new MainWorkerFactory({
|
|
20
27
|
* workers: [
|
|
21
28
|
* { name: 'sum', role: 'compute', func: sumWorker, partition: true },
|
|
22
|
-
* ],
|
|
29
|
+
* ] as const,
|
|
23
30
|
* });
|
|
24
31
|
*
|
|
25
32
|
* const settled = await foreman.runWorker('sum', { srcData: [1, 2, 3, 4] });
|
|
@@ -28,6 +35,7 @@ export declare function extractTransferables(value: unknown, seen?: Set<object>)
|
|
|
28
35
|
declare class MainWorkerFactory<TConfigs extends readonly WorkerConfig<WorkerFunction<any, any>>[]> {
|
|
29
36
|
private readonly _workers;
|
|
30
37
|
private readonly _threads;
|
|
38
|
+
private readonly _persistentWorkers;
|
|
31
39
|
/**
|
|
32
40
|
* Creates a new `MainWorkerFactory`.
|
|
33
41
|
*
|
|
@@ -176,5 +184,80 @@ declare class MainWorkerFactory<TConfigs extends readonly WorkerConfig<WorkerFun
|
|
|
176
184
|
* });
|
|
177
185
|
*/
|
|
178
186
|
collectResults<T = unknown, R = T extends (infer Item)[] ? Item[] : T[]>(settled: TypedSettledResults<T>, options?: CollectOptions<T, R>): Promise<CollectedResult<R>>;
|
|
187
|
+
/**
|
|
188
|
+
* Runs a chain of workers where each step's output feeds directly into the
|
|
189
|
+
* next step — **without passing through the main thread**.
|
|
190
|
+
*
|
|
191
|
+
* Internally, adjacent workers are connected via `MessageChannel` ports.
|
|
192
|
+
* Only the final result is sent back to the main thread, minimising
|
|
193
|
+
* serialisation overhead for large intermediate data.
|
|
194
|
+
*
|
|
195
|
+
* @typeParam TResult - The expected type of the final pipeline output.
|
|
196
|
+
* Defaults to `unknown` if not specified.
|
|
197
|
+
*
|
|
198
|
+
* @param steps - An ordered array of {@link PipelineStep} objects. The first
|
|
199
|
+
* step must include `srcData`; subsequent steps receive the previous
|
|
200
|
+
* step's output as `{ data: previousOutput, index: 0 }`.
|
|
201
|
+
*
|
|
202
|
+
* @returns A promise that resolves with the final step's output.
|
|
203
|
+
* @throws {Error} When `steps` is empty or a worker name is not found.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* const result = await foreman.pipeline<FilteredPost[]>([
|
|
207
|
+
* { worker: 'fetchPosts', srcData: { url: '/api/posts' } },
|
|
208
|
+
* { worker: 'transformPosts' },
|
|
209
|
+
* { worker: 'filterPosts' },
|
|
210
|
+
* ]);
|
|
211
|
+
* console.log(result); // final transformed + filtered data
|
|
212
|
+
*/
|
|
213
|
+
pipeline<TResult = unknown>(steps: PipelineStep[]): Promise<TResult>;
|
|
214
|
+
/**
|
|
215
|
+
* Runs a persistent worker that caches its dataset between calls.
|
|
216
|
+
*
|
|
217
|
+
* On the first call, provide both `dataset` and `config`. The worker stores
|
|
218
|
+
* the dataset in memory. On subsequent calls, only `config` is needed — the
|
|
219
|
+
* worker reuses the cached dataset and reprocesses it with the new config.
|
|
220
|
+
*
|
|
221
|
+
* The worker stays alive until {@link release} is called.
|
|
222
|
+
*
|
|
223
|
+
* @param workerName - Name of the registered worker.
|
|
224
|
+
* @param params - Object with optional `dataset` and required `config`.
|
|
225
|
+
* @returns The worker function's return value.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* // First call: send dataset + config
|
|
229
|
+
* const r1 = await factory.runPersistent('transform', {
|
|
230
|
+
* dataset: largeArray,
|
|
231
|
+
* config: { multiplier: 2 },
|
|
232
|
+
* });
|
|
233
|
+
*
|
|
234
|
+
* // Subsequent calls: only config, dataset is cached
|
|
235
|
+
* const r2 = await factory.runPersistent('transform', {
|
|
236
|
+
* config: { multiplier: 5 },
|
|
237
|
+
* });
|
|
238
|
+
*
|
|
239
|
+
* // Update dataset when needed
|
|
240
|
+
* const r3 = await factory.runPersistent('transform', {
|
|
241
|
+
* dataset: newArray,
|
|
242
|
+
* config: { multiplier: 3 },
|
|
243
|
+
* });
|
|
244
|
+
*
|
|
245
|
+
* // Release when done
|
|
246
|
+
* factory.release('transform');
|
|
247
|
+
*/
|
|
248
|
+
runPersistent<TResult = unknown>(workerName: string, params: {
|
|
249
|
+
dataset?: unknown;
|
|
250
|
+
config: unknown;
|
|
251
|
+
}): Promise<TResult>;
|
|
252
|
+
/**
|
|
253
|
+
* Releases a persistent worker, freeing its cached dataset and terminating
|
|
254
|
+
* the thread.
|
|
255
|
+
*
|
|
256
|
+
* After calling `release`, subsequent `runPersistent` calls for this worker
|
|
257
|
+
* will create a fresh instance (requiring a new dataset).
|
|
258
|
+
*
|
|
259
|
+
* @param workerName - Name of the persistent worker to release.
|
|
260
|
+
*/
|
|
261
|
+
release(workerName: string): void;
|
|
179
262
|
}
|
|
180
263
|
export default MainWorkerFactory;
|
|
@@ -147,3 +147,10 @@ export interface CollectedResult<R> {
|
|
|
147
147
|
/** Raw rejected results, if any */
|
|
148
148
|
errors: PromiseRejectedResult[];
|
|
149
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,4 +1,13 @@
|
|
|
1
1
|
import { WorkerFunction } from '../main-worker-factory/types';
|
|
2
|
+
export declare enum WorkerMode {
|
|
3
|
+
Default = "default",
|
|
4
|
+
Pipeline = "pipeline",
|
|
5
|
+
Persistent = "persistent"
|
|
6
|
+
}
|
|
7
|
+
export interface WorkerFactoryOptions {
|
|
8
|
+
/** The worker execution mode. Defaults to `WorkerMode.Default`. */
|
|
9
|
+
mode?: WorkerMode;
|
|
10
|
+
}
|
|
2
11
|
/**
|
|
3
12
|
* Low-level factory that serialises a {@link WorkerFunction} into a Blob URL
|
|
4
13
|
* and spawns a native `Worker` from it.
|
|
@@ -7,8 +16,18 @@ import { WorkerFunction } from '../main-worker-factory/types';
|
|
|
7
16
|
* It handles the mechanics of turning a plain TypeScript function into a
|
|
8
17
|
* runnable worker thread — you rarely need to use it directly.
|
|
9
18
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
19
|
+
* Supports three execution modes via {@link WorkerMode}:
|
|
20
|
+
* - **Default** — one-shot worker that processes a single message and is
|
|
21
|
+
* terminated after responding.
|
|
22
|
+
* - **Pipeline** — stays alive and forwards output to the next worker via
|
|
23
|
+
* `MessagePort`, enabling worker-to-worker data flow without main-thread
|
|
24
|
+
* round-trips.
|
|
25
|
+
* - **Persistent** — stays alive indefinitely, caches a dataset in memory,
|
|
26
|
+
* and re-processes it with different configs on subsequent messages.
|
|
27
|
+
*
|
|
28
|
+
* The worker script is generated by the template corresponding to the chosen
|
|
29
|
+
* mode, which wraps the function with a message listener and
|
|
30
|
+
* transferable-extraction logic.
|
|
12
31
|
*/
|
|
13
32
|
declare class WorkerFactory {
|
|
14
33
|
readonly _worker: Worker;
|
|
@@ -21,8 +40,10 @@ declare class WorkerFactory {
|
|
|
21
40
|
* @param workerFunction - The function to run inside the worker thread.
|
|
22
41
|
* Must be self-contained — it cannot reference variables from the outer
|
|
23
42
|
* scope because it is serialised via `.toString()`.
|
|
43
|
+
* @param options - Optional configuration. Set `mode` to control the
|
|
44
|
+
* worker execution mode (default, pipeline, or persistent).
|
|
24
45
|
*/
|
|
25
|
-
constructor(workerFunction: WorkerFunction);
|
|
46
|
+
constructor(workerFunction: WorkerFunction, options?: WorkerFactoryOptions);
|
|
26
47
|
/**
|
|
27
48
|
* Returns the underlying native `Worker` instance.
|
|
28
49
|
*
|
package/package.json
CHANGED