knitting 0.1.63 → 0.1.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +623 -342
  2. package/knitting.browser.d.ts +3 -1
  3. package/knitting.browser.js +1 -1
  4. package/knitting.d.ts +3 -1
  5. package/knitting.js +2 -1
  6. package/map.md +0 -6
  7. package/package.json +10 -5
  8. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  9. package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
  10. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  11. package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
  12. package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
  14. package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
  15. package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
  16. package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
  18. package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
  19. package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
  20. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  21. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  22. package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
  23. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  24. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  25. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  26. package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
  27. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  28. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  29. package/scripts/build-native-addons.ts +5 -0
  30. package/shared-memory.d.ts +3 -0
  31. package/shared-memory.js +3 -0
  32. package/src/api.js +158 -71
  33. package/src/common/with-resolvers.js +2 -5
  34. package/src/common/worker-runtime.d.ts +7 -0
  35. package/src/common/worker-runtime.js +7 -0
  36. package/src/connections/buffer-reference.d.ts +10 -36
  37. package/src/connections/buffer-reference.js +15 -170
  38. package/src/connections/node-addons.d.ts +1 -1
  39. package/src/connections/node-addons.js +11 -1
  40. package/src/connections/shared-array-buffer-payload.d.ts +7 -0
  41. package/src/connections/shared-array-buffer-payload.js +27 -11
  42. package/src/debug/gate.js +1 -1
  43. package/src/debug/handle.d.ts +6 -1
  44. package/src/debug/handle.js +14 -6
  45. package/src/error.d.ts +9 -0
  46. package/src/error.js +16 -2
  47. package/src/knitting_buffer_pointer.cc +57 -2
  48. package/src/knitting_doorbell.cc +220 -0
  49. package/src/memory/knitting-body.d.ts +44 -0
  50. package/src/memory/knitting-body.js +51 -0
  51. package/src/memory/knitting-buffer-http.d.ts +116 -0
  52. package/src/memory/knitting-buffer-http.js +255 -0
  53. package/src/memory/knitting-buffer.d.ts +250 -0
  54. package/src/memory/knitting-buffer.js +695 -0
  55. package/src/memory/lazy-region-registry.d.ts +83 -0
  56. package/src/memory/lazy-region-registry.js +355 -0
  57. package/src/memory/lock.d.ts +80 -15
  58. package/src/memory/lock.js +473 -139
  59. package/src/memory/payloadCodec.d.ts +18 -2
  60. package/src/memory/payloadCodec.js +340 -76
  61. package/src/memory/regionRegistry.d.ts +6 -0
  62. package/src/memory/regionRegistry.js +125 -240
  63. package/src/memory/shared-buffer-io.d.ts +7 -0
  64. package/src/memory/shared-buffer-io.js +34 -8
  65. package/src/permission/protocol.d.ts +1 -0
  66. package/src/permission/protocol.js +8 -3
  67. package/src/runtime/deno-doorbell.d.ts +26 -0
  68. package/src/runtime/deno-doorbell.js +117 -0
  69. package/src/runtime/dispatcher.d.ts +13 -6
  70. package/src/runtime/dispatcher.js +101 -63
  71. package/src/runtime/host-arg-arena.d.ts +3 -0
  72. package/src/runtime/host-arg-arena.js +16 -0
  73. package/src/runtime/inline-executor.js +2 -1
  74. package/src/runtime/node-doorbell.d.ts +14 -0
  75. package/src/runtime/node-doorbell.js +84 -0
  76. package/src/runtime/pool.d.ts +30 -15
  77. package/src/runtime/pool.js +199 -151
  78. package/src/runtime/process-worker.d.ts +9 -0
  79. package/src/runtime/process-worker.js +32 -3
  80. package/src/runtime/tx-queue.d.ts +4 -6
  81. package/src/runtime/tx-queue.js +63 -48
  82. package/src/runtime/worker-common.d.ts +7 -0
  83. package/src/runtime/worker-common.js +28 -2
  84. package/src/types.d.ts +66 -78
  85. package/src/worker/loop.js +95 -60
  86. package/src/worker/rx-queue.d.ts +2 -3
  87. package/src/worker/rx-queue.js +34 -40
  88. package/src/worker/safety/index.d.ts +1 -1
  89. package/src/worker/safety/index.js +1 -1
  90. package/src/worker/safety/process.d.ts +2 -0
  91. package/src/worker/safety/process.js +8 -1
  92. package/src/worker/safety/startup.js +11 -6
  93. package/src/worker/shared-return.d.ts +9 -0
  94. package/src/worker/shared-return.js +22 -0
  95. package/src/worker/task-loader.js +1 -2
  96. package/src/worker/timers.d.ts +2 -6
  97. package/src/worker/timers.js +39 -22
  98. package/unsafe.d.ts +2 -1
  99. package/unsafe.js +2 -1
@@ -0,0 +1,83 @@
1
+ /**
2
+ * A payload-region registry with more than 64 identities and a lazy free set.
3
+ *
4
+ * Two independent changes from `regionRegistry.ts`:
5
+ *
6
+ * 1. **Width.** That registry packs `start | slot` into one Uint32 and steals
7
+ * the low 6 bits for the identity, which is exactly the 64-slot ceiling
8
+ * (regions are 64-byte aligned, so only 6 bits are spare). This one unpacks
9
+ * the table into `tableStart` + `tableSlot`, and its host-local
10
+ * `usedBits0/1` scalars become an Int32Array of `slots / 32` words plus a
11
+ * one-bit-per-word "this word still has a free identity" mask so the
12
+ * free-slot search stays O(1) instead of O(words).
13
+ *
14
+ * 2. **Laziness.** That registry's `findAndInsert` calls `updateTable()` on
15
+ * every allocation (an atomic load per word plus an O(tableLength)
16
+ * compaction) and then walks the sorted table for a first fit. `lazy` mode
17
+ * instead borrows the sender-shadow rule from `lock.ts`: reconcile only
18
+ * when the cached free set is exhausted. Staleness can only mark an already-free
19
+ * identity as busy, never a busy one as free, so a stale read costs
20
+ * capacity and never correctness -- the same false-busy-only property the
21
+ * queue's `ensureSenderStateHasFree` relies on.
22
+ *
23
+ * Between reconciles the allocator is a bump pointer over the arena: append
24
+ * past the highest live region, O(1), no scan and no insert shift. Holes
25
+ * left by freed regions below the tail are reclaimed only when the bump
26
+ * window is exhausted (a reconcile, then a sorted first-fit) or when every
27
+ * identity has drained, which resets the arena to offset 0. That is the
28
+ * "old data is fine" trade: the allocator is eventually consistent with the
29
+ * real free set, and pays for it in arena high-water, not in correctness.
30
+ */
31
+ /** Region alignment, matching the shipped registry. */
32
+ export declare const REGION_ALIGN = 64;
33
+ /** hostBits and workerBits each own one 64-byte line: 16 words = 512 slots. */
34
+ export declare const MAX_REGION_SLOTS = 512;
35
+ /**
36
+ * Largest arena this registry will manage.
37
+ *
38
+ * Offsets and sizes are folded with int32 bitwise operators (the alignment
39
+ * round-up, the bump-pointer compare), so an arena that reaches into the
40
+ * unsigned range would wrap a size negative and let it pass a bound it should
41
+ * have failed. Capping at 2 GiB minus one alignment unit keeps every
42
+ * intermediate -- `byteLength + (REGION_ALIGN - 1)` included -- inside the
43
+ * positive int32 range.
44
+ */
45
+ export declare const MAX_ARENA_BYTE_LENGTH: number;
46
+ export type LazyRegionRegistryMode = "eager" | "lazy";
47
+ export type LazyRegionRegistryOptions = {
48
+ /** Region identities. Multiple of 32, up to `MAX_REGION_SLOTS`. */
49
+ slots?: number;
50
+ /**
51
+ * `eager` reproduces the shipped policy at the new width: reconcile every
52
+ * allocation, always sorted first-fit. `lazy` reconciles on exhaustion and
53
+ * bump-allocates in between.
54
+ */
55
+ mode?: LazyRegionRegistryMode;
56
+ /** Bump window before the allocator must reclaim holes. */
57
+ arenaByteLength?: number;
58
+ lockSector?: SharedArrayBuffer;
59
+ };
60
+ export type LazyRegionRegistry = ReturnType<typeof createLazyRegionRegistry>;
61
+ export declare const createLazyRegionRegistry: ({ slots, mode, arenaByteLength, lockSector, }?: LazyRegionRegistryOptions) => {
62
+ allocRegion: (byteLength: number) => number;
63
+ regionStart: (slot: number) => number;
64
+ trimRegion: (slot: number, byteLength: number) => number;
65
+ slotContaining: (offset: number, byteLength: number) => number;
66
+ free: (slot: number) => void;
67
+ reconcile: () => boolean;
68
+ stats: () => {
69
+ slots: number;
70
+ mode: LazyRegionRegistryMode;
71
+ tableLength: number;
72
+ tailEnd: number;
73
+ highWater: number;
74
+ reconciles: number;
75
+ firstFits: number;
76
+ appends: number;
77
+ resets: number;
78
+ };
79
+ resetStats: () => void;
80
+ hostBits: Int32Array<SharedArrayBuffer>;
81
+ workerBits: Int32Array<SharedArrayBuffer>;
82
+ lockSAB: SharedArrayBuffer;
83
+ };
@@ -0,0 +1,355 @@
1
+ /**
2
+ * A payload-region registry with more than 64 identities and a lazy free set.
3
+ *
4
+ * Two independent changes from `regionRegistry.ts`:
5
+ *
6
+ * 1. **Width.** That registry packs `start | slot` into one Uint32 and steals
7
+ * the low 6 bits for the identity, which is exactly the 64-slot ceiling
8
+ * (regions are 64-byte aligned, so only 6 bits are spare). This one unpacks
9
+ * the table into `tableStart` + `tableSlot`, and its host-local
10
+ * `usedBits0/1` scalars become an Int32Array of `slots / 32` words plus a
11
+ * one-bit-per-word "this word still has a free identity" mask so the
12
+ * free-slot search stays O(1) instead of O(words).
13
+ *
14
+ * 2. **Laziness.** That registry's `findAndInsert` calls `updateTable()` on
15
+ * every allocation (an atomic load per word plus an O(tableLength)
16
+ * compaction) and then walks the sorted table for a first fit. `lazy` mode
17
+ * instead borrows the sender-shadow rule from `lock.ts`: reconcile only
18
+ * when the cached free set is exhausted. Staleness can only mark an already-free
19
+ * identity as busy, never a busy one as free, so a stale read costs
20
+ * capacity and never correctness -- the same false-busy-only property the
21
+ * queue's `ensureSenderStateHasFree` relies on.
22
+ *
23
+ * Between reconciles the allocator is a bump pointer over the arena: append
24
+ * past the highest live region, O(1), no scan and no insert shift. Holes
25
+ * left by freed regions below the tail are reclaimed only when the bump
26
+ * window is exhausted (a reconcile, then a sorted first-fit) or when every
27
+ * identity has drained, which resets the arena to offset 0. That is the
28
+ * "old data is fine" trade: the allocator is eventually consistent with the
29
+ * real free set, and pays for it in arena high-water, not in correctness.
30
+ */
31
+ import { LOCK_SECTOR_BYTE_LENGTH, PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, PAYLOAD_LOCK_WORKER_BITS_OFFSET_BYTES, } from "./lock.js";
32
+ /** Region alignment, matching the shipped registry. */
33
+ export const REGION_ALIGN = 64;
34
+ /** hostBits and workerBits each own one 64-byte line: 16 words = 512 slots. */
35
+ export const MAX_REGION_SLOTS = 512;
36
+ /**
37
+ * Largest arena this registry will manage.
38
+ *
39
+ * Offsets and sizes are folded with int32 bitwise operators (the alignment
40
+ * round-up, the bump-pointer compare), so an arena that reaches into the
41
+ * unsigned range would wrap a size negative and let it pass a bound it should
42
+ * have failed. Capping at 2 GiB minus one alignment unit keeps every
43
+ * intermediate -- `byteLength + (REGION_ALIGN - 1)` included -- inside the
44
+ * positive int32 range.
45
+ */
46
+ export const MAX_ARENA_BYTE_LENGTH = 0x7fffffff - (REGION_ALIGN - 1);
47
+ const clz32 = Math.clz32;
48
+ const a_load = Atomics.load;
49
+ const a_store = Atomics.store;
50
+ const a_xor = Atomics.xor;
51
+ /** Index of the lowest set bit. */
52
+ const ctz = (word) => 31 - clz32((word & -word) >>> 0);
53
+ export const createLazyRegionRegistry = ({ slots = 128, mode = "lazy", arenaByteLength = 64 * 1024 * 1024, lockSector, } = {}) => {
54
+ if (slots < 32 || slots > MAX_REGION_SLOTS || (slots & 31) !== 0) {
55
+ throw new RangeError(`slots must be a multiple of 32 in [32, ${MAX_REGION_SLOTS}]`);
56
+ }
57
+ if (!Number.isSafeInteger(arenaByteLength) || arenaByteLength < REGION_ALIGN ||
58
+ arenaByteLength > MAX_ARENA_BYTE_LENGTH) {
59
+ throw new RangeError(`arenaByteLength must be an integer in ` +
60
+ `[${REGION_ALIGN}, ${MAX_ARENA_BYTE_LENGTH}]`);
61
+ }
62
+ const words = slots >>> 5;
63
+ const lazy = mode === "lazy";
64
+ /**
65
+ * Identities are addressed by index, never by mask: `slots` is only required
66
+ * to be a multiple of 32, so `slot & (slots - 1)` is a valid reduction only
67
+ * when `slots` happens to be a power of two. At `slots: 96` it folded slot 32
68
+ * onto slot 0, handing two live regions the same offset and the same release
69
+ * bit. Callers pass identities this registry itself handed out, so an
70
+ * out-of-range one is a bug worth surfacing rather than silently aliasing.
71
+ */
72
+ const assertSlot = (slot) => {
73
+ if (!Number.isSafeInteger(slot) || slot < 0 || slot >= slots) {
74
+ throw new RangeError(`region identity ${slot} outside 0..${slots - 1}`);
75
+ }
76
+ return slot;
77
+ };
78
+ const lockSAB = lockSector ?? new SharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH);
79
+ const hostBits = new Int32Array(lockSAB, PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, words);
80
+ const workerBits = new Int32Array(lockSAB, PAYLOAD_LOCK_WORKER_BITS_OFFSET_BYTES, words);
81
+ // Host-local shadows. `used` is the host's belief about live identities;
82
+ // `hostLast` mirrors what the host has published into `hostBits`.
83
+ const used = new Int32Array(words);
84
+ const hostLast = new Int32Array(words);
85
+ const freedScratch = new Int32Array(words);
86
+ // One bit per word: set while that word still holds a free identity.
87
+ const ALL_WORDS = words === 32 ? -1 : ((1 << words) - 1) | 0;
88
+ let freeWordMask = ALL_WORDS;
89
+ // Extent table, sorted by offset, unpacked so the identity is not confined
90
+ // to the spare alignment bits of the offset.
91
+ const tableStart = new Uint32Array(slots);
92
+ const tableSlot = new Uint16Array(slots);
93
+ let tableLength = 0;
94
+ const startBySlot = new Uint32Array(slots);
95
+ const sizeBySlot = new Uint32Array(slots);
96
+ /** End of the highest live region: the bump pointer. */
97
+ let tailEnd = 0;
98
+ let highWater = 0;
99
+ let reconciles = 0;
100
+ let firstFits = 0;
101
+ let resets = 0;
102
+ let appends = 0;
103
+ const findFreeSlot = () => {
104
+ let mask = freeWordMask;
105
+ while (mask !== 0) {
106
+ const w = ctz(mask);
107
+ const available = ~used[w];
108
+ if (available !== 0) {
109
+ const slot = (w << 5) + ctz(available);
110
+ return slot < slots ? slot : -1;
111
+ }
112
+ freeWordMask = (freeWordMask & ~(1 << w)) | 0;
113
+ mask = freeWordMask;
114
+ }
115
+ return -1;
116
+ };
117
+ const reserveSlot = (slot) => {
118
+ const w = slot >>> 5;
119
+ const bit = (1 << (slot & 31)) | 0;
120
+ used[w] = (used[w] | bit) | 0;
121
+ hostLast[w] = (hostLast[w] ^ bit) | 0;
122
+ a_store(hostBits, w, hostLast[w]);
123
+ if (~used[w] === 0)
124
+ freeWordMask = (freeWordMask & ~(1 << w)) | 0;
125
+ };
126
+ /**
127
+ * Fold the consumer's release toggles into the host shadow. Returns true
128
+ * when anything was reclaimed. This is the only place that reads the shared
129
+ * words, so it is the only cost laziness removes from the alloc path.
130
+ */
131
+ const reconcile = () => {
132
+ reconciles++;
133
+ let anyFreed = 0;
134
+ let stillUsed = 0;
135
+ for (let w = 0; w < words; w++) {
136
+ const live = used[w];
137
+ if (live === 0) {
138
+ freedScratch[w] = 0;
139
+ continue;
140
+ }
141
+ const freed = (~(hostLast[w] ^ a_load(workerBits, w)) & live) | 0;
142
+ freedScratch[w] = freed;
143
+ anyFreed |= freed;
144
+ stillUsed |= (live & ~freed) | 0;
145
+ }
146
+ if (anyFreed === 0)
147
+ return false;
148
+ // Every identity drained: reset the arena instead of compacting.
149
+ if (stillUsed === 0) {
150
+ resets++;
151
+ tableLength = 0;
152
+ tailEnd = 0;
153
+ used.fill(0);
154
+ freeWordMask = ALL_WORDS;
155
+ return true;
156
+ }
157
+ for (let w = 0; w < words; w++) {
158
+ const freed = freedScratch[w];
159
+ if (freed === 0)
160
+ continue;
161
+ used[w] = (used[w] & ~freed) | 0;
162
+ freeWordMask = (freeWordMask | (1 << w)) | 0;
163
+ }
164
+ let write = 0;
165
+ for (let read = 0; read < tableLength; read++) {
166
+ const slot = tableSlot[read];
167
+ if ((freedScratch[slot >>> 5] & (1 << (slot & 31))) !== 0)
168
+ continue;
169
+ if (write !== read) {
170
+ tableStart[write] = tableStart[read];
171
+ tableSlot[write] = slot;
172
+ }
173
+ write++;
174
+ }
175
+ tableLength = write;
176
+ // Regions never overlap, so sorted by start is sorted by end.
177
+ tailEnd = write === 0
178
+ ? 0
179
+ : (tableStart[write - 1] + sizeBySlot[tableSlot[write - 1]]) >>> 0;
180
+ return true;
181
+ };
182
+ /** O(1) append past the highest live region. */
183
+ const append = (slot, size) => {
184
+ appends++;
185
+ const start = tailEnd;
186
+ tableStart[tableLength] = start;
187
+ tableSlot[tableLength] = slot;
188
+ tableLength++;
189
+ tailEnd = start + size;
190
+ if (tailEnd > highWater)
191
+ highWater = tailEnd;
192
+ startBySlot[slot] = start;
193
+ sizeBySlot[slot] = size;
194
+ reserveSlot(slot);
195
+ return slot;
196
+ };
197
+ /** Sorted first fit over the holes left below the tail. */
198
+ const firstFit = (slot, size) => {
199
+ firstFits++;
200
+ let previousEnd = 0;
201
+ let insertAt = tableLength;
202
+ for (let at = 0; at < tableLength; at++) {
203
+ const start = tableStart[at];
204
+ if (((start - previousEnd) >>> 0) >= (size >>> 0)) {
205
+ insertAt = at;
206
+ break;
207
+ }
208
+ previousEnd = (start + sizeBySlot[tableSlot[at]]) >>> 0;
209
+ }
210
+ if (previousEnd + size > arenaByteLength)
211
+ return -1;
212
+ for (let at = tableLength; at > insertAt; at--) {
213
+ tableStart[at] = tableStart[at - 1];
214
+ tableSlot[at] = tableSlot[at - 1];
215
+ }
216
+ tableStart[insertAt] = previousEnd;
217
+ tableSlot[insertAt] = slot;
218
+ tableLength++;
219
+ startBySlot[slot] = previousEnd;
220
+ sizeBySlot[slot] = size;
221
+ const end = previousEnd + size;
222
+ if (end > tailEnd)
223
+ tailEnd = end;
224
+ if (end > highWater)
225
+ highWater = end;
226
+ reserveSlot(slot);
227
+ return slot;
228
+ };
229
+ const allocRegion = (byteLength) => {
230
+ // `byteLength | 0` truncated instead of rejecting: a 2 GiB request wrapped
231
+ // to a negative size, passed the arena bound it should have failed, and
232
+ // left `tailEnd` negative for the life of the pool. Anything the arena
233
+ // cannot hold is -1, which is the caller's signal to take the overflow
234
+ // path -- the same answer it already gets when identities run out.
235
+ if (!Number.isSafeInteger(byteLength) || byteLength < 0 ||
236
+ byteLength > arenaByteLength) {
237
+ return -1;
238
+ }
239
+ const size = (byteLength + (REGION_ALIGN - 1)) & ~(REGION_ALIGN - 1);
240
+ if (!lazy)
241
+ reconcile();
242
+ let slot = findFreeSlot();
243
+ if (slot === -1) {
244
+ if (!reconcile())
245
+ return -1;
246
+ slot = findFreeSlot();
247
+ if (slot === -1)
248
+ return -1;
249
+ }
250
+ if (tableLength >= slots || (tailEnd + size) > arenaByteLength) {
251
+ reconcile();
252
+ slot = findFreeSlot();
253
+ if (slot === -1 || tableLength >= slots)
254
+ return -1;
255
+ }
256
+ if (lazy && (tailEnd + size) <= arenaByteLength)
257
+ return append(slot, size);
258
+ return firstFit(slot, size);
259
+ };
260
+ const regionStart = (slot) => startBySlot[assertSlot(slot)];
261
+ /**
262
+ * Shrink a region to `byteLength`, returning its new aligned size.
263
+ *
264
+ * This is what makes a streamed body of unknown length affordable: reserve
265
+ * an upper bound, fill it, then give back what was not used. When the region
266
+ * is the tail allocation -- the common case, since it was just bump
267
+ * allocated -- the bump pointer rewinds and nothing is wasted at all.
268
+ * Otherwise the tail of the region simply becomes a hole, reclaimed on the
269
+ * next reconcile like any other.
270
+ *
271
+ * Growing is not offered: it would have to relocate the bytes, which is the
272
+ * copy the caller is trying to avoid.
273
+ */
274
+ const trimRegion = (slot, byteLength) => {
275
+ const index = assertSlot(slot);
276
+ const current = sizeBySlot[index];
277
+ if (!Number.isSafeInteger(byteLength) || byteLength < 0)
278
+ return current;
279
+ const size = (byteLength + (REGION_ALIGN - 1)) & ~(REGION_ALIGN - 1);
280
+ if (size >= current)
281
+ return current;
282
+ const start = startBySlot[index];
283
+ if (start + current === tailEnd)
284
+ tailEnd = start + size;
285
+ sizeBySlot[index] = size;
286
+ return size;
287
+ };
288
+ /**
289
+ * The identity whose extent contains [offset, offset + byteLength), or -1.
290
+ * Binary search over the offset-sorted extent table: exact, no side table to
291
+ * maintain, and it accepts interior offsets so a subarray of a borrowed view
292
+ * still resolves to its region.
293
+ *
294
+ * Under lazy mode the table can still list regions the consumer has already
295
+ * released but the owner has not reconciled. That is deliberate: those bytes
296
+ * are not reusable until the owner observes the release, so a view into one
297
+ * is still valid.
298
+ */
299
+ const slotContaining = (offset, byteLength) => {
300
+ let lo = 0;
301
+ let hi = tableLength - 1;
302
+ while (lo <= hi) {
303
+ const mid = (lo + hi) >>> 1;
304
+ const start = tableStart[mid];
305
+ if (offset < start) {
306
+ hi = mid - 1;
307
+ continue;
308
+ }
309
+ const slot = tableSlot[mid];
310
+ const end = start + sizeBySlot[slot];
311
+ if (offset >= end) {
312
+ lo = mid + 1;
313
+ continue;
314
+ }
315
+ // Starts inside this region: it fits, or it runs past the extent.
316
+ return offset + byteLength <= end ? slot : -1;
317
+ }
318
+ return -1;
319
+ };
320
+ /** Consumer-side release: one XOR toggle per region, as shipped. */
321
+ const free = (slot) => {
322
+ const s = assertSlot(slot);
323
+ a_xor(workerBits, s >>> 5, (1 << (s & 31)) | 0);
324
+ };
325
+ const stats = () => ({
326
+ slots,
327
+ mode,
328
+ tableLength,
329
+ tailEnd,
330
+ highWater,
331
+ reconciles,
332
+ firstFits,
333
+ appends,
334
+ resets,
335
+ });
336
+ const resetStats = () => {
337
+ reconciles = 0;
338
+ firstFits = 0;
339
+ appends = 0;
340
+ resets = 0;
341
+ };
342
+ return {
343
+ allocRegion,
344
+ regionStart,
345
+ trimRegion,
346
+ slotContaining,
347
+ free,
348
+ reconcile,
349
+ stats,
350
+ resetStats,
351
+ hostBits,
352
+ workerBits,
353
+ lockSAB,
354
+ };
355
+ };
@@ -5,9 +5,6 @@ export declare const registerLockPayloadCodec: (encode: EncodePayloadFactory, de
5
5
  import { type SharedBufferSource } from "../common/shared-buffer-region.js";
6
6
  import { type LockBufferTextCompat } from "../common/shared-buffer-text.js";
7
7
  import { type PayloadBufferOptions } from "./payload-config.js";
8
- /**
9
- * TODO: Compose all the instance where the array is passed as argument
10
- */
11
8
  export declare const PayloadSignal: {
12
9
  readonly UNREACHABLE: 0;
13
10
  readonly BigInt: 2;
@@ -62,6 +59,12 @@ export declare const PayloadBuffer: {
62
59
  readonly EnvelopeDynamicHeaderStringExternal: 52;
63
60
  readonly NumericArray: 53;
64
61
  readonly StaticNumericArray: 54;
62
+ /** Binary payload already stored in the dynamic arena. */
63
+ readonly ArenaBinary: 55;
64
+ /** A returned Uint8Array whose ArrayBuffer ownership was moved to the host. */
65
+ readonly MovedBinary: 56;
66
+ /** A returned ArrayBuffer whose ownership was moved to the host. */
67
+ readonly MovedArrayBuffer: 57;
65
68
  };
66
69
  export type PayloadBuffer = typeof PayloadBuffer[keyof typeof PayloadBuffer];
67
70
  export declare const payloadBufferName: (value: number) => string;
@@ -97,6 +100,8 @@ export declare const finishPromisePayload: (task: Task) => void;
97
100
  export declare const isPromisePayloadPending: (task: Task) => boolean;
98
101
  export declare const resetTaskLocalFlags: (task: Task) => void;
99
102
  export declare const addTaskFinalizer: (task: Task, finalizer: () => void) => void;
103
+ /** Take an owner-lifetime transport hold, if the value supports one. */
104
+ export declare const takePayloadTransportHold: (value: unknown) => (() => void) | undefined;
100
105
  export declare const attachPayloadTransportFinalizer: (task: Task, value: unknown) => void;
101
106
  export declare const runTaskFinalizers: (task: Task) => void;
102
107
  export declare const TaskIndex: {
@@ -107,18 +112,19 @@ export declare const TaskIndex: {
107
112
  /**
108
113
  * Host -> worker request function id (low 16 bits).
109
114
  * High 16 bits are reserved for caller metadata on request path.
110
- * NOTE: shares the same storage word as `FlagsToHost`.
115
+ * Shares storage with `FlagsToHost`.
111
116
  */
112
117
  readonly FunctionID: 0;
113
118
  readonly ID: 1;
114
119
  readonly Type: 2;
115
120
  readonly Start: 3;
116
- readonly End: 4;
117
- readonly PayloadLen: 5;
118
121
  /**
119
- * Low 5 bits: region slot index (0..31).
120
- * High 27 bits: reserved for caller metadata (e.g. enqueue timing).
122
+ * Payload length; bit 31 stores the high bit of a dynamic-region index.
123
+ * Write the length before `tagTaskSlot()` and mask bit 31 when reading it.
121
124
  */
125
+ readonly End: 4;
126
+ readonly PayloadLen: 5;
127
+ /** Low 5 bits hold the dynamic-region index; the remaining bits hold metadata. */
122
128
  readonly slotBuffer: 6;
123
129
  readonly Size: 8;
124
130
  /**
@@ -175,6 +181,26 @@ export declare const STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32: number;
175
181
  export declare const STEAL_LIVE_SLOT_OFFSET_U32: number;
176
182
  /** Host-owned arm word for the return-lock completion doorbell. */
177
183
  export declare const DOORBELL_ARMED_SLOT_OFFSET_U32: number;
184
+ /**
185
+ * Non-wrapping 64-bit claim head for the `ticket` discipline. The producer
186
+ * publishes only the low 32 bits of the tail. A successful head CAS validates
187
+ * the snapshot: with that head unchanged, at most 32 tickets can be pending,
188
+ * so unsigned subtraction recovers the exact distance even across tail wrap.
189
+ * Head and tail occupy different slots' header lines (slots 0 and 1).
190
+ */
191
+ export declare const STEAL_TICKET_HEAD_SLOT_OFFSET_U32: number;
192
+ /** Negative head permanently closes a failed ticket queue. */
193
+ export declare const STEAL_TICKET_FAILED = -1n;
194
+ /**
195
+ * Publication-order ring. `order[t & 31]` is the slot the producer filled for
196
+ * ticket `t`, so a ticket names a lane without the ticket having to *be* the
197
+ * lane: the producer keeps its free-bit allocation and claims follow publication
198
+ * order. Cell `i` uses the former ACK word; words 14..15
199
+ * are reserved for the head in slot 0 and the tail in slot 1.
200
+ */
201
+ export declare const STEAL_TICKET_ORDER_SLOT_OFFSET_U32: number;
202
+ /** Ring cells, and therefore the wrap mask, follow the slot count. */
203
+ export declare const STEAL_TICKET_RING_MASK: number;
178
204
  export declare const HEADER_U32_LENGTH: number;
179
205
  export declare const HEADER_BYTE_LENGTH: number;
180
206
  export declare const makeTask: () => Task;
@@ -184,21 +210,39 @@ type ResolveHostOptions = {
184
210
  shouldSettle?: (task: Task) => boolean;
185
211
  activeRejectPlaceholder?: Task["reject"];
186
212
  };
213
+ export type Lock2 = ReturnType<typeof lock2>;
187
214
  /**
188
- * Complexity: 7 / 10
215
+ * Claim discipline for stealing consumers.
189
216
  *
190
- * SAFETY:
191
- * - Single producer/consumer; do not call encode/decode concurrently.
192
- * - Shared buffers must be the same between host/worker.
193
- * - encode/decode are not re-entrant; payload codec uses a shared scratch buffer.
217
+ * - `dekker`: per-consumer intent words and an O(N) peer survey.
218
+ * - `ticket`: one monotonic counter; a CAS on the head claims lanes in
219
+ * publication order, with no regions and no per-consumer state.
194
220
  */
195
- export type Lock2 = ReturnType<typeof lock2>;
221
+ export declare const STEAL_CLAIM_DISCIPLINES: readonly ["dekker", "ticket"];
222
+ export type StealClaimDiscipline = typeof STEAL_CLAIM_DISCIPLINES[number];
223
+ /**
224
+ * Validate a claim discipline coming from an untrusted source — an env var, or
225
+ * a JS caller the type system never saw.
226
+ *
227
+ * This throws rather than falling back, because a silent fallback is worse than
228
+ * a crash here: a stale `cas-mask` setting or a typo would quietly run Dekker,
229
+ * and every benchmark and test built on it would report Dekker's numbers under
230
+ * another name. Both sides of a lock must also agree, so an unrecognised value
231
+ * that resolved differently on host and worker would corrupt the protocol.
232
+ */
233
+ export declare const assertStealClaim: (value: unknown, source: string) => StealClaimDiscipline;
234
+ /**
235
+ * The discipline chosen when nothing selects one. Host and worker default
236
+ * independently, so this must be a single shared constant: two sides landing on
237
+ * different disciplines would read the same buffer under different protocols.
238
+ */
239
+ export declare const DEFAULT_STEAL_CLAIM: StealClaimDiscipline;
196
240
  export type WaitAsyncState = "not-equal" | "ok" | "timed-out";
197
241
  export type WaitAsyncResult = {
198
242
  async: boolean;
199
243
  value: WaitAsyncState | PromiseLike<WaitAsyncState>;
200
244
  };
201
- export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, consumers, consumerId, regionLanes, notifyOnHostPublish, }: {
245
+ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, sharedReturn, moveReturn, consumers, consumerId, regionLanes, stealClaim, notifyOnHostPublish, notifyHostPublish, traceClaim, }: {
202
246
  headers?: SharedBufferSource;
203
247
  headerSlotStrideU32?: number;
204
248
  LockBoundSector?: SharedBufferSource;
@@ -210,6 +254,17 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
210
254
  resultList?: RingQueue<Task>;
211
255
  recycleList?: RingQueue<Task>;
212
256
  processBoundary?: boolean;
257
+ /**
258
+ * Hand large returns to the consumer as a borrowed region instead of copying
259
+ * them out. Set only on a worker's return lane.
260
+ */
261
+ sharedReturn?: boolean;
262
+ /**
263
+ * Move large top-level ArrayBuffer and Uint8Array returns to the host.
264
+ * Unlike `sharedReturn`, the host owns the result and it never expires.
265
+ * Set only on a thread worker's return lane.
266
+ */
267
+ moveReturn?: boolean;
213
268
  /**
214
269
  * Number of consumer endpoints sharing this lock. `1` (default) keeps the
215
270
  * classic single-consumer path. `> 1` enables region-Dekker work stealing and
@@ -220,8 +275,17 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
220
275
  consumerId?: number;
221
276
  /** Lanes claimed per Dekker handshake. Paper rule: `slots / regionLanes >= consumers + 1`. */
222
277
  regionLanes?: number;
278
+ /** Mutual-exclusion discipline for region claiming. */
279
+ stealClaim?: StealClaimDiscipline;
223
280
  /** Notify a host-side wait after this endpoint publishes a frame. */
224
281
  notifyOnHostPublish?: boolean;
282
+ /** Runtime-native host wake used when Atomics.waitAsync cannot wake it. */
283
+ notifyHostPublish?: () => void;
284
+ /**
285
+ * Debug sink for steal claims. Only consulted when `decode` is chosen, so an
286
+ * untraced lock returns the bare claim function; see {@link traceStealClaims}.
287
+ */
288
+ traceClaim?: (message: string) => void;
225
289
  }) => {
226
290
  enlist: (task: Task) => true;
227
291
  encode: (task: Task, state?: number) => boolean;
@@ -237,6 +301,7 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
237
301
  recyclecList: RingQueue<Task>;
238
302
  resolveHost: ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }: ResolveHostOptions) => () => number;
239
303
  waitForHostChange: (timeoutMs?: number) => WaitAsyncResult | undefined;
304
+ armHostNotifier: () => boolean;
240
305
  setHostWaiterArmed: (armed: boolean) => void;
241
306
  hasPendingFrames: () => boolean;
242
307
  getPendingFrameCount: () => number;