knitting 0.1.63 → 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 (80) 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 +105 -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/knitting_buffer_pointer.cc +57 -2
  39. package/src/knitting_doorbell.cc +220 -0
  40. package/src/memory/knitting-body.d.ts +44 -0
  41. package/src/memory/knitting-body.js +51 -0
  42. package/src/memory/knitting-buffer-http.d.ts +116 -0
  43. package/src/memory/knitting-buffer-http.js +255 -0
  44. package/src/memory/knitting-buffer.d.ts +250 -0
  45. package/src/memory/knitting-buffer.js +695 -0
  46. package/src/memory/lazy-region-registry.d.ts +83 -0
  47. package/src/memory/lazy-region-registry.js +355 -0
  48. package/src/memory/lock.d.ts +38 -15
  49. package/src/memory/lock.js +205 -79
  50. package/src/memory/payloadCodec.d.ts +18 -2
  51. package/src/memory/payloadCodec.js +309 -65
  52. package/src/memory/regionRegistry.d.ts +6 -0
  53. package/src/memory/regionRegistry.js +125 -240
  54. package/src/memory/shared-buffer-io.d.ts +7 -0
  55. package/src/memory/shared-buffer-io.js +34 -8
  56. package/src/runtime/deno-doorbell.d.ts +26 -0
  57. package/src/runtime/deno-doorbell.js +117 -0
  58. package/src/runtime/dispatcher.d.ts +8 -6
  59. package/src/runtime/dispatcher.js +80 -58
  60. package/src/runtime/host-arg-arena.d.ts +3 -0
  61. package/src/runtime/host-arg-arena.js +16 -0
  62. package/src/runtime/node-doorbell.d.ts +14 -0
  63. package/src/runtime/node-doorbell.js +84 -0
  64. package/src/runtime/pool.d.ts +21 -15
  65. package/src/runtime/pool.js +104 -116
  66. package/src/runtime/process-worker.d.ts +9 -0
  67. package/src/runtime/process-worker.js +22 -2
  68. package/src/runtime/tx-queue.d.ts +2 -5
  69. package/src/runtime/tx-queue.js +52 -48
  70. package/src/types.d.ts +35 -71
  71. package/src/worker/loop.js +79 -57
  72. package/src/worker/rx-queue.d.ts +2 -3
  73. package/src/worker/rx-queue.js +34 -40
  74. package/src/worker/shared-return.d.ts +9 -0
  75. package/src/worker/shared-return.js +22 -0
  76. package/src/worker/task-loader.js +1 -2
  77. package/src/worker/timers.d.ts +2 -6
  78. package/src/worker/timers.js +14 -19
  79. package/unsafe.d.ts +2 -1
  80. package/unsafe.js +2 -1
@@ -4,7 +4,6 @@ import "../memory/payloadCodec.js";
4
4
  import { makeTask, resetTaskLocalFlags, runTaskFinalizers, TaskIndex, } from "../memory/lock.js";
5
5
  import { withResolvers } from "../common/with-resolvers.js";
6
6
  import { AbortSignalPoolExhausted, OneShotDeferred, } from "../shared/abortSignal.js";
7
- import { withBufferReferenceReturnReleaser, } from "../connections/buffer-reference.js";
8
7
  const SLOT_INDEX_MASK = 31;
9
8
  const SLOT_META_MASK = 0x07ffffff;
10
9
  const SLOT_META_SHIFT = 5;
@@ -14,7 +13,7 @@ const FUNCTION_META_SHIFT = 16;
14
13
  const ABORT_SIGNAL_META_OFFSET = 1;
15
14
  const NO_ABORT_SIGNAL = -1;
16
15
  const p_now = performance.now.bind(performance);
17
- export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, releaseBufferReferenceReturn, abortSignals, now, }) {
16
+ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abortSignals, now, }) {
18
17
  const PLACE_HOLDER = (_) => {
19
18
  throw ("UNREACHABLE FROM PLACE HOLDER (main)");
20
19
  };
@@ -66,33 +65,18 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
66
65
  ].map((each) => typeof each.setHostWaiterArmed === "function"
67
66
  ? each.setHostWaiterArmed
68
67
  : (_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.
68
+ const returnNativeArmers = [
69
+ returnLock,
70
+ ...(extraReturnLocks ?? []),
71
+ ].map((each) => typeof each.armHostNotifier === "function"
72
+ ? each.armHostNotifier
73
+ : () => false);
88
74
  const completeFrame = returnResolvers.length === 1
89
- ? releaseBufferReferenceReturn === undefined
90
- ? returnResolvers[0]
91
- : () => withBufferReferenceReturnReleaser(releaseBufferReferenceReturn, returnResolvers[0])
75
+ ? returnResolvers[0]
92
76
  : () => {
93
77
  let resolved = 0 | 0;
94
78
  for (let i = 0; i < returnResolvers.length; i++) {
95
- resolved = (resolved + resolveReturnAt(i)) | 0;
79
+ resolved = (resolved + returnResolvers[i]()) | 0;
96
80
  }
97
81
  return resolved;
98
82
  };
@@ -103,19 +87,35 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
103
87
  const completionArmed = new Uint8Array(returnWaiters.length);
104
88
  let completionWake;
105
89
  let completionGeneration = 0 | 0;
90
+ const completionGenerations = new Int32Array(returnWaiters.length);
91
+ // Reuse one callback per lane and capture the generation before re-arming.
92
+ const completionCallbacks = returnWaiters.map((_, index) => () => {
93
+ if (completionArmed[index] === 0)
94
+ return;
95
+ const generation = completionGenerations[index];
96
+ completionArmed[index] = 0;
97
+ returnArmers[index](false);
98
+ if (generation !== completionGeneration)
99
+ return;
100
+ completionWake?.();
101
+ });
106
102
  const setCompletionWaiterArmed = (armed) => {
107
103
  for (const setArmed of returnArmers)
108
104
  setArmed(armed);
109
105
  };
110
106
  const waitForCompletion = (onWake, timeoutMs) => {
111
107
  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
108
  let supported = true;
116
109
  for (let index = 0; index < returnWaiters.length; index++) {
117
- if (completionArmed[index] !== 0)
110
+ if (completionArmed[index] !== 0) {
111
+ // Re-arm persistent waiters with an atomic check; a result may have
112
+ // arrived while the gate was off, and setting ARMED alone can strand it.
113
+ if (!returnNativeArmers[index]()) {
114
+ onWake();
115
+ return true;
116
+ }
118
117
  continue;
118
+ }
119
119
  let wait;
120
120
  try {
121
121
  wait = returnWaiters[index](timeoutMs);
@@ -128,20 +128,14 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
128
128
  break;
129
129
  }
130
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)
131
+ completionGenerations[index] = completionGeneration;
132
+ const wakeLane = completionCallbacks[index];
133
+ if (!wait.async) {
142
134
  wakeLane();
143
- else
144
- Promise.resolve(wait.value).then(wakeLane, wakeLane);
135
+ // A synchronous wake may disarm the whole queue, so stop here.
136
+ return true;
137
+ }
138
+ Promise.resolve(wait.value).then(wakeLane, wakeLane);
145
139
  }
146
140
  if (!supported) {
147
141
  completionWake = undefined;
@@ -150,6 +144,21 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
150
144
  }
151
145
  return supported;
152
146
  };
147
+ /**
148
+ * Native callbacks (Deno's threadSafe UnsafeCallback) cannot use waitAsync,
149
+ * but they use the same shared arm word. Each lane is armed and checked for
150
+ * a publication in one operation; a false return means the dispatcher must
151
+ * drain again rather than sleep waiting for a ring that already happened.
152
+ */
153
+ const armCompletionNotifier = () => {
154
+ for (const arm of returnNativeArmers) {
155
+ if (arm())
156
+ continue;
157
+ setCompletionWaiterArmed(false);
158
+ return false;
159
+ }
160
+ return true;
161
+ };
153
162
  const hasActiveTasks = () => {
154
163
  const count = (inUsed - getPendingPromiseCount()) | 0;
155
164
  return count > 0;
@@ -183,13 +192,8 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
183
192
  txIdle,
184
193
  completeFrame,
185
194
  waitForCompletion,
195
+ armCompletionNotifier,
186
196
  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
- },
193
197
  enqueue: (functionID, timeout, abortSignal) => {
194
198
  const HAS_TIMER = timeout !== undefined;
195
199
  const functionIDMasked = functionID & FUNCTION_ID_MASK;
package/src/types.d.ts CHANGED
@@ -14,7 +14,7 @@ interface WorkerContext {
14
14
  txIdle(): boolean;
15
15
  call(descriptor: WorkerCall): WorkerInvoke;
16
16
  /** Ask the worker to leave its dispatch loop before termination. */
17
- requestStop?(): Promise<void>;
17
+ requestStop?(): Promise<boolean>;
18
18
  kills(): Promise<void>;
19
19
  }
20
20
  type CreateContext = WorkerContext;
@@ -34,30 +34,31 @@ type WorkerData = {
34
34
  lock: LockBuffers;
35
35
  returnLock: LockBuffers;
36
36
  payloadConfig?: PayloadBufferOptions;
37
- bufferReferenceReturn?: "copy" | "borrow";
37
+ /** Enable borrowed returns for this worker's return lane. */
38
+ sharedReturn?: boolean;
38
39
  permission?: ResolvedPermissionProtocol;
39
40
  /** Whether this host can arm an async completion waiter on the return lock. */
40
41
  notifyOnHostPublish?: boolean;
41
- /**
42
- * Work stealing. When present, `lock` is a submit region shared by every
43
- * worker and this worker claims from it as consumer `consumerId` of
44
- * `consumers`, rather than owning a private request lane. `returnLock` stays
45
- * private — the endpoint that claims a task owns its response.
46
- */
42
+ /** Process-local IPC channel can carry coalesced completion doorbells. */
43
+ processCompletionDoorbell?: boolean;
44
+ /** Process-local Deno callback pointer for waking the host completion pump. */
45
+ denoCompletionDoorbell?: bigint;
46
+ /** Process-local Node uv_async handle for waking the host completion pump. */
47
+ nodeCompletionDoorbell?: bigint;
48
+ /** Shared submit lock and private return lock for a stealing worker. */
47
49
  steal?: {
48
50
  consumers: number;
49
51
  consumerId: number;
50
52
  regionLanes: number;
53
+ /** Region mutual-exclusion discipline; see `DispatcherSettings.stealClaim`. */
54
+ claim?: "dekker" | "cas-mask";
51
55
  };
52
56
  };
53
57
  type UnsafeOptions = {
54
- /**
55
- * Experimental `BufferReference` return lifetime.
56
- *
57
- * `"copy"` is safe after worker release. `"borrow"` skips the Deno/Bun copy,
58
- * but must be released before producer shutdown and must not outlive its ref.
59
- */
60
- BufferReferenceReturn?: "copy" | "borrow";
58
+ /** Enable borrowed large returns and the zero-copy `sharedBytes()` path. */
59
+ SharedBytes?: boolean;
60
+ /** Enable borrowed large arguments and `pool.sharedArgBytes()`. */
61
+ SharedArgs?: boolean;
61
62
  };
62
63
  type LockBuffers = {
63
64
  headers: SharedBufferSource;
@@ -183,6 +184,11 @@ type Pool<T extends Record<string, TaskLike<any> | TaskFunctionLike>> = {
183
184
  * Thrown errors/rejections reject here as Error objects with cause chains.
184
185
  */
185
186
  call: FunctionMapType<T>;
187
+ /**
188
+ * Allocate an argument buffer. Borrowed buffers are recycled after 32 later
189
+ * large arguments, so the receiving task must consume them before awaiting.
190
+ */
191
+ sharedArgBytes: (byteLength: number) => Uint8Array;
186
192
  };
187
193
  type ReturnFixed<A extends TaskInput = undefined, B extends Args = undefined, AS extends AbortSignalOption = undefined> = FixPoint<A, B, AS> & SecondPart & {
188
194
  createPool: (options?: CreatePool) => SingleTaskPool<A, B, AS>;
@@ -309,10 +315,10 @@ type WorkerSettings = {
309
315
  /**
310
316
  * How process workers discover their shared-memory control channel.
311
317
  *
312
- * "inherit" keeps the POSIX fd-inheritance path and is the default outside
313
- * Windows. "named" creates an OS-named shared-memory object that wrappers
314
- * such as containers can reopen by name when they share the same IPC
315
- * namespace.
318
+ * "inherit" keeps the POSIX fd-inheritance path. "named" creates an
319
+ * OS-named shared-memory object that wrappers such as containers can reopen
320
+ * by name when they share the same IPC namespace. Deno-hosted pools and
321
+ * Windows use a named mapping automatically.
316
322
  */
317
323
  processSharedMemory?: ProcessSharedMemoryMode | ProcessSharedMemorySettings;
318
324
  timers?: WorkerTimers;
@@ -344,47 +350,19 @@ type WorkerTimers = {
344
350
  pauseNanoseconds?: number;
345
351
  };
346
352
  type DispatcherSettings = {
347
- /**
348
- * How many immediate notify loops before the dispatcher stops re-arming the
349
- * pump for free.
350
- *
351
- * The default depends on what the dispatcher escalates *to*. Polling
352
- * escalates to a `setTimeout` ladder costing ~1.1ms even at delay 0, so it
353
- * defaults to 128 — escalating is expensive and the wide window also batches
354
- * completions. A doorbell escalates to `Atomics.waitAsync` at roughly the
355
- * price of one hop, so pools that have one default to 1. Pools without a
356
- * doorbell — Deno, or `doorbell: false` — keep 128.
357
- *
358
- * Setting this explicitly opts out of that coupling for every pool shape.
359
- */
353
+ /** Number of immediate notify loops before backoff starts. */
360
354
  stallFreeLoops?: number;
361
355
  /**
362
356
  * Max backoff delay (milliseconds).
363
357
  */
364
358
  maxBackoffMs?: number;
365
359
  /**
366
- * Replace idle completion polling with an `Atomics.waitAsync` doorbell when
367
- * the host runtime supports it.
368
- *
369
- * Defaults to enabled on Node and Bun at any worker count, where it costs
370
- * 1.6-3.9x less host CPU per completed call. Under HTTP load, where the host
371
- * has real work of its own, that converts to +13% to +36% throughput.
372
- *
373
- * Turn it off for a pool that oversubscribes its machine. The doorbell only
374
- * progresses when the host gets scheduled, so once workers occupy every core
375
- * a wake must preempt one: measured +5% to +12.6% rps while workers+host fit
376
- * within the cores, -22% to -32% once they do not. That is not gated
377
- * automatically because the core count cannot be probed portably.
378
- *
379
- * Forced off, overriding an explicit `true`, where a doorbell cannot work:
380
- * on Deno, whose `waitAsync` does not wake an idle event loop, and for
381
- * process workers, which live in another process and so cannot ring a host
382
- * waiter at all — V8 keeps its Atomics waiter list per isolate, which is why
383
- * they wake through a native futex addon instead. Compiled (Porffor) workers
384
- * never reach this path: they reject `host` outright and use pipes rather
385
- * than shared memory.
360
+ * Use a completion doorbell when supported. Deno uses a thread-safe FFI
361
+ * callback; process and compiled workers use their own completion transport.
386
362
  */
387
363
  doorbell?: boolean;
364
+ /** Use Node's native `uv_async_t` completion bridge when available. */
365
+ nativeDoorbell?: boolean;
388
366
  /**
389
367
  * Host dispatcher topology.
390
368
  * - `"per-thread"`: each worker owns its dispatcher and macro channel.
@@ -398,29 +376,15 @@ type DispatcherSettings = {
398
376
  * `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
399
377
  */
400
378
  dispatcher?: "per-thread" | "serial-channel";
401
- /**
402
- * Work stealing: one shared submit region that any worker may
403
- * claim from, private return lanes, and a pool-global pending registry. The
404
- * endpoint that claims a task owns its response.
405
- *
406
- * Enabled by default for compatible multi-worker thread and process pools
407
- * unless a balancer or private-lane dispatcher was explicitly selected.
408
- * One-worker pools, inliners, compiled/Porffor workers, and pools above the
409
- * current 31-claimant protocol limit retain their existing transport. Set
410
- * `false` (or `KNITTING_STEAL=0`) to opt out; `KNITTING_STEAL=1` explicitly
411
- * opts in and overrides a balancer/dispatcher selection.
412
- */
379
+ /** Use a shared submit region with private return lanes. */
413
380
  steal?: boolean;
414
381
  /**
415
- * Lanes claimed per stealing handshake (a power of two, `slots / g >=
416
- * workers + 1`). Defaults to the widest region the lane budget allows, which
417
- * amortises arbitration best for cheap tasks.
418
- *
419
- * **A region is a batch.** For expensive tasks, a wide region lets one worker
420
- * claim work the others could have run in parallel; set this to `1` (or a
421
- * small value) when per-task cost dominates arbitration cost.
382
+ * Number of lanes claimed per handshake. Use smaller regions for expensive
383
+ * tasks; Dekker requires at least one spare region per live consumer.
422
384
  */
423
385
  stealRegionLanes?: number;
386
+ /** Region-claim discipline: per-consumer Dekker intents or a shared CAS mask. */
387
+ stealClaim?: "dekker" | "cas-mask";
424
388
  };
425
389
  type CreatePool = {
426
390
  /** Number of workers. Default: 1. */
@@ -1,21 +1,22 @@
1
- import { addRuntimeDataListener, createRuntimeMessageChannel, RUNTIME_IS_MAIN_THREAD, RUNTIME_IS_PROCESS_WORKER, RUNTIME_PARENT_PORT, RUNTIME_WORKER_DATA, } from "../common/worker-runtime.js";
1
+ import { createRuntimeMessageChannel, PROCESS_COMPLETION_DOORBELL, RUNTIME_IS_MAIN_THREAD, RUNTIME_IS_PROCESS_WORKER, RUNTIME_PARENT_PORT, RUNTIME_WORKER_DATA, } from "../common/worker-runtime.js";
2
2
  import { isSharedBufferSource } from "../common/shared-buffer-region.js";
3
3
  import { isLockBufferTextCompat } from "../common/shared-buffer-text.js";
4
+ import { createDenoCompletionNotifier } from "../runtime/deno-doorbell.js";
5
+ import { createNodeCompletionNotifier } from "../runtime/node-doorbell.js";
4
6
  import { createWorkerRxQueue } from "./rx-queue.js";
7
+ import { installSharedReturn } from "./shared-return.js";
5
8
  import { createSharedMemoryTransport, WORKER_STOP, } from "../ipc/transport/shared-memory.js";
6
9
  import { lock2 } 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.
10
+ // Registers the payload codec before any lock is built.
9
11
  import "../memory/payloadCodec.js";
10
12
  import { getFunctions } from "./task-loader.js";
11
- import { pauseGeneric, sleepUntilChanged, whilePausing } from "./timers.js";
13
+ import { IS_BUN_WINDOWS, pauseGeneric, sleepUntilChanged, whilePausing, } from "./timers.js";
12
14
  import { IS_ANDROMEDA, RUNTIME, SET_IMMEDIATE } from "../common/runtime.js";
13
15
  import { getNodeProcess } from "../common/node-compat.js";
14
16
  import { assertWorkerImportsResolved, assertWorkerSharedMemoryBootData, installPerformanceNowGuard, installTerminationGuard, installUnhandledRejectionSilencer, scrubWorkerDataSensitiveBuffers, } from "./safety/index.js";
15
17
  import { signalAbortFactory } from "../shared/abortSignal.js";
16
18
  import { runWorkerBootstrap } from "./bootstrap.js";
17
19
  import { getProcessWorkerNativeWaitU32, installProcessWorkerBootstrap, } from "./process-worker-bootstrap.js";
18
- import { readBufferReferenceReturnReleaseMessage, } from "../connections/buffer-reference.js";
19
20
  import { resolveDebugNamespaces } from "../debug/gate.js";
20
21
  const WORKER_FATAL_MESSAGE_KEY = "__knittingWorkerFatal";
21
22
  const reportWorkerStartupFatal = (error) => {
@@ -53,27 +54,12 @@ const reportWorkerStartupFatal = (error) => {
53
54
  }
54
55
  }
55
56
  };
56
- const installBufferReferenceReleaseListener = (releaseReturnedBufferReference) => {
57
- const handleMessage = (message) => {
58
- const token = readBufferReferenceReturnReleaseMessage(message);
59
- if (token !== undefined)
60
- releaseReturnedBufferReference(token);
61
- };
62
- if (RUNTIME_PARENT_PORT !== undefined) {
63
- addRuntimeDataListener(RUNTIME_PARENT_PORT, handleMessage);
64
- return;
65
- }
66
- const scope = globalThis;
67
- scope.addEventListener?.("message", (event) => {
68
- handleMessage(event?.data);
69
- });
70
- };
71
57
  export const workerMainLoop = async (startupData) => {
72
58
  // Startup-only safety layer: no per-iteration checks in the hot loop.
73
59
  installTerminationGuard();
74
60
  installUnhandledRejectionSilencer();
75
61
  installPerformanceNowGuard();
76
- const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, bufferReferenceReturn, permission, notifyOnHostPublish, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
62
+ const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, sharedReturn, permission, notifyOnHostPublish, processCompletionDoorbell, denoCompletionDoorbell, nodeCompletionDoorbell, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
77
63
  scrubWorkerDataSensitiveBuffers(startupData);
78
64
  assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
79
65
  const debugNamespaces = resolveDebugNamespaces(debug);
@@ -84,7 +70,7 @@ export const workerMainLoop = async (startupData) => {
84
70
  namespaces: debugNamespaces,
85
71
  }))
86
72
  : undefined;
87
- // const object, not `enum`: Andromeda's Nova engine can't parse `enum`.
73
+ // Andromeda cannot parse enums.
88
74
  const Comment = {
89
75
  thisIsAHint: 0,
90
76
  };
@@ -110,7 +96,24 @@ export const workerMainLoop = async (startupData) => {
110
96
  consumers: steal?.consumers,
111
97
  consumerId: steal?.consumerId,
112
98
  regionLanes: steal?.regionLanes,
99
+ stealClaim: steal?.claim,
113
100
  });
101
+ const notifyDenoHost = createDenoCompletionNotifier(denoCompletionDoorbell);
102
+ const notifyNodeHost = createNodeCompletionNotifier(nodeCompletionDoorbell);
103
+ const processParentPort = processCompletionDoorbell === true
104
+ ? RUNTIME_PARENT_PORT
105
+ : undefined;
106
+ // Bun on Windows flushes process messages only after an event-loop turn.
107
+ let processRingPending = false;
108
+ const flushProcessRing = IS_BUN_WINDOWS;
109
+ const notifyProcessHost = processParentPort === undefined
110
+ ? undefined
111
+ : flushProcessRing
112
+ ? () => {
113
+ processParentPort.postMessage(PROCESS_COMPLETION_DOORBELL);
114
+ processRingPending = true;
115
+ }
116
+ : () => processParentPort.postMessage(PROCESS_COMPLETION_DOORBELL);
114
117
  const returnLockState = lock2({
115
118
  headers: returnLock.headers,
116
119
  headerSlotStrideU32: returnLock.headerSlotStrideU32,
@@ -120,9 +123,22 @@ export const workerMainLoop = async (startupData) => {
120
123
  payloadConfig,
121
124
  textCompat: returnLock.textCompat,
122
125
  processBoundary: RUNTIME_IS_PROCESS_WORKER,
126
+ sharedReturn: sharedReturn === true,
127
+ // A thread return moves its source before publication. Node transfers the
128
+ // backing-store ownership; Deno and Bun make one owned host copy before
129
+ // the worker drops its pin. Both avoid the borrowed-return lifetime.
130
+ moveReturn: !RUNTIME_IS_PROCESS_WORKER,
123
131
  // The host parks on this lock's publication word when it has no work to
124
132
  // flush. Request locks are host-produced and must not wake that waiter.
125
- notifyOnHostPublish,
133
+ notifyOnHostPublish: notifyOnHostPublish ||
134
+ notifyDenoHost !== undefined ||
135
+ notifyNodeHost !== undefined ||
136
+ notifyProcessHost !== undefined,
137
+ notifyHostPublish: notifyDenoHost !== undefined
138
+ ? () => notifyDenoHost(thread)
139
+ : notifyNodeHost !== undefined
140
+ ? notifyNodeHost
141
+ : notifyProcessHost,
126
142
  });
127
143
  const timers = workerOptions?.timers;
128
144
  const spinMicroseconds = timers?.spinMicroseconds ??
@@ -163,16 +179,16 @@ export const workerMainLoop = async (startupData) => {
163
179
  maxSignals: abortSignalMax,
164
180
  })
165
181
  : undefined;
166
- const { enqueueLock, serviceBatchImmediate, hasCompleted, writeBatch, hasPending, getAwaiting, drainReturnReleases, releaseReturnedBufferReference, } = createWorkerRxQueue({
182
+ const { enqueueLock, serviceBatchImmediate, hasCompleted, writeBatch, getAwaiting, drainReturnReleases, releaseDeferredReturns, } = createWorkerRxQueue({
167
183
  listOfFunctions,
168
184
  workerOptions,
169
185
  lock: lockState,
170
186
  returnLock: returnLockState,
171
- borrowReturnedBufferReferences: bufferReferenceReturn === "borrow",
172
187
  hasAborted: abortSignals?.hasAborted,
173
188
  stealing: steal !== undefined,
174
189
  });
175
- installBufferReferenceReleaseListener(releaseReturnedBufferReference);
190
+ if (sharedReturn === true)
191
+ installSharedReturn(returnLock.payload);
176
192
  a_store(rxStatus, 0, 1);
177
193
  const WRITE_MAX = 64;
178
194
  const pauseUntil = sleepUntilChanged({
@@ -224,9 +240,9 @@ export const workerMainLoop = async (startupData) => {
224
240
  };
225
241
  const traceSignals = dbg?.enabled("signals") === true;
226
242
  const _hasCompleted = hasCompleted;
227
- const _hasPending = hasPending;
228
243
  const _getAwaiting = getAwaiting;
229
244
  const _drainReturnReleases = drainReturnReleases;
245
+ const _releaseDeferredReturns = releaseDeferredReturns;
230
246
  const _pauseSpin = pauseSpin;
231
247
  const _enqueueLock = traceSignals
232
248
  ? () => {
@@ -259,9 +275,24 @@ export const workerMainLoop = async (startupData) => {
259
275
  pauseUntil(value, spinMicroseconds, parkMs);
260
276
  }
261
277
  : pauseUntil;
262
- const flushBeforeClaim = steal !== undefined;
278
+ // Stealing flushes completed work before claiming more; private lanes claim
279
+ // first to keep their single-consumer path short.
280
+ const pump = steal !== undefined
281
+ ? () => {
282
+ let progressed = false;
283
+ if (_hasCompleted() && _writeBatch(WRITE_MAX) > 0)
284
+ progressed = true;
285
+ return _enqueueLock() || progressed;
286
+ }
287
+ : () => {
288
+ const claimed = _enqueueLock();
289
+ return (_hasCompleted() && _writeBatch(WRITE_MAX) > 0) || claimed;
290
+ };
263
291
  /** Leave the dispatch loop and acknowledge shutdown. */
264
292
  const stopLoop = () => {
293
+ // A shutdown rejects unconsumed calls, so no return can still be adopted.
294
+ // Do not let a worker-isolate exit strand native BufferReference pins.
295
+ _releaseDeferredReturns();
265
296
  a_store(stopView, 0, WORKER_STOP.acknowledged);
266
297
  a_store(rxStatus, 0, 0);
267
298
  try {
@@ -272,41 +303,20 @@ export const workerMainLoop = async (startupData) => {
272
303
  };
273
304
  const loop = () => {
274
305
  isInMacro = false;
306
+ // Recompute progress on every pass so idle workers can park.
275
307
  let progressed = true;
276
308
  let awaiting = 0;
277
309
  while (true) {
278
310
  if (stopView[0] !== WORKER_STOP.running)
279
311
  return stopLoop();
280
- if (flushBeforeClaim) {
281
- // Stealing only. Flush finished work before taking more on: a computed
282
- // response should not wait behind a claim, and that claim can block on
283
- // a peer withdrawing its intent. Measured to hurt the per-lane path,
284
- // where a claim is a cheap private decode and picking work up promptly
285
- // matters more, so the classic order is kept below.
286
- // Reordering must not make `progressed` sticky: it answers "did this
287
- // iteration move anything", so it has to start false every pass or the
288
- // park below is unreachable and the worker spins a core forever.
289
- progressed = false;
290
- if (_hasCompleted()) {
291
- if (_writeBatch(WRITE_MAX) > 0)
292
- progressed = true;
293
- }
294
- progressed = _enqueueLock() || progressed;
295
- }
296
- else {
297
- progressed = _enqueueLock();
298
- if (_hasCompleted()) {
299
- if (_writeBatch(WRITE_MAX) > 0)
300
- progressed = true;
301
- }
302
- }
312
+ progressed = pump();
303
313
  _drainReturnReleases();
304
- if (_hasPending()) {
305
- if (_serviceBatchImmediate() > 0)
306
- progressed = true;
307
- }
314
+ if (_serviceBatchImmediate() > 0)
315
+ progressed = true;
308
316
  if ((awaiting = _getAwaiting()) > 0) {
309
- if (awaiting !== lastAwaiting)
317
+ // Promise callbacks need the loop to yield; delay only when no progress
318
+ // was made, otherwise queued work pays an unnecessary timer hop.
319
+ if (progressed || awaiting !== lastAwaiting)
310
320
  awaitingSpins = 0;
311
321
  lastAwaiting = awaiting;
312
322
  awaitingSpins++;
@@ -315,9 +325,21 @@ export const workerMainLoop = async (startupData) => {
315
325
  return;
316
326
  }
317
327
  awaitingSpins = lastAwaiting = 0;
328
+ if (processRingPending) {
329
+ processRingPending = false;
330
+ scheduleTimer(0);
331
+ return;
332
+ }
318
333
  if (!progressed) {
319
334
  if (txStatus[Comment.thisIsAHint] === 1) {
320
- _pauseSpin();
335
+ // During a hint spin only host-driven state can change.
336
+ do {
337
+ _pauseSpin();
338
+ if (stopView[0] !== WORKER_STOP.running)
339
+ return stopLoop();
340
+ if (pump())
341
+ break;
342
+ } while (txStatus[Comment.thisIsAHint] === 1);
321
343
  continue;
322
344
  }
323
345
  _pauseUntil(wakeToken, spinMicroseconds, parkMs);
@@ -6,7 +6,6 @@ type ArgumentsForCreateWorkerQueue = {
6
6
  workerOptions?: WorkerSettings;
7
7
  lock: Lock2;
8
8
  returnLock: Lock2;
9
- borrowReturnedBufferReferences?: boolean;
10
9
  hasAborted?: (signal: number) => boolean;
11
10
  now?: () => number;
12
11
  /**
@@ -21,14 +20,14 @@ type ArgumentsForCreateWorkerQueue = {
21
20
  stealing?: boolean;
22
21
  };
23
22
  export type CreateWorkerRxQueue = ReturnType<typeof createWorkerRxQueue>;
24
- export declare const createWorkerRxQueue: ({ listOfFunctions, workerOptions, lock, returnLock, borrowReturnedBufferReferences, hasAborted, now, stealing, }: ArgumentsForCreateWorkerQueue) => {
23
+ export declare const createWorkerRxQueue: ({ listOfFunctions, workerOptions, lock, returnLock, hasAborted, now, stealing, }: ArgumentsForCreateWorkerQueue) => {
25
24
  hasCompleted: () => boolean;
26
25
  hasPending: () => boolean;
27
26
  writeBatch: (max: number) => number;
28
27
  serviceBatchImmediate: () => number;
29
28
  enqueueLock: () => boolean;
30
29
  drainReturnReleases: () => void;
31
- releaseReturnedBufferReference: (token: bigint) => void;
30
+ releaseDeferredReturns: () => void;
32
31
  hasAwaiting: () => boolean;
33
32
  getAwaiting: () => number;
34
33
  };