@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 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=(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 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 ${t}(event.data);
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
- `;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
+ `,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
- `,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;
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 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 = (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 ${t}(event.data);
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
- class w {
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
- p(this, "_worker");
41
- const r = m(e.toString()), s = new Blob([r], {
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(s));
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 W {
192
+ class b {
57
193
  constructor(e) {
58
194
  this.results = e;
59
195
  }
60
196
  }
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)
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 b {
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
- p(this, "_workers");
74
- p(this, "_threads");
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 w(e);
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, r) {
239
+ partitionArray(e, t) {
103
240
  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 = [];
106
- 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;
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 o;
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((r) => r.name === e);
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: r,
150
- ...s
286
+ srcData: t,
287
+ ...n
151
288
  }) {
152
- const n = this.findWorkerByName(e);
153
- if (!n)
289
+ const a = this.findWorkerByName(e);
290
+ if (!a)
154
291
  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,
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: f, ...s },
159
- c,
160
- o
161
- ), i = await Promise.allSettled(a);
162
- return new W(i);
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, 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;
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: r,
186
- index: i,
187
- data: { data: l, ...f }
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, r = 2) {
342
+ async runWorkerWithRetry(e, t = 2) {
206
343
  try {
207
344
  return await this.initiateWorker(e);
208
- } catch (s) {
209
- if (r > 0)
345
+ } catch (n) {
346
+ if (t > 0)
210
347
  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;
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: r,
238
- index: s,
239
- data: n
374
+ workerName: t,
375
+ index: n,
376
+ data: a
240
377
  }) {
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
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
- }, 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 },
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: l.data.error
393
+ message: i.data.error
257
394
  })
258
395
  });
259
396
  return;
260
397
  }
261
- c({
262
- index: s,
263
- workerConfigs: { workerFunc: e, workerName: r, index: s, data: n },
398
+ f({
399
+ index: n,
400
+ workerConfigs: { workerFunc: e, workerName: t, index: n, data: a },
264
401
  successResult: new MessageEvent("message", {
265
- data: (u = l.data) == null ? void 0 : u.data
402
+ data: (d = i.data) == null ? void 0 : d.data
266
403
  })
267
- }), a.terminate();
404
+ }), r.terminate();
268
405
  };
269
- const i = {
270
- index: s,
271
- ...Array.isArray(n) ? { data: n } : n
406
+ const o = {
407
+ index: n,
408
+ ...Array.isArray(a) ? { data: a } : a
272
409
  };
273
- a.postMessage(i, g(i));
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, 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()";
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((a, i) => {
316
- const l = `
317
- const reducer = ${o};
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
- `, 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);
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: s.length,
334
- failed: n.length,
335
- errors: n
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
- b as MainWorkerFactory,
341
- w as WorkerFactory
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
- * The worker script is generated by {@link workerTemplate}, which wraps the
11
- * function with a message listener and transferable-extraction logic.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@offmain/workerkit",
3
- "version": "0.9.2",
3
+ "version": "0.11.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",