@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 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 y=Object.defineProperty;var m=(t,e,r)=>e in t?y(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var g=(t,e,r)=>m(t,typeof e!="symbol"?e+"":e,r);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const w=t=>`
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 ${t}(event.data);
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
- `;class p{constructor(e){g(this,"_worker");const r=w(e.toString()),s=new Blob([r],{type:"application/javascript"});this._worker=new Worker(URL.createObjectURL(s))}get getWorker(){return this._worker}}class W{constructor(e){this.results=e}}function k(t,e=new Set){return t===null||typeof t!="object"?[]:e.has(t)?[]:(e.add(t),t instanceof ArrayBuffer||t instanceof MessagePort||typeof ImageBitmap<"u"&&t instanceof ImageBitmap||typeof OffscreenCanvas<"u"&&t instanceof OffscreenCanvas?[t]:ArrayBuffer.isView(t)?[t.buffer]:Array.isArray(t)?t.flatMap(r=>k(r,e)):Object.values(t).flatMap(r=>k(r,e)))}class v{constructor(e){g(this,"_workers");g(this,"_threads");this._workers=e.workers,this._threads=navigator.hardwareConcurrency}initWorker(e){return new p(e)}partitionArray(e,r){if(!e.length)return[];if(r<=0)throw new Error("numChunks must be positive");const s=Math.min(r,e.length),n=Math.floor(e.length/s),c=e.length%s,o=[];let f=0;for(let a=0;a<s;a++){const i=n+(a<c?1:0);o.push(e.slice(f,f+i)),f+=i}return o}findWorkerByName(e){return this._workers.find(r=>r.name===e)}async runWorker(e,{srcData:r,...s}){const n=this.findWorkerByName(e);if(!n)return Promise.reject(new Error(`Worker "${e}" not found`));const c=n.maxConcurrency??this._threads,o=!!(Array.isArray(r)&&r.length>1&&n.partition),f=o?this.partitionArray(r,c):r,a=this.createWorkerPromises(n,e,{data:f,...s},c,o),i=await Promise.allSettled(a);return new W(i)}createWorkerPromises(e,r,s,n,c){const{data:o,...f}=s;return Array.from({length:n},(a,i)=>{const l=c&&Array.isArray(o)?o[i]:o;return this.runWorkerWithRetry({workerFunc:e.func,workerName:r,index:i,data:{data:l,...f}},e.retries)})}async runWorkerWithRetry(e,r=2){try{return await this.initiateWorker(e)}catch(s){if(r>0)return console.error(`Worker ${e.index} failed, retrying (${r} left):`,s),this.runWorkerWithRetry(e,r-1);throw console.error("Worker failed after all retries:",s),s}}initiateWorker({workerFunc:e,workerName:r,index:s,data:n}){return new Promise((c,o)=>{const a=this.initWorker(e).getWorker;a.onerror=l=>{a.terminate(),o({index:s,workerConfigs:{workerFunc:e,workerName:r,index:s,data:n},failedResult:l})},a.onmessage=l=>{var h,u;if(((h=l.data)==null?void 0:h.ok)===!1){a.terminate(),o({index:s,workerConfigs:{workerFunc:e,workerName:r,index:s,data:n},failedResult:new ErrorEvent("error",{message:l.data.error})});return}c({index:s,workerConfigs:{workerFunc:e,workerName:r,index:s,data:n},successResult:new MessageEvent("message",{data:(u=l.data)==null?void 0:u.data})}),a.terminate()};const i={index:s,...Array.isArray(n)?{data:n}:n};a.postMessage(i,k(i))})}async collectResults(e,r={}){const s=e.results.filter(a=>a.status==="fulfilled"),n=e.results.filter(a=>a.status==="rejected"),c=s.map(a=>a.value.successResult.data),o=r.reducer?r.reducer.toString():"(shards) => shards.flat()";return{data:await new Promise((a,i)=>{const l=`
25
- const reducer = ${o};
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
- `,h=new Blob([l],{type:"application/javascript"}),u=new Worker(URL.createObjectURL(h));u.onmessage=d=>{u.terminate(),d.data.ok?a(d.data.data):i(new Error(d.data.error))},u.onerror=d=>{u.terminate(),i(d)},u.postMessage(c)}),succeeded:s.length,failed:n.length,errors:n}}}exports.MainWorkerFactory=v;exports.WorkerFactory=p;
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 k = Object.defineProperty;
2
- var y = (t, e, r) => e in t ? k(t, e, { enumerable: !0, configurable: !0, writable: !0, value: r }) : t[e] = r;
3
- var p = (t, e, r) => y(t, typeof e != "symbol" ? e + "" : e, r);
4
- const m = (t) => `
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 ${t}(event.data);
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 w {
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
- p(this, "_worker");
41
- const r = m(e.toString()), s = new Blob([r], {
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(s));
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 W {
130
+ class P {
57
131
  constructor(e) {
58
132
  this.results = e;
59
133
  }
60
134
  }
61
- function g(t, e = /* @__PURE__ */ new Set()) {
62
- return t === null || typeof t != "object" ? [] : e.has(t) ? [] : (e.add(t), t instanceof ArrayBuffer || t instanceof MessagePort || typeof ImageBitmap < "u" && t instanceof ImageBitmap || typeof OffscreenCanvas < "u" && t instanceof OffscreenCanvas ? [t] : ArrayBuffer.isView(t) ? [t.buffer] : Array.isArray(t) ? t.flatMap((r) => g(r, e)) : Object.values(t).flatMap(
63
- (r) => g(r, e)
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 b {
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
- p(this, "_workers");
74
- p(this, "_threads");
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 w(e);
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, r) {
176
+ partitionArray(e, t) {
103
177
  if (!e.length) return [];
104
- if (r <= 0) throw new Error("numChunks must be positive");
105
- const s = Math.min(r, e.length), n = Math.floor(e.length / s), c = e.length % s, o = [];
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 a = 0; a < s; a++) {
108
- const i = n + (a < c ? 1 : 0);
109
- o.push(e.slice(f, f + i)), f += i;
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 o;
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((r) => r.name === e);
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: r,
150
- ...s
223
+ srcData: t,
224
+ ...n
151
225
  }) {
152
- const n = this.findWorkerByName(e);
153
- if (!n)
226
+ const a = this.findWorkerByName(e);
227
+ if (!a)
154
228
  return Promise.reject(new Error(`Worker "${e}" not found`));
155
- const c = n.maxConcurrency ?? this._threads, o = !!(Array.isArray(r) && r.length > 1 && n.partition), f = o ? this.partitionArray(r, c) : r, a = this.createWorkerPromises(
156
- n,
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, ...s },
159
- c,
160
- o
161
- ), i = await Promise.allSettled(a);
162
- return new W(i);
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, r, s, n, c) {
179
- const { data: o, ...f } = s;
180
- return Array.from({ length: n }, (a, i) => {
181
- const l = c && Array.isArray(o) ? o[i] : o;
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: r,
186
- index: i,
187
- data: { data: l, ...f }
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, r = 2) {
279
+ async runWorkerWithRetry(e, t = 2) {
206
280
  try {
207
281
  return await this.initiateWorker(e);
208
- } catch (s) {
209
- if (r > 0)
282
+ } catch (n) {
283
+ if (t > 0)
210
284
  return console.error(
211
- `Worker ${e.index} failed, retrying (${r} left):`,
212
- s
213
- ), this.runWorkerWithRetry(e, r - 1);
214
- throw console.error("Worker failed after all retries:", s), s;
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: r,
238
- index: s,
239
- data: n
311
+ workerName: t,
312
+ index: n,
313
+ data: a
240
314
  }) {
241
- return new Promise((c, o) => {
242
- const a = this.initWorker(e).getWorker;
243
- a.onerror = (l) => {
244
- a.terminate(), o({
245
- index: s,
246
- workerConfigs: { workerFunc: e, workerName: r, index: s, data: n },
247
- failedResult: l
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
- }, a.onmessage = (l) => {
250
- var h, u;
251
- if (((h = l.data) == null ? void 0 : h.ok) === !1) {
252
- a.terminate(), o({
253
- index: s,
254
- workerConfigs: { workerFunc: e, workerName: r, index: s, data: n },
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: l.data.error
330
+ message: i.data.error
257
331
  })
258
332
  });
259
333
  return;
260
334
  }
261
- c({
262
- index: s,
263
- workerConfigs: { workerFunc: e, workerName: r, index: s, data: n },
335
+ u({
336
+ index: n,
337
+ workerConfigs: { workerFunc: e, workerName: t, index: n, data: a },
264
338
  successResult: new MessageEvent("message", {
265
- data: (u = l.data) == null ? void 0 : u.data
339
+ data: (p = i.data) == null ? void 0 : p.data
266
340
  })
267
- }), a.terminate();
341
+ }), r.terminate();
268
342
  };
269
- const i = {
270
- index: s,
271
- ...Array.isArray(n) ? { data: n } : n
343
+ const s = {
344
+ index: n,
345
+ ...Array.isArray(a) ? { data: a } : a
272
346
  };
273
- a.postMessage(i, g(i));
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, r = {}) {
309
- const s = e.results.filter(
310
- (a) => a.status === "fulfilled"
311
- ), n = e.results.filter(
312
- (a) => a.status === "rejected"
313
- ), c = s.map((a) => a.value.successResult.data), o = r.reducer ? r.reducer.toString() : "(shards) => shards.flat()";
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((a, i) => {
316
- const l = `
317
- const reducer = ${o};
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
- `, h = new Blob([l], { type: "application/javascript" }), u = new Worker(URL.createObjectURL(h));
327
- u.onmessage = (d) => {
328
- u.terminate(), d.data.ok ? a(d.data.data) : i(new Error(d.data.error));
329
- }, u.onerror = (d) => {
330
- u.terminate(), i(d);
331
- }, u.postMessage(c);
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: s.length,
334
- failed: n.length,
335
- errors: n
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
- b as MainWorkerFactory,
341
- w as WorkerFactory
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@offmain/workerkit",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "A lightweight manager for running functions in Web Workers with partitioning, retries, and concurrency control",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",