@atolljs/node 0.1.4 → 0.1.5

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.
@@ -0,0 +1,697 @@
1
+ // ../../src/log.ts
2
+ var LEVELS = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, off: 5 };
3
+ var THREAD = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope ? "worker" : "main";
4
+ var consoleSink = ({ level, scope, message, data, thread, at }) => {
5
+ const prefix = `[${(at / 1e3).toFixed(2)}s ${thread}:${scope}]`;
6
+ const fn = level === "error" ? console.error : level === "warn" ? console.warn : level === "info" ? console.info : console.debug;
7
+ if (data !== void 0) fn(`${prefix} ${message}`, data);
8
+ else fn(`${prefix} ${message}`);
9
+ };
10
+ var minLevel = "info";
11
+ var sink = consoleSink;
12
+ function log(level, scope, message, data) {
13
+ if (LEVELS[level] < LEVELS[minLevel]) return;
14
+ sink({ level, scope, message, data, thread: THREAD, at: performance.now() });
15
+ }
16
+ function scoped(scope) {
17
+ return {
18
+ trace: (message, data) => log("trace", scope, message, data),
19
+ debug: (message, data) => log("debug", scope, message, data),
20
+ info: (message, data) => log("info", scope, message, data),
21
+ warn: (message, data) => log("warn", scope, message, data),
22
+ error: (message, data) => log("error", scope, message, data)
23
+ };
24
+ }
25
+ var fmtBytes = (bytes) => bytes >= 1 << 20 ? `${(bytes / (1 << 20)).toFixed(1)}MB` : bytes >= 1024 ? `${(bytes / 1024).toFixed(1)}KB` : `${bytes}B`;
26
+
27
+ // ../../src/contract/sharedMemory.ts
28
+ var memLog = scoped("memory");
29
+ var codecTextEncoder = new TextEncoder();
30
+ var codecTextDecoder = new TextDecoder();
31
+ var CONNECTOR_FACTORIES = /* @__PURE__ */ new Map();
32
+ function registerConnectorFactory(kind, factory) {
33
+ CONNECTOR_FACTORIES.set(kind, factory);
34
+ }
35
+ var TYPED_ARRAYS = {
36
+ Int32: Int32Array,
37
+ Float64: Float64Array,
38
+ BigInt64: BigInt64Array,
39
+ Uint8: Uint8Array
40
+ };
41
+ registerConnectorFactory("number", (d, ctx, byteOffset) => {
42
+ const view = new Float64Array(ctx.buffer, byteOffset, 1);
43
+ return { byteOffset, byteLength: d.byteLength, read: () => view[0], write: (v) => {
44
+ view[0] = v;
45
+ } };
46
+ });
47
+ registerConnectorFactory("boolean", (d, ctx, byteOffset) => {
48
+ const view = new Uint8Array(ctx.buffer, byteOffset, 1);
49
+ return { byteOffset, byteLength: d.byteLength, read: () => view[0] === 1, write: (v) => {
50
+ view[0] = v ? 1 : 0;
51
+ } };
52
+ });
53
+ for (const kind of Object.keys(TYPED_ARRAYS)) {
54
+ registerConnectorFactory(kind, (d, ctx, byteOffset) => {
55
+ const Ctor = TYPED_ARRAYS[kind];
56
+ const view = new Ctor(ctx.buffer, byteOffset, d.byteLength / Ctor.BYTES_PER_ELEMENT);
57
+ return { byteOffset, byteLength: d.byteLength, read: () => view, write: (v) => view.set(v) };
58
+ });
59
+ }
60
+ registerConnectorFactory("string", (d, ctx, byteOffset) => {
61
+ const encoder = new TextEncoder();
62
+ const decoder = new TextDecoder();
63
+ const capacity = d.byteLength - 4;
64
+ const lengthView = new Uint32Array(ctx.buffer, byteOffset, 1);
65
+ const writeBytes = (bytes) => {
66
+ if (bytes.byteLength > capacity) {
67
+ throw new Error(`Encoded value of ${bytes.byteLength} bytes exceeds field capacity of ${capacity} bytes at offset ${byteOffset}`);
68
+ }
69
+ lengthView[0] = bytes.byteLength;
70
+ new Uint8Array(ctx.buffer, byteOffset + 4, bytes.byteLength).set(bytes);
71
+ };
72
+ const readBytes = () => new Uint8Array(ctx.buffer, byteOffset + 4, lengthView[0]).slice();
73
+ return {
74
+ byteOffset,
75
+ byteLength: d.byteLength,
76
+ read: () => decoder.decode(readBytes()),
77
+ write: (v) => writeBytes(encoder.encode(d.schema ? d.schema.parse(v) : v))
78
+ };
79
+ });
80
+ function makeIO(ctx) {
81
+ const view = new DataView(ctx.buffer);
82
+ const byteView = new Uint8Array(ctx.buffer);
83
+ const encoder = new TextEncoder();
84
+ const decoder = new TextDecoder();
85
+ const scalarIO = {
86
+ i8: { get: (o) => view.getInt8(o), set: (o, v) => view.setInt8(o, v) },
87
+ u8: { get: (o) => view.getUint8(o), set: (o, v) => view.setUint8(o, v) },
88
+ i16: { get: (o) => view.getInt16(o), set: (o, v) => view.setInt16(o, v) },
89
+ u16: { get: (o) => view.getUint16(o), set: (o, v) => view.setUint16(o, v) },
90
+ i32: { get: (o) => view.getInt32(o), set: (o, v) => view.setInt32(o, v) },
91
+ u32: { get: (o) => view.getUint32(o), set: (o, v) => view.setUint32(o, v) },
92
+ f32: { get: (o) => view.getFloat32(o), set: (o, v) => view.setFloat32(o, v) },
93
+ f64: { get: (o) => view.getFloat64(o), set: (o, v) => view.setFloat64(o, v) },
94
+ i64: { get: (o) => view.getBigInt64(o), set: (o, v) => view.setBigInt64(o, v) },
95
+ u64: { get: (o) => view.getBigUint64(o), set: (o, v) => view.setBigUint64(o, v) }
96
+ };
97
+ const readString = (offset, max) => {
98
+ const zero = byteView.indexOf(0, offset);
99
+ const len = zero === -1 || zero - offset >= max ? max : zero - offset;
100
+ return decoder.decode(byteView.slice(offset, offset + len));
101
+ };
102
+ let scratch = new Uint8Array(64);
103
+ const writeString = (offset, max, value) => {
104
+ if (scratch.length < max) scratch = new Uint8Array(max);
105
+ const { read, written } = encoder.encodeInto(value, scratch);
106
+ if (read < value.length || written > max) {
107
+ throw new Error(`String exceeds inline field capacity of ${max} bytes`);
108
+ }
109
+ byteView.set(scratch.subarray(0, written), offset);
110
+ if (written < max) byteView.fill(0, offset + written, offset + max);
111
+ };
112
+ const ops = (spec) => {
113
+ if (typeof spec === "string") {
114
+ const io = scalarIO[spec];
115
+ return { get: (o) => io.get(o), set: (o, v) => io.set(o, v) };
116
+ }
117
+ if ("bool" in spec) {
118
+ return { get: (o) => !!view.getUint8(o), set: (o, v) => view.setUint8(o, v ? 1 : 0) };
119
+ }
120
+ return { get: (o) => readString(o, spec.string), set: (o, v) => writeString(o, spec.string, v) };
121
+ };
122
+ const compile = (layout, offsets) => {
123
+ const readers = /* @__PURE__ */ new Map();
124
+ const writers = /* @__PURE__ */ new Map();
125
+ for (const key of Object.keys(layout)) {
126
+ const { get, set } = ops(layout[key]);
127
+ const rel = offsets[key];
128
+ readers.set(key, (base, out) => {
129
+ out[key] = get(base + rel);
130
+ });
131
+ writers.set(key, (base, record) => set(base + rel, record[key]));
132
+ }
133
+ const allReaders = [...readers.values()];
134
+ const allWriters = [...writers.values()];
135
+ return {
136
+ read(base, out = {}, fieldNames) {
137
+ if (fieldNames) {
138
+ for (const key of fieldNames) readers.get(key)(base, out);
139
+ } else {
140
+ for (const read of allReaders) read(base, out);
141
+ }
142
+ return out;
143
+ },
144
+ write(base, record, fieldNames) {
145
+ if (fieldNames) {
146
+ for (const key of fieldNames) writers.get(key)(base, record);
147
+ } else {
148
+ for (const write of allWriters) write(base, record);
149
+ }
150
+ }
151
+ };
152
+ };
153
+ return { ops, compile };
154
+ }
155
+ var structuredFactory = (d, ctx, byteOffset) => {
156
+ const { schema, byteLength, kind } = d;
157
+ const fixed = d;
158
+ if (fixed.layout && fixed.offsets && fixed.recordSize !== void 0) {
159
+ const flag = new Uint32Array(ctx.buffer, byteOffset, 2);
160
+ const base = byteOffset + 8;
161
+ const record = makeIO(ctx).compile(fixed.layout, fixed.offsets);
162
+ return {
163
+ byteOffset,
164
+ byteLength,
165
+ read: () => flag[0] ? record.read(base, {}) : void 0,
166
+ // unwritten on either thread
167
+ write: (v) => {
168
+ record.write(base, schema ? schema.parse(v) : v);
169
+ flag[0] = 1;
170
+ }
171
+ };
172
+ }
173
+ if (fixed.element !== void 0 && fixed.elementStride !== void 0 && fixed.capacity !== void 0) {
174
+ const flag = new Uint32Array(ctx.buffer, byteOffset, 1);
175
+ const count = new Uint32Array(ctx.buffer, byteOffset + 4, 1);
176
+ const el = makeIO(ctx).ops(fixed.element);
177
+ const stride = fixed.elementStride;
178
+ const capacity2 = fixed.capacity;
179
+ const base = byteOffset + 8;
180
+ return {
181
+ byteOffset,
182
+ byteLength,
183
+ read: () => {
184
+ if (!flag[0]) return void 0;
185
+ const n = count[0];
186
+ return Array.from({ length: n }, (_, i) => el.get(base + i * stride));
187
+ },
188
+ write: (v) => {
189
+ const parsed = schema ? schema.parse(v) : v;
190
+ if (parsed.length > capacity2) {
191
+ throw new Error(`${parsed.length} elements exceeds array capacity of ${capacity2}`);
192
+ }
193
+ parsed.forEach((v2, i) => el.set(base + i * stride, v2));
194
+ count[0] = parsed.length;
195
+ flag[0] = 1;
196
+ }
197
+ };
198
+ }
199
+ const capacity = byteLength - 4;
200
+ const lengthView = new Uint32Array(ctx.buffer, byteOffset, 1);
201
+ const writeBytes = (bytes) => {
202
+ if (bytes.byteLength > capacity) {
203
+ throw new Error(`Encoded value of ${bytes.byteLength} bytes exceeds field capacity of ${capacity} bytes at offset ${byteOffset}`);
204
+ }
205
+ lengthView[0] = bytes.byteLength;
206
+ new Uint8Array(ctx.buffer, byteOffset + 4, bytes.byteLength).set(bytes);
207
+ };
208
+ const readBytes = () => new Uint8Array(ctx.buffer, byteOffset + 4, lengthView[0]).slice();
209
+ return {
210
+ byteOffset,
211
+ byteLength,
212
+ read: () => {
213
+ const bytes = readBytes();
214
+ if (bytes.byteLength === 0) return void 0;
215
+ const value = ctx.codec.decode(bytes);
216
+ return schema ? schema.parse(value) : value;
217
+ },
218
+ write: (v) => writeBytes(ctx.codec.encode(schema ? schema.parse(v) : v))
219
+ };
220
+ };
221
+ registerConnectorFactory("object", structuredFactory);
222
+ registerConnectorFactory("array", structuredFactory);
223
+ function listFactory(d, ctx, byteOffset) {
224
+ const info = d;
225
+ const { offsets, recordSize, count } = info;
226
+ const record = makeIO(ctx).compile(info.layout, offsets);
227
+ const checkIndex = (index) => {
228
+ if (index < 0 || index >= count) {
229
+ throw new RangeError(`Record index ${index} out of bounds (0..${count - 1})`);
230
+ }
231
+ };
232
+ const readAt = (index, out = {}, fieldNames) => {
233
+ checkIndex(index);
234
+ return record.read(byteOffset + index * recordSize, out, fieldNames);
235
+ };
236
+ const writeAt = (index, rec, fieldNames) => {
237
+ checkIndex(index);
238
+ record.write(byteOffset + index * recordSize, rec, fieldNames);
239
+ };
240
+ const connector = {
241
+ byteOffset,
242
+ byteLength: d.byteLength,
243
+ recordCount: count,
244
+ recordSize,
245
+ readAt,
246
+ writeAt,
247
+ commit: () => {
248
+ if (ctx.version) {
249
+ Atomics.add(ctx.version.view, ctx.version.index, 1);
250
+ Atomics.notify(ctx.version.view, ctx.version.index);
251
+ }
252
+ },
253
+ read: () => Array.from({ length: count }, (_, i) => readAt(i)),
254
+ write: (records) => {
255
+ if (records.length > count) {
256
+ throw new Error(`${records.length} records exceeds list array capacity of ${count}`);
257
+ }
258
+ records.forEach((r, i) => writeAt(i, r));
259
+ }
260
+ };
261
+ return connector;
262
+ }
263
+ registerConnectorFactory("list", listFactory);
264
+ var definedSharedMemories = [];
265
+ function bindSharedMemories(buffer) {
266
+ for (const memory of definedSharedMemories) {
267
+ memory.bind(buffer);
268
+ }
269
+ memLog.info(`bound ${definedSharedMemories.length} contract(s) \u2014 ${fmtBytes(buffer.byteLength)} buffer`);
270
+ }
271
+
272
+ // ../../src/pool/memory.ts
273
+ var MemoryManager = class {
274
+ memory;
275
+ maxPages;
276
+ constructor(config = {}) {
277
+ const initialPages = config.initialPages ?? 16;
278
+ this.maxPages = config.maximumPages ?? 16384;
279
+ this.memory = new WebAssembly.Memory({
280
+ initial: initialPages,
281
+ maximum: this.maxPages,
282
+ shared: true
283
+ });
284
+ }
285
+ /**
286
+ * Automatically checks if memory needs to grow to accommodate new bytes,
287
+ * and expands via memory.grow() if necessary. Thread-safe.
288
+ */
289
+ ensureCapacity(requiredBytes) {
290
+ const currentByteLength = this.memory.buffer.byteLength;
291
+ if (currentByteLength >= requiredBytes) return;
292
+ const bytesNeeded = requiredBytes - currentByteLength;
293
+ const pagesNeeded = Math.ceil(bytesNeeded / 65536);
294
+ const currentPages = this.memory.buffer.byteLength / 65536;
295
+ if (currentPages + pagesNeeded > this.maxPages) {
296
+ throw new Error(`Cannot grow memory: exceeds maximum page limit of ${this.maxPages}`);
297
+ }
298
+ this.memory.grow(pagesNeeded);
299
+ }
300
+ /**
301
+ * Creates a typed view over the shared memory buffer at a specific byte offset.
302
+ */
303
+ getView(type, byteOffset = 0, length) {
304
+ const buffer = this.memory.buffer;
305
+ switch (type) {
306
+ case "Int32":
307
+ return new Int32Array(buffer, byteOffset, length);
308
+ case "Float64":
309
+ return new Float64Array(buffer, byteOffset, length);
310
+ case "BigInt64":
311
+ return new BigInt64Array(buffer, byteOffset, length);
312
+ case "Uint8":
313
+ return new Uint8Array(buffer, byteOffset, length);
314
+ default:
315
+ throw new Error(`Unsupported view type: ${type}`);
316
+ }
317
+ }
318
+ /**
319
+ * Exposes the raw SharedArrayBuffer
320
+ */
321
+ getBuffer() {
322
+ return this.memory.buffer;
323
+ }
324
+ };
325
+
326
+ // ../../src/pool/errors.ts
327
+ var PoolQueueFullError = class extends Error {
328
+ constructor(message = "WorkerPool task queue is full") {
329
+ super(message);
330
+ this.name = "PoolQueueFullError";
331
+ }
332
+ };
333
+ var TaskAbortedError = class extends Error {
334
+ constructor(message = "task aborted") {
335
+ super(message);
336
+ this.name = "AbortError";
337
+ }
338
+ };
339
+ var TaskTimeoutError = class extends Error {
340
+ constructor(message = "task timed out") {
341
+ super(message);
342
+ this.name = "TaskTimeoutError";
343
+ }
344
+ };
345
+ var WorkerCrashedError = class extends Error {
346
+ constructor(message = "worker crashed") {
347
+ super(message);
348
+ this.name = "WorkerCrashedError";
349
+ }
350
+ };
351
+
352
+ // ../../src/pool/workerPool.ts
353
+ var poolLog = scoped("pool");
354
+ var taskLog = scoped("task");
355
+ var agg = () => ({
356
+ count: 0,
357
+ mean: 0,
358
+ max: 0,
359
+ add(v) {
360
+ this.count++;
361
+ this.mean += (v - this.mean) / this.count;
362
+ if (v > this.max) this.max = v;
363
+ }
364
+ });
365
+ var WorkerPoolImpl = class {
366
+ slots = [];
367
+ pending = /* @__PURE__ */ new Map();
368
+ nextMessageId = 0;
369
+ queue = [];
370
+ memoryManager;
371
+ workerUrl;
372
+ spawn;
373
+ concurrency;
374
+ maxQueue;
375
+ respawn;
376
+ taskTimeout;
377
+ closed = false;
378
+ drainWaiters = [];
379
+ statsAgg = {
380
+ completed: 0,
381
+ failed: 0,
382
+ /** aborts + timeouts. */
383
+ aborted: 0,
384
+ waitMs: agg(),
385
+ runMs: agg()
386
+ };
387
+ sharedMemory;
388
+ /** The attached memory-persistence adapter, if the config supplied one. */
389
+ persistence;
390
+ constructor(config) {
391
+ if (config.sharedMemory) {
392
+ if (typeof SharedArrayBuffer === "undefined" || typeof crossOriginIsolated !== "undefined" && !crossOriginIsolated) {
393
+ throw new Error(
394
+ "WorkerPool requires SharedArrayBuffer in a cross-origin isolated context \u2014 serve the page with COOP/COEP headers."
395
+ );
396
+ }
397
+ }
398
+ if (!config.workerUrl && !config.createWorker) {
399
+ throw new Error("WorkerPool requires either `workerUrl` or `createWorker` in its config.");
400
+ }
401
+ this.workerUrl = config.workerUrl;
402
+ this.spawn = config.createWorker ?? (() => new Worker(this.workerUrl, { type: "module" }));
403
+ this.concurrency = config.concurrency ?? 1;
404
+ this.maxQueue = config.maxQueue ?? Infinity;
405
+ this.respawn = config.respawn ?? true;
406
+ this.taskTimeout = config.taskTimeout;
407
+ if (config.sharedMemory) {
408
+ this.sharedMemory = config.sharedMemory;
409
+ this.memoryManager = new MemoryManager(config.memory);
410
+ this.memoryManager.ensureCapacity(this.sharedMemory.totalBytes);
411
+ this.sharedMemory.bind(this.memoryManager.getBuffer());
412
+ this.persistence = config.persistence?.(config.sharedMemory);
413
+ }
414
+ const requestedSize = config.poolSize ?? "auto";
415
+ const poolSize = requestedSize === "auto" ? navigator.hardwareConcurrency ?? 4 : requestedSize;
416
+ poolLog.info(
417
+ this.sharedMemory ? `spawning ${poolSize} worker(s) with ${fmtBytes(this.sharedMemory.totalBytes)} shared buffer` : `spawning ${poolSize} worker(s), message-only`,
418
+ { workerUrl: config.workerUrl ? String(config.workerUrl) : "createWorker()" }
419
+ );
420
+ poolLog.debug(`tip: setLogLevel('trace') for per-task detail`);
421
+ for (let i = 0; i < poolSize; i++) {
422
+ this.addWorker(this.spawn());
423
+ }
424
+ const tasks = config.tasks ?? {};
425
+ for (const [name, contract] of Object.entries(tasks)) {
426
+ if (name in this) {
427
+ poolLog.warn(`task "${name}" collides with a WorkerPool member \u2014 not installed`);
428
+ continue;
429
+ }
430
+ this[name] = (...args) => this.runTask(contract, ...args);
431
+ }
432
+ if (Object.keys(tasks).length > 0) {
433
+ poolLog.info(`task methods: ${Object.keys(tasks).join(", ")}`);
434
+ }
435
+ }
436
+ /** Adds a spawned worker: one message + one error listener, then the INIT handshake. */
437
+ addWorker(worker) {
438
+ const slot = { worker, inFlight: 0 };
439
+ worker.addEventListener("message", (event) => {
440
+ this.handleReply(slot, event.data);
441
+ });
442
+ worker.addEventListener("error", (event) => {
443
+ this.handleWorkerError(slot, event);
444
+ });
445
+ if (this.memoryManager) {
446
+ worker.postMessage({ type: "INIT_MEMORY", memory: this.memoryManager.memory });
447
+ } else {
448
+ worker.postMessage({ type: "INIT" });
449
+ }
450
+ this.slots.push(slot);
451
+ poolLog.debug(`worker spawned, ${this.memoryManager ? "INIT_MEMORY" : "INIT"} sent`);
452
+ return slot;
453
+ }
454
+ handleReply(slot, data) {
455
+ const entry = data.messageId !== void 0 ? this.pending.get(data.messageId) : void 0;
456
+ if (!entry) return;
457
+ this.pending.delete(data.messageId);
458
+ slot.inFlight--;
459
+ this.detach(entry);
460
+ if (!entry.done) {
461
+ entry.done = true;
462
+ this.statsAgg.runMs.add(performance.now() - (entry.sentAt ?? entry.enqueuedAt));
463
+ if (data.success) {
464
+ this.statsAgg.completed++;
465
+ taskLog.debug(`\u2190 ${entry.contract.taskId} ok`);
466
+ entry.resolve(data.result);
467
+ } else {
468
+ this.statsAgg.failed++;
469
+ taskLog.warn(`\u2190 ${entry.contract.taskId} failed: ${data.error}`);
470
+ entry.reject(new Error(data.error));
471
+ }
472
+ }
473
+ this.drainQueue();
474
+ this.checkDrained();
475
+ }
476
+ handleWorkerError(slot, event) {
477
+ if (this.closed) return;
478
+ const e = event;
479
+ const message = e?.message ?? (e?.error ? String(e.error) : "worker error");
480
+ poolLog.warn(`worker error: ${message}`);
481
+ for (const [id, entry] of [...this.pending]) {
482
+ if (entry.slot === slot) {
483
+ this.pending.delete(id);
484
+ this.detach(entry);
485
+ if (entry.done) continue;
486
+ entry.done = true;
487
+ this.statsAgg.failed++;
488
+ entry.reject(new WorkerCrashedError(message));
489
+ }
490
+ }
491
+ const idx = this.slots.indexOf(slot);
492
+ if (idx >= 0) this.slots.splice(idx, 1);
493
+ slot.worker.terminate();
494
+ if (this.respawn) {
495
+ poolLog.info("respawning crashed worker");
496
+ this.addWorker(this.spawn());
497
+ } else if (this.slots.length === 0) {
498
+ for (const entry of this.queue.splice(0)) {
499
+ entry.done = true;
500
+ this.detach(entry);
501
+ this.statsAgg.failed++;
502
+ entry.reject(new WorkerCrashedError(message));
503
+ }
504
+ }
505
+ this.drainQueue();
506
+ this.checkDrained();
507
+ }
508
+ /** Least-busy worker below its concurrency cap, if any. */
509
+ pickWorker() {
510
+ let best;
511
+ for (const slot of this.slots) {
512
+ if (slot.inFlight >= this.concurrency) continue;
513
+ if (!best || slot.inFlight < best.inFlight) best = slot;
514
+ }
515
+ return best;
516
+ }
517
+ drainQueue() {
518
+ let slot;
519
+ while (this.queue.length > 0 && (slot = this.pickWorker())) {
520
+ const entry = this.queue.shift();
521
+ if (entry.done) continue;
522
+ this.send(slot, entry);
523
+ }
524
+ }
525
+ send(slot, entry) {
526
+ const messageId = this.nextMessageId++;
527
+ entry.messageId = messageId;
528
+ entry.slot = slot;
529
+ entry.sentAt = performance.now();
530
+ this.statsAgg.waitMs.add(performance.now() - entry.enqueuedAt);
531
+ slot.inFlight++;
532
+ this.pending.set(messageId, entry);
533
+ taskLog.debug(`\u2192 ${entry.contract.taskId}`, { worker: this.slots.indexOf(slot), args: entry.args });
534
+ slot.worker.postMessage({
535
+ type: "EXECUTE_TASK",
536
+ messageId,
537
+ taskId: entry.contract.taskId,
538
+ args: entry.args
539
+ });
540
+ }
541
+ /** Detaches signal/timeout listeners from a finalized entry. */
542
+ detach(entry) {
543
+ if (entry.timer) clearTimeout(entry.timer);
544
+ if (entry.signal && entry.onSignalAbort) {
545
+ entry.signal.removeEventListener("abort", entry.onSignalAbort);
546
+ }
547
+ }
548
+ /**
549
+ * Abort/timeout path: a queued task is removed outright; an in-flight task
550
+ * stays in `pending` as an orphan — the worker is still running it, so the
551
+ * slot remains occupied until the reply arrives (handleReply frees it).
552
+ */
553
+ cancel(entry, err) {
554
+ if (entry.done) return;
555
+ const qi = this.queue.indexOf(entry);
556
+ if (qi >= 0) this.queue.splice(qi, 1);
557
+ entry.done = true;
558
+ this.detach(entry);
559
+ this.statsAgg.aborted++;
560
+ entry.reject(err);
561
+ this.drainQueue();
562
+ this.checkDrained();
563
+ }
564
+ checkDrained() {
565
+ if (this.queue.length === 0 && this.pending.size === 0) {
566
+ const waiters = this.drainWaiters.splice(0);
567
+ for (const w of waiters) w();
568
+ }
569
+ }
570
+ /**
571
+ * Full dispatch: least-busy scheduling, `concurrency` cap per worker,
572
+ * FIFO queue bounded by `maxQueue`, abort/timeout, and queue→reply timing.
573
+ */
574
+ dispatch(contract, args, options) {
575
+ if (this.closed || this.slots.length === 0) {
576
+ return Promise.reject(new WorkerCrashedError("WorkerPool has been terminated \u2014 no workers available."));
577
+ }
578
+ if (options?.signal?.aborted) {
579
+ return Promise.reject(new TaskAbortedError());
580
+ }
581
+ return new Promise((resolve, reject) => {
582
+ const entry = {
583
+ contract,
584
+ args,
585
+ enqueuedAt: performance.now(),
586
+ resolve,
587
+ reject,
588
+ done: false,
589
+ signal: options?.signal
590
+ };
591
+ if (options?.signal) {
592
+ entry.onSignalAbort = () => this.cancel(entry, new TaskAbortedError());
593
+ options.signal.addEventListener("abort", entry.onSignalAbort, { once: true });
594
+ }
595
+ const timeout = options?.timeout ?? this.taskTimeout;
596
+ if (timeout !== void 0) {
597
+ entry.timer = setTimeout(() => this.cancel(entry, new TaskTimeoutError()), timeout);
598
+ }
599
+ const slot = this.pickWorker();
600
+ if (slot) {
601
+ this.send(slot, entry);
602
+ } else if (this.queue.length >= this.maxQueue) {
603
+ entry.done = true;
604
+ this.detach(entry);
605
+ reject(new PoolQueueFullError());
606
+ return;
607
+ } else {
608
+ this.queue.push(entry);
609
+ }
610
+ });
611
+ }
612
+ /**
613
+ * Dispatches a task to the least-busy worker in the pool.
614
+ * The task contract guarantees the argument and result types across the thread boundary.
615
+ */
616
+ runTask(contract, ...args) {
617
+ return this.dispatch(contract, args);
618
+ }
619
+ /**
620
+ * The shared buffer handed to workers via INIT_MEMORY — undefined on
621
+ * message-only pools. Useful for passing the same buffer to auxiliaries
622
+ * (e.g. a second pool of HTTP workers via withSharedBuffer).
623
+ */
624
+ get sharedBuffer() {
625
+ return this.memoryManager?.getBuffer();
626
+ }
627
+ /**
628
+ * Live snapshot of the pool's worker slots — for auxiliary messaging that
629
+ * isn't task dispatch (e.g. routing accepted sockets into workers). The
630
+ * array is a copy; respawned workers appear on the next read. Do NOT use
631
+ * this to dispatch tasks — runTask/dispatch own the scheduling protocol.
632
+ */
633
+ get workers() {
634
+ return this.slots.map((s) => s.worker);
635
+ }
636
+ /** Running aggregates — no histograms, just counts and means. */
637
+ stats() {
638
+ return {
639
+ workers: this.slots.length,
640
+ idle: this.slots.filter((s) => s.inFlight === 0).length,
641
+ inFlight: this.pending.size,
642
+ queued: this.queue.length,
643
+ completed: this.statsAgg.completed,
644
+ failed: this.statsAgg.failed,
645
+ aborted: this.statsAgg.aborted,
646
+ waitMs: { count: this.statsAgg.waitMs.count, mean: this.statsAgg.waitMs.mean, max: this.statsAgg.waitMs.max },
647
+ runMs: { count: this.statsAgg.runMs.count, mean: this.statsAgg.runMs.mean, max: this.statsAgg.runMs.max }
648
+ };
649
+ }
650
+ /** Resolves once the queue is empty and nothing is in flight, then terminates. */
651
+ close() {
652
+ if (this.queue.length === 0 && this.pending.size === 0) {
653
+ this.terminate();
654
+ return Promise.resolve();
655
+ }
656
+ return new Promise((resolve) => {
657
+ this.drainWaiters.push(() => {
658
+ this.terminate();
659
+ resolve();
660
+ });
661
+ });
662
+ }
663
+ terminate() {
664
+ this.closed = true;
665
+ void this.persistence?.stop()?.catch?.(
666
+ (e) => poolLog.warn("memory persistence stop failed", e)
667
+ );
668
+ poolLog.info(`terminating ${this.slots.length} worker(s)`);
669
+ const crashed = new WorkerCrashedError("pool terminated");
670
+ for (const [, entry] of this.pending) {
671
+ this.detach(entry);
672
+ if (entry.done) continue;
673
+ entry.done = true;
674
+ this.statsAgg.failed++;
675
+ entry.reject(crashed);
676
+ }
677
+ this.pending.clear();
678
+ for (const entry of this.queue.splice(0)) {
679
+ entry.done = true;
680
+ this.detach(entry);
681
+ this.statsAgg.failed++;
682
+ entry.reject(crashed);
683
+ }
684
+ for (const slot of this.slots) {
685
+ slot.worker.terminate();
686
+ }
687
+ this.slots = [];
688
+ this.checkDrained();
689
+ }
690
+ };
691
+ var WorkerPool = WorkerPoolImpl;
692
+
693
+ export {
694
+ scoped,
695
+ bindSharedMemories,
696
+ WorkerPool
697
+ };