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
@@ -9,6 +9,7 @@
9
9
  #include <memory>
10
10
  #include <mutex>
11
11
  #include <unordered_map>
12
+ #include <vector>
12
13
 
13
14
  namespace knitting_buffer_pointer {
14
15
 
@@ -27,12 +28,51 @@ struct RetainedBackingStore {
27
28
  std::shared_ptr<v8::BackingStore> store;
28
29
  size_t byte_offset;
29
30
  size_t byte_length;
31
+ // The isolate that produced the move. This is only a registry hold: a
32
+ // consumer's adopted ArrayBuffer owns its own shared_ptr.
33
+ v8::Isolate* producer_isolate;
30
34
  };
31
35
 
32
36
  std::mutex backing_mutex;
33
37
  uint64_t next_backing_id = 1;
34
38
  std::unordered_map<uint64_t, RetainedBackingStore> retained_backings;
35
39
 
40
+ // Node tears a worker down without running its JavaScript finalizers. Both
41
+ // registries are process-global, so discard holds produced by that environment
42
+ // during teardown. Any consumer that adopted a backing store already has an
43
+ // independent shared_ptr and is unaffected.
44
+ void CleanupEnvironment(void* data) {
45
+ v8::Isolate* isolate = static_cast<v8::Isolate*>(data);
46
+ // Collect first, destroy on scope exit: releasing a hold must not run under
47
+ // the registry lock that found it.
48
+ std::vector<std::unique_ptr<RetainedReference>> released_refs;
49
+ std::vector<RetainedBackingStore> released_backings;
50
+
51
+ {
52
+ std::lock_guard<std::mutex> lock(retained_mutex);
53
+ for (auto it = retained_refs.begin(); it != retained_refs.end();) {
54
+ if (it->second->isolate != isolate) {
55
+ ++it;
56
+ continue;
57
+ }
58
+ released_refs.push_back(std::move(it->second));
59
+ it = retained_refs.erase(it);
60
+ }
61
+ }
62
+
63
+ {
64
+ std::lock_guard<std::mutex> lock(backing_mutex);
65
+ for (auto it = retained_backings.begin(); it != retained_backings.end();) {
66
+ if (it->second.producer_isolate != isolate) {
67
+ ++it;
68
+ continue;
69
+ }
70
+ released_backings.push_back(std::move(it->second));
71
+ it = retained_backings.erase(it);
72
+ }
73
+ }
74
+ }
75
+
36
76
  void ThrowType(v8::Isolate* isolate, const char* message) {
37
77
  isolate->ThrowException(v8::Exception::TypeError(
38
78
  v8::String::NewFromUtf8(isolate, message).ToLocalChecked()
@@ -318,7 +358,18 @@ void RetainBackingStore(const v8::FunctionCallbackInfo<v8::Value>& args) {
318
358
  uintptr_t address = reinterpret_cast<uintptr_t>(base + byte_offset);
319
359
 
320
360
  // Detach only after taking the shared_ptr; ordinary JS buffers use no key.
321
- DetachDefaultArrayBuffer(buffer);
361
+ // A failed detach must not be ignored: retaining the store while the source
362
+ // stays live hands the consumer an "owned" buffer that still aliases memory
363
+ // the producer can write. WASM memory and externally pinned buffers are the
364
+ // ones that land here, and the caller falls back to copying.
365
+ if (!DetachDefaultArrayBuffer(buffer)) {
366
+ ThrowType(
367
+ isolate,
368
+ "retainBackingStore requires a detachable ArrayBuffer; this buffer "
369
+ "cannot be moved without leaving the source aliasing it"
370
+ );
371
+ return;
372
+ }
322
373
 
323
374
  uint64_t token = 0;
324
375
  {
@@ -327,7 +378,9 @@ void RetainBackingStore(const v8::FunctionCallbackInfo<v8::Value>& args) {
327
378
  if (token == 0) token = next_backing_id++;
328
379
  retained_backings.emplace(
329
380
  token,
330
- RetainedBackingStore{ std::move(store), byte_offset, byte_length }
381
+ RetainedBackingStore{
382
+ std::move(store), byte_offset, byte_length, isolate
383
+ }
331
384
  );
332
385
  }
333
386
 
@@ -410,6 +463,8 @@ void Initialize(
410
463
  v8::Local<v8::Value>,
411
464
  v8::Local<v8::Context>
412
465
  ) {
466
+ v8::Isolate* isolate = exports->GetIsolate();
467
+ node::AddEnvironmentCleanupHook(isolate, CleanupEnvironment, isolate);
413
468
  NODE_SET_METHOD(exports, "getPointer", GetPointer);
414
469
  NODE_SET_METHOD(exports, "retainPointer", RetainPointer);
415
470
  NODE_SET_METHOD(exports, "releasePointer", ReleasePointer);
@@ -0,0 +1,220 @@
1
+ #include <node.h>
2
+ #include <uv.h>
3
+ #include <v8.h>
4
+
5
+ #include <atomic>
6
+ #include <cstdint>
7
+ #include <mutex>
8
+ #include <unordered_set>
9
+
10
+ namespace knitting_doorbell {
11
+
12
+ struct CompletionDoorbell {
13
+ uv_async_t async;
14
+ v8::Isolate* isolate;
15
+ v8::Global<v8::Context> context;
16
+ v8::Global<v8::Function> callback;
17
+ std::atomic<bool> closed{false};
18
+ };
19
+
20
+ std::mutex doorbells_mutex;
21
+ std::unordered_set<CompletionDoorbell*> doorbells;
22
+
23
+ void ThrowType(v8::Isolate* isolate, const char* message) {
24
+ isolate->ThrowException(v8::Exception::TypeError(
25
+ v8::String::NewFromUtf8(isolate, message).ToLocalChecked()
26
+ ));
27
+ }
28
+
29
+ void ThrowUv(v8::Isolate* isolate, const char* message, int status) {
30
+ const char* uv_message = uv_strerror(status);
31
+ v8::Local<v8::String> detail = v8::String::NewFromUtf8(
32
+ isolate,
33
+ uv_message == nullptr ? message : uv_message
34
+ ).ToLocalChecked();
35
+ isolate->ThrowException(v8::Exception::Error(detail));
36
+ }
37
+
38
+ bool ReadDoorbellPointer(
39
+ const v8::FunctionCallbackInfo<v8::Value>& args,
40
+ CompletionDoorbell** out
41
+ ) {
42
+ v8::Isolate* isolate = args.GetIsolate();
43
+ if (args.Length() < 1 || !args[0]->IsBigInt()) {
44
+ ThrowType(isolate, "completion doorbell pointer must be a bigint");
45
+ return false;
46
+ }
47
+
48
+ bool lossless = false;
49
+ uint64_t raw = args[0].As<v8::BigInt>()->Uint64Value(&lossless);
50
+ if (!lossless || raw == 0) {
51
+ ThrowType(isolate, "completion doorbell pointer must be non-zero");
52
+ return false;
53
+ }
54
+
55
+ *out = reinterpret_cast<CompletionDoorbell*>(
56
+ static_cast<uintptr_t>(raw)
57
+ );
58
+ return true;
59
+ }
60
+
61
+ void DisposeDoorbell(uv_handle_t* handle) {
62
+ auto* doorbell = static_cast<CompletionDoorbell*>(handle->data);
63
+ if (doorbell == nullptr) return;
64
+ doorbell->callback.Reset();
65
+ doorbell->context.Reset();
66
+ delete doorbell;
67
+ }
68
+
69
+ void InvokeDoorbell(uv_async_t* async) {
70
+ auto* doorbell = static_cast<CompletionDoorbell*>(async->data);
71
+ if (doorbell == nullptr || doorbell->closed.load(std::memory_order_acquire)) {
72
+ return;
73
+ }
74
+
75
+ v8::Isolate* isolate = doorbell->isolate;
76
+ v8::HandleScope scope(isolate);
77
+ v8::Local<v8::Context> context = doorbell->context.Get(isolate);
78
+ v8::Local<v8::Function> callback = doorbell->callback.Get(isolate);
79
+ if (context.IsEmpty() || callback.IsEmpty()) return;
80
+
81
+ v8::Context::Scope context_scope(context);
82
+ v8::TryCatch try_catch(isolate);
83
+ // This is called by Node's event-loop thread, not the worker that rang the
84
+ // handle. Do not let a user callback exception escape into libuv.
85
+ v8::MaybeLocal<v8::Value> call_result = callback->Call(
86
+ context,
87
+ v8::Undefined(isolate),
88
+ 0,
89
+ nullptr
90
+ );
91
+ (void)call_result;
92
+ }
93
+
94
+ void CleanupDoorbell(void* data);
95
+
96
+ void CloseDoorbell(CompletionDoorbell* doorbell, bool remove_cleanup_hook) {
97
+ bool should_close = false;
98
+ {
99
+ std::lock_guard<std::mutex> lock(doorbells_mutex);
100
+ auto found = doorbells.find(doorbell);
101
+ if (found == doorbells.end()) return;
102
+ doorbell->closed.store(true, std::memory_order_release);
103
+ doorbells.erase(found);
104
+ should_close = true;
105
+ }
106
+
107
+ if (remove_cleanup_hook) {
108
+ node::RemoveEnvironmentCleanupHook(
109
+ doorbell->isolate,
110
+ CleanupDoorbell,
111
+ doorbell
112
+ );
113
+ }
114
+ if (should_close && !uv_is_closing(reinterpret_cast<uv_handle_t*>(&doorbell->async))) {
115
+ uv_close(reinterpret_cast<uv_handle_t*>(&doorbell->async), DisposeDoorbell);
116
+ }
117
+ }
118
+
119
+ void CleanupDoorbell(void* data) {
120
+ CloseDoorbell(static_cast<CompletionDoorbell*>(data), false);
121
+ }
122
+
123
+ void CreateCompletionDoorbell(const v8::FunctionCallbackInfo<v8::Value>& args) {
124
+ v8::Isolate* isolate = args.GetIsolate();
125
+ if (args.Length() < 1 || !args[0]->IsFunction()) {
126
+ ThrowType(isolate, "createCompletionDoorbell(callback) requires a function");
127
+ return;
128
+ }
129
+
130
+ uv_loop_t* loop = node::GetCurrentEventLoop(isolate);
131
+ if (loop == nullptr) {
132
+ ThrowUv(isolate, "Node event loop is unavailable", UV_EINVAL);
133
+ return;
134
+ }
135
+
136
+ auto* doorbell = new CompletionDoorbell();
137
+ doorbell->isolate = isolate;
138
+ doorbell->async.data = doorbell;
139
+ const int status = uv_async_init(loop, &doorbell->async, InvokeDoorbell);
140
+ if (status != 0) {
141
+ delete doorbell;
142
+ ThrowUv(isolate, "uv_async_init failed", status);
143
+ return;
144
+ }
145
+
146
+ doorbell->context.Reset(isolate, isolate->GetCurrentContext());
147
+ doorbell->callback.Reset(isolate, args[0].As<v8::Function>());
148
+ {
149
+ std::lock_guard<std::mutex> lock(doorbells_mutex);
150
+ doorbells.insert(doorbell);
151
+ }
152
+ node::AddEnvironmentCleanupHook(isolate, CleanupDoorbell, doorbell);
153
+
154
+ args.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(
155
+ isolate,
156
+ static_cast<uint64_t>(reinterpret_cast<uintptr_t>(doorbell))
157
+ ));
158
+ }
159
+
160
+ void RingCompletionDoorbell(const v8::FunctionCallbackInfo<v8::Value>& args) {
161
+ CompletionDoorbell* doorbell = nullptr;
162
+ if (!ReadDoorbellPointer(args, &doorbell)) return;
163
+
164
+ int status = UV_EINVAL;
165
+ {
166
+ // The registry guards a host close racing a worker-thread ring. Holding it
167
+ // through uv_async_send is safe: the send is non-blocking and coalesced.
168
+ std::lock_guard<std::mutex> lock(doorbells_mutex);
169
+ if (
170
+ doorbells.contains(doorbell) &&
171
+ !doorbell->closed.load(std::memory_order_acquire)
172
+ ) {
173
+ status = uv_async_send(&doorbell->async);
174
+ }
175
+ }
176
+ args.GetReturnValue().Set(status == 0);
177
+ }
178
+
179
+ void UnrefCompletionDoorbell(const v8::FunctionCallbackInfo<v8::Value>& args) {
180
+ CompletionDoorbell* doorbell = nullptr;
181
+ if (!ReadDoorbellPointer(args, &doorbell)) return;
182
+
183
+ bool found = false;
184
+ {
185
+ std::lock_guard<std::mutex> lock(doorbells_mutex);
186
+ if (doorbells.contains(doorbell)) {
187
+ uv_unref(reinterpret_cast<uv_handle_t*>(&doorbell->async));
188
+ found = true;
189
+ }
190
+ }
191
+ args.GetReturnValue().Set(found);
192
+ }
193
+
194
+ void CloseCompletionDoorbell(const v8::FunctionCallbackInfo<v8::Value>& args) {
195
+ CompletionDoorbell* doorbell = nullptr;
196
+ if (!ReadDoorbellPointer(args, &doorbell)) return;
197
+
198
+ bool found = false;
199
+ {
200
+ std::lock_guard<std::mutex> lock(doorbells_mutex);
201
+ found = doorbells.contains(doorbell);
202
+ }
203
+ if (found) CloseDoorbell(doorbell, true);
204
+ args.GetReturnValue().Set(found);
205
+ }
206
+
207
+ void Initialize(
208
+ v8::Local<v8::Object> exports,
209
+ v8::Local<v8::Value>,
210
+ v8::Local<v8::Context>
211
+ ) {
212
+ NODE_SET_METHOD(exports, "createCompletionDoorbell", CreateCompletionDoorbell);
213
+ NODE_SET_METHOD(exports, "ringCompletionDoorbell", RingCompletionDoorbell);
214
+ NODE_SET_METHOD(exports, "unrefCompletionDoorbell", UnrefCompletionDoorbell);
215
+ NODE_SET_METHOD(exports, "closeCompletionDoorbell", CloseCompletionDoorbell);
216
+ }
217
+
218
+ NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize)
219
+
220
+ } // namespace knitting_doorbell
@@ -0,0 +1,44 @@
1
+ /**
2
+ * One handle for a request body, whichever way it travels.
3
+ *
4
+ * A body uses either pooled arena bytes or a moved `BufferReference`;
5
+ * `allocOrRefer()` hides that choice behind one disposable handle.
6
+ *
7
+ * The host owns the body, while each send holds it until the call settles, so
8
+ * early disposal cannot recycle bytes still in use by a worker. The worker gets
9
+ * a `Uint8Array` and never releases the body.
10
+ *
11
+ * The bytes are valid only until the host releases. A worker that wants to
12
+ * keep them past the call must copy.
13
+ */
14
+ import { type KnittingAllocator, type KnittingBufferDescriptor } from "./knitting-buffer.js";
15
+ import { BufferReference } from "../connections/buffer-reference.js";
16
+ /** What `allocator.transport()` hands a worker so it can attach once. */
17
+ export type KnittingTransport = ReturnType<KnittingAllocator["transport"]>;
18
+ /**
19
+ * The value a body travels as. Three shapes, because a body has three homes:
20
+ * the arena (a descriptor), the heap (a moved reference), or a standalone
21
+ * buffer when the body did not fit the arena. `openBody` resolves all three.
22
+ */
23
+ export type KnittingBodyWire = KnittingBufferDescriptor | BufferReference | SharedArrayBuffer;
24
+ /** The host's handle: send `wire`, read `u8()`, and dispose when done. */
25
+ export type KnittingBody = {
26
+ /** Pass this as the task argument. */
27
+ readonly wire: KnittingBodyWire;
28
+ readonly byteLength: number;
29
+ /** The bytes on the host side, whichever transport was chosen. */
30
+ u8(): Uint8Array;
31
+ release(): void;
32
+ [Symbol.dispose](): void;
33
+ };
34
+ /**
35
+ * Attach this worker to the host's arena and return the opener.
36
+ *
37
+ * Call it once per worker from a bootstrap module -- `transport()` nests
38
+ * SharedArrayBuffers in an object, which survives the bootstrap's structured
39
+ * clone but not a task payload's encoding.
40
+ *
41
+ * The returned function is total over `KnittingBodyWire`: whatever the host
42
+ * chose, the task sees bytes.
43
+ */
44
+ export declare const createBodyReader: (transport: KnittingTransport) => ((wire: KnittingBodyWire) => Uint8Array);
@@ -0,0 +1,51 @@
1
+ /**
2
+ * One handle for a request body, whichever way it travels.
3
+ *
4
+ * A body uses either pooled arena bytes or a moved `BufferReference`;
5
+ * `allocOrRefer()` hides that choice behind one disposable handle.
6
+ *
7
+ * The host owns the body, while each send holds it until the call settles, so
8
+ * early disposal cannot recycle bytes still in use by a worker. The worker gets
9
+ * a `Uint8Array` and never releases the body.
10
+ *
11
+ * The bytes are valid only until the host releases. A worker that wants to
12
+ * keep them past the call must copy.
13
+ */
14
+ import { attachKnittingAllocator, } from "./knitting-buffer.js";
15
+ import { isBufferReferenceValue, } from "../connections/buffer-reference.js";
16
+ const hasSharedArrayBuffer = typeof SharedArrayBuffer === "function";
17
+ /**
18
+ * True for a buffer that arrived over a transport.
19
+ *
20
+ * knitting ships a SharedArrayBuffer by pointer and rebuilds it branded as an
21
+ * ArrayBuffer, so `instanceof SharedArrayBuffer` is false on the far side even
22
+ * though the memory is shared.
23
+ */
24
+ const isTransportedBuffer = (value) => (hasSharedArrayBuffer && value instanceof SharedArrayBuffer) ||
25
+ value instanceof ArrayBuffer;
26
+ /**
27
+ * Attach this worker to the host's arena and return the opener.
28
+ *
29
+ * Call it once per worker from a bootstrap module -- `transport()` nests
30
+ * SharedArrayBuffers in an object, which survives the bootstrap's structured
31
+ * clone but not a task payload's encoding.
32
+ *
33
+ * The returned function is total over `KnittingBodyWire`: whatever the host
34
+ * chose, the task sees bytes.
35
+ */
36
+ export const createBodyReader = (transport) => {
37
+ const lane = attachKnittingAllocator(transport);
38
+ return (wire) => {
39
+ // Moved: this worker holds the only live alias, and the host releases the
40
+ // reference once the call settles.
41
+ if (isBufferReferenceValue(wire))
42
+ return wire.toUint8Array();
43
+ // A body too large for the arena travels as its own buffer. It carries no
44
+ // identity, so there is nothing to borrow and nothing to release.
45
+ if (isTransportedBuffer(wire))
46
+ return lane.adopt(wire).u8();
47
+ // Pooled: borrowed for the call. `borrow` is what makes it impossible for
48
+ // this worker to release an identity the host still owns.
49
+ return lane.adopt(wire, { borrow: true }).u8();
50
+ };
51
+ };
@@ -0,0 +1,116 @@
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 type { KnittingAllocator, KnittingSharedBuffer } from "./knitting-buffer.js";
26
+ import { BufferReference } from "../connections/buffer-reference.js";
27
+ /**
28
+ * Bodies at least this large stream, if their length is known in advance.
29
+ *
30
+ * The crossover between the two strategies is runtime-dependent; re-measure
31
+ * with `bench/http-body-oha.ts` if body sizes cluster near it.
32
+ */
33
+ export declare const HTTP_BODY_STREAM_THRESHOLD_BYTES: number;
34
+ /**
35
+ * Bodies at or above this size are moved with `BufferReference` by
36
+ * `readBodyOrRefer`.
37
+ *
38
+ * Intentionally higher than `SHARED_RETURN_MIN_BYTES`, the crossover for an
39
+ * already materialized buffer: request handling has to consume the stream
40
+ * first, so the move only pays once the body is large enough to dominate that.
41
+ * Tune it for the application's body sizes and in-flight request count.
42
+ */
43
+ export declare const HTTP_BODY_REFERENCE_THRESHOLD_BYTES: number;
44
+ export type ReadBodyOptions = {
45
+ /**
46
+ * Bodies of at least this many bytes stream into a preallocated region
47
+ * instead of being materialized first. Below the crossover, streaming is
48
+ * slower; see `HTTP_BODY_STREAM_THRESHOLD_BYTES`.
49
+ */
50
+ streamThresholdBytes?: number;
51
+ /**
52
+ * Reject a body larger than this.
53
+ *
54
+ * A declared length is a claim by the client, and the memory is committed
55
+ * on the strength of that claim before a single byte arrives -- so an
56
+ * unbounded default is a request-sized allocation primitive for anyone who
57
+ * can send a header. A body that declares nothing is read against the same
58
+ * cap rather than buffered whole and measured afterwards.
59
+ *
60
+ * `readBodyIntoRegion` defaults it to the allocator's `arenaByteLength`,
61
+ * which is the most a pooled region can hold anyway. The helpers that
62
+ * allocate somewhere else cannot infer a bound and require one.
63
+ */
64
+ maxByteLength?: number;
65
+ };
66
+ /** `readBodyIntoBytes` allocates through a caller-supplied function, so only
67
+ * the caller knows what that allocation can afford. */
68
+ export type ReadBodyIntoBytesOptions = ReadBodyOptions & {
69
+ maxByteLength: number;
70
+ };
71
+ /** `readBodyOrRefer` exists to handle bodies too large for the arena, so
72
+ * neither the pool nor the crossover implies a bound. */
73
+ export type ReadBodyOrReferOptions = ReadBodyOptions & {
74
+ maxByteLength: number;
75
+ /** Move bodies at or above this size with `BufferReference`. */
76
+ referenceAboveBytes?: number;
77
+ };
78
+ /** What these helpers need of an allocator: a region, and its ceiling. */
79
+ export type RegionAllocator = Pick<KnittingAllocator, "alloc" | "arenaByteLength">;
80
+ export type ReadBodyPayload = KnittingSharedBuffer | BufferReference;
81
+ /**
82
+ * Read `request`'s body into bytes from `allocate`.
83
+ *
84
+ * The generic form behind `readBodyIntoRegion`. `allocate` may be anything
85
+ * that hands back a writable `Uint8Array` of the requested size -- a
86
+ * `KnittingAllocator` region's view, or `pool.sharedArgBytes`, which borrows
87
+ * from the arena the workers already read from, so the bytes reach a task
88
+ * without a further copy.
89
+ *
90
+ * The returned view is exactly the bytes that arrived, which is not
91
+ * necessarily what `Content-Length` claimed.
92
+ */
93
+ export declare const readBodyIntoBytes: (request: Request, allocate: (byteLength: number) => Uint8Array, { streamThresholdBytes, maxByteLength, }: ReadBodyIntoBytesOptions) => Promise<Uint8Array>;
94
+ /**
95
+ * Read `request`'s body into a region from `allocator`.
96
+ *
97
+ * The caller owns the region and must `release()` it (or let the
98
+ * allocator's collector backstop reclaim it). The returned region's
99
+ * `byteLength` is what
100
+ * actually arrived, which is not necessarily what `Content-Length` claimed.
101
+ */
102
+ export declare const readBodyIntoRegion: (request: Request, allocator: RegionAllocator, { streamThresholdBytes, maxByteLength, }?: ReadBodyOptions) => Promise<KnittingSharedBuffer>;
103
+ /**
104
+ * Read a request body into the representation that is cheaper to transport.
105
+ *
106
+ * Below `referenceAboveBytes`, the result is a `KnittingSharedBuffer` owned by
107
+ * the supplied allocator. At or above it, the request is materialized into a
108
+ * heap `ArrayBuffer` and moved into a `BufferReference`; constructing the
109
+ * reference detaches the materialized source. The caller owns the result and
110
+ * must release a `KnittingSharedBuffer` or `BufferReference` when finished.
111
+ *
112
+ * A missing `Content-Length` is materialized before choosing the result, so a
113
+ * genuinely large chunked body still takes the reference path. This helper is
114
+ * for thread workers; `BufferReference` cannot cross a process boundary.
115
+ */
116
+ export declare const readBodyOrRefer: (request: Request, allocator: RegionAllocator, { referenceAboveBytes, ...bodyOptions }: ReadBodyOrReferOptions) => Promise<ReadBodyPayload>;