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
@@ -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,8 @@ 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
+ /** Shared region-owner mask for the `cas-mask` claim discipline. */
185
+ export declare const STEAL_CLAIM_MASK_SLOT_OFFSET_U32: number;
178
186
  export declare const HEADER_U32_LENGTH: number;
179
187
  export declare const HEADER_BYTE_LENGTH: number;
180
188
  export declare const makeTask: () => Task;
@@ -184,21 +192,20 @@ type ResolveHostOptions = {
184
192
  shouldSettle?: (task: Task) => boolean;
185
193
  activeRejectPlaceholder?: Task["reject"];
186
194
  };
195
+ export type Lock2 = ReturnType<typeof lock2>;
187
196
  /**
188
- * Complexity: 7 / 10
197
+ * Region mutual-exclusion discipline for stealing consumers.
189
198
  *
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.
199
+ * - `dekker`: per-consumer intent words and an O(N) peer survey.
200
+ * - `cas-mask`: one shared owner mask for all regions.
194
201
  */
195
- export type Lock2 = ReturnType<typeof lock2>;
202
+ export type StealClaimDiscipline = "dekker" | "cas-mask";
196
203
  export type WaitAsyncState = "not-equal" | "ok" | "timed-out";
197
204
  export type WaitAsyncResult = {
198
205
  async: boolean;
199
206
  value: WaitAsyncState | PromiseLike<WaitAsyncState>;
200
207
  };
201
- export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, consumers, consumerId, regionLanes, notifyOnHostPublish, }: {
208
+ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, sharedReturn, moveReturn, consumers, consumerId, regionLanes, stealClaim, notifyOnHostPublish, notifyHostPublish, }: {
202
209
  headers?: SharedBufferSource;
203
210
  headerSlotStrideU32?: number;
204
211
  LockBoundSector?: SharedBufferSource;
@@ -210,6 +217,17 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
210
217
  resultList?: RingQueue<Task>;
211
218
  recycleList?: RingQueue<Task>;
212
219
  processBoundary?: boolean;
220
+ /**
221
+ * Hand large returns to the consumer as a borrowed region instead of copying
222
+ * them out. Set only on a worker's return lane.
223
+ */
224
+ sharedReturn?: boolean;
225
+ /**
226
+ * Move large top-level ArrayBuffer and Uint8Array returns to the host.
227
+ * Unlike `sharedReturn`, the host owns the result and it never expires.
228
+ * Set only on a thread worker's return lane.
229
+ */
230
+ moveReturn?: boolean;
213
231
  /**
214
232
  * Number of consumer endpoints sharing this lock. `1` (default) keeps the
215
233
  * classic single-consumer path. `> 1` enables region-Dekker work stealing and
@@ -220,8 +238,12 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
220
238
  consumerId?: number;
221
239
  /** Lanes claimed per Dekker handshake. Paper rule: `slots / regionLanes >= consumers + 1`. */
222
240
  regionLanes?: number;
241
+ /** Mutual-exclusion discipline for region claiming. */
242
+ stealClaim?: StealClaimDiscipline;
223
243
  /** Notify a host-side wait after this endpoint publishes a frame. */
224
244
  notifyOnHostPublish?: boolean;
245
+ /** Runtime-native host wake used when Atomics.waitAsync cannot wake it. */
246
+ notifyHostPublish?: () => void;
225
247
  }) => {
226
248
  enlist: (task: Task) => true;
227
249
  encode: (task: Task, state?: number) => boolean;
@@ -237,6 +259,7 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
237
259
  recyclecList: RingQueue<Task>;
238
260
  resolveHost: ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }: ResolveHostOptions) => () => number;
239
261
  waitForHostChange: (timeoutMs?: number) => WaitAsyncResult | undefined;
262
+ armHostNotifier: () => boolean;
240
263
  setHostWaiterArmed: (armed: boolean) => void;
241
264
  hasPendingFrames: () => boolean;
242
265
  getPendingFrameCount: () => number;