knitting 0.1.63 → 0.1.73

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 (99) hide show
  1. package/README.md +623 -342
  2. package/knitting.browser.d.ts +3 -1
  3. package/knitting.browser.js +1 -1
  4. package/knitting.d.ts +3 -1
  5. package/knitting.js +2 -1
  6. package/map.md +0 -6
  7. package/package.json +10 -5
  8. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  9. package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
  10. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  11. package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
  12. package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
  14. package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
  15. package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
  16. package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
  18. package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
  19. package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
  20. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  21. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  22. package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
  23. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  24. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  25. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  26. package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
  27. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  28. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  29. package/scripts/build-native-addons.ts +5 -0
  30. package/shared-memory.d.ts +3 -0
  31. package/shared-memory.js +3 -0
  32. package/src/api.js +158 -71
  33. package/src/common/with-resolvers.js +2 -5
  34. package/src/common/worker-runtime.d.ts +7 -0
  35. package/src/common/worker-runtime.js +7 -0
  36. package/src/connections/buffer-reference.d.ts +10 -36
  37. package/src/connections/buffer-reference.js +15 -170
  38. package/src/connections/node-addons.d.ts +1 -1
  39. package/src/connections/node-addons.js +11 -1
  40. package/src/connections/shared-array-buffer-payload.d.ts +7 -0
  41. package/src/connections/shared-array-buffer-payload.js +27 -11
  42. package/src/debug/gate.js +1 -1
  43. package/src/debug/handle.d.ts +6 -1
  44. package/src/debug/handle.js +14 -6
  45. package/src/error.d.ts +9 -0
  46. package/src/error.js +16 -2
  47. package/src/knitting_buffer_pointer.cc +57 -2
  48. package/src/knitting_doorbell.cc +220 -0
  49. package/src/memory/knitting-body.d.ts +44 -0
  50. package/src/memory/knitting-body.js +51 -0
  51. package/src/memory/knitting-buffer-http.d.ts +116 -0
  52. package/src/memory/knitting-buffer-http.js +255 -0
  53. package/src/memory/knitting-buffer.d.ts +250 -0
  54. package/src/memory/knitting-buffer.js +695 -0
  55. package/src/memory/lazy-region-registry.d.ts +83 -0
  56. package/src/memory/lazy-region-registry.js +355 -0
  57. package/src/memory/lock.d.ts +80 -15
  58. package/src/memory/lock.js +473 -139
  59. package/src/memory/payloadCodec.d.ts +18 -2
  60. package/src/memory/payloadCodec.js +340 -76
  61. package/src/memory/regionRegistry.d.ts +6 -0
  62. package/src/memory/regionRegistry.js +125 -240
  63. package/src/memory/shared-buffer-io.d.ts +7 -0
  64. package/src/memory/shared-buffer-io.js +34 -8
  65. package/src/permission/protocol.d.ts +1 -0
  66. package/src/permission/protocol.js +8 -3
  67. package/src/runtime/deno-doorbell.d.ts +26 -0
  68. package/src/runtime/deno-doorbell.js +117 -0
  69. package/src/runtime/dispatcher.d.ts +13 -6
  70. package/src/runtime/dispatcher.js +101 -63
  71. package/src/runtime/host-arg-arena.d.ts +3 -0
  72. package/src/runtime/host-arg-arena.js +16 -0
  73. package/src/runtime/inline-executor.js +2 -1
  74. package/src/runtime/node-doorbell.d.ts +14 -0
  75. package/src/runtime/node-doorbell.js +84 -0
  76. package/src/runtime/pool.d.ts +30 -15
  77. package/src/runtime/pool.js +199 -151
  78. package/src/runtime/process-worker.d.ts +9 -0
  79. package/src/runtime/process-worker.js +32 -3
  80. package/src/runtime/tx-queue.d.ts +4 -6
  81. package/src/runtime/tx-queue.js +63 -48
  82. package/src/runtime/worker-common.d.ts +7 -0
  83. package/src/runtime/worker-common.js +28 -2
  84. package/src/types.d.ts +66 -78
  85. package/src/worker/loop.js +95 -60
  86. package/src/worker/rx-queue.d.ts +2 -3
  87. package/src/worker/rx-queue.js +34 -40
  88. package/src/worker/safety/index.d.ts +1 -1
  89. package/src/worker/safety/index.js +1 -1
  90. package/src/worker/safety/process.d.ts +2 -0
  91. package/src/worker/safety/process.js +8 -1
  92. package/src/worker/safety/startup.js +11 -6
  93. package/src/worker/shared-return.d.ts +9 -0
  94. package/src/worker/shared-return.js +22 -0
  95. package/src/worker/task-loader.js +1 -2
  96. package/src/worker/timers.d.ts +2 -6
  97. package/src/worker/timers.js +39 -22
  98. package/unsafe.d.ts +2 -1
  99. package/unsafe.js +2 -1
@@ -1,20 +1,20 @@
1
1
  import { createHostTxQueue } from "./tx-queue.js";
2
- import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessStealMemoryLayout, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
3
- import { serializeWorkerBootstrapData, terminateWorkerQuietly, toWorkerCompatExecArgv, toWorkerSafeExecArgv, } from "./worker-common.js";
2
+ import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessStealMemoryLayout, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, processWorkerUsesIpc, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
3
+ import { serializeWorkerBootstrapData, terminateWorkerQuietly, isNodePermissionExecFlag, toWorkerSafeExecArgv, warnDroppedWorkerExecArgv, } from "./worker-common.js";
4
4
  import { createSharedMemoryTransport, TRANSPORT_SIGNAL_BYTES, WORKER_STOP, } from "../ipc/transport/shared-memory.js";
5
5
  import { ChannelHandler, hostDispatcherLoop, } from "./dispatcher.js";
6
- import { HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
7
- // Side-effect import: registers the payload codec (cycle break for Andromeda;
8
- // see lock.ts). Must run before any lock2() call.
6
+ import { KnittingError } from "../error.js";
7
+ import { createNodeCompletionDoorbell } from "./node-doorbell.js";
8
+ import { DEFAULT_STEAL_CLAIM, HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
9
+ // Registers the payload codec before any lock is built.
9
10
  import "../memory/payloadCodec.js";
10
11
  import "../worker/loop.js";
11
12
  import { createSharedArrayBuffer, createWasmSharedArrayBuffer, IS_BROWSER, RUNTIME, } from "../common/runtime.js";
12
- import { HAS_NODE_WORKER_THREADS, RUNTIME_WORKER, } from "../common/worker-runtime.js";
13
+ import { HAS_NODE_WORKER_THREADS, isProcessCompletionDoorbell, RUNTIME_WORKER, } from "../common/worker-runtime.js";
13
14
  import { probeLockBufferTextCompat } from "../common/shared-buffer-text.js";
14
15
  import { signalAbortFactory } from "../shared/abortSignal.js";
15
16
  import { createLockControlCarpet } from "../memory/byte-carpet.js";
16
17
  import { resolvePayloadBufferOptions, } from "../memory/payload-config.js";
17
- import { createBufferReferenceReturnReleaseMessage, detachArrayBufferBestEffort, } from "../connections/buffer-reference.js";
18
18
  import { spawnCompiledWorkerContext } from "./compiled-worker.js";
19
19
  const WORKER_FATAL_MESSAGE_KEY = "__knittingWorkerFatal";
20
20
  const isWorkerFatalMessage = (value) => !!value &&
@@ -22,9 +22,16 @@ const isWorkerFatalMessage = (value) => !!value &&
22
22
  typeof value[WORKER_FATAL_MESSAGE_KEY] === "string";
23
23
  // Bound the wait for a worker to acknowledge loop exit.
24
24
  const WORKER_STOP_ACK_TIMEOUT_MS = 50;
25
- const WORKER_STOP_NEEDS_ACK = RUNTIME === "deno";
26
- // Keep idle workers self-healing if an Atomics.notify wake is missed.
27
- const DEFAULT_WORKER_PARK_MS = 1;
25
+ // Node owns returned BufferReference backing stores in a process-global native
26
+ // registry. Give its worker loop a chance to drain deferred producer pins
27
+ // before the host falls back to terminate().
28
+ const WORKER_STOP_NEEDS_ACK = RUNTIME === "deno" || RUNTIME === "node";
29
+ // The park timeout is only a safety net: the host wakes a parked thread worker
30
+ // after every publish, so a long park costs no latency and idles at ~0 CPU.
31
+ const DEFAULT_WORKER_PARK_MS = 1000;
32
+ // A process worker can only be rung across processes by a Node host waking a
33
+ // Node child; every other pairing rediscovers work on this timeout.
34
+ const DEFAULT_PROCESS_WORKER_PARK_MS = 1;
28
35
  const SINGLE_WORKER_SPIN_US = 50;
29
36
  const DEFAULT_ABORT_SIGNAL_CAPACITY = 258;
30
37
  const sanitizePositiveInteger = (value) => {
@@ -36,7 +43,10 @@ const sanitizePositiveInteger = (value) => {
36
43
  const resolveAbortSignalCapacity = (value) => sanitizePositiveInteger(value) ?? DEFAULT_ABORT_SIGNAL_CAPACITY;
37
44
  const abortSignalByteLength = (capacity) => Math.max(1, Math.ceil(capacity / 32)) * Uint32Array.BYTES_PER_ELEMENT;
38
45
  const withDefaultWorkerTimers = (options, workerCount) => {
39
- const parkMs = options?.timers?.parkMs ?? DEFAULT_WORKER_PARK_MS;
46
+ const parkMs = options?.timers?.parkMs ??
47
+ (options?.runtime === "process"
48
+ ? DEFAULT_PROCESS_WORKER_PARK_MS
49
+ : DEFAULT_WORKER_PARK_MS);
40
50
  // `??` and not `||`: an explicit `spinMicroseconds: 0` is the whole point of
41
51
  // the multi-worker default and must not fall through to it.
42
52
  const spinMicroseconds = options?.timers?.spinMicroseconds ??
@@ -57,16 +67,7 @@ const withFixedPayloadConfig = (config) => ({
57
67
  mode: "fixed",
58
68
  payloadInitialBytes: config.payloadMaxByteLength,
59
69
  });
60
- /**
61
- * Build the buffers, host-side locks and pending registry that a stealing pool
62
- * shares. One submit region for every worker to claim from, one private return
63
- * region per worker, and a single pool-global queue so a response arriving on
64
- * any lane settles the right promise.
65
- *
66
- * Region width follows paper §6.1: the largest power-of-two `g` leaving a spare
67
- * region for a delayed claimant (`slots / g >= consumers + 1`). At 32 slots that
68
- * caps how wide a region can be well before it caps the worker count.
69
- */
70
+ /** Build the shared submit and private return buffers for a stealing pool. */
70
71
  export const resolveStealRegionLanes = (consumers) => {
71
72
  for (let lanes = LockBound.slots; lanes >= 1; lanes >>= 1) {
72
73
  if (LockBound.slots / lanes >= consumers + 1)
@@ -74,9 +75,17 @@ export const resolveStealRegionLanes = (consumers) => {
74
75
  }
75
76
  throw new RangeError(`${consumers} stealing workers need more than ${LockBound.slots} lanes`);
76
77
  };
78
+ /**
79
+ * Return the widest region allowed by the selected claim discipline. Only
80
+ * Dekker needs a region per consumer; `ticket` has no regions at all and uses
81
+ * the width purely as a claim batch size.
82
+ */
83
+ export const resolveMaxStealRegionLanes = (consumers, stealClaim = DEFAULT_STEAL_CLAIM) => stealClaim === "dekker"
84
+ ? resolveStealRegionLanes(consumers)
85
+ : LockBound.slots;
77
86
  /** Maximum claimants that leave the protocol's required spare region. */
78
87
  export const MAX_STEAL_CONSUMERS = LockBound.slots - 1;
79
- export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, processWorker, }) => {
88
+ export const createStealPoolBuffers = ({ threads, payload, sharedArgs, regionLanes, stealClaim, abortSignalCapacity, usesAbortSignal, processWorker, }) => {
80
89
  const basePayloadConfig = resolvePayloadBufferOptions({ options: payload });
81
90
  const payloadConfig = processWorker === undefined
82
91
  ? basePayloadConfig
@@ -100,9 +109,10 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
100
109
  payload: payloadSab,
101
110
  }),
102
111
  });
103
- const maxLanes = resolveStealRegionLanes(threads);
112
+ // Keep the default width valid for Dekker; wider regions are explicit.
113
+ const maxLanes = resolveMaxStealRegionLanes(threads, stealClaim);
104
114
  const lanes = regionLanes === undefined
105
- ? maxLanes
115
+ ? resolveStealRegionLanes(threads)
106
116
  : Math.min(Math.max(1, regionLanes | 0), maxLanes);
107
117
  const resolvedAbortSignalCapacity = resolveAbortSignalCapacity(abortSignalCapacity);
108
118
  const processMemory = processWorker === undefined ? undefined : createProcessStealMemoryLayout({
@@ -133,7 +143,11 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
133
143
  textCompat: submitBuffers.textCompat,
134
144
  consumers: threads,
135
145
  regionLanes: lanes,
146
+ stealClaim,
136
147
  processBoundary: processMemory !== undefined,
148
+ // Shared arguments are opt-in because the receiving task may outlive the
149
+ // borrowed region.
150
+ sharedReturn: sharedArgs === true,
137
151
  });
138
152
  const returnBuffers = [];
139
153
  const hostReturnLocks = [];
@@ -179,13 +193,14 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
179
193
  hostReturnLocks,
180
194
  sharedQueue,
181
195
  regionLanes: lanes,
196
+ stealClaim,
182
197
  processMemory,
183
198
  abortSignalSAB,
184
199
  abortSignalMax: processMemory?.abortSignalMax ??
185
200
  (abortSignalSAB === undefined ? undefined : resolvedAbortSignalCapacity),
186
201
  };
187
202
  };
188
- export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, stealPool, }) => {
203
+ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, debugEpoch, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, requestedExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }) => {
189
204
  if (workerOptions?.runtime === "compiled") {
190
205
  return spawnCompiledWorkerContext({
191
206
  list,
@@ -200,12 +215,20 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
200
215
  const poliWorker = RUNTIME_WORKER;
201
216
  const resolvedWorkerOptions = serializeWorkerBootstrapData(withDefaultWorkerTimers(workerOptions, workerCount ?? totalNumberOfThread));
202
217
  const useProcessWorkerRuntime = resolvedWorkerOptions.runtime === "process";
218
+ const canUseDenoCompletionDoorbell = !useProcessWorkerRuntime &&
219
+ denoCompletionDoorbell !== undefined;
203
220
  const processWorkerRuntime = useProcessWorkerRuntime
204
221
  ? readProcessWorkerRuntime(resolvedWorkerOptions)
205
222
  : undefined;
206
223
  const processWorkerCommandPrefix = useProcessWorkerRuntime
207
224
  ? readProcessWorkerCommandPrefix(resolvedWorkerOptions)
208
225
  : undefined;
226
+ const canUseProcessCompletionDoorbell = useProcessWorkerRuntime &&
227
+ host?.doorbell !== false &&
228
+ processWorkerUsesIpc({
229
+ processRuntime: processWorkerRuntime,
230
+ commandPrefix: processWorkerCommandPrefix,
231
+ });
209
232
  const processSharedMemorySettings = useProcessWorkerRuntime
210
233
  ? readProcessSharedMemorySettings(resolvedWorkerOptions)
211
234
  : undefined;
@@ -238,10 +261,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
238
261
  const abortBytes = stealPool === undefined && usesAbortSignal === true
239
262
  ? abortSignalByteLength(resolvedAbortSignalCapacity)
240
263
  : 0;
241
- // A stealing process pool carves every lane out of one mapping, so lane
242
- // `thread` must find its slice there. Falling back to a private layout would
243
- // silently give this lane its own submit region — it would boot and then
244
- // never see a task the host published to the shared one.
264
+ // Stealing process workers use their slice of the pool-wide mapping.
245
265
  const stealProcessMemory = stealPool?.processMemory;
246
266
  const processWorkerMemory = !useProcessWorkerRuntime
247
267
  ? undefined
@@ -264,18 +284,14 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
264
284
  createWasmSharedArrayBuffer;
265
285
  const createPayloadBuffer = processSharedMemory?.createBuffer;
266
286
  const makePayloadBuffer = () => createPayloadBuffer
267
- // ProcessSharedBuffer is fixed-size today, so reserve the configured
268
- // payload ceiling instead of relying on SAB growth.
287
+ // Process-shared buffers are fixed-size.
269
288
  ? createPayloadBuffer(resolvedPayloadConfig.payloadMaxByteLength)
270
289
  : resolvedPayloadConfig.mode === "growable"
271
290
  ? createSharedArrayBuffer(resolvedPayloadConfig.payloadInitialBytes, resolvedPayloadConfig.payloadMaxByteLength)
272
291
  : createSharedArrayBuffer(resolvedPayloadConfig.payloadInitialBytes);
273
292
  const makeLockControlLayout = () => {
274
- // Keep the hottest control words in one compact front strip:
275
- // transport signals -> request lock -> return lock.
276
- // Request/return headers stay in separate contiguous slabs to preserve
277
- // sequential batching locality.
278
- // Abort bitmap stays at the tail.
293
+ // Place hot control words first; keep headers contiguous and the abort bitmap
294
+ // at the tail.
279
295
  return createLockControlCarpet({
280
296
  signalBytes,
281
297
  abortBytes,
@@ -350,68 +366,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
350
366
  processRuntime: processWorkerRuntime,
351
367
  signal: signalBox.opView,
352
368
  });
353
- const outstandingBorrowedReturns = bufferReferenceReturn === "borrow" && typeof WeakRef === "function"
354
- ? new Map()
355
- : undefined;
356
- const releaseBorrowedReturnToken = (token) => {
357
- outstandingBorrowedReturns?.delete(token);
358
- worker?.postMessage?.(createBufferReferenceReturnReleaseMessage(token));
359
- };
360
- const revokeOutstandingBorrowedReturns = (copyBytes) => {
361
- if (outstandingBorrowedReturns === undefined)
362
- return;
363
- for (const [token, record] of outstandingBorrowedReturns) {
364
- try {
365
- const ref = record.ref.deref();
366
- if (ref !== undefined) {
367
- ref.revokeBorrow(copyBytes);
368
- continue;
369
- }
370
- const aliasBuffer = record.aliasBuffer?.deref();
371
- if (aliasBuffer !== undefined &&
372
- detachArrayBufferBestEffort(record.runtime, aliasBuffer) &&
373
- copyBytes) {
374
- releaseBorrowedReturnToken(token);
375
- }
376
- }
377
- catch {
378
- // best effort
379
- }
380
- }
381
- outstandingBorrowedReturns.clear();
382
- };
383
- const returnHooks = bufferReferenceReturn === "borrow"
384
- ? {
385
- release: releaseBorrowedReturnToken,
386
- track: (ref, token, aliasBuffer) => {
387
- if (outstandingBorrowedReturns === undefined)
388
- return;
389
- const record = outstandingBorrowedReturns.get(token);
390
- if (record !== undefined) {
391
- if (aliasBuffer !== undefined) {
392
- record.aliasBuffer = new WeakRef(aliasBuffer);
393
- }
394
- return;
395
- }
396
- outstandingBorrowedReturns.set(token, {
397
- ref: new WeakRef(ref),
398
- aliasBuffer: aliasBuffer === undefined
399
- ? undefined
400
- : new WeakRef(aliasBuffer),
401
- runtime: ref.runtime,
402
- });
403
- },
404
- }
405
- : undefined;
406
369
  const queue = stealPool?.sharedQueue ?? createHostTxQueue({
407
370
  lock,
408
371
  returnLock,
409
372
  abortSignals,
410
- releaseBufferReferenceReturn: returnHooks,
411
373
  });
412
- if (stealPool !== undefined) {
413
- stealPool.sharedQueue.setReturnHooks(stealPool.consumerId, returnHooks);
414
- }
415
374
  const { enqueue, rejectAll, txIdle, } = queue;
416
375
  const thisSignal = signalBox.opView;
417
376
  const a_add = Atomics.add;
@@ -420,8 +379,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
420
379
  const canNotifySignal = thisSignal.buffer instanceof SharedArrayBuffer;
421
380
  const notifySignal = nativeNotifySignal ??
422
381
  (canNotifySignal ? (() => a_notify(thisSignal, 0, 1)) : undefined);
423
- // Wakes this lane's worker when it is parked (rxStatus 0). Used by send()
424
- // (new work just arrived) — the dispatcher drain wakes it the same way.
382
+ // Wake this lane when its worker is parked.
425
383
  const laneWake = () => {
426
384
  if (a_load(signalBox.rxStatus, 0) === 0) {
427
385
  a_add(thisSignal, 0, 1);
@@ -434,10 +392,26 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
434
392
  const ownsChannel = sharedChannelHandler === undefined &&
435
393
  stealPool === undefined;
436
394
  const ownChannel = sharedChannelHandler ?? new ChannelHandler();
437
- // Under stealing there is one queue, so one dispatcher drives it from the
438
- // pool; a per-lane dispatcher would double-drain the shared registry.
439
- const { check: dispatcherCheck } = stealPool !== undefined
440
- ? { check: undefined }
395
+ const threadCanLoadNodeAddons = permission?.enabled !== true ||
396
+ permission.unsafe === true || permission.node.allowAddons === true;
397
+ const useNativeNodeDoorbell = RUNTIME === "node" &&
398
+ !useProcessWorkerRuntime && host?.doorbell !== false &&
399
+ host?.nativeDoorbell === true && threadCanLoadNodeAddons;
400
+ if (RUNTIME === "node" && !useProcessWorkerRuntime &&
401
+ host?.doorbell !== false && host?.nativeDoorbell === true &&
402
+ !threadCanLoadNodeAddons) {
403
+ hostDebug?.("native Node doorbell disabled by worker permissions; using the " +
404
+ "portable wake path (set permission.node.allowAddons=true to enable)");
405
+ }
406
+ // Create the Node doorbell before bootstrapping the worker.
407
+ let nodeCompletionWake;
408
+ const nodeCompletionDoorbell = useNativeNodeDoorbell
409
+ ? createNodeCompletionDoorbell(() => nodeCompletionWake?.())
410
+ : undefined;
411
+ const canUseNodeCompletionDoorbell = nodeCompletionDoorbell !== undefined;
412
+ // A stealing pool has one dispatcher for its shared queue.
413
+ const { check: dispatcherCheck, wakeCompletion: directCompletionWake, } = stealPool !== undefined
414
+ ? { check: undefined, wakeCompletion: undefined }
441
415
  : hostDispatcherLoop({
442
416
  signalBox,
443
417
  queue,
@@ -445,7 +419,15 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
445
419
  dispatcherOptions: host,
446
420
  notifySignal: nativeNotifySignal,
447
421
  crossProcess: useProcessWorkerRuntime,
422
+ nativeCompletionDoorbell: canUseDenoCompletionDoorbell ||
423
+ canUseNodeCompletionDoorbell,
424
+ processCompletionDoorbell: canUseProcessCompletionDoorbell,
448
425
  });
426
+ if (canUseDenoCompletionDoorbell && dispatcherCheck !== undefined) {
427
+ denoCompletionDoorbell.listen(thread, directCompletionWake);
428
+ }
429
+ nodeCompletionWake = directCompletionWake;
430
+ let processCompletionWake = directCompletionWake;
449
431
  if (ownsChannel && dispatcherCheck !== undefined) {
450
432
  ownChannel.open(dispatcherCheck);
451
433
  channelHandler = ownChannel;
@@ -453,7 +435,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
453
435
  if (dispatcherCheck.isRunning === true)
454
436
  return;
455
437
  dispatcherCheck.isRunning = true;
456
- Promise.resolve().then(dispatcherCheck);
438
+ queueMicrotask(dispatcherCheck);
457
439
  laneWake();
458
440
  };
459
441
  }
@@ -478,24 +460,34 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
478
460
  at,
479
461
  thread,
480
462
  debug,
463
+ debugEpoch,
481
464
  workerOptions: resolvedWorkerOptions,
482
465
  totalNumberOfThread,
483
466
  startAt: signalBox.startAt,
484
467
  lock: lockBuffers,
485
468
  returnLock: returnLockBuffers,
486
469
  payloadConfig: resolvedPayloadConfig,
487
- bufferReferenceReturn,
470
+ sharedReturn: sharedBytesEnabled === true,
488
471
  permission,
489
- // Mirrors the dispatcher's `canUseDoorbell`: a cross-process worker cannot
490
- // ring the host's waiter, so do not make it pay the atomic op.
491
- notifyOnHostPublish: !useProcessWorkerRuntime &&
492
- host?.doorbell !== false &&
493
- (RUNTIME === "bun" || RUNTIME === "node") &&
494
- typeof Atomics.waitAsync === "function",
472
+ // Select the completion doorbell supported by the worker runtime.
473
+ notifyOnHostPublish: host?.doorbell !== false &&
474
+ (canUseProcessCompletionDoorbell ||
475
+ canUseNodeCompletionDoorbell ||
476
+ (!useProcessWorkerRuntime &&
477
+ (RUNTIME === "bun" || RUNTIME === "node") &&
478
+ typeof Atomics.waitAsync === "function")),
479
+ processCompletionDoorbell: canUseProcessCompletionDoorbell,
480
+ denoCompletionDoorbell: canUseDenoCompletionDoorbell
481
+ ? denoCompletionDoorbell.pointer
482
+ : undefined,
483
+ nodeCompletionDoorbell: canUseNodeCompletionDoorbell
484
+ ? nodeCompletionDoorbell.pointer
485
+ : undefined,
495
486
  steal: stealPool === undefined ? undefined : {
496
487
  consumers: stealPool.consumers,
497
488
  consumerId: stealPool.consumerId,
498
489
  regionLanes: stealPool.regionLanes,
490
+ claim: stealPool.stealClaim,
499
491
  },
500
492
  };
501
493
  const baseWorkerOptions = {
@@ -507,6 +499,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
507
499
  const withExecArgv = workerExecArgv && workerExecArgv.length > 0
508
500
  ? { ...baseWorkerOptions, execArgv: workerExecArgv }
509
501
  : baseWorkerOptions;
502
+ const requiredNodePermissionFlags = RUNTIME === "node" &&
503
+ permission?.enabled === true &&
504
+ permission.unsafe !== true
505
+ ? permission.node.flags.filter(isNodePermissionExecFlag)
506
+ : [];
510
507
  if (processWorkerMemory !== undefined) {
511
508
  worker = spawnProcessWorker({
512
509
  workerUrl,
@@ -519,43 +516,59 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
519
516
  }
520
517
  else if (HAS_NODE_WORKER_THREADS) {
521
518
  try {
522
- worker = new WorkerCtor(workerUrl, withExecArgv);
523
- }
524
- catch (error) {
525
- if (error?.code === "ERR_WORKER_INVALID_EXEC_ARGV") {
526
- const fallbackExecArgv = toWorkerSafeExecArgv(withExecArgv.execArgv);
527
- if (fallbackExecArgv && fallbackExecArgv.length > 0) {
528
- try {
529
- worker = new WorkerCtor(workerUrl, { ...baseWorkerOptions, execArgv: fallbackExecArgv });
519
+ // The flags the worker actually started with.
520
+ let appliedExecArgv;
521
+ try {
522
+ worker = new WorkerCtor(workerUrl, withExecArgv);
523
+ appliedExecArgv = withExecArgv.execArgv;
524
+ }
525
+ catch (error) {
526
+ if (error?.code === "ERR_WORKER_INVALID_EXEC_ARGV") {
527
+ const fallbackExecArgv = toWorkerSafeExecArgv(withExecArgv.execArgv);
528
+ const missingPermissionFlags = requiredNodePermissionFlags.filter((flag) => !fallbackExecArgv?.includes(flag));
529
+ if (missingPermissionFlags.length > 0) {
530
+ throw new Error("Node rejected the permission flags required by Knitting's " +
531
+ `thread worker policy (${missingPermissionFlags.join(", ")}). ` +
532
+ "Refusing to retry without them.");
530
533
  }
531
- catch (fallbackError) {
532
- if (fallbackError?.code ===
533
- "ERR_WORKER_INVALID_EXEC_ARGV") {
534
- const compatExecArgv = toWorkerCompatExecArgv(fallbackExecArgv);
535
- if (compatExecArgv && compatExecArgv.length > 0) {
536
- try {
537
- worker = new WorkerCtor(workerUrl, { ...baseWorkerOptions, execArgv: compatExecArgv });
538
- }
539
- catch {
540
- worker = new WorkerCtor(workerUrl, baseWorkerOptions);
541
- }
534
+ if (fallbackExecArgv && fallbackExecArgv.length > 0) {
535
+ try {
536
+ worker = new WorkerCtor(workerUrl, { ...baseWorkerOptions, execArgv: fallbackExecArgv });
537
+ appliedExecArgv = fallbackExecArgv;
538
+ }
539
+ catch (fallbackError) {
540
+ if (requiredNodePermissionFlags.length > 0) {
541
+ throw new Error("Node could not start a thread worker with Knitting's " +
542
+ "permission policy. Refusing to start it without those " +
543
+ `permissions. ${String(fallbackError)}`);
542
544
  }
543
- else {
545
+ if (fallbackError?.code ===
546
+ "ERR_WORKER_INVALID_EXEC_ARGV") {
544
547
  worker = new WorkerCtor(workerUrl, baseWorkerOptions);
545
548
  }
549
+ else {
550
+ throw fallbackError;
551
+ }
546
552
  }
547
- else {
548
- throw fallbackError;
549
- }
553
+ }
554
+ else if (requiredNodePermissionFlags.length > 0) {
555
+ throw new Error("Node rejected the permission flags required by Knitting's " +
556
+ "thread worker policy. Refusing to start the worker without " +
557
+ "those permissions.");
558
+ }
559
+ else {
560
+ worker = new WorkerCtor(workerUrl, baseWorkerOptions);
550
561
  }
551
562
  }
552
563
  else {
553
- worker = new WorkerCtor(workerUrl, baseWorkerOptions);
564
+ throw error;
554
565
  }
555
566
  }
556
- else {
557
- throw error;
558
- }
567
+ warnDroppedWorkerExecArgv(requestedExecArgv, appliedExecArgv);
568
+ }
569
+ catch (error) {
570
+ nodeCompletionDoorbell?.close();
571
+ throw error;
559
572
  }
560
573
  }
561
574
  else {
@@ -565,6 +578,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
565
578
  worker.postMessage?.(workerDataPayload);
566
579
  }
567
580
  let closedReason;
581
+ // A worker that exits because the host asked it to stop is a closed thread,
582
+ // not a crash. The stop is acknowledged in shared memory before the worker
583
+ // unwinds, so on a slow host the `exit` event can land ahead of `kills()`
584
+ // and label an orderly shutdown "Worker exited with code 0".
585
+ let stopRequested = false;
568
586
  const deactivateStealConsumer = () => {
569
587
  stealPool?.hostSubmitLock.deactivateStealConsumer(stealPool.consumerId);
570
588
  };
@@ -579,26 +597,39 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
579
597
  deactivateStealConsumer();
580
598
  }
581
599
  };
582
- // `workerBytesReadable`: while the worker is still alive its pinned bytes
583
- // can be copied out; after a crash/exit they are gone and borrows are
584
- // detached so stale reads fail loud instead of aliasing freed memory.
585
- const markWorkerClosed = (reason, workerBytesReadable = false) => {
600
+ const markWorkerClosed = (code, reason, cause) => {
586
601
  if (closedReason)
587
602
  return;
588
603
  closedReason = reason;
589
- revokeOutstandingBorrowedReturns(workerBytesReadable);
590
- rejectAll(reason);
604
+ const error = new KnittingError(code, reason, cause);
605
+ if (stealPool !== undefined && stealPool.stealClaim !== "ticket") {
606
+ // Dekker's shared queue outlives one consumer: the surviving workers
607
+ // keep claiming from it, so only the calls in flight are rejected.
608
+ rejectAll(error);
609
+ }
610
+ else {
611
+ // A lane's own queue has no other consumer, so a call accepted after
612
+ // this point would stay pending forever: close it and reject them all.
613
+ // Under ticket stealing a claimed ticket may be lost at any point
614
+ // between CAS and retirement, so the whole shared registry closes and
615
+ // task-id/slot reuse is prohibited.
616
+ queue.close(error);
617
+ }
591
618
  channelHandler?.close();
592
619
  };
593
620
  const onWorkerMessage = (message) => {
621
+ if (isProcessCompletionDoorbell(message)) {
622
+ processCompletionWake?.();
623
+ return;
624
+ }
594
625
  if (!isWorkerFatalMessage(message))
595
626
  return;
596
- markWorkerClosed(`Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`, true);
627
+ markWorkerClosed("WORKER_STARTUP_FAILED", `Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`);
597
628
  terminateFailedWorker();
598
629
  };
599
630
  const onWorkerError = (error) => {
600
631
  const message = String(error?.message ?? error);
601
- markWorkerClosed(`Worker crashed: ${message}`);
632
+ markWorkerClosed("WORKER_CRASHED", `Worker crashed: ${message}`, error);
602
633
  terminateFailedWorker();
603
634
  };
604
635
  const nodeWorker = worker;
@@ -606,13 +637,16 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
606
637
  nodeWorker.on("message", onWorkerMessage);
607
638
  nodeWorker.on("error", onWorkerError);
608
639
  nodeWorker.on("exit", (code) => {
609
- // Exit is the point at which this endpoint can no longer write WANT, so
610
- // survivors may safely ignore any intent it left behind.
611
640
  deactivateStealConsumer();
641
+ nodeCompletionDoorbell?.close();
612
642
  if (closedReason !== undefined)
613
643
  return;
644
+ if (stopRequested) {
645
+ markWorkerClosed("THREAD_CLOSED", "Thread closed");
646
+ return;
647
+ }
614
648
  const normalized = typeof code === "number" ? code : -1;
615
- markWorkerClosed(`Worker exited with code ${normalized}`);
649
+ markWorkerClosed("WORKER_EXITED", `Worker exited with code ${normalized}`);
616
650
  });
617
651
  }
618
652
  else {
@@ -646,34 +680,35 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
646
680
  return pending;
647
681
  };
648
682
  };
649
- // Ask the worker to leave its loop before termination.
650
683
  const requestWorkerStop = async () => {
684
+ stopRequested = true;
651
685
  const stopView = signalBox.stopView;
652
686
  if (stopView === undefined)
653
- return;
687
+ return true;
654
688
  try {
655
689
  Atomics.store(stopView, 0, WORKER_STOP.requested);
656
690
  laneWake();
657
691
  notifySignal?.();
658
692
  }
659
693
  catch {
660
- return;
694
+ return false;
661
695
  }
662
696
  if (!WORKER_STOP_NEEDS_ACK)
663
- return;
697
+ return true;
664
698
  const deadline = Date.now() + WORKER_STOP_ACK_TIMEOUT_MS;
665
699
  while (Atomics.load(stopView, 0) !== WORKER_STOP.acknowledged) {
666
700
  if (Date.now() >= deadline)
667
- return;
701
+ return false;
668
702
  await new Promise((resolve) => setTimeout(resolve, 1));
669
703
  }
704
+ return true;
670
705
  };
671
706
  const context = {
672
707
  txIdle,
673
708
  call,
674
709
  requestStop: requestWorkerStop,
675
710
  kills: async () => {
676
- markWorkerClosed("Thread closed", true);
711
+ markWorkerClosed("THREAD_CLOSED", "Thread closed");
677
712
  // Process workers must exit before their shared memory is unmapped.
678
713
  const awaitExit = processWorkerMemory !== undefined;
679
714
  const termination = terminateWorkerQuietly(worker, awaitExit);
@@ -687,9 +722,22 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
687
722
  laneWake: sharedChannelHandler !== undefined || stealPool !== undefined
688
723
  ? laneWake
689
724
  : undefined,
725
+ laneAwake: stealPool !== undefined
726
+ ? () => a_load(signalBox.rxStatus, 0) !== 0
727
+ : undefined,
690
728
  bindSend: sharedChannelHandler !== undefined || stealPool !== undefined
691
729
  ? ((fn) => void (dispatchSend = fn))
692
730
  : undefined,
731
+ bindCompletionWake: (canUseProcessCompletionDoorbell ||
732
+ canUseNodeCompletionDoorbell) &&
733
+ stealPool !== undefined
734
+ ? ((fn) => {
735
+ processCompletionWake = fn;
736
+ nodeCompletionWake = fn;
737
+ })
738
+ : undefined,
739
+ processCompletionDoorbell: canUseProcessCompletionDoorbell,
740
+ nodeCompletionDoorbell: canUseNodeCompletionDoorbell,
693
741
  };
694
742
  return context;
695
743
  };
@@ -92,6 +92,15 @@ export declare const createProcessWorkerNativeSignalNotifier: ({ processRuntime,
92
92
  processRuntime: ProcessWorkerRuntime | undefined;
93
93
  signal: Int32Array;
94
94
  }) => (() => void) | undefined;
95
+ /**
96
+ * Whether this host/child pair has the Node-compatible IPC channel that the
97
+ * process completion-doorbell prototype uses. Deno process workers currently
98
+ * boot through environment data and do not have that channel.
99
+ */
100
+ export declare const processWorkerUsesIpc: ({ processRuntime, commandPrefix, }: {
101
+ processRuntime: ProcessWorkerRuntime | undefined;
102
+ commandPrefix?: ProcessWorkerCommandPrefix;
103
+ }) => boolean;
95
104
  export declare const spawnProcessWorker: (options: {
96
105
  workerUrl: string | URL;
97
106
  bootPayload: ProcessWorkerBootPayload;