@offmain/workerkit 0.11.0 → 0.12.2
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 -8
- package/dist/index.cjs +9 -9
- package/dist/index.js +166 -148
- package/dist/types/tools/main-worker-factory/main-worker-factory.d.ts +3 -3
- package/dist/types/tools/main-worker-factory/types.d.ts +20 -6
- package/dist/types/tools/worker-factory/worker-factory.d.ts +5 -3
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -57,14 +57,68 @@ console.log(data); // [15]
|
|
|
57
57
|
|
|
58
58
|
## WorkerConfig Options
|
|
59
59
|
|
|
60
|
-
| Option | Type
|
|
61
|
-
| ---------------- |
|
|
62
|
-
| `name` | `string`
|
|
63
|
-
| `role` | `string`
|
|
64
|
-
| `func` | `Function`
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
60
|
+
| Option | Type | Default | Description |
|
|
61
|
+
| ---------------- | --------------- | ------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `name` | `string` | — | Unique identifier used to call the worker |
|
|
63
|
+
| `role` | `string` | — | Logical grouping label |
|
|
64
|
+
| `func` | `Function` | — | The exported worker function to run (optional if `workerURL` is provided) |
|
|
65
|
+
| `workerURL` | `string \| URL` | — | Worker script URL or `new URL(..., import.meta.url)` for module bundlers |
|
|
66
|
+
| `maxConcurrency` | `number` | `navigator.hardwareConcurrency` | Max parallel worker instances — defaults to the number of logical CPU cores reported by the browser |
|
|
67
|
+
| `retries` | `number` | `0` | How many times to retry a failed shard before marking it as rejected |
|
|
68
|
+
| `partition` | `boolean` | `false` | Split array input across multiple workers automatically |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Module Bundler Integration (`workerURL`)
|
|
73
|
+
|
|
74
|
+
When worker logic relies on external npm packages (such as Luxon, `date-fns`, `i18next`, or custom data transformation modules), dynamic inline stringification (`.toString()`) cannot access those closed-over imports.
|
|
75
|
+
|
|
76
|
+
Passing `workerURL` enables modern module bundlers (Webpack 5, Vite, Rollup, Parcel) to analyze and bundle the worker file along with all of its dependencies into a dedicated ES module worker chunk.
|
|
77
|
+
|
|
78
|
+
### Dedicated Worker Script
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// transform-data.worker.ts
|
|
82
|
+
import { format } from 'date-fns';
|
|
83
|
+
import { t } from './i18n.ts';
|
|
84
|
+
|
|
85
|
+
self.addEventListener('message', (event) => {
|
|
86
|
+
try {
|
|
87
|
+
const { items, locale } = event.data.data;
|
|
88
|
+
const result = items.map((item: any) => ({
|
|
89
|
+
...item,
|
|
90
|
+
formattedDate: format(new Date(item.timestamp), 'yyyy-MM-dd'),
|
|
91
|
+
label: t('transaction', locale),
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
self.postMessage({ ok: true, data: result });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
self.postMessage({ ok: false, error: (err as Error).message });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Registering with `workerURL`
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { MainWorkerFactory } from '@offmain/workerkit';
|
|
105
|
+
|
|
106
|
+
const factory = new MainWorkerFactory({
|
|
107
|
+
workers: [
|
|
108
|
+
{
|
|
109
|
+
name: 'transformData',
|
|
110
|
+
role: 'compute',
|
|
111
|
+
// Webpack 5 / Vite statically analyzes new URL(..., import.meta.url)
|
|
112
|
+
workerURL: new URL('./transform-data.worker.ts', import.meta.url),
|
|
113
|
+
maxConcurrency: 4,
|
|
114
|
+
},
|
|
115
|
+
] as const,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const settled = await factory.runWorker('transformData', {
|
|
119
|
+
srcData: { locale: 'es', items: [{ timestamp: Date.now() }] },
|
|
120
|
+
});
|
|
121
|
+
```
|
|
68
122
|
|
|
69
123
|
---
|
|
70
124
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var v=Object.defineProperty;var y=(n,e,r)=>e in n?v(n,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):n[e]=r;var k=(n,e,r)=>y(n,typeof e!="symbol"?e+"":e,r);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const m=n=>`
|
|
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,13 +15,13 @@ 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 ${n}(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
|
-
`,P=
|
|
24
|
+
`,P=n=>`
|
|
25
25
|
const extractTransferables = (value, seen = new Set()) => {
|
|
26
26
|
if (value === null || typeof value !== 'object') return [];
|
|
27
27
|
if (seen.has(value)) return [];
|
|
@@ -36,7 +36,7 @@ const extractTransferables = (value, seen = new Set()) => {
|
|
|
36
36
|
return Object.values(value).flatMap(v => extractTransferables(v, seen));
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const workerFn = ${
|
|
39
|
+
const workerFn = ${n};
|
|
40
40
|
let outputPort = null;
|
|
41
41
|
let inputPort = null;
|
|
42
42
|
let pendingData = null;
|
|
@@ -93,7 +93,7 @@ self.addEventListener('message', (event) => {
|
|
|
93
93
|
pendingData = event.data;
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
|
-
`,
|
|
96
|
+
`,W=n=>`
|
|
97
97
|
const extractTransferables = (value, seen = new Set()) => {
|
|
98
98
|
if (value === null || typeof value !== 'object') return [];
|
|
99
99
|
if (seen.has(value)) return [];
|
|
@@ -108,7 +108,7 @@ const extractTransferables = (value, seen = new Set()) => {
|
|
|
108
108
|
return Object.values(value).flatMap(v => extractTransferables(v, seen));
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
const workerFn = ${
|
|
111
|
+
const workerFn = ${n};
|
|
112
112
|
let cachedDataset = null;
|
|
113
113
|
|
|
114
114
|
self.addEventListener('message', async (event) => {
|
|
@@ -149,8 +149,8 @@ self.addEventListener('message', async (event) => {
|
|
|
149
149
|
self.postMessage({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
150
150
|
}
|
|
151
151
|
});
|
|
152
|
-
`;var
|
|
153
|
-
const reducer = ${
|
|
152
|
+
`;var w=(n=>(n.Default="default",n.Pipeline="pipeline",n.Persistent="persistent",n))(w||{});const M=Object.freeze({persistent:W,pipeline:P,default:m});class h{constructor(e,r){k(this,"_worker");if(r!=null&&r.workerURL)this._worker=new Worker(r.workerURL,{type:"module"});else if(e){const o=(r==null?void 0:r.mode)??"default",a=M[o](e.toString()),i=new Blob([a],{type:"application/javascript"});this._worker=new Worker(URL.createObjectURL(i))}else throw new Error("Either workerFunction or options.workerURL must be provided to WorkerFactory.")}get getWorker(){return this._worker}}class b{constructor(e){this.results=e}}function g(n,e=new Set){return n===null||typeof n!="object"?[]:e.has(n)?[]:(e.add(n),n instanceof ArrayBuffer||n instanceof MessagePort||typeof ImageBitmap<"u"&&n instanceof ImageBitmap||typeof OffscreenCanvas<"u"&&n instanceof OffscreenCanvas?[n]:ArrayBuffer.isView(n)?[n.buffer]:Array.isArray(n)?n.flatMap(r=>g(r,e)):Object.values(n).flatMap(r=>g(r,e)))}class E{constructor(e){k(this,"_workers");k(this,"_threads");k(this,"_persistentWorkers",new Map);this._workers=e.workers,this._threads=navigator.hardwareConcurrency}initWorker(e){return new h(e.func,{workerURL:e.workerURL})}partitionArray(e,r){if(!e.length)return[];if(r<=0)throw new Error("numChunks must be positive");const o=Math.min(r,e.length),a=Math.floor(e.length/o),i=e.length%o,l=[];let f=0;for(let t=0;t<o;t++){const s=a+(t<i?1:0);l.push(e.slice(f,f+s)),f+=s}return l}findWorkerByName(e){return this._workers.find(r=>r.name===e)}async runWorker(e,{srcData:r,...o}){const a=this.findWorkerByName(e);if(!a)return Promise.reject(new Error(`Worker "${e}" not found`));const i=a.maxConcurrency??this._threads,l=!!(Array.isArray(r)&&r.length>1&&a.partition),f=l?this.partitionArray(r,i):r,t=this.createWorkerPromises(a,e,{data:f,...o},i,l),s=await Promise.allSettled(t);return new b(s)}createWorkerPromises(e,r,o,a,i){const{data:l,...f}=o;return Array.from({length:a},(t,s)=>{const u=i&&Array.isArray(l)?l[s]:l;return this.runWorkerWithRetry({workerFunc:e.func,workerURL:e.workerURL,workerName:r,index:s,data:{data:u,...f}},e.retries)})}async runWorkerWithRetry(e,r=2){try{return await this.initiateWorker(e)}catch(o){if(r>0)return console.error(`Worker ${e.index} failed, retrying (${r} left):`,o),this.runWorkerWithRetry(e,r-1);throw console.error("Worker failed after all retries:",o),o}}initiateWorker({workerFunc:e,workerURL:r,workerName:o,index:a,data:i}){return new Promise((l,f)=>{const s=this.initWorker({name:o,role:"",func:e,workerURL:r}).getWorker;s.onerror=c=>{s.terminate(),f({index:a,workerConfigs:{workerFunc:e,workerName:o,index:a,data:i},failedResult:c})},s.onmessage=c=>{var d,p;if(((d=c.data)==null?void 0:d.ok)===!1){s.terminate(),f({index:a,workerConfigs:{workerFunc:e,workerName:o,index:a,data:i},failedResult:new ErrorEvent("error",{message:c.data.error})});return}l({index:a,workerConfigs:{workerFunc:e,workerName:o,index:a,data:i},successResult:new MessageEvent("message",{data:(p=c.data)==null?void 0:p.data})}),s.terminate()};const u={index:a,...Array.isArray(i)?{data:i}:i};s.postMessage(u,g(u))})}async collectResults(e,r={}){const o=e.results.filter(t=>t.status==="fulfilled"),a=e.results.filter(t=>t.status==="rejected"),i=o.map(t=>t.value.successResult.data),l=r.reducer?r.reducer.toString():"(shards) => shards.flat()";return{data:await new Promise((t,s)=>{const u=`
|
|
153
|
+
const reducer = ${l};
|
|
154
154
|
self.addEventListener('message', (event) => {
|
|
155
155
|
try {
|
|
156
156
|
const result = reducer(event.data);
|
|
@@ -159,4 +159,4 @@ self.addEventListener('message', async (event) => {
|
|
|
159
159
|
self.postMessage({ ok: false, error: String(err) });
|
|
160
160
|
}
|
|
161
161
|
});
|
|
162
|
-
`,c=new Blob([
|
|
162
|
+
`,c=new Blob([u],{type:"application/javascript"}),d=new Worker(URL.createObjectURL(c));d.onmessage=p=>{d.terminate(),p.data.ok?t(p.data.data):s(new Error(p.data.error))},d.onerror=p=>{d.terminate(),s(p)},d.postMessage(i)}),succeeded:o.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 r=e[0],o=this.findWorkerByName(r.worker);if(!o)throw new Error(`Worker "${r.worker}" not found`);const i=this.initWorker(o).getWorker;return new Promise((l,f)=>{i.onmessage=s=>{var u,c;i.terminate(),((u=s.data)==null?void 0:u.ok)===!1?f(new Error(s.data.error)):l((c=s.data)==null?void 0:c.data)},i.onerror=s=>{i.terminate(),f(s)};const t=r.srcData??{};i.postMessage({data:t,index:0},g(t))})}return new Promise((r,o)=>{const a=[],i=[];for(const t of e){const s=this.findWorkerByName(t.worker);if(!s){o(new Error(`Worker "${t.worker}" not found`));return}const u=new h(s.func,{mode:w.Pipeline,workerURL:s.workerURL});a.push(u.getWorker)}for(let t=0;t<a.length-1;t++)i.push(new MessageChannel);for(let t=0;t<a.length;t++){const s=[],u={};t>0&&(u.inputPort=i[t-1].port1,s.push(u.inputPort)),t<a.length-1&&(u.outputPort=i[t].port2,s.push(u.outputPort)),a[t].postMessage({__pipeline_ports__:!0,...u},s)}const l=a[a.length-1];l.onmessage=t=>{var s,u;a.forEach(c=>c.terminate()),((s=t.data)==null?void 0:s.ok)===!1?o(new Error(t.data.error)):r((u=t.data)==null?void 0:u.data)},l.onerror=t=>{a.forEach(s=>s.terminate()),o(t)};const f=e[0].srcData??{};a[0].postMessage({data:f,index:0},g(f))})}async runPersistent(e,r){const o=this.findWorkerByName(e);if(!o)throw new Error(`Worker "${e}" not found`);let a=this._persistentWorkers.get(e);return a||(a=new h(o.func,{mode:w.Persistent,workerURL:o.workerURL}).getWorker,this._persistentWorkers.set(e,a)),new Promise((i,l)=>{a.onmessage=t=>{var s,u;((s=t.data)==null?void 0:s.ok)===!1?l(new Error(t.data.error)):i((u=t.data)==null?void 0:u.data)},a.onerror=t=>{l(t)};const f={type:"run",config:r.config};r.dataset!==void 0&&(f.dataset=r.dataset),a.postMessage(f,g(f))})}release(e){const r=this._persistentWorkers.get(e);r&&(r.postMessage({type:"release"}),r.terminate(),this._persistentWorkers.delete(e))}}exports.MainWorkerFactory=E;exports.WorkerFactory=h;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
var
|
|
2
|
-
var m = (
|
|
3
|
-
var h = (
|
|
4
|
-
const y = (
|
|
1
|
+
var v = Object.defineProperty;
|
|
2
|
+
var m = (n, e, r) => e in n ? v(n, e, { enumerable: !0, configurable: !0, writable: !0, value: r }) : n[e] = r;
|
|
3
|
+
var h = (n, e, r) => m(n, typeof e != "symbol" ? e + "" : e, r);
|
|
4
|
+
const y = (n) => `
|
|
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,13 +18,13 @@ 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 ${n}(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 = (
|
|
27
|
+
`, P = (n) => `
|
|
28
28
|
const extractTransferables = (value, seen = new Set()) => {
|
|
29
29
|
if (value === null || typeof value !== 'object') return [];
|
|
30
30
|
if (seen.has(value)) return [];
|
|
@@ -39,7 +39,7 @@ const extractTransferables = (value, seen = new Set()) => {
|
|
|
39
39
|
return Object.values(value).flatMap(v => extractTransferables(v, seen));
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const workerFn = ${
|
|
42
|
+
const workerFn = ${n};
|
|
43
43
|
let outputPort = null;
|
|
44
44
|
let inputPort = null;
|
|
45
45
|
let pendingData = null;
|
|
@@ -96,7 +96,7 @@ self.addEventListener('message', (event) => {
|
|
|
96
96
|
pendingData = event.data;
|
|
97
97
|
}
|
|
98
98
|
});
|
|
99
|
-
`,
|
|
99
|
+
`, W = (n) => `
|
|
100
100
|
const extractTransferables = (value, seen = new Set()) => {
|
|
101
101
|
if (value === null || typeof value !== 'object') return [];
|
|
102
102
|
if (seen.has(value)) return [];
|
|
@@ -111,7 +111,7 @@ const extractTransferables = (value, seen = new Set()) => {
|
|
|
111
111
|
return Object.values(value).flatMap(v => extractTransferables(v, seen));
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
-
const workerFn = ${
|
|
114
|
+
const workerFn = ${n};
|
|
115
115
|
let cachedDataset = null;
|
|
116
116
|
|
|
117
117
|
self.addEventListener('message', async (event) => {
|
|
@@ -153,9 +153,9 @@ self.addEventListener('message', async (event) => {
|
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
155
|
`;
|
|
156
|
-
var
|
|
157
|
-
const
|
|
158
|
-
persistent:
|
|
156
|
+
var w = /* @__PURE__ */ ((n) => (n.Default = "default", n.Pipeline = "pipeline", n.Persistent = "persistent", n))(w || {});
|
|
157
|
+
const M = Object.freeze({
|
|
158
|
+
persistent: W,
|
|
159
159
|
pipeline: P,
|
|
160
160
|
default: y
|
|
161
161
|
});
|
|
@@ -168,16 +168,23 @@ class k {
|
|
|
168
168
|
*
|
|
169
169
|
* @param workerFunction - The function to run inside the worker thread.
|
|
170
170
|
* Must be self-contained — it cannot reference variables from the outer
|
|
171
|
-
* scope because it is
|
|
171
|
+
* scope because it is serialized via `.toString()`.
|
|
172
172
|
* @param options - Optional configuration. Set `mode` to control the
|
|
173
173
|
* worker execution mode (default, pipeline, or persistent).
|
|
174
174
|
*/
|
|
175
|
-
constructor(e,
|
|
175
|
+
constructor(e, r) {
|
|
176
176
|
h(this, "_worker");
|
|
177
|
-
|
|
178
|
-
type: "
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
if (r != null && r.workerURL)
|
|
178
|
+
this._worker = new Worker(r.workerURL, { type: "module" });
|
|
179
|
+
else if (e) {
|
|
180
|
+
const o = (r == null ? void 0 : r.mode) ?? "default", a = M[o](e.toString()), i = new Blob([a], {
|
|
181
|
+
type: "application/javascript"
|
|
182
|
+
});
|
|
183
|
+
this._worker = new Worker(URL.createObjectURL(i));
|
|
184
|
+
} else
|
|
185
|
+
throw new Error(
|
|
186
|
+
"Either workerFunction or options.workerURL must be provided to WorkerFactory."
|
|
187
|
+
);
|
|
181
188
|
}
|
|
182
189
|
/**
|
|
183
190
|
* Returns the underlying native `Worker` instance.
|
|
@@ -194,12 +201,12 @@ class b {
|
|
|
194
201
|
this.results = e;
|
|
195
202
|
}
|
|
196
203
|
}
|
|
197
|
-
function
|
|
198
|
-
return
|
|
199
|
-
(
|
|
204
|
+
function g(n, e = /* @__PURE__ */ new Set()) {
|
|
205
|
+
return n === null || typeof n != "object" ? [] : e.has(n) ? [] : (e.add(n), n instanceof ArrayBuffer || n instanceof MessagePort || typeof ImageBitmap < "u" && n instanceof ImageBitmap || typeof OffscreenCanvas < "u" && n instanceof OffscreenCanvas ? [n] : ArrayBuffer.isView(n) ? [n.buffer] : Array.isArray(n) ? n.flatMap((r) => g(r, e)) : Object.values(n).flatMap(
|
|
206
|
+
(r) => g(r, e)
|
|
200
207
|
));
|
|
201
208
|
}
|
|
202
|
-
class
|
|
209
|
+
class A {
|
|
203
210
|
/**
|
|
204
211
|
* Creates a new `MainWorkerFactory`.
|
|
205
212
|
*
|
|
@@ -212,13 +219,15 @@ class B {
|
|
|
212
219
|
this._workers = e.workers, this._threads = navigator.hardwareConcurrency;
|
|
213
220
|
}
|
|
214
221
|
/**
|
|
215
|
-
* Instantiates a {@link WorkerFactory} for the given worker
|
|
222
|
+
* Instantiates a {@link WorkerFactory} for the given worker configuration.
|
|
216
223
|
*
|
|
217
|
-
* @param
|
|
224
|
+
* @param config - The worker configuration containing `func` or `workerURL`.
|
|
218
225
|
* @returns A new `WorkerFactory` wrapping the worker.
|
|
219
226
|
*/
|
|
220
227
|
initWorker(e) {
|
|
221
|
-
return new k(e
|
|
228
|
+
return new k(e.func, {
|
|
229
|
+
workerURL: e.workerURL
|
|
230
|
+
});
|
|
222
231
|
}
|
|
223
232
|
/**
|
|
224
233
|
* Splits an array into up to `numChunks` evenly-sized sub-arrays.
|
|
@@ -236,16 +245,16 @@ class B {
|
|
|
236
245
|
* partitionArray([1, 2, 3, 4, 5], 3);
|
|
237
246
|
* // → [[1, 2], [3, 4], [5]]
|
|
238
247
|
*/
|
|
239
|
-
partitionArray(e,
|
|
248
|
+
partitionArray(e, r) {
|
|
240
249
|
if (!e.length) return [];
|
|
241
|
-
if (
|
|
242
|
-
const
|
|
250
|
+
if (r <= 0) throw new Error("numChunks must be positive");
|
|
251
|
+
const o = Math.min(r, e.length), a = Math.floor(e.length / o), i = e.length % o, f = [];
|
|
243
252
|
let l = 0;
|
|
244
|
-
for (let
|
|
245
|
-
const
|
|
246
|
-
|
|
253
|
+
for (let t = 0; t < o; t++) {
|
|
254
|
+
const s = a + (t < i ? 1 : 0);
|
|
255
|
+
f.push(e.slice(l, l + s)), l += s;
|
|
247
256
|
}
|
|
248
|
-
return
|
|
257
|
+
return f;
|
|
249
258
|
}
|
|
250
259
|
/**
|
|
251
260
|
* Looks up a registered worker configuration by name.
|
|
@@ -254,7 +263,7 @@ class B {
|
|
|
254
263
|
* @returns The matching config, or `undefined` if not found.
|
|
255
264
|
*/
|
|
256
265
|
findWorkerByName(e) {
|
|
257
|
-
return this._workers.find((
|
|
266
|
+
return this._workers.find((r) => r.name === e);
|
|
258
267
|
}
|
|
259
268
|
/**
|
|
260
269
|
* Runs a named worker against the provided data, distributing work across
|
|
@@ -283,20 +292,20 @@ class B {
|
|
|
283
292
|
* const settled = await foreman.runWorker('sum', { srcData: [1, 2, 3] });
|
|
284
293
|
*/
|
|
285
294
|
async runWorker(e, {
|
|
286
|
-
srcData:
|
|
287
|
-
...
|
|
295
|
+
srcData: r,
|
|
296
|
+
...o
|
|
288
297
|
}) {
|
|
289
298
|
const a = this.findWorkerByName(e);
|
|
290
299
|
if (!a)
|
|
291
300
|
return Promise.reject(new Error(`Worker "${e}" not found`));
|
|
292
|
-
const
|
|
301
|
+
const i = a.maxConcurrency ?? this._threads, f = !!(Array.isArray(r) && r.length > 1 && a.partition), l = f ? this.partitionArray(r, i) : r, t = this.createWorkerPromises(
|
|
293
302
|
a,
|
|
294
303
|
e,
|
|
295
|
-
{ data: l, ...
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
),
|
|
299
|
-
return new b(
|
|
304
|
+
{ data: l, ...o },
|
|
305
|
+
i,
|
|
306
|
+
f
|
|
307
|
+
), s = await Promise.allSettled(t);
|
|
308
|
+
return new b(s);
|
|
300
309
|
}
|
|
301
310
|
/**
|
|
302
311
|
* Builds the array of per-thread worker promises for a single `runWorker`
|
|
@@ -312,16 +321,17 @@ class B {
|
|
|
312
321
|
* @param isPartitioned - Whether `data` is a pre-split array of shards.
|
|
313
322
|
* @returns An array of promises, one per thread.
|
|
314
323
|
*/
|
|
315
|
-
createWorkerPromises(e,
|
|
316
|
-
const { data:
|
|
317
|
-
return Array.from({ length: a }, (
|
|
318
|
-
const
|
|
324
|
+
createWorkerPromises(e, r, o, a, i) {
|
|
325
|
+
const { data: f, ...l } = o;
|
|
326
|
+
return Array.from({ length: a }, (t, s) => {
|
|
327
|
+
const u = i && Array.isArray(f) ? f[s] : f;
|
|
319
328
|
return this.runWorkerWithRetry(
|
|
320
329
|
{
|
|
321
330
|
workerFunc: e.func,
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
331
|
+
workerURL: e.workerURL,
|
|
332
|
+
workerName: r,
|
|
333
|
+
index: s,
|
|
334
|
+
data: { data: u, ...l }
|
|
325
335
|
},
|
|
326
336
|
e.retries
|
|
327
337
|
);
|
|
@@ -339,16 +349,16 @@ class B {
|
|
|
339
349
|
* @returns The successful {@link WorkerResult} once the worker resolves.
|
|
340
350
|
* @throws The last caught error when all retries are exhausted.
|
|
341
351
|
*/
|
|
342
|
-
async runWorkerWithRetry(e,
|
|
352
|
+
async runWorkerWithRetry(e, r = 2) {
|
|
343
353
|
try {
|
|
344
354
|
return await this.initiateWorker(e);
|
|
345
|
-
} catch (
|
|
346
|
-
if (
|
|
355
|
+
} catch (o) {
|
|
356
|
+
if (r > 0)
|
|
347
357
|
return console.error(
|
|
348
|
-
`Worker ${e.index} failed, retrying (${
|
|
349
|
-
|
|
350
|
-
), this.runWorkerWithRetry(e,
|
|
351
|
-
throw console.error("Worker failed after all retries:",
|
|
358
|
+
`Worker ${e.index} failed, retrying (${r} left):`,
|
|
359
|
+
o
|
|
360
|
+
), this.runWorkerWithRetry(e, r - 1);
|
|
361
|
+
throw console.error("Worker failed after all retries:", o), o;
|
|
352
362
|
}
|
|
353
363
|
}
|
|
354
364
|
/**
|
|
@@ -366,48 +376,54 @@ class B {
|
|
|
366
376
|
* The underlying `Worker` is always terminated after the first message,
|
|
367
377
|
* whether it succeeded or failed.
|
|
368
378
|
*
|
|
369
|
-
* @param instanceConfig - Worker function, name, shard index, and data.
|
|
379
|
+
* @param instanceConfig - Worker function, URL, name, shard index, and data.
|
|
370
380
|
* @returns A promise that resolves with the worker's result.
|
|
371
381
|
*/
|
|
372
382
|
initiateWorker({
|
|
373
383
|
workerFunc: e,
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
384
|
+
workerURL: r,
|
|
385
|
+
workerName: o,
|
|
386
|
+
index: a,
|
|
387
|
+
data: i
|
|
377
388
|
}) {
|
|
378
|
-
return new Promise((f,
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
389
|
+
return new Promise((f, l) => {
|
|
390
|
+
const s = this.initWorker({
|
|
391
|
+
name: o,
|
|
392
|
+
role: "",
|
|
393
|
+
func: e,
|
|
394
|
+
workerURL: r
|
|
395
|
+
}).getWorker;
|
|
396
|
+
s.onerror = (c) => {
|
|
397
|
+
s.terminate(), l({
|
|
398
|
+
index: a,
|
|
399
|
+
workerConfigs: { workerFunc: e, workerName: o, index: a, data: i },
|
|
400
|
+
failedResult: c
|
|
385
401
|
});
|
|
386
|
-
},
|
|
387
|
-
var
|
|
388
|
-
if (((
|
|
389
|
-
|
|
390
|
-
index:
|
|
391
|
-
workerConfigs: { workerFunc: e, workerName:
|
|
402
|
+
}, s.onmessage = (c) => {
|
|
403
|
+
var d, p;
|
|
404
|
+
if (((d = c.data) == null ? void 0 : d.ok) === !1) {
|
|
405
|
+
s.terminate(), l({
|
|
406
|
+
index: a,
|
|
407
|
+
workerConfigs: { workerFunc: e, workerName: o, index: a, data: i },
|
|
392
408
|
failedResult: new ErrorEvent("error", {
|
|
393
|
-
message:
|
|
409
|
+
message: c.data.error
|
|
394
410
|
})
|
|
395
411
|
});
|
|
396
412
|
return;
|
|
397
413
|
}
|
|
398
414
|
f({
|
|
399
|
-
index:
|
|
400
|
-
workerConfigs: { workerFunc: e, workerName:
|
|
415
|
+
index: a,
|
|
416
|
+
workerConfigs: { workerFunc: e, workerName: o, index: a, data: i },
|
|
401
417
|
successResult: new MessageEvent("message", {
|
|
402
|
-
data: (
|
|
418
|
+
data: (p = c.data) == null ? void 0 : p.data
|
|
403
419
|
})
|
|
404
|
-
}),
|
|
420
|
+
}), s.terminate();
|
|
405
421
|
};
|
|
406
|
-
const
|
|
407
|
-
index:
|
|
408
|
-
...Array.isArray(
|
|
422
|
+
const u = {
|
|
423
|
+
index: a,
|
|
424
|
+
...Array.isArray(i) ? { data: i } : i
|
|
409
425
|
};
|
|
410
|
-
|
|
426
|
+
s.postMessage(u, g(u));
|
|
411
427
|
});
|
|
412
428
|
}
|
|
413
429
|
/**
|
|
@@ -442,16 +458,16 @@ class B {
|
|
|
442
458
|
* reducer: (shards) => shards.flat().reduce((a, b) => a + b, 0),
|
|
443
459
|
* });
|
|
444
460
|
*/
|
|
445
|
-
async collectResults(e,
|
|
446
|
-
const
|
|
447
|
-
(
|
|
461
|
+
async collectResults(e, r = {}) {
|
|
462
|
+
const o = e.results.filter(
|
|
463
|
+
(t) => t.status === "fulfilled"
|
|
448
464
|
), a = e.results.filter(
|
|
449
|
-
(
|
|
450
|
-
),
|
|
465
|
+
(t) => t.status === "rejected"
|
|
466
|
+
), i = o.map((t) => t.value.successResult.data), f = r.reducer ? r.reducer.toString() : "(shards) => shards.flat()";
|
|
451
467
|
return {
|
|
452
|
-
data: await new Promise((
|
|
453
|
-
const
|
|
454
|
-
const reducer = ${
|
|
468
|
+
data: await new Promise((t, s) => {
|
|
469
|
+
const u = `
|
|
470
|
+
const reducer = ${f};
|
|
455
471
|
self.addEventListener('message', (event) => {
|
|
456
472
|
try {
|
|
457
473
|
const result = reducer(event.data);
|
|
@@ -460,14 +476,14 @@ class B {
|
|
|
460
476
|
self.postMessage({ ok: false, error: String(err) });
|
|
461
477
|
}
|
|
462
478
|
});
|
|
463
|
-
`, c = new Blob([
|
|
464
|
-
d.onmessage = (
|
|
465
|
-
d.terminate(),
|
|
466
|
-
}, d.onerror = (
|
|
467
|
-
d.terminate(),
|
|
468
|
-
}, d.postMessage(
|
|
479
|
+
`, c = new Blob([u], { type: "application/javascript" }), d = new Worker(URL.createObjectURL(c));
|
|
480
|
+
d.onmessage = (p) => {
|
|
481
|
+
d.terminate(), p.data.ok ? t(p.data.data) : s(new Error(p.data.error));
|
|
482
|
+
}, d.onerror = (p) => {
|
|
483
|
+
d.terminate(), s(p);
|
|
484
|
+
}, d.postMessage(i);
|
|
469
485
|
}),
|
|
470
|
-
succeeded:
|
|
486
|
+
succeeded: o.length,
|
|
471
487
|
failed: a.length,
|
|
472
488
|
errors: a
|
|
473
489
|
};
|
|
@@ -502,56 +518,57 @@ class B {
|
|
|
502
518
|
if (e.length === 0)
|
|
503
519
|
throw new Error("Pipeline requires at least one step");
|
|
504
520
|
if (e.length === 1) {
|
|
505
|
-
const
|
|
506
|
-
if (!
|
|
507
|
-
const
|
|
508
|
-
return new Promise((
|
|
509
|
-
|
|
510
|
-
var
|
|
511
|
-
|
|
512
|
-
},
|
|
513
|
-
|
|
521
|
+
const r = e[0], o = this.findWorkerByName(r.worker);
|
|
522
|
+
if (!o) throw new Error(`Worker "${r.worker}" not found`);
|
|
523
|
+
const i = this.initWorker(o).getWorker;
|
|
524
|
+
return new Promise((f, l) => {
|
|
525
|
+
i.onmessage = (s) => {
|
|
526
|
+
var u, c;
|
|
527
|
+
i.terminate(), ((u = s.data) == null ? void 0 : u.ok) === !1 ? l(new Error(s.data.error)) : f((c = s.data) == null ? void 0 : c.data);
|
|
528
|
+
}, i.onerror = (s) => {
|
|
529
|
+
i.terminate(), l(s);
|
|
514
530
|
};
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
{ data:
|
|
518
|
-
|
|
531
|
+
const t = r.srcData ?? {};
|
|
532
|
+
i.postMessage(
|
|
533
|
+
{ data: t, index: 0 },
|
|
534
|
+
g(t)
|
|
519
535
|
);
|
|
520
536
|
});
|
|
521
537
|
}
|
|
522
|
-
return new Promise((
|
|
523
|
-
const a = [],
|
|
524
|
-
for (const
|
|
525
|
-
const
|
|
526
|
-
if (!
|
|
527
|
-
|
|
538
|
+
return new Promise((r, o) => {
|
|
539
|
+
const a = [], i = [];
|
|
540
|
+
for (const t of e) {
|
|
541
|
+
const s = this.findWorkerByName(t.worker);
|
|
542
|
+
if (!s) {
|
|
543
|
+
o(new Error(`Worker "${t.worker}" not found`));
|
|
528
544
|
return;
|
|
529
545
|
}
|
|
530
|
-
const
|
|
531
|
-
mode:
|
|
546
|
+
const u = new k(s.func, {
|
|
547
|
+
mode: w.Pipeline,
|
|
548
|
+
workerURL: s.workerURL
|
|
532
549
|
});
|
|
533
|
-
a.push(
|
|
550
|
+
a.push(u.getWorker);
|
|
534
551
|
}
|
|
535
|
-
for (let
|
|
536
|
-
|
|
537
|
-
for (let
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
{ __pipeline_ports__: !0, ...
|
|
541
|
-
|
|
552
|
+
for (let t = 0; t < a.length - 1; t++)
|
|
553
|
+
i.push(new MessageChannel());
|
|
554
|
+
for (let t = 0; t < a.length; t++) {
|
|
555
|
+
const s = [], u = {};
|
|
556
|
+
t > 0 && (u.inputPort = i[t - 1].port1, s.push(u.inputPort)), t < a.length - 1 && (u.outputPort = i[t].port2, s.push(u.outputPort)), a[t].postMessage(
|
|
557
|
+
{ __pipeline_ports__: !0, ...u },
|
|
558
|
+
s
|
|
542
559
|
);
|
|
543
560
|
}
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
var
|
|
547
|
-
a.forEach((c) => c.terminate()), ((
|
|
548
|
-
},
|
|
549
|
-
a.forEach((
|
|
561
|
+
const f = a[a.length - 1];
|
|
562
|
+
f.onmessage = (t) => {
|
|
563
|
+
var s, u;
|
|
564
|
+
a.forEach((c) => c.terminate()), ((s = t.data) == null ? void 0 : s.ok) === !1 ? o(new Error(t.data.error)) : r((u = t.data) == null ? void 0 : u.data);
|
|
565
|
+
}, f.onerror = (t) => {
|
|
566
|
+
a.forEach((s) => s.terminate()), o(t);
|
|
550
567
|
};
|
|
551
568
|
const l = e[0].srcData ?? {};
|
|
552
569
|
a[0].postMessage(
|
|
553
570
|
{ data: l, index: 0 },
|
|
554
|
-
|
|
571
|
+
g(l)
|
|
555
572
|
);
|
|
556
573
|
});
|
|
557
574
|
}
|
|
@@ -589,24 +606,25 @@ class B {
|
|
|
589
606
|
* // Release when done
|
|
590
607
|
* factory.release('transform');
|
|
591
608
|
*/
|
|
592
|
-
async runPersistent(e,
|
|
593
|
-
const
|
|
594
|
-
if (!
|
|
609
|
+
async runPersistent(e, r) {
|
|
610
|
+
const o = this.findWorkerByName(e);
|
|
611
|
+
if (!o) throw new Error(`Worker "${e}" not found`);
|
|
595
612
|
let a = this._persistentWorkers.get(e);
|
|
596
|
-
return a || (a = new k(
|
|
597
|
-
mode:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
613
|
+
return a || (a = new k(o.func, {
|
|
614
|
+
mode: w.Persistent,
|
|
615
|
+
workerURL: o.workerURL
|
|
616
|
+
}).getWorker, this._persistentWorkers.set(e, a)), new Promise((i, f) => {
|
|
617
|
+
a.onmessage = (t) => {
|
|
618
|
+
var s, u;
|
|
619
|
+
((s = t.data) == null ? void 0 : s.ok) === !1 ? f(new Error(t.data.error)) : i((u = t.data) == null ? void 0 : u.data);
|
|
620
|
+
}, a.onerror = (t) => {
|
|
621
|
+
f(t);
|
|
604
622
|
};
|
|
605
623
|
const l = {
|
|
606
624
|
type: "run",
|
|
607
|
-
config:
|
|
625
|
+
config: r.config
|
|
608
626
|
};
|
|
609
|
-
|
|
627
|
+
r.dataset !== void 0 && (l.dataset = r.dataset), a.postMessage(l, g(l));
|
|
610
628
|
});
|
|
611
629
|
}
|
|
612
630
|
/**
|
|
@@ -619,11 +637,11 @@ class B {
|
|
|
619
637
|
* @param workerName - Name of the persistent worker to release.
|
|
620
638
|
*/
|
|
621
639
|
release(e) {
|
|
622
|
-
const
|
|
623
|
-
|
|
640
|
+
const r = this._persistentWorkers.get(e);
|
|
641
|
+
r && (r.postMessage({ type: "release" }), r.terminate(), this._persistentWorkers.delete(e));
|
|
624
642
|
}
|
|
625
643
|
}
|
|
626
644
|
export {
|
|
627
|
-
|
|
645
|
+
A as MainWorkerFactory,
|
|
628
646
|
k as WorkerFactory
|
|
629
647
|
};
|
|
@@ -45,9 +45,9 @@ declare class MainWorkerFactory<TConfigs extends readonly WorkerConfig<WorkerFun
|
|
|
45
45
|
workers: TConfigs;
|
|
46
46
|
});
|
|
47
47
|
/**
|
|
48
|
-
* Instantiates a {@link WorkerFactory} for the given worker
|
|
48
|
+
* Instantiates a {@link WorkerFactory} for the given worker configuration.
|
|
49
49
|
*
|
|
50
|
-
* @param
|
|
50
|
+
* @param config - The worker configuration containing `func` or `workerURL`.
|
|
51
51
|
* @returns A new `WorkerFactory` wrapping the worker.
|
|
52
52
|
*/
|
|
53
53
|
private initWorker;
|
|
@@ -147,7 +147,7 @@ declare class MainWorkerFactory<TConfigs extends readonly WorkerConfig<WorkerFun
|
|
|
147
147
|
* The underlying `Worker` is always terminated after the first message,
|
|
148
148
|
* whether it succeeded or failed.
|
|
149
149
|
*
|
|
150
|
-
* @param instanceConfig - Worker function, name, shard index, and data.
|
|
150
|
+
* @param instanceConfig - Worker function, URL, name, shard index, and data.
|
|
151
151
|
* @returns A promise that resolves with the worker's result.
|
|
152
152
|
*/
|
|
153
153
|
private initiateWorker;
|
|
@@ -23,8 +23,13 @@ export interface WorkerConfig<TFunc extends WorkerFunction = WorkerFunction> {
|
|
|
23
23
|
name: WorkerName;
|
|
24
24
|
/** Human-readable role label (e.g. `'compute'`, `'transform'`). */
|
|
25
25
|
role: WorkerRole;
|
|
26
|
-
/** The worker function that will be serialised and run in a thread. */
|
|
27
|
-
func
|
|
26
|
+
/** The worker function that will be serialised and run in a thread. Optional if `workerURL` is provided. */
|
|
27
|
+
func?: TFunc;
|
|
28
|
+
/**
|
|
29
|
+
* The worker script URL (or `URL` object created via `new URL(..., import.meta.url)`).
|
|
30
|
+
* Enables module bundlers like Webpack 5, Vite, Rollup, and Parcel to bundle worker code and its dependencies.
|
|
31
|
+
*/
|
|
32
|
+
workerURL?: string | URL;
|
|
28
33
|
/**
|
|
29
34
|
* Maximum number of parallel threads to spawn for this worker.
|
|
30
35
|
* Defaults to `navigator.hardwareConcurrency` when omitted.
|
|
@@ -41,6 +46,11 @@ export interface WorkerConfig<TFunc extends WorkerFunction = WorkerFunction> {
|
|
|
41
46
|
* array.
|
|
42
47
|
*/
|
|
43
48
|
partition?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* A list of worker functions that must complete before this worker can start.
|
|
51
|
+
* If any dependency fails, this worker will not run.
|
|
52
|
+
*/
|
|
53
|
+
dependencies?: Array<() => void>;
|
|
44
54
|
}
|
|
45
55
|
/**
|
|
46
56
|
* Derives a `name → function` map from a readonly tuple of
|
|
@@ -52,9 +62,11 @@ export interface WorkerConfig<TFunc extends WorkerFunction = WorkerFunction> {
|
|
|
52
62
|
* @typeParam T - The readonly tuple of `WorkerConfig` values.
|
|
53
63
|
*/
|
|
54
64
|
export type WorkerConfigMap<T extends readonly WorkerConfig<WorkerFunction<any, any>>[]> = {
|
|
55
|
-
[K in T[number]['name']]: Extract<T[number], {
|
|
65
|
+
[K in T[number]['name']]: NonNullable<Extract<T[number], {
|
|
66
|
+
name: K;
|
|
67
|
+
}>['func']> extends WorkerFunction ? NonNullable<Extract<T[number], {
|
|
56
68
|
name: K;
|
|
57
|
-
}>['func'];
|
|
69
|
+
}>['func']> : WorkerFunction;
|
|
58
70
|
};
|
|
59
71
|
/**
|
|
60
72
|
* Extracts the `params` type from a {@link WorkerFunction}.
|
|
@@ -91,8 +103,10 @@ export interface MainWorkerFactoryWorker extends WorkerConfig {
|
|
|
91
103
|
export interface WorkerInstanceConfig<TFunc extends WorkerFunction = WorkerFunction> {
|
|
92
104
|
/** Name of the parent worker config, used in logs and error objects. */
|
|
93
105
|
workerName: WorkerName;
|
|
94
|
-
/** The function serialised and executed inside the thread. */
|
|
95
|
-
workerFunc
|
|
106
|
+
/** The function serialised and executed inside the thread (optional if `workerURL` is set). */
|
|
107
|
+
workerFunc?: TFunc;
|
|
108
|
+
/** Worker script URL (string or URL object). */
|
|
109
|
+
workerURL?: string | URL;
|
|
96
110
|
/** Zero-based shard index assigned to this thread. */
|
|
97
111
|
index: number;
|
|
98
112
|
/** The data payload (full or partitioned shard) sent to the thread. */
|
|
@@ -7,9 +7,11 @@ export declare enum WorkerMode {
|
|
|
7
7
|
export interface WorkerFactoryOptions {
|
|
8
8
|
/** The worker execution mode. Defaults to `WorkerMode.Default`. */
|
|
9
9
|
mode?: WorkerMode;
|
|
10
|
+
/** The worker URL to use instead of creating a worker from a function. */
|
|
11
|
+
workerURL?: string | URL;
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
|
-
* Low-level factory that
|
|
14
|
+
* Low-level factory that serializes a {@link WorkerFunction} into a Blob URL
|
|
13
15
|
* and spawns a native `Worker` from it.
|
|
14
16
|
*
|
|
15
17
|
* `WorkerFactory` is an internal building block used by `MainWorkerFactory`.
|
|
@@ -39,11 +41,11 @@ declare class WorkerFactory {
|
|
|
39
41
|
*
|
|
40
42
|
* @param workerFunction - The function to run inside the worker thread.
|
|
41
43
|
* Must be self-contained — it cannot reference variables from the outer
|
|
42
|
-
* scope because it is
|
|
44
|
+
* scope because it is serialized via `.toString()`.
|
|
43
45
|
* @param options - Optional configuration. Set `mode` to control the
|
|
44
46
|
* worker execution mode (default, pipeline, or persistent).
|
|
45
47
|
*/
|
|
46
|
-
constructor(workerFunction
|
|
48
|
+
constructor(workerFunction?: WorkerFunction, options?: WorkerFactoryOptions);
|
|
47
49
|
/**
|
|
48
50
|
* Returns the underlying native `Worker` instance.
|
|
49
51
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@offmain/workerkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.2",
|
|
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",
|
|
@@ -76,6 +76,9 @@
|
|
|
76
76
|
"typescript": "^5.7.2",
|
|
77
77
|
"vite": "^6.3.3",
|
|
78
78
|
"vite-plugin-dts": "^4.5.4",
|
|
79
|
-
"vitest": "^1.0.0"
|
|
79
|
+
"vitest": "^1.0.0",
|
|
80
|
+
"luxon": "^3.7.2",
|
|
81
|
+
"i18next": "^25.5.1",
|
|
82
|
+
"@types/luxon": "^3.7.2"
|
|
80
83
|
}
|
|
81
84
|
}
|