knitting 0.1.63 → 0.1.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +525 -335
  2. package/knitting.browser.js +1 -1
  3. package/map.md +0 -6
  4. package/package.json +3 -3
  5. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  6. package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
  7. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  8. package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
  9. package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
  11. package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
  12. package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
  13. package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
  14. package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
  15. package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
  16. package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
  17. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  18. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  19. package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
  20. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  21. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  22. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  23. package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
  24. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  25. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  26. package/scripts/build-native-addons.ts +5 -0
  27. package/shared-memory.d.ts +3 -0
  28. package/shared-memory.js +3 -0
  29. package/src/api.js +105 -42
  30. package/src/common/with-resolvers.js +2 -5
  31. package/src/common/worker-runtime.d.ts +7 -0
  32. package/src/common/worker-runtime.js +7 -0
  33. package/src/connections/buffer-reference.d.ts +10 -36
  34. package/src/connections/buffer-reference.js +15 -170
  35. package/src/connections/node-addons.d.ts +1 -1
  36. package/src/connections/shared-array-buffer-payload.d.ts +7 -0
  37. package/src/connections/shared-array-buffer-payload.js +27 -11
  38. package/src/knitting_buffer_pointer.cc +57 -2
  39. package/src/knitting_doorbell.cc +220 -0
  40. package/src/memory/knitting-body.d.ts +44 -0
  41. package/src/memory/knitting-body.js +51 -0
  42. package/src/memory/knitting-buffer-http.d.ts +116 -0
  43. package/src/memory/knitting-buffer-http.js +255 -0
  44. package/src/memory/knitting-buffer.d.ts +250 -0
  45. package/src/memory/knitting-buffer.js +695 -0
  46. package/src/memory/lazy-region-registry.d.ts +83 -0
  47. package/src/memory/lazy-region-registry.js +355 -0
  48. package/src/memory/lock.d.ts +38 -15
  49. package/src/memory/lock.js +205 -79
  50. package/src/memory/payloadCodec.d.ts +18 -2
  51. package/src/memory/payloadCodec.js +309 -65
  52. package/src/memory/regionRegistry.d.ts +6 -0
  53. package/src/memory/regionRegistry.js +125 -240
  54. package/src/memory/shared-buffer-io.d.ts +7 -0
  55. package/src/memory/shared-buffer-io.js +34 -8
  56. package/src/runtime/deno-doorbell.d.ts +26 -0
  57. package/src/runtime/deno-doorbell.js +117 -0
  58. package/src/runtime/dispatcher.d.ts +8 -6
  59. package/src/runtime/dispatcher.js +80 -58
  60. package/src/runtime/host-arg-arena.d.ts +3 -0
  61. package/src/runtime/host-arg-arena.js +16 -0
  62. package/src/runtime/node-doorbell.d.ts +14 -0
  63. package/src/runtime/node-doorbell.js +84 -0
  64. package/src/runtime/pool.d.ts +21 -15
  65. package/src/runtime/pool.js +104 -116
  66. package/src/runtime/process-worker.d.ts +9 -0
  67. package/src/runtime/process-worker.js +22 -2
  68. package/src/runtime/tx-queue.d.ts +2 -5
  69. package/src/runtime/tx-queue.js +52 -48
  70. package/src/types.d.ts +35 -71
  71. package/src/worker/loop.js +79 -57
  72. package/src/worker/rx-queue.d.ts +2 -3
  73. package/src/worker/rx-queue.js +34 -40
  74. package/src/worker/shared-return.d.ts +9 -0
  75. package/src/worker/shared-return.js +22 -0
  76. package/src/worker/task-loader.js +1 -2
  77. package/src/worker/timers.d.ts +2 -6
  78. package/src/worker/timers.js +14 -19
  79. package/unsafe.d.ts +2 -1
  80. package/unsafe.js +2 -1
@@ -1,11 +1,17 @@
1
1
  import type { Task } from "./lock.js";
2
2
  import { type SharedBufferSource } from "../common/shared-buffer-region.js";
3
+ /** Dynamic payload-region identities; independent from 32 queue header slots. */
4
+ export declare const DYNAMIC_PAYLOAD_SLOTS = 64;
5
+ export declare const DYNAMIC_PAYLOAD_SLOT_MASK: number;
3
6
  export type RegisterMalloc = ReturnType<typeof register>;
4
7
  export declare const register: ({ lockSector, }: {
5
8
  lockSector?: SharedBufferSource;
6
9
  }) => {
7
10
  allocTask: (task: Task) => number;
11
+ allocRegion: (payloadLen: number) => number;
12
+ regionStart: (slot: number) => number;
8
13
  setSlotLength: (slotIndex: number, payloadLen: number) => boolean;
14
+ tagTaskSlot: (task: Task, slot: number) => void;
9
15
  lockSAB: import("../common/shared-buffer-region.js").SharedBuffer;
10
16
  free: (index: number) => void;
11
17
  hostBits: Int32Array<import("../common/shared-buffer-region.js").SharedBuffer>;
@@ -1,282 +1,167 @@
1
- import { LOCK_SECTOR_BYTE_LENGTH, LockBound, PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, PAYLOAD_LOCK_WORKER_BITS_OFFSET_BYTES, TASK_SLOT_INDEX_MASK, TaskIndex, } from "./lock.js";
1
+ import { LOCK_SECTOR_BYTE_LENGTH, PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, PAYLOAD_LOCK_WORKER_BITS_OFFSET_BYTES, TASK_SLOT_INDEX_MASK, TaskIndex, } from "./lock.js";
2
2
  import { createWasmSharedArrayBuffer } from "../common/runtime.js";
3
3
  import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
4
- // Low 5 bits = slot index, high 27 bits = caller meta.
5
- // Inlined from setTaskSlotIndex to avoid cross-closure call on hot path.
6
- const SLOT_META_PACKED_MASK = 0xFFFFFFE0; // (~0x1F) >>> 0
4
+ /** Dynamic payload-region identities; independent from 32 queue header slots. */
5
+ export const DYNAMIC_PAYLOAD_SLOTS = 64;
6
+ export const DYNAMIC_PAYLOAD_SLOT_MASK = DYNAMIC_PAYLOAD_SLOTS - 1;
7
+ const PAYLOAD_LOCK_STATE_WORDS = DYNAMIC_PAYLOAD_SLOTS / 32;
8
+ // Low 6 bits store the region index; the remaining bits store its byte offset.
9
+ const EMPTY = 0xFFFFFFBF >>> 0; // 111...10111111
10
+ const SLOT_META_PACKED_MASK = (~TASK_SLOT_INDEX_MASK) >>> 0;
7
11
  export const register = ({ lockSector, }) => {
8
12
  const lockRegion = toSharedBufferRegion(lockSector ?? createWasmSharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH));
9
13
  const lockSAB = lockRegion.sab;
10
- const hostBits = new Int32Array(lockSAB, lockRegion.byteOffset + PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, 1);
11
- const workerBits = new Int32Array(lockSAB, lockRegion.byteOffset + PAYLOAD_LOCK_WORKER_BITS_OFFSET_BYTES, 1);
12
- const startAndIndex = new Uint32Array(LockBound.slots);
13
- const size64bit = new Uint32Array(LockBound.slots);
14
- const clz32 = Math.clz32;
14
+ // Dynamic-region state shares the queue lock's cache-line padding.
15
+ const hostBits = new Int32Array(lockSAB, lockRegion.byteOffset + PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, PAYLOAD_LOCK_STATE_WORDS);
16
+ const workerBits = new Int32Array(lockSAB, lockRegion.byteOffset + PAYLOAD_LOCK_WORKER_BITS_OFFSET_BYTES, PAYLOAD_LOCK_STATE_WORDS);
17
+ const startAndIndex = new Uint32Array(DYNAMIC_PAYLOAD_SLOTS);
18
+ const size64bit = new Uint32Array(DYNAMIC_PAYLOAD_SLOTS);
19
+ // Keep region offsets indexed by slot for O(1) lookup.
20
+ const startBySlot = new Uint32Array(DYNAMIC_PAYLOAD_SLOTS);
15
21
  const a_load = Atomics.load;
16
22
  const a_store = Atomics.store;
17
23
  const a_xor = Atomics.xor;
18
- const EMPTY = 0xFFFFFFFF >>> 0;
19
- const SLOT_MASK = TASK_SLOT_INDEX_MASK;
24
+ const SLOT_MASK = DYNAMIC_PAYLOAD_SLOT_MASK;
20
25
  const START_MASK = (~SLOT_MASK) >>> 0;
21
26
  startAndIndex.fill(EMPTY);
22
27
  let tableLength = 0;
23
- let usedBits = 0 | 0;
24
- // scalar state (faster than Uint32Array(1))
25
- let hostLast = 0 | 0;
28
+ // Keep the common <=32-region path in scalar words.
29
+ let usedBits0 = 0 | 0;
30
+ let usedBits1 = 0 | 0;
31
+ let hostLast0 = 0 | 0;
32
+ let hostLast1 = 0 | 0;
26
33
  const startAndIndexToArray = (length) => startAndIndex.slice(0, length);
27
- const compactFreeBitsStable = (b, freeBits) => {
34
+ const slotWord = (slot) => slot >>> 5;
35
+ const slotBit = (slot) => 1 << (slot & 31);
36
+ const compactFreed = (freeBits0, freeBits1) => {
28
37
  const sai = startAndIndex;
29
- let w = 0 | 0;
30
- b = b | 0;
31
- freeBits = freeBits >>> 0;
32
- for (let r = 0; r < b; r++) {
33
- const v = sai[r];
34
- if (v === EMPTY)
38
+ let write = 0;
39
+ for (let read = 0; read < tableLength; read++) {
40
+ const value = sai[read];
41
+ const slot = value & SLOT_MASK;
42
+ const freeBits = slot < 32 ? freeBits0 : freeBits1;
43
+ if ((freeBits & slotBit(slot)) !== 0)
35
44
  continue;
36
- if ((freeBits & (1 << (v & SLOT_MASK))) !== 0)
37
- continue;
38
- if (w !== r)
39
- sai[w] = v;
40
- w++;
45
+ if (write !== read)
46
+ sai[write] = value;
47
+ write++;
48
+ }
49
+ for (let at = write; at < tableLength; at++)
50
+ sai[at] = EMPTY;
51
+ tableLength = write;
52
+ };
53
+ const findFreeSlot = () => {
54
+ let available = ~usedBits0;
55
+ if (available !== 0) {
56
+ return 31 - Math.clz32((available & -available) >>> 0);
41
57
  }
42
- const live = w;
43
- for (; w < b; w++)
44
- sai[w] = EMPTY;
45
- return live;
58
+ available = ~usedBits1;
59
+ return available === 0
60
+ ? -1
61
+ : 32 + 31 - Math.clz32((available & -available) >>> 0);
46
62
  };
63
+ /** Store the sixth region-index bit in bit 31 of the task length word. */
64
+ const tagTaskSlot = (task, slot) => {
65
+ task[TaskIndex.End] = ((task[TaskIndex.End] & 0x7FFFFFFF) | ((slot & 32) << 26)) >>> 0;
66
+ };
67
+ const reserveSlot = (slot) => {
68
+ const bit = slotBit(slot);
69
+ if (slot < 32) {
70
+ usedBits0 = (usedBits0 | bit) | 0;
71
+ hostLast0 = (hostLast0 ^ bit) | 0;
72
+ a_store(hostBits, 0, hostLast0);
73
+ }
74
+ else {
75
+ usedBits1 = (usedBits1 | bit) | 0;
76
+ hostLast1 = (hostLast1 ^ bit) | 0;
77
+ a_store(hostBits, 1, hostLast1);
78
+ }
79
+ };
80
+ // Reconcile completed reads and remove freed regions from the table.
47
81
  const updateTable = () => {
48
- // state = which bits are currently "in use" under toggle-protocol
49
- const w = a_load(workerBits, 0) | 0;
50
- const state = (hostLast ^ w) >>> 0;
51
- // freeBits = bits where host/worker agree (toggle resolved)
52
- let freeBits = (~state) >>> 0;
53
- if (tableLength === 0 || freeBits === 0)
82
+ if (tableLength === 0)
54
83
  return;
55
- // all cleared
56
- if (freeBits === EMPTY) {
84
+ const freeBits0 = usedBits0 === 0
85
+ ? 0
86
+ : (~(hostLast0 ^ a_load(workerBits, 0)) & usedBits0) | 0;
87
+ const freeBits1 = usedBits1 === 0
88
+ ? 0
89
+ : (~(hostLast1 ^ a_load(workerBits, 1)) & usedBits1) | 0;
90
+ if ((freeBits0 | freeBits1) === 0)
91
+ return;
92
+ if (freeBits0 === usedBits0 && freeBits1 === usedBits1) {
57
93
  tableLength = 0;
58
- usedBits = 0 | 0;
94
+ usedBits0 = 0;
95
+ usedBits1 = 0;
59
96
  return;
60
97
  }
61
- // only bother with bits we actually consider used
62
- freeBits &= usedBits;
63
- if (freeBits === 0)
64
- return;
65
- usedBits &= ~freeBits;
66
- tableLength = compactFreeBitsStable(tableLength, freeBits);
98
+ usedBits0 = (usedBits0 & ~freeBits0) | 0;
99
+ usedBits1 = (usedBits1 & ~freeBits1) | 0;
100
+ compactFreed(freeBits0, freeBits1);
67
101
  };
68
- // Hot-path allocation: reconcile frees, optionally reuse an exact-fit freed
69
- // region in place, otherwise compact survivors while looking for the first gap.
70
- const findAndInsert = (task, size) => {
71
- const sai = startAndIndex;
72
- const sz = size64bit;
73
- let tl = tableLength;
74
- let insertAt = -1;
75
- let insertStart = 0;
76
- let prevEnd = 0;
77
- let didCompactScan = false;
78
- if (tl === 0 && usedBits === 0) {
79
- sai[0] = 0;
80
- sz[0] = size;
81
- task[TaskIndex.Start] = 0;
82
- task[TaskIndex.slotBuffer] =
83
- ((task[TaskIndex.slotBuffer] & SLOT_META_PACKED_MASK) | 0) >>> 0;
84
- tableLength = 1;
85
- usedBits = 1;
86
- hostLast ^= 1;
87
- return 0;
88
- }
89
- if (tl !== 0) {
90
- const w = a_load(workerBits, 0) | 0;
91
- let freeBits = (~(hostLast ^ w)) >>> 0;
92
- if (freeBits !== 0)
93
- freeBits &= usedBits;
94
- // Every tracked slot is free, so restart the arena from offset 0. A free
95
- // racing after this snapshot can only delay reclamation until the next
96
- // allocation; it cannot make a live slot appear free.
97
- if (freeBits === (usedBits >>> 0)) {
98
- tableLength = 0;
99
- usedBits = 0 | 0;
100
- tl = 0;
101
- freeBits = 0 >>> 0;
102
- }
103
- else if (freeBits !== 0) {
104
- for (let i = 0; i < tl; i++) {
105
- const v = sai[i];
106
- const reclaimedSlot = v & SLOT_MASK;
107
- const reclaimedBit = 1 << reclaimedSlot;
108
- if ((freeBits & reclaimedBit) === 0)
109
- continue;
110
- if ((sz[reclaimedSlot] >>> 0) !== (size >>> 0))
111
- continue;
112
- const availableBits = (~usedBits) >>> 0;
113
- const freeBit = (availableBits & -availableBits) >>> 0;
114
- if (freeBit === 0)
115
- return -1;
116
- const slotIndex = 31 - clz32(freeBit);
117
- const start = v & START_MASK;
118
- sai[i] = (start | slotIndex) >>> 0;
119
- sz[slotIndex] = size;
120
- task[TaskIndex.Start] = start;
121
- task[TaskIndex.slotBuffer] =
122
- ((task[TaskIndex.slotBuffer] & SLOT_META_PACKED_MASK) |
123
- slotIndex) >>> 0;
124
- usedBits = (usedBits & ~reclaimedBit) | freeBit;
125
- hostLast ^= freeBit;
126
- return slotIndex;
127
- }
128
- }
129
- if (tl !== 0 && freeBits !== 0 && freeBits !== EMPTY) {
130
- didCompactScan = true;
131
- let write = 0;
132
- for (let read = 0; read < tl; read++) {
133
- const v = sai[read];
134
- const slot = v & SLOT_MASK;
135
- if ((freeBits & (1 << slot)) !== 0)
136
- continue;
137
- const curStart = v & START_MASK;
138
- if (insertAt === -1 && ((curStart - prevEnd) >>> 0) >= (size >>> 0)) {
139
- insertAt = write;
140
- insertStart = prevEnd;
141
- }
142
- if (write !== read)
143
- sai[write] = v;
144
- write++;
145
- prevEnd = (curStart + (sz[slot] >>> 0)) >>> 0;
146
- }
147
- for (let i = write; i < tl; i++)
148
- sai[i] = EMPTY;
149
- if (freeBits !== 0)
150
- usedBits &= ~freeBits;
151
- tableLength = tl = write;
152
- }
153
- }
154
- if (tl >= LockBound.slots)
102
+ // The extent table stays sorted by offset and is limited to 64 regions.
103
+ const findAndInsert = (size) => {
104
+ updateTable();
105
+ if (tableLength >= DYNAMIC_PAYLOAD_SLOTS)
155
106
  return -1;
156
- const availableBits = (~usedBits) >>> 0;
157
- const freeBit = (availableBits & -availableBits) >>> 0;
158
- if (freeBit === 0)
107
+ const slot = findFreeSlot();
108
+ if (slot === -1)
159
109
  return -1;
160
- const slotIndex = 31 - clz32(freeBit);
161
- if (!didCompactScan && tl !== 0) {
162
- const firstStart = sai[0] & START_MASK;
163
- if (firstStart >= (size >>> 0)) {
164
- for (let i = tl; i > 0; i--)
165
- sai[i] = sai[i - 1];
166
- sai[0] = slotIndex;
167
- sz[slotIndex] = size;
168
- task[TaskIndex.Start] = 0;
169
- task[TaskIndex.slotBuffer] =
170
- ((task[TaskIndex.slotBuffer] & SLOT_META_PACKED_MASK) | slotIndex) >>>
171
- 0;
172
- tableLength = tl + 1;
173
- usedBits |= freeBit;
174
- hostLast ^= freeBit;
175
- return slotIndex;
110
+ let previousEnd = 0;
111
+ let insertAt = tableLength;
112
+ for (let at = 0; at < tableLength; at++) {
113
+ const value = startAndIndex[at];
114
+ const start = value & START_MASK;
115
+ if (((start - previousEnd) >>> 0) >= (size >>> 0)) {
116
+ insertAt = at;
117
+ break;
176
118
  }
177
- for (let at = 0; at + 1 < tl; at++) {
178
- const cur = sai[at];
179
- const curStart = cur & START_MASK;
180
- const curEnd = (curStart + (sz[cur & SLOT_MASK] >>> 0)) >>> 0;
181
- const nextStart = sai[at + 1] & START_MASK;
182
- if ((nextStart - curEnd) >>> 0 < (size >>> 0))
183
- continue;
184
- for (let i = tl; i > at + 1; i--)
185
- sai[i] = sai[i - 1];
186
- sai[at + 1] = (curEnd | slotIndex) >>> 0;
187
- sz[slotIndex] = size;
188
- task[TaskIndex.Start] = curEnd;
189
- task[TaskIndex.slotBuffer] =
190
- ((task[TaskIndex.slotBuffer] & SLOT_META_PACKED_MASK) | slotIndex) >>>
191
- 0;
192
- tableLength = tl + 1;
193
- usedBits |= freeBit;
194
- hostLast ^= freeBit;
195
- return slotIndex;
196
- }
197
- const last = sai[tl - 1];
198
- const lastStart = last & START_MASK;
199
- const newStart = (lastStart + (sz[last & SLOT_MASK] >>> 0)) >>> 0;
200
- sai[tl] = (newStart | slotIndex) >>> 0;
201
- sz[slotIndex] = size;
202
- task[TaskIndex.Start] = newStart;
203
- task[TaskIndex.slotBuffer] =
204
- ((task[TaskIndex.slotBuffer] & SLOT_META_PACKED_MASK) | slotIndex) >>>
205
- 0;
206
- tableLength = tl + 1;
207
- usedBits |= freeBit;
208
- hostLast ^= freeBit;
209
- return slotIndex;
210
- }
211
- if (tl === 0) {
212
- sai[0] = slotIndex;
213
- sz[slotIndex] = size;
214
- task[TaskIndex.Start] = 0;
215
- task[TaskIndex.slotBuffer] =
216
- ((task[TaskIndex.slotBuffer] & SLOT_META_PACKED_MASK) | slotIndex) >>>
217
- 0;
218
- tableLength = 1;
219
- usedBits |= freeBit;
220
- hostLast ^= freeBit;
221
- return slotIndex;
119
+ previousEnd = (start + (size64bit[value & SLOT_MASK] >>> 0)) >>> 0;
222
120
  }
223
- if (insertAt !== -1) {
224
- for (let i = tl; i > insertAt; i--)
225
- sai[i] = sai[i - 1];
226
- sai[insertAt] = (insertStart | slotIndex) >>> 0;
227
- sz[slotIndex] = size;
228
- task[TaskIndex.Start] = insertStart;
229
- task[TaskIndex.slotBuffer] =
230
- ((task[TaskIndex.slotBuffer] & SLOT_META_PACKED_MASK) | slotIndex) >>>
231
- 0;
232
- tableLength = tl + 1;
233
- usedBits |= freeBit;
234
- hostLast ^= freeBit;
235
- return slotIndex;
121
+ for (let at = tableLength; at > insertAt; at--) {
122
+ startAndIndex[at] = startAndIndex[at - 1];
236
123
  }
237
- sai[tl] = (prevEnd | slotIndex) >>> 0;
238
- sz[slotIndex] = size;
239
- task[TaskIndex.Start] = prevEnd;
240
- task[TaskIndex.slotBuffer] =
241
- ((task[TaskIndex.slotBuffer] & SLOT_META_PACKED_MASK) | slotIndex) >>> 0;
242
- tableLength = tl + 1;
243
- usedBits |= freeBit;
244
- hostLast ^= freeBit;
245
- return slotIndex;
124
+ startAndIndex[insertAt] = (previousEnd | slot) >>> 0;
125
+ size64bit[slot] = size >>> 0;
126
+ startBySlot[slot] = previousEnd >>> 0;
127
+ tableLength++;
128
+ reserveSlot(slot);
129
+ return slot;
246
130
  };
131
+ /**
132
+ * Reserve a region for `payloadLen` bytes without a task to stamp it into.
133
+ * Returns the region index, or -1 when every identity is taken. Its offset is
134
+ * `regionStart(slot)`.
135
+ */
136
+ const allocRegion = (payloadLen) => findAndInsert(((payloadLen | 0) + 63) & ~63);
137
+ /** Byte offset of a region reserved by `allocRegion` or `allocTask`. */
138
+ const regionStart = (slot) => startBySlot[slot & SLOT_MASK];
247
139
  const allocTask = (task) => {
248
- const payloadLen = task[TaskIndex.PayloadLen] | 0;
249
- const size = (payloadLen + 63) & ~63;
250
- const slotIndex = findAndInsert(task, size);
251
- if (slotIndex === -1)
140
+ const slot = findAndInsert(((task[TaskIndex.PayloadLen] | 0) + 63) & ~63);
141
+ if (slot === -1)
252
142
  return -1;
253
- a_store(hostBits, 0, hostLast);
254
- return slotIndex;
143
+ task[TaskIndex.Start] = startBySlot[slot];
144
+ task[TaskIndex.slotBuffer] = ((task[TaskIndex.slotBuffer] & SLOT_META_PACKED_MASK) |
145
+ (slot & TASK_SLOT_INDEX_MASK)) >>> 0;
146
+ tagTaskSlot(task, slot);
147
+ return slot;
255
148
  };
256
149
  const setSlotLength = (slotIndex, payloadLen) => {
257
- slotIndex = slotIndex & TASK_SLOT_INDEX_MASK;
258
- //const bit = 1 << slotIndex;
259
- //if ((usedBits & bit) === 0) return false;
260
- //const current = size64bit[slotIndex] >>> 0;
261
- const aligned = ((payloadLen | 0) + 63) & ~63;
262
- //if (aligned < 0) return false;
263
- //if ((aligned >>> 0) > current) return false;
264
- size64bit[slotIndex] = aligned >>> 0;
150
+ const slot = slotIndex & SLOT_MASK;
151
+ size64bit[slot] = (((payloadLen | 0) + 63) & ~63) >>> 0;
265
152
  return true;
266
153
  };
267
- // Always a read-modify-write. `free()` genuinely has two callers even without
268
- // work stealing: the encode-side rollback (`failDynamicWriteAfterReserve`)
269
- // and the decode-side release (`freeTaskSlot`). Each held a private
270
- // `workerLast` shadow and blind-stored the whole word, so one overwrote the
271
- // other's toggles — the allocator then handed out a region that was still
272
- // live, aliasing payloads. XOR of distinct bits commutes, so one atomic
273
- // toggle per free is correct for any number of writers.
154
+ // XOR preserves independent release toggles from encode and decode paths.
274
155
  const free = (index) => {
275
- a_xor(workerBits, 0, 1 << (index & TASK_SLOT_INDEX_MASK));
156
+ const slot = index & SLOT_MASK;
157
+ a_xor(workerBits, slotWord(slot), slotBit(slot));
276
158
  };
277
159
  return {
278
160
  allocTask,
161
+ allocRegion,
162
+ regionStart,
279
163
  setSlotLength,
164
+ tagTaskSlot,
280
165
  lockSAB,
281
166
  free,
282
167
  hostBits,
@@ -10,6 +10,13 @@ export declare const createSharedDynamicBufferIO: ({ sab, payloadConfig, textCom
10
10
  payloadConfig?: PayloadBufferOptions;
11
11
  textCompat?: SharedBufferTextCompat;
12
12
  }) => {
13
+ ensureCapacity: (neededBytes: number) => boolean;
14
+ syncGrowth: () => void;
15
+ capacityBytes: () => number;
16
+ /** The buffer the dynamic views currently sit on; growth can replace it. */
17
+ currentBuffer: () => SharedBuffer;
18
+ /** Byte offset of region 0 inside that buffer; growth can move it. */
19
+ regionBase: () => number;
13
20
  readUtf8: (start: number, end: number) => string;
14
21
  writeBinary: (src: Uint8Array, start?: number) => number;
15
22
  writeBuffer: (src: Uint8Array, start?: number) => number;
@@ -127,11 +127,29 @@ export const createSharedDynamicBufferIO = ({ sab, payloadConfig, textCompat, })
127
127
  let buf = requireBufferView?.(lockSAB, baseByteOffset);
128
128
  let f64 = new Float64Array(lockSAB, baseByteOffset + DYNAMIC_HEADER_BYTES, Math.max(0, backingByteLength - DYNAMIC_HEADER_BYTES) >>> 3);
129
129
  const capacityBytes = () => backingByteLength - DYNAMIC_HEADER_BYTES;
130
+ const adoptBuffer = () => {
131
+ baseByteOffset = 0;
132
+ backingByteLength = lockSAB.byteLength;
133
+ u8 = new Uint8Array(lockSAB, baseByteOffset + DYNAMIC_HEADER_BYTES, backingByteLength - DYNAMIC_HEADER_BYTES);
134
+ buf = requireBufferView?.(lockSAB, baseByteOffset);
135
+ f64 = new Float64Array(lockSAB, baseByteOffset + DYNAMIC_HEADER_BYTES, (backingByteLength - DYNAMIC_HEADER_BYTES) >>> 3);
136
+ };
137
+ /** Refresh views after the other endpoint grows the shared arena. */
138
+ const syncGrowth = () => {
139
+ if (!canGrow)
140
+ return;
141
+ if (lockSAB.byteLength === backingByteLength)
142
+ return;
143
+ adoptBuffer();
144
+ };
130
145
  const ensureCapacity = (neededBytes) => {
131
146
  if (capacityBytes() >= neededBytes)
132
147
  return true;
133
148
  if (!canGrow)
134
149
  return false;
150
+ syncGrowth();
151
+ if (capacityBytes() >= neededBytes)
152
+ return true;
135
153
  try {
136
154
  if (!(lockSAB instanceof SharedArrayBuffer))
137
155
  return false;
@@ -140,11 +158,7 @@ export const createSharedDynamicBufferIO = ({ sab, payloadConfig, textCompat, })
140
158
  catch {
141
159
  return false;
142
160
  }
143
- baseByteOffset = 0;
144
- backingByteLength = lockSAB.byteLength;
145
- u8 = new Uint8Array(lockSAB, baseByteOffset + DYNAMIC_HEADER_BYTES, backingByteLength - DYNAMIC_HEADER_BYTES);
146
- buf = requireBufferView?.(lockSAB, baseByteOffset);
147
- f64 = new Float64Array(lockSAB, baseByteOffset + DYNAMIC_HEADER_BYTES, (backingByteLength - DYNAMIC_HEADER_BYTES) >>> 3);
161
+ adoptBuffer();
148
162
  return true;
149
163
  };
150
164
  const readUtf8 = (start, end) => {
@@ -228,6 +242,13 @@ export const createSharedDynamicBufferIO = ({ sab, payloadConfig, textCompat, })
228
242
  return written;
229
243
  };
230
244
  return {
245
+ ensureCapacity,
246
+ syncGrowth,
247
+ capacityBytes,
248
+ /** The buffer the dynamic views currently sit on; growth can replace it. */
249
+ currentBuffer: () => lockSAB,
250
+ /** Byte offset of region 0 inside that buffer; growth can move it. */
251
+ regionBase: () => u8.byteOffset,
231
252
  readUtf8,
232
253
  writeBinary,
233
254
  writeBuffer,
@@ -244,7 +265,7 @@ export const createSharedDynamicBufferIO = ({ sab, payloadConfig, textCompat, })
244
265
  writeUtf8,
245
266
  };
246
267
  };
247
- // it has to be convert it to 8
268
+ // Static payload words are stored in eight-byte units.
248
269
  export const createSharedStaticBufferIO = ({ headersBuffer, slotStrideU32, textCompat, }) => {
249
270
  const bufferCtor = getBufferCtor();
250
271
  const buffer = headersBuffer instanceof Uint32Array
@@ -291,9 +312,14 @@ export const createSharedStaticBufferIO = ({ headersBuffer, slotStrideU32, textC
291
312
  return out;
292
313
  };
293
314
  const canWrite = (start, length) => (start | 0) >= 0 && (start + length) <= writableBytes;
315
+ // Reuse one fixed view per slot; writes no longer allocate a subarray.
316
+ const slotUtf8Targets = new Array(LockBound.slots);
317
+ for (let i = 0; i < LockBound.slots; i++) {
318
+ const start = slotByteOffsets[i];
319
+ slotUtf8Targets[i] = baseU8.subarray(start, start + writableBytes);
320
+ }
294
321
  const writeUtf8 = (str, at) => {
295
- const start = slotByteOffsets[at];
296
- const target = baseU8.subarray(start, start + writableBytes);
322
+ const target = slotUtf8Targets[at];
297
323
  if (!baseBuf) {
298
324
  const { read, written } = sharedBufferEncodeInto(str, target, textCompat);
299
325
  if (read !== str.length)
@@ -0,0 +1,26 @@
1
+ export type DenoCompletionDoorbell = {
2
+ /** A process-local callback pointer a Deno thread worker may invoke. */
3
+ pointer: bigint;
4
+ /** Routes a worker lane's ring to its host dispatcher. */
5
+ listen: (lane: number, notify: () => void) => void;
6
+ /** Lets an unclean shutdown exit without invalidating a live worker pointer. */
7
+ unref: () => void;
8
+ /** Invalidates the callback only after every worker that holds it has exited. */
9
+ close: () => void;
10
+ };
11
+ /**
12
+ * Creates Deno's native thread-to-event-loop bridge without loading a custom
13
+ * library. `threadSafe()` is essential: ordinary UnsafeCallbacks may be
14
+ * called cross-thread, but do not wake an idle Deno event loop.
15
+ *
16
+ * The default pool configuration asks Deno for FFI permission once, so the
17
+ * native wake path is used whenever the caller approves it. `--allow-ffi`
18
+ * skips the prompt; `--deny-ffi`, `--no-prompt`, or a rejection preserves the
19
+ * portable polling dispatcher. Set `host.doorbell` to `false` to avoid asking.
20
+ */
21
+ export declare const createDenoCompletionDoorbell: () => DenoCompletionDoorbell | undefined;
22
+ /**
23
+ * Reconstruct a host-owned thread-safe callback in a Deno worker. The pointer
24
+ * stays process-local and is never included in process-worker boot payloads.
25
+ */
26
+ export declare const createDenoCompletionNotifier: (pointer: bigint | undefined) => ((lane: number) => void) | undefined;