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
|
@@ -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
|
-
|
|
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{
|
|
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>;
|