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
@@ -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
  /**
@@ -248,6 +257,13 @@ export const STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 2;
248
257
  export const STEAL_LIVE_SLOT_OFFSET_U32 = STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 + 1;
249
258
  /** Host-owned arm word for the return-lock completion doorbell. */
250
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;
251
267
  export const HEADER_U32_LENGTH = LockBound.header +
252
268
  (HEADER_SLOT_STRIDE_U32 * LockBound.slots);
253
269
  export const HEADER_BYTE_LENGTH = HEADER_U32_LENGTH *
@@ -287,18 +303,17 @@ const makeTaskFrom = (array, at) => {
287
303
  fillTaskFrom(task, array, at);
288
304
  return task;
289
305
  };
290
- // could be inlined
291
306
  const settleTask = (task) => {
292
307
  if (task[TaskIndex["FlagsToHost"]] === 0) {
293
308
  task.resolve(task.value);
294
309
  }
295
310
  else {
296
311
  task.reject(task.value);
297
- // restarting the flag
312
+ // Clear the rejection marker before the task slot is reused.
298
313
  task[TaskIndex["FlagsToHost"]] = 0;
299
314
  }
300
315
  };
301
- 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, }) => {
302
317
  // Layout within `lockSectorRegion`:
303
318
  // - hostBits starts at byte 0
304
319
  // - workerBits starts at byte 64
@@ -306,9 +321,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
306
321
  // The remaining two cache lines in the 256-byte sector are reserved for the
307
322
  // payload allocator lock (`PAYLOAD_LOCK_*` at bytes 128 and 192).
308
323
  //
309
- // Important: encode() always toggles `hostBits` and decode/resolveHost always
310
- // toggles `workerBits`, regardless of which thread calls them. This is why
311
- // 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.
312
326
  const lockSectorRegion = toSharedBufferRegion(LockBoundSector ??
313
327
  createWasmSharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH));
314
328
  const LockBoundSAB = lockSectorRegion.sab;
@@ -335,23 +349,22 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
335
349
  headers: headersRegion,
336
350
  payload: payloadSAB,
337
351
  });
338
- // ---- work stealing (multi-consumer) state ----
339
- // With N consumers the pending set is A ^ ACK[0] ^ ... ^ ACK[N-1]; every word
340
- // 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].
341
353
  const stealConsumers = Math.max(1, (consumers ?? 1) | 0);
342
354
  const stealEnabled = stealConsumers > 1;
343
- // A lock2 built without `consumerId` is the producer endpoint: it encodes but
344
- // never claims. It still frees payload slots, so it needs an acknowledgement
345
- // word of its own rather than sharing consumer 0's.
355
+ // The producer encodes and frees payload slots but never claims a region.
346
356
  const stealIsProducer = consumerId === undefined;
347
357
  const stealId = (consumerId ?? 0) | 0;
348
358
  const stealRegionLanes = (regionLanes ?? 8) | 0;
349
359
  const stealRegions = (LockBound.slots / stealRegionLanes) | 0;
360
+ const stealClaimMask = stealClaim === "cas-mask";
350
361
  if (stealEnabled) {
351
362
  if (stealRegionLanes < 1 || (stealRegionLanes & (stealRegionLanes - 1)) !== 0) {
352
363
  throw new RangeError("regionLanes must be a power of two");
353
364
  }
354
- 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) {
355
368
  throw new RangeError(`regionLanes=${stealRegionLanes} yields ${stealRegions} regions, ` +
356
369
  `too few for ${stealConsumers} consumers`);
357
370
  }
@@ -368,11 +381,16 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
368
381
  STEAL_WANT_SLOT_OFFSET_U32;
369
382
  }
370
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;
371
386
  const stealAllLiveMask = stealConsumers === 32
372
387
  ? -1
373
388
  : ((1 << stealConsumers) - 1) | 0;
374
389
  if (stealEnabled && stealIsProducer) {
375
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);
376
394
  }
377
395
  const stealIsLive = (mask, consumer) => (mask & (1 << consumer)) !== 0;
378
396
  let promiseHandler;
@@ -391,6 +409,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
391
409
  lockSector: payloadLockRegion,
392
410
  textCompat: resolvedTextCompat,
393
411
  processBoundary,
412
+ sharedReturn,
413
+ moveReturn,
394
414
  onPromise: (task, isRejected, value) => {
395
415
  if ((task[TASK_LOCAL_FLAGS_INDEX] & TASK_LOCAL_PROMISE_TRACKED_FLAG) !==
396
416
  0 &&
@@ -422,9 +442,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
422
442
  const resolved = resultList ?? new RingQueue();
423
443
  let deferredCount = 0 | 0;
424
444
  let pendingPromiseCount = 0 | 0;
425
- // Atomics aliases (hot path)
426
445
  const a_load = Atomics.load;
427
446
  const a_store = Atomics.store;
447
+ const a_compareExchange = Atomics.compareExchange;
428
448
  const a_notify = Atomics.notify;
429
449
  const shouldNotifyHostPublish = notifyOnHostPublish === true;
430
450
  const a_waitAsync = typeof Atomics.waitAsync === "function"
@@ -432,14 +452,13 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
432
452
  : undefined;
433
453
  // Sender-side cached shadow of the receiver-owned queue word. Under the XSC
434
454
  // false-busy-only sender-side staleness property, this may hide newly freed
435
- // lanes but cannot make a genuinely pending lane appear free. Refresh only
455
+ // lanes but cannot make a pending lane appear free. Refresh only
436
456
  // when the cached free set is exhausted.
437
457
  let workerShadow = 0 | 0;
438
458
  // Under stealing, workerBits also records retired lanes.
439
459
  const refreshWorkerShadow = () => workerShadow = a_load(workerBits, 0) | 0;
440
460
  refreshWorkerShadow();
441
461
  const ensureSenderStateHasFree = (state) => (~state) !== 0 ? state : (LastLocal ^ refreshWorkerShadow()) | 0;
442
- // RingQueue method aliases (hot path)
443
462
  const toBeSentPush = (task) => toBeSent.push(task);
444
463
  const toBeSentShift = () => toBeSent.shiftNoClear();
445
464
  const toBeSentUnshift = (task) => toBeSent.unshift(task);
@@ -563,8 +582,22 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
563
582
  };
564
583
  const storeHost = (bit) => {
565
584
  a_store(hostBits, 0, LastLocal = (LastLocal ^ bit) | 0);
566
- if (shouldNotifyHostPublish && a_load(doorbellArmed, 0) !== 0) {
567
- 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
+ }
568
601
  }
569
602
  };
570
603
  const storeWorker = (bit) => a_store(workerBits, 0, LastWorker = (LastWorker ^ bit) | 0);
@@ -607,9 +640,6 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
607
640
  return true;
608
641
  };
609
642
  const hasSpace = () => (hostBits[0] ^ LastWorker) !== 0;
610
- /**
611
- * WORKER SIDE: decode
612
- */
613
643
  const decode = () => {
614
644
  let diff = (a_load(hostBits, 0) ^ LastWorker) | 0;
615
645
  if (diff === 0)
@@ -640,21 +670,21 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
640
670
  lastTake = last;
641
671
  return true;
642
672
  };
643
- /**
644
- * WORKER SIDE: decode, region-Dekker stealing variant.
645
- *
646
- * Claims one whole region of `stealRegionLanes` lanes per handshake, so the
647
- * StoreLoad barrier is amortised over the region rather than paid per task.
648
- * Priority is fixed by endpoint id: a junior withdraws for a senior, so at
649
- * most one consumer ever acknowledges a published generation.
650
- *
651
- * The C reference needs an explicit mfence between the intent store and the
652
- * peer loads. `Atomics.store` is sequentially consistent in JS, so that
653
- * barrier is implicit here and holds on x86 and ARM alike.
654
- */
655
673
  const stealLaneMask = (region) => stealRegionLanes === 32
656
674
  ? -1
657
675
  : ((((1 << stealRegionLanes) - 1) << (region * stealRegionLanes)) | 0);
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));
687
+ };
658
688
  const stealJuniorWants = (intent) => {
659
689
  const live = a_load(stealView, stealLiveIndex) | 0;
660
690
  for (let c = stealId + 1; c < stealConsumers; c++) {
@@ -667,6 +697,7 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
667
697
  };
668
698
  const stealHome = ((stealRegions * stealId) / stealConsumers) | 0;
669
699
  let stealCursor = stealHome;
700
+ /** Decode one region using the Dekker claim protocol. */
670
701
  const decodeSteal = () => {
671
702
  // workerBits includes retired lanes under stealing.
672
703
  const pending = (a_load(hostBits, 0) ^ a_load(workerBits, 0)) | 0;
@@ -682,6 +713,8 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
682
713
  }
683
714
  }
684
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.
685
718
  let peerIntent = 0 | 0;
686
719
  let seniorIntent = 0 | 0;
687
720
  for (let c = 0; c < stealConsumers; c++) {
@@ -709,59 +742,138 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
709
742
  return false;
710
743
  const intent = (1 << region) | 0;
711
744
  a_store(stealView, stealWantIndex[stealId], intent);
712
- // 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.
713
748
  let seniorConflict = false;
714
- let juniorConflict = false;
715
- for (let c = 0; c < stealConsumers; c++) {
716
- if (c === stealId || !stealIsLive(liveBeforeClaim, c))
749
+ for (let c = 0; c < stealId; c++) {
750
+ if (!stealIsLive(liveBeforeClaim, c))
717
751
  continue;
718
- if ((a_load(stealView, stealWantIndex[c]) & intent) === 0)
719
- continue;
720
- if (c < stealId)
752
+ if ((a_load(stealView, stealWantIndex[c]) & intent) !== 0) {
721
753
  seniorConflict = true;
722
- else
723
- juniorConflict = true;
754
+ break;
755
+ }
724
756
  }
725
757
  if (seniorConflict) {
726
758
  a_store(stealView, stealWantIndex[stealId], 0);
727
759
  return false;
728
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
+ }
729
770
  if (juniorConflict) {
730
771
  while (stealJuniorWants(intent)) { /* junior withdraws */ }
731
772
  }
732
- // Last control loads before ownership. After this no control word is read
733
- // until every slot in `take` has been decoded.
773
+ // No control word is read again until all selected slots are decoded.
734
774
  const take = ((a_load(hostBits, 0) ^ a_load(workerBits, 0)) &
735
775
  stealLaneMask(region)) | 0;
736
776
  if (take === 0) {
737
777
  a_store(stealView, stealWantIndex[stealId], 0);
738
778
  return false;
739
779
  }
740
- // Retire decoded lanes and release the region even if decoding throws.
780
+ // Retire lanes and release the claim even if decoding throws.
741
781
  let lanes = take;
742
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;
743
787
  try {
744
- while (lanes !== 0) {
745
- const bit = (lanes & -lanes) | 0;
746
- decodeAt(31 - clz32(bit >>> 0));
747
- lanes = (lanes & (lanes - 1)) | 0;
748
- done = (done ^ bit) | 0;
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;
749
798
  }
750
799
  }
751
800
  finally {
752
- // ACK before clearing intent.
801
+ // Publish the ACK before clearing intent.
753
802
  LastWorker = (LastWorker ^ done) | 0;
754
803
  if (done !== 0)
755
804
  Atomics.xor(workerBits, 0, done);
756
805
  a_store(stealView, stealWantIndex[stealId], 0);
757
- // Restart from this consumer's home region.
758
806
  stealCursor = stealHome;
759
807
  }
760
808
  return true;
761
809
  };
762
- /**
763
- * HOST SIDE: decode version
764
- */
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
+ }
875
+ return true;
876
+ };
765
877
  const resolveHost = ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }) => {
766
878
  const getTask = takeTask({ queue });
767
879
  let lastResolved = 32;
@@ -887,27 +999,21 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
887
999
  return modified;
888
1000
  };
889
1001
  };
890
- /**
891
- * HOST SIDE: wait until the producer changes hostBits.
892
- *
893
- * LastWorker is the host's acknowledgement shadow. Passing it as the
894
- * expected value makes the arm race-free: a publication between the drain
895
- * and this call returns `not-equal` synchronously instead of being lost.
896
- */
1002
+ /** Wait for a producer publication without losing an intervening wakeup. */
897
1003
  const waitForHostChange = (timeoutMs) => {
898
1004
  if (a_waitAsync === undefined) {
899
- a_store(doorbellArmed, 0, 0);
1005
+ a_store(doorbellArmed, 0, DOORBELL_OFF);
900
1006
  return undefined;
901
1007
  }
902
- a_store(doorbellArmed, 0, 1);
1008
+ a_store(doorbellArmed, 0, DOORBELL_ARMED);
903
1009
  try {
904
1010
  const wait = a_waitAsync(hostBits, 0, LastWorker | 0, timeoutMs);
905
1011
  if (!wait.async)
906
- a_store(doorbellArmed, 0, 0);
1012
+ a_store(doorbellArmed, 0, DOORBELL_OFF);
907
1013
  return wait;
908
1014
  }
909
1015
  catch {
910
- a_store(doorbellArmed, 0, 0);
1016
+ a_store(doorbellArmed, 0, DOORBELL_OFF);
911
1017
  // Some runtimes reject particular SharedArrayBuffer implementations
912
1018
  // (for example a growable or native-backed buffer). Fall back to the
913
1019
  // existing dispatcher rather than turning a capability issue into a
@@ -915,6 +1021,18 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
915
1021
  return undefined;
916
1022
  }
917
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
+ };
918
1036
  const decodeAt = (at) => {
919
1037
  const off = (at * headersSlotStride) + slotBaseU32;
920
1038
  const recycled = recycleShift();
@@ -972,6 +1090,11 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
972
1090
  }
973
1091
  const bit = 1 << id;
974
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
+ }
975
1098
  return (previous & bit) !== 0;
976
1099
  };
977
1100
  return {
@@ -981,7 +1104,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
981
1104
  encodeAll,
982
1105
  publish,
983
1106
  flushPending,
984
- decode: stealEnabled ? decodeSteal : decode,
1107
+ decode: stealEnabled
1108
+ ? (stealClaimMask ? decodeStealCasMask : decodeSteal)
1109
+ : decode,
985
1110
  hasSpace,
986
1111
  resolved,
987
1112
  hostBits,
@@ -989,8 +1114,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
989
1114
  recyclecList,
990
1115
  resolveHost,
991
1116
  waitForHostChange,
1117
+ armHostNotifier,
992
1118
  setHostWaiterArmed: (armed) => {
993
- a_store(doorbellArmed, 0, armed ? 1 : 0);
1119
+ a_store(doorbellArmed, 0, armed ? DOORBELL_ARMED : DOORBELL_OFF);
994
1120
  },
995
1121
  hasPendingFrames: () => toBeSent.size !== 0,
996
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;