knitting 0.1.60 → 0.1.62

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.
Files changed (46) hide show
  1. package/BROWSER.md +134 -0
  2. package/README.md +94 -6
  3. package/knitting.browser.d.ts +6 -0
  4. package/knitting.browser.js +1 -0
  5. package/package.json +11 -2
  6. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  7. package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
  8. package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
  9. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
  11. package/prebuilds/darwin-arm64-node-137/knitting_shm.node +0 -0
  12. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  13. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  14. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  15. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  16. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  18. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  19. package/src/api.js +136 -25
  20. package/src/common/runtime.d.ts +2 -1
  21. package/src/common/runtime.js +8 -1
  22. package/src/common/task-source.d.ts +9 -0
  23. package/src/common/task-source.js +15 -0
  24. package/src/memory/lock.d.ts +37 -1
  25. package/src/memory/lock.js +262 -5
  26. package/src/memory/payloadCodec.js +2 -6
  27. package/src/memory/regionRegistry.d.ts +1 -3
  28. package/src/memory/regionRegistry.js +16 -14
  29. package/src/permission/protocol.js +5 -1
  30. package/src/runtime/dispatcher.d.ts +11 -8
  31. package/src/runtime/dispatcher.js +163 -21
  32. package/src/runtime/inline-executor.js +6 -1
  33. package/src/runtime/pool.d.ts +127 -4
  34. package/src/runtime/pool.js +262 -73
  35. package/src/runtime/process-worker.d.ts +24 -12
  36. package/src/runtime/process-worker.js +185 -115
  37. package/src/runtime/tx-queue.d.ts +17 -1
  38. package/src/runtime/tx-queue.js +120 -15
  39. package/src/runtime/worker-common.d.ts +13 -0
  40. package/src/runtime/worker-common.js +103 -0
  41. package/src/types.d.ts +75 -4
  42. package/src/worker/loop.js +34 -5
  43. package/src/worker/rx-queue.d.ts +11 -1
  44. package/src/worker/rx-queue.js +4 -1
  45. package/src/worker/timers.d.ts +8 -1
  46. package/src/worker/timers.js +20 -17
@@ -2,6 +2,7 @@ import { HEADER_SLOT_STRIDE_U32, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../
2
2
  import { createByteCarpet, getHeaderBlockByteLength, makeSharedBufferRegion, } from "../memory/byte-carpet.js";
3
3
  import { RUNTIME } from "../common/runtime.js";
4
4
  import { debugHas } from "../debug/gate.js";
5
+ import { toWorkerCompatExecArgv, } from "./worker-common.js";
5
6
  import { RUNTIME_POOL_DEPTH, RUNTIME_POOL_DEPTH_ENV, RUNTIME_PROCESS_WORKER_BOOT_ENV, RUNTIME_PROCESS_WORKER_BOOT_VERSION, RUNTIME_PROCESS_WORKER_ENV, } from "../common/worker-runtime.js";
6
7
  import { getNodeBuiltinModule, getNodeProcess } from "../common/node-compat.js";
7
8
  import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
@@ -20,51 +21,6 @@ const fileURLToPathCompat = (value) => {
20
21
  }
21
22
  return url.fileURLToPath(value);
22
23
  };
23
- const execFlagKey = (flag) => flag.split("=", 1)[0];
24
- const NODE_PERMISSION_EXEC_FLAGS = new Set([
25
- "--permission",
26
- "--experimental-permission",
27
- "--allow-fs-read",
28
- "--allow-fs-write",
29
- "--allow-worker",
30
- "--allow-child-process",
31
- "--allow-net",
32
- "--allow-addons",
33
- "--allow-ffi",
34
- "--allow-wasi",
35
- ]);
36
- const NODE_WORKER_SAFE_EXEC_FLAGS = new Set([
37
- "--experimental-ffi",
38
- "--experimental-transform-types",
39
- "--expose-gc",
40
- "--no-warnings",
41
- ...NODE_PERMISSION_EXEC_FLAGS,
42
- ]);
43
- const isNodeWorkerSafeExecFlag = (flag) => NODE_WORKER_SAFE_EXEC_FLAGS.has(execFlagKey(flag));
44
- const isNodePermissionExecFlag = (flag) => NODE_PERMISSION_EXEC_FLAGS.has(execFlagKey(flag));
45
- export const toWorkerSafeExecArgv = (flags) => {
46
- if (!flags || flags.length === 0)
47
- return undefined;
48
- const filtered = flags.filter(isNodeWorkerSafeExecFlag);
49
- if (filtered.length === 0)
50
- return undefined;
51
- const seen = new Set();
52
- const deduped = [];
53
- for (const flag of filtered) {
54
- if (seen.has(flag))
55
- continue;
56
- seen.add(flag);
57
- deduped.push(flag);
58
- }
59
- return deduped;
60
- };
61
- export const toWorkerCompatExecArgv = (flags) => {
62
- const safe = toWorkerSafeExecArgv(flags);
63
- if (!safe || safe.length === 0)
64
- return undefined;
65
- const compat = safe.filter((flag) => !isNodePermissionExecFlag(flag));
66
- return compat.length > 0 ? compat : undefined;
67
- };
68
24
  const toProcessSharedMemorySize = (byteLength) => {
69
25
  if (!Number.isFinite(byteLength) || byteLength <= 0) {
70
26
  throw new RangeError("process shared memory byteLength must be positive");
@@ -118,19 +74,18 @@ const DENO_PROCESS_WORKER_INTERNAL_FLAGS = [
118
74
  `--allow-env=${DENO_PROCESS_WORKER_BOOT_ENV_ALLOW}`,
119
75
  "--allow-ffi",
120
76
  ];
121
- const nodeMajorVersion = () => {
122
- const version = getNodeProcess()?.versions?.node ?? "";
123
- const major = Number.parseInt(version.split(".", 1)[0] ?? "", 10);
124
- return Number.isInteger(major) ? major : 0;
125
- };
126
- const nodeProcessWorkerUsesFfi = () => {
127
- const major = nodeMajorVersion();
128
- return major >= 26 && major % 2 === 0;
77
+ const parseNodeMajorVersion = (version) => {
78
+ const match = /^v?(\d+)/.exec(String(version).trim());
79
+ if (match === null)
80
+ return undefined;
81
+ const major = Number.parseInt(match[1], 10);
82
+ return Number.isInteger(major) && major > 0 ? major : undefined;
129
83
  };
130
- const nodeProcessWorkerInternalExecArgv = () => {
84
+ const nodeProcessWorkerUsesFfi = (nodeMajor) => nodeMajor !== undefined && nodeMajor >= 26 && nodeMajor % 2 === 0;
85
+ const nodeProcessWorkerInternalExecArgv = (nodeMajor) => {
131
86
  return [
132
87
  "--no-warnings",
133
- ...(nodeProcessWorkerUsesFfi()
88
+ ...(nodeProcessWorkerUsesFfi(nodeMajor)
134
89
  ? ["--experimental-ffi"]
135
90
  : ["--experimental-transform-types"]),
136
91
  ];
@@ -236,6 +191,106 @@ export const createProcessWorkerMemoryLayout = ({ signalBytes, abortBytes, paylo
236
191
  },
237
192
  };
238
193
  };
194
+ export const createProcessStealMemoryLayout = ({ threads, signalBytes, abortBytes, abortSignalMax, payloadBytes, sharedMemory, }) => {
195
+ if (!Number.isInteger(threads) || threads < 2) {
196
+ throw new RangeError("process stealing needs at least two workers");
197
+ }
198
+ const carpet = createByteCarpet();
199
+ const requestLockSlice = carpet.take("requestLockSector", LOCK_SECTOR_BYTE_LENGTH);
200
+ const requestHeadersSlice = carpet.take("requestHeaders", getHeaderBlockByteLength({
201
+ slotCount: LockBound.slots,
202
+ slotStrideU32: HEADER_SLOT_STRIDE_U32,
203
+ alignTo: 64,
204
+ }));
205
+ const abortSignalsSlice = carpet.take("abortSignals", abortBytes);
206
+ const requestPayloadSlice = carpet.take("requestPayload", payloadBytes);
207
+ const workerSlices = Array.from({ length: threads }, (_, thread) => ({
208
+ signal: carpet.take(`signals-${thread}`, signalBytes),
209
+ returnLock: carpet.take(`returnLockSector-${thread}`, LOCK_SECTOR_BYTE_LENGTH),
210
+ returnHeaders: carpet.take(`returnHeaders-${thread}`, getHeaderBlockByteLength({
211
+ slotCount: LockBound.slots,
212
+ slotStrideU32: HEADER_SLOT_STRIDE_U32,
213
+ alignTo: 64,
214
+ })),
215
+ returnPayload: carpet.take(`returnPayload-${thread}`, payloadBytes),
216
+ }));
217
+ const primitives = getProcessWorkerSharedMemoryPrimitives();
218
+ const forceNamed = sharedMemory.mode === "named" || isWindowsRuntimeHost();
219
+ const mapping = primitives.createSharedMemory(forceNamed
220
+ ? {
221
+ size: carpet.byteLength(),
222
+ mode: "create",
223
+ name: makeProcessWorkerMemoryName(0, sharedMemory.namePrefix),
224
+ }
225
+ : { size: carpet.byteLength() });
226
+ const descriptor = FileDescriptor.fromMapping(mapping);
227
+ if (isWindowsRuntimeHost() && descriptor.name === undefined) {
228
+ throw new Error("Windows process worker shared memory must use a named mapping");
229
+ }
230
+ if (sharedMemory.mode === "named" && descriptor.name === undefined) {
231
+ throw new Error("processSharedMemory mode named needs a named shared-memory backend");
232
+ }
233
+ const buffer = mapping.buffer;
234
+ const bind = (slice) => makeSharedBufferRegion(buffer, slice.byteOffset, slice.byteLength);
235
+ const abortSignals = bind(abortSignalsSlice);
236
+ const requestLock = {
237
+ headers: bind(requestHeadersSlice),
238
+ headerSlotStrideU32: HEADER_SLOT_STRIDE_U32,
239
+ lockSector: bind(requestLockSlice),
240
+ payloadSector: bind(requestLockSlice),
241
+ };
242
+ // Each worker calls cleanup, so named-memory unlink happens only after every
243
+ // endpoint has released its view. The per-worker mapping wrappers below do
244
+ // not expose close(); the host views remain attached until host exit because
245
+ // Bun's external ArrayBuffer finalizer can race an explicit munmap.
246
+ let remaining = threads;
247
+ let cleaned = false;
248
+ const cleanupSharedMapping = () => {
249
+ if (cleaned)
250
+ return;
251
+ remaining--;
252
+ if (remaining > 0)
253
+ return;
254
+ cleaned = true;
255
+ const name = descriptor.name;
256
+ if (name !== undefined && sharedMemory.unlinkOnShutdown) {
257
+ primitives.unlinkSharedMemory?.(name);
258
+ }
259
+ };
260
+ const workerMapping = {
261
+ ...mapping,
262
+ close: undefined,
263
+ };
264
+ const workers = workerSlices.map((slices) => {
265
+ const returnLock = {
266
+ headers: bind(slices.returnHeaders),
267
+ headerSlotStrideU32: HEADER_SLOT_STRIDE_U32,
268
+ lockSector: bind(slices.returnLock),
269
+ payloadSector: bind(slices.returnLock),
270
+ };
271
+ return {
272
+ mapping: workerMapping,
273
+ descriptor,
274
+ controlLayout: {
275
+ controlSAB: buffer,
276
+ signals: bind(slices.signal),
277
+ abortSignals,
278
+ lock: requestLock,
279
+ returnLock,
280
+ slices: carpet.slices,
281
+ },
282
+ lockPayload: bind(requestPayloadSlice),
283
+ returnPayload: bind(slices.returnPayload),
284
+ cleanup: cleanupSharedMapping,
285
+ };
286
+ });
287
+ return {
288
+ mapping,
289
+ descriptor,
290
+ workers,
291
+ abortSignalMax,
292
+ };
293
+ };
239
294
  const toChildProcessSharedBufferMetadata = (source, descriptor) => {
240
295
  const region = toSharedBufferRegion(source);
241
296
  const metadata = descriptor.toMetadata();
@@ -254,49 +309,6 @@ const toProcessWorkerWireLockBuffers = (lock, descriptor) => ({
254
309
  payload: toChildProcessSharedBufferMetadata(lock.payload, descriptor),
255
310
  payloadSector: toChildProcessSharedBufferMetadata(lock.payloadSector, descriptor),
256
311
  });
257
- const isPlainRecord = (value) => {
258
- if (value === null || typeof value !== "object")
259
- return false;
260
- const prototype = Object.getPrototypeOf(value);
261
- return prototype === Object.prototype || prototype === null;
262
- };
263
- const serializeWorkerBootstrapValue = (value, seen = new WeakMap()) => {
264
- if (value instanceof ProcessSharedBuffer)
265
- return value.toMetadata();
266
- if (value === null || typeof value !== "object")
267
- return value;
268
- const existing = seen.get(value);
269
- if (existing !== undefined)
270
- return existing;
271
- if (Array.isArray(value)) {
272
- const out = [];
273
- seen.set(value, out);
274
- for (const item of value) {
275
- out.push(serializeWorkerBootstrapValue(item, seen));
276
- }
277
- return out;
278
- }
279
- if (!isPlainRecord(value))
280
- return value;
281
- const out = {};
282
- seen.set(value, out);
283
- for (const [key, item] of Object.entries(value)) {
284
- out[key] = serializeWorkerBootstrapValue(item, seen);
285
- }
286
- return out;
287
- };
288
- export const serializeWorkerBootstrapData = (options) => {
289
- const bootstrap = options.bootstrap;
290
- if (bootstrap === undefined || bootstrap.data === undefined)
291
- return options;
292
- return {
293
- ...options,
294
- bootstrap: {
295
- ...bootstrap,
296
- data: serializeWorkerBootstrapValue(bootstrap.data),
297
- },
298
- };
299
- };
300
312
  export const toProcessWorkerBootPayload = (workerData, memory) => ({
301
313
  version: RUNTIME_PROCESS_WORKER_BOOT_VERSION,
302
314
  workerData: {
@@ -417,10 +429,62 @@ const processWorkerNodeBinary = (commandPrefix) => {
417
429
  : undefined) ??
418
430
  DEFAULT_NODE_BINARY;
419
431
  };
420
- const processWorkerNodeExecArgv = (permission) => {
432
+ const processWorkerNodeMajorCache = new Map();
433
+ const resolveProcessWorkerNodeMajor = (commandPrefix) => {
434
+ const nodeProcess = getNodeProcess();
435
+ if (commandPrefix === undefined &&
436
+ nodeProcess !== undefined &&
437
+ nodeProcess?.env?.NODE_BINARY === undefined &&
438
+ RUNTIME === "node") {
439
+ return parseNodeMajorVersion(nodeProcess.versions?.node);
440
+ }
441
+ const nodeBinary = processWorkerNodeBinary(commandPrefix);
442
+ const cacheKey = JSON.stringify([commandPrefix ?? null, nodeBinary]);
443
+ if (processWorkerNodeMajorCache.has(cacheKey)) {
444
+ return processWorkerNodeMajorCache.get(cacheKey);
445
+ }
446
+ let major;
447
+ try {
448
+ const childProcess = getNodeBuiltinModule("node:child_process");
449
+ if (typeof childProcess?.spawnSync === "function") {
450
+ const command = commandPrefix?.[0] ?? nodeBinary;
451
+ const args = commandPrefix === undefined
452
+ ? ["--version"]
453
+ : [...commandPrefix.slice(1), nodeBinary, "--version"];
454
+ const probe = childProcess.spawnSync(command, args, {
455
+ encoding: "utf8",
456
+ stdio: ["ignore", "pipe", "ignore"],
457
+ timeout: 2_000,
458
+ windowsHide: true,
459
+ });
460
+ if (probe.status === 0) {
461
+ major = parseNodeMajorVersion(probe.stdout);
462
+ }
463
+ }
464
+ }
465
+ catch {
466
+ // Unknown targets conservatively use the Node 22/24 addon transport.
467
+ }
468
+ processWorkerNodeMajorCache.set(cacheKey, major);
469
+ return major;
470
+ };
471
+ export const readProcessWorkerNodeMajor = (options) => resolveProcessWorkerNodeMajor(readProcessWorkerCommandPrefix(options));
472
+ const nodeProcessWorkerFlagIsSupported = (flag, nodeMajor) => {
473
+ const key = flag.split("=", 1)[0];
474
+ if (key === "--experimental-ffi" || key === "--allow-ffi") {
475
+ return nodeProcessWorkerUsesFfi(nodeMajor);
476
+ }
477
+ if (key === "--allow-net") {
478
+ return nodeMajor !== undefined && nodeMajor >= 25;
479
+ }
480
+ return true;
481
+ };
482
+ const processWorkerNodeExecArgv = (permission, nodeMajor) => {
421
483
  const out = [];
422
484
  const seen = new Set();
423
485
  const add = (flag) => {
486
+ if (!nodeProcessWorkerFlagIsSupported(flag, nodeMajor))
487
+ return;
424
488
  if (seen.has(flag))
425
489
  return;
426
490
  seen.add(flag);
@@ -429,16 +493,17 @@ const processWorkerNodeExecArgv = (permission) => {
429
493
  for (const flag of toWorkerCompatExecArgv(getNodeProcess()?.execArgv) ?? []) {
430
494
  add(flag);
431
495
  }
432
- for (const flag of nodeProcessWorkerInternalExecArgv())
496
+ for (const flag of nodeProcessWorkerInternalExecArgv(nodeMajor))
433
497
  add(flag);
434
498
  if (permission?.enabled === true && permission.unsafe !== true) {
435
499
  for (const flag of permission.node.flags)
436
500
  add(flag);
437
- if (permission.netAll && nodeMajorVersion() >= 25)
501
+ if (permission.netAll && nodeMajor !== undefined && nodeMajor >= 25) {
438
502
  add("--allow-net");
503
+ }
439
504
  // Node process workers need one native transport capability regardless of
440
505
  // task permissions: addons on Node 22/24, node:ffi on supported Node 26+ LTS.
441
- add(nodeProcessWorkerUsesFfi() ? "--allow-ffi" : "--allow-addons");
506
+ add(nodeProcessWorkerUsesFfi(nodeMajor) ? "--allow-ffi" : "--allow-addons");
442
507
  }
443
508
  return out;
444
509
  };
@@ -454,9 +519,10 @@ const processWorkerCommand = ({ processRuntime, workerUrl, bun, deno, commandPre
454
519
  ];
455
520
  }
456
521
  else if (processRuntime === "node") {
522
+ const nodeMajor = resolveProcessWorkerNodeMajor(commandPrefix);
457
523
  command = [
458
524
  processWorkerNodeBinary(commandPrefix),
459
- ...processWorkerNodeExecArgv(permission),
525
+ ...processWorkerNodeExecArgv(permission, nodeMajor),
460
526
  workerPath,
461
527
  ];
462
528
  }
@@ -596,9 +662,23 @@ const spawnNodeHostedProcessWorker = ({ workerUrl, bootPayload, memory, processR
596
662
  queueMicrotask(() => child.send?.(bootPayload));
597
663
  }
598
664
  child.on("error", events.emitError);
599
- child.on("exit", (code) => events.emitExit(code ?? -1));
665
+ let resolveExit = () => { };
666
+ const exited = new Promise((resolve) => {
667
+ resolveExit = resolve;
668
+ });
669
+ child.on("exit", (code) => {
670
+ resolveExit();
671
+ events.emitExit(code ?? -1);
672
+ });
600
673
  return {
601
- terminate: () => child.kill(),
674
+ terminate: () => {
675
+ try {
676
+ child.kill();
677
+ }
678
+ catch {
679
+ }
680
+ return exited;
681
+ },
602
682
  unref: () => child.unref?.(),
603
683
  on: events.on,
604
684
  };
@@ -687,16 +767,6 @@ export const spawnProcessWorker = (options) => {
687
767
  throw new Error("process worker runtime is only available in Node, Deno, or Bun");
688
768
  }
689
769
  };
690
- export const terminateWorkerQuietly = (worker) => {
691
- try {
692
- // Runaway worker termination can be slow or stuck on some runtimes; once the
693
- // pool is closing it must not keep the host process alive.
694
- worker.unref?.();
695
- void Promise.resolve(worker.terminate()).catch(() => { });
696
- }
697
- catch {
698
- }
699
- };
700
770
  export const cleanupProcessWorkerMemoryQuietly = (memory) => {
701
771
  try {
702
772
  memory?.cleanup();
@@ -11,15 +11,31 @@ type CreateHostTxQueueArgs = {
11
11
  max?: number;
12
12
  lock: Lock2;
13
13
  returnLock: Lock2;
14
+ /**
15
+ * Extra return locks to drain into the same pending registry.
16
+ *
17
+ * Under work stealing the host publishes into one shared submit lock and any
18
+ * worker may claim the task, so the response comes back on whichever lane
19
+ * executed it. Task IDs index this queue's `queue` array, which is
20
+ * pool-global, so every lane's `resolveHost` can settle into it — the
21
+ * stealing worker owns the response and the host demultiplexes by ID.
22
+ *
23
+ * When omitted, only `returnLock` is drained (the classic one-lane shape).
24
+ */
25
+ extraReturnLocks?: readonly Lock2[];
14
26
  releaseBufferReferenceReturn?: ((token: bigint) => void) | BufferReferenceReturnHooks;
15
27
  abortSignals?: Pick<SignalAbortStore, "getSignal" | "setSignal" | "resetSignal" | "closeNow">;
16
28
  now?: () => number;
17
29
  };
18
- export declare function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenceReturn, abortSignals, now, }: CreateHostTxQueueArgs): {
30
+ type ReturnHooks = ((token: bigint) => void) | BufferReferenceReturnHooks;
31
+ export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, releaseBufferReferenceReturn, abortSignals, now, }: CreateHostTxQueueArgs): {
19
32
  rejectAll: (reason: string) => void;
20
33
  hasPendingFrames: () => boolean;
21
34
  txIdle: () => boolean;
22
35
  completeFrame: () => number;
36
+ waitForCompletion: (onWake: () => void, timeoutMs?: number) => boolean;
37
+ setCompletionWaiterArmed: (armed: boolean) => void;
38
+ setReturnHooks: (lane: number, hooks: ReturnHooks | undefined) => void;
23
39
  enqueue: (functionID: FunctionID, timeout?: TaskTimeout, abortSignal?: AbortSignalOption) => (rawArgs: RawArguments) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
24
40
  flushToWorker: () => boolean;
25
41
  enqueueKnown: (task: QueueTask) => boolean;
@@ -14,7 +14,7 @@ const FUNCTION_META_SHIFT = 16;
14
14
  const ABORT_SIGNAL_META_OFFSET = 1;
15
15
  const NO_ABORT_SIGNAL = -1;
16
16
  const p_now = performance.now.bind(performance);
17
- export function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenceReturn, abortSignals, now, }) {
17
+ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, releaseBufferReferenceReturn, abortSignals, now, }) {
18
18
  const PLACE_HOLDER = (_) => {
19
19
  throw ("UNREACHABLE FROM PLACE HOLDER (main)");
20
20
  };
@@ -37,22 +37,119 @@ export function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenc
37
37
  let inUsed = 0 | 0;
38
38
  const resetSignal = abortSignals?.resetSignal;
39
39
  const nowTime = now ?? p_now;
40
- const resolveReturn = returnLock.resolveHost({
40
+ const onReturnResolved = (task) => {
41
+ inUsed = (inUsed - 1) | 0;
42
+ resetTaskLocalFlags(task);
43
+ runTaskFinalizers(task);
44
+ task.value = null;
45
+ task.resolve = PLACE_HOLDER;
46
+ task.reject = PLACE_HOLDER;
47
+ freePush(task[TaskIndex.ID]);
48
+ };
49
+ const returnResolvers = [
50
+ returnLock,
51
+ ...(extraReturnLocks ?? []),
52
+ ].map((each) => each.resolveHost({
41
53
  queue,
42
54
  activeRejectPlaceholder: PLACE_HOLDER,
43
- onResolved: (task) => {
44
- inUsed = (inUsed - 1) | 0;
45
- resetTaskLocalFlags(task);
46
- runTaskFinalizers(task);
47
- task.value = null;
48
- task.resolve = PLACE_HOLDER;
49
- task.reject = PLACE_HOLDER;
50
- freePush(task[TaskIndex.ID]);
51
- },
52
- });
53
- const completeFrame = releaseBufferReferenceReturn === undefined
54
- ? resolveReturn
55
- : () => withBufferReferenceReturnReleaser(releaseBufferReferenceReturn, resolveReturn);
55
+ onResolved: onReturnResolved,
56
+ }));
57
+ const returnWaiters = [
58
+ returnLock,
59
+ ...(extraReturnLocks ?? []),
60
+ ].map((each) => typeof each.waitForHostChange === "function"
61
+ ? each.waitForHostChange
62
+ : () => undefined);
63
+ const returnArmers = [
64
+ returnLock,
65
+ ...(extraReturnLocks ?? []),
66
+ ].map((each) => typeof each.setHostWaiterArmed === "function"
67
+ ? each.setHostWaiterArmed
68
+ : (_armed) => { });
69
+ // A stealing queue drains one private return lock per worker. Borrowed
70
+ // BufferReferences must therefore be claimed with the hooks belonging to the
71
+ // worker that produced that particular return. The hooks are bound after the
72
+ // worker context exists, while the pool-global queue is built before workers
73
+ // spawn, so keep one mutable hook slot per return lane.
74
+ const returnHooks = new Array(returnResolvers.length);
75
+ if (releaseBufferReferenceReturn !== undefined) {
76
+ returnHooks[0] = releaseBufferReferenceReturn;
77
+ }
78
+ const resolveReturnAt = (index) => {
79
+ const resolve = returnResolvers[index];
80
+ const hooks = returnHooks[index];
81
+ return hooks === undefined
82
+ ? resolve()
83
+ : withBufferReferenceReturnReleaser(hooks, resolve);
84
+ };
85
+ // Preserve the original one-lane fast path exactly: no wrapper, array lookup,
86
+ // or hook branch is paid by the default one-worker transport. Mutable
87
+ // per-return-lane hooks are needed only by a multi-worker stealing queue.
88
+ const completeFrame = returnResolvers.length === 1
89
+ ? releaseBufferReferenceReturn === undefined
90
+ ? returnResolvers[0]
91
+ : () => withBufferReferenceReturnReleaser(releaseBufferReferenceReturn, returnResolvers[0])
92
+ : () => {
93
+ let resolved = 0 | 0;
94
+ for (let i = 0; i < returnResolvers.length; i++) {
95
+ resolved = (resolved + resolveReturnAt(i)) | 0;
96
+ }
97
+ return resolved;
98
+ };
99
+ // A stealing queue has one private return lock per worker. Keep at most one
100
+ // waitAsync waiter per lane: after lane A rings, lane B's unresolved waiter
101
+ // must survive the next arm or Atomics.notify(..., 1) could wake an obsolete
102
+ // waiter and starve the live one until the watchdog timeout.
103
+ const completionArmed = new Uint8Array(returnWaiters.length);
104
+ let completionWake;
105
+ let completionGeneration = 0 | 0;
106
+ const setCompletionWaiterArmed = (armed) => {
107
+ for (const setArmed of returnArmers)
108
+ setArmed(armed);
109
+ };
110
+ const waitForCompletion = (onWake, timeoutMs) => {
111
+ completionWake = onWake;
112
+ // A persistent waiter may still be pending after a send preempted the
113
+ // dispatcher. Re-arm its shared gate before relying on that waiter again.
114
+ setCompletionWaiterArmed(true);
115
+ let supported = true;
116
+ for (let index = 0; index < returnWaiters.length; index++) {
117
+ if (completionArmed[index] !== 0)
118
+ continue;
119
+ let wait;
120
+ try {
121
+ wait = returnWaiters[index](timeoutMs);
122
+ }
123
+ catch {
124
+ wait = undefined;
125
+ }
126
+ if (wait === undefined) {
127
+ supported = false;
128
+ break;
129
+ }
130
+ completionArmed[index] = 1;
131
+ const generation = completionGeneration;
132
+ const wakeLane = () => {
133
+ if (completionArmed[index] === 0)
134
+ return;
135
+ completionArmed[index] = 0;
136
+ returnArmers[index](false);
137
+ if (generation !== completionGeneration)
138
+ return;
139
+ completionWake?.();
140
+ };
141
+ if (!wait.async)
142
+ wakeLane();
143
+ else
144
+ Promise.resolve(wait.value).then(wakeLane, wakeLane);
145
+ }
146
+ if (!supported) {
147
+ completionWake = undefined;
148
+ setCompletionWaiterArmed(false);
149
+ completionGeneration = (completionGeneration + 1) | 0;
150
+ }
151
+ return supported;
152
+ };
56
153
  const hasActiveTasks = () => {
57
154
  const count = (inUsed - getPendingPromiseCount()) | 0;
58
155
  return count > 0;
@@ -85,6 +182,14 @@ export function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenc
85
182
  hasPendingFrames,
86
183
  txIdle,
87
184
  completeFrame,
185
+ waitForCompletion,
186
+ setCompletionWaiterArmed,
187
+ setReturnHooks: (lane, hooks) => {
188
+ if (!Number.isInteger(lane) || lane < 0 || lane >= returnHooks.length) {
189
+ throw new RangeError(`return lane ${lane} out of range`);
190
+ }
191
+ returnHooks[lane] = hooks;
192
+ },
88
193
  enqueue: (functionID, timeout, abortSignal) => {
89
194
  const HAS_TIMER = timeout !== undefined;
90
195
  const functionIDMasked = functionID & FUNCTION_ID_MASK;
@@ -0,0 +1,13 @@
1
+ import type { WorkerSettings } from "../types.js";
2
+ export type SpawnedWorker = {
3
+ terminate: () => unknown;
4
+ unref?: () => unknown;
5
+ postMessage?: (message: unknown) => void;
6
+ };
7
+ export type NodeWorkerLike = {
8
+ on?: (event: "error" | "exit" | "message", listener: (...args: unknown[]) => void) => void;
9
+ };
10
+ export declare const toWorkerSafeExecArgv: (flags: string[] | undefined) => string[] | undefined;
11
+ export declare const toWorkerCompatExecArgv: (flags: string[] | undefined) => string[] | undefined;
12
+ export declare const serializeWorkerBootstrapData: (options: WorkerSettings) => WorkerSettings;
13
+ export declare const terminateWorkerQuietly: (worker: SpawnedWorker) => Promise<void>;