@offmain/workerkit 0.9.2 → 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 +62 -0
- package/dist/index.cjs +77 -5
- package/dist/index.js +247 -97
- package/dist/types/tools/main-worker-factory/index.d.ts +1 -1
- package/dist/types/tools/main-worker-factory/main-worker-factory.d.ts +23 -1
- package/dist/types/tools/main-worker-factory/types.d.ts +7 -0
- package/dist/types/tools/worker-factory/worker-factory.d.ts +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,68 @@ 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
|
+
|
|
95
157
|
## ESLint Plugin
|
|
96
158
|
|
|
97
159
|
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
|
|
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 [];
|
|
@@ -15,14 +15,86 @@ 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 ${o}(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
|
+
`,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};
|
|
26
98
|
self.addEventListener('message', (event) => {
|
|
27
99
|
try {
|
|
28
100
|
const result = reducer(event.data);
|
|
@@ -31,4 +103,4 @@ self.addEventListener('message', async (event) => {
|
|
|
31
103
|
self.postMessage({ ok: false, error: String(err) });
|
|
32
104
|
}
|
|
33
105
|
});
|
|
34
|
-
`,
|
|
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;
|
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 = (o, e, t) => e in o ? w(o, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : o[e] = t;
|
|
3
|
+
var h = (o, e, t) => m(o, typeof e != "symbol" ? e + "" : e, t);
|
|
4
|
+
const y = (o) => `
|
|
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,86 @@ 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 ${o}(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
|
+
`, v = (o) => `
|
|
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 = ${o};
|
|
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
|
+
});
|
|
27
99
|
`;
|
|
28
|
-
class
|
|
100
|
+
class k {
|
|
29
101
|
/**
|
|
30
102
|
* Creates a new `Worker` from the given function.
|
|
31
103
|
*
|
|
@@ -35,13 +107,15 @@ class w {
|
|
|
35
107
|
* @param workerFunction - The function to run inside the worker thread.
|
|
36
108
|
* Must be self-contained — it cannot reference variables from the outer
|
|
37
109
|
* scope because it is serialised via `.toString()`.
|
|
110
|
+
* @param options - Optional configuration. Set `pipeline: true` for
|
|
111
|
+
* pipeline-aware workers that support MessagePort forwarding.
|
|
38
112
|
*/
|
|
39
|
-
constructor(e) {
|
|
40
|
-
|
|
41
|
-
const
|
|
113
|
+
constructor(e, t) {
|
|
114
|
+
h(this, "_worker");
|
|
115
|
+
const a = (t != null && t.pipeline ? v : y)(e.toString()), u = new Blob([a], {
|
|
42
116
|
type: "application/javascript"
|
|
43
117
|
});
|
|
44
|
-
this._worker = new Worker(URL.createObjectURL(
|
|
118
|
+
this._worker = new Worker(URL.createObjectURL(u));
|
|
45
119
|
}
|
|
46
120
|
/**
|
|
47
121
|
* Returns the underlying native `Worker` instance.
|
|
@@ -53,25 +127,25 @@ class w {
|
|
|
53
127
|
return this._worker;
|
|
54
128
|
}
|
|
55
129
|
}
|
|
56
|
-
class
|
|
130
|
+
class P {
|
|
57
131
|
constructor(e) {
|
|
58
132
|
this.results = e;
|
|
59
133
|
}
|
|
60
134
|
}
|
|
61
|
-
function g(
|
|
62
|
-
return
|
|
63
|
-
(
|
|
135
|
+
function g(o, e = /* @__PURE__ */ new Set()) {
|
|
136
|
+
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(
|
|
137
|
+
(t) => g(t, e)
|
|
64
138
|
));
|
|
65
139
|
}
|
|
66
|
-
class
|
|
140
|
+
class M {
|
|
67
141
|
/**
|
|
68
142
|
* Creates a new `MainWorkerFactory`.
|
|
69
143
|
*
|
|
70
144
|
* @param options - Configuration object containing the `workers` registry.
|
|
71
145
|
*/
|
|
72
146
|
constructor(e) {
|
|
73
|
-
|
|
74
|
-
|
|
147
|
+
h(this, "_workers");
|
|
148
|
+
h(this, "_threads");
|
|
75
149
|
this._workers = e.workers, this._threads = navigator.hardwareConcurrency;
|
|
76
150
|
}
|
|
77
151
|
/**
|
|
@@ -81,7 +155,7 @@ class b {
|
|
|
81
155
|
* @returns A new `WorkerFactory` wrapping the worker.
|
|
82
156
|
*/
|
|
83
157
|
initWorker(e) {
|
|
84
|
-
return new
|
|
158
|
+
return new k(e);
|
|
85
159
|
}
|
|
86
160
|
/**
|
|
87
161
|
* Splits an array into up to `numChunks` evenly-sized sub-arrays.
|
|
@@ -99,16 +173,16 @@ class b {
|
|
|
99
173
|
* partitionArray([1, 2, 3, 4, 5], 3);
|
|
100
174
|
* // → [[1, 2], [3, 4], [5]]
|
|
101
175
|
*/
|
|
102
|
-
partitionArray(e,
|
|
176
|
+
partitionArray(e, t) {
|
|
103
177
|
if (!e.length) return [];
|
|
104
|
-
if (
|
|
105
|
-
const
|
|
178
|
+
if (t <= 0) throw new Error("numChunks must be positive");
|
|
179
|
+
const n = Math.min(t, e.length), a = Math.floor(e.length / n), u = e.length % n, l = [];
|
|
106
180
|
let f = 0;
|
|
107
|
-
for (let
|
|
108
|
-
const
|
|
109
|
-
|
|
181
|
+
for (let r = 0; r < n; r++) {
|
|
182
|
+
const s = a + (r < u ? 1 : 0);
|
|
183
|
+
l.push(e.slice(f, f + s)), f += s;
|
|
110
184
|
}
|
|
111
|
-
return
|
|
185
|
+
return l;
|
|
112
186
|
}
|
|
113
187
|
/**
|
|
114
188
|
* Looks up a registered worker configuration by name.
|
|
@@ -117,7 +191,7 @@ class b {
|
|
|
117
191
|
* @returns The matching config, or `undefined` if not found.
|
|
118
192
|
*/
|
|
119
193
|
findWorkerByName(e) {
|
|
120
|
-
return this._workers.find((
|
|
194
|
+
return this._workers.find((t) => t.name === e);
|
|
121
195
|
}
|
|
122
196
|
/**
|
|
123
197
|
* Runs a named worker against the provided data, distributing work across
|
|
@@ -146,20 +220,20 @@ class b {
|
|
|
146
220
|
* const settled = await foreman.runWorker('sum', { srcData: [1, 2, 3] });
|
|
147
221
|
*/
|
|
148
222
|
async runWorker(e, {
|
|
149
|
-
srcData:
|
|
150
|
-
...
|
|
223
|
+
srcData: t,
|
|
224
|
+
...n
|
|
151
225
|
}) {
|
|
152
|
-
const
|
|
153
|
-
if (!
|
|
226
|
+
const a = this.findWorkerByName(e);
|
|
227
|
+
if (!a)
|
|
154
228
|
return Promise.reject(new Error(`Worker "${e}" not found`));
|
|
155
|
-
const
|
|
156
|
-
|
|
229
|
+
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(
|
|
230
|
+
a,
|
|
157
231
|
e,
|
|
158
|
-
{ data: f, ...
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
),
|
|
162
|
-
return new
|
|
232
|
+
{ data: f, ...n },
|
|
233
|
+
u,
|
|
234
|
+
l
|
|
235
|
+
), s = await Promise.allSettled(r);
|
|
236
|
+
return new P(s);
|
|
163
237
|
}
|
|
164
238
|
/**
|
|
165
239
|
* Builds the array of per-thread worker promises for a single `runWorker`
|
|
@@ -175,16 +249,16 @@ class b {
|
|
|
175
249
|
* @param isPartitioned - Whether `data` is a pre-split array of shards.
|
|
176
250
|
* @returns An array of promises, one per thread.
|
|
177
251
|
*/
|
|
178
|
-
createWorkerPromises(e,
|
|
179
|
-
const { data:
|
|
180
|
-
return Array.from({ length:
|
|
181
|
-
const
|
|
252
|
+
createWorkerPromises(e, t, n, a, u) {
|
|
253
|
+
const { data: l, ...f } = n;
|
|
254
|
+
return Array.from({ length: a }, (r, s) => {
|
|
255
|
+
const i = u && Array.isArray(l) ? l[s] : l;
|
|
182
256
|
return this.runWorkerWithRetry(
|
|
183
257
|
{
|
|
184
258
|
workerFunc: e.func,
|
|
185
|
-
workerName:
|
|
186
|
-
index:
|
|
187
|
-
data: { data:
|
|
259
|
+
workerName: t,
|
|
260
|
+
index: s,
|
|
261
|
+
data: { data: i, ...f }
|
|
188
262
|
},
|
|
189
263
|
e.retries
|
|
190
264
|
);
|
|
@@ -202,16 +276,16 @@ class b {
|
|
|
202
276
|
* @returns The successful {@link WorkerResult} once the worker resolves.
|
|
203
277
|
* @throws The last caught error when all retries are exhausted.
|
|
204
278
|
*/
|
|
205
|
-
async runWorkerWithRetry(e,
|
|
279
|
+
async runWorkerWithRetry(e, t = 2) {
|
|
206
280
|
try {
|
|
207
281
|
return await this.initiateWorker(e);
|
|
208
|
-
} catch (
|
|
209
|
-
if (
|
|
282
|
+
} catch (n) {
|
|
283
|
+
if (t > 0)
|
|
210
284
|
return console.error(
|
|
211
|
-
`Worker ${e.index} failed, retrying (${
|
|
212
|
-
|
|
213
|
-
), this.runWorkerWithRetry(e,
|
|
214
|
-
throw console.error("Worker failed after all retries:",
|
|
285
|
+
`Worker ${e.index} failed, retrying (${t} left):`,
|
|
286
|
+
n
|
|
287
|
+
), this.runWorkerWithRetry(e, t - 1);
|
|
288
|
+
throw console.error("Worker failed after all retries:", n), n;
|
|
215
289
|
}
|
|
216
290
|
}
|
|
217
291
|
/**
|
|
@@ -234,43 +308,43 @@ class b {
|
|
|
234
308
|
*/
|
|
235
309
|
initiateWorker({
|
|
236
310
|
workerFunc: e,
|
|
237
|
-
workerName:
|
|
238
|
-
index:
|
|
239
|
-
data:
|
|
311
|
+
workerName: t,
|
|
312
|
+
index: n,
|
|
313
|
+
data: a
|
|
240
314
|
}) {
|
|
241
|
-
return new Promise((
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
index:
|
|
246
|
-
workerConfigs: { workerFunc: e, workerName:
|
|
247
|
-
failedResult:
|
|
315
|
+
return new Promise((u, l) => {
|
|
316
|
+
const r = this.initWorker(e).getWorker;
|
|
317
|
+
r.onerror = (i) => {
|
|
318
|
+
r.terminate(), l({
|
|
319
|
+
index: n,
|
|
320
|
+
workerConfigs: { workerFunc: e, workerName: t, index: n, data: a },
|
|
321
|
+
failedResult: i
|
|
248
322
|
});
|
|
249
|
-
},
|
|
250
|
-
var
|
|
251
|
-
if (((
|
|
252
|
-
|
|
253
|
-
index:
|
|
254
|
-
workerConfigs: { workerFunc: e, workerName:
|
|
323
|
+
}, r.onmessage = (i) => {
|
|
324
|
+
var c, p;
|
|
325
|
+
if (((c = i.data) == null ? void 0 : c.ok) === !1) {
|
|
326
|
+
r.terminate(), l({
|
|
327
|
+
index: n,
|
|
328
|
+
workerConfigs: { workerFunc: e, workerName: t, index: n, data: a },
|
|
255
329
|
failedResult: new ErrorEvent("error", {
|
|
256
|
-
message:
|
|
330
|
+
message: i.data.error
|
|
257
331
|
})
|
|
258
332
|
});
|
|
259
333
|
return;
|
|
260
334
|
}
|
|
261
|
-
|
|
262
|
-
index:
|
|
263
|
-
workerConfigs: { workerFunc: e, workerName:
|
|
335
|
+
u({
|
|
336
|
+
index: n,
|
|
337
|
+
workerConfigs: { workerFunc: e, workerName: t, index: n, data: a },
|
|
264
338
|
successResult: new MessageEvent("message", {
|
|
265
|
-
data: (
|
|
339
|
+
data: (p = i.data) == null ? void 0 : p.data
|
|
266
340
|
})
|
|
267
|
-
}),
|
|
341
|
+
}), r.terminate();
|
|
268
342
|
};
|
|
269
|
-
const
|
|
270
|
-
index:
|
|
271
|
-
...Array.isArray(
|
|
343
|
+
const s = {
|
|
344
|
+
index: n,
|
|
345
|
+
...Array.isArray(a) ? { data: a } : a
|
|
272
346
|
};
|
|
273
|
-
|
|
347
|
+
r.postMessage(s, g(s));
|
|
274
348
|
});
|
|
275
349
|
}
|
|
276
350
|
/**
|
|
@@ -305,16 +379,16 @@ class b {
|
|
|
305
379
|
* reducer: (shards) => shards.flat().reduce((a, b) => a + b, 0),
|
|
306
380
|
* });
|
|
307
381
|
*/
|
|
308
|
-
async collectResults(e,
|
|
309
|
-
const
|
|
310
|
-
(
|
|
311
|
-
),
|
|
312
|
-
(
|
|
313
|
-
),
|
|
382
|
+
async collectResults(e, t = {}) {
|
|
383
|
+
const n = e.results.filter(
|
|
384
|
+
(r) => r.status === "fulfilled"
|
|
385
|
+
), a = e.results.filter(
|
|
386
|
+
(r) => r.status === "rejected"
|
|
387
|
+
), u = n.map((r) => r.value.successResult.data), l = t.reducer ? t.reducer.toString() : "(shards) => shards.flat()";
|
|
314
388
|
return {
|
|
315
|
-
data: await new Promise((
|
|
316
|
-
const
|
|
317
|
-
const reducer = ${
|
|
389
|
+
data: await new Promise((r, s) => {
|
|
390
|
+
const i = `
|
|
391
|
+
const reducer = ${l};
|
|
318
392
|
self.addEventListener('message', (event) => {
|
|
319
393
|
try {
|
|
320
394
|
const result = reducer(event.data);
|
|
@@ -323,20 +397,96 @@ class b {
|
|
|
323
397
|
self.postMessage({ ok: false, error: String(err) });
|
|
324
398
|
}
|
|
325
399
|
});
|
|
326
|
-
`,
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
},
|
|
330
|
-
|
|
331
|
-
},
|
|
400
|
+
`, c = new Blob([i], { type: "application/javascript" }), p = new Worker(URL.createObjectURL(c));
|
|
401
|
+
p.onmessage = (d) => {
|
|
402
|
+
p.terminate(), d.data.ok ? r(d.data.data) : s(new Error(d.data.error));
|
|
403
|
+
}, p.onerror = (d) => {
|
|
404
|
+
p.terminate(), s(d);
|
|
405
|
+
}, p.postMessage(u);
|
|
332
406
|
}),
|
|
333
|
-
succeeded:
|
|
334
|
-
failed:
|
|
335
|
-
errors:
|
|
407
|
+
succeeded: n.length,
|
|
408
|
+
failed: a.length,
|
|
409
|
+
errors: a
|
|
336
410
|
};
|
|
337
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Runs a chain of workers where each step's output feeds directly into the
|
|
414
|
+
* next step — **without passing through the main thread**.
|
|
415
|
+
*
|
|
416
|
+
* Internally, adjacent workers are connected via `MessageChannel` ports.
|
|
417
|
+
* Only the final result is sent back to the main thread, minimising
|
|
418
|
+
* serialisation overhead for large intermediate data.
|
|
419
|
+
*
|
|
420
|
+
* @param steps - An ordered array of pipeline steps. The first step must
|
|
421
|
+
* include `srcData`; subsequent steps receive the previous step's output.
|
|
422
|
+
*
|
|
423
|
+
* @returns A promise that resolves with the final step's output.
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* const result = await foreman.pipeline([
|
|
427
|
+
* { worker: 'fetchPosts', srcData: { url: '/api/posts' } },
|
|
428
|
+
* { worker: 'transformPosts' },
|
|
429
|
+
* { worker: 'filterPosts' },
|
|
430
|
+
* ]);
|
|
431
|
+
* console.log(result); // final transformed + filtered data
|
|
432
|
+
*/
|
|
433
|
+
async pipeline(e) {
|
|
434
|
+
if (e.length === 0)
|
|
435
|
+
throw new Error("Pipeline requires at least one step");
|
|
436
|
+
if (e.length === 1) {
|
|
437
|
+
const t = e[0], n = this.findWorkerByName(t.worker);
|
|
438
|
+
if (!n) throw new Error(`Worker "${t.worker}" not found`);
|
|
439
|
+
const u = this.initWorker(n.func).getWorker;
|
|
440
|
+
return new Promise((l, f) => {
|
|
441
|
+
u.onmessage = (s) => {
|
|
442
|
+
var i, c;
|
|
443
|
+
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);
|
|
444
|
+
}, u.onerror = (s) => {
|
|
445
|
+
u.terminate(), f(s);
|
|
446
|
+
};
|
|
447
|
+
const r = t.srcData ?? {};
|
|
448
|
+
u.postMessage(
|
|
449
|
+
{ data: r, index: 0 },
|
|
450
|
+
g(r)
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return new Promise((t, n) => {
|
|
455
|
+
const a = [], u = [];
|
|
456
|
+
for (const r of e) {
|
|
457
|
+
const s = this.findWorkerByName(r.worker);
|
|
458
|
+
if (!s) {
|
|
459
|
+
n(new Error(`Worker "${r.worker}" not found`));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const i = new k(s.func, { pipeline: !0 });
|
|
463
|
+
a.push(i.getWorker);
|
|
464
|
+
}
|
|
465
|
+
for (let r = 0; r < a.length - 1; r++)
|
|
466
|
+
u.push(new MessageChannel());
|
|
467
|
+
for (let r = 0; r < a.length; r++) {
|
|
468
|
+
const s = [], i = {};
|
|
469
|
+
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(
|
|
470
|
+
{ __pipeline_ports__: !0, ...i },
|
|
471
|
+
s
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const l = a[a.length - 1];
|
|
475
|
+
l.onmessage = (r) => {
|
|
476
|
+
var s, i;
|
|
477
|
+
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);
|
|
478
|
+
}, l.onerror = (r) => {
|
|
479
|
+
a.forEach((s) => s.terminate()), n(r);
|
|
480
|
+
};
|
|
481
|
+
const f = e[0].srcData ?? {};
|
|
482
|
+
a[0].postMessage(
|
|
483
|
+
{ data: f, index: 0 },
|
|
484
|
+
g(f)
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
338
488
|
}
|
|
339
489
|
export {
|
|
340
|
-
|
|
341
|
-
|
|
490
|
+
M as MainWorkerFactory,
|
|
491
|
+
k as WorkerFactory
|
|
342
492
|
};
|
|
@@ -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)
|
|
@@ -176,5 +176,27 @@ declare class MainWorkerFactory<TConfigs extends readonly WorkerConfig<WorkerFun
|
|
|
176
176
|
* });
|
|
177
177
|
*/
|
|
178
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>;
|
|
179
201
|
}
|
|
180
202
|
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,8 @@
|
|
|
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
|
+
}
|
|
2
6
|
/**
|
|
3
7
|
* Low-level factory that serialises a {@link WorkerFunction} into a Blob URL
|
|
4
8
|
* and spawns a native `Worker` from it.
|
|
@@ -21,8 +25,10 @@ declare class WorkerFactory {
|
|
|
21
25
|
* @param workerFunction - The function to run inside the worker thread.
|
|
22
26
|
* Must be self-contained — it cannot reference variables from the outer
|
|
23
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.
|
|
24
30
|
*/
|
|
25
|
-
constructor(workerFunction: WorkerFunction);
|
|
31
|
+
constructor(workerFunction: WorkerFunction, options?: WorkerFactoryOptions);
|
|
26
32
|
/**
|
|
27
33
|
* Returns the underlying native `Worker` instance.
|
|
28
34
|
*
|
package/package.json
CHANGED