knitting 0.1.63 → 0.1.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +623 -342
  2. package/knitting.browser.d.ts +3 -1
  3. package/knitting.browser.js +1 -1
  4. package/knitting.d.ts +3 -1
  5. package/knitting.js +2 -1
  6. package/map.md +0 -6
  7. package/package.json +10 -5
  8. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  9. package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
  10. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  11. package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
  12. package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
  14. package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
  15. package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
  16. package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
  18. package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
  19. package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
  20. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  21. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  22. package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
  23. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  24. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  25. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  26. package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
  27. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  28. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  29. package/scripts/build-native-addons.ts +5 -0
  30. package/shared-memory.d.ts +3 -0
  31. package/shared-memory.js +3 -0
  32. package/src/api.js +158 -71
  33. package/src/common/with-resolvers.js +2 -5
  34. package/src/common/worker-runtime.d.ts +7 -0
  35. package/src/common/worker-runtime.js +7 -0
  36. package/src/connections/buffer-reference.d.ts +10 -36
  37. package/src/connections/buffer-reference.js +15 -170
  38. package/src/connections/node-addons.d.ts +1 -1
  39. package/src/connections/node-addons.js +11 -1
  40. package/src/connections/shared-array-buffer-payload.d.ts +7 -0
  41. package/src/connections/shared-array-buffer-payload.js +27 -11
  42. package/src/debug/gate.js +1 -1
  43. package/src/debug/handle.d.ts +6 -1
  44. package/src/debug/handle.js +14 -6
  45. package/src/error.d.ts +9 -0
  46. package/src/error.js +16 -2
  47. package/src/knitting_buffer_pointer.cc +57 -2
  48. package/src/knitting_doorbell.cc +220 -0
  49. package/src/memory/knitting-body.d.ts +44 -0
  50. package/src/memory/knitting-body.js +51 -0
  51. package/src/memory/knitting-buffer-http.d.ts +116 -0
  52. package/src/memory/knitting-buffer-http.js +255 -0
  53. package/src/memory/knitting-buffer.d.ts +250 -0
  54. package/src/memory/knitting-buffer.js +695 -0
  55. package/src/memory/lazy-region-registry.d.ts +83 -0
  56. package/src/memory/lazy-region-registry.js +355 -0
  57. package/src/memory/lock.d.ts +80 -15
  58. package/src/memory/lock.js +473 -139
  59. package/src/memory/payloadCodec.d.ts +18 -2
  60. package/src/memory/payloadCodec.js +340 -76
  61. package/src/memory/regionRegistry.d.ts +6 -0
  62. package/src/memory/regionRegistry.js +125 -240
  63. package/src/memory/shared-buffer-io.d.ts +7 -0
  64. package/src/memory/shared-buffer-io.js +34 -8
  65. package/src/permission/protocol.d.ts +1 -0
  66. package/src/permission/protocol.js +8 -3
  67. package/src/runtime/deno-doorbell.d.ts +26 -0
  68. package/src/runtime/deno-doorbell.js +117 -0
  69. package/src/runtime/dispatcher.d.ts +13 -6
  70. package/src/runtime/dispatcher.js +101 -63
  71. package/src/runtime/host-arg-arena.d.ts +3 -0
  72. package/src/runtime/host-arg-arena.js +16 -0
  73. package/src/runtime/inline-executor.js +2 -1
  74. package/src/runtime/node-doorbell.d.ts +14 -0
  75. package/src/runtime/node-doorbell.js +84 -0
  76. package/src/runtime/pool.d.ts +30 -15
  77. package/src/runtime/pool.js +199 -151
  78. package/src/runtime/process-worker.d.ts +9 -0
  79. package/src/runtime/process-worker.js +32 -3
  80. package/src/runtime/tx-queue.d.ts +4 -6
  81. package/src/runtime/tx-queue.js +63 -48
  82. package/src/runtime/worker-common.d.ts +7 -0
  83. package/src/runtime/worker-common.js +28 -2
  84. package/src/types.d.ts +66 -78
  85. package/src/worker/loop.js +95 -60
  86. package/src/worker/rx-queue.d.ts +2 -3
  87. package/src/worker/rx-queue.js +34 -40
  88. package/src/worker/safety/index.d.ts +1 -1
  89. package/src/worker/safety/index.js +1 -1
  90. package/src/worker/safety/process.d.ts +2 -0
  91. package/src/worker/safety/process.js +8 -1
  92. package/src/worker/safety/startup.js +11 -6
  93. package/src/worker/shared-return.d.ts +9 -0
  94. package/src/worker/shared-return.js +22 -0
  95. package/src/worker/task-loader.js +1 -2
  96. package/src/worker/timers.d.ts +2 -6
  97. package/src/worker/timers.js +39 -22
  98. package/unsafe.d.ts +2 -1
  99. package/unsafe.js +2 -1
@@ -1,23 +1,26 @@
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
- import { assertWorkerImportsResolved, assertWorkerSharedMemoryBootData, installPerformanceNowGuard, installTerminationGuard, installUnhandledRejectionSilencer, scrubWorkerDataSensitiveBuffers, } from "./safety/index.js";
16
+ import { assertWorkerImportsResolved, assertWorkerSharedMemoryBootData, installPerformanceNowGuard, installTerminationGuard, installUnhandledRejectionSilencer, reportSilencedRejections, 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";
22
+ // Park timeout while finished results wait for return slots (see the loop).
23
+ const BLOCKED_RETURN_PARK_MS = 1;
21
24
  const reportWorkerStartupFatal = (error) => {
22
25
  const message = String(error?.message ?? error);
23
26
  const payload = {
@@ -53,38 +56,28 @@ const reportWorkerStartupFatal = (error) => {
53
56
  }
54
57
  }
55
58
  };
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
59
  export const workerMainLoop = async (startupData) => {
72
60
  // Startup-only safety layer: no per-iteration checks in the hot loop.
73
61
  installTerminationGuard();
74
62
  installUnhandledRejectionSilencer();
75
63
  installPerformanceNowGuard();
76
- const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, bufferReferenceReturn, permission, notifyOnHostPublish, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
64
+ const { debug, debugEpoch, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, sharedReturn, permission, notifyOnHostPublish, processCompletionDoorbell, denoCompletionDoorbell, nodeCompletionDoorbell, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
77
65
  scrubWorkerDataSensitiveBuffers(startupData);
78
66
  assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
79
67
  const debugNamespaces = resolveDebugNamespaces(debug);
80
68
  const dbg = debugNamespaces.size > 0
81
- ? await import("../debug/handle.ts").then((module) => module.initDebug({
69
+ ? await import("../debug/handle.js").then((module) => module.initDebug({
82
70
  name: `w${thread}`,
83
71
  runtime: RUNTIME,
84
72
  namespaces: debugNamespaces,
73
+ epoch: debugEpoch,
85
74
  }))
86
75
  : undefined;
87
- // const object, not `enum`: Andromeda's Nova engine can't parse `enum`.
76
+ if (dbg?.enabled("lifecycle") === true) {
77
+ // Detached task promises reject silently otherwise (see the silencer).
78
+ reportSilencedRejections((reason) => dbg.log("lifecycle", `unhandled rejection silenced: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`));
79
+ }
80
+ // Andromeda cannot parse enums.
88
81
  const Comment = {
89
82
  thisIsAHint: 0,
90
83
  };
@@ -110,7 +103,28 @@ export const workerMainLoop = async (startupData) => {
110
103
  consumers: steal?.consumers,
111
104
  consumerId: steal?.consumerId,
112
105
  regionLanes: steal?.regionLanes,
106
+ stealClaim: steal?.claim,
107
+ // Undefined unless `steal` is traced, so the claim path stays untouched.
108
+ traceClaim: steal !== undefined && dbg?.enabled("steal") === true
109
+ ? (message) => dbg.log("steal", message)
110
+ : undefined,
113
111
  });
112
+ const notifyDenoHost = createDenoCompletionNotifier(denoCompletionDoorbell);
113
+ const notifyNodeHost = createNodeCompletionNotifier(nodeCompletionDoorbell);
114
+ const processParentPort = processCompletionDoorbell === true
115
+ ? RUNTIME_PARENT_PORT
116
+ : undefined;
117
+ // Bun on Windows flushes process messages only after an event-loop turn.
118
+ let processRingPending = false;
119
+ const flushProcessRing = IS_BUN_WINDOWS;
120
+ const notifyProcessHost = processParentPort === undefined
121
+ ? undefined
122
+ : flushProcessRing
123
+ ? () => {
124
+ processParentPort.postMessage(PROCESS_COMPLETION_DOORBELL);
125
+ processRingPending = true;
126
+ }
127
+ : () => processParentPort.postMessage(PROCESS_COMPLETION_DOORBELL);
114
128
  const returnLockState = lock2({
115
129
  headers: returnLock.headers,
116
130
  headerSlotStrideU32: returnLock.headerSlotStrideU32,
@@ -120,9 +134,22 @@ export const workerMainLoop = async (startupData) => {
120
134
  payloadConfig,
121
135
  textCompat: returnLock.textCompat,
122
136
  processBoundary: RUNTIME_IS_PROCESS_WORKER,
137
+ sharedReturn: sharedReturn === true,
138
+ // A thread return moves its source before publication. Node transfers the
139
+ // backing-store ownership; Deno and Bun make one owned host copy before
140
+ // the worker drops its pin. Both avoid the borrowed-return lifetime.
141
+ moveReturn: !RUNTIME_IS_PROCESS_WORKER,
123
142
  // The host parks on this lock's publication word when it has no work to
124
143
  // flush. Request locks are host-produced and must not wake that waiter.
125
- notifyOnHostPublish,
144
+ notifyOnHostPublish: notifyOnHostPublish ||
145
+ notifyDenoHost !== undefined ||
146
+ notifyNodeHost !== undefined ||
147
+ notifyProcessHost !== undefined,
148
+ notifyHostPublish: notifyDenoHost !== undefined
149
+ ? () => notifyDenoHost(thread)
150
+ : notifyNodeHost !== undefined
151
+ ? notifyNodeHost
152
+ : notifyProcessHost,
126
153
  });
127
154
  const timers = workerOptions?.timers;
128
155
  const spinMicroseconds = timers?.spinMicroseconds ??
@@ -163,16 +190,16 @@ export const workerMainLoop = async (startupData) => {
163
190
  maxSignals: abortSignalMax,
164
191
  })
165
192
  : undefined;
166
- const { enqueueLock, serviceBatchImmediate, hasCompleted, writeBatch, hasPending, getAwaiting, drainReturnReleases, releaseReturnedBufferReference, } = createWorkerRxQueue({
193
+ const { enqueueLock, serviceBatchImmediate, hasCompleted, writeBatch, getAwaiting, drainReturnReleases, releaseDeferredReturns, } = createWorkerRxQueue({
167
194
  listOfFunctions,
168
195
  workerOptions,
169
196
  lock: lockState,
170
197
  returnLock: returnLockState,
171
- borrowReturnedBufferReferences: bufferReferenceReturn === "borrow",
172
198
  hasAborted: abortSignals?.hasAborted,
173
199
  stealing: steal !== undefined,
174
200
  });
175
- installBufferReferenceReleaseListener(releaseReturnedBufferReference);
201
+ if (sharedReturn === true)
202
+ installSharedReturn(returnLock.payload);
176
203
  a_store(rxStatus, 0, 1);
177
204
  const WRITE_MAX = 64;
178
205
  const pauseUntil = sleepUntilChanged({
@@ -224,9 +251,9 @@ export const workerMainLoop = async (startupData) => {
224
251
  };
225
252
  const traceSignals = dbg?.enabled("signals") === true;
226
253
  const _hasCompleted = hasCompleted;
227
- const _hasPending = hasPending;
228
254
  const _getAwaiting = getAwaiting;
229
255
  const _drainReturnReleases = drainReturnReleases;
256
+ const _releaseDeferredReturns = releaseDeferredReturns;
230
257
  const _pauseSpin = pauseSpin;
231
258
  const _enqueueLock = traceSignals
232
259
  ? () => {
@@ -259,9 +286,24 @@ export const workerMainLoop = async (startupData) => {
259
286
  pauseUntil(value, spinMicroseconds, parkMs);
260
287
  }
261
288
  : pauseUntil;
262
- const flushBeforeClaim = steal !== undefined;
289
+ // Stealing flushes completed work before claiming more; private lanes claim
290
+ // first to keep their single-consumer path short.
291
+ const pump = steal !== undefined
292
+ ? () => {
293
+ let progressed = false;
294
+ if (_hasCompleted() && _writeBatch(WRITE_MAX) > 0)
295
+ progressed = true;
296
+ return _enqueueLock() || progressed;
297
+ }
298
+ : () => {
299
+ const claimed = _enqueueLock();
300
+ return (_hasCompleted() && _writeBatch(WRITE_MAX) > 0) || claimed;
301
+ };
263
302
  /** Leave the dispatch loop and acknowledge shutdown. */
264
303
  const stopLoop = () => {
304
+ // A shutdown rejects unconsumed calls, so no return can still be adopted.
305
+ // Do not let a worker-isolate exit strand native BufferReference pins.
306
+ _releaseDeferredReturns();
265
307
  a_store(stopView, 0, WORKER_STOP.acknowledged);
266
308
  a_store(rxStatus, 0, 0);
267
309
  try {
@@ -272,41 +314,20 @@ export const workerMainLoop = async (startupData) => {
272
314
  };
273
315
  const loop = () => {
274
316
  isInMacro = false;
317
+ // Recompute progress on every pass so idle workers can park.
275
318
  let progressed = true;
276
319
  let awaiting = 0;
277
320
  while (true) {
278
321
  if (stopView[0] !== WORKER_STOP.running)
279
322
  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
- }
323
+ progressed = pump();
303
324
  _drainReturnReleases();
304
- if (_hasPending()) {
305
- if (_serviceBatchImmediate() > 0)
306
- progressed = true;
307
- }
325
+ if (_serviceBatchImmediate() > 0)
326
+ progressed = true;
308
327
  if ((awaiting = _getAwaiting()) > 0) {
309
- if (awaiting !== lastAwaiting)
328
+ // Promise callbacks need the loop to yield; delay only when no progress
329
+ // was made, otherwise queued work pays an unnecessary timer hop.
330
+ if (progressed || awaiting !== lastAwaiting)
310
331
  awaitingSpins = 0;
311
332
  lastAwaiting = awaiting;
312
333
  awaitingSpins++;
@@ -315,12 +336,26 @@ export const workerMainLoop = async (startupData) => {
315
336
  return;
316
337
  }
317
338
  awaitingSpins = lastAwaiting = 0;
339
+ if (processRingPending) {
340
+ processRingPending = false;
341
+ scheduleTimer(0);
342
+ return;
343
+ }
318
344
  if (!progressed) {
319
345
  if (txStatus[Comment.thisIsAHint] === 1) {
320
- _pauseSpin();
346
+ // During a hint spin only host-driven state can change.
347
+ do {
348
+ _pauseSpin();
349
+ if (stopView[0] !== WORKER_STOP.running)
350
+ return stopLoop();
351
+ if (pump())
352
+ break;
353
+ } while (txStatus[Comment.thisIsAHint] === 1);
321
354
  continue;
322
355
  }
323
- _pauseUntil(wakeToken, spinMicroseconds, parkMs);
356
+ // Results that could not be written wait on the host freeing return
357
+ // slots, which rings no worker in particular: keep that park short.
358
+ _pauseUntil(wakeToken, spinMicroseconds, _hasCompleted() ? Math.min(parkMs, BLOCKED_RETURN_PARK_MS) : parkMs);
324
359
  wakeToken = a_load(opView, 0);
325
360
  }
326
361
  }
@@ -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
  };
@@ -1,12 +1,12 @@
1
1
  import RingQueue from "../ipc/tools/ring-queue.js";
2
2
  import { attachPayloadTransportFinalizer, runTaskFinalizers, TaskFlag, TaskIndex, } from "../memory/lock.js";
3
3
  import { composeWorkerRunner } from "./composable-runners.js";
4
- import { BUFFER_REFERENCE_RETURN_RELEASE_TOKEN } from "../connections/buffer-reference.js";
5
- export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, returnLock, borrowReturnedBufferReferences, hasAborted, now, stealing, }) => {
4
+ /** Tasks run per `serviceBatchImmediate` call before returning to the loop. */
5
+ const SERVICE_BATCH_MAX = 32;
6
+ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, returnLock, hasAborted, now, stealing, }) => {
6
7
  const PLACE_HOLDER = (_) => {
7
8
  throw ("UNREACHABLE FROM PLACE HOLDER (thread)");
8
9
  };
9
- let hasAnythingFinished = 0;
10
10
  let awaiting = 0;
11
11
  const jobs = listOfFunctions.reduce((acc, fixed) => (acc.push(fixed.run), acc), []);
12
12
  const toWork = new RingQueue();
@@ -26,12 +26,7 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
26
26
  const returnHostBits = returnLock.hostBits;
27
27
  const returnWorkerBits = returnLock.workerBits;
28
28
  const deferredReleases = [];
29
- const explicitReturnReleases = new Map();
30
- const drainReturnReleases = () => {
31
- if (deferredReleases.length === 0)
32
- return;
33
- if ((a_load(returnHostBits, 0) ^ a_load(returnWorkerBits, 0)) !== 0)
34
- return;
29
+ const releaseDeferredReturns = () => {
35
30
  for (let i = 0; i < deferredReleases.length; i++) {
36
31
  try {
37
32
  deferredReleases[i]();
@@ -42,18 +37,17 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
42
37
  }
43
38
  deferredReleases.length = 0;
44
39
  };
45
- const releaseReturnedBufferReference = (token) => {
46
- const key = token.toString();
47
- const release = explicitReturnReleases.get(key);
48
- if (release === undefined)
40
+ const drainReturnReleases = () => {
41
+ if (deferredReleases.length === 0)
49
42
  return;
50
- explicitReturnReleases.delete(key);
51
- try {
52
- release();
53
- }
54
- catch {
55
- // best effort
56
- }
43
+ // `returnHostBits` is the publication word of the return lock, and only
44
+ // this thread ever writes it (encode() toggles hostBits, the host toggles
45
+ // workerBits). Reading our own word plainly skips an `Atomics.load`, which
46
+ // is a non-inlined call an order of magnitude dearer than a plain read;
47
+ // the peer's word still needs the atomic.
48
+ if ((returnHostBits[0] ^ a_load(returnWorkerBits, 0)) !== 0)
49
+ return;
50
+ releaseDeferredReturns();
57
51
  };
58
52
  const runByIndex = listOfFunctions.reduce((acc, fixed, idx) => {
59
53
  const job = jobs[idx];
@@ -65,9 +59,13 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
65
59
  }));
66
60
  return acc;
67
61
  }, []);
62
+ // A settled task is either written into the return lock straight away or
63
+ // parked in `pendingFrames`, so the queue's own size already answers "is
64
+ // there anything left to flush" — the separate counter this used to keep was
65
+ // an increment and a decrement per task tracking a value it could read.
68
66
  const hasCompleted = workerOptions?.resolveAfterFinishingAll === true
69
- ? () => hasAnythingFinished !== 0 && toWork.size === 0
70
- : () => hasAnythingFinished !== 0;
67
+ ? () => pendingFrames.size !== 0 && toWork.size === 0
68
+ : () => pendingFrames.size !== 0;
71
69
  const { decode, resolved } = lock;
72
70
  const resolvedShift = () => resolved.shiftNoClear();
73
71
  const enqueueLock = () => {
@@ -86,34 +84,22 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
86
84
  }
87
85
  return true;
88
86
  };
89
- const encodeReturnSafe = (slot) => {
90
- if (!returnLock.encode(slot))
91
- return false;
92
- return true;
93
- };
87
+ const returnEncode = returnLock.encode;
94
88
  const sendReturn = (slot, shouldReject) => {
95
89
  slot[IDX_FLAGS] = shouldReject ? FLAG_REJECT : 0;
96
- if (!encodeReturnSafe(slot))
90
+ if (!returnEncode(slot))
97
91
  return false;
98
92
  // Preserve return finalizers past slot recycle; release after drain.
99
93
  if (slot.finalize !== undefined) {
100
- const token = slot.finalize[BUFFER_REFERENCE_RETURN_RELEASE_TOKEN];
101
- if (token === undefined || borrowReturnedBufferReferences !== true) {
102
- deferredReleases.push(slot.finalize);
103
- }
104
- else {
105
- explicitReturnReleases.set(token.toString(), slot.finalize);
106
- }
94
+ deferredReleases.push(slot.finalize);
107
95
  slot.finalize = undefined;
108
96
  }
109
- hasAnythingFinished--;
110
97
  recyclePush(slot);
111
98
  return true;
112
99
  };
113
100
  const settleNow = (slot, isError, value, wasAwaited) => {
114
101
  runTaskFinalizers(slot);
115
102
  slot.value = value;
116
- hasAnythingFinished++;
117
103
  if (wasAwaited && awaiting > 0)
118
104
  awaiting--;
119
105
  const shouldReject = isError ||
@@ -145,14 +131,20 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
145
131
  },
146
132
  serviceBatchImmediate: () => {
147
133
  let processed = 0;
148
- while (processed < 5 && toWork.size !== 0) {
134
+ // Every trip back out to the dispatch loop costs a claim attempt and the
135
+ // per-pass bookkeeping, so a low cap makes that scaffolding a per-task
136
+ // tax on cheap tasks. Run a longer run when the return lock is keeping
137
+ // up, and cut it short the moment a settle lands in `pendingFrames` --
138
+ // that means the return region is full and the right move is to go
139
+ // flush it rather than pile up more finished work.
140
+ while (processed < SERVICE_BATCH_MAX && toWork.size !== 0) {
149
141
  const slot = toWorkShift();
150
142
  try {
151
143
  const fnIndex = slot[TaskIndex.FunctionID] & FUNCTION_ID_MASK;
152
144
  const result = runByIndex[fnIndex](slot);
153
145
  slot[IDX_FLAGS] = 0;
154
- slot.value = null;
155
146
  if (result instanceof Promise) {
147
+ slot.value = null;
156
148
  awaiting++;
157
149
  result.then((value) => settleNow(slot, false, value, true), (err) => settleNow(slot, true, err, true));
158
150
  }
@@ -164,12 +156,14 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
164
156
  settleNow(slot, true, err, false);
165
157
  }
166
158
  ++processed;
159
+ if (pendingFrames.size !== 0)
160
+ break;
167
161
  }
168
162
  return processed;
169
163
  },
170
164
  enqueueLock,
171
165
  drainReturnReleases,
172
- releaseReturnedBufferReference,
166
+ releaseDeferredReturns,
173
167
  hasAwaiting: () => awaiting > 0,
174
168
  getAwaiting: () => awaiting,
175
169
  };
@@ -1,4 +1,4 @@
1
- export { installTerminationGuard, installUnhandledRejectionSilencer, } from "./process.js";
1
+ export { installTerminationGuard, installUnhandledRejectionSilencer, reportSilencedRejections, } from "./process.js";
2
2
  export { installPerformanceNowGuard } from "./performance.js";
3
3
  export { scrubWorkerDataSensitiveBuffers } from "./worker-data.js";
4
4
  export { assertWorkerSharedMemoryBootData, assertWorkerImportsResolved, } from "./startup.js";
@@ -1,4 +1,4 @@
1
- export { installTerminationGuard, installUnhandledRejectionSilencer, } from "./process.js";
1
+ export { installTerminationGuard, installUnhandledRejectionSilencer, reportSilencedRejections, } from "./process.js";
2
2
  export { installPerformanceNowGuard } from "./performance.js";
3
3
  export { scrubWorkerDataSensitiveBuffers } from "./worker-data.js";
4
4
  export { assertWorkerSharedMemoryBootData, assertWorkerImportsResolved, } from "./startup.js";
@@ -1,2 +1,4 @@
1
1
  export declare const installTerminationGuard: () => void;
2
+ /** Debug hook: see what the silencer below swallows. Unset by default. */
3
+ export declare const reportSilencedRejections: (report: ((reason: unknown) => void) | undefined) => void;
2
4
  export declare const installUnhandledRejectionSilencer: () => void;
@@ -66,6 +66,11 @@ export const installTerminationGuard = () => {
66
66
  }
67
67
  proc.__knittingTerminationGuard = true;
68
68
  };
69
+ let reportSilencedRejection;
70
+ /** Debug hook: see what the silencer below swallows. Unset by default. */
71
+ export const reportSilencedRejections = (report) => {
72
+ reportSilencedRejection = report;
73
+ };
69
74
  export const installUnhandledRejectionSilencer = () => {
70
75
  const proc = getNodeProcess();
71
76
  if (!proc || typeof proc.on !== "function") {
@@ -75,5 +80,7 @@ export const installUnhandledRejectionSilencer = () => {
75
80
  return;
76
81
  proc.__knittingUnhandledRejectionSilencer = true;
77
82
  // Worker task code may create detached promises; keep workers alive.
78
- proc.on("unhandledRejection", () => { });
83
+ proc.on("unhandledRejection", (reason) => {
84
+ reportSilencedRejection?.(reason);
85
+ });
79
86
  };
@@ -21,10 +21,15 @@ export const assertWorkerImportsResolved = ({ list, ids, names, listOfFunctions
21
21
  if (listOfFunctions.length > 0 &&
22
22
  (names === undefined || listOfFunctions.length === names.length))
23
23
  return;
24
- console.log(list);
25
- console.log(ids);
26
- if (names !== undefined)
27
- console.log(names);
28
- console.log(listOfFunctions);
29
- throw new Error("No imports were found.");
24
+ const found = new Set(listOfFunctions.map((entry) => entry?.name));
25
+ const missing = (names ?? []).filter((name) => !found.has(name));
26
+ const tasks = missing.length > 0
27
+ ? ` Missing task export(s): ${missing.join(", ")}.`
28
+ : "";
29
+ const modules = list.length > 0 ? ` Looked in: ${list.join(", ")}.` : "";
30
+ throw new Error(`No imports were found.${tasks}${modules}` +
31
+ " Workers re-import each listed module and look tasks up by export" +
32
+ " name: a bare function must be exported from the module that calls" +
33
+ " createPool, and a bundle that inlines task modules drops their" +
34
+ " exports.");
30
35
  };
@@ -0,0 +1,9 @@
1
+ /** Point `sharedBytes` at the arena of `payload`, this worker's return lane. */
2
+ export declare const installSharedReturn: (payload: object) => void;
3
+ export declare const resetSharedReturn: () => void;
4
+ /**
5
+ * Allocate an uninitialized borrowed return buffer. The caller must write every
6
+ * byte it returns and must not retain the view beyond the borrow window.
7
+ * Ordinary allocation is used when borrowing is unavailable.
8
+ */
9
+ export declare const sharedBytes: (byteLength: number, zeroFill?: boolean) => Uint8Array;
@@ -0,0 +1,22 @@
1
+ import { getSharedReturnAllocator } from "../memory/payloadCodec.js";
2
+ // `sharedBytes` uses the worker's return-lane arena; outside that context it
3
+ // falls back to an ordinary Uint8Array.
4
+ let allocate;
5
+ /** Point `sharedBytes` at the arena of `payload`, this worker's return lane. */
6
+ export const installSharedReturn = (payload) => {
7
+ allocate = getSharedReturnAllocator(payload);
8
+ };
9
+ export const resetSharedReturn = () => {
10
+ allocate = undefined;
11
+ };
12
+ /**
13
+ * Allocate an uninitialized borrowed return buffer. The caller must write every
14
+ * byte it returns and must not retain the view beyond the borrow window.
15
+ * Ordinary allocation is used when borrowing is unavailable.
16
+ */
17
+ export const sharedBytes = (byteLength, zeroFill = false) => {
18
+ if (!Number.isInteger(byteLength) || byteLength < 0) {
19
+ throw new RangeError("sharedBytes(byteLength) requires a non-negative integer");
20
+ }
21
+ return allocate?.(byteLength, zeroFill) ?? new Uint8Array(byteLength);
22
+ };
@@ -1,7 +1,6 @@
1
1
  import { endpointSymbol } from "../common/task-symbol.js";
2
2
  import { toImportSpecifier, toModuleUrl } from "../common/module-url.js";
3
- // const object, not `const enum`: Andromeda's Nova engine can't parse `enum`.
4
- // No longer inlined, but only read on cold paths.
3
+ // Use a const object; Andromeda's Nova engine cannot parse `enum`.
5
4
  export const TimeoutKind = {
6
5
  Reject: 0,
7
6
  Resolve: 1,
@@ -2,6 +2,7 @@ type PauseOptions = {
2
2
  pauseInNanoseconds?: number;
3
3
  };
4
4
  export type NativeWaitU32 = (buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, expected: number, timeoutMs?: number) => unknown;
5
+ export declare const IS_BUN_WINDOWS: boolean;
5
6
  export declare const whilePausing: ({ pauseInNanoseconds }: PauseOptions) => () => void;
6
7
  export declare const pauseGeneric: () => void;
7
8
  export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait, flushBeforeClaim, }: {
@@ -14,12 +15,7 @@ export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxSta
14
15
  write?: () => number | boolean;
15
16
  nativeWaitU32?: NativeWaitU32;
16
17
  useSharedMemoryWait?: boolean;
17
- /**
18
- * Stealing only: write results back before attempting to claim more work.
19
- * A claim can spin waiting for a peer to withdraw its intent, so claiming
20
- * first puts arbitration latency in front of a finished response. Measured
21
- * to hurt the per-lane path, so it stays off by default.
22
- */
18
+ /** Stealing only: flush results before claiming more work. */
23
19
  flushBeforeClaim?: boolean;
24
20
  }) => (value: number, spinMicroseconds: number, parkMs?: number) => void;
25
21
  export {};