@atolljs/node 0.1.12 → 0.1.14
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/dist/index.js +4 -5
- package/dist/redis.js +1 -4
- package/package.json +2 -2
- package/dist/chunk-VEJT6OVD.js +0 -1040
package/dist/index.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
WorkerPool,
|
|
3
|
-
bindSharedMemories
|
|
4
|
-
} from "./chunk-VEJT6OVD.js";
|
|
5
|
-
|
|
6
1
|
// src/worker.ts
|
|
7
2
|
import { Worker as NodeWorker } from "node:worker_threads";
|
|
8
3
|
var NodeWorkerAdapter = class {
|
|
@@ -62,6 +57,9 @@ function createNodeWorker(source, options) {
|
|
|
62
57
|
|
|
63
58
|
// src/pool.ts
|
|
64
59
|
import { Worker as NodeWorker2 } from "node:worker_threads";
|
|
60
|
+
import {
|
|
61
|
+
WorkerPool
|
|
62
|
+
} from "@atolljs/core";
|
|
65
63
|
var toWorker = (w) => w instanceof NodeWorker2 ? createNodeWorker(w) : w;
|
|
66
64
|
function createNodePool(config) {
|
|
67
65
|
const { workerFile, worker, createWorker, ...rest } = config;
|
|
@@ -80,6 +78,7 @@ function createNodePool(config) {
|
|
|
80
78
|
|
|
81
79
|
// src/sharedBuffer.ts
|
|
82
80
|
import { isMainThread, parentPort, workerData } from "node:worker_threads";
|
|
81
|
+
import { bindSharedMemories } from "@atolljs/core";
|
|
83
82
|
var SHARED_BUFFER = "SHARED_BUFFER";
|
|
84
83
|
function withSharedBuffer(spawn, buffer) {
|
|
85
84
|
return () => {
|
package/dist/redis.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atolljs/node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Node runtime adapter — node:worker_threads pools, no framework",
|
|
5
5
|
"homepage": "https://jwhenry3.github.io/atolljs/consumer/quickstart/",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"registry": "https://registry.npmjs.org"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@atolljs/core": "0.1.
|
|
37
|
+
"@atolljs/core": "^0.1.14"
|
|
38
38
|
},
|
|
39
39
|
"repository": {
|
|
40
40
|
"type": "git",
|
package/dist/chunk-VEJT6OVD.js
DELETED
|
@@ -1,1040 +0,0 @@
|
|
|
1
|
-
// ../../src/devtools.ts
|
|
2
|
-
var THREAD = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope ? "worker" : "main";
|
|
3
|
-
var sink = null;
|
|
4
|
-
var emitDevtools = (event) => {
|
|
5
|
-
if (sink) sink({ ...event, at: performance.now(), thread: THREAD });
|
|
6
|
-
};
|
|
7
|
-
var forwardDevtools = (event) => {
|
|
8
|
-
if (sink) sink(event);
|
|
9
|
-
};
|
|
10
|
-
var devtoolsEnabled = () => sink !== null;
|
|
11
|
-
var previewValue = (value, max = 2048) => {
|
|
12
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
13
|
-
let out;
|
|
14
|
-
try {
|
|
15
|
-
out = JSON.stringify(value, (_k, v) => {
|
|
16
|
-
if (typeof v === "function") return "[fn]";
|
|
17
|
-
if (typeof v === "bigint") return `${v}n`;
|
|
18
|
-
if (v instanceof ArrayBuffer) return `[ArrayBuffer ${v.byteLength}B]`;
|
|
19
|
-
if (ArrayBuffer.isView(v)) return `[${v.constructor.name} ${v.byteLength}B]`;
|
|
20
|
-
if (v instanceof Map) return { "[Map]": [...v.entries()].slice(0, 50) };
|
|
21
|
-
if (v instanceof Set) return { "[Set]": [...v].slice(0, 50) };
|
|
22
|
-
if (v && typeof v === "object") {
|
|
23
|
-
if (seen.has(v)) return "[cycle]";
|
|
24
|
-
seen.add(v);
|
|
25
|
-
}
|
|
26
|
-
return v;
|
|
27
|
-
}) ?? String(value);
|
|
28
|
-
} catch {
|
|
29
|
-
out = String(value);
|
|
30
|
-
}
|
|
31
|
-
return out.length > max ? `${out.slice(0, max)}\u2026` : out;
|
|
32
|
-
};
|
|
33
|
-
var estimateCloneBytes = (value, budget = 5e3) => {
|
|
34
|
-
let nodes = 0;
|
|
35
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
36
|
-
const walk = (v) => {
|
|
37
|
-
if (++nodes > budget) return 0;
|
|
38
|
-
switch (typeof v) {
|
|
39
|
-
case "string":
|
|
40
|
-
return 4 + v.length * 2;
|
|
41
|
-
case "number":
|
|
42
|
-
return 8;
|
|
43
|
-
case "boolean":
|
|
44
|
-
return 4;
|
|
45
|
-
case "bigint":
|
|
46
|
-
return 16;
|
|
47
|
-
case "undefined":
|
|
48
|
-
return 1;
|
|
49
|
-
case "object": {
|
|
50
|
-
if (v === null) return 1;
|
|
51
|
-
if (v instanceof ArrayBuffer) return v.byteLength;
|
|
52
|
-
if (ArrayBuffer.isView(v)) return v.byteLength;
|
|
53
|
-
if (typeof SharedArrayBuffer !== "undefined" && v instanceof SharedArrayBuffer) return 8;
|
|
54
|
-
if (seen.has(v)) return 4;
|
|
55
|
-
seen.add(v);
|
|
56
|
-
let n = 8;
|
|
57
|
-
if (v instanceof Map) for (const [k, x] of v) n += walk(k) + walk(x);
|
|
58
|
-
else if (v instanceof Set) for (const x of v) n += walk(x);
|
|
59
|
-
else if (Array.isArray(v)) for (const x of v) n += walk(x);
|
|
60
|
-
else for (const k of Object.keys(v)) n += 4 + k.length * 2 + walk(v[k]);
|
|
61
|
-
return n;
|
|
62
|
-
}
|
|
63
|
-
default:
|
|
64
|
-
return 0;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
return walk(value);
|
|
68
|
-
};
|
|
69
|
-
var commands = /* @__PURE__ */ new Map();
|
|
70
|
-
var registerDevtoolsCommand = (name, handler) => {
|
|
71
|
-
commands.set(name, handler);
|
|
72
|
-
return () => {
|
|
73
|
-
if (commands.get(name) === handler) commands.delete(name);
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
var BODY_PREVIEW = 8 * 1024;
|
|
77
|
-
var seq = 0;
|
|
78
|
-
var named = /* @__PURE__ */ new Map();
|
|
79
|
-
var nextDevtoolsId = (prefix, name) => {
|
|
80
|
-
const slug = name?.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
81
|
-
if (!slug) return `${prefix}-${++seq}`;
|
|
82
|
-
const base = `${slug}-${prefix === "pool" ? "p" : prefix === "worker" ? "w" : prefix}`;
|
|
83
|
-
const n = (named.get(base) ?? 0) + 1;
|
|
84
|
-
named.set(base, n);
|
|
85
|
-
return n === 1 ? base : `${base}${n}`;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// ../../src/log.ts
|
|
89
|
-
var LEVELS = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, off: 5 };
|
|
90
|
-
var THREAD2 = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope ? "worker" : "main";
|
|
91
|
-
var consoleSink = ({ level, scope, message, data, thread, at }) => {
|
|
92
|
-
const prefix = `[${(at / 1e3).toFixed(2)}s ${thread}:${scope}]`;
|
|
93
|
-
const fn = level === "error" ? console.error : level === "warn" ? console.warn : level === "info" ? console.info : console.debug;
|
|
94
|
-
if (data !== void 0) fn(`${prefix} ${message}`, data);
|
|
95
|
-
else fn(`${prefix} ${message}`);
|
|
96
|
-
};
|
|
97
|
-
var minLevel = "info";
|
|
98
|
-
var sink2 = consoleSink;
|
|
99
|
-
var teeing = false;
|
|
100
|
-
function log(level, scope, message, data) {
|
|
101
|
-
if (LEVELS[level] < LEVELS[minLevel]) return;
|
|
102
|
-
sink2({ level, scope, message, data, thread: THREAD2, at: performance.now() });
|
|
103
|
-
if (!teeing && devtoolsEnabled()) {
|
|
104
|
-
teeing = true;
|
|
105
|
-
try {
|
|
106
|
-
emitDevtools({
|
|
107
|
-
type: "log",
|
|
108
|
-
level,
|
|
109
|
-
scope,
|
|
110
|
-
message,
|
|
111
|
-
...data !== void 0 ? { data: previewValue(data, 1024) } : {}
|
|
112
|
-
});
|
|
113
|
-
} finally {
|
|
114
|
-
teeing = false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
function scoped(scope) {
|
|
119
|
-
return {
|
|
120
|
-
trace: (message, data) => log("trace", scope, message, data),
|
|
121
|
-
debug: (message, data) => log("debug", scope, message, data),
|
|
122
|
-
info: (message, data) => log("info", scope, message, data),
|
|
123
|
-
warn: (message, data) => log("warn", scope, message, data),
|
|
124
|
-
error: (message, data) => log("error", scope, message, data)
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
var fmtBytes = (bytes) => bytes >= 1 << 20 ? `${(bytes / (1 << 20)).toFixed(1)}MB` : bytes >= 1024 ? `${(bytes / 1024).toFixed(1)}KB` : `${bytes}B`;
|
|
128
|
-
|
|
129
|
-
// ../../src/contract/sharedMemory.ts
|
|
130
|
-
var memLog = scoped("memory");
|
|
131
|
-
var codecTextEncoder = new TextEncoder();
|
|
132
|
-
var codecTextDecoder = new TextDecoder();
|
|
133
|
-
var CONNECTOR_FACTORIES = /* @__PURE__ */ new Map();
|
|
134
|
-
function registerConnectorFactory(kind, factory) {
|
|
135
|
-
CONNECTOR_FACTORIES.set(kind, factory);
|
|
136
|
-
}
|
|
137
|
-
var TYPED_ARRAYS = {
|
|
138
|
-
Int32: Int32Array,
|
|
139
|
-
Float64: Float64Array,
|
|
140
|
-
BigInt64: BigInt64Array,
|
|
141
|
-
Uint8: Uint8Array
|
|
142
|
-
};
|
|
143
|
-
registerConnectorFactory("number", (d, ctx, byteOffset) => {
|
|
144
|
-
const view = new Float64Array(ctx.buffer, byteOffset, 1);
|
|
145
|
-
return { byteOffset, byteLength: d.byteLength, read: () => view[0], write: (v) => {
|
|
146
|
-
view[0] = v;
|
|
147
|
-
} };
|
|
148
|
-
});
|
|
149
|
-
registerConnectorFactory("boolean", (d, ctx, byteOffset) => {
|
|
150
|
-
const view = new Uint8Array(ctx.buffer, byteOffset, 1);
|
|
151
|
-
return { byteOffset, byteLength: d.byteLength, read: () => view[0] === 1, write: (v) => {
|
|
152
|
-
view[0] = v ? 1 : 0;
|
|
153
|
-
} };
|
|
154
|
-
});
|
|
155
|
-
for (const kind of Object.keys(TYPED_ARRAYS)) {
|
|
156
|
-
registerConnectorFactory(kind, (d, ctx, byteOffset) => {
|
|
157
|
-
const Ctor = TYPED_ARRAYS[kind];
|
|
158
|
-
const view = new Ctor(ctx.buffer, byteOffset, d.byteLength / Ctor.BYTES_PER_ELEMENT);
|
|
159
|
-
return { byteOffset, byteLength: d.byteLength, read: () => view, write: (v) => view.set(v) };
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
registerConnectorFactory("string", (d, ctx, byteOffset) => {
|
|
163
|
-
const encoder = new TextEncoder();
|
|
164
|
-
const decoder = new TextDecoder();
|
|
165
|
-
const capacity = d.byteLength - 4;
|
|
166
|
-
const lengthView = new Uint32Array(ctx.buffer, byteOffset, 1);
|
|
167
|
-
const writeBytes = (bytes) => {
|
|
168
|
-
if (bytes.byteLength > capacity) {
|
|
169
|
-
throw new Error(`Encoded value of ${bytes.byteLength} bytes exceeds field capacity of ${capacity} bytes at offset ${byteOffset}`);
|
|
170
|
-
}
|
|
171
|
-
lengthView[0] = bytes.byteLength;
|
|
172
|
-
new Uint8Array(ctx.buffer, byteOffset + 4, bytes.byteLength).set(bytes);
|
|
173
|
-
};
|
|
174
|
-
const readBytes = () => new Uint8Array(ctx.buffer, byteOffset + 4, lengthView[0]).slice();
|
|
175
|
-
return {
|
|
176
|
-
byteOffset,
|
|
177
|
-
byteLength: d.byteLength,
|
|
178
|
-
read: () => decoder.decode(readBytes()),
|
|
179
|
-
write: (v) => writeBytes(encoder.encode(d.schema ? d.schema.parse(v) : v))
|
|
180
|
-
};
|
|
181
|
-
});
|
|
182
|
-
function makeIO(ctx) {
|
|
183
|
-
const view = new DataView(ctx.buffer);
|
|
184
|
-
const byteView = new Uint8Array(ctx.buffer);
|
|
185
|
-
const encoder = new TextEncoder();
|
|
186
|
-
const decoder = new TextDecoder();
|
|
187
|
-
const scalarIO = {
|
|
188
|
-
i8: { get: (o) => view.getInt8(o), set: (o, v) => view.setInt8(o, v) },
|
|
189
|
-
u8: { get: (o) => view.getUint8(o), set: (o, v) => view.setUint8(o, v) },
|
|
190
|
-
i16: { get: (o) => view.getInt16(o), set: (o, v) => view.setInt16(o, v) },
|
|
191
|
-
u16: { get: (o) => view.getUint16(o), set: (o, v) => view.setUint16(o, v) },
|
|
192
|
-
i32: { get: (o) => view.getInt32(o), set: (o, v) => view.setInt32(o, v) },
|
|
193
|
-
u32: { get: (o) => view.getUint32(o), set: (o, v) => view.setUint32(o, v) },
|
|
194
|
-
f32: { get: (o) => view.getFloat32(o), set: (o, v) => view.setFloat32(o, v) },
|
|
195
|
-
f64: { get: (o) => view.getFloat64(o), set: (o, v) => view.setFloat64(o, v) },
|
|
196
|
-
i64: { get: (o) => view.getBigInt64(o), set: (o, v) => view.setBigInt64(o, v) },
|
|
197
|
-
u64: { get: (o) => view.getBigUint64(o), set: (o, v) => view.setBigUint64(o, v) }
|
|
198
|
-
};
|
|
199
|
-
const readString = (offset, max) => {
|
|
200
|
-
const zero = byteView.indexOf(0, offset);
|
|
201
|
-
const len = zero === -1 || zero - offset >= max ? max : zero - offset;
|
|
202
|
-
return decoder.decode(byteView.slice(offset, offset + len));
|
|
203
|
-
};
|
|
204
|
-
let scratch = new Uint8Array(64);
|
|
205
|
-
const writeString = (offset, max, value) => {
|
|
206
|
-
if (scratch.length < max) scratch = new Uint8Array(max);
|
|
207
|
-
const { read, written } = encoder.encodeInto(value, scratch);
|
|
208
|
-
if (read < value.length || written > max) {
|
|
209
|
-
throw new Error(`String exceeds inline field capacity of ${max} bytes`);
|
|
210
|
-
}
|
|
211
|
-
byteView.set(scratch.subarray(0, written), offset);
|
|
212
|
-
if (written < max) byteView.fill(0, offset + written, offset + max);
|
|
213
|
-
};
|
|
214
|
-
const ops = (spec) => {
|
|
215
|
-
if (typeof spec === "string") {
|
|
216
|
-
const io = scalarIO[spec];
|
|
217
|
-
return { get: (o) => io.get(o), set: (o, v) => io.set(o, v) };
|
|
218
|
-
}
|
|
219
|
-
if ("bool" in spec) {
|
|
220
|
-
return { get: (o) => !!view.getUint8(o), set: (o, v) => view.setUint8(o, v ? 1 : 0) };
|
|
221
|
-
}
|
|
222
|
-
return { get: (o) => readString(o, spec.string), set: (o, v) => writeString(o, spec.string, v) };
|
|
223
|
-
};
|
|
224
|
-
const compile = (layout, offsets) => {
|
|
225
|
-
const readers = /* @__PURE__ */ new Map();
|
|
226
|
-
const writers = /* @__PURE__ */ new Map();
|
|
227
|
-
for (const key of Object.keys(layout)) {
|
|
228
|
-
const { get, set } = ops(layout[key]);
|
|
229
|
-
const rel = offsets[key];
|
|
230
|
-
readers.set(key, (base, out) => {
|
|
231
|
-
out[key] = get(base + rel);
|
|
232
|
-
});
|
|
233
|
-
writers.set(key, (base, record) => set(base + rel, record[key]));
|
|
234
|
-
}
|
|
235
|
-
const allReaders = [...readers.values()];
|
|
236
|
-
const allWriters = [...writers.values()];
|
|
237
|
-
return {
|
|
238
|
-
read(base, out = {}, fieldNames) {
|
|
239
|
-
if (fieldNames) {
|
|
240
|
-
for (const key of fieldNames) readers.get(key)(base, out);
|
|
241
|
-
} else {
|
|
242
|
-
for (const read of allReaders) read(base, out);
|
|
243
|
-
}
|
|
244
|
-
return out;
|
|
245
|
-
},
|
|
246
|
-
write(base, record, fieldNames) {
|
|
247
|
-
if (fieldNames) {
|
|
248
|
-
for (const key of fieldNames) writers.get(key)(base, record);
|
|
249
|
-
} else {
|
|
250
|
-
for (const write of allWriters) write(base, record);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
};
|
|
255
|
-
return { ops, compile };
|
|
256
|
-
}
|
|
257
|
-
var structuredFactory = (d, ctx, byteOffset) => {
|
|
258
|
-
const { schema, byteLength, kind } = d;
|
|
259
|
-
const fixed = d;
|
|
260
|
-
if (fixed.layout && fixed.offsets && fixed.recordSize !== void 0) {
|
|
261
|
-
const flag = new Uint32Array(ctx.buffer, byteOffset, 2);
|
|
262
|
-
const base = byteOffset + 8;
|
|
263
|
-
const record = makeIO(ctx).compile(fixed.layout, fixed.offsets);
|
|
264
|
-
return {
|
|
265
|
-
byteOffset,
|
|
266
|
-
byteLength,
|
|
267
|
-
read: () => flag[0] ? record.read(base, {}) : void 0,
|
|
268
|
-
// unwritten on either thread
|
|
269
|
-
write: (v) => {
|
|
270
|
-
record.write(base, schema ? schema.parse(v) : v);
|
|
271
|
-
flag[0] = 1;
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
if (fixed.element !== void 0 && fixed.elementStride !== void 0 && fixed.capacity !== void 0) {
|
|
276
|
-
const flag = new Uint32Array(ctx.buffer, byteOffset, 1);
|
|
277
|
-
const count = new Uint32Array(ctx.buffer, byteOffset + 4, 1);
|
|
278
|
-
const el = makeIO(ctx).ops(fixed.element);
|
|
279
|
-
const stride = fixed.elementStride;
|
|
280
|
-
const capacity2 = fixed.capacity;
|
|
281
|
-
const base = byteOffset + 8;
|
|
282
|
-
return {
|
|
283
|
-
byteOffset,
|
|
284
|
-
byteLength,
|
|
285
|
-
read: () => {
|
|
286
|
-
if (!flag[0]) return void 0;
|
|
287
|
-
const n = count[0];
|
|
288
|
-
return Array.from({ length: n }, (_, i) => el.get(base + i * stride));
|
|
289
|
-
},
|
|
290
|
-
write: (v) => {
|
|
291
|
-
const parsed = schema ? schema.parse(v) : v;
|
|
292
|
-
if (parsed.length > capacity2) {
|
|
293
|
-
throw new Error(`${parsed.length} elements exceeds array capacity of ${capacity2}`);
|
|
294
|
-
}
|
|
295
|
-
parsed.forEach((v2, i) => el.set(base + i * stride, v2));
|
|
296
|
-
count[0] = parsed.length;
|
|
297
|
-
flag[0] = 1;
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
const capacity = byteLength - 4;
|
|
302
|
-
const lengthView = new Uint32Array(ctx.buffer, byteOffset, 1);
|
|
303
|
-
const writeBytes = (bytes) => {
|
|
304
|
-
if (bytes.byteLength > capacity) {
|
|
305
|
-
throw new Error(`Encoded value of ${bytes.byteLength} bytes exceeds field capacity of ${capacity} bytes at offset ${byteOffset}`);
|
|
306
|
-
}
|
|
307
|
-
lengthView[0] = bytes.byteLength;
|
|
308
|
-
new Uint8Array(ctx.buffer, byteOffset + 4, bytes.byteLength).set(bytes);
|
|
309
|
-
};
|
|
310
|
-
const readBytes = () => new Uint8Array(ctx.buffer, byteOffset + 4, lengthView[0]).slice();
|
|
311
|
-
return {
|
|
312
|
-
byteOffset,
|
|
313
|
-
byteLength,
|
|
314
|
-
read: () => {
|
|
315
|
-
const bytes = readBytes();
|
|
316
|
-
if (bytes.byteLength === 0) return void 0;
|
|
317
|
-
const value = ctx.codec.decode(bytes);
|
|
318
|
-
return schema ? schema.parse(value) : value;
|
|
319
|
-
},
|
|
320
|
-
write: (v) => writeBytes(ctx.codec.encode(schema ? schema.parse(v) : v))
|
|
321
|
-
};
|
|
322
|
-
};
|
|
323
|
-
registerConnectorFactory("object", structuredFactory);
|
|
324
|
-
registerConnectorFactory("array", structuredFactory);
|
|
325
|
-
function listFactory(d, ctx, byteOffset) {
|
|
326
|
-
const info = d;
|
|
327
|
-
const { offsets, recordSize, count } = info;
|
|
328
|
-
const record = makeIO(ctx).compile(info.layout, offsets);
|
|
329
|
-
const checkIndex = (index) => {
|
|
330
|
-
if (index < 0 || index >= count) {
|
|
331
|
-
throw new RangeError(`Record index ${index} out of bounds (0..${count - 1})`);
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
const readAt = (index, out = {}, fieldNames) => {
|
|
335
|
-
checkIndex(index);
|
|
336
|
-
return record.read(byteOffset + index * recordSize, out, fieldNames);
|
|
337
|
-
};
|
|
338
|
-
const writeAt = (index, rec, fieldNames) => {
|
|
339
|
-
checkIndex(index);
|
|
340
|
-
record.write(byteOffset + index * recordSize, rec, fieldNames);
|
|
341
|
-
};
|
|
342
|
-
const connector = {
|
|
343
|
-
byteOffset,
|
|
344
|
-
byteLength: d.byteLength,
|
|
345
|
-
recordCount: count,
|
|
346
|
-
recordSize,
|
|
347
|
-
readAt,
|
|
348
|
-
writeAt,
|
|
349
|
-
commit: () => {
|
|
350
|
-
if (ctx.version) {
|
|
351
|
-
const prev = Atomics.add(ctx.version.view, ctx.version.index, 1);
|
|
352
|
-
Atomics.notify(ctx.version.view, ctx.version.index);
|
|
353
|
-
if (ctx.path && devtoolsEnabled()) {
|
|
354
|
-
emitDevtools({ type: "memory:write", path: ctx.path, version: prev + 1 });
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
},
|
|
358
|
-
read: () => Array.from({ length: count }, (_, i) => readAt(i)),
|
|
359
|
-
write: (records) => {
|
|
360
|
-
if (records.length > count) {
|
|
361
|
-
throw new Error(`${records.length} records exceeds list array capacity of ${count}`);
|
|
362
|
-
}
|
|
363
|
-
records.forEach((r, i) => writeAt(i, r));
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
return connector;
|
|
367
|
-
}
|
|
368
|
-
registerConnectorFactory("list", listFactory);
|
|
369
|
-
var definedSharedMemories = [];
|
|
370
|
-
function bindSharedMemories(buffer) {
|
|
371
|
-
for (const memory of definedSharedMemories) {
|
|
372
|
-
memory.bind(buffer);
|
|
373
|
-
}
|
|
374
|
-
memLog.info(`bound ${definedSharedMemories.length} contract(s) \u2014 ${fmtBytes(buffer.byteLength)} buffer`);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// ../../src/pool/memory.ts
|
|
378
|
-
var MemoryManager = class {
|
|
379
|
-
memory;
|
|
380
|
-
maxPages;
|
|
381
|
-
constructor(config = {}) {
|
|
382
|
-
const initialPages = config.initialPages ?? 16;
|
|
383
|
-
this.maxPages = config.maximumPages ?? 16384;
|
|
384
|
-
this.memory = new WebAssembly.Memory({
|
|
385
|
-
initial: initialPages,
|
|
386
|
-
maximum: this.maxPages,
|
|
387
|
-
shared: true
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Automatically checks if memory needs to grow to accommodate new bytes,
|
|
392
|
-
* and expands via memory.grow() if necessary. Thread-safe.
|
|
393
|
-
*/
|
|
394
|
-
ensureCapacity(requiredBytes) {
|
|
395
|
-
const currentByteLength = this.memory.buffer.byteLength;
|
|
396
|
-
if (currentByteLength >= requiredBytes) return;
|
|
397
|
-
const bytesNeeded = requiredBytes - currentByteLength;
|
|
398
|
-
const pagesNeeded = Math.ceil(bytesNeeded / 65536);
|
|
399
|
-
const currentPages = this.memory.buffer.byteLength / 65536;
|
|
400
|
-
if (currentPages + pagesNeeded > this.maxPages) {
|
|
401
|
-
throw new Error(`Cannot grow memory: exceeds maximum page limit of ${this.maxPages}`);
|
|
402
|
-
}
|
|
403
|
-
this.memory.grow(pagesNeeded);
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Creates a typed view over the shared memory buffer at a specific byte offset.
|
|
407
|
-
*/
|
|
408
|
-
getView(type, byteOffset = 0, length) {
|
|
409
|
-
const buffer = this.memory.buffer;
|
|
410
|
-
switch (type) {
|
|
411
|
-
case "Int32":
|
|
412
|
-
return new Int32Array(buffer, byteOffset, length);
|
|
413
|
-
case "Float64":
|
|
414
|
-
return new Float64Array(buffer, byteOffset, length);
|
|
415
|
-
case "BigInt64":
|
|
416
|
-
return new BigInt64Array(buffer, byteOffset, length);
|
|
417
|
-
case "Uint8":
|
|
418
|
-
return new Uint8Array(buffer, byteOffset, length);
|
|
419
|
-
default:
|
|
420
|
-
throw new Error(`Unsupported view type: ${type}`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Exposes the raw SharedArrayBuffer
|
|
425
|
-
*/
|
|
426
|
-
getBuffer() {
|
|
427
|
-
return this.memory.buffer;
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
// ../../src/pool/errors.ts
|
|
432
|
-
var PoolQueueFullError = class extends Error {
|
|
433
|
-
constructor(message = "WorkerPool task queue is full") {
|
|
434
|
-
super(message);
|
|
435
|
-
this.name = "PoolQueueFullError";
|
|
436
|
-
}
|
|
437
|
-
};
|
|
438
|
-
var TaskAbortedError = class extends Error {
|
|
439
|
-
constructor(message = "task aborted") {
|
|
440
|
-
super(message);
|
|
441
|
-
this.name = "AbortError";
|
|
442
|
-
}
|
|
443
|
-
};
|
|
444
|
-
var TaskTimeoutError = class extends Error {
|
|
445
|
-
constructor(message = "task timed out") {
|
|
446
|
-
super(message);
|
|
447
|
-
this.name = "TaskTimeoutError";
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
var WorkerCrashedError = class extends Error {
|
|
451
|
-
constructor(message = "worker crashed") {
|
|
452
|
-
super(message);
|
|
453
|
-
this.name = "WorkerCrashedError";
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
// ../../src/userTiming.ts
|
|
458
|
-
var measureAtoll = (name, start, end, t) => {
|
|
459
|
-
const perf = globalThis.performance;
|
|
460
|
-
if (typeof perf?.measure !== "function") return;
|
|
461
|
-
try {
|
|
462
|
-
perf.measure(name, {
|
|
463
|
-
start,
|
|
464
|
-
end: Math.max(end, start),
|
|
465
|
-
detail: {
|
|
466
|
-
devtools: {
|
|
467
|
-
dataType: "track-entry",
|
|
468
|
-
track: t.track,
|
|
469
|
-
trackGroup: "atoll",
|
|
470
|
-
color: t.color ?? "primary",
|
|
471
|
-
properties: t.properties,
|
|
472
|
-
tooltipText: t.tooltipText
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
perf.clearMeasures?.(name);
|
|
477
|
-
} catch {
|
|
478
|
-
}
|
|
479
|
-
};
|
|
480
|
-
var measureTaskCall = (runnerId, taskId, callId, slot, start, end, ok) => measureAtoll(`atoll task ${taskId}`, start, end, {
|
|
481
|
-
track: runnerId,
|
|
482
|
-
color: ok ? "primary" : "error",
|
|
483
|
-
properties: [["runner", runnerId], ["slot", slot], ["call", callId], ["outcome", ok ? "ok" : "error"]],
|
|
484
|
-
tooltipText: `${taskId} on ${runnerId}#${slot}: ${(end - start).toFixed(1)}ms`
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
// ../../src/pool/workerChannel.ts
|
|
488
|
-
function setupChannelMemory(config, owner) {
|
|
489
|
-
if (config.sharedMemory || config.sharedBuffer !== void 0) {
|
|
490
|
-
if (typeof SharedArrayBuffer === "undefined" || typeof crossOriginIsolated !== "undefined" && !crossOriginIsolated) {
|
|
491
|
-
throw new Error(
|
|
492
|
-
`${owner} requires SharedArrayBuffer in a cross-origin isolated context \u2014 serve the page with COOP/COEP headers.`
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
let injectedBuffer;
|
|
497
|
-
if (config.sharedBuffer !== void 0) {
|
|
498
|
-
injectedBuffer = typeof config.sharedBuffer === "function" ? config.sharedBuffer() : config.sharedBuffer;
|
|
499
|
-
if (!injectedBuffer) {
|
|
500
|
-
throw new Error(
|
|
501
|
-
`${owner}: \`sharedBuffer\` resolved to an empty buffer \u2014 the producing pool or contract has no bound memory yet.`
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
if (!config.sharedMemory) return { sharedMemory: void 0, injectedBuffer, persistence: void 0 };
|
|
506
|
-
const sharedMemory = config.sharedMemory;
|
|
507
|
-
let memoryManager;
|
|
508
|
-
if (injectedBuffer) {
|
|
509
|
-
sharedMemory.bind(injectedBuffer);
|
|
510
|
-
} else {
|
|
511
|
-
memoryManager = new MemoryManager(config.memory);
|
|
512
|
-
memoryManager.ensureCapacity(sharedMemory.totalBytes);
|
|
513
|
-
sharedMemory.bind(memoryManager.getBuffer());
|
|
514
|
-
}
|
|
515
|
-
return { sharedMemory, memoryManager, injectedBuffer, persistence: config.persistence?.(sharedMemory) };
|
|
516
|
-
}
|
|
517
|
-
function sendInit(worker, mem) {
|
|
518
|
-
if (mem.memoryManager) {
|
|
519
|
-
worker.postMessage({ type: "INIT_MEMORY", memory: mem.memoryManager.memory, devtools: devtoolsEnabled() });
|
|
520
|
-
return "INIT_MEMORY";
|
|
521
|
-
}
|
|
522
|
-
if (mem.injectedBuffer) {
|
|
523
|
-
worker.postMessage({ type: "INIT_MEMORY", buffer: mem.injectedBuffer, devtools: devtoolsEnabled() });
|
|
524
|
-
return "INIT_MEMORY";
|
|
525
|
-
}
|
|
526
|
-
worker.postMessage({ type: "INIT", devtools: devtoolsEnabled() });
|
|
527
|
-
return "INIT";
|
|
528
|
-
}
|
|
529
|
-
function forwardWorkerDevtools(runnerId, slot, ev) {
|
|
530
|
-
const qualify = (id) => `${runnerId}#${slot}~${id}`;
|
|
531
|
-
forwardDevtools({
|
|
532
|
-
...ev,
|
|
533
|
-
...ev.poolId !== void 0 ? { poolId: qualify(ev.poolId) } : {},
|
|
534
|
-
worker: ev.worker === void 0 ? { poolId: runnerId, slot } : { poolId: qualify(ev.worker.poolId), slot: ev.worker.slot }
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// ../../src/pool/devtoolsCommands.ts
|
|
539
|
-
var CHAOS_FAILURE = "chaos: injected failure";
|
|
540
|
-
var KILL_MESSAGE = "killed from devtools";
|
|
541
|
-
var runners = /* @__PURE__ */ new Map();
|
|
542
|
-
var uninstall = null;
|
|
543
|
-
var runnerOf = (args) => {
|
|
544
|
-
const id = args.poolId;
|
|
545
|
-
if (typeof id !== "string") throw new Error("poolId (string) is required");
|
|
546
|
-
const r = runners.get(id);
|
|
547
|
-
if (!r) {
|
|
548
|
-
throw new Error(
|
|
549
|
-
id.includes("~") ? `runner '${id}' lives inside a worker: nested pools are not controllable from the dashboard` : `no live runner '${id}' on this thread`
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
return r;
|
|
553
|
-
};
|
|
554
|
-
var rate = (v, name) => {
|
|
555
|
-
if (v === void 0 || v === null) return void 0;
|
|
556
|
-
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1) {
|
|
557
|
-
throw new Error(`${name} must be a number in 0..1`);
|
|
558
|
-
}
|
|
559
|
-
return v || void 0;
|
|
560
|
-
};
|
|
561
|
-
var parseChaos = (args) => {
|
|
562
|
-
const { delayMs } = args;
|
|
563
|
-
if (delayMs !== void 0 && delayMs !== null && (typeof delayMs !== "number" || !Number.isFinite(delayMs) || delayMs < 0)) {
|
|
564
|
-
throw new Error("delayMs must be a non-negative number");
|
|
565
|
-
}
|
|
566
|
-
const failRate = rate(args.failRate, "failRate");
|
|
567
|
-
const timeoutRate = rate(args.timeoutRate, "timeoutRate");
|
|
568
|
-
if ((failRate ?? 0) + (timeoutRate ?? 0) > 1) throw new Error("failRate + timeoutRate must not exceed 1");
|
|
569
|
-
const chaos = {};
|
|
570
|
-
if (delayMs) chaos.delayMs = delayMs;
|
|
571
|
-
if (failRate) chaos.failRate = failRate;
|
|
572
|
-
if (timeoutRate) chaos.timeoutRate = timeoutRate;
|
|
573
|
-
return Object.keys(chaos).length ? chaos : null;
|
|
574
|
-
};
|
|
575
|
-
var listRunners = () => [...runners.values()].map((r) => ({
|
|
576
|
-
poolId: r.poolId,
|
|
577
|
-
label: r.label ?? r.poolId,
|
|
578
|
-
size: r.size(),
|
|
579
|
-
dedicated: r.dedicated,
|
|
580
|
-
chaos: r.getChaos()
|
|
581
|
-
}));
|
|
582
|
-
var install = () => {
|
|
583
|
-
const offs = [
|
|
584
|
-
registerDevtoolsCommand("pool.list", () => listRunners()),
|
|
585
|
-
registerDevtoolsCommand("pool.stats", (args) => runnerOf(args).stats()),
|
|
586
|
-
registerDevtoolsCommand("worker.kill", (args) => {
|
|
587
|
-
const r = runnerOf(args);
|
|
588
|
-
const slot = args.slot ?? 0;
|
|
589
|
-
if (typeof slot !== "number" || !Number.isInteger(slot) || slot < 0) {
|
|
590
|
-
throw new Error("slot must be a non-negative integer");
|
|
591
|
-
}
|
|
592
|
-
r.kill(slot);
|
|
593
|
-
return { poolId: r.poolId, slot, killed: true };
|
|
594
|
-
}),
|
|
595
|
-
registerDevtoolsCommand("pool.chaos", (args) => {
|
|
596
|
-
const r = runnerOf(args);
|
|
597
|
-
r.setChaos(parseChaos(args));
|
|
598
|
-
return r.getChaos();
|
|
599
|
-
})
|
|
600
|
-
];
|
|
601
|
-
return () => offs.forEach((off) => off());
|
|
602
|
-
};
|
|
603
|
-
var registerDevtoolsRunner = (handle) => {
|
|
604
|
-
runners.set(handle.poolId, handle);
|
|
605
|
-
uninstall ??= install();
|
|
606
|
-
return () => {
|
|
607
|
-
if (runners.get(handle.poolId) !== handle) return;
|
|
608
|
-
runners.delete(handle.poolId);
|
|
609
|
-
if (runners.size === 0 && uninstall) {
|
|
610
|
-
uninstall();
|
|
611
|
-
uninstall = null;
|
|
612
|
-
}
|
|
613
|
-
};
|
|
614
|
-
};
|
|
615
|
-
var applyChaos = (chaos, call) => {
|
|
616
|
-
const r = Math.random();
|
|
617
|
-
const fail = r < (chaos.failRate ?? 0);
|
|
618
|
-
const timeout = !fail && r < (chaos.failRate ?? 0) + (chaos.timeoutRate ?? 0);
|
|
619
|
-
if (!chaos.delayMs && !fail && !timeout) return call.post();
|
|
620
|
-
setTimeout(() => {
|
|
621
|
-
if (!call.live()) return;
|
|
622
|
-
if (fail) return call.fail();
|
|
623
|
-
if (timeout) call.timeout();
|
|
624
|
-
call.post();
|
|
625
|
-
}, chaos.delayMs ?? 0);
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
// ../../src/pool/workerPool.ts
|
|
629
|
-
var poolLog = scoped("pool");
|
|
630
|
-
var taskLog = scoped("task");
|
|
631
|
-
var agg = () => ({
|
|
632
|
-
count: 0,
|
|
633
|
-
mean: 0,
|
|
634
|
-
max: 0,
|
|
635
|
-
add(v) {
|
|
636
|
-
this.count++;
|
|
637
|
-
this.mean += (v - this.mean) / this.count;
|
|
638
|
-
if (v > this.max) this.max = v;
|
|
639
|
-
}
|
|
640
|
-
});
|
|
641
|
-
var WorkerPoolImpl = class {
|
|
642
|
-
slots = [];
|
|
643
|
-
pending = /* @__PURE__ */ new Map();
|
|
644
|
-
nextMessageId = 0;
|
|
645
|
-
queue = [];
|
|
646
|
-
mem;
|
|
647
|
-
memoryManager;
|
|
648
|
-
/** A caller-supplied buffer shipped via INIT_MEMORY (sub-worker / second-pool sharing). */
|
|
649
|
-
injectedBuffer;
|
|
650
|
-
workerUrl;
|
|
651
|
-
spawn;
|
|
652
|
-
concurrency;
|
|
653
|
-
maxQueue;
|
|
654
|
-
respawn;
|
|
655
|
-
taskTimeout;
|
|
656
|
-
closed = false;
|
|
657
|
-
/** Dashboard fault injection (`pool.chaos`); null unless devtools set it. */
|
|
658
|
-
chaos = null;
|
|
659
|
-
devtoolsOff;
|
|
660
|
-
drainWaiters = [];
|
|
661
|
-
statsAgg = {
|
|
662
|
-
completed: 0,
|
|
663
|
-
failed: 0,
|
|
664
|
-
/** aborts + timeouts. */
|
|
665
|
-
aborted: 0,
|
|
666
|
-
waitMs: agg(),
|
|
667
|
-
runMs: agg()
|
|
668
|
-
};
|
|
669
|
-
/**
|
|
670
|
-
* Devtools correlation id, stamped on every event this pool emits:
|
|
671
|
-
* '<name>-p' when the config has a `name`, else 'pool-N'.
|
|
672
|
-
*/
|
|
673
|
-
poolId;
|
|
674
|
-
label;
|
|
675
|
-
nextCallId = 0;
|
|
676
|
-
sharedMemory;
|
|
677
|
-
/** The attached memory-persistence adapter, if the config supplied one. */
|
|
678
|
-
persistence;
|
|
679
|
-
constructor(config) {
|
|
680
|
-
const mem = setupChannelMemory(config, "WorkerPool");
|
|
681
|
-
if (!config.workerUrl && !config.createWorker) {
|
|
682
|
-
throw new Error("WorkerPool requires either `workerUrl` or `createWorker` in its config.");
|
|
683
|
-
}
|
|
684
|
-
this.workerUrl = config.workerUrl;
|
|
685
|
-
this.spawn = config.createWorker ?? (() => new Worker(this.workerUrl, { type: "module" }));
|
|
686
|
-
this.concurrency = config.concurrency ?? 1;
|
|
687
|
-
this.maxQueue = config.maxQueue ?? Infinity;
|
|
688
|
-
this.respawn = config.respawn ?? true;
|
|
689
|
-
this.taskTimeout = config.taskTimeout;
|
|
690
|
-
this.label = config.name;
|
|
691
|
-
this.poolId = nextDevtoolsId("pool", config.name);
|
|
692
|
-
this.mem = mem;
|
|
693
|
-
this.sharedMemory = mem.sharedMemory;
|
|
694
|
-
this.memoryManager = mem.memoryManager;
|
|
695
|
-
this.injectedBuffer = mem.injectedBuffer;
|
|
696
|
-
this.persistence = mem.persistence;
|
|
697
|
-
const requestedSize = config.poolSize ?? "auto";
|
|
698
|
-
const poolSize = requestedSize === "auto" ? typeof navigator !== "undefined" ? navigator.hardwareConcurrency ?? 4 : 4 : requestedSize;
|
|
699
|
-
poolLog.info(
|
|
700
|
-
this.sharedMemory ? `spawning ${poolSize} worker(s) with ${fmtBytes(this.sharedMemory.totalBytes)} shared buffer` : this.injectedBuffer ? `spawning ${poolSize} worker(s) with ${fmtBytes(this.injectedBuffer.byteLength)} inherited buffer` : `spawning ${poolSize} worker(s), message-only`,
|
|
701
|
-
{ workerUrl: config.workerUrl ? String(config.workerUrl) : "createWorker()" }
|
|
702
|
-
);
|
|
703
|
-
poolLog.debug(`tip: setLogLevel('trace') for per-task detail`);
|
|
704
|
-
emitDevtools({
|
|
705
|
-
type: "pool:init",
|
|
706
|
-
poolId: this.poolId,
|
|
707
|
-
label: this.label,
|
|
708
|
-
poolSize,
|
|
709
|
-
concurrency: this.concurrency,
|
|
710
|
-
memoryBytes: this.sharedMemory?.totalBytes ?? this.injectedBuffer?.byteLength
|
|
711
|
-
});
|
|
712
|
-
for (let i = 0; i < poolSize; i++) {
|
|
713
|
-
this.addWorker(this.spawn());
|
|
714
|
-
}
|
|
715
|
-
if (devtoolsEnabled()) {
|
|
716
|
-
this.devtoolsOff = registerDevtoolsRunner({
|
|
717
|
-
poolId: this.poolId,
|
|
718
|
-
label: this.label,
|
|
719
|
-
dedicated: false,
|
|
720
|
-
size: () => this.slots.length,
|
|
721
|
-
stats: () => this.stats(),
|
|
722
|
-
kill: (slot) => {
|
|
723
|
-
const s = this.slots[slot];
|
|
724
|
-
if (!s) throw new Error(`${this.poolId} has no worker slot ${slot}`);
|
|
725
|
-
this.handleWorkerError(s, { message: KILL_MESSAGE });
|
|
726
|
-
},
|
|
727
|
-
getChaos: () => this.chaos,
|
|
728
|
-
setChaos: (chaos) => {
|
|
729
|
-
this.chaos = chaos;
|
|
730
|
-
}
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
const tasks = config.tasks ?? {};
|
|
734
|
-
for (const [name, contract] of Object.entries(tasks)) {
|
|
735
|
-
if (name in this) {
|
|
736
|
-
poolLog.warn(`task "${name}" collides with a WorkerPool member \u2014 not installed`);
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
this[name] = (...args) => this.runTask(contract, ...args);
|
|
740
|
-
}
|
|
741
|
-
if (Object.keys(tasks).length > 0) {
|
|
742
|
-
poolLog.info(`task methods: ${Object.keys(tasks).join(", ")}`);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
/** Adds a spawned worker: one message + one error listener, then the INIT handshake. */
|
|
746
|
-
addWorker(worker) {
|
|
747
|
-
const slot = { worker, inFlight: 0 };
|
|
748
|
-
worker.addEventListener("message", (event) => {
|
|
749
|
-
const data = event.data;
|
|
750
|
-
if (data.type === "ATOLL_DEVTOOLS" && data.event) {
|
|
751
|
-
forwardWorkerDevtools(this.poolId, this.slots.indexOf(slot), data.event);
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
this.handleReply(slot, data);
|
|
755
|
-
});
|
|
756
|
-
worker.addEventListener("error", (event) => {
|
|
757
|
-
this.handleWorkerError(slot, event);
|
|
758
|
-
});
|
|
759
|
-
const init = sendInit(worker, this.mem);
|
|
760
|
-
this.slots.push(slot);
|
|
761
|
-
poolLog.debug(`worker spawned, ${init} sent`);
|
|
762
|
-
emitDevtools({ type: "worker:spawn", poolId: this.poolId, slot: this.slots.indexOf(slot) });
|
|
763
|
-
return slot;
|
|
764
|
-
}
|
|
765
|
-
handleReply(slot, data) {
|
|
766
|
-
const entry = data.messageId !== void 0 ? this.pending.get(data.messageId) : void 0;
|
|
767
|
-
if (!entry) return;
|
|
768
|
-
this.pending.delete(data.messageId);
|
|
769
|
-
slot.inFlight--;
|
|
770
|
-
this.detach(entry);
|
|
771
|
-
if (!entry.done) {
|
|
772
|
-
entry.done = true;
|
|
773
|
-
const start = entry.sentAt ?? entry.enqueuedAt;
|
|
774
|
-
const runMs = performance.now() - start;
|
|
775
|
-
this.statsAgg.runMs.add(runMs);
|
|
776
|
-
if (data.success) {
|
|
777
|
-
this.statsAgg.completed++;
|
|
778
|
-
taskLog.debug(`\u2190 ${entry.contract.taskId} ok`);
|
|
779
|
-
if (devtoolsEnabled()) {
|
|
780
|
-
emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "ok", runMs, resultBytes: estimateCloneBytes(data.result) });
|
|
781
|
-
measureTaskCall(this.poolId, entry.contract.taskId, entry.callId, this.slots.indexOf(slot), start, start + runMs, true);
|
|
782
|
-
}
|
|
783
|
-
entry.resolve(data.result);
|
|
784
|
-
} else {
|
|
785
|
-
this.statsAgg.failed++;
|
|
786
|
-
taskLog.warn(`\u2190 ${entry.contract.taskId} failed: ${data.error}`);
|
|
787
|
-
if (devtoolsEnabled()) {
|
|
788
|
-
emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "error", runMs, error: data.error });
|
|
789
|
-
measureTaskCall(this.poolId, entry.contract.taskId, entry.callId, this.slots.indexOf(slot), start, start + runMs, false);
|
|
790
|
-
}
|
|
791
|
-
entry.reject(new Error(data.error));
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
this.drainQueue();
|
|
795
|
-
this.checkDrained();
|
|
796
|
-
}
|
|
797
|
-
handleWorkerError(slot, event) {
|
|
798
|
-
if (this.closed) return;
|
|
799
|
-
const e = event;
|
|
800
|
-
const message = e?.message ?? (e?.error ? String(e.error) : "worker error");
|
|
801
|
-
poolLog.warn(`worker error: ${message}`);
|
|
802
|
-
const idx = this.slots.indexOf(slot);
|
|
803
|
-
emitDevtools({ type: "worker:error", poolId: this.poolId, slot: idx, message });
|
|
804
|
-
for (const [id, entry] of [...this.pending]) {
|
|
805
|
-
if (entry.slot === slot) {
|
|
806
|
-
this.pending.delete(id);
|
|
807
|
-
this.detach(entry);
|
|
808
|
-
if (entry.done) continue;
|
|
809
|
-
entry.done = true;
|
|
810
|
-
this.statsAgg.failed++;
|
|
811
|
-
emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "crashed", error: message });
|
|
812
|
-
entry.reject(new WorkerCrashedError(message));
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
if (idx >= 0) this.slots.splice(idx, 1);
|
|
816
|
-
slot.worker.terminate();
|
|
817
|
-
if (this.respawn) {
|
|
818
|
-
poolLog.info("respawning crashed worker");
|
|
819
|
-
this.addWorker(this.spawn());
|
|
820
|
-
emitDevtools({ type: "worker:respawn", poolId: this.poolId, slot: this.slots.length - 1 });
|
|
821
|
-
} else if (this.slots.length === 0) {
|
|
822
|
-
for (const entry of this.queue.splice(0)) {
|
|
823
|
-
entry.done = true;
|
|
824
|
-
this.detach(entry);
|
|
825
|
-
this.statsAgg.failed++;
|
|
826
|
-
emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "crashed", error: message });
|
|
827
|
-
entry.reject(new WorkerCrashedError(message));
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
this.drainQueue();
|
|
831
|
-
this.checkDrained();
|
|
832
|
-
}
|
|
833
|
-
/** Least-busy worker below its concurrency cap, if any. */
|
|
834
|
-
pickWorker() {
|
|
835
|
-
let best;
|
|
836
|
-
for (const slot of this.slots) {
|
|
837
|
-
if (slot.inFlight >= this.concurrency) continue;
|
|
838
|
-
if (!best || slot.inFlight < best.inFlight) best = slot;
|
|
839
|
-
}
|
|
840
|
-
return best;
|
|
841
|
-
}
|
|
842
|
-
drainQueue() {
|
|
843
|
-
let slot;
|
|
844
|
-
while (this.queue.length > 0 && (slot = this.pickWorker())) {
|
|
845
|
-
const entry = this.queue.shift();
|
|
846
|
-
if (entry.done) continue;
|
|
847
|
-
this.send(slot, entry);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
send(slot, entry) {
|
|
851
|
-
const messageId = this.nextMessageId++;
|
|
852
|
-
entry.messageId = messageId;
|
|
853
|
-
entry.slot = slot;
|
|
854
|
-
entry.sentAt = performance.now();
|
|
855
|
-
const waitMs = entry.sentAt - entry.enqueuedAt;
|
|
856
|
-
this.statsAgg.waitMs.add(waitMs);
|
|
857
|
-
slot.inFlight++;
|
|
858
|
-
this.pending.set(messageId, entry);
|
|
859
|
-
const slotIndex = this.slots.indexOf(slot);
|
|
860
|
-
taskLog.debug(`\u2192 ${entry.contract.taskId}`, { worker: slotIndex, args: entry.args });
|
|
861
|
-
if (devtoolsEnabled()) {
|
|
862
|
-
emitDevtools({ type: "task:dispatch", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, slot: slotIndex, waitMs, argBytes: estimateCloneBytes(entry.args) });
|
|
863
|
-
}
|
|
864
|
-
const msg = { type: "EXECUTE_TASK", messageId, taskId: entry.contract.taskId, args: entry.args };
|
|
865
|
-
if (this.chaos) {
|
|
866
|
-
applyChaos(this.chaos, {
|
|
867
|
-
live: () => this.pending.get(messageId) === entry,
|
|
868
|
-
fail: () => this.handleReply(slot, { messageId, taskId: msg.taskId, success: false, error: CHAOS_FAILURE }),
|
|
869
|
-
timeout: () => this.cancel(entry, new TaskTimeoutError()),
|
|
870
|
-
post: () => slot.worker.postMessage(msg)
|
|
871
|
-
});
|
|
872
|
-
} else {
|
|
873
|
-
slot.worker.postMessage(msg);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
/** Detaches signal/timeout listeners from a finalized entry. */
|
|
877
|
-
detach(entry) {
|
|
878
|
-
if (entry.timer) clearTimeout(entry.timer);
|
|
879
|
-
if (entry.signal && entry.onSignalAbort) {
|
|
880
|
-
entry.signal.removeEventListener("abort", entry.onSignalAbort);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
/**
|
|
884
|
-
* Abort/timeout path: a queued task is removed outright; an in-flight task
|
|
885
|
-
* stays in `pending` as an orphan — the worker is still running it, so the
|
|
886
|
-
* slot remains occupied until the reply arrives (handleReply frees it).
|
|
887
|
-
*/
|
|
888
|
-
cancel(entry, err) {
|
|
889
|
-
if (entry.done) return;
|
|
890
|
-
const qi = this.queue.indexOf(entry);
|
|
891
|
-
if (qi >= 0) this.queue.splice(qi, 1);
|
|
892
|
-
entry.done = true;
|
|
893
|
-
this.detach(entry);
|
|
894
|
-
this.statsAgg.aborted++;
|
|
895
|
-
emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: err instanceof TaskTimeoutError ? "timeout" : "aborted" });
|
|
896
|
-
entry.reject(err);
|
|
897
|
-
this.drainQueue();
|
|
898
|
-
this.checkDrained();
|
|
899
|
-
}
|
|
900
|
-
checkDrained() {
|
|
901
|
-
if (this.queue.length === 0 && this.pending.size === 0) {
|
|
902
|
-
const waiters = this.drainWaiters.splice(0);
|
|
903
|
-
for (const w of waiters) w();
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
/**
|
|
907
|
-
* Full dispatch: least-busy scheduling, `concurrency` cap per worker,
|
|
908
|
-
* FIFO queue bounded by `maxQueue`, abort/timeout, and queue→reply timing.
|
|
909
|
-
*/
|
|
910
|
-
dispatch(contract, args, options) {
|
|
911
|
-
if (this.closed || this.slots.length === 0) {
|
|
912
|
-
return Promise.reject(new WorkerCrashedError("WorkerPool has been terminated \u2014 no workers available."));
|
|
913
|
-
}
|
|
914
|
-
if (options?.signal?.aborted) {
|
|
915
|
-
return Promise.reject(new TaskAbortedError());
|
|
916
|
-
}
|
|
917
|
-
return new Promise((resolve, reject) => {
|
|
918
|
-
const entry = {
|
|
919
|
-
contract,
|
|
920
|
-
args,
|
|
921
|
-
callId: ++this.nextCallId,
|
|
922
|
-
enqueuedAt: performance.now(),
|
|
923
|
-
resolve,
|
|
924
|
-
reject,
|
|
925
|
-
done: false,
|
|
926
|
-
signal: options?.signal
|
|
927
|
-
};
|
|
928
|
-
if (options?.signal) {
|
|
929
|
-
entry.onSignalAbort = () => this.cancel(entry, new TaskAbortedError());
|
|
930
|
-
options.signal.addEventListener("abort", entry.onSignalAbort, { once: true });
|
|
931
|
-
}
|
|
932
|
-
const timeout = options?.timeout ?? this.taskTimeout;
|
|
933
|
-
if (timeout !== void 0) {
|
|
934
|
-
entry.timer = setTimeout(() => this.cancel(entry, new TaskTimeoutError()), timeout);
|
|
935
|
-
}
|
|
936
|
-
emitDevtools({ type: "task:enqueue", poolId: this.poolId, callId: entry.callId, taskId: contract.taskId });
|
|
937
|
-
const slot = this.pickWorker();
|
|
938
|
-
if (slot) {
|
|
939
|
-
this.send(slot, entry);
|
|
940
|
-
} else if (this.queue.length >= this.maxQueue) {
|
|
941
|
-
entry.done = true;
|
|
942
|
-
this.detach(entry);
|
|
943
|
-
emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: contract.taskId, outcome: "queue-full" });
|
|
944
|
-
reject(new PoolQueueFullError());
|
|
945
|
-
return;
|
|
946
|
-
} else {
|
|
947
|
-
this.queue.push(entry);
|
|
948
|
-
}
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
/**
|
|
952
|
-
* Dispatches a task to the least-busy worker in the pool.
|
|
953
|
-
* The task contract guarantees the argument and result types across the thread boundary.
|
|
954
|
-
*/
|
|
955
|
-
runTask(contract, ...args) {
|
|
956
|
-
return this.dispatch(contract, args);
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* The shared buffer handed to workers via INIT_MEMORY — undefined on
|
|
960
|
-
* message-only pools. Useful for passing the same buffer to auxiliaries
|
|
961
|
-
* (e.g. a second pool of HTTP workers via withSharedBuffer).
|
|
962
|
-
*/
|
|
963
|
-
get sharedBuffer() {
|
|
964
|
-
return this.memoryManager?.getBuffer() ?? this.injectedBuffer;
|
|
965
|
-
}
|
|
966
|
-
/**
|
|
967
|
-
* Live snapshot of the pool's worker slots — for auxiliary messaging that
|
|
968
|
-
* isn't task dispatch (e.g. routing accepted sockets into workers). The
|
|
969
|
-
* array is a copy; respawned workers appear on the next read. Do NOT use
|
|
970
|
-
* this to dispatch tasks — runTask/dispatch own the scheduling protocol.
|
|
971
|
-
*/
|
|
972
|
-
get workers() {
|
|
973
|
-
return this.slots.map((s) => s.worker);
|
|
974
|
-
}
|
|
975
|
-
/** Running aggregates — no histograms, just counts and means. */
|
|
976
|
-
stats() {
|
|
977
|
-
return {
|
|
978
|
-
workers: this.slots.length,
|
|
979
|
-
idle: this.slots.filter((s) => s.inFlight === 0).length,
|
|
980
|
-
inFlight: this.pending.size,
|
|
981
|
-
queued: this.queue.length,
|
|
982
|
-
completed: this.statsAgg.completed,
|
|
983
|
-
failed: this.statsAgg.failed,
|
|
984
|
-
aborted: this.statsAgg.aborted,
|
|
985
|
-
waitMs: { count: this.statsAgg.waitMs.count, mean: this.statsAgg.waitMs.mean, max: this.statsAgg.waitMs.max },
|
|
986
|
-
runMs: { count: this.statsAgg.runMs.count, mean: this.statsAgg.runMs.mean, max: this.statsAgg.runMs.max }
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
/** Resolves once the queue is empty and nothing is in flight, then terminates. */
|
|
990
|
-
close() {
|
|
991
|
-
if (this.queue.length === 0 && this.pending.size === 0) {
|
|
992
|
-
this.terminate();
|
|
993
|
-
return Promise.resolve();
|
|
994
|
-
}
|
|
995
|
-
return new Promise((resolve) => {
|
|
996
|
-
this.drainWaiters.push(() => {
|
|
997
|
-
this.terminate();
|
|
998
|
-
resolve();
|
|
999
|
-
});
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
terminate() {
|
|
1003
|
-
this.closed = true;
|
|
1004
|
-
this.devtoolsOff?.();
|
|
1005
|
-
void this.persistence?.stop()?.catch?.(
|
|
1006
|
-
(e) => poolLog.warn("memory persistence stop failed", e)
|
|
1007
|
-
);
|
|
1008
|
-
poolLog.info(`terminating ${this.slots.length} worker(s)`);
|
|
1009
|
-
emitDevtools({ type: "pool:terminate", poolId: this.poolId });
|
|
1010
|
-
const crashed = new WorkerCrashedError("pool terminated");
|
|
1011
|
-
for (const [, entry] of this.pending) {
|
|
1012
|
-
this.detach(entry);
|
|
1013
|
-
if (entry.done) continue;
|
|
1014
|
-
entry.done = true;
|
|
1015
|
-
this.statsAgg.failed++;
|
|
1016
|
-
emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "crashed", error: "pool terminated" });
|
|
1017
|
-
entry.reject(crashed);
|
|
1018
|
-
}
|
|
1019
|
-
this.pending.clear();
|
|
1020
|
-
for (const entry of this.queue.splice(0)) {
|
|
1021
|
-
entry.done = true;
|
|
1022
|
-
this.detach(entry);
|
|
1023
|
-
this.statsAgg.failed++;
|
|
1024
|
-
emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "crashed", error: "pool terminated" });
|
|
1025
|
-
entry.reject(crashed);
|
|
1026
|
-
}
|
|
1027
|
-
for (const slot of this.slots) {
|
|
1028
|
-
slot.worker.terminate();
|
|
1029
|
-
}
|
|
1030
|
-
this.slots = [];
|
|
1031
|
-
this.checkDrained();
|
|
1032
|
-
}
|
|
1033
|
-
};
|
|
1034
|
-
var WorkerPool = WorkerPoolImpl;
|
|
1035
|
-
|
|
1036
|
-
export {
|
|
1037
|
-
scoped,
|
|
1038
|
-
bindSharedMemories,
|
|
1039
|
-
WorkerPool
|
|
1040
|
-
};
|