knitting 0.1.60 → 0.1.62

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 (46) hide show
  1. package/BROWSER.md +134 -0
  2. package/README.md +94 -6
  3. package/knitting.browser.d.ts +6 -0
  4. package/knitting.browser.js +1 -0
  5. package/package.json +11 -2
  6. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  7. package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
  8. package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
  9. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
  11. package/prebuilds/darwin-arm64-node-137/knitting_shm.node +0 -0
  12. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  13. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  14. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  15. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  16. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  18. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  19. package/src/api.js +136 -25
  20. package/src/common/runtime.d.ts +2 -1
  21. package/src/common/runtime.js +8 -1
  22. package/src/common/task-source.d.ts +9 -0
  23. package/src/common/task-source.js +15 -0
  24. package/src/memory/lock.d.ts +37 -1
  25. package/src/memory/lock.js +262 -5
  26. package/src/memory/payloadCodec.js +2 -6
  27. package/src/memory/regionRegistry.d.ts +1 -3
  28. package/src/memory/regionRegistry.js +16 -14
  29. package/src/permission/protocol.js +5 -1
  30. package/src/runtime/dispatcher.d.ts +11 -8
  31. package/src/runtime/dispatcher.js +163 -21
  32. package/src/runtime/inline-executor.js +6 -1
  33. package/src/runtime/pool.d.ts +127 -4
  34. package/src/runtime/pool.js +262 -73
  35. package/src/runtime/process-worker.d.ts +24 -12
  36. package/src/runtime/process-worker.js +185 -115
  37. package/src/runtime/tx-queue.d.ts +17 -1
  38. package/src/runtime/tx-queue.js +120 -15
  39. package/src/runtime/worker-common.d.ts +13 -0
  40. package/src/runtime/worker-common.js +103 -0
  41. package/src/types.d.ts +75 -4
  42. package/src/worker/loop.js +34 -5
  43. package/src/worker/rx-queue.d.ts +11 -1
  44. package/src/worker/rx-queue.js +4 -1
  45. package/src/worker/timers.d.ts +8 -1
  46. package/src/worker/timers.js +20 -17
@@ -226,6 +226,29 @@ export const HEADER_TASK_LINE_U32 = LOCK_CACHE_LINE_BYTES /
226
226
  export const HEADER_STATIC_PAYLOAD_U32 = TaskIndex.TotalBuff -
227
227
  HEADER_TASK_LINE_U32;
228
228
  export const HEADER_TASK_OFFSET_IN_SLOT_U32 = HEADER_STATIC_PAYLOAD_U32;
229
+ // Work-stealing control words live in the unused tail of each slot's header
230
+ // 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.
235
+ export const STEAL_ACK_SLOT_OFFSET_U32 = HEADER_TASK_OFFSET_IN_SLOT_U32 +
236
+ TaskIndex.Size;
237
+ export const STEAL_WANT_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 1;
238
+ /**
239
+ * The payload allocator needs its own participant-owned ACK per consumer, since
240
+ * several workers can free payload slots from one shared allocator. No intent
241
+ * word: freeing needs no arbitration, a consumer only frees what it drained.
242
+ */
243
+ export const STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 2;
244
+ /**
245
+ * Host-owned liveness mask for stealing consumers. A dead claimant may leave
246
+ * its WANT word set forever; survivors ignore WANT from consumers whose bit is
247
+ * clear here. This preserves single-writer ownership of every WANT word.
248
+ */
249
+ export const STEAL_LIVE_SLOT_OFFSET_U32 = STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 + 1;
250
+ /** Host-owned arm word for the return-lock completion doorbell. */
251
+ export const DOORBELL_ARMED_SLOT_OFFSET_U32 = STEAL_LIVE_SLOT_OFFSET_U32 + 1;
229
252
  export const HEADER_U32_LENGTH = LockBound.header +
230
253
  (HEADER_SLOT_STRIDE_U32 * LockBound.slots);
231
254
  export const HEADER_BYTE_LENGTH = HEADER_U32_LENGTH *
@@ -276,7 +299,7 @@ const settleTask = (task) => {
276
299
  task[TaskIndex["FlagsToHost"]] = 0;
277
300
  }
278
301
  };
279
- export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, }) => {
302
+ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, consumers, consumerId, regionLanes, notifyOnHostPublish, }) => {
280
303
  // Layout within `lockSectorRegion`:
281
304
  // - hostBits starts at byte 0
282
305
  // - workerBits starts at byte 64
@@ -295,6 +318,11 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
295
318
  const headersRegion = toSharedBufferRegion(headers ?? createWasmSharedArrayBuffer(HEADER_BYTE_LENGTH));
296
319
  const headersBuffer = new Uint32Array(headersRegion.sab, headersRegion.byteOffset, headersRegion.byteLength >>> 2);
297
320
  const headersSlotStride = headerSlotStrideU32 ?? HEADER_SLOT_STRIDE_U32;
321
+ // The first task cache line has four unused control words after the task
322
+ // header. Keep the doorbell arm bit in one of those words so the host and
323
+ // worker lock instances can share it without allocating another SAB.
324
+ const doorbellArmed = new Int32Array(headersRegion.sab, headersRegion.byteOffset +
325
+ (DOORBELL_ARMED_SLOT_OFFSET_U32 * Uint32Array.BYTES_PER_ELEMENT), 1);
298
326
  const resolvedPayloadConfig = resolvePayloadBufferOptions({
299
327
  sab: payload,
300
328
  options: payloadConfig,
@@ -308,6 +336,55 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
308
336
  headers: headersRegion,
309
337
  payload: payloadSAB,
310
338
  });
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.
342
+ const stealConsumers = Math.max(1, (consumers ?? 1) | 0);
343
+ 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.
347
+ const stealIsProducer = consumerId === undefined;
348
+ const stealId = (consumerId ?? 0) | 0;
349
+ const stealRegionLanes = (regionLanes ?? 8) | 0;
350
+ const stealRegions = (LockBound.slots / stealRegionLanes) | 0;
351
+ if (stealEnabled) {
352
+ if (stealRegionLanes < 1 || (stealRegionLanes & (stealRegionLanes - 1)) !== 0) {
353
+ throw new RangeError("regionLanes must be a power of two");
354
+ }
355
+ if (stealRegions < stealConsumers) {
356
+ throw new RangeError(`regionLanes=${stealRegionLanes} yields ${stealRegions} regions, ` +
357
+ `too few for ${stealConsumers} consumers`);
358
+ }
359
+ if (stealId < 0 || stealId >= stealConsumers) {
360
+ throw new RangeError(`consumerId ${stealId} out of range`);
361
+ }
362
+ }
363
+ // Int32 alias over the headers SAB so the control words get signed bit math
364
+ // consistent with the rest of the protocol.
365
+ const stealView = new Int32Array(headersRegion.sab, headersRegion.byteOffset, headersRegion.byteLength >>> 2);
366
+ const stealAckIndex = new Int32Array(stealConsumers);
367
+ const stealWantIndex = new Int32Array(stealConsumers);
368
+ 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;
372
+ }
373
+ const stealLiveIndex = LockBound.header + STEAL_LIVE_SLOT_OFFSET_U32;
374
+ const stealAllLiveMask = stealConsumers === 32
375
+ ? -1
376
+ : ((1 << stealConsumers) - 1) | 0;
377
+ if (stealEnabled && stealIsProducer) {
378
+ Atomics.store(stealView, stealLiveIndex, stealAllLiveMask);
379
+ }
380
+ 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
+ };
311
388
  let promiseHandler;
312
389
  if (registeredEncodePayload === undefined ||
313
390
  registeredDecodePayload === undefined) {
@@ -358,12 +435,20 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
358
435
  // Atomics aliases (hot path)
359
436
  const a_load = Atomics.load;
360
437
  const a_store = Atomics.store;
438
+ const a_notify = Atomics.notify;
439
+ const shouldNotifyHostPublish = notifyOnHostPublish === true;
440
+ const a_waitAsync = typeof Atomics.waitAsync === "function"
441
+ ? Atomics.waitAsync.bind(Atomics)
442
+ : undefined;
361
443
  // Sender-side cached shadow of the receiver-owned queue word. Under the XSC
362
444
  // false-busy-only sender-side staleness property, this may hide newly freed
363
445
  // lanes but cannot make a genuinely pending lane appear free. Refresh only
364
446
  // when the cached free set is exhausted.
365
- let workerShadow = a_load(workerBits, 0) | 0;
366
- const refreshWorkerShadow = () => workerShadow = a_load(workerBits, 0) | 0;
447
+ let workerShadow = 0 | 0;
448
+ const refreshWorkerShadow = stealEnabled
449
+ ? () => workerShadow = stealAckXorAll()
450
+ : () => workerShadow = a_load(workerBits, 0) | 0;
451
+ refreshWorkerShadow();
367
452
  const ensureSenderStateHasFree = (state) => (~state) !== 0 ? state : (LastLocal ^ refreshWorkerShadow()) | 0;
368
453
  // RingQueue method aliases (hot path)
369
454
  const toBeSentPush = (task) => toBeSent.push(task);
@@ -487,7 +572,12 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
487
572
  deferredCount = 0 | 0;
488
573
  return toBeSent.isEmpty;
489
574
  };
490
- const storeHost = (bit) => a_store(hostBits, 0, LastLocal = (LastLocal ^ bit) | 0);
575
+ const storeHost = (bit) => {
576
+ a_store(hostBits, 0, LastLocal = (LastLocal ^ bit) | 0);
577
+ if (shouldNotifyHostPublish && a_load(doorbellArmed, 0) !== 0) {
578
+ a_notify(hostBits, 0, 1);
579
+ }
580
+ };
491
581
  const storeWorker = (bit) => a_store(workerBits, 0, LastWorker = (LastWorker ^ bit) | 0);
492
582
  const encode = (task, state = (LastLocal ^ workerShadow) | 0) => {
493
583
  state = ensureSenderStateHasFree(state);
@@ -561,6 +651,122 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
561
651
  lastTake = last;
562
652
  return true;
563
653
  };
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
+ const stealLaneMask = (region) => stealRegionLanes === 32
667
+ ? -1
668
+ : ((((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
+ };
677
+ const stealJuniorWants = (intent) => {
678
+ const live = a_load(stealView, stealLiveIndex) | 0;
679
+ for (let c = stealId + 1; c < stealConsumers; c++) {
680
+ if (!stealIsLive(live, c))
681
+ continue;
682
+ if ((a_load(stealView, stealWantIndex[c]) & intent) !== 0)
683
+ return true;
684
+ }
685
+ return false;
686
+ };
687
+ let stealCursor = ((stealRegions * stealId) / stealConsumers) | 0;
688
+ const decodeSteal = () => {
689
+ const pending = (a_load(hostBits, 0) ^ LastWorker ^ stealPeerAcks()) | 0;
690
+ if (pending === 0)
691
+ return false;
692
+ let pendingRegions = 0 | 0;
693
+ if (stealRegions === 1)
694
+ pendingRegions = 1;
695
+ else {
696
+ for (let r = 0; r < stealRegions; r++) {
697
+ if ((pending & stealLaneMask(r)) !== 0)
698
+ pendingRegions |= 1 << r;
699
+ }
700
+ }
701
+ const liveBeforeClaim = a_load(stealView, stealLiveIndex) | 0;
702
+ let peerIntent = 0 | 0;
703
+ let seniorIntent = 0 | 0;
704
+ for (let c = 0; c < stealConsumers; c++) {
705
+ if (c === stealId || !stealIsLive(liveBeforeClaim, c))
706
+ continue;
707
+ const value = a_load(stealView, stealWantIndex[c]) | 0;
708
+ peerIntent |= value;
709
+ if (c < stealId)
710
+ seniorIntent |= value;
711
+ }
712
+ const notSenior = pendingRegions & ~seniorIntent;
713
+ if (notSenior === 0)
714
+ return false;
715
+ const clean = pendingRegions & ~peerIntent;
716
+ const candidates = clean !== 0 ? clean : notSenior;
717
+ let region = -1;
718
+ for (let step = 0; step < stealRegions; step++) {
719
+ const candidate = (stealCursor + step) % stealRegions;
720
+ if ((candidates & (1 << candidate)) !== 0) {
721
+ region = candidate;
722
+ break;
723
+ }
724
+ }
725
+ if (region < 0)
726
+ return false;
727
+ const intent = (1 << region) | 0;
728
+ a_store(stealView, stealWantIndex[stealId], intent);
729
+ // Dekker StoreLoad: implicit, `Atomics.store` above is seq-cst.
730
+ let seniorConflict = false;
731
+ let juniorConflict = false;
732
+ for (let c = 0; c < stealConsumers; c++) {
733
+ if (c === stealId || !stealIsLive(liveBeforeClaim, c))
734
+ continue;
735
+ if ((a_load(stealView, stealWantIndex[c]) & intent) === 0)
736
+ continue;
737
+ if (c < stealId)
738
+ seniorConflict = true;
739
+ else
740
+ juniorConflict = true;
741
+ }
742
+ if (seniorConflict) {
743
+ a_store(stealView, stealWantIndex[stealId], 0);
744
+ return false;
745
+ }
746
+ if (juniorConflict) {
747
+ while (stealJuniorWants(intent)) { /* junior withdraws */ }
748
+ }
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()) &
752
+ stealLaneMask(region)) | 0;
753
+ if (take === 0) {
754
+ a_store(stealView, stealWantIndex[stealId], 0);
755
+ return false;
756
+ }
757
+ 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;
768
+ return true;
769
+ };
564
770
  /**
565
771
  * HOST SIDE: decode version
566
772
  */
@@ -689,6 +895,34 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
689
895
  return modified;
690
896
  };
691
897
  };
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
+ */
905
+ const waitForHostChange = (timeoutMs) => {
906
+ if (a_waitAsync === undefined) {
907
+ a_store(doorbellArmed, 0, 0);
908
+ return undefined;
909
+ }
910
+ a_store(doorbellArmed, 0, 1);
911
+ try {
912
+ const wait = a_waitAsync(hostBits, 0, LastWorker | 0, timeoutMs);
913
+ if (!wait.async)
914
+ a_store(doorbellArmed, 0, 0);
915
+ return wait;
916
+ }
917
+ catch {
918
+ a_store(doorbellArmed, 0, 0);
919
+ // Some runtimes reject particular SharedArrayBuffer implementations
920
+ // (for example a growable or native-backed buffer). Fall back to the
921
+ // existing dispatcher rather than turning a capability issue into a
922
+ // hung pool.
923
+ return undefined;
924
+ }
925
+ };
692
926
  const decodeAt = (at) => {
693
927
  const off = (at * headersSlotStride) + slotBaseU32;
694
928
  const recycled = recycleShift();
@@ -730,6 +964,24 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
730
964
  deferredCount = 0 | 0;
731
965
  pendingPromiseCount = 0 | 0;
732
966
  };
967
+ /**
968
+ * HOST SIDE: remove a terminated consumer from stealing arbitration.
969
+ *
970
+ * Its ACK remains part of the generation XOR forever, because it records
971
+ * lanes that consumer retired while alive. Only its stale WANT becomes
972
+ * ineligible. The caller must invoke this only after the endpoint can no
973
+ * longer execute; there is deliberately no matching reactivation operation.
974
+ */
975
+ const deactivateStealConsumer = (id) => {
976
+ if (!stealEnabled || !stealIsProducer)
977
+ return false;
978
+ if (!Number.isInteger(id) || id < 0 || id >= stealConsumers) {
979
+ throw new RangeError(`consumerId ${id} out of range`);
980
+ }
981
+ const bit = 1 << id;
982
+ const previous = Atomics.and(stealView, stealLiveIndex, ~bit);
983
+ return (previous & bit) !== 0;
984
+ };
733
985
  return {
734
986
  enlist,
735
987
  encode,
@@ -737,17 +989,22 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
737
989
  encodeAll,
738
990
  publish,
739
991
  flushPending,
740
- decode,
992
+ decode: stealEnabled ? decodeSteal : decode,
741
993
  hasSpace,
742
994
  resolved,
743
995
  hostBits,
744
996
  workerBits,
745
997
  recyclecList,
746
998
  resolveHost,
999
+ waitForHostChange,
1000
+ setHostWaiterArmed: (armed) => {
1001
+ a_store(doorbellArmed, 0, armed ? 1 : 0);
1002
+ },
747
1003
  hasPendingFrames: () => toBeSent.size !== 0,
748
1004
  getPendingFrameCount: () => toBeSent.size | 0,
749
1005
  getPendingPromiseCount: () => pendingPromiseCount | 0,
750
1006
  resetPendingState,
1007
+ deactivateStealConsumer,
751
1008
  takeDeferredCount: () => {
752
1009
  const count = deferredCount | 0;
753
1010
  deferredCount = 0 | 0;
@@ -311,9 +311,7 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
311
311
  options: payload?.config ?? payloadConfig,
312
312
  });
313
313
  const maxPayloadBytes = resolvedPayloadConfig.maxPayloadBytes;
314
- const { allocTask, setSlotLength, free } = register({
315
- lockSector,
316
- });
314
+ const { allocTask, setSlotLength, free } = register({ lockSector });
317
315
  const { writeBinary: writeDynamicBinary, writeBuffer: writeDynamicBuffer, writeArrayBuffer: writeDynamicArrayBuffer, write8Binary: writeDynamic8Binary, writeUtf8: writeDynamicUtf8, } = createSharedDynamicBufferIO({
318
316
  sab: payloadSab,
319
317
  payloadConfig: resolvedPayloadConfig,
@@ -1219,9 +1217,7 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1219
1217
  sab: payloadSab,
1220
1218
  options: payload?.config ?? payloadConfig,
1221
1219
  });
1222
- const { free } = register({
1223
- lockSector,
1224
- });
1220
+ const { free } = register({ lockSector });
1225
1221
  const freeTaskSlot = (task) => free(getTaskSlotIndex(task));
1226
1222
  const { readUtf8: readDynamicUtf8, readBytesCopy: readDynamicBytesCopy, readBytesBufferCopy: readDynamicBufferCopy, readBufferCopy: readDynamicBuffer, readBytesArrayBufferCopy: readDynamicArrayBufferCopy, readArrayBufferCopy: readDynamicArrayBuffer, read8BytesFloatCopy: readDynamic8BytesFloatCopy, read8BytesFloatView: readDynamic8BytesFloatView, } = createSharedDynamicBufferIO({
1227
1223
  sab: payloadSab,
@@ -1,10 +1,8 @@
1
1
  import type { Task } from "./lock.js";
2
2
  import { type SharedBufferSource } from "../common/shared-buffer-region.js";
3
3
  export type RegisterMalloc = ReturnType<typeof register>;
4
- export type RegionRegistryPublishMode = "plain" | "atomic";
5
- export declare const register: ({ lockSector, publishMode, }: {
4
+ export declare const register: ({ lockSector, }: {
6
5
  lockSector?: SharedBufferSource;
7
- publishMode?: RegionRegistryPublishMode;
8
6
  }) => {
9
7
  allocTask: (task: Task) => number;
10
8
  setSlotLength: (slotIndex: number, payloadLen: number) => boolean;
@@ -4,7 +4,7 @@ import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
4
4
  // Low 5 bits = slot index, high 27 bits = caller meta.
5
5
  // Inlined from setTaskSlotIndex to avoid cross-closure call on hot path.
6
6
  const SLOT_META_PACKED_MASK = 0xFFFFFFE0; // (~0x1F) >>> 0
7
- export const register = ({ lockSector, publishMode = "plain", }) => {
7
+ export const register = ({ lockSector, }) => {
8
8
  const lockRegion = toSharedBufferRegion(lockSector ?? createWasmSharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH));
9
9
  const lockSAB = lockRegion.sab;
10
10
  const hostBits = new Int32Array(lockSAB, lockRegion.byteOffset + PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, 1);
@@ -14,7 +14,7 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
14
14
  const clz32 = Math.clz32;
15
15
  const a_load = Atomics.load;
16
16
  const a_store = Atomics.store;
17
- const useAtomicPublish = publishMode === "atomic";
17
+ const a_xor = Atomics.xor;
18
18
  const EMPTY = 0xFFFFFFFF >>> 0;
19
19
  const SLOT_MASK = TASK_SLOT_INDEX_MASK;
20
20
  const START_MASK = (~SLOT_MASK) >>> 0;
@@ -23,7 +23,6 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
23
23
  let usedBits = 0 | 0;
24
24
  // scalar state (faster than Uint32Array(1))
25
25
  let hostLast = 0 | 0;
26
- let workerLast = 0 | 0;
27
26
  const startAndIndexToArray = (length) => startAndIndex.slice(0, length);
28
27
  const compactFreeBitsStable = (b, freeBits) => {
29
28
  const sai = startAndIndex;
@@ -92,10 +91,14 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
92
91
  let freeBits = (~(hostLast ^ w)) >>> 0;
93
92
  if (freeBits !== 0)
94
93
  freeBits &= usedBits;
95
- if (freeBits === EMPTY) {
94
+ // Every tracked slot is free, so restart the arena from offset 0. A free
95
+ // racing after this snapshot can only delay reclamation until the next
96
+ // allocation; it cannot make a live slot appear free.
97
+ if (freeBits === (usedBits >>> 0)) {
96
98
  tableLength = 0;
97
99
  usedBits = 0 | 0;
98
100
  tl = 0;
101
+ freeBits = 0 >>> 0;
99
102
  }
100
103
  else if (freeBits !== 0) {
101
104
  for (let i = 0; i < tl; i++) {
@@ -247,10 +250,7 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
247
250
  const slotIndex = findAndInsert(task, size);
248
251
  if (slotIndex === -1)
249
252
  return -1;
250
- if (useAtomicPublish)
251
- a_store(hostBits, 0, hostLast);
252
- else
253
- hostBits[0] = hostLast;
253
+ a_store(hostBits, 0, hostLast);
254
254
  return slotIndex;
255
255
  };
256
256
  const setSlotLength = (slotIndex, payloadLen) => {
@@ -264,13 +264,15 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
264
264
  size64bit[slotIndex] = aligned >>> 0;
265
265
  return true;
266
266
  };
267
+ // Always a read-modify-write. `free()` genuinely has two callers even without
268
+ // work stealing: the encode-side rollback (`failDynamicWriteAfterReserve`)
269
+ // and the decode-side release (`freeTaskSlot`). Each held a private
270
+ // `workerLast` shadow and blind-stored the whole word, so one overwrote the
271
+ // other's toggles — the allocator then handed out a region that was still
272
+ // live, aliasing payloads. XOR of distinct bits commutes, so one atomic
273
+ // toggle per free is correct for any number of writers.
267
274
  const free = (index) => {
268
- index = index & TASK_SLOT_INDEX_MASK;
269
- workerLast ^= 1 << index;
270
- if (useAtomicPublish)
271
- a_store(workerBits, 0, workerLast);
272
- else
273
- workerBits[0] = workerLast;
275
+ a_xor(workerBits, 0, 1 << (index & TASK_SLOT_INDEX_MASK));
274
276
  };
275
277
  return {
276
278
  allocTask,
@@ -1,4 +1,4 @@
1
- import { RUNTIME } from "../common/runtime.js";
1
+ import { IS_BROWSER, RUNTIME } from "../common/runtime.js";
2
2
  import { toCanonicalPath as toSharedCanonicalPath } from "../common/path-canonical.js";
3
3
  import { getNodeBuiltinModule, getNodeProcess } from "../common/node-compat.js";
4
4
  const requireNodeModule = (specifier) => {
@@ -436,6 +436,10 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
436
436
  const input = normalizeProtocolInput(permission);
437
437
  if (!input)
438
438
  return undefined;
439
+ // Browsers have no filesystem, process, or runtime flags to police, and the
440
+ // strict default would otherwise reach for `node:path` and throw.
441
+ if (IS_BROWSER)
442
+ return undefined;
439
443
  const rawMode = input.mode;
440
444
  const mode = (rawMode === "unsafe" || rawMode === "off")
441
445
  ? "unsafe"
@@ -2,12 +2,14 @@ import { type MultiQueue } from "./tx-queue.js";
2
2
  import { type MainSignal } from "../ipc/transport/shared-memory.js";
3
3
  import { type RuntimeMessageChannelLike, type RuntimeMessagePortLike } from "../common/worker-runtime.js";
4
4
  import type { DispatcherSettings } from "../types.js";
5
- export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, }, channelHandler, dispatcherOptions, notifySignal, }: {
5
+ export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, }: {
6
6
  queue: MultiQueue;
7
7
  signalBox: MainSignal;
8
8
  channelHandler: ChannelHandler;
9
9
  dispatcherOptions?: DispatcherSettings;
10
10
  notifySignal?: () => void;
11
+ /** Workers live in other processes; disables the doorbell. */
12
+ crossProcess?: boolean;
11
13
  }) => {
12
14
  check: {
13
15
  (): void;
@@ -20,20 +22,21 @@ type CheckWithState = (() => void) & {
20
22
  rerun: boolean;
21
23
  };
22
24
  export type DispatcherCheck = CheckWithState;
25
+ export type ChannelHandlerPump = "auto" | "channel";
23
26
  export declare class ChannelHandler {
24
27
  #private;
25
- channel: RuntimeMessageChannelLike;
26
- port1: RuntimeMessagePortLike;
27
- port2: RuntimeMessagePortLike;
28
- constructor();
28
+ channel: RuntimeMessageChannelLike | undefined;
29
+ port1: RuntimeMessagePortLike | undefined;
30
+ port2: RuntimeMessagePortLike | undefined;
31
+ constructor(pump?: ChannelHandlerPump);
29
32
  notify(): void;
30
33
  /**
31
- * Opens the channel (if not already open) and sets the onmessage handler.
32
- * This is the setup so `notify` can send a message to the port 1.
34
+ * Registers the handler the pump calls back into. On the channel pump this is
35
+ * also where the ports are opened, so `notify` can reach port 1.
33
36
  */
34
37
  open(f: () => void): void;
35
38
  /**
36
- * Closes the channel if it is open.
39
+ * Detaches the handler, and closes the channel if this pump has one.
37
40
  */
38
41
  close(): void;
39
42
  }