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.
- package/README.md +623 -342
- package/knitting.browser.d.ts +3 -1
- package/knitting.browser.js +1 -1
- package/knitting.d.ts +3 -1
- package/knitting.js +2 -1
- package/map.md +0 -6
- package/package.json +10 -5
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/scripts/build-native-addons.ts +5 -0
- package/shared-memory.d.ts +3 -0
- package/shared-memory.js +3 -0
- package/src/api.js +158 -71
- package/src/common/with-resolvers.js +2 -5
- package/src/common/worker-runtime.d.ts +7 -0
- package/src/common/worker-runtime.js +7 -0
- package/src/connections/buffer-reference.d.ts +10 -36
- package/src/connections/buffer-reference.js +15 -170
- package/src/connections/node-addons.d.ts +1 -1
- package/src/connections/node-addons.js +11 -1
- package/src/connections/shared-array-buffer-payload.d.ts +7 -0
- package/src/connections/shared-array-buffer-payload.js +27 -11
- package/src/debug/gate.js +1 -1
- package/src/debug/handle.d.ts +6 -1
- package/src/debug/handle.js +14 -6
- package/src/error.d.ts +9 -0
- package/src/error.js +16 -2
- package/src/knitting_buffer_pointer.cc +57 -2
- package/src/knitting_doorbell.cc +220 -0
- package/src/memory/knitting-body.d.ts +44 -0
- package/src/memory/knitting-body.js +51 -0
- package/src/memory/knitting-buffer-http.d.ts +116 -0
- package/src/memory/knitting-buffer-http.js +255 -0
- package/src/memory/knitting-buffer.d.ts +250 -0
- package/src/memory/knitting-buffer.js +695 -0
- package/src/memory/lazy-region-registry.d.ts +83 -0
- package/src/memory/lazy-region-registry.js +355 -0
- package/src/memory/lock.d.ts +80 -15
- package/src/memory/lock.js +473 -139
- package/src/memory/payloadCodec.d.ts +18 -2
- package/src/memory/payloadCodec.js +340 -76
- package/src/memory/regionRegistry.d.ts +6 -0
- package/src/memory/regionRegistry.js +125 -240
- package/src/memory/shared-buffer-io.d.ts +7 -0
- package/src/memory/shared-buffer-io.js +34 -8
- package/src/permission/protocol.d.ts +1 -0
- package/src/permission/protocol.js +8 -3
- package/src/runtime/deno-doorbell.d.ts +26 -0
- package/src/runtime/deno-doorbell.js +117 -0
- package/src/runtime/dispatcher.d.ts +13 -6
- package/src/runtime/dispatcher.js +101 -63
- package/src/runtime/host-arg-arena.d.ts +3 -0
- package/src/runtime/host-arg-arena.js +16 -0
- package/src/runtime/inline-executor.js +2 -1
- package/src/runtime/node-doorbell.d.ts +14 -0
- package/src/runtime/node-doorbell.js +84 -0
- package/src/runtime/pool.d.ts +30 -15
- package/src/runtime/pool.js +199 -151
- package/src/runtime/process-worker.d.ts +9 -0
- package/src/runtime/process-worker.js +32 -3
- package/src/runtime/tx-queue.d.ts +4 -6
- package/src/runtime/tx-queue.js +63 -48
- package/src/runtime/worker-common.d.ts +7 -0
- package/src/runtime/worker-common.js +28 -2
- package/src/types.d.ts +66 -78
- package/src/worker/loop.js +95 -60
- package/src/worker/rx-queue.d.ts +2 -3
- package/src/worker/rx-queue.js +34 -40
- package/src/worker/safety/index.d.ts +1 -1
- package/src/worker/safety/index.js +1 -1
- package/src/worker/safety/process.d.ts +2 -0
- package/src/worker/safety/process.js +8 -1
- package/src/worker/safety/startup.js +11 -6
- package/src/worker/shared-return.d.ts +9 -0
- package/src/worker/shared-return.js +22 -0
- package/src/worker/task-loader.js +1 -2
- package/src/worker/timers.d.ts +2 -6
- package/src/worker/timers.js +39 -22
- package/unsafe.d.ts +2 -1
- package/unsafe.js +2 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read an HTTP request body into a pooled shared-memory region.
|
|
3
|
+
*
|
|
4
|
+
* Two strategies, because neither wins everywhere:
|
|
5
|
+
*
|
|
6
|
+
* - **Materialize.** Let the runtime assemble the body on the heap, then
|
|
7
|
+
* copy it into a region. One copy, no per-chunk bookkeeping.
|
|
8
|
+
* - **Stream.** Preallocate a region of the declared length and write each
|
|
9
|
+
* chunk straight into it. No heap body at all, but a reader per request.
|
|
10
|
+
*
|
|
11
|
+
* A small body arrives as a single chunk, so streaming saves no copy and still
|
|
12
|
+
* pays for the reader; a large one is split across many chunks, and there
|
|
13
|
+
* writing straight into the region wins on p99 as much as on throughput. The
|
|
14
|
+
* crossover is the body size at which chunking starts, so it depends on the
|
|
15
|
+
* runtime and the network in front of it -- `HTTP_BODY_STREAM_THRESHOLD_BYTES`
|
|
16
|
+
* is a starting point, not a constant of nature. `bench/http-body-oha.ts`
|
|
17
|
+
* sweeps it.
|
|
18
|
+
*
|
|
19
|
+
* Streaming needs a length up front, so a body with no `Content-Length` is
|
|
20
|
+
* always materialized. Reserving an upper bound instead is possible with
|
|
21
|
+
* `allocator.allocUpTo()` but is a worse default: the reservation has to fit
|
|
22
|
+
* the bump window, and a bound that does not fit takes the standalone-SAB
|
|
23
|
+
* valve.
|
|
24
|
+
*/
|
|
25
|
+
import { BufferReference } from "../connections/buffer-reference.js";
|
|
26
|
+
/**
|
|
27
|
+
* Bodies at least this large stream, if their length is known in advance.
|
|
28
|
+
*
|
|
29
|
+
* The crossover between the two strategies is runtime-dependent; re-measure
|
|
30
|
+
* with `bench/http-body-oha.ts` if body sizes cluster near it.
|
|
31
|
+
*/
|
|
32
|
+
export const HTTP_BODY_STREAM_THRESHOLD_BYTES = 192 * 1024;
|
|
33
|
+
/**
|
|
34
|
+
* Bodies at or above this size are moved with `BufferReference` by
|
|
35
|
+
* `readBodyOrRefer`.
|
|
36
|
+
*
|
|
37
|
+
* Intentionally higher than `SHARED_RETURN_MIN_BYTES`, the crossover for an
|
|
38
|
+
* already materialized buffer: request handling has to consume the stream
|
|
39
|
+
* first, so the move only pays once the body is large enough to dominate that.
|
|
40
|
+
* Tune it for the application's body sizes and in-flight request count.
|
|
41
|
+
*/
|
|
42
|
+
export const HTTP_BODY_REFERENCE_THRESHOLD_BYTES = 2 * 1024 * 1024;
|
|
43
|
+
/** The declared body length, or -1 when it is absent or not a sane integer. */
|
|
44
|
+
const declaredLength = (request) => {
|
|
45
|
+
const header = request.headers.get("content-length");
|
|
46
|
+
if (header === null)
|
|
47
|
+
return -1;
|
|
48
|
+
const value = Number(header);
|
|
49
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : -1;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Read a body whose length was not declared, against `maxByteLength`.
|
|
53
|
+
*
|
|
54
|
+
* Buffering the whole thing and checking afterwards is the same exposure as
|
|
55
|
+
* trusting `Content-Length`: an attacker only has to omit the header. Reading
|
|
56
|
+
* against the cap stops at the first chunk that crosses it, and cancels the
|
|
57
|
+
* stream so the sender stops rather than filling a socket buffer.
|
|
58
|
+
*/
|
|
59
|
+
const materialize = async (request, maxByteLength) => {
|
|
60
|
+
if (!Number.isFinite(maxByteLength) || request.body === null) {
|
|
61
|
+
return request.bytes !== undefined
|
|
62
|
+
? await request.bytes()
|
|
63
|
+
: new Uint8Array(await request.arrayBuffer());
|
|
64
|
+
}
|
|
65
|
+
const chunks = [];
|
|
66
|
+
let total = 0;
|
|
67
|
+
const reader = request.body.getReader();
|
|
68
|
+
try {
|
|
69
|
+
for (;;) {
|
|
70
|
+
const { done, value } = await reader.read();
|
|
71
|
+
if (done)
|
|
72
|
+
break;
|
|
73
|
+
total += value.byteLength;
|
|
74
|
+
if (total > maxByteLength) {
|
|
75
|
+
await reader.cancel();
|
|
76
|
+
throw new RangeError(`body is over the ${maxByteLength} byte limit`);
|
|
77
|
+
}
|
|
78
|
+
chunks.push(value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
reader.releaseLock();
|
|
83
|
+
}
|
|
84
|
+
if (chunks.length === 1)
|
|
85
|
+
return chunks[0];
|
|
86
|
+
const out = new Uint8Array(total);
|
|
87
|
+
let at = 0;
|
|
88
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
89
|
+
out.set(chunks[i], at);
|
|
90
|
+
at += chunks[i].byteLength;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Read `request`'s body into bytes from `allocate`.
|
|
96
|
+
*
|
|
97
|
+
* The generic form behind `readBodyIntoRegion`. `allocate` may be anything
|
|
98
|
+
* that hands back a writable `Uint8Array` of the requested size -- a
|
|
99
|
+
* `KnittingAllocator` region's view, or `pool.sharedArgBytes`, which borrows
|
|
100
|
+
* from the arena the workers already read from, so the bytes reach a task
|
|
101
|
+
* without a further copy.
|
|
102
|
+
*
|
|
103
|
+
* The returned view is exactly the bytes that arrived, which is not
|
|
104
|
+
* necessarily what `Content-Length` claimed.
|
|
105
|
+
*/
|
|
106
|
+
export const readBodyIntoBytes = async (request, allocate, { streamThresholdBytes = HTTP_BODY_STREAM_THRESHOLD_BYTES, maxByteLength, }) => {
|
|
107
|
+
if (!Number.isSafeInteger(maxByteLength) || maxByteLength < 0) {
|
|
108
|
+
throw new RangeError("maxByteLength must be a non-negative safe integer");
|
|
109
|
+
}
|
|
110
|
+
const req = request;
|
|
111
|
+
const declared = declaredLength(req);
|
|
112
|
+
if (declared > maxByteLength) {
|
|
113
|
+
throw new RangeError(`body declares ${declared} bytes, over the ${maxByteLength} limit`);
|
|
114
|
+
}
|
|
115
|
+
if (declared >= streamThresholdBytes && req.body !== null) {
|
|
116
|
+
const out = allocate(declared);
|
|
117
|
+
let at = 0;
|
|
118
|
+
const reader = req.body.getReader();
|
|
119
|
+
try {
|
|
120
|
+
for (;;) {
|
|
121
|
+
const { done, value } = await reader.read();
|
|
122
|
+
if (done)
|
|
123
|
+
break;
|
|
124
|
+
if (at + value.byteLength > declared) {
|
|
125
|
+
throw new RangeError(`body exceeds its declared ${declared} bytes`);
|
|
126
|
+
}
|
|
127
|
+
out.set(value, at);
|
|
128
|
+
at += value.byteLength;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
reader.releaseLock();
|
|
133
|
+
}
|
|
134
|
+
return at === declared ? out : out.subarray(0, at);
|
|
135
|
+
}
|
|
136
|
+
const bytes = await materialize(req, maxByteLength);
|
|
137
|
+
if (bytes.byteLength > maxByteLength) {
|
|
138
|
+
throw new RangeError(`body is ${bytes.byteLength} bytes, over the ${maxByteLength} limit`);
|
|
139
|
+
}
|
|
140
|
+
const out = allocate(bytes.byteLength);
|
|
141
|
+
out.set(bytes);
|
|
142
|
+
return out;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Read `request`'s body into a region from `allocator`.
|
|
146
|
+
*
|
|
147
|
+
* The caller owns the region and must `release()` it (or let the
|
|
148
|
+
* allocator's collector backstop reclaim it). The returned region's
|
|
149
|
+
* `byteLength` is what
|
|
150
|
+
* actually arrived, which is not necessarily what `Content-Length` claimed.
|
|
151
|
+
*/
|
|
152
|
+
export const readBodyIntoRegion = async (request, allocator, { streamThresholdBytes = HTTP_BODY_STREAM_THRESHOLD_BYTES,
|
|
153
|
+
// The arena is the ceiling on a pooled region: a larger body cannot be
|
|
154
|
+
// pooled at all, it can only take the standalone-SAB valve, which is the
|
|
155
|
+
// allocation an attacker would be aiming for.
|
|
156
|
+
maxByteLength = allocator.arenaByteLength, } = {}) => {
|
|
157
|
+
const req = request;
|
|
158
|
+
const declared = declaredLength(req);
|
|
159
|
+
if (declared > maxByteLength) {
|
|
160
|
+
throw new RangeError(`body declares ${declared} bytes, over the ${maxByteLength} limit`);
|
|
161
|
+
}
|
|
162
|
+
// Stream only when the length is known and large enough to pay for it.
|
|
163
|
+
if (declared >= streamThresholdBytes && req.body !== null) {
|
|
164
|
+
const region = allocator.alloc(declared);
|
|
165
|
+
try {
|
|
166
|
+
const out = region.u8();
|
|
167
|
+
let at = 0;
|
|
168
|
+
const reader = req.body.getReader();
|
|
169
|
+
try {
|
|
170
|
+
for (;;) {
|
|
171
|
+
const { done, value } = await reader.read();
|
|
172
|
+
if (done)
|
|
173
|
+
break;
|
|
174
|
+
// Content-Length is a claim, not a guarantee. Writing past the
|
|
175
|
+
// region would corrupt whatever region follows it in the arena.
|
|
176
|
+
if (at + value.byteLength > declared) {
|
|
177
|
+
throw new RangeError(`body exceeds its declared ${declared} bytes`);
|
|
178
|
+
}
|
|
179
|
+
out.set(value, at);
|
|
180
|
+
at += value.byteLength;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
reader.releaseLock();
|
|
185
|
+
}
|
|
186
|
+
// A body shorter than it claimed leaves a tail of stale arena bytes;
|
|
187
|
+
// commit hands that tail back and reports the real length.
|
|
188
|
+
return at === declared ? region : region.commit(at);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
region.release();
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const bytes = await materialize(req, maxByteLength);
|
|
196
|
+
if (bytes.byteLength > maxByteLength) {
|
|
197
|
+
throw new RangeError(`body is ${bytes.byteLength} bytes, over the ${maxByteLength} limit`);
|
|
198
|
+
}
|
|
199
|
+
const region = allocator.alloc(bytes.byteLength);
|
|
200
|
+
try {
|
|
201
|
+
region.u8().set(bytes);
|
|
202
|
+
return region;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
region.release();
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const copyBytesIntoRegion = (bytes, allocator) => {
|
|
210
|
+
const region = allocator.alloc(bytes.byteLength);
|
|
211
|
+
try {
|
|
212
|
+
region.u8().set(bytes);
|
|
213
|
+
return region;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
region.release();
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Read a request body into the representation that is cheaper to transport.
|
|
222
|
+
*
|
|
223
|
+
* Below `referenceAboveBytes`, the result is a `KnittingSharedBuffer` owned by
|
|
224
|
+
* the supplied allocator. At or above it, the request is materialized into a
|
|
225
|
+
* heap `ArrayBuffer` and moved into a `BufferReference`; constructing the
|
|
226
|
+
* reference detaches the materialized source. The caller owns the result and
|
|
227
|
+
* must release a `KnittingSharedBuffer` or `BufferReference` when finished.
|
|
228
|
+
*
|
|
229
|
+
* A missing `Content-Length` is materialized before choosing the result, so a
|
|
230
|
+
* genuinely large chunked body still takes the reference path. This helper is
|
|
231
|
+
* for thread workers; `BufferReference` cannot cross a process boundary.
|
|
232
|
+
*/
|
|
233
|
+
export const readBodyOrRefer = async (request, allocator, { referenceAboveBytes = HTTP_BODY_REFERENCE_THRESHOLD_BYTES, ...bodyOptions }) => {
|
|
234
|
+
if (!Number.isSafeInteger(referenceAboveBytes) || referenceAboveBytes < 0) {
|
|
235
|
+
throw new RangeError("referenceAboveBytes must be a non-negative safe integer");
|
|
236
|
+
}
|
|
237
|
+
// Reference allocations need an explicit runtime size bound.
|
|
238
|
+
if (!Number.isSafeInteger(bodyOptions.maxByteLength) ||
|
|
239
|
+
bodyOptions.maxByteLength < 0) {
|
|
240
|
+
throw new RangeError("maxByteLength must be a non-negative safe integer");
|
|
241
|
+
}
|
|
242
|
+
const req = request;
|
|
243
|
+
const declared = declaredLength(req);
|
|
244
|
+
// A known-large body can stream directly into the heap buffer that will be
|
|
245
|
+
// moved. A body without a length must also use this path so we can choose on
|
|
246
|
+
// the actual byte count after consuming it.
|
|
247
|
+
if (declared < 0 || declared >= referenceAboveBytes) {
|
|
248
|
+
const bytes = await readBodyIntoBytes(request, (byteLength) => new Uint8Array(byteLength), bodyOptions);
|
|
249
|
+
if (bytes.byteLength >= referenceAboveBytes) {
|
|
250
|
+
return new BufferReference(bytes);
|
|
251
|
+
}
|
|
252
|
+
return copyBytesIntoRegion(bytes, allocator);
|
|
253
|
+
}
|
|
254
|
+
return readBodyIntoRegion(request, allocator, bodyOptions);
|
|
255
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `KnittingSharedBuffer` -- a thread-level pool of shared byte
|
|
3
|
+
* regions whose handle mimics `SharedArrayBuffer` rather than `Buffer`.
|
|
4
|
+
*
|
|
5
|
+
* Shape:
|
|
6
|
+
*
|
|
7
|
+
* - Each thread owns one pool: its own arena SAB, its own lock sector, its
|
|
8
|
+
* own identity space. Nothing is shared between pools except the SABs a
|
|
9
|
+
* descriptor points at.
|
|
10
|
+
* - `alloc(n)` takes a region from the lazy registry
|
|
11
|
+
* (`lazy-region-registry.ts`). Identity exhaustion is not a failure: it
|
|
12
|
+
* falls back to a standalone `SharedArrayBuffer`, so the pool never blocks
|
|
13
|
+
* and never evicts a live borrow.
|
|
14
|
+
* - The **region** owns the bytes. Views are minted from it (`u8()`,
|
|
15
|
+
* `view(Ctor)`) and are plain, non-owning typed arrays. That is the same
|
|
16
|
+
* split JS already has between a `SharedArrayBuffer` and its views, and it
|
|
17
|
+
* is what keeps a `Float64Array` over the region from being silently
|
|
18
|
+
* orphaned when some unrelated `Uint8Array` over it is dropped.
|
|
19
|
+
* - The wire form is a descriptor, never bytes. A consumer thread attaches
|
|
20
|
+
* to the producer's SABs once and materializes its own region handle.
|
|
21
|
+
* - Release is one XOR into the owning lane's shared word, from whichever
|
|
22
|
+
* thread holds the last handle.
|
|
23
|
+
*
|
|
24
|
+
* Why GC-driven release is safe here, given the lazy registry:
|
|
25
|
+
*
|
|
26
|
+
* An identity is reusable only after the owner has *observed* its release
|
|
27
|
+
* toggle (`reconcile` clears the used bit only when hostLast ^ workerBits
|
|
28
|
+
* agree). So an identity cannot be handed out again while a release for it
|
|
29
|
+
* is still outstanding, which means a late release can never apply to a
|
|
30
|
+
* newer generation -- there is no ABA to protect against with a generation
|
|
31
|
+
* counter. A forgotten handle costs one identity until the collector runs;
|
|
32
|
+
* it cannot corrupt a live region. That degradation is capacity-only, and
|
|
33
|
+
* the standalone-SAB fallback is what keeps it from turning into a stall.
|
|
34
|
+
*
|
|
35
|
+
* That argument holds for exactly one releaser per identity, which is why
|
|
36
|
+
* sending a region is `moveTo()` and not `describe()`. Two releasers
|
|
37
|
+
* reintroduce the ABA the toggle cannot see: the first release lets
|
|
38
|
+
* `reconcile` recycle the identity into a new region, and the second one
|
|
39
|
+
* then frees a live stranger, whose bytes the next `alloc` hands out while
|
|
40
|
+
* it is still being read. `describe()` is inspection only.
|
|
41
|
+
*
|
|
42
|
+
* What it still cannot do: revoke a view that was already minted. Minting is
|
|
43
|
+
* checked -- `u8()` after release throws instead of handing back a window onto
|
|
44
|
+
* somebody else's recycled bytes -- but a view handed out earlier and retained
|
|
45
|
+
* past release keeps aliasing the region. That is a JS limitation, not a
|
|
46
|
+
* design choice, and it is why `copy()` exists.
|
|
47
|
+
*/
|
|
48
|
+
import { type LazyRegionRegistryMode } from "./lazy-region-registry.js";
|
|
49
|
+
import { type ReadBodyOrReferOptions } from "./knitting-buffer-http.js";
|
|
50
|
+
import type { KnittingBody } from "./knitting-body.js";
|
|
51
|
+
export declare const KNITTING_BUFFER_CODEC = "knitting.buffer";
|
|
52
|
+
export type KnittingBufferDescriptor = {
|
|
53
|
+
codec: typeof KNITTING_BUFFER_CODEC;
|
|
54
|
+
kind: "region";
|
|
55
|
+
lane: number;
|
|
56
|
+
slot: number;
|
|
57
|
+
byteOffset: number;
|
|
58
|
+
byteLength: number;
|
|
59
|
+
} | {
|
|
60
|
+
codec: typeof KNITTING_BUFFER_CODEC;
|
|
61
|
+
kind: "buffer";
|
|
62
|
+
lane: number;
|
|
63
|
+
byteLength: number;
|
|
64
|
+
buffer: SharedArrayBuffer;
|
|
65
|
+
};
|
|
66
|
+
type ViewConstructor<T extends ArrayBufferView> = new (buffer: ArrayBufferLike, byteOffset: number, length: number) => T;
|
|
67
|
+
/**
|
|
68
|
+
* Module-internal ownership transfer. Reachable only through the allocator's
|
|
69
|
+
* `moveTo()`, which is the one place a region legitimately stops being ours.
|
|
70
|
+
*/
|
|
71
|
+
declare const DISOWN: unique symbol;
|
|
72
|
+
/**
|
|
73
|
+
* An owned region of shared memory. Mints views; does not pretend to be one.
|
|
74
|
+
*
|
|
75
|
+
* Exported for `instanceof` and for typing. It is not constructible: regions
|
|
76
|
+
* come from `createKnittingAllocator().alloc()` or from adopting a descriptor.
|
|
77
|
+
*/
|
|
78
|
+
export declare class KnittingSharedBuffer {
|
|
79
|
+
#private;
|
|
80
|
+
constructor(mint: symbol, buffer: SharedArrayBuffer, byteOffset: number, byteLength: number, lane?: number, slot?: number, release?: (free: boolean) => void, trim?: (byteLength: number) => void);
|
|
81
|
+
get byteLength(): number;
|
|
82
|
+
get byteOffset(): number;
|
|
83
|
+
/** Owning lane, and the region identity within it (-1 for a standalone SAB). */
|
|
84
|
+
get lane(): number;
|
|
85
|
+
get slot(): number;
|
|
86
|
+
/** True once this handle is spent, whether by `release()` or by a move. */
|
|
87
|
+
get released(): boolean;
|
|
88
|
+
/** True when ownership was handed to a consumer by `allocator.moveTo()`. */
|
|
89
|
+
get moved(): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* The standalone SharedArrayBuffer behind an overflow region, or undefined
|
|
92
|
+
* for a pooled one.
|
|
93
|
+
*
|
|
94
|
+
* Pooled regions deliberately have no way to reach their backing store: it
|
|
95
|
+
* is the whole arena, and handing it out would expose every other live
|
|
96
|
+
* region's bytes. An overflow region owns its buffer outright, so it is the
|
|
97
|
+
* only one that can travel as a buffer rather than as a descriptor.
|
|
98
|
+
*/
|
|
99
|
+
static standaloneBufferOf(region: KnittingSharedBuffer): SharedArrayBuffer | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Surrender the identity without freeing it: after this the handle is inert
|
|
102
|
+
* and the consumer that adopted the descriptor is the sole releaser.
|
|
103
|
+
*
|
|
104
|
+
* A no-op on a spent handle, so a double `moveTo()` cannot hand the same
|
|
105
|
+
* identity to two consumers -- the second call throws before reaching here.
|
|
106
|
+
*/
|
|
107
|
+
static [DISOWN](region: KnittingSharedBuffer): void;
|
|
108
|
+
/** The byte view. Memoized: repeated calls do not construct. */
|
|
109
|
+
u8(): Uint8Array;
|
|
110
|
+
/** A typed view over the whole region. Memoized per constructor. */
|
|
111
|
+
view<T extends ArrayBufferView>(Ctor: ViewConstructor<T>): T;
|
|
112
|
+
/**
|
|
113
|
+
* Give back the tail of a region that was reserved larger than needed, and
|
|
114
|
+
* report the region as `byteLength` bytes from here on.
|
|
115
|
+
*
|
|
116
|
+
* The point is streamed input of unknown length: an HTTP body with no
|
|
117
|
+
* `Content-Length` cannot be sized up front, so reserve an upper bound,
|
|
118
|
+
* write into it, then commit what actually arrived. Views minted before the
|
|
119
|
+
* commit are dropped, because their length is now wrong.
|
|
120
|
+
*
|
|
121
|
+
* Shrink only. Growing would mean relocating the bytes, which is the copy
|
|
122
|
+
* this whole path exists to avoid.
|
|
123
|
+
*/
|
|
124
|
+
commit(byteLength: number): this;
|
|
125
|
+
/** An independently owned copy, valid after this region is released. */
|
|
126
|
+
copy(): Uint8Array;
|
|
127
|
+
release(): void;
|
|
128
|
+
[Symbol.dispose](): void;
|
|
129
|
+
}
|
|
130
|
+
/** A region handed over directly. One `instanceof`. */
|
|
131
|
+
export declare const detectByInstance: (value: unknown) => KnittingSharedBuffer | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* A view this pool minted. One WeakMap hit, and it never touches `.buffer` --
|
|
134
|
+
* which matters because reading `.buffer` on a heap-backed typed array can
|
|
135
|
+
* materialize the ArrayBuffer wrapper on first access.
|
|
136
|
+
*/
|
|
137
|
+
export declare const detectByMintedView: (value: unknown) => KnittingSharedBuffer | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Any view that lands inside a pool arena, including a `subarray` of a minted
|
|
140
|
+
* view. Costs a `.buffer` read, a WeakMap hit, and a binary search over the
|
|
141
|
+
* live extent table.
|
|
142
|
+
*/
|
|
143
|
+
export declare const detectByArena: (value: unknown) => KnittingBufferDescriptor | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* The full check the codec would run on a payload value. Returns a descriptor
|
|
146
|
+
* to ship by reference, or undefined to serialize the value normally.
|
|
147
|
+
*
|
|
148
|
+
* Detection only: like `describe()`, it does not transfer ownership. Whatever
|
|
149
|
+
* ships the descriptor owes the consumer one of the two safe pairings
|
|
150
|
+
* documented on `describe()`.
|
|
151
|
+
*/
|
|
152
|
+
export declare const detectRegion: (value: unknown) => KnittingBufferDescriptor | undefined;
|
|
153
|
+
/**
|
|
154
|
+
* Bump window, and therefore the pool's memory high-water.
|
|
155
|
+
*
|
|
156
|
+
* Deliberately small. A wide window makes *allocation* cheaper -- the bump
|
|
157
|
+
* pointer runs longer before it has to reconcile and reclaim holes -- but that
|
|
158
|
+
* only counts the bookkeeping, never the bytes. Once real traffic writes them
|
|
159
|
+
* the ranking inverts: a wide window sprays consecutive regions across cold
|
|
160
|
+
* memory, and the cache misses cost more than the reconcile it avoided.
|
|
161
|
+
* Locality wins, so the default is a small multiple of a typical live set
|
|
162
|
+
* rather than the whole arena.
|
|
163
|
+
*
|
|
164
|
+
* It is a default, not a recommendation: size it to `payload x in-flight`.
|
|
165
|
+
* Two concurrent 1 MiB payloads do not fit here, and a reservation the window
|
|
166
|
+
* cannot satisfy takes the overflow path rather than waiting. `stats()`
|
|
167
|
+
* reports `overflows` precisely so this is visible instead of mysterious.
|
|
168
|
+
*/
|
|
169
|
+
export declare const DEFAULT_ARENA_BYTE_LENGTH: number;
|
|
170
|
+
export type KnittingAllocatorOptions = {
|
|
171
|
+
lane?: number;
|
|
172
|
+
slots?: number;
|
|
173
|
+
/** Bump window; see `DEFAULT_ARENA_BYTE_LENGTH` for why small is the default. */
|
|
174
|
+
arenaByteLength?: number;
|
|
175
|
+
/**
|
|
176
|
+
* Collector backstop for a missed release.
|
|
177
|
+
* true register every region
|
|
178
|
+
* false never register: a missed release leaks an identity
|
|
179
|
+
* "pressure" register only once live identities cross the watermark
|
|
180
|
+
*
|
|
181
|
+
* "pressure" applies the same argument as the lazy reconcile: while the pool
|
|
182
|
+
* is roomy a forgotten identity costs nothing, so paying the registration
|
|
183
|
+
* cost on every region to reclaim it early buys nothing. Registration starts
|
|
184
|
+
* when scarcity makes reclaim worth its price. Worst case is bounded: regions handed out while
|
|
185
|
+
* roomy are never registered, so at watermark w at most w of the identities
|
|
186
|
+
* can be stranded before every new region becomes reclaimable.
|
|
187
|
+
*/
|
|
188
|
+
gcBackstop?: boolean | "pressure";
|
|
189
|
+
backstopWatermark?: number;
|
|
190
|
+
};
|
|
191
|
+
/** This pool's own counters merged with its region registry's. */
|
|
192
|
+
export type KnittingAllocatorStats = {
|
|
193
|
+
pooled: number;
|
|
194
|
+
overflows: number;
|
|
195
|
+
registered: number;
|
|
196
|
+
live: number;
|
|
197
|
+
slots: number;
|
|
198
|
+
mode: LazyRegionRegistryMode;
|
|
199
|
+
tableLength: number;
|
|
200
|
+
tailEnd: number;
|
|
201
|
+
highWater: number;
|
|
202
|
+
reconciles: number;
|
|
203
|
+
firstFits: number;
|
|
204
|
+
appends: number;
|
|
205
|
+
resets: number;
|
|
206
|
+
};
|
|
207
|
+
/** What a consumer thread needs to attach: SABs, not pointers. */
|
|
208
|
+
export type KnittingAllocatorTransport = {
|
|
209
|
+
lane: number;
|
|
210
|
+
lockSAB: SharedArrayBuffer;
|
|
211
|
+
arena: SharedArrayBuffer;
|
|
212
|
+
slots: number;
|
|
213
|
+
arenaByteLength: number;
|
|
214
|
+
};
|
|
215
|
+
/** Written out rather than inferred: JSR cannot resolve an inferred return type. */
|
|
216
|
+
export type KnittingAllocator = {
|
|
217
|
+
lane: number;
|
|
218
|
+
/** Bump window this pool was built with; the ceiling on a pooled region. */
|
|
219
|
+
arenaByteLength: number;
|
|
220
|
+
alloc: (byteLength: number) => KnittingSharedBuffer;
|
|
221
|
+
allocUpTo: (maxByteLength: number) => KnittingSharedBuffer;
|
|
222
|
+
allocOrRefer: (request: Request, options: ReadBodyOrReferOptions) => Promise<KnittingBody>;
|
|
223
|
+
describe: (region: KnittingSharedBuffer) => KnittingBufferDescriptor;
|
|
224
|
+
moveTo: (region: KnittingSharedBuffer) => KnittingBufferDescriptor;
|
|
225
|
+
reconcile: () => boolean;
|
|
226
|
+
stats: () => KnittingAllocatorStats;
|
|
227
|
+
resetCounters: () => void;
|
|
228
|
+
transport: () => KnittingAllocatorTransport;
|
|
229
|
+
};
|
|
230
|
+
/** The consumer side of another thread's pool: it adopts, it never allocates. */
|
|
231
|
+
export type AttachedKnittingAllocator = {
|
|
232
|
+
lane: number;
|
|
233
|
+
adopt: (descriptor: KnittingBufferDescriptor | SharedArrayBuffer, options?: {
|
|
234
|
+
gcBackstop?: boolean;
|
|
235
|
+
borrow?: boolean;
|
|
236
|
+
}) => KnittingSharedBuffer;
|
|
237
|
+
};
|
|
238
|
+
export declare const createKnittingAllocator: ({ lane, slots, arenaByteLength, gcBackstop, backstopWatermark, }?: KnittingAllocatorOptions) => KnittingAllocator;
|
|
239
|
+
/**
|
|
240
|
+
* The consumer side of another thread's pool. It never allocates; it
|
|
241
|
+
* materializes regions over the producer's arena and releases the producer's
|
|
242
|
+
* identity with one XOR into the shared word.
|
|
243
|
+
*/
|
|
244
|
+
export declare const attachKnittingAllocator: ({ lane, lockSAB, arena, slots }: {
|
|
245
|
+
lane: number;
|
|
246
|
+
lockSAB: SharedArrayBuffer;
|
|
247
|
+
arena: SharedArrayBuffer;
|
|
248
|
+
slots: number;
|
|
249
|
+
}) => AttachedKnittingAllocator;
|
|
250
|
+
export {};
|