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
@@ -0,0 +1,117 @@
1
+ import { RUNTIME } from "../common/runtime.js";
2
+ const getDeno = () => globalThis.Deno;
3
+ /**
4
+ * Creates Deno's native thread-to-event-loop bridge without loading a custom
5
+ * library. `threadSafe()` is essential: ordinary UnsafeCallbacks may be
6
+ * called cross-thread, but do not wake an idle Deno event loop.
7
+ *
8
+ * The default pool configuration asks Deno for FFI permission once, so the
9
+ * native wake path is used whenever the caller approves it. `--allow-ffi`
10
+ * skips the prompt; `--deny-ffi`, `--no-prompt`, or a rejection preserves the
11
+ * portable polling dispatcher. Set `host.doorbell` to `false` to avoid asking.
12
+ */
13
+ export const createDenoCompletionDoorbell = () => {
14
+ if (RUNTIME !== "deno")
15
+ return undefined;
16
+ const deno = getDeno();
17
+ const queryPermission = deno?.permissions?.querySync;
18
+ const requestPermission = deno?.permissions?.requestSync;
19
+ const Callback = deno?.UnsafeCallback;
20
+ const pointerValue = deno?.UnsafePointer?.value;
21
+ if (typeof queryPermission !== "function" ||
22
+ typeof Callback?.threadSafe !== "function" ||
23
+ typeof pointerValue !== "function") {
24
+ return undefined;
25
+ }
26
+ // `querySync` avoids a redundant prompt for an already granted or explicitly
27
+ // denied capability. Only the normal `prompt` state asks, and a refusal
28
+ // leaves the portable polling path intact.
29
+ try {
30
+ let state = queryPermission({ name: "ffi" }).state;
31
+ if (state === "prompt" && typeof requestPermission === "function") {
32
+ state = requestPermission({ name: "ffi" }).state;
33
+ }
34
+ if (state !== "granted")
35
+ return undefined;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ const listeners = new Map();
41
+ let closed = false;
42
+ let callback;
43
+ try {
44
+ callback = Callback.threadSafe({ parameters: ["i32"], result: "void" }, (lane) => {
45
+ if (closed)
46
+ return;
47
+ try {
48
+ listeners.get(lane)?.();
49
+ }
50
+ catch {
51
+ // FFI callbacks must never propagate an exception into the worker
52
+ // thread that rang them. The dispatcher watchdog remains a final
53
+ // liveness backstop if a host pump has already been closed.
54
+ }
55
+ });
56
+ const pointer = pointerValue(callback.pointer);
57
+ if (typeof pointer !== "bigint" || pointer === 0n) {
58
+ callback.close();
59
+ return undefined;
60
+ }
61
+ return {
62
+ pointer,
63
+ listen: (lane, notify) => {
64
+ if (!closed)
65
+ listeners.set(lane, notify);
66
+ },
67
+ unref: () => {
68
+ try {
69
+ callback.unref?.();
70
+ }
71
+ catch {
72
+ // best effort; a runtime may already be tearing down its callback
73
+ }
74
+ },
75
+ close: () => {
76
+ if (closed)
77
+ return;
78
+ closed = true;
79
+ listeners.clear();
80
+ callback.close();
81
+ },
82
+ };
83
+ }
84
+ catch {
85
+ // Creating a callback is native code. Treat an unsupported Deno build just
86
+ // like a denied FFI capability and keep the portable poll path.
87
+ try {
88
+ callback?.close();
89
+ }
90
+ catch {
91
+ // best effort; callback may not have been initialized
92
+ }
93
+ return undefined;
94
+ }
95
+ };
96
+ /**
97
+ * Reconstruct a host-owned thread-safe callback in a Deno worker. The pointer
98
+ * stays process-local and is never included in process-worker boot payloads.
99
+ */
100
+ export const createDenoCompletionNotifier = (pointer) => {
101
+ if (RUNTIME !== "deno" || pointer === undefined || pointer === 0n) {
102
+ return undefined;
103
+ }
104
+ const deno = getDeno();
105
+ const FnPointer = deno?.UnsafeFnPointer;
106
+ const pointerCreate = deno?.UnsafePointer?.create;
107
+ if (typeof FnPointer !== "function" || typeof pointerCreate !== "function") {
108
+ return undefined;
109
+ }
110
+ try {
111
+ const callback = new FnPointer(pointerCreate(pointer), { parameters: ["i32"], result: "void" });
112
+ return (lane) => callback.call(lane);
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ };
@@ -2,20 +2,25 @@ import { type MultiQueue } from "./tx-queue.js";
2
2
  import { type MainSignal } from "../ipc/transport/shared-memory.js";
3
3
  import { type RuntimeMessageChannelLike, type RuntimeMessagePortLike } from "../common/worker-runtime.js";
4
4
  import type { DispatcherSettings } from "../types.js";
5
- export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, }: {
5
+ export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }: {
6
6
  queue: MultiQueue;
7
7
  signalBox: MainSignal;
8
8
  channelHandler: ChannelHandler;
9
9
  dispatcherOptions?: DispatcherSettings;
10
10
  notifySignal?: () => void;
11
- /** Workers live in other processes; disables the doorbell. */
11
+ /** Workers live in other processes; disables only the atomic doorbell. */
12
12
  crossProcess?: boolean;
13
+ /** A runtime-native completion ring that wakes the host event loop. */
14
+ nativeCompletionDoorbell?: boolean;
15
+ /** A process IPC completion doorbell that wakes the host event loop. */
16
+ processCompletionDoorbell?: boolean;
13
17
  }) => {
14
18
  check: {
15
19
  (): void;
16
20
  isRunning: boolean;
17
21
  rerun: boolean;
18
22
  };
23
+ wakeCompletion: () => void;
19
24
  };
20
25
  type CheckWithState = (() => void) & {
21
26
  isRunning: boolean;
@@ -30,10 +35,7 @@ export declare class ChannelHandler {
30
35
  port2: RuntimeMessagePortLike | undefined;
31
36
  constructor(pump?: ChannelHandlerPump);
32
37
  notify(): void;
33
- /**
34
- * Registers the handler the pump calls back into. On the channel pump this is
35
- * also where the ports are opened, so `notify` can reach port 1.
36
- */
38
+ /** Register the pump handler and start channel ports when present. */
37
39
  open(f: () => void): void;
38
40
  /**
39
41
  * Detaches the handler, and closes the channel if this pump has one.
@@ -1,34 +1,13 @@
1
1
  import { createRuntimeMessageChannel, } from "../common/worker-runtime.js";
2
2
  import { RUNTIME, SET_IMMEDIATE } from "../common/runtime.js";
3
- /**
4
- * Macrotask primitive for the host pump, picked per runtime: a round trip costs
5
- * 757ns on Bun via MessageChannel but 4662ns on Deno, where `setImmediate` is
6
- * 1110ns. Bun keeps the channel; browser and Andromeda have no choice.
7
- *
8
- * `serial-channel` opts back into the channel explicitly (see `src/api.ts`): one
9
- * hop drives every lane's check in turn and relies on the channel's delivery to
10
- * do it, so a merely cheaper pump starves it -- it cost 25% throughput there.
11
- */
3
+ /** Runtime-specific macrotask primitive used by the host dispatcher. */
12
4
  const IMMEDIATE_PUMP = RUNTIME === "deno" || RUNTIME === "node"
13
5
  ? SET_IMMEDIATE
14
6
  : undefined;
15
- /**
16
- * Free drain hops before `scheduleNotify` stops re-arming the pump for free.
17
- *
18
- * The window has to match what the dispatcher escalates *to*, so these are
19
- * chosen together and `canUseDoorbell` picks between them. Polling escalates to
20
- * the `setTimeout` ladder, whose finest rung is ~1.1ms on every runtime, so it
21
- * wants a wide window; that sleep is also load-bearing, since it batches
22
- * completions. A doorbell escalates to `Atomics.waitAsync` at about the price of
23
- * one hop, so it wants a narrow one.
24
- *
25
- * Mixing them is the trap: a doorbell behind the wide window keeps every poll
26
- * hop *and* adds the arm, and loses to plain polling at every thread count.
27
- * Measurements behind both values: `docs/host-doorbell-proposal.md`.
28
- */
7
+ // Polling tolerates a wider free window; a doorbell should arm promptly.
29
8
  const POLL_STALL_FREE_LOOPS = 128;
30
9
  const DOORBELL_STALL_FREE_LOOPS = 1;
31
- export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, }) => {
10
+ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }) => {
32
11
  const a_load = Atomics.load;
33
12
  const a_store = Atomics.store;
34
13
  const a_notify = Atomics.notify;
@@ -39,14 +18,14 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
39
18
  a_notify(opView, 0, 1);
40
19
  });
41
20
  const notify = () => channelHandler.notify();
42
- // `crossProcess` is a capability, not a preference, so it overrides an
43
- // explicit `doorbell: true`: V8's Atomics waiter list is per isolate, so a
44
- // worker in another process can never ring the host's waiter, and an armed
45
- // doorbell would just sleep to the watchdog.
46
- const canUseDoorbell = crossProcess !== true &&
47
- (dispatcherOptions?.doorbell ?? true) &&
48
- (RUNTIME === "bun" || RUNTIME === "node") &&
21
+ // Atomics waiters are process-local, so cross-process workers cannot use this
22
+ // doorbell.
23
+ const canUseAtomicDoorbell = (RUNTIME === "bun" || RUNTIME === "node") &&
49
24
  typeof Atomics.waitAsync === "function";
25
+ const canUseDoorbell = (dispatcherOptions?.doorbell ?? true) &&
26
+ (processCompletionDoorbell === true ||
27
+ (crossProcess !== true &&
28
+ (canUseAtomicDoorbell || nativeCompletionDoorbell === true)));
50
29
  let doorbellEnabled = canUseDoorbell;
51
30
  let doorbellArmed = false;
52
31
  let doorbellEpoch = 0 | 0;
@@ -80,24 +59,55 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
80
59
  doorbellEpoch = token;
81
60
  doorbellArmed = true;
82
61
  let woke = false;
83
- const wake = () => {
62
+ const wake = (direct = false) => {
84
63
  if (!doorbellArmed || doorbellEpoch !== token || woke)
85
64
  return;
86
65
  woke = true;
87
66
  doorbellArmed = false;
88
67
  doorbellEpoch = (doorbellEpoch + 1) | 0;
89
68
  setCompletionWaiterArmed(false);
69
+ if (direct) {
70
+ // A native arm that observes an already-published completion has no
71
+ // callback to wait for. Drain it now: routing this known result back
72
+ // through setImmediate recreates the very millisecond-sized hop that
73
+ // the native doorbell exists to remove.
74
+ check.isRunning = true;
75
+ check();
76
+ return;
77
+ }
90
78
  // Keep the existing macrotask boundary. Calling check directly from a
91
79
  // resolved waitAsync promise can chain microtasks under a hot workload
92
80
  // and starve host I/O.
93
81
  notify();
94
82
  };
95
83
  let supported = false;
96
- try {
97
- supported = waitForCompletion(wake, DOORBELL_WATCHDOG_MS);
84
+ if (nativeCompletionDoorbell === true ||
85
+ processCompletionDoorbell === true) {
86
+ // A native arm reports two different things and they must not be
87
+ // conflated: `false` means a completion was already published so the arm
88
+ // deliberately did not ring, which is a normal hot-path outcome. Only a
89
+ // throw means the transport is unusable. Treating the race as
90
+ // "unsupported" disabled the doorbell permanently on its second arm.
91
+ let armed = false;
92
+ let usable = true;
93
+ try {
94
+ armed = armCompletionNotifier();
95
+ }
96
+ catch {
97
+ usable = false;
98
+ }
99
+ supported = usable;
100
+ // The arm itself drains the completion it just observed.
101
+ if (usable && !armed)
102
+ wake(true);
98
103
  }
99
- catch {
100
- supported = false;
104
+ else {
105
+ try {
106
+ supported = waitForCompletion(wake, DOORBELL_WATCHDOG_MS);
107
+ }
108
+ catch {
109
+ supported = false;
110
+ }
101
111
  }
102
112
  // Some runtimes reject waitAsync on their SharedArrayBuffer variants; fall
103
113
  // back to polling rather than hanging the pool.
@@ -111,7 +121,9 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
111
121
  doorbellArmed = false;
112
122
  doorbellEpoch = (doorbellEpoch + 1) | 0;
113
123
  setCompletionWaiterArmed(false);
114
- notify();
124
+ if (nativeCompletionDoorbell !== true &&
125
+ processCompletionDoorbell !== true)
126
+ notify();
115
127
  }
116
128
  };
117
129
  const check = () => {
@@ -124,9 +136,7 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
124
136
  // A waitAsync cannot be cancelled, so bumping its epoch makes the eventual
125
137
  // callback inert instead.
126
138
  cancelDoorbell();
127
- // Idle lane: skip the drain. Otherwise it pays a txStatus write, an rxStatus
128
- // load, and an Atomics.notify that wakes a parked worker for nothing — the
129
- // waste that makes serial-channel latency grow with thread count.
139
+ // Avoid waking an idle lane.
130
140
  if (txIdle()) {
131
141
  check.isRunning = false;
132
142
  return;
@@ -139,15 +149,13 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
139
149
  do {
140
150
  check.rerun = false;
141
151
  txStatus[0] = 1;
142
- // Wake the worker before draining so it can start processing while we flush.
152
+ // Wake a parked worker whenever its receive status is clear. Pending
153
+ // frames are not a reliable gate because the queue may have room.
143
154
  if (a_load(rxStatus, 0) === 0) {
144
155
  a_store(opView, 0, 1);
145
156
  wakeSignal();
146
157
  }
147
- // Local vars so V8 keeps them as unboxed int32. Only a reaped completion
148
- // counts as progress for `stallCount`: the pump exists to notice work a
149
- // worker cannot announce, and letting a flush reset the counter would
150
- // leave the escalation unreachable for a pool that never runs dry.
158
+ // Only completed frames count as progress for backoff purposes.
151
159
  let completed = false;
152
160
  let progressed = true;
153
161
  while (progressed) {
@@ -164,7 +172,9 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
164
172
  }
165
173
  txStatus[0] = 0;
166
174
  if (!txIdle()) {
167
- if (completed || hasPendingFrames()) {
175
+ // Queued frames indicate back-pressure, not progress; only a completion
176
+ // frees a request slot.
177
+ if (completed) {
168
178
  stallCount = 0 | 0;
169
179
  }
170
180
  else {
@@ -182,6 +192,15 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
182
192
  };
183
193
  check.isRunning = false;
184
194
  check.rerun = false;
195
+ /** Enter the drain from a native or IPC completion event. */
196
+ const wakeCompletion = () => {
197
+ if (inFlight || check.isRunning) {
198
+ check.rerun = true;
199
+ return;
200
+ }
201
+ check.isRunning = true;
202
+ check();
203
+ };
185
204
  const scheduleNotify = () => {
186
205
  if (stallCount <= stallFreeLoops) {
187
206
  notify();
@@ -213,7 +232,7 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
213
232
  }
214
233
  }, delay);
215
234
  };
216
- return { check };
235
+ return { check, wakeCompletion };
217
236
  };
218
237
  export class ChannelHandler {
219
238
  // Set only when the pump is a MessageChannel. The `setImmediate` pump has no
@@ -223,14 +242,16 @@ export class ChannelHandler {
223
242
  port2;
224
243
  #handler;
225
244
  #notify;
245
+ #pending = false;
246
+ #run = () => {
247
+ // Clear before draining so a busy lane can request the next turn.
248
+ this.#pending = false;
249
+ this.#handler?.();
250
+ };
226
251
  constructor(pump = "auto") {
227
252
  if (pump === "auto" && IMMEDIATE_PUMP !== undefined) {
228
253
  const immediate = IMMEDIATE_PUMP;
229
- // Allocated once; the pump fires hundreds of thousands of times a second.
230
- // Routing via `#handler` also makes a callback outliving `close()` inert.
231
- const run = () => {
232
- this.#handler?.();
233
- };
254
+ const run = this.#run;
234
255
  this.#notify = () => {
235
256
  immediate(run);
236
257
  };
@@ -246,23 +267,24 @@ export class ChannelHandler {
246
267
  };
247
268
  }
248
269
  notify() {
270
+ // Coalesce notifications into one pending drain.
271
+ if (this.#pending)
272
+ return;
273
+ this.#pending = true;
249
274
  this.#notify();
250
275
  }
251
- /**
252
- * Registers the handler the pump calls back into. On the channel pump this is
253
- * also where the ports are opened, so `notify` can reach port 1.
254
- */
276
+ /** Register the pump handler and start channel ports when present. */
255
277
  open(f) {
256
278
  this.#handler = f;
257
279
  const port1 = this.port1;
258
280
  if (port1 === undefined)
259
281
  return;
260
282
  if (typeof port1.on === "function") {
261
- port1.on("message", f);
283
+ port1.on("message", this.#run);
262
284
  }
263
285
  else {
264
286
  // @ts-ignore
265
- port1.onmessage = f;
287
+ port1.onmessage = this.#run;
266
288
  }
267
289
  this.port1?.start?.();
268
290
  this.port2?.start?.();
@@ -0,0 +1,3 @@
1
+ export type HostArgAllocator = (byteLength: number) => Uint8Array;
2
+ /** Create an argument allocator with a plain-allocation fallback. */
3
+ export declare const createHostArgAllocator: (payload: object | undefined) => HostArgAllocator;
@@ -0,0 +1,16 @@
1
+ import { getSharedReturnAllocator } from "../memory/payloadCodec.js";
2
+ /** Create an argument allocator with a plain-allocation fallback. */
3
+ export const createHostArgAllocator = (payload) => {
4
+ const allocate = payload === undefined
5
+ ? undefined
6
+ : getSharedReturnAllocator(payload);
7
+ if (allocate === undefined) {
8
+ return (byteLength) => new Uint8Array(byteLength);
9
+ }
10
+ return (byteLength) => {
11
+ if (!Number.isInteger(byteLength) || byteLength < 0) {
12
+ throw new RangeError("sharedArgBytes(byteLength) requires a non-negative integer");
13
+ }
14
+ return allocate(byteLength, false) ?? new Uint8Array(byteLength);
15
+ };
16
+ };
@@ -0,0 +1,14 @@
1
+ export type NodeCompletionDoorbell = {
2
+ /** Process-local handle a Node thread worker may ring through the addon. */
3
+ pointer: bigint;
4
+ /** Invalidates the native handle after every worker that holds it has exited. */
5
+ close: () => void;
6
+ };
7
+ /**
8
+ * Bridge a Node worker thread into the host's libuv I/O phase. The addon owns
9
+ * the `uv_async_t`; this wrapper owns the JS callback and makes missing native
10
+ * prebuilds a normal capability fallback.
11
+ */
12
+ export declare const createNodeCompletionDoorbell: (notify: () => void) => NodeCompletionDoorbell | undefined;
13
+ /** Rebuild the process-local native ring in a Node thread worker. */
14
+ export declare const createNodeCompletionNotifier: (pointer: bigint | undefined) => (() => void) | undefined;
@@ -0,0 +1,84 @@
1
+ import { RUNTIME } from "../common/runtime.js";
2
+ import { getNodeBuiltinModule } from "../common/node-compat.js";
3
+ import { loadNodeNativeAddon } from "../connections/node-addons.js";
4
+ const loadDoorbellAddon = () => {
5
+ if (RUNTIME !== "node")
6
+ return undefined;
7
+ const nodeModule = getNodeBuiltinModule("node:module");
8
+ if (nodeModule === undefined)
9
+ return undefined;
10
+ try {
11
+ return loadNodeNativeAddon(nodeModule.createRequire(import.meta.url), "knitting_doorbell");
12
+ }
13
+ catch {
14
+ // Native wakeups are an optional fast path. A missing prebuild must retain
15
+ // the portable Atomics.waitAsync dispatcher rather than preventing plain
16
+ // thread workers from starting.
17
+ return undefined;
18
+ }
19
+ };
20
+ /**
21
+ * Bridge a Node worker thread into the host's libuv I/O phase. The addon owns
22
+ * the `uv_async_t`; this wrapper owns the JS callback and makes missing native
23
+ * prebuilds a normal capability fallback.
24
+ */
25
+ export const createNodeCompletionDoorbell = (notify) => {
26
+ const addon = loadDoorbellAddon();
27
+ if (addon === undefined)
28
+ return undefined;
29
+ let pointer;
30
+ let closed = false;
31
+ try {
32
+ pointer = addon.createCompletionDoorbell(() => {
33
+ if (closed)
34
+ return;
35
+ try {
36
+ notify();
37
+ }
38
+ catch {
39
+ // An exception must not escape a libuv callback. A scheduled host
40
+ // drain or the dispatcher watchdog still provides a liveness backstop.
41
+ }
42
+ });
43
+ if (typeof pointer !== "bigint" || pointer === 0n)
44
+ return undefined;
45
+ // A dormant pool must not be kept alive solely by an idle uv_async_t.
46
+ addon.unrefCompletionDoorbell(pointer);
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ return {
52
+ pointer,
53
+ close: () => {
54
+ if (closed)
55
+ return;
56
+ closed = true;
57
+ try {
58
+ addon.closeCompletionDoorbell(pointer);
59
+ }
60
+ catch {
61
+ // Shutdown can race Node environment teardown; the addon's environment
62
+ // cleanup hook owns the remaining lifetime in that case.
63
+ }
64
+ },
65
+ };
66
+ };
67
+ /** Rebuild the process-local native ring in a Node thread worker. */
68
+ export const createNodeCompletionNotifier = (pointer) => {
69
+ if (RUNTIME !== "node" || pointer === undefined || pointer === 0n) {
70
+ return undefined;
71
+ }
72
+ const addon = loadDoorbellAddon();
73
+ if (addon === undefined)
74
+ return undefined;
75
+ return () => {
76
+ try {
77
+ addon.ringCompletionDoorbell(pointer);
78
+ }
79
+ catch {
80
+ // The host can close while a forced worker teardown is in flight. Its
81
+ // return-lock watchdog covers that final in-flight publication.
82
+ }
83
+ };
84
+ };
@@ -2,28 +2,25 @@ import { createHostTxQueue } from "./tx-queue.js";
2
2
  import { type ProcessSharedMemoryBacking, type ProcessStealMemoryLayout, type ResolvedProcessSharedMemorySettings } from "./process-worker.js";
3
3
  import { type Sab } from "../ipc/transport/shared-memory.js";
4
4
  import { ChannelHandler, type DispatcherCheck } from "./dispatcher.js";
5
- import { lock2, type Task } from "../memory/lock.js";
5
+ import type { DenoCompletionDoorbell } from "./deno-doorbell.js";
6
+ import { lock2, type StealClaimDiscipline, type Task } from "../memory/lock.js";
6
7
  import "../memory/payloadCodec.js";
7
8
  import type { DebugOptions, DispatcherSettings, LockBuffers, WorkerContext, WorkerData, WorkerSettings } from "../types.js";
8
9
  import "../worker/loop.js";
9
10
  import { type PayloadBufferOptions } from "../memory/payload-config.js";
10
- /**
11
- * Build the buffers, host-side locks and pending registry that a stealing pool
12
- * shares. One submit region for every worker to claim from, one private return
13
- * region per worker, and a single pool-global queue so a response arriving on
14
- * any lane settles the right promise.
15
- *
16
- * Region width follows paper §6.1: the largest power-of-two `g` leaving a spare
17
- * region for a delayed claimant (`slots / g >= consumers + 1`). At 32 slots that
18
- * caps how wide a region can be well before it caps the worker count.
19
- */
11
+ /** Build the shared submit and private return buffers for a stealing pool. */
20
12
  export declare const resolveStealRegionLanes: (consumers: number) => number;
13
+ /** Return the widest region allowed by the selected claim discipline. */
14
+ export declare const resolveMaxStealRegionLanes: (consumers: number, stealClaim?: StealClaimDiscipline) => number;
21
15
  /** Maximum claimants that leave the protocol's required spare region. */
22
16
  export declare const MAX_STEAL_CONSUMERS: number;
23
- export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, processWorker, }: {
17
+ export declare const createStealPoolBuffers: ({ threads, payload, sharedArgs, regionLanes, stealClaim, abortSignalCapacity, usesAbortSignal, processWorker, }: {
24
18
  threads: number;
25
19
  payload?: PayloadBufferOptions;
20
+ /** Hand large arguments to workers as borrowed regions. See `unsafe.SharedArgs`. */
21
+ sharedArgs?: boolean;
26
22
  regionLanes?: number;
23
+ stealClaim?: StealClaimDiscipline;
27
24
  abortSignalCapacity?: number;
28
25
  usesAbortSignal?: boolean;
29
26
  processWorker?: {
@@ -53,6 +50,7 @@ export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, a
53
50
  activeRejectPlaceholder?: Task["reject"];
54
51
  }) => () => number;
55
52
  waitForHostChange: (timeoutMs?: number) => import("../memory/lock.js").WaitAsyncResult | undefined;
53
+ armHostNotifier: () => boolean;
56
54
  setHostWaiterArmed: (armed: boolean) => void;
57
55
  hasPendingFrames: () => boolean;
58
56
  getPendingFrameCount: () => number;
@@ -82,6 +80,7 @@ export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, a
82
80
  activeRejectPlaceholder?: Task["reject"];
83
81
  }) => () => number;
84
82
  waitForHostChange: (timeoutMs?: number) => import("../memory/lock.js").WaitAsyncResult | undefined;
83
+ armHostNotifier: () => boolean;
85
84
  setHostWaiterArmed: (armed: boolean) => void;
86
85
  hasPendingFrames: () => boolean;
87
86
  getPendingFrameCount: () => number;
@@ -97,19 +96,20 @@ export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, a
97
96
  txIdle: () => boolean;
98
97
  completeFrame: () => number;
99
98
  waitForCompletion: (onWake: () => void, timeoutMs?: number) => boolean;
99
+ armCompletionNotifier: () => boolean;
100
100
  setCompletionWaiterArmed: (armed: boolean) => void;
101
- setReturnHooks: (lane: number, hooks: (import("../connections/buffer-reference.js").BufferReferenceReturnHooks | ((token: bigint) => void)) | undefined) => void;
102
101
  enqueue: (functionID: number, timeout?: import("../types.js").TaskTimeout, abortSignal?: import("../types.js").AbortSignalOption) => (rawArgs: unknown) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
103
102
  flushToWorker: () => boolean;
104
103
  enqueueKnown: (task: Task) => boolean;
105
104
  settlePromisePayload: (task: Task, isRejected: boolean, value: unknown) => boolean;
106
105
  };
107
106
  regionLanes: number;
107
+ stealClaim: StealClaimDiscipline;
108
108
  processMemory: ProcessStealMemoryLayout;
109
109
  abortSignalSAB: SharedArrayBuffer | import("../types.js").SharedBufferRegion;
110
110
  abortSignalMax: number;
111
111
  };
112
- export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, stealPool, }: {
112
+ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }: {
113
113
  list: string[];
114
114
  ids: number[];
115
115
  names: string[];
@@ -131,7 +131,7 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
131
131
  permission?: WorkerData["permission"];
132
132
  host?: DispatcherSettings;
133
133
  payload?: PayloadBufferOptions;
134
- bufferReferenceReturn?: "copy" | "borrow";
134
+ sharedBytesEnabled?: boolean;
135
135
  abortSignalCapacity?: number;
136
136
  usesAbortSignal?: boolean;
137
137
  /**
@@ -139,6 +139,8 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
139
139
  * macro channel that runs all lane checks.
140
140
  */
141
141
  sharedChannelHandler?: ChannelHandler;
142
+ /** Deno-only native callback shared by every thread worker in this pool. */
143
+ denoCompletionDoorbell?: DenoCompletionDoorbell;
142
144
  /**
143
145
  * Work-stealing wiring. When present the submit region, both host-side locks
144
146
  * and the pending registry are owned by `createPool` and shared: every worker
@@ -155,6 +157,7 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
155
157
  consumers: number;
156
158
  consumerId: number;
157
159
  regionLanes: number;
160
+ stealClaim?: StealClaimDiscipline;
158
161
  abortSignalSAB?: LockBuffers["headers"];
159
162
  abortSignalMax?: number;
160
163
  processMemory?: ProcessStealMemoryLayout;
@@ -169,5 +172,8 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
169
172
  dispatcherCheck?: DispatcherCheck;
170
173
  laneWake?: () => void;
171
174
  bindSend?: (fn: () => void) => void;
175
+ bindCompletionWake?: (fn: () => void) => void;
176
+ processCompletionDoorbell?: boolean;
177
+ nodeCompletionDoorbell?: boolean;
172
178
  });
173
179
  export type CreateContext = WorkerContext;