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
@@ -1,4 +1,4 @@
1
- import { attachPayloadTransportFinalizer, beginPromisePayload, finishPromisePayload, getTaskSlotIndex, HEADER_BYTE_LENGTH, HEADER_SLOT_STRIDE_U32, HEADER_STATIC_PAYLOAD_U32, LockBound, PayloadBuffer, payloadBufferName, PayloadSignal, registerLockPayloadCodec, TaskIndex, } from "./lock.js";
1
+ import { attachPayloadTransportFinalizer, beginPromisePayload, finishPromisePayload, HEADER_BYTE_LENGTH, HEADER_SLOT_STRIDE_U32, HEADER_STATIC_PAYLOAD_U32, LockBound, PayloadBuffer, payloadBufferName, PayloadSignal, registerLockPayloadCodec, TASK_SLOT_INDEX_MASK, TaskIndex, } from "./lock.js";
2
2
  import { register } from "./regionRegistry.js";
3
3
  import { createSharedDynamicBufferIO, createSharedStaticBufferIO, } from "./shared-buffer-io.js";
4
4
  import { getStridedRegionSpanBytes } from "./byte-carpet.js";
@@ -6,7 +6,8 @@ import { encoderError, ErrorKnitting } from "../error.js";
6
6
  import { Envelope } from "../common/envelope.js";
7
7
  import { resolvePayloadBufferOptions, } from "./payload-config.js";
8
8
  import { getSharedArrayBufferPayload, SHARED_ARRAY_BUFFER_CODEC_ID, SHARED_ARRAY_BUFFER_NUMERIC_TRANSFER, SHARED_ARRAY_BUFFER_NUMERIC_WORDS, } from "../connections/shared-array-buffer-payload.js";
9
- import { BUFFER_REFERENCE_NUMERIC_TRANSFER, BufferReference, isBufferReferenceValue, } from "../connections/buffer-reference.js";
9
+ import { BUFFER_REFERENCE_NUMERIC_TRANSFER, BufferReference, isArrayBufferDetached, isBufferReferenceValue, } from "../connections/buffer-reference.js";
10
+ import { getBufferReferenceCapabilities, } from "../connections/buffer-reference-native.js";
10
11
  import { isProcessSharedBufferValue, ProcessSharedBuffer, } from "../connections/process-shared-buffer.js";
11
12
  import { isNumericArray, numericArrayFromFloat64, } from "../connections/numeric-array.js";
12
13
  const memory = new ArrayBuffer(8);
@@ -99,7 +100,7 @@ const readTrustedExternalPayloadMetadata = (value) => {
99
100
  }
100
101
  return value.toMetadata();
101
102
  };
102
- const decodeExternalPayload = (raw, processBoundary) => {
103
+ const decodeExternalPayload = (raw, processBoundary, transportKey) => {
103
104
  const payload = parseJSON(raw);
104
105
  if (!arrayIsArray(payload) || payload.length !== 2)
105
106
  return payload;
@@ -113,22 +114,33 @@ const decodeExternalPayload = (raw, processBoundary) => {
113
114
  }
114
115
  const codec = externalPayloadGlobal.__KNITTING_PAYLOAD_CODECS__?.[codecId];
115
116
  return typeof codec?.decode === "function"
116
- ? codec.decode(metadata)
117
+ ? codec.decode(metadata, transportKey)
117
118
  : { codec: codecId, metadata };
118
119
  };
119
120
  const PROCESS_SHARED_BUFFER_NUMERIC_WORDS = 8;
120
121
  const BUFFER_REFERENCE_NUMERIC_WORDS = 8;
121
122
  const NUMERIC_SENTINEL = 0xffffffff;
122
- const decodeNumericExternalPayload = (codecId, words) => {
123
+ /** Minimum size for automatically borrowed byte returns. */
124
+ export const SHARED_RETURN_MIN_BYTES = 256 * 1024;
125
+ /** Number of later borrowed returns kept before a region is recycled. */
126
+ export const SHARED_RETURN_BORROW_WINDOW = 32;
127
+ // Each payload buffer has one allocator for the worker return lane.
128
+ const sharedReturnAllocators = new WeakMap();
129
+ /** The borrowed-region allocator for `payload`, if an encoder built one. */
130
+ export const getSharedReturnAllocator = (payload) => sharedReturnAllocators.get(payload);
131
+ const decodeNumericExternalPayload = (codecId, words, transportKey) => {
123
132
  const codec = externalPayloadGlobal.__KNITTING_PAYLOAD_CODECS__?.[codecId];
124
133
  if (typeof codec?.decodeNumeric === "function") {
125
- return codec.decodeNumeric(words);
134
+ return codec.decodeNumeric(words, transportKey);
126
135
  }
127
136
  return { codec: codecId, metadata: Array.from(words) };
128
137
  };
129
138
  const decodeProcessSharedBufferNumericWords = (words) => decodeNumericExternalPayload(PROCESS_SHARED_BUFFER_CODEC_ID, words);
130
139
  const decodeBufferReferenceNumericWords = (words) => decodeNumericExternalPayload(BUFFER_REFERENCE_CODEC_ID, words);
131
- const decodeSharedArrayBufferNumericWords = (words) => decodeNumericExternalPayload(SHARED_ARRAY_BUFFER_CODEC_ID, words);
140
+ // The transport key scopes the host's adopted-alias cache to one return lane:
141
+ // producer tokens restart at 1 in every worker isolate, so a shared cache would
142
+ // serve one worker's buffer for another worker's identical token.
143
+ const decodeSharedArrayBufferNumericWords = (words, transportKey) => decodeNumericExternalPayload(SHARED_ARRAY_BUFFER_CODEC_ID, words, transportKey);
132
144
  const tryEncodePrimitiveTask = (task) => {
133
145
  const value = task.value;
134
146
  switch (typeof value) {
@@ -304,15 +316,28 @@ const requireStaticIO = (headersBuffer, headerSlotStrideU32, textCompat) => {
304
316
  * Returns `true` when the payload is encoded successfully.
305
317
  * Returns `false` when dynamic payload space could not be reserved.
306
318
  */
307
- export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, onPromise, processBoundary = false, }) => {
319
+ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headersBuffer, headerSlotStrideU32, textCompat, onPromise, processBoundary = false, sharedReturn = false, moveReturn = false, }) => {
308
320
  const payloadSab = payload?.sab ?? sab;
309
321
  const resolvedPayloadConfig = resolvePayloadBufferOptions({
310
322
  sab: payloadSab,
311
323
  options: payload?.config ?? payloadConfig,
312
324
  });
313
325
  const maxPayloadBytes = resolvedPayloadConfig.maxPayloadBytes;
314
- const { allocTask, setSlotLength, free } = register({ lockSector });
315
- const { writeBinary: writeDynamicBinary, writeBuffer: writeDynamicBuffer, writeArrayBuffer: writeDynamicArrayBuffer, write8Binary: writeDynamic8Binary, writeUtf8: writeDynamicUtf8, } = createSharedDynamicBufferIO({
326
+ // Automatic moves are an optimization. Keep ordinary byte returns working
327
+ // when this runtime has no usable ownership/FFI backend.
328
+ const canMoveReturn = moveReturn && (() => {
329
+ try {
330
+ getBufferReferenceCapabilities();
331
+ return true;
332
+ }
333
+ catch {
334
+ return false;
335
+ }
336
+ })();
337
+ const registryHandle = register({ lockSector });
338
+ const { allocTask, setSlotLength, tagTaskSlot, free } = registryHandle;
339
+ const { allocRegion, regionStart } = registryHandle;
340
+ const { writeBinary: writeDynamicBinary, writeBuffer: writeDynamicBuffer, writeArrayBuffer: writeDynamicArrayBuffer, write8Binary: writeDynamic8Binary, writeUtf8: writeDynamicUtf8, readBytesView: dynamicRegionView, ensureCapacity: ensureDynamicCapacity, currentBuffer: currentDynamicBuffer, regionBase: dynamicRegionBase, } = createSharedDynamicBufferIO({
316
341
  sab: payloadSab,
317
342
  payloadConfig: resolvedPayloadConfig,
318
343
  textCompat: textCompat?.payload,
@@ -350,9 +375,72 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
350
375
  return exactBytes;
351
376
  };
352
377
  const dynamicUtf8ReserveBytes = (task, text, label) => dynamicUtf8ReserveBytesWithExtra(task, text, 0, label);
378
+ // Borrowed regions are tracked as [slot, start, end] triples, oldest first.
379
+ const borrowed = [];
380
+ const BORROW_STRIDE = 3;
381
+ /** Limit borrowed regions to half the payload arena. */
382
+ const maxBorrowBytes = ((resolvedPayloadConfig.payloadMaxByteLength >> 1) /
383
+ SHARED_RETURN_BORROW_WINDOW) | 0;
384
+ /** Reserve a region to hand over by reference, or -1 when none is free. */
385
+ const reserveBorrowedRegion = (bytes) => {
386
+ if (bytes > maxBorrowBytes)
387
+ return -1;
388
+ const slot = allocRegion(bytes);
389
+ if (slot === -1)
390
+ return -1;
391
+ const start = regionStart(slot);
392
+ if (!ensureDynamicCapacity(start + bytes)) {
393
+ free(slot);
394
+ return -1;
395
+ }
396
+ borrowed.push(slot, start, start + bytes);
397
+ if (borrowed.length > SHARED_RETURN_BORROW_WINDOW * BORROW_STRIDE) {
398
+ free(borrowed[0]);
399
+ borrowed.splice(0, BORROW_STRIDE);
400
+ }
401
+ return slot;
402
+ };
403
+ const dropBorrowedRegion = (slot) => {
404
+ for (let at = 0; at < borrowed.length; at += BORROW_STRIDE) {
405
+ if (borrowed[at] === slot) {
406
+ borrowed.splice(at, BORROW_STRIDE);
407
+ break;
408
+ }
409
+ }
410
+ free(slot);
411
+ };
412
+ /** Return the arena offset when `view` is inside a live borrowed region. */
413
+ const borrowedStartOf = (view) => {
414
+ if (borrowed.length === 0)
415
+ return -1;
416
+ if (view.buffer !== currentDynamicBuffer())
417
+ return -1;
418
+ const start = view.byteOffset - dynamicRegionBase();
419
+ const end = start + view.byteLength;
420
+ for (let at = borrowed.length - BORROW_STRIDE; at >= 0; at -= BORROW_STRIDE) {
421
+ if (start >= borrowed[at + 1] && end <= borrowed[at + 2])
422
+ return start;
423
+ }
424
+ return -1;
425
+ };
426
+ /** Allocate a borrowed return region, or fall back when none is available. */
427
+ const allocateSharedReturn = (byteLength, zeroFill = false) => {
428
+ const slot = reserveBorrowedRegion(byteLength);
429
+ if (slot === -1)
430
+ return undefined;
431
+ const start = regionStart(slot);
432
+ const view = dynamicRegionView(start, start + byteLength);
433
+ if (zeroFill)
434
+ view.fill(0);
435
+ return view;
436
+ };
437
+ if (sharedReturn && payloadSab !== undefined) {
438
+ sharedReturnAllocators.set(payloadSab, allocateSharedReturn);
439
+ }
353
440
  const reserveDynamic = (task, bytes) => {
354
441
  task[TaskIndex.PayloadLen] = bytes;
355
- // PayloadCodec only reserves after the lock has guaranteed capacity.
442
+ // -1 is dynamic-region backpressure. Returning false leaves this frame on
443
+ // the lock's existing pending list until a receiver releases a region.
356
444
  return allocTask(task);
357
445
  };
358
446
  let objectDynamicSlot = -1;
@@ -419,6 +507,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
419
507
  return false;
420
508
  task[TaskIndex.Type] = PayloadBuffer.Error;
421
509
  const reservedSlot = reserveDynamicObject(task, reserveBytes);
510
+ if (reservedSlot === -1)
511
+ return false;
422
512
  const written = writeDynamicUtf8(text, task[TaskIndex.Start], reserveBytes);
423
513
  if (written < 0)
424
514
  return failDynamicWriteAfterReserve(task, reservedSlot);
@@ -443,6 +533,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
443
533
  return false;
444
534
  }
445
535
  const reservedSlot = reserveDynamicObject(task, bytes);
536
+ if (reservedSlot === -1)
537
+ return false;
446
538
  const written = writeDynamicBinary(bytesView, task[TaskIndex.Start]);
447
539
  if (written < 0)
448
540
  return failDynamicWriteAfterReserve(task, reservedSlot);
@@ -453,6 +545,15 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
453
545
  };
454
546
  const encodeObjectUint8Array = (task, slotIndex, bytesView) => {
455
547
  const bytes = bytesView.byteLength;
548
+ // Preserve an arena-backed subarray as an offset/length frame.
549
+ const borrowedStart = borrowedStartOf(bytesView);
550
+ if (borrowedStart !== -1) {
551
+ task[TaskIndex.Type] = PayloadBuffer.ArenaBinary;
552
+ task[TaskIndex.Start] = borrowedStart;
553
+ task[TaskIndex.PayloadLen] = bytes;
554
+ task.value = null;
555
+ return true;
556
+ }
456
557
  if (bytes <= staticMaxBytes) {
457
558
  writeStaticExactUint8Array(bytesView, slotIndex);
458
559
  task[TaskIndex.Type] = PayloadBuffer.StaticBinary;
@@ -460,10 +561,16 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
460
561
  task.value = null;
461
562
  return true;
462
563
  }
564
+ if (canMoveReturn && bytes >= SHARED_RETURN_MIN_BYTES &&
565
+ tryEncodeMovedReturn(task, slotIndex, bytesView, PayloadBuffer.MovedBinary)) {
566
+ return true;
567
+ }
463
568
  task[TaskIndex.Type] = PayloadBuffer.Binary;
464
569
  if (!ensureWithinDynamicLimit(task, bytes, "Binary"))
465
570
  return false;
466
571
  const reservedSlot = reserveDynamicObject(task, bytes);
572
+ if (reservedSlot === -1)
573
+ return false;
467
574
  const written = writeDynamicBinary(bytesView, task[TaskIndex.Start]);
468
575
  if (written < 0)
469
576
  return failDynamicWriteAfterReserve(task, reservedSlot);
@@ -487,6 +594,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
487
594
  if (!ensureWithinDynamicLimit(task, bytes, "Buffer"))
488
595
  return false;
489
596
  const reservedSlot = reserveDynamicObject(task, bytes);
597
+ if (reservedSlot === -1)
598
+ return false;
490
599
  const written = writeDynamicBuffer(buffer, task[TaskIndex.Start]);
491
600
  if (written < 0)
492
601
  return failDynamicWriteAfterReserve(task, reservedSlot);
@@ -510,6 +619,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
510
619
  if (!ensureWithinDynamicLimit(task, bytes, "Float64Array"))
511
620
  return false;
512
621
  const reservedSlot = reserveDynamicObject(task, bytes);
622
+ if (reservedSlot === -1)
623
+ return false;
513
624
  const written = writeDynamic8Binary(float64, task[TaskIndex.Start]);
514
625
  if (written < 0)
515
626
  return failDynamicWriteAfterReserve(task, reservedSlot);
@@ -541,6 +652,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
541
652
  if (!ensureWithinDynamicLimit(task, bytes, "NumericArray"))
542
653
  return false;
543
654
  const reservedSlot = reserveDynamicObject(task, bytes);
655
+ if (reservedSlot === -1)
656
+ return false;
544
657
  const written = writeDynamic8Binary(float64, task[TaskIndex.Start]);
545
658
  if (written < 0)
546
659
  return failDynamicWriteAfterReserve(task, reservedSlot);
@@ -560,10 +673,16 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
560
673
  return true;
561
674
  }
562
675
  }
676
+ if (canMoveReturn && bytes >= SHARED_RETURN_MIN_BYTES &&
677
+ tryEncodeMovedReturn(task, slotIndex, arrayBuffer, PayloadBuffer.MovedArrayBuffer)) {
678
+ return true;
679
+ }
563
680
  task[TaskIndex.Type] = PayloadBuffer.ArrayBuffer;
564
681
  if (!ensureWithinDynamicLimit(task, bytes, "ArrayBuffer"))
565
682
  return false;
566
683
  const reservedSlot = reserveDynamicObject(task, bytes);
684
+ if (reservedSlot === -1)
685
+ return false;
567
686
  const written = writeDynamicArrayBuffer(arrayBuffer, task[TaskIndex.Start]);
568
687
  if (written < 0)
569
688
  return failDynamicWriteAfterReserve(task, reservedSlot);
@@ -619,6 +738,68 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
619
738
  task.value = null;
620
739
  return true;
621
740
  };
741
+ /**
742
+ * The safe counterpart to an arena borrow. The producer's source is detached
743
+ * only after its task has settled; the host adopts the backing store before
744
+ * its acknowledgement lets the producer release the registry pin.
745
+ *
746
+ * This needs its own frame type because public task results remain ordinary
747
+ * Uint8Array / ArrayBuffer values rather than exposing BufferReference.
748
+ */
749
+ const tryEncodeMovedReturn = (task, slotIndex, source, type) => {
750
+ if (source.byteLength > 0xffffffff)
751
+ return false;
752
+ const backing = ArrayBuffer.isView(source) ? source.buffer : source;
753
+ if (typeof SharedArrayBuffer === "function" &&
754
+ backing instanceof SharedArrayBuffer) {
755
+ return false;
756
+ }
757
+ // Narrowed by the check above: what is left is a detachable ArrayBuffer.
758
+ const store = backing;
759
+ // A move takes the whole backing store, not the slice that was returned.
760
+ // `return scratch.subarray(0, n)` would detach the producer's entire
761
+ // scratch buffer and break its next call, so a partial view is copied
762
+ // like any other return. Only a view that *is* its buffer can move.
763
+ if (ArrayBuffer.isView(source) &&
764
+ (source.byteOffset !== 0 || source.byteLength !== store.byteLength)) {
765
+ return false;
766
+ }
767
+ // Moving is an optimization, so a source this runtime cannot detach --
768
+ // WASM memory, a buffer an external API pinned -- copies rather than
769
+ // failing the task. Construction is what detaches: a throw before it
770
+ // leaves the bytes intact and the copy path still works, while a throw
771
+ // after it has already consumed them has to be reported.
772
+ let reference;
773
+ try {
774
+ reference = new BufferReference(source);
775
+ }
776
+ catch (error) {
777
+ if (isArrayBufferDetached(store))
778
+ throw error;
779
+ return false;
780
+ }
781
+ // Belt and braces for a runtime that reports success without detaching.
782
+ // The host would be handed a store still aliasing memory the producer can
783
+ // write; the source is intact in that case, so dropping the reference and
784
+ // copying is both safe and the honest answer.
785
+ if (!isArrayBufferDetached(store)) {
786
+ reference.release();
787
+ return false;
788
+ }
789
+ const words = BufferReference.prototype[BUFFER_REFERENCE_NUMERIC_TRANSFER]
790
+ .call(reference);
791
+ // The size check above guarantees the numeric form for a local reference.
792
+ // Do not fall back after construction: the source has intentionally moved.
793
+ if (words === undefined) {
794
+ reference.release();
795
+ throw new Error("Moved BufferReference has no numeric transport form");
796
+ }
797
+ task[TaskIndex.Type] = type;
798
+ task[TaskIndex.PayloadLen] = writeStaticU32Words(words, BUFFER_REFERENCE_NUMERIC_WORDS, slotIndex);
799
+ attachPayloadTransportFinalizer(task, reference);
800
+ task.value = null;
801
+ return true;
802
+ };
622
803
  const tryEncodeSharedArrayBufferNumeric = (task, slotIndex, value) => {
623
804
  const words = value[SHARED_ARRAY_BUFFER_NUMERIC_TRANSFER]?.(lockSector);
624
805
  if (words === undefined)
@@ -719,6 +900,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
719
900
  if (reserveBytes < 0)
720
901
  return false;
721
902
  const reservedSlot = reserveDynamicObject(task, reserveBytes);
903
+ if (reservedSlot === -1)
904
+ return false;
722
905
  const written = writeDynamicUtf8(text, task[TaskIndex.Start], reserveBytes);
723
906
  if (written < 0)
724
907
  return failDynamicWriteAfterReserve(task, reservedSlot);
@@ -728,6 +911,48 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
728
911
  task.value = null;
729
912
  return true;
730
913
  };
914
+ /** Encode plain objects, arrays, and null-prototype objects as JSON. */
915
+ const encodeObjectJson = (task, slotIndex, objectValue) => {
916
+ let text;
917
+ try {
918
+ text = stringifyJSON(objectValue);
919
+ }
920
+ catch (error) {
921
+ const detail = error instanceof Error ? error.message : String(error);
922
+ return encoderError({
923
+ task,
924
+ type: ErrorKnitting.Json,
925
+ onPromise,
926
+ detail,
927
+ });
928
+ }
929
+ if (text.length <= staticMaxBytes) {
930
+ const written = writeStaticUtf8(text, slotIndex);
931
+ if (written !== -1) {
932
+ task[TaskIndex.Type] = PayloadBuffer.StaticJson;
933
+ task[TaskIndex.PayloadLen] = written;
934
+ // Retain any transport-owned memory until the call settles.
935
+ attachPayloadTransportFinalizer(task, objectValue);
936
+ task.value = null;
937
+ return true;
938
+ }
939
+ }
940
+ task[TaskIndex.Type] = PayloadBuffer.Json;
941
+ const reserveBytes = dynamicUtf8ReserveBytes(task, text, "Json");
942
+ if (reserveBytes < 0)
943
+ return false;
944
+ const reservedSlot = reserveDynamicObject(task, reserveBytes);
945
+ if (reservedSlot === -1)
946
+ return false;
947
+ const written = writeDynamicUtf8(text, task[TaskIndex.Start], reserveBytes);
948
+ if (written < 0)
949
+ return failDynamicWriteAfterReserve(task, reservedSlot);
950
+ task[TaskIndex.PayloadLen] = written;
951
+ setSlotLength(reservedSlot, written);
952
+ attachPayloadTransportFinalizer(task, objectValue);
953
+ task.value = null;
954
+ return true;
955
+ };
731
956
  const encodeObjectDate = (task, date) => {
732
957
  Float64View[0] = date.getTime();
733
958
  task[TaskIndex.Type] = PayloadBuffer.Date;
@@ -799,6 +1024,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
799
1024
  if (!ensureWithinDynamicLimit(task, payloadReserveBytes, "EnvelopeStaticHeaderPayload"))
800
1025
  return false;
801
1026
  const reservedSlot = reserveDynamicObject(task, payloadReserveBytes);
1027
+ if (reservedSlot === -1)
1028
+ return false;
802
1029
  task[TaskIndex.Type] = headerIsString
803
1030
  ? PayloadBuffer.EnvelopeStaticHeaderString
804
1031
  : PayloadBuffer.EnvelopeStaticHeader;
@@ -811,7 +1038,11 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
811
1038
  }
812
1039
  setSlotLength(reservedSlot, payloadWritten);
813
1040
  }
1041
+ else {
1042
+ setSlotLength(reservedSlot, 0);
1043
+ }
814
1044
  task.value = null;
1045
+ tagTaskSlot(task, reservedSlot);
815
1046
  return true;
816
1047
  }
817
1048
  const headerReserveBytes = dynamicUtf8ReserveBytesWithExtra(task, headerText, payloadLength, headerIsString ? "EnvelopeDynamicHeaderString" : "EnvelopeDynamicHeader");
@@ -821,6 +1052,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
821
1052
  ? PayloadBuffer.EnvelopeDynamicHeaderString
822
1053
  : PayloadBuffer.EnvelopeDynamicHeader;
823
1054
  const reservedSlot = reserveDynamicObject(task, headerReserveBytes + payloadLength);
1055
+ if (reservedSlot === -1)
1056
+ return false;
824
1057
  const baseStart = task[TaskIndex.Start];
825
1058
  const writtenHeaderBytes = writeDynamicUtf8(headerText, baseStart, headerReserveBytes);
826
1059
  if (writtenHeaderBytes < 0) {
@@ -836,6 +1069,7 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
836
1069
  task[TaskIndex.End] = payloadLength;
837
1070
  setSlotLength(reservedSlot, writtenHeaderBytes + payloadLength);
838
1071
  task.value = null;
1072
+ tagTaskSlot(task, reservedSlot);
839
1073
  return true;
840
1074
  };
841
1075
  const encodeEnvelopeExternalBody = (task, slotIndex, headerText, headerIsString, externalBody, trustedReservedCodec) => {
@@ -901,6 +1135,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
901
1135
  if (!ensureWithinDynamicLimit(task, bodyLength, "EnvelopeStaticHeaderExternal"))
902
1136
  return false;
903
1137
  const reservedSlot = reserveDynamicObject(task, bodyLength);
1138
+ if (reservedSlot === -1)
1139
+ return false;
904
1140
  task[TaskIndex.Type] = headerIsString
905
1141
  ? PayloadBuffer.EnvelopeStaticHeaderStringExternal
906
1142
  : PayloadBuffer.EnvelopeStaticHeaderExternal;
@@ -913,6 +1149,7 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
913
1149
  setSlotLength(reservedSlot, bodyWritten);
914
1150
  attachPayloadTransportFinalizer(task, externalBody);
915
1151
  task.value = null;
1152
+ tagTaskSlot(task, reservedSlot);
916
1153
  return true;
917
1154
  }
918
1155
  const headerReserveBytes = dynamicUtf8ReserveBytesWithExtra(task, headerText, bodyLength, headerIsString
@@ -924,6 +1161,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
924
1161
  ? PayloadBuffer.EnvelopeDynamicHeaderStringExternal
925
1162
  : PayloadBuffer.EnvelopeDynamicHeaderExternal;
926
1163
  const reservedSlot = reserveDynamicObject(task, headerReserveBytes + bodyLength);
1164
+ if (reservedSlot === -1)
1165
+ return false;
927
1166
  const baseStart = task[TaskIndex.Start];
928
1167
  const writtenHeaderBytes = writeDynamicUtf8(headerText, baseStart, headerReserveBytes);
929
1168
  if (writtenHeaderBytes < 0) {
@@ -938,6 +1177,7 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
938
1177
  setSlotLength(reservedSlot, writtenHeaderBytes + bodyLength);
939
1178
  attachPayloadTransportFinalizer(task, externalBody);
940
1179
  task.value = null;
1180
+ tagTaskSlot(task, reservedSlot);
941
1181
  return true;
942
1182
  };
943
1183
  const encodeObjectEnvelope = (task, slotIndex, envelope) => {
@@ -1005,6 +1245,10 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1005
1245
  return false;
1006
1246
  }
1007
1247
  const reservedSlot = reserveDynamic(task, binaryBytes);
1248
+ if (reservedSlot === -1) {
1249
+ clearBigIntScratch(binaryBytes);
1250
+ return false;
1251
+ }
1008
1252
  const written = writeDynamicBinary(binary, task[TaskIndex.Start]);
1009
1253
  if (written < 0) {
1010
1254
  clearBigIntScratch(binaryBytes);
@@ -1026,57 +1270,23 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1026
1270
  objectDynamicSlot = -1;
1027
1271
  try {
1028
1272
  const objectValue = args;
1273
+ const objectProto = objectGetPrototypeOf(objectValue);
1274
+ // Plain objects can skip the typed-array and external-payload checks.
1275
+ if (objectProto === objectPrototype) {
1276
+ return encodeObjectJson(task, slotIndex, objectValue);
1277
+ }
1029
1278
  const sharedArrayBufferPayload = getSharedArrayBufferPayload(objectValue);
1030
1279
  if (sharedArrayBufferPayload !== undefined) {
1031
1280
  return encodeObjectExternalPayload(task, slotIndex, sharedArrayBufferPayload, true);
1032
1281
  }
1033
- const objectProto = objectGetPrototypeOf(objectValue);
1034
1282
  if (isRuntimeUint8Array(objectValue)) {
1035
1283
  return encodeObjectUint8Array(task, slotIndex, objectValue);
1036
1284
  }
1037
1285
  if (arrayIsArray(objectValue) && isNumericArray(objectValue)) {
1038
1286
  return encodeObjectNumericArray(task, slotIndex, objectValue);
1039
1287
  }
1040
- if (arrayIsArray(objectValue) ||
1041
- objectProto === objectPrototype ||
1042
- objectProto === null) {
1043
- let text;
1044
- try {
1045
- text = stringifyJSON(objectValue);
1046
- }
1047
- catch (error) {
1048
- const detail = error instanceof Error
1049
- ? error.message
1050
- : String(error);
1051
- return encoderError({
1052
- task,
1053
- type: ErrorKnitting.Json,
1054
- onPromise,
1055
- detail,
1056
- });
1057
- }
1058
- if (text.length <= staticMaxBytes) {
1059
- const written = writeStaticUtf8(text, slotIndex);
1060
- if (written !== -1) {
1061
- task[TaskIndex.Type] = PayloadBuffer.StaticJson;
1062
- task[TaskIndex.PayloadLen] = written;
1063
- task.value = null;
1064
- return true;
1065
- }
1066
- }
1067
- task[TaskIndex.Type] = PayloadBuffer.Json;
1068
- const reserveBytes = dynamicUtf8ReserveBytes(task, text, "Json");
1069
- if (reserveBytes < 0)
1070
- return false;
1071
- const reservedSlot = reserveDynamicObject(task, reserveBytes);
1072
- const written = writeDynamicUtf8(text, task[TaskIndex.Start], reserveBytes);
1073
- if (written < 0) {
1074
- return failDynamicWriteAfterReserve(task, reservedSlot);
1075
- }
1076
- task[TaskIndex.PayloadLen] = written;
1077
- setSlotLength(reservedSlot, written);
1078
- task.value = null;
1079
- return true;
1288
+ if (arrayIsArray(objectValue) || objectProto === null) {
1289
+ return encodeObjectJson(task, slotIndex, objectValue);
1080
1290
  }
1081
1291
  if (isBufferReferenceValue(objectValue) ||
1082
1292
  isProcessSharedBufferValue(objectValue)) {
@@ -1165,6 +1375,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1165
1375
  if (reserveBytes < 0)
1166
1376
  return false;
1167
1377
  const reservedSlot = reserveDynamic(task, reserveBytes);
1378
+ if (reservedSlot === -1)
1379
+ return false;
1168
1380
  const written = writeDynamicUtf8(text, task[TaskIndex.Start], reserveBytes);
1169
1381
  if (written < 0) {
1170
1382
  return failDynamicWriteAfterReserve(task, reservedSlot);
@@ -1197,6 +1409,8 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1197
1409
  if (reserveBytes < 0)
1198
1410
  return false;
1199
1411
  const reservedSlot = reserveDynamic(task, reserveBytes);
1412
+ if (reservedSlot === -1)
1413
+ return false;
1200
1414
  const written = writeDynamicUtf8(key, task[TaskIndex.Start], reserveBytes);
1201
1415
  if (written < 0) {
1202
1416
  return failDynamicWriteAfterReserve(task, reservedSlot);
@@ -1218,8 +1432,11 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1218
1432
  options: payload?.config ?? payloadConfig,
1219
1433
  });
1220
1434
  const { free } = register({ lockSector });
1221
- const freeTaskSlot = (task) => free(getTaskSlotIndex(task));
1222
- const { readUtf8: readDynamicUtf8, readBytesCopy: readDynamicBytesCopy, readBytesBufferCopy: readDynamicBufferCopy, readBufferCopy: readDynamicBuffer, readBytesArrayBufferCopy: readDynamicArrayBufferCopy, readArrayBufferCopy: readDynamicArrayBuffer, read8BytesFloatCopy: readDynamic8BytesFloatCopy, read8BytesFloatView: readDynamic8BytesFloatView, } = createSharedDynamicBufferIO({
1435
+ // Region identity is 6 bits only for dynamic payloads. Its high bit rides in
1436
+ // End's otherwise-unused top bit, preserving the 5-bit queue-slot field.
1437
+ const freeTaskSlot = (task) => free((task[TaskIndex.slotBuffer] & TASK_SLOT_INDEX_MASK) |
1438
+ ((task[TaskIndex.End] >>> 31) << 5));
1439
+ const { readUtf8: readDynamicUtf8, readBytesCopy: readDynamicBytesCopy, readBytesBufferCopy: readDynamicBufferCopy, readBufferCopy: readDynamicBuffer, readBytesArrayBufferCopy: readDynamicArrayBufferCopy, readArrayBufferCopy: readDynamicArrayBuffer, read8BytesFloatCopy: readDynamic8BytesFloatCopy, read8BytesFloatView: readDynamic8BytesFloatView, readBytesView: readDynamicBytesView, syncGrowth: syncDynamicGrowth, } = createSharedDynamicBufferIO({
1223
1440
  sab: payloadSab,
1224
1441
  payloadConfig: resolvedPayloadConfig,
1225
1442
  textCompat: textCompat?.payload,
@@ -1230,10 +1447,13 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1230
1447
  // synchronously when building the ProcessSharedBuffer.
1231
1448
  const processSharedBufferWords = new Uint32Array(PROCESS_SHARED_BUFFER_NUMERIC_WORDS);
1232
1449
  const sharedArrayBufferWords = new Uint32Array(SHARED_ARRAY_BUFFER_NUMERIC_WORDS);
1450
+ // `readStaticU32Words` returns the whole scratch, and the SAB codec dispatches
1451
+ // on word count, so hand it a view cut to the frame's own length.
1452
+ const sharedArrayBufferWordViews = [];
1453
+ for (let count = 0; count <= SHARED_ARRAY_BUFFER_NUMERIC_WORDS; count++) {
1454
+ sharedArrayBufferWordViews.push(sharedArrayBufferWords.subarray(0, count));
1455
+ }
1233
1456
  const bufferReferenceWords = new Uint32Array(BUFFER_REFERENCE_NUMERIC_WORDS);
1234
- // TODO: remove slotIndex and make that all their callers
1235
- // store the slot in their Task, to just get it when it comes
1236
- // to the static versions of decoding
1237
1457
  return (task, slotIndex, specialFlags) => {
1238
1458
  const payloadType = task[TaskIndex.Type];
1239
1459
  if (processBoundary &&
@@ -1287,7 +1507,7 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1287
1507
  const header = task[TaskIndex.Type] === PayloadBuffer.EnvelopeStaticHeaderString
1288
1508
  ? rawHeader
1289
1509
  : parseJSON(rawHeader);
1290
- const payloadLength = task[TaskIndex.End];
1510
+ const payloadLength = task[TaskIndex.End] & 0x7FFFFFFF;
1291
1511
  const payload = payloadLength > 0
1292
1512
  ? readDynamicArrayBufferCopy(task[TaskIndex.Start], task[TaskIndex.Start] + payloadLength)
1293
1513
  : new ArrayBuffer(0);
@@ -1299,7 +1519,7 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1299
1519
  case PayloadBuffer.EnvelopeDynamicHeaderString: {
1300
1520
  const headerStart = task[TaskIndex.Start];
1301
1521
  const payloadStart = headerStart + task[TaskIndex.PayloadLen];
1302
- const payloadLength = task[TaskIndex.End];
1522
+ const payloadLength = task[TaskIndex.End] & 0x7FFFFFFF;
1303
1523
  const rawHeader = readDynamicUtf8(headerStart, payloadStart);
1304
1524
  const header = task[TaskIndex.Type] === PayloadBuffer.EnvelopeDynamicHeaderString
1305
1525
  ? rawHeader
@@ -1319,7 +1539,7 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1319
1539
  ? rawHeader
1320
1540
  : parseJSON(rawHeader);
1321
1541
  const bodyStart = task[TaskIndex.Start];
1322
- const body = decodeExternalPayload(readDynamicUtf8(bodyStart, bodyStart + task[TaskIndex.End]), processBoundary);
1542
+ const body = decodeExternalPayload(readDynamicUtf8(bodyStart, bodyStart + (task[TaskIndex.End] & 0x7FFFFFFF)), processBoundary);
1323
1543
  task.value = new Envelope(header, body);
1324
1544
  freeTaskSlot(task);
1325
1545
  return;
@@ -1333,7 +1553,7 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1333
1553
  PayloadBuffer.EnvelopeDynamicHeaderStringExternal
1334
1554
  ? rawHeader
1335
1555
  : parseJSON(rawHeader);
1336
- const body = decodeExternalPayload(readDynamicUtf8(bodyStart, bodyStart + task[TaskIndex.End]), processBoundary);
1556
+ const body = decodeExternalPayload(readDynamicUtf8(bodyStart, bodyStart + (task[TaskIndex.End] & 0x7FFFFFFF)), processBoundary);
1337
1557
  task.value = new Envelope(header, body);
1338
1558
  freeTaskSlot(task);
1339
1559
  return;
@@ -1413,21 +1633,39 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1413
1633
  return;
1414
1634
  }
1415
1635
  case PayloadBuffer.ExternalPayload:
1416
- task.value = decodeExternalPayload(readDynamicUtf8(task[TaskIndex.Start], task[TaskIndex.Start] + task[TaskIndex.PayloadLen]), processBoundary);
1636
+ task.value = decodeExternalPayload(readDynamicUtf8(task[TaskIndex.Start], task[TaskIndex.Start] + task[TaskIndex.PayloadLen]), processBoundary, lockSector);
1417
1637
  freeTaskSlot(task);
1418
1638
  return;
1419
1639
  case PayloadBuffer.StaticExternalPayload:
1420
- task.value = decodeExternalPayload(readStaticUtf8(0, task[TaskIndex.PayloadLen], slotIndex), processBoundary);
1640
+ task.value = decodeExternalPayload(readStaticUtf8(0, task[TaskIndex.PayloadLen], slotIndex), processBoundary, lockSector);
1421
1641
  return;
1422
1642
  case PayloadBuffer.ProcessSharedBuffer:
1423
1643
  task.value = decodeProcessSharedBufferNumericWords(readStaticU32Words(processSharedBufferWords, PROCESS_SHARED_BUFFER_NUMERIC_WORDS, slotIndex));
1424
1644
  return;
1425
- case PayloadBuffer.SharedArrayBuffer:
1426
- task.value = decodeSharedArrayBufferNumericWords(readStaticU32Words(sharedArrayBufferWords, task[TaskIndex.PayloadLen] >>> 2, slotIndex));
1645
+ case PayloadBuffer.SharedArrayBuffer: {
1646
+ const wordCount = task[TaskIndex.PayloadLen] >>> 2;
1647
+ task.value = decodeSharedArrayBufferNumericWords(readStaticU32Words(sharedArrayBufferWordViews[wordCount] ?? sharedArrayBufferWords, wordCount, slotIndex), lockSector);
1427
1648
  return;
1649
+ }
1428
1650
  case PayloadBuffer.BufferReference:
1429
1651
  task.value = decodeBufferReferenceNumericWords(readStaticU32Words(bufferReferenceWords, BUFFER_REFERENCE_NUMERIC_WORDS, slotIndex));
1430
1652
  return;
1653
+ case PayloadBuffer.MovedBinary: {
1654
+ const reference = decodeBufferReferenceNumericWords(readStaticU32Words(bufferReferenceWords, BUFFER_REFERENCE_NUMERIC_WORDS, slotIndex));
1655
+ if (!isBufferReferenceValue(reference)) {
1656
+ throw new TypeError("Invalid moved Uint8Array payload");
1657
+ }
1658
+ task.value = reference.toUint8Array();
1659
+ return;
1660
+ }
1661
+ case PayloadBuffer.MovedArrayBuffer: {
1662
+ const reference = decodeBufferReferenceNumericWords(readStaticU32Words(bufferReferenceWords, BUFFER_REFERENCE_NUMERIC_WORDS, slotIndex));
1663
+ if (!isBufferReferenceValue(reference)) {
1664
+ throw new TypeError("Invalid moved ArrayBuffer payload");
1665
+ }
1666
+ task.value = reference.toArrayBuffer();
1667
+ return;
1668
+ }
1431
1669
  case PayloadBuffer.Date:
1432
1670
  Uint32View[0] = task[TaskIndex.Start];
1433
1671
  Uint32View[1] = task[TaskIndex.End];
@@ -1437,6 +1675,12 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1437
1675
  task.value = parseErrorPayload(readDynamicUtf8(task[TaskIndex.Start], task[TaskIndex.Start] + task[TaskIndex.PayloadLen]));
1438
1676
  freeTaskSlot(task);
1439
1677
  return;
1678
+ // Borrowed region: decode a view into the shared arena.
1679
+ case PayloadBuffer.ArenaBinary:
1680
+ // A borrowed region can sit past the length this endpoint last saw.
1681
+ syncDynamicGrowth();
1682
+ task.value = readDynamicBytesView(task[TaskIndex.Start], task[TaskIndex.Start] + task[TaskIndex.PayloadLen]);
1683
+ return;
1440
1684
  case PayloadBuffer.Binary:
1441
1685
  {
1442
1686
  const buffer = readDynamicBufferCopy(task[TaskIndex.Start], task[TaskIndex.Start] + task[TaskIndex.PayloadLen]);