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
@@ -9,12 +9,7 @@ import { createSharedArrayBuffer, createWasmSharedArrayBuffer, } from "../common
9
9
  import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
10
10
  import { probeLockBufferTextCompat, } from "../common/shared-buffer-text.js";
11
11
  import { resolvePayloadBufferOptions, } from "./payload-config.js";
12
- /**
13
- * TODO: Compose all the instance where the array is passed as argument
14
- */
15
- // const objects replace `enum`s throughout this module: Andromeda's Nova engine
16
- // panics on `enum`. Value access, duplicate-value members, and type preserved;
17
- // identical emit on Node/Deno/Bun.
12
+ // Use const objects instead of enums; Andromeda's Nova engine cannot parse enums.
18
13
  export const PayloadSignal = {
19
14
  UNREACHABLE: 0,
20
15
  BigInt: 2,
@@ -68,6 +63,12 @@ export const PayloadBuffer = {
68
63
  EnvelopeDynamicHeaderStringExternal: 52,
69
64
  NumericArray: 53,
70
65
  StaticNumericArray: 54,
66
+ /** Binary payload already stored in the dynamic arena. */
67
+ ArenaBinary: 55,
68
+ /** A returned Uint8Array whose ArrayBuffer ownership was moved to the host. */
69
+ MovedBinary: 56,
70
+ /** A returned ArrayBuffer whose ownership was moved to the host. */
71
+ MovedArrayBuffer: 57,
71
72
  };
72
73
  // Value -> name lookup (enum reverse-map), for payload-limit error labels.
73
74
  // Later keys win on duplicate values (11 -> "String"), as TS enums do.
@@ -116,6 +117,13 @@ export const addTaskFinalizer = (task, finalizer) => {
116
117
  }
117
118
  };
118
119
  };
120
+ /** Take an owner-lifetime transport hold, if the value supports one. */
121
+ export const takePayloadTransportHold = (value) => {
122
+ if (value === null || typeof value !== "object")
123
+ return undefined;
124
+ const finalizer = value[PayloadTransportFinalizer]?.();
125
+ return typeof finalizer === "function" ? finalizer : undefined;
126
+ };
119
127
  export const attachPayloadTransportFinalizer = (task, value) => {
120
128
  if (task.finalize !== undefined || value === null || typeof value !== "object") {
121
129
  return;
@@ -143,18 +151,19 @@ export const TaskIndex = {
143
151
  /**
144
152
  * Host -> worker request function id (low 16 bits).
145
153
  * High 16 bits are reserved for caller metadata on request path.
146
- * NOTE: shares the same storage word as `FlagsToHost`.
154
+ * Shares storage with `FlagsToHost`.
147
155
  */
148
156
  FunctionID: 0,
149
157
  ID: 1,
150
158
  Type: 2,
151
159
  Start: 3,
152
- End: 4,
153
- PayloadLen: 5,
154
160
  /**
155
- * Low 5 bits: region slot index (0..31).
156
- * High 27 bits: reserved for caller metadata (e.g. enqueue timing).
161
+ * Payload length; bit 31 stores the high bit of a dynamic-region index.
162
+ * Write the length before `tagTaskSlot()` and mask bit 31 when reading it.
157
163
  */
164
+ End: 4,
165
+ PayloadLen: 5,
166
+ /** Low 5 bits hold the dynamic-region index; the remaining bits hold metadata. */
158
167
  slotBuffer: 6,
159
168
  Size: 8,
160
169
  /**
@@ -228,10 +237,9 @@ export const HEADER_STATIC_PAYLOAD_U32 = TaskIndex.TotalBuff -
228
237
  export const HEADER_TASK_OFFSET_IN_SLOT_U32 = HEADER_STATIC_PAYLOAD_U32;
229
238
  // Work-stealing control words live in the unused tail of each slot's header
230
239
  // cache line. Task words occupy 0..7 (`TASK_LOCAL_FLAGS_INDEX = 7`) of the
231
- // 16-word line, leaving words 8..15 free. Consumer `c` owns slot `c`'s pair, and
232
- // because the slot stride is exactly 9 cache lines those pairs land on distinct
233
- // lines. `ACK[c]` and `WANT[c]` share one line on purpose: same single writer,
234
- // and a peer reading one pulls the other for free.
240
+ // 16-word line, leaving words 8..15 free. Consumer `c` owns slot `c`'s words,
241
+ // and the slot stride keeps those words on separate cache lines.
242
+ // The ACK word was removed; retirement is folded into `workerBits`.
235
243
  export const STEAL_ACK_SLOT_OFFSET_U32 = HEADER_TASK_OFFSET_IN_SLOT_U32 +
236
244
  TaskIndex.Size;
237
245
  export const STEAL_WANT_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 1;
@@ -249,6 +257,13 @@ export const STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 2;
249
257
  export const STEAL_LIVE_SLOT_OFFSET_U32 = STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 + 1;
250
258
  /** Host-owned arm word for the return-lock completion doorbell. */
251
259
  export const DOORBELL_ARMED_SLOT_OFFSET_U32 = STEAL_LIVE_SLOT_OFFSET_U32 + 1;
260
+ /** Shared region-owner mask for the `cas-mask` claim discipline. */
261
+ export const STEAL_CLAIM_MASK_SLOT_OFFSET_U32 = DOORBELL_ARMED_SLOT_OFFSET_U32 + 1;
262
+ // A producer claims ARMED before ringing the host. Keeping SIGNALLED until the
263
+ // host drains coalesces a burst of published frames into one native/IPC wake.
264
+ const DOORBELL_OFF = 0;
265
+ const DOORBELL_ARMED = 1;
266
+ const DOORBELL_SIGNALLED = 2;
252
267
  export const HEADER_U32_LENGTH = LockBound.header +
253
268
  (HEADER_SLOT_STRIDE_U32 * LockBound.slots);
254
269
  export const HEADER_BYTE_LENGTH = HEADER_U32_LENGTH *
@@ -288,18 +303,17 @@ const makeTaskFrom = (array, at) => {
288
303
  fillTaskFrom(task, array, at);
289
304
  return task;
290
305
  };
291
- // could be inlined
292
306
  const settleTask = (task) => {
293
307
  if (task[TaskIndex["FlagsToHost"]] === 0) {
294
308
  task.resolve(task.value);
295
309
  }
296
310
  else {
297
311
  task.reject(task.value);
298
- // restarting the flag
312
+ // Clear the rejection marker before the task slot is reused.
299
313
  task[TaskIndex["FlagsToHost"]] = 0;
300
314
  }
301
315
  };
302
- export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, consumers, consumerId, regionLanes, notifyOnHostPublish, }) => {
316
+ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, sharedReturn, moveReturn, consumers, consumerId, regionLanes, stealClaim, notifyOnHostPublish, notifyHostPublish, }) => {
303
317
  // Layout within `lockSectorRegion`:
304
318
  // - hostBits starts at byte 0
305
319
  // - workerBits starts at byte 64
@@ -307,9 +321,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
307
321
  // The remaining two cache lines in the 256-byte sector are reserved for the
308
322
  // payload allocator lock (`PAYLOAD_LOCK_*` at bytes 128 and 192).
309
323
  //
310
- // Important: encode() always toggles `hostBits` and decode/resolveHost always
311
- // toggles `workerBits`, regardless of which thread calls them. This is why
312
- // the "return lock" (worker->host responses) still publishes into `hostBits`.
324
+ // encode toggles hostBits; decode/resolveHost toggles workerBits. The return
325
+ // lock therefore publishes responses through hostBits as well.
313
326
  const lockSectorRegion = toSharedBufferRegion(LockBoundSector ??
314
327
  createWasmSharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH));
315
328
  const LockBoundSAB = lockSectorRegion.sab;
@@ -336,23 +349,22 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
336
349
  headers: headersRegion,
337
350
  payload: payloadSAB,
338
351
  });
339
- // ---- work stealing (multi-consumer) state ----
340
- // With N consumers the pending set is A ^ ACK[0] ^ ... ^ ACK[N-1]; every word
341
- // still has exactly one writer, so no shared writable word is introduced.
352
+ // Work-stealing state. Pending lanes are A ^ ACK[0] ^ ... ^ ACK[N-1].
342
353
  const stealConsumers = Math.max(1, (consumers ?? 1) | 0);
343
354
  const stealEnabled = stealConsumers > 1;
344
- // A lock2 built without `consumerId` is the producer endpoint: it encodes but
345
- // never claims. It still frees payload slots, so it needs an acknowledgement
346
- // word of its own rather than sharing consumer 0's.
355
+ // The producer encodes and frees payload slots but never claims a region.
347
356
  const stealIsProducer = consumerId === undefined;
348
357
  const stealId = (consumerId ?? 0) | 0;
349
358
  const stealRegionLanes = (regionLanes ?? 8) | 0;
350
359
  const stealRegions = (LockBound.slots / stealRegionLanes) | 0;
360
+ const stealClaimMask = stealClaim === "cas-mask";
351
361
  if (stealEnabled) {
352
362
  if (stealRegionLanes < 1 || (stealRegionLanes & (stealRegionLanes - 1)) !== 0) {
353
363
  throw new RangeError("regionLanes must be a power of two");
354
364
  }
355
- if (stealRegions < stealConsumers) {
365
+ // Dekker requires at least one region per live consumer plus a spare.
366
+ // CAS-mask claimants can retry when all regions are owned.
367
+ if (!stealClaimMask && stealRegions < stealConsumers) {
356
368
  throw new RangeError(`regionLanes=${stealRegionLanes} yields ${stealRegions} regions, ` +
357
369
  `too few for ${stealConsumers} consumers`);
358
370
  }
@@ -363,28 +375,24 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
363
375
  // Int32 alias over the headers SAB so the control words get signed bit math
364
376
  // consistent with the rest of the protocol.
365
377
  const stealView = new Int32Array(headersRegion.sab, headersRegion.byteOffset, headersRegion.byteLength >>> 2);
366
- const stealAckIndex = new Int32Array(stealConsumers);
367
378
  const stealWantIndex = new Int32Array(stealConsumers);
368
379
  for (let c = 0; c < stealConsumers; c++) {
369
- const slotBase = (c * headersSlotStride) + LockBound.header;
370
- stealAckIndex[c] = slotBase + STEAL_ACK_SLOT_OFFSET_U32;
371
- stealWantIndex[c] = slotBase + STEAL_WANT_SLOT_OFFSET_U32;
380
+ stealWantIndex[c] = (c * headersSlotStride) + LockBound.header +
381
+ STEAL_WANT_SLOT_OFFSET_U32;
372
382
  }
373
383
  const stealLiveIndex = LockBound.header + STEAL_LIVE_SLOT_OFFSET_U32;
384
+ /** Single shared owner bitmask, used by `cas-mask` only. */
385
+ const stealMaskIndex = LockBound.header + STEAL_CLAIM_MASK_SLOT_OFFSET_U32;
374
386
  const stealAllLiveMask = stealConsumers === 32
375
387
  ? -1
376
388
  : ((1 << stealConsumers) - 1) | 0;
377
389
  if (stealEnabled && stealIsProducer) {
378
390
  Atomics.store(stealView, stealLiveIndex, stealAllLiveMask);
391
+ // A carpet can outlive one pool on a reused buffer; never inherit owners.
392
+ if (stealClaimMask)
393
+ Atomics.store(stealView, stealMaskIndex, 0);
379
394
  }
380
395
  const stealIsLive = (mask, consumer) => (mask & (1 << consumer)) !== 0;
381
- const stealAckXorAll = () => {
382
- let x = 0 | 0;
383
- for (let c = 0; c < stealConsumers; c++) {
384
- x = (x ^ a_load(stealView, stealAckIndex[c])) | 0;
385
- }
386
- return x;
387
- };
388
396
  let promiseHandler;
389
397
  if (registeredEncodePayload === undefined ||
390
398
  registeredDecodePayload === undefined) {
@@ -401,6 +409,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
401
409
  lockSector: payloadLockRegion,
402
410
  textCompat: resolvedTextCompat,
403
411
  processBoundary,
412
+ sharedReturn,
413
+ moveReturn,
404
414
  onPromise: (task, isRejected, value) => {
405
415
  if ((task[TASK_LOCAL_FLAGS_INDEX] & TASK_LOCAL_PROMISE_TRACKED_FLAG) !==
406
416
  0 &&
@@ -432,9 +442,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
432
442
  const resolved = resultList ?? new RingQueue();
433
443
  let deferredCount = 0 | 0;
434
444
  let pendingPromiseCount = 0 | 0;
435
- // Atomics aliases (hot path)
436
445
  const a_load = Atomics.load;
437
446
  const a_store = Atomics.store;
447
+ const a_compareExchange = Atomics.compareExchange;
438
448
  const a_notify = Atomics.notify;
439
449
  const shouldNotifyHostPublish = notifyOnHostPublish === true;
440
450
  const a_waitAsync = typeof Atomics.waitAsync === "function"
@@ -442,15 +452,13 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
442
452
  : undefined;
443
453
  // Sender-side cached shadow of the receiver-owned queue word. Under the XSC
444
454
  // false-busy-only sender-side staleness property, this may hide newly freed
445
- // lanes but cannot make a genuinely pending lane appear free. Refresh only
455
+ // lanes but cannot make a pending lane appear free. Refresh only
446
456
  // when the cached free set is exhausted.
447
457
  let workerShadow = 0 | 0;
448
- const refreshWorkerShadow = stealEnabled
449
- ? () => workerShadow = stealAckXorAll()
450
- : () => workerShadow = a_load(workerBits, 0) | 0;
458
+ // Under stealing, workerBits also records retired lanes.
459
+ const refreshWorkerShadow = () => workerShadow = a_load(workerBits, 0) | 0;
451
460
  refreshWorkerShadow();
452
461
  const ensureSenderStateHasFree = (state) => (~state) !== 0 ? state : (LastLocal ^ refreshWorkerShadow()) | 0;
453
- // RingQueue method aliases (hot path)
454
462
  const toBeSentPush = (task) => toBeSent.push(task);
455
463
  const toBeSentShift = () => toBeSent.shiftNoClear();
456
464
  const toBeSentUnshift = (task) => toBeSent.unshift(task);
@@ -574,8 +582,22 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
574
582
  };
575
583
  const storeHost = (bit) => {
576
584
  a_store(hostBits, 0, LastLocal = (LastLocal ^ bit) | 0);
577
- if (shouldNotifyHostPublish && a_load(doorbellArmed, 0) !== 0) {
578
- a_notify(hostBits, 0, 1);
585
+ if (shouldNotifyHostPublish &&
586
+ a_compareExchange(doorbellArmed, 0, DOORBELL_ARMED, DOORBELL_SIGNALLED) === DOORBELL_ARMED) {
587
+ if (notifyHostPublish !== undefined) {
588
+ try {
589
+ notifyHostPublish();
590
+ }
591
+ catch {
592
+ // A callback can be unavailable after forced worker teardown. Restore
593
+ // the arm only when it still belongs to this publication, so a later
594
+ // result can retry without overwriting a host-side disarm/re-arm.
595
+ a_compareExchange(doorbellArmed, 0, DOORBELL_SIGNALLED, DOORBELL_ARMED);
596
+ }
597
+ }
598
+ else {
599
+ a_notify(hostBits, 0, 1);
600
+ }
579
601
  }
580
602
  };
581
603
  const storeWorker = (bit) => a_store(workerBits, 0, LastWorker = (LastWorker ^ bit) | 0);
@@ -618,9 +640,6 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
618
640
  return true;
619
641
  };
620
642
  const hasSpace = () => (hostBits[0] ^ LastWorker) !== 0;
621
- /**
622
- * WORKER SIDE: decode
623
- */
624
643
  const decode = () => {
625
644
  let diff = (a_load(hostBits, 0) ^ LastWorker) | 0;
626
645
  if (diff === 0)
@@ -651,28 +670,20 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
651
670
  lastTake = last;
652
671
  return true;
653
672
  };
654
- /**
655
- * WORKER SIDE: decode, region-Dekker stealing variant.
656
- *
657
- * Claims one whole region of `stealRegionLanes` lanes per handshake, so the
658
- * StoreLoad barrier is amortised over the region rather than paid per task.
659
- * Priority is fixed by endpoint id: a junior withdraws for a senior, so at
660
- * most one consumer ever acknowledges a published generation.
661
- *
662
- * The C reference needs an explicit mfence between the intent store and the
663
- * peer loads. `Atomics.store` is sequentially consistent in JS, so that
664
- * barrier is implicit here and holds on x86 and ARM alike.
665
- */
666
673
  const stealLaneMask = (region) => stealRegionLanes === 32
667
674
  ? -1
668
675
  : ((((1 << stealRegionLanes) - 1) << (region * stealRegionLanes)) | 0);
669
- const stealPeerAcks = () => {
670
- let x = 0 | 0;
671
- for (let c = 0; c < stealConsumers; c++) {
672
- if (c !== stealId)
673
- x = (x ^ a_load(stealView, stealAckIndex[c])) | 0;
674
- }
675
- return x;
676
+ const stealRegionBits = stealRegionLanes === 32
677
+ ? -1
678
+ : ((1 << stealRegionLanes) - 1) | 0;
679
+ /** Find the oldest pending lane in a claimed region. */
680
+ const stealOldestLane = (region, take) => {
681
+ const low = (region * stealRegionLanes) | 0;
682
+ const lanes = ((take >>> low) & stealRegionBits) | 0;
683
+ const predecessors = ((lanes >>> 1) | (lanes << (stealRegionLanes - 1))) & stealRegionBits;
684
+ const oldest = (lanes & ~predecessors) | 0;
685
+ return low +
686
+ (oldest === 0 ? stealRegionLanes - 1 : 31 - clz32(oldest >>> 0));
676
687
  };
677
688
  const stealJuniorWants = (intent) => {
678
689
  const live = a_load(stealView, stealLiveIndex) | 0;
@@ -684,9 +695,12 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
684
695
  }
685
696
  return false;
686
697
  };
687
- let stealCursor = ((stealRegions * stealId) / stealConsumers) | 0;
698
+ const stealHome = ((stealRegions * stealId) / stealConsumers) | 0;
699
+ let stealCursor = stealHome;
700
+ /** Decode one region using the Dekker claim protocol. */
688
701
  const decodeSteal = () => {
689
- const pending = (a_load(hostBits, 0) ^ LastWorker ^ stealPeerAcks()) | 0;
702
+ // workerBits includes retired lanes under stealing.
703
+ const pending = (a_load(hostBits, 0) ^ a_load(workerBits, 0)) | 0;
690
704
  if (pending === 0)
691
705
  return false;
692
706
  let pendingRegions = 0 | 0;
@@ -699,6 +713,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
699
713
  }
700
714
  }
701
715
  const liveBeforeClaim = a_load(stealView, stealLiveIndex) | 0;
716
+ // Prefer regions no live peer currently wants; the handshake below provides
717
+ // the actual mutual exclusion.
702
718
  let peerIntent = 0 | 0;
703
719
  let seniorIntent = 0 | 0;
704
720
  for (let c = 0; c < stealConsumers; c++) {
@@ -726,50 +742,138 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
726
742
  return false;
727
743
  const intent = (1 << region) | 0;
728
744
  a_store(stealView, stealWantIndex[stealId], intent);
729
- // Dekker StoreLoad: implicit, `Atomics.store` above is seq-cst.
745
+ // Atomics.store is sequentially consistent, so it supplies the StoreLoad
746
+ // barrier required by Dekker.
747
+ // A senior claimant has priority.
730
748
  let seniorConflict = false;
731
- let juniorConflict = false;
732
- for (let c = 0; c < stealConsumers; c++) {
733
- if (c === stealId || !stealIsLive(liveBeforeClaim, c))
749
+ for (let c = 0; c < stealId; c++) {
750
+ if (!stealIsLive(liveBeforeClaim, c))
734
751
  continue;
735
- if ((a_load(stealView, stealWantIndex[c]) & intent) === 0)
736
- continue;
737
- if (c < stealId)
752
+ if ((a_load(stealView, stealWantIndex[c]) & intent) !== 0) {
738
753
  seniorConflict = true;
739
- else
740
- juniorConflict = true;
754
+ break;
755
+ }
741
756
  }
742
757
  if (seniorConflict) {
743
758
  a_store(stealView, stealWantIndex[stealId], 0);
744
759
  return false;
745
760
  }
761
+ let juniorConflict = false;
762
+ for (let c = stealId + 1; c < stealConsumers; c++) {
763
+ if (!stealIsLive(liveBeforeClaim, c))
764
+ continue;
765
+ if ((a_load(stealView, stealWantIndex[c]) & intent) !== 0) {
766
+ juniorConflict = true;
767
+ break;
768
+ }
769
+ }
746
770
  if (juniorConflict) {
747
771
  while (stealJuniorWants(intent)) { /* junior withdraws */ }
748
772
  }
749
- // Last control loads before ownership. After this no control word is read
750
- // until every slot in `take` has been decoded.
751
- const take = ((a_load(hostBits, 0) ^ LastWorker ^ stealPeerAcks()) &
773
+ // No control word is read again until all selected slots are decoded.
774
+ const take = ((a_load(hostBits, 0) ^ a_load(workerBits, 0)) &
752
775
  stealLaneMask(region)) | 0;
753
776
  if (take === 0) {
754
777
  a_store(stealView, stealWantIndex[stealId], 0);
755
778
  return false;
756
779
  }
780
+ // Retire lanes and release the claim even if decoding throws.
757
781
  let lanes = take;
758
- while (lanes !== 0) {
759
- decodeAt(31 - clz32((lanes & -lanes) >>> 0));
760
- lanes = (lanes & (lanes - 1)) | 0;
761
- }
762
- // Retire the whole claimed mask with one ACK store, then release the region.
763
- // ACK before intent-clear (paper Assumption 2(ii)).
764
- LastWorker = (LastWorker ^ take) | 0;
765
- a_store(stealView, stealAckIndex[stealId], LastWorker);
766
- a_store(stealView, stealWantIndex[stealId], 0);
767
- stealCursor = (region + 1) % stealRegions;
782
+ let done = 0 | 0;
783
+ // Decode the claimed lanes in producer order.
784
+ let at = stealOldestLane(region, take);
785
+ const regionLow = (region * stealRegionLanes) | 0;
786
+ const regionHigh = (regionLow + stealRegionLanes - 1) | 0;
787
+ try {
788
+ for (let step = 0; step < stealRegionLanes; step++) {
789
+ const bit = (1 << at) | 0;
790
+ if ((lanes & bit) !== 0) {
791
+ decodeAt(at);
792
+ lanes = (lanes ^ bit) | 0;
793
+ done = (done ^ bit) | 0;
794
+ if (lanes === 0)
795
+ break;
796
+ }
797
+ at = at === regionLow ? regionHigh : at - 1;
798
+ }
799
+ }
800
+ finally {
801
+ // Publish the ACK before clearing intent.
802
+ LastWorker = (LastWorker ^ done) | 0;
803
+ if (done !== 0)
804
+ Atomics.xor(workerBits, 0, done);
805
+ a_store(stealView, stealWantIndex[stealId], 0);
806
+ stealCursor = stealHome;
807
+ }
808
+ return true;
809
+ };
810
+ /** Claim one region in the shared owner mask. */
811
+ const decodeStealCasMask = () => {
812
+ const pending = (a_load(hostBits, 0) ^ a_load(workerBits, 0)) | 0;
813
+ if (pending === 0)
814
+ return false;
815
+ let owners = a_load(stealView, stealMaskIndex) | 0;
816
+ let region = -1;
817
+ let intent = 0 | 0;
818
+ for (let step = 0; step < stealRegions && region < 0; step++) {
819
+ const candidate = stealCursor + step < stealRegions
820
+ ? stealCursor + step
821
+ : stealCursor + step - stealRegions;
822
+ if ((pending & stealLaneMask(candidate)) === 0)
823
+ continue;
824
+ const bit = (1 << candidate) | 0;
825
+ // Retry a changed mask once before trying another region.
826
+ for (let attempt = 0; attempt < 2; attempt++) {
827
+ if ((owners & bit) !== 0)
828
+ break;
829
+ const previous = a_compareExchange(stealView, stealMaskIndex, owners, (owners | bit) | 0) | 0;
830
+ if (previous === owners) {
831
+ region = candidate;
832
+ intent = bit;
833
+ break;
834
+ }
835
+ owners = previous;
836
+ }
837
+ }
838
+ if (region < 0)
839
+ return false;
840
+ a_store(stealView, stealWantIndex[stealId], intent);
841
+ const take = ((a_load(hostBits, 0) ^ a_load(workerBits, 0)) &
842
+ stealLaneMask(region)) | 0;
843
+ if (take === 0) {
844
+ a_store(stealView, stealWantIndex[stealId], 0);
845
+ Atomics.and(stealView, stealMaskIndex, ~intent);
846
+ return false;
847
+ }
848
+ let lanes = take;
849
+ let done = 0 | 0;
850
+ // Decode the claimed lanes in producer order.
851
+ let at = stealOldestLane(region, take);
852
+ const regionLow = (region * stealRegionLanes) | 0;
853
+ const regionHigh = (regionLow + stealRegionLanes - 1) | 0;
854
+ try {
855
+ for (let step = 0; step < stealRegionLanes; step++) {
856
+ const bit = (1 << at) | 0;
857
+ if ((lanes & bit) !== 0) {
858
+ decodeAt(at);
859
+ lanes = (lanes ^ bit) | 0;
860
+ done = (done ^ bit) | 0;
861
+ if (lanes === 0)
862
+ break;
863
+ }
864
+ at = at === regionLow ? regionHigh : at - 1;
865
+ }
866
+ }
867
+ finally {
868
+ LastWorker = (LastWorker ^ done) | 0;
869
+ if (done !== 0)
870
+ Atomics.xor(workerBits, 0, done);
871
+ a_store(stealView, stealWantIndex[stealId], 0);
872
+ Atomics.and(stealView, stealMaskIndex, ~intent);
873
+ stealCursor = stealHome;
874
+ }
768
875
  return true;
769
876
  };
770
- /**
771
- * HOST SIDE: decode version
772
- */
773
877
  const resolveHost = ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }) => {
774
878
  const getTask = takeTask({ queue });
775
879
  let lastResolved = 32;
@@ -895,27 +999,21 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
895
999
  return modified;
896
1000
  };
897
1001
  };
898
- /**
899
- * HOST SIDE: wait until the producer changes hostBits.
900
- *
901
- * LastWorker is the host's acknowledgement shadow. Passing it as the
902
- * expected value makes the arm race-free: a publication between the drain
903
- * and this call returns `not-equal` synchronously instead of being lost.
904
- */
1002
+ /** Wait for a producer publication without losing an intervening wakeup. */
905
1003
  const waitForHostChange = (timeoutMs) => {
906
1004
  if (a_waitAsync === undefined) {
907
- a_store(doorbellArmed, 0, 0);
1005
+ a_store(doorbellArmed, 0, DOORBELL_OFF);
908
1006
  return undefined;
909
1007
  }
910
- a_store(doorbellArmed, 0, 1);
1008
+ a_store(doorbellArmed, 0, DOORBELL_ARMED);
911
1009
  try {
912
1010
  const wait = a_waitAsync(hostBits, 0, LastWorker | 0, timeoutMs);
913
1011
  if (!wait.async)
914
- a_store(doorbellArmed, 0, 0);
1012
+ a_store(doorbellArmed, 0, DOORBELL_OFF);
915
1013
  return wait;
916
1014
  }
917
1015
  catch {
918
- a_store(doorbellArmed, 0, 0);
1016
+ a_store(doorbellArmed, 0, DOORBELL_OFF);
919
1017
  // Some runtimes reject particular SharedArrayBuffer implementations
920
1018
  // (for example a growable or native-backed buffer). Fall back to the
921
1019
  // existing dispatcher rather than turning a capability issue into a
@@ -923,6 +1021,18 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
923
1021
  return undefined;
924
1022
  }
925
1023
  };
1024
+ /**
1025
+ * Arm a native host notifier with the same no-lost-wakeup property as
1026
+ * waitAsync: a publication before or during the arm is observed directly,
1027
+ * while one after the observation sees the shared armed bit and rings.
1028
+ */
1029
+ const armHostNotifier = () => {
1030
+ a_store(doorbellArmed, 0, DOORBELL_ARMED);
1031
+ if (a_load(hostBits, 0) === (LastWorker | 0))
1032
+ return true;
1033
+ a_store(doorbellArmed, 0, DOORBELL_OFF);
1034
+ return false;
1035
+ };
926
1036
  const decodeAt = (at) => {
927
1037
  const off = (at * headersSlotStride) + slotBaseU32;
928
1038
  const recycled = recycleShift();
@@ -980,6 +1090,11 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
980
1090
  }
981
1091
  const bit = 1 << id;
982
1092
  const previous = Atomics.and(stealView, stealLiveIndex, ~bit);
1093
+ if (stealClaimMask) {
1094
+ const stranded = a_load(stealView, stealWantIndex[id]) | 0;
1095
+ if (stranded !== 0)
1096
+ Atomics.and(stealView, stealMaskIndex, ~stranded);
1097
+ }
983
1098
  return (previous & bit) !== 0;
984
1099
  };
985
1100
  return {
@@ -989,7 +1104,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
989
1104
  encodeAll,
990
1105
  publish,
991
1106
  flushPending,
992
- decode: stealEnabled ? decodeSteal : decode,
1107
+ decode: stealEnabled
1108
+ ? (stealClaimMask ? decodeStealCasMask : decodeSteal)
1109
+ : decode,
993
1110
  hasSpace,
994
1111
  resolved,
995
1112
  hostBits,
@@ -997,8 +1114,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
997
1114
  recyclecList,
998
1115
  resolveHost,
999
1116
  waitForHostChange,
1117
+ armHostNotifier,
1000
1118
  setHostWaiterArmed: (armed) => {
1001
- a_store(doorbellArmed, 0, armed ? 1 : 0);
1119
+ a_store(doorbellArmed, 0, armed ? DOORBELL_ARMED : DOORBELL_OFF);
1002
1120
  },
1003
1121
  hasPendingFrames: () => toBeSent.size !== 0,
1004
1122
  getPendingFrameCount: () => toBeSent.size | 0,
@@ -1,12 +1,18 @@
1
1
  import { type PromisePayloadHandler, type Task } from "./lock.js";
2
2
  import type { LockBufferTextCompat } from "../common/shared-buffer-text.js";
3
3
  import { type PayloadBufferOptions } from "./payload-config.js";
4
- import type { SharedBufferSource } from "../common/shared-buffer-region.js";
4
+ import { type SharedBufferSource } from "../common/shared-buffer-region.js";
5
+ /** Minimum size for automatically borrowed byte returns. */
6
+ export declare const SHARED_RETURN_MIN_BYTES: number;
7
+ /** Number of later borrowed returns kept before a region is recycled. */
8
+ export declare const SHARED_RETURN_BORROW_WINDOW = 32;
9
+ /** The borrowed-region allocator for `payload`, if an encoder built one. */
10
+ export declare const getSharedReturnAllocator: (payload: object) => ((byteLength: number, zeroFill?: boolean) => Uint8Array | undefined) | undefined;
5
11
  /**
6
12
  * Returns `true` when the payload is encoded successfully.
7
13
  * Returns `false` when dynamic payload space could not be reserved.
8
14
  */
9
- export declare const encodePayload: ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, onPromise, processBoundary, }: {
15
+ export declare const encodePayload: ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, onPromise, processBoundary, sharedReturn, moveReturn, }: {
10
16
  lockSector?: SharedBufferSource;
11
17
  payload?: {
12
18
  sab?: SharedBufferSource;
@@ -29,6 +35,16 @@ export declare const encodePayload: ({ lockSector, payload, sab, payloadConfig,
29
35
  * Process transports must set this on both encoders and decoders.
30
36
  */
31
37
  processBoundary?: boolean;
38
+ /**
39
+ * Enable explicit `sharedBytes()` allocations on a worker return lane.
40
+ * Ordinary byte returns never become borrowed implicitly.
41
+ */
42
+ sharedReturn?: boolean;
43
+ /**
44
+ * Move large top-level byte returns into an owned host ArrayBuffer. Unlike a
45
+ * borrowed return, the result stays valid until the host drops it.
46
+ */
47
+ moveReturn?: boolean;
32
48
  }) => (task: Task, slotIndex: number) => boolean;
33
49
  export declare const decodePayload: ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, host, processBoundary, }: {
34
50
  lockSector?: SharedBufferSource;