knitting 0.1.62 → 0.1.70

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 (84) hide show
  1. package/README.md +525 -335
  2. package/knitting.browser.js +1 -1
  3. package/map.md +0 -6
  4. package/package.json +3 -3
  5. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  6. package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
  7. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  8. package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
  9. package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
  11. package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
  12. package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
  13. package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
  14. package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
  15. package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
  16. package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
  17. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  18. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  19. package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
  20. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  21. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  22. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  23. package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
  24. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  25. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  26. package/scripts/build-native-addons.ts +5 -0
  27. package/shared-memory.d.ts +3 -0
  28. package/shared-memory.js +3 -0
  29. package/src/api.js +109 -42
  30. package/src/common/with-resolvers.js +2 -5
  31. package/src/common/worker-runtime.d.ts +7 -0
  32. package/src/common/worker-runtime.js +7 -0
  33. package/src/connections/buffer-reference.d.ts +10 -36
  34. package/src/connections/buffer-reference.js +15 -170
  35. package/src/connections/node-addons.d.ts +1 -1
  36. package/src/connections/shared-array-buffer-payload.d.ts +7 -0
  37. package/src/connections/shared-array-buffer-payload.js +27 -11
  38. package/src/ipc/transport/shared-memory.d.ts +9 -1
  39. package/src/ipc/transport/shared-memory.js +13 -1
  40. package/src/knitting_buffer_pointer.cc +57 -2
  41. package/src/knitting_doorbell.cc +220 -0
  42. package/src/memory/knitting-body.d.ts +44 -0
  43. package/src/memory/knitting-body.js +51 -0
  44. package/src/memory/knitting-buffer-http.d.ts +116 -0
  45. package/src/memory/knitting-buffer-http.js +255 -0
  46. package/src/memory/knitting-buffer.d.ts +250 -0
  47. package/src/memory/knitting-buffer.js +695 -0
  48. package/src/memory/lazy-region-registry.d.ts +83 -0
  49. package/src/memory/lazy-region-registry.js +355 -0
  50. package/src/memory/lock.d.ts +38 -15
  51. package/src/memory/lock.js +227 -109
  52. package/src/memory/payloadCodec.d.ts +18 -2
  53. package/src/memory/payloadCodec.js +309 -65
  54. package/src/memory/regionRegistry.d.ts +6 -0
  55. package/src/memory/regionRegistry.js +125 -240
  56. package/src/memory/shared-buffer-io.d.ts +7 -0
  57. package/src/memory/shared-buffer-io.js +34 -8
  58. package/src/runtime/deno-doorbell.d.ts +26 -0
  59. package/src/runtime/deno-doorbell.js +117 -0
  60. package/src/runtime/dispatcher.d.ts +8 -6
  61. package/src/runtime/dispatcher.js +80 -58
  62. package/src/runtime/host-arg-arena.d.ts +3 -0
  63. package/src/runtime/host-arg-arena.js +16 -0
  64. package/src/runtime/node-doorbell.d.ts +14 -0
  65. package/src/runtime/node-doorbell.js +84 -0
  66. package/src/runtime/pool.d.ts +27 -15
  67. package/src/runtime/pool.js +138 -116
  68. package/src/runtime/process-worker.d.ts +9 -0
  69. package/src/runtime/process-worker.js +22 -2
  70. package/src/runtime/tx-queue.d.ts +2 -5
  71. package/src/runtime/tx-queue.js +52 -48
  72. package/src/runtime/worker-common.d.ts +2 -1
  73. package/src/runtime/worker-common.js +19 -5
  74. package/src/types.d.ts +36 -70
  75. package/src/worker/loop.js +95 -62
  76. package/src/worker/rx-queue.d.ts +2 -3
  77. package/src/worker/rx-queue.js +34 -40
  78. package/src/worker/shared-return.d.ts +9 -0
  79. package/src/worker/shared-return.js +22 -0
  80. package/src/worker/task-loader.js +1 -2
  81. package/src/worker/timers.d.ts +2 -6
  82. package/src/worker/timers.js +14 -19
  83. package/unsafe.d.ts +2 -1
  84. package/unsafe.js +2 -1
@@ -1,27 +1,33 @@
1
1
  import { createHostTxQueue } from "./tx-queue.js";
2
- import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessStealMemoryLayout, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
2
+ import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessStealMemoryLayout, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, processWorkerUsesIpc, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
3
3
  import { serializeWorkerBootstrapData, terminateWorkerQuietly, toWorkerCompatExecArgv, toWorkerSafeExecArgv, } from "./worker-common.js";
4
- import { createSharedMemoryTransport, TRANSPORT_SIGNAL_BYTES, } from "../ipc/transport/shared-memory.js";
4
+ import { createSharedMemoryTransport, TRANSPORT_SIGNAL_BYTES, WORKER_STOP, } from "../ipc/transport/shared-memory.js";
5
5
  import { ChannelHandler, hostDispatcherLoop, } from "./dispatcher.js";
6
+ import { createNodeCompletionDoorbell } from "./node-doorbell.js";
6
7
  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.
8
+ // Registers the payload codec before any lock is built.
9
9
  import "../memory/payloadCodec.js";
10
10
  import "../worker/loop.js";
11
11
  import { createSharedArrayBuffer, createWasmSharedArrayBuffer, IS_BROWSER, RUNTIME, } from "../common/runtime.js";
12
- import { HAS_NODE_WORKER_THREADS, RUNTIME_WORKER, } from "../common/worker-runtime.js";
12
+ import { HAS_NODE_WORKER_THREADS, isProcessCompletionDoorbell, RUNTIME_WORKER, } from "../common/worker-runtime.js";
13
13
  import { probeLockBufferTextCompat } from "../common/shared-buffer-text.js";
14
14
  import { signalAbortFactory } from "../shared/abortSignal.js";
15
15
  import { createLockControlCarpet } from "../memory/byte-carpet.js";
16
16
  import { resolvePayloadBufferOptions, } from "../memory/payload-config.js";
17
- import { createBufferReferenceReturnReleaseMessage, detachArrayBufferBestEffort, } from "../connections/buffer-reference.js";
18
17
  import { spawnCompiledWorkerContext } from "./compiled-worker.js";
19
18
  const WORKER_FATAL_MESSAGE_KEY = "__knittingWorkerFatal";
20
19
  const isWorkerFatalMessage = (value) => !!value &&
21
20
  typeof value === "object" &&
22
21
  typeof value[WORKER_FATAL_MESSAGE_KEY] === "string";
22
+ // Bound the wait for a worker to acknowledge loop exit.
23
+ const WORKER_STOP_ACK_TIMEOUT_MS = 50;
24
+ // Node owns returned BufferReference backing stores in a process-global native
25
+ // registry. Give its worker loop a chance to drain deferred producer pins
26
+ // before the host falls back to terminate().
27
+ const WORKER_STOP_NEEDS_ACK = RUNTIME === "deno" || RUNTIME === "node";
23
28
  // Keep idle workers self-healing if an Atomics.notify wake is missed.
24
29
  const DEFAULT_WORKER_PARK_MS = 1;
30
+ const SINGLE_WORKER_SPIN_US = 50;
25
31
  const DEFAULT_ABORT_SIGNAL_CAPACITY = 258;
26
32
  const sanitizePositiveInteger = (value) => {
27
33
  if (!Number.isFinite(value))
@@ -31,15 +37,20 @@ const sanitizePositiveInteger = (value) => {
31
37
  };
32
38
  const resolveAbortSignalCapacity = (value) => sanitizePositiveInteger(value) ?? DEFAULT_ABORT_SIGNAL_CAPACITY;
33
39
  const abortSignalByteLength = (capacity) => Math.max(1, Math.ceil(capacity / 32)) * Uint32Array.BYTES_PER_ELEMENT;
34
- const withDefaultWorkerTimers = (options) => {
40
+ const withDefaultWorkerTimers = (options, workerCount) => {
35
41
  const parkMs = options?.timers?.parkMs ?? DEFAULT_WORKER_PARK_MS;
42
+ // `??` and not `||`: an explicit `spinMicroseconds: 0` is the whole point of
43
+ // the multi-worker default and must not fall through to it.
44
+ const spinMicroseconds = options?.timers?.spinMicroseconds ??
45
+ (workerCount <= 1 ? SINGLE_WORKER_SPIN_US : 0);
36
46
  if (options === undefined)
37
- return { timers: { parkMs } };
47
+ return { timers: { parkMs, spinMicroseconds } };
38
48
  return {
39
49
  ...options,
40
50
  timers: {
41
51
  ...options.timers,
42
52
  parkMs,
53
+ spinMicroseconds,
43
54
  },
44
55
  };
45
56
  };
@@ -48,16 +59,7 @@ const withFixedPayloadConfig = (config) => ({
48
59
  mode: "fixed",
49
60
  payloadInitialBytes: config.payloadMaxByteLength,
50
61
  });
51
- /**
52
- * Build the buffers, host-side locks and pending registry that a stealing pool
53
- * shares. One submit region for every worker to claim from, one private return
54
- * region per worker, and a single pool-global queue so a response arriving on
55
- * any lane settles the right promise.
56
- *
57
- * Region width follows paper §6.1: the largest power-of-two `g` leaving a spare
58
- * region for a delayed claimant (`slots / g >= consumers + 1`). At 32 slots that
59
- * caps how wide a region can be well before it caps the worker count.
60
- */
62
+ /** Build the shared submit and private return buffers for a stealing pool. */
61
63
  export const resolveStealRegionLanes = (consumers) => {
62
64
  for (let lanes = LockBound.slots; lanes >= 1; lanes >>= 1) {
63
65
  if (LockBound.slots / lanes >= consumers + 1)
@@ -65,9 +67,13 @@ export const resolveStealRegionLanes = (consumers) => {
65
67
  }
66
68
  throw new RangeError(`${consumers} stealing workers need more than ${LockBound.slots} lanes`);
67
69
  };
70
+ /** Return the widest region allowed by the selected claim discipline. */
71
+ export const resolveMaxStealRegionLanes = (consumers, stealClaim) => stealClaim !== undefined && stealClaim !== "dekker"
72
+ ? LockBound.slots
73
+ : resolveStealRegionLanes(consumers);
68
74
  /** Maximum claimants that leave the protocol's required spare region. */
69
75
  export const MAX_STEAL_CONSUMERS = LockBound.slots - 1;
70
- export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, processWorker, }) => {
76
+ export const createStealPoolBuffers = ({ threads, payload, sharedArgs, regionLanes, stealClaim, abortSignalCapacity, usesAbortSignal, processWorker, }) => {
71
77
  const basePayloadConfig = resolvePayloadBufferOptions({ options: payload });
72
78
  const payloadConfig = processWorker === undefined
73
79
  ? basePayloadConfig
@@ -91,9 +97,10 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
91
97
  payload: payloadSab,
92
98
  }),
93
99
  });
94
- const maxLanes = resolveStealRegionLanes(threads);
100
+ // Keep the default width valid for Dekker; wider CAS-mask regions are explicit.
101
+ const maxLanes = resolveMaxStealRegionLanes(threads, stealClaim);
95
102
  const lanes = regionLanes === undefined
96
- ? maxLanes
103
+ ? resolveStealRegionLanes(threads)
97
104
  : Math.min(Math.max(1, regionLanes | 0), maxLanes);
98
105
  const resolvedAbortSignalCapacity = resolveAbortSignalCapacity(abortSignalCapacity);
99
106
  const processMemory = processWorker === undefined ? undefined : createProcessStealMemoryLayout({
@@ -124,7 +131,11 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
124
131
  textCompat: submitBuffers.textCompat,
125
132
  consumers: threads,
126
133
  regionLanes: lanes,
134
+ stealClaim,
127
135
  processBoundary: processMemory !== undefined,
136
+ // Shared arguments are opt-in because the receiving task may outlive the
137
+ // borrowed region.
138
+ sharedReturn: sharedArgs === true,
128
139
  });
129
140
  const returnBuffers = [];
130
141
  const hostReturnLocks = [];
@@ -170,13 +181,14 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
170
181
  hostReturnLocks,
171
182
  sharedQueue,
172
183
  regionLanes: lanes,
184
+ stealClaim,
173
185
  processMemory,
174
186
  abortSignalSAB,
175
187
  abortSignalMax: processMemory?.abortSignalMax ??
176
188
  (abortSignalSAB === undefined ? undefined : resolvedAbortSignalCapacity),
177
189
  };
178
190
  };
179
- export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, stealPool, }) => {
191
+ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }) => {
180
192
  if (workerOptions?.runtime === "compiled") {
181
193
  return spawnCompiledWorkerContext({
182
194
  list,
@@ -189,14 +201,22 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
189
201
  }
190
202
  const tsFileUrl = new URL(import.meta.url);
191
203
  const poliWorker = RUNTIME_WORKER;
192
- const resolvedWorkerOptions = serializeWorkerBootstrapData(withDefaultWorkerTimers(workerOptions));
204
+ const resolvedWorkerOptions = serializeWorkerBootstrapData(withDefaultWorkerTimers(workerOptions, workerCount ?? totalNumberOfThread));
193
205
  const useProcessWorkerRuntime = resolvedWorkerOptions.runtime === "process";
206
+ const canUseDenoCompletionDoorbell = !useProcessWorkerRuntime &&
207
+ denoCompletionDoorbell !== undefined;
194
208
  const processWorkerRuntime = useProcessWorkerRuntime
195
209
  ? readProcessWorkerRuntime(resolvedWorkerOptions)
196
210
  : undefined;
197
211
  const processWorkerCommandPrefix = useProcessWorkerRuntime
198
212
  ? readProcessWorkerCommandPrefix(resolvedWorkerOptions)
199
213
  : undefined;
214
+ const canUseProcessCompletionDoorbell = useProcessWorkerRuntime &&
215
+ host?.doorbell !== false &&
216
+ processWorkerUsesIpc({
217
+ processRuntime: processWorkerRuntime,
218
+ commandPrefix: processWorkerCommandPrefix,
219
+ });
200
220
  const processSharedMemorySettings = useProcessWorkerRuntime
201
221
  ? readProcessSharedMemorySettings(resolvedWorkerOptions)
202
222
  : undefined;
@@ -229,10 +249,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
229
249
  const abortBytes = stealPool === undefined && usesAbortSignal === true
230
250
  ? abortSignalByteLength(resolvedAbortSignalCapacity)
231
251
  : 0;
232
- // A stealing process pool carves every lane out of one mapping, so lane
233
- // `thread` must find its slice there. Falling back to a private layout would
234
- // silently give this lane its own submit region — it would boot and then
235
- // never see a task the host published to the shared one.
252
+ // Stealing process workers use their slice of the pool-wide mapping.
236
253
  const stealProcessMemory = stealPool?.processMemory;
237
254
  const processWorkerMemory = !useProcessWorkerRuntime
238
255
  ? undefined
@@ -255,18 +272,14 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
255
272
  createWasmSharedArrayBuffer;
256
273
  const createPayloadBuffer = processSharedMemory?.createBuffer;
257
274
  const makePayloadBuffer = () => createPayloadBuffer
258
- // ProcessSharedBuffer is fixed-size today, so reserve the configured
259
- // payload ceiling instead of relying on SAB growth.
275
+ // Process-shared buffers are fixed-size.
260
276
  ? createPayloadBuffer(resolvedPayloadConfig.payloadMaxByteLength)
261
277
  : resolvedPayloadConfig.mode === "growable"
262
278
  ? createSharedArrayBuffer(resolvedPayloadConfig.payloadInitialBytes, resolvedPayloadConfig.payloadMaxByteLength)
263
279
  : createSharedArrayBuffer(resolvedPayloadConfig.payloadInitialBytes);
264
280
  const makeLockControlLayout = () => {
265
- // Keep the hottest control words in one compact front strip:
266
- // transport signals -> request lock -> return lock.
267
- // Request/return headers stay in separate contiguous slabs to preserve
268
- // sequential batching locality.
269
- // Abort bitmap stays at the tail.
281
+ // Place hot control words first; keep headers contiguous and the abort bitmap
282
+ // at the tail.
270
283
  return createLockControlCarpet({
271
284
  signalBytes,
272
285
  abortBytes,
@@ -341,68 +354,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
341
354
  processRuntime: processWorkerRuntime,
342
355
  signal: signalBox.opView,
343
356
  });
344
- const outstandingBorrowedReturns = bufferReferenceReturn === "borrow" && typeof WeakRef === "function"
345
- ? new Map()
346
- : undefined;
347
- const releaseBorrowedReturnToken = (token) => {
348
- outstandingBorrowedReturns?.delete(token);
349
- worker?.postMessage?.(createBufferReferenceReturnReleaseMessage(token));
350
- };
351
- const revokeOutstandingBorrowedReturns = (copyBytes) => {
352
- if (outstandingBorrowedReturns === undefined)
353
- return;
354
- for (const [token, record] of outstandingBorrowedReturns) {
355
- try {
356
- const ref = record.ref.deref();
357
- if (ref !== undefined) {
358
- ref.revokeBorrow(copyBytes);
359
- continue;
360
- }
361
- const aliasBuffer = record.aliasBuffer?.deref();
362
- if (aliasBuffer !== undefined &&
363
- detachArrayBufferBestEffort(record.runtime, aliasBuffer) &&
364
- copyBytes) {
365
- releaseBorrowedReturnToken(token);
366
- }
367
- }
368
- catch {
369
- // best effort
370
- }
371
- }
372
- outstandingBorrowedReturns.clear();
373
- };
374
- const returnHooks = bufferReferenceReturn === "borrow"
375
- ? {
376
- release: releaseBorrowedReturnToken,
377
- track: (ref, token, aliasBuffer) => {
378
- if (outstandingBorrowedReturns === undefined)
379
- return;
380
- const record = outstandingBorrowedReturns.get(token);
381
- if (record !== undefined) {
382
- if (aliasBuffer !== undefined) {
383
- record.aliasBuffer = new WeakRef(aliasBuffer);
384
- }
385
- return;
386
- }
387
- outstandingBorrowedReturns.set(token, {
388
- ref: new WeakRef(ref),
389
- aliasBuffer: aliasBuffer === undefined
390
- ? undefined
391
- : new WeakRef(aliasBuffer),
392
- runtime: ref.runtime,
393
- });
394
- },
395
- }
396
- : undefined;
397
357
  const queue = stealPool?.sharedQueue ?? createHostTxQueue({
398
358
  lock,
399
359
  returnLock,
400
360
  abortSignals,
401
- releaseBufferReferenceReturn: returnHooks,
402
361
  });
403
- if (stealPool !== undefined) {
404
- stealPool.sharedQueue.setReturnHooks(stealPool.consumerId, returnHooks);
405
- }
406
362
  const { enqueue, rejectAll, txIdle, } = queue;
407
363
  const thisSignal = signalBox.opView;
408
364
  const a_add = Atomics.add;
@@ -411,8 +367,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
411
367
  const canNotifySignal = thisSignal.buffer instanceof SharedArrayBuffer;
412
368
  const notifySignal = nativeNotifySignal ??
413
369
  (canNotifySignal ? (() => a_notify(thisSignal, 0, 1)) : undefined);
414
- // Wakes this lane's worker when it is parked (rxStatus 0). Used by send()
415
- // (new work just arrived) — the dispatcher drain wakes it the same way.
370
+ // Wake this lane when its worker is parked.
416
371
  const laneWake = () => {
417
372
  if (a_load(signalBox.rxStatus, 0) === 0) {
418
373
  a_add(thisSignal, 0, 1);
@@ -425,10 +380,16 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
425
380
  const ownsChannel = sharedChannelHandler === undefined &&
426
381
  stealPool === undefined;
427
382
  const ownChannel = sharedChannelHandler ?? new ChannelHandler();
428
- // Under stealing there is one queue, so one dispatcher drives it from the
429
- // pool; a per-lane dispatcher would double-drain the shared registry.
430
- const { check: dispatcherCheck } = stealPool !== undefined
431
- ? { check: undefined }
383
+ // Create the Node doorbell before bootstrapping the worker.
384
+ let nodeCompletionWake;
385
+ const nodeCompletionDoorbell = !useProcessWorkerRuntime &&
386
+ host?.doorbell !== false && host?.nativeDoorbell === true
387
+ ? createNodeCompletionDoorbell(() => nodeCompletionWake?.())
388
+ : undefined;
389
+ const canUseNodeCompletionDoorbell = nodeCompletionDoorbell !== undefined;
390
+ // A stealing pool has one dispatcher for its shared queue.
391
+ const { check: dispatcherCheck, wakeCompletion: directCompletionWake, } = stealPool !== undefined
392
+ ? { check: undefined, wakeCompletion: undefined }
432
393
  : hostDispatcherLoop({
433
394
  signalBox,
434
395
  queue,
@@ -436,7 +397,15 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
436
397
  dispatcherOptions: host,
437
398
  notifySignal: nativeNotifySignal,
438
399
  crossProcess: useProcessWorkerRuntime,
400
+ nativeCompletionDoorbell: canUseDenoCompletionDoorbell ||
401
+ canUseNodeCompletionDoorbell,
402
+ processCompletionDoorbell: canUseProcessCompletionDoorbell,
439
403
  });
404
+ if (canUseDenoCompletionDoorbell && dispatcherCheck !== undefined) {
405
+ denoCompletionDoorbell.listen(thread, directCompletionWake);
406
+ }
407
+ nodeCompletionWake = directCompletionWake;
408
+ let processCompletionWake = directCompletionWake;
440
409
  if (ownsChannel && dispatcherCheck !== undefined) {
441
410
  ownChannel.open(dispatcherCheck);
442
411
  channelHandler = ownChannel;
@@ -444,7 +413,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
444
413
  if (dispatcherCheck.isRunning === true)
445
414
  return;
446
415
  dispatcherCheck.isRunning = true;
447
- Promise.resolve().then(dispatcherCheck);
416
+ queueMicrotask(dispatcherCheck);
448
417
  laneWake();
449
418
  };
450
419
  }
@@ -475,18 +444,27 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
475
444
  lock: lockBuffers,
476
445
  returnLock: returnLockBuffers,
477
446
  payloadConfig: resolvedPayloadConfig,
478
- bufferReferenceReturn,
447
+ sharedReturn: sharedBytesEnabled === true,
479
448
  permission,
480
- // Mirrors the dispatcher's `canUseDoorbell`: a cross-process worker cannot
481
- // ring the host's waiter, so do not make it pay the atomic op.
482
- notifyOnHostPublish: !useProcessWorkerRuntime &&
483
- host?.doorbell !== false &&
484
- (RUNTIME === "bun" || RUNTIME === "node") &&
485
- typeof Atomics.waitAsync === "function",
449
+ // Select the completion doorbell supported by the worker runtime.
450
+ notifyOnHostPublish: host?.doorbell !== false &&
451
+ (canUseProcessCompletionDoorbell ||
452
+ canUseNodeCompletionDoorbell ||
453
+ (!useProcessWorkerRuntime &&
454
+ (RUNTIME === "bun" || RUNTIME === "node") &&
455
+ typeof Atomics.waitAsync === "function")),
456
+ processCompletionDoorbell: canUseProcessCompletionDoorbell,
457
+ denoCompletionDoorbell: canUseDenoCompletionDoorbell
458
+ ? denoCompletionDoorbell.pointer
459
+ : undefined,
460
+ nodeCompletionDoorbell: canUseNodeCompletionDoorbell
461
+ ? nodeCompletionDoorbell.pointer
462
+ : undefined,
486
463
  steal: stealPool === undefined ? undefined : {
487
464
  consumers: stealPool.consumers,
488
465
  consumerId: stealPool.consumerId,
489
466
  regionLanes: stealPool.regionLanes,
467
+ claim: stealPool.stealClaim,
490
468
  },
491
469
  };
492
470
  const baseWorkerOptions = {
@@ -556,6 +534,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
556
534
  worker.postMessage?.(workerDataPayload);
557
535
  }
558
536
  let closedReason;
537
+ // A worker that exits because the host asked it to stop is a closed thread,
538
+ // not a crash. The stop is acknowledged in shared memory before the worker
539
+ // unwinds, so on a slow host the `exit` event can land ahead of `kills()`
540
+ // and label an orderly shutdown "Worker exited with code 0".
541
+ let stopRequested = false;
559
542
  const deactivateStealConsumer = () => {
560
543
  stealPool?.hostSubmitLock.deactivateStealConsumer(stealPool.consumerId);
561
544
  };
@@ -570,21 +553,21 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
570
553
  deactivateStealConsumer();
571
554
  }
572
555
  };
573
- // `workerBytesReadable`: while the worker is still alive its pinned bytes
574
- // can be copied out; after a crash/exit they are gone and borrows are
575
- // detached so stale reads fail loud instead of aliasing freed memory.
576
- const markWorkerClosed = (reason, workerBytesReadable = false) => {
556
+ const markWorkerClosed = (reason) => {
577
557
  if (closedReason)
578
558
  return;
579
559
  closedReason = reason;
580
- revokeOutstandingBorrowedReturns(workerBytesReadable);
581
560
  rejectAll(reason);
582
561
  channelHandler?.close();
583
562
  };
584
563
  const onWorkerMessage = (message) => {
564
+ if (isProcessCompletionDoorbell(message)) {
565
+ processCompletionWake?.();
566
+ return;
567
+ }
585
568
  if (!isWorkerFatalMessage(message))
586
569
  return;
587
- markWorkerClosed(`Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`, true);
570
+ markWorkerClosed(`Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`);
588
571
  terminateFailedWorker();
589
572
  };
590
573
  const onWorkerError = (error) => {
@@ -597,11 +580,14 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
597
580
  nodeWorker.on("message", onWorkerMessage);
598
581
  nodeWorker.on("error", onWorkerError);
599
582
  nodeWorker.on("exit", (code) => {
600
- // Exit is the point at which this endpoint can no longer write WANT, so
601
- // survivors may safely ignore any intent it left behind.
602
583
  deactivateStealConsumer();
584
+ nodeCompletionDoorbell?.close();
603
585
  if (closedReason !== undefined)
604
586
  return;
587
+ if (stopRequested) {
588
+ markWorkerClosed("Thread closed");
589
+ return;
590
+ }
605
591
  const normalized = typeof code === "number" ? code : -1;
606
592
  markWorkerClosed(`Worker exited with code ${normalized}`);
607
593
  });
@@ -637,13 +623,39 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
637
623
  return pending;
638
624
  };
639
625
  };
626
+ const requestWorkerStop = async () => {
627
+ stopRequested = true;
628
+ const stopView = signalBox.stopView;
629
+ if (stopView === undefined)
630
+ return true;
631
+ try {
632
+ Atomics.store(stopView, 0, WORKER_STOP.requested);
633
+ laneWake();
634
+ notifySignal?.();
635
+ }
636
+ catch {
637
+ return false;
638
+ }
639
+ if (!WORKER_STOP_NEEDS_ACK)
640
+ return true;
641
+ const deadline = Date.now() + WORKER_STOP_ACK_TIMEOUT_MS;
642
+ while (Atomics.load(stopView, 0) !== WORKER_STOP.acknowledged) {
643
+ if (Date.now() >= deadline)
644
+ return false;
645
+ await new Promise((resolve) => setTimeout(resolve, 1));
646
+ }
647
+ return true;
648
+ };
640
649
  const context = {
641
650
  txIdle,
642
651
  call,
652
+ requestStop: requestWorkerStop,
643
653
  kills: async () => {
644
- markWorkerClosed("Thread closed", true);
645
- const termination = terminateWorkerQuietly(worker);
646
- if (processWorkerMemory !== undefined)
654
+ markWorkerClosed("Thread closed");
655
+ // Process workers must exit before their shared memory is unmapped.
656
+ const awaitExit = processWorkerMemory !== undefined;
657
+ const termination = terminateWorkerQuietly(worker, awaitExit);
658
+ if (awaitExit)
647
659
  await termination;
648
660
  cleanupProcessWorkerMemoryQuietly(processWorkerMemory);
649
661
  },
@@ -656,6 +668,16 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
656
668
  bindSend: sharedChannelHandler !== undefined || stealPool !== undefined
657
669
  ? ((fn) => void (dispatchSend = fn))
658
670
  : undefined,
671
+ bindCompletionWake: (canUseProcessCompletionDoorbell ||
672
+ canUseNodeCompletionDoorbell) &&
673
+ stealPool !== undefined
674
+ ? ((fn) => {
675
+ processCompletionWake = fn;
676
+ nodeCompletionWake = fn;
677
+ })
678
+ : undefined,
679
+ processCompletionDoorbell: canUseProcessCompletionDoorbell,
680
+ nodeCompletionDoorbell: canUseNodeCompletionDoorbell,
659
681
  };
660
682
  return context;
661
683
  };
@@ -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;
@@ -109,6 +109,13 @@ const isWindowsRuntimeHost = () => {
109
109
  return denoOs === "windows";
110
110
  return globalThis.process?.platform === "win32";
111
111
  };
112
+ const processWorkerNeedsNamedMemory = (sharedMemory) =>
113
+ // `Deno.Command` does not reliably carry an anonymous memfd/shm fd into a
114
+ // child as stdin: Linux can hand the child a non-mappable descriptor and
115
+ // macOS cannot always reopen the host fd through `/dev/fd`. Named mappings
116
+ // are reopened by the worker instead, which works across Deno's supported
117
+ // hosts and process-worker runtimes.
118
+ sharedMemory.mode === "named" || isWindowsRuntimeHost() || RUNTIME === "deno";
112
119
  let processWorkerMemoryNameCounter = 0;
113
120
  const makeProcessWorkerMemoryName = (thread, prefix = "kpw") => {
114
121
  const processId = globalThis.process?.pid ??
@@ -142,7 +149,7 @@ export const createProcessWorkerMemoryLayout = ({ signalBytes, abortBytes, paylo
142
149
  const requestPayloadSlice = carpet.take("requestPayload", payloadBytes);
143
150
  const returnPayloadSlice = carpet.take("returnPayload", payloadBytes);
144
151
  const primitives = getProcessWorkerSharedMemoryPrimitives();
145
- const forceNamed = sharedMemory.mode === "named" || isWindowsRuntimeHost();
152
+ const forceNamed = processWorkerNeedsNamedMemory(sharedMemory);
146
153
  const mapping = primitives.createSharedMemory(forceNamed
147
154
  ? {
148
155
  size: carpet.byteLength(),
@@ -215,7 +222,7 @@ export const createProcessStealMemoryLayout = ({ threads, signalBytes, abortByte
215
222
  returnPayload: carpet.take(`returnPayload-${thread}`, payloadBytes),
216
223
  }));
217
224
  const primitives = getProcessWorkerSharedMemoryPrimitives();
218
- const forceNamed = sharedMemory.mode === "named" || isWindowsRuntimeHost();
225
+ const forceNamed = processWorkerNeedsNamedMemory(sharedMemory);
219
226
  const mapping = primitives.createSharedMemory(forceNamed
220
227
  ? {
221
228
  size: carpet.byteLength(),
@@ -546,6 +553,19 @@ export const createProcessWorkerNativeSignalNotifier = ({ processRuntime, signal
546
553
  return undefined;
547
554
  }
548
555
  };
556
+ /**
557
+ * Whether this host/child pair has the Node-compatible IPC channel that the
558
+ * process completion-doorbell prototype uses. Deno process workers currently
559
+ * boot through environment data and do not have that channel.
560
+ */
561
+ export const processWorkerUsesIpc = ({ processRuntime, commandPrefix, }) => {
562
+ if (processRuntime === undefined || commandPrefix !== undefined)
563
+ return false;
564
+ if (RUNTIME === "node") {
565
+ return processRuntime === "node" || processRuntime === "bun";
566
+ }
567
+ return RUNTIME === "bun" && processRuntime === "bun";
568
+ };
549
569
  const createProcessWorkerEventHub = () => {
550
570
  const messageHandlers = [];
551
571
  const errorHandlers = [];
@@ -2,7 +2,6 @@ import "../memory/payloadCodec.js";
2
2
  import { type Lock2, type Task } from "../memory/lock.js";
3
3
  import type { AbortSignalOption, TaskTimeout } from "../types.js";
4
4
  import { type SignalAbortStore } from "../shared/abortSignal.js";
5
- import { type BufferReferenceReturnHooks } from "../connections/buffer-reference.js";
6
5
  type RawArguments = unknown;
7
6
  type FunctionID = number;
8
7
  type QueueTask = Task;
@@ -23,19 +22,17 @@ type CreateHostTxQueueArgs = {
23
22
  * When omitted, only `returnLock` is drained (the classic one-lane shape).
24
23
  */
25
24
  extraReturnLocks?: readonly Lock2[];
26
- releaseBufferReferenceReturn?: ((token: bigint) => void) | BufferReferenceReturnHooks;
27
25
  abortSignals?: Pick<SignalAbortStore, "getSignal" | "setSignal" | "resetSignal" | "closeNow">;
28
26
  now?: () => number;
29
27
  };
30
- type ReturnHooks = ((token: bigint) => void) | BufferReferenceReturnHooks;
31
- export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, releaseBufferReferenceReturn, abortSignals, now, }: CreateHostTxQueueArgs): {
28
+ export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abortSignals, now, }: CreateHostTxQueueArgs): {
32
29
  rejectAll: (reason: string) => void;
33
30
  hasPendingFrames: () => boolean;
34
31
  txIdle: () => boolean;
35
32
  completeFrame: () => number;
36
33
  waitForCompletion: (onWake: () => void, timeoutMs?: number) => boolean;
34
+ armCompletionNotifier: () => boolean;
37
35
  setCompletionWaiterArmed: (armed: boolean) => void;
38
- setReturnHooks: (lane: number, hooks: ReturnHooks | undefined) => void;
39
36
  enqueue: (functionID: FunctionID, timeout?: TaskTimeout, abortSignal?: AbortSignalOption) => (rawArgs: RawArguments) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
40
37
  flushToWorker: () => boolean;
41
38
  enqueueKnown: (task: QueueTask) => boolean;