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
@@ -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;
@@ -10,4 +10,5 @@ export type NodeWorkerLike = {
10
10
  export declare const toWorkerSafeExecArgv: (flags: string[] | undefined) => string[] | undefined;
11
11
  export declare const toWorkerCompatExecArgv: (flags: string[] | undefined) => string[] | undefined;
12
12
  export declare const serializeWorkerBootstrapData: (options: WorkerSettings) => WorkerSettings;
13
- export declare const terminateWorkerQuietly: (worker: SpawnedWorker) => Promise<void>;
13
+ /** Terminate a worker, optionally waiting for it to exit. */
14
+ export declare const terminateWorkerQuietly: (worker: SpawnedWorker, awaitExit?: boolean) => Promise<void>;
@@ -90,12 +90,26 @@ export const serializeWorkerBootstrapData = (options) => {
90
90
  },
91
91
  };
92
92
  };
93
- export const terminateWorkerQuietly = (worker) => {
93
+ // Bound the wait for process-worker exit.
94
+ const WORKER_EXIT_WAIT_MS = 2_000;
95
+ /** Terminate a worker, optionally waiting for it to exit. */
96
+ export const terminateWorkerQuietly = (worker, awaitExit = false) => {
94
97
  try {
95
- // Runaway worker termination can be slow or stuck on some runtimes; once the
96
- // pool is closing it must not keep the host process alive.
97
- worker.unref?.();
98
- return Promise.resolve(worker.terminate()).then(() => { }, () => { });
98
+ if (!awaitExit)
99
+ worker.unref?.();
100
+ const exited = Promise.resolve(worker.terminate()).then(() => { }, () => { });
101
+ if (!awaitExit)
102
+ return exited;
103
+ return new Promise((resolve) => {
104
+ const timer = setTimeout(() => {
105
+ worker.unref?.();
106
+ resolve();
107
+ }, WORKER_EXIT_WAIT_MS);
108
+ void exited.then(() => {
109
+ clearTimeout(timer);
110
+ resolve();
111
+ });
112
+ });
99
113
  }
100
114
  catch {
101
115
  return Promise.resolve();
package/src/types.d.ts CHANGED
@@ -13,6 +13,8 @@ type WorkerInvoke = (args: Uint8Array) => Promise<unknown>;
13
13
  interface WorkerContext {
14
14
  txIdle(): boolean;
15
15
  call(descriptor: WorkerCall): WorkerInvoke;
16
+ /** Ask the worker to leave its dispatch loop before termination. */
17
+ requestStop?(): Promise<boolean>;
16
18
  kills(): Promise<void>;
17
19
  }
18
20
  type CreateContext = WorkerContext;
@@ -32,30 +34,31 @@ type WorkerData = {
32
34
  lock: LockBuffers;
33
35
  returnLock: LockBuffers;
34
36
  payloadConfig?: PayloadBufferOptions;
35
- bufferReferenceReturn?: "copy" | "borrow";
37
+ /** Enable borrowed returns for this worker's return lane. */
38
+ sharedReturn?: boolean;
36
39
  permission?: ResolvedPermissionProtocol;
37
40
  /** Whether this host can arm an async completion waiter on the return lock. */
38
41
  notifyOnHostPublish?: boolean;
39
- /**
40
- * Work stealing. When present, `lock` is a submit region shared by every
41
- * worker and this worker claims from it as consumer `consumerId` of
42
- * `consumers`, rather than owning a private request lane. `returnLock` stays
43
- * private — the endpoint that claims a task owns its response.
44
- */
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. */
45
49
  steal?: {
46
50
  consumers: number;
47
51
  consumerId: number;
48
52
  regionLanes: number;
53
+ /** Region mutual-exclusion discipline; see `DispatcherSettings.stealClaim`. */
54
+ claim?: "dekker" | "cas-mask";
49
55
  };
50
56
  };
51
57
  type UnsafeOptions = {
52
- /**
53
- * Experimental `BufferReference` return lifetime.
54
- *
55
- * `"copy"` is safe after worker release. `"borrow"` skips the Deno/Bun copy,
56
- * but must be released before producer shutdown and must not outlive its ref.
57
- */
58
- 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;
59
62
  };
60
63
  type LockBuffers = {
61
64
  headers: SharedBufferSource;
@@ -181,6 +184,11 @@ type Pool<T extends Record<string, TaskLike<any> | TaskFunctionLike>> = {
181
184
  * Thrown errors/rejections reject here as Error objects with cause chains.
182
185
  */
183
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;
184
192
  };
185
193
  type ReturnFixed<A extends TaskInput = undefined, B extends Args = undefined, AS extends AbortSignalOption = undefined> = FixPoint<A, B, AS> & SecondPart & {
186
194
  createPool: (options?: CreatePool) => SingleTaskPool<A, B, AS>;
@@ -307,10 +315,10 @@ type WorkerSettings = {
307
315
  /**
308
316
  * How process workers discover their shared-memory control channel.
309
317
  *
310
- * "inherit" keeps the POSIX fd-inheritance path and is the default outside
311
- * Windows. "named" creates an OS-named shared-memory object that wrappers
312
- * such as containers can reopen by name when they share the same IPC
313
- * 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.
314
322
  */
315
323
  processSharedMemory?: ProcessSharedMemoryMode | ProcessSharedMemorySettings;
316
324
  timers?: WorkerTimers;
@@ -342,47 +350,19 @@ type WorkerTimers = {
342
350
  pauseNanoseconds?: number;
343
351
  };
344
352
  type DispatcherSettings = {
345
- /**
346
- * How many immediate notify loops before the dispatcher stops re-arming the
347
- * pump for free.
348
- *
349
- * The default depends on what the dispatcher escalates *to*. Polling
350
- * escalates to a `setTimeout` ladder costing ~1.1ms even at delay 0, so it
351
- * defaults to 128 — escalating is expensive and the wide window also batches
352
- * completions. A doorbell escalates to `Atomics.waitAsync` at roughly the
353
- * price of one hop, so pools that have one default to 1. Pools without a
354
- * doorbell — Deno, or `doorbell: false` — keep 128.
355
- *
356
- * Setting this explicitly opts out of that coupling for every pool shape.
357
- */
353
+ /** Number of immediate notify loops before backoff starts. */
358
354
  stallFreeLoops?: number;
359
355
  /**
360
356
  * Max backoff delay (milliseconds).
361
357
  */
362
358
  maxBackoffMs?: number;
363
359
  /**
364
- * Replace idle completion polling with an `Atomics.waitAsync` doorbell when
365
- * the host runtime supports it.
366
- *
367
- * Defaults to enabled on Node and Bun at any worker count, where it costs
368
- * 1.6-3.9x less host CPU per completed call. Under HTTP load, where the host
369
- * has real work of its own, that converts to +13% to +36% throughput.
370
- *
371
- * Turn it off for a pool that oversubscribes its machine. The doorbell only
372
- * progresses when the host gets scheduled, so once workers occupy every core
373
- * a wake must preempt one: measured +5% to +12.6% rps while workers+host fit
374
- * within the cores, -22% to -32% once they do not. That is not gated
375
- * automatically because the core count cannot be probed portably.
376
- *
377
- * Forced off, overriding an explicit `true`, where a doorbell cannot work:
378
- * on Deno, whose `waitAsync` does not wake an idle event loop, and for
379
- * process workers, which live in another process and so cannot ring a host
380
- * waiter at all — V8 keeps its Atomics waiter list per isolate, which is why
381
- * they wake through a native futex addon instead. Compiled (Porffor) workers
382
- * never reach this path: they reject `host` outright and use pipes rather
383
- * 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.
384
362
  */
385
363
  doorbell?: boolean;
364
+ /** Use Node's native `uv_async_t` completion bridge when available. */
365
+ nativeDoorbell?: boolean;
386
366
  /**
387
367
  * Host dispatcher topology.
388
368
  * - `"per-thread"`: each worker owns its dispatcher and macro channel.
@@ -396,29 +376,15 @@ type DispatcherSettings = {
396
376
  * `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
397
377
  */
398
378
  dispatcher?: "per-thread" | "serial-channel";
399
- /**
400
- * Work stealing: one shared submit region that any worker may
401
- * claim from, private return lanes, and a pool-global pending registry. The
402
- * endpoint that claims a task owns its response.
403
- *
404
- * Enabled by default for compatible multi-worker thread and process pools
405
- * unless a balancer or private-lane dispatcher was explicitly selected.
406
- * One-worker pools, inliners, compiled/Porffor workers, and pools above the
407
- * current 31-claimant protocol limit retain their existing transport. Set
408
- * `false` (or `KNITTING_STEAL=0`) to opt out; `KNITTING_STEAL=1` explicitly
409
- * opts in and overrides a balancer/dispatcher selection.
410
- */
379
+ /** Use a shared submit region with private return lanes. */
411
380
  steal?: boolean;
412
381
  /**
413
- * Lanes claimed per stealing handshake (a power of two, `slots / g >=
414
- * workers + 1`). Defaults to the widest region the lane budget allows, which
415
- * amortises arbitration best for cheap tasks.
416
- *
417
- * **A region is a batch.** For expensive tasks, a wide region lets one worker
418
- * claim work the others could have run in parallel; set this to `1` (or a
419
- * 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.
420
384
  */
421
385
  stealRegionLanes?: number;
386
+ /** Region-claim discipline: per-consumer Dekker intents or a shared CAS mask. */
387
+ stealClaim?: "dekker" | "cas-mask";
422
388
  };
423
389
  type CreatePool = {
424
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";
5
- import { createSharedMemoryTransport } from "../ipc/transport/shared-memory.js";
7
+ import { installSharedReturn } from "./shared-return.js";
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,24 +123,36 @@ 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 ??
129
- Math.max(1, totalNumberOfThread) * 50;
145
+ (totalNumberOfThread <= 1 ? 50 : 0);
130
146
  const parkMs = dbg !== undefined
131
147
  ? Number.POSITIVE_INFINITY
132
- : (timers?.parkMs ??
133
- Math.max(1, totalNumberOfThread) * 50);
148
+ : (timers?.parkMs ?? 1);
134
149
  const pauseSpin = (() => {
135
150
  const fn = typeof timers?.pauseNanoseconds === "number"
136
151
  ? whilePausing({ pauseInNanoseconds: timers.pauseNanoseconds })
137
152
  : pauseGeneric;
138
153
  return () => fn(); // always a closure wrapper
139
154
  })();
140
- const { opView, rxStatus, txStatus } = signals;
155
+ const { opView, rxStatus, txStatus, stopView } = signals;
141
156
  const a_store = Atomics.store;
142
157
  const a_load = Atomics.load;
143
158
  const nativeWaitU32 = getProcessWorkerNativeWaitU32();
@@ -164,16 +179,16 @@ export const workerMainLoop = async (startupData) => {
164
179
  maxSignals: abortSignalMax,
165
180
  })
166
181
  : undefined;
167
- const { enqueueLock, serviceBatchImmediate, hasCompleted, writeBatch, hasPending, getAwaiting, drainReturnReleases, releaseReturnedBufferReference, } = createWorkerRxQueue({
182
+ const { enqueueLock, serviceBatchImmediate, hasCompleted, writeBatch, getAwaiting, drainReturnReleases, releaseDeferredReturns, } = createWorkerRxQueue({
168
183
  listOfFunctions,
169
184
  workerOptions,
170
185
  lock: lockState,
171
186
  returnLock: returnLockState,
172
- borrowReturnedBufferReferences: bufferReferenceReturn === "borrow",
173
187
  hasAborted: abortSignals?.hasAborted,
174
188
  stealing: steal !== undefined,
175
189
  });
176
- installBufferReferenceReleaseListener(releaseReturnedBufferReference);
190
+ if (sharedReturn === true)
191
+ installSharedReturn(returnLock.payload);
177
192
  a_store(rxStatus, 0, 1);
178
193
  const WRITE_MAX = 64;
179
194
  const pauseUntil = sleepUntilChanged({
@@ -225,9 +240,9 @@ export const workerMainLoop = async (startupData) => {
225
240
  };
226
241
  const traceSignals = dbg?.enabled("signals") === true;
227
242
  const _hasCompleted = hasCompleted;
228
- const _hasPending = hasPending;
229
243
  const _getAwaiting = getAwaiting;
230
244
  const _drainReturnReleases = drainReturnReleases;
245
+ const _releaseDeferredReturns = releaseDeferredReturns;
231
246
  const _pauseSpin = pauseSpin;
232
247
  const _enqueueLock = traceSignals
233
248
  ? () => {
@@ -260,42 +275,48 @@ export const workerMainLoop = async (startupData) => {
260
275
  pauseUntil(value, spinMicroseconds, parkMs);
261
276
  }
262
277
  : pauseUntil;
263
- 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
+ };
291
+ /** Leave the dispatch loop and acknowledge shutdown. */
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();
296
+ a_store(stopView, 0, WORKER_STOP.acknowledged);
297
+ a_store(rxStatus, 0, 0);
298
+ try {
299
+ port1.close?.();
300
+ port2.close?.();
301
+ }
302
+ catch { }
303
+ };
264
304
  const loop = () => {
265
305
  isInMacro = false;
306
+ // Recompute progress on every pass so idle workers can park.
266
307
  let progressed = true;
267
308
  let awaiting = 0;
268
309
  while (true) {
269
- if (flushBeforeClaim) {
270
- // Stealing only. Flush finished work before taking more on: a computed
271
- // response should not wait behind a claim, and that claim can block on
272
- // a peer withdrawing its intent. Measured to hurt the per-lane path,
273
- // where a claim is a cheap private decode and picking work up promptly
274
- // matters more, so the classic order is kept below.
275
- // Reordering must not make `progressed` sticky: it answers "did this
276
- // iteration move anything", so it has to start false every pass or the
277
- // park below is unreachable and the worker spins a core forever.
278
- progressed = false;
279
- if (_hasCompleted()) {
280
- if (_writeBatch(WRITE_MAX) > 0)
281
- progressed = true;
282
- }
283
- progressed = _enqueueLock() || progressed;
284
- }
285
- else {
286
- progressed = _enqueueLock();
287
- if (_hasCompleted()) {
288
- if (_writeBatch(WRITE_MAX) > 0)
289
- progressed = true;
290
- }
291
- }
310
+ if (stopView[0] !== WORKER_STOP.running)
311
+ return stopLoop();
312
+ progressed = pump();
292
313
  _drainReturnReleases();
293
- if (_hasPending()) {
294
- if (_serviceBatchImmediate() > 0)
295
- progressed = true;
296
- }
314
+ if (_serviceBatchImmediate() > 0)
315
+ progressed = true;
297
316
  if ((awaiting = _getAwaiting()) > 0) {
298
- 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)
299
320
  awaitingSpins = 0;
300
321
  lastAwaiting = awaiting;
301
322
  awaitingSpins++;
@@ -304,9 +325,21 @@ export const workerMainLoop = async (startupData) => {
304
325
  return;
305
326
  }
306
327
  awaitingSpins = lastAwaiting = 0;
328
+ if (processRingPending) {
329
+ processRingPending = false;
330
+ scheduleTimer(0);
331
+ return;
332
+ }
307
333
  if (!progressed) {
308
334
  if (txStatus[Comment.thisIsAHint] === 1) {
309
- _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);
310
343
  continue;
311
344
  }
312
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
  };