knitting 0.1.51 → 0.1.52

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 (54) hide show
  1. package/README.md +213 -54
  2. package/knitting.d.ts +1 -0
  3. package/map.md +15 -3
  4. package/package.json +14 -2
  5. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  6. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  7. package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
  8. package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
  9. package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
  11. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  12. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  14. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  15. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  16. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  17. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  18. package/scripts/build-native-addons.ts +5 -0
  19. package/src/api.d.ts +3 -10
  20. package/src/api.js +25 -21
  21. package/src/common/envelope.d.ts +9 -3
  22. package/src/common/envelope.js +14 -0
  23. package/src/common/worker-runtime.d.ts +2 -0
  24. package/src/common/worker-runtime.js +9 -0
  25. package/src/connections/buffer-reference-native.d.ts +56 -0
  26. package/src/connections/buffer-reference-native.js +217 -0
  27. package/src/connections/buffer-reference.d.ts +76 -0
  28. package/src/connections/buffer-reference.js +459 -0
  29. package/src/connections/index.d.ts +1 -0
  30. package/src/connections/index.js +1 -0
  31. package/src/connections/node-addons.d.ts +1 -1
  32. package/src/connections/node-buffer-pointer.d.ts +20 -0
  33. package/src/connections/node-buffer-pointer.js +16 -0
  34. package/src/connections/shared-array-buffer-payload.d.ts +36 -0
  35. package/src/connections/shared-array-buffer-payload.js +235 -0
  36. package/src/knitting_buffer_pointer.cc +425 -0
  37. package/src/memory/lock.d.ts +12 -1
  38. package/src/memory/lock.js +47 -4
  39. package/src/memory/payloadCodec.js +220 -37
  40. package/src/runtime/pool.d.ts +2 -1
  41. package/src/runtime/pool.js +8 -1
  42. package/src/runtime/process-worker.js +3 -1
  43. package/src/runtime/tx-queue.d.ts +3 -2
  44. package/src/runtime/tx-queue.js +18 -13
  45. package/src/types.d.ts +26 -18
  46. package/src/utils/http.d.ts +21 -0
  47. package/src/utils/http.js +93 -0
  48. package/src/worker/loop.js +23 -3
  49. package/src/worker/rx-queue.d.ts +4 -1
  50. package/src/worker/rx-queue.js +53 -4
  51. package/unsafe.d.ts +1 -0
  52. package/unsafe.js +1 -0
  53. package/utils.d.ts +1 -0
  54. package/utils.js +1 -0
@@ -0,0 +1,235 @@
1
+ import { RUNTIME } from "../common/runtime.js";
2
+ import { getNodeProcess } from "../common/node-compat.js";
3
+ import { getBufferReferenceCapabilities } from "./buffer-reference-native.js";
4
+ // Thread-worker SharedArrayBuffer transport by process-local pointer.
5
+ // SABs are shared, not moved or detached; process workers reject them.
6
+ export const SHARED_ARRAY_BUFFER_CODEC_ID = "knitting.sharedArrayBuffer";
7
+ export const SHARED_ARRAY_BUFFER_NUMERIC_TRANSFER = Symbol.for("knitting.sharedArrayBuffer.numericTransfer");
8
+ export const SHARED_ARRAY_BUFFER_NUMERIC_WORDS = 8;
9
+ const SHARED_ARRAY_BUFFER_TOKEN_NUMERIC_WORDS = 2;
10
+ const EXTERNAL_PAYLOAD_BRAND = Symbol.for("knitting.payloadCodec");
11
+ const getProcessId = () => {
12
+ const proc = getNodeProcess();
13
+ if (proc !== undefined && typeof proc.pid === "number")
14
+ return proc.pid;
15
+ const deno = globalThis.Deno;
16
+ if (typeof deno?.pid === "number")
17
+ return deno.pid;
18
+ return 0;
19
+ };
20
+ const PROCESS_ORIGIN = `${RUNTIME}:${getProcessId()}`;
21
+ const hasSharedArrayBuffer = typeof SharedArrayBuffer === "function";
22
+ export const isSharedArrayBufferValue = (value) => hasSharedArrayBuffer && value instanceof SharedArrayBuffer;
23
+ const pinnedBySab = new WeakMap();
24
+ const payloadBySharedBuffer = new WeakMap();
25
+ const warmedTokensByTransport = new WeakMap();
26
+ const cachedSharedBuffersByToken = new Map();
27
+ const pinFinalizer = typeof FinalizationRegistry === "function"
28
+ ? new FinalizationRegistry((token) => {
29
+ try {
30
+ getBufferReferenceCapabilities().releaseShared(token);
31
+ }
32
+ catch {
33
+ // best effort
34
+ }
35
+ })
36
+ : undefined;
37
+ const splitU64 = (value) => [
38
+ Number(value & 0xffffffffn) >>> 0,
39
+ Number((value >> 32n) & 0xffffffffn) >>> 0,
40
+ ];
41
+ const joinU64 = (low, high) => (BigInt(high >>> 0) << 32n) | BigInt(low >>> 0);
42
+ const encodeRuntime = (runtime) => {
43
+ switch (runtime) {
44
+ case "node":
45
+ return 1;
46
+ case "deno":
47
+ return 2;
48
+ case "bun":
49
+ return 3;
50
+ default:
51
+ return 0;
52
+ }
53
+ };
54
+ const decodeRuntime = (value) => {
55
+ switch (value) {
56
+ case 1:
57
+ return "node";
58
+ case 2:
59
+ return "deno";
60
+ case 3:
61
+ return "bun";
62
+ default:
63
+ return undefined;
64
+ }
65
+ };
66
+ const getWarmTokens = (transportKey) => {
67
+ if (transportKey === undefined)
68
+ return undefined;
69
+ let warmTokens = warmedTokensByTransport.get(transportKey);
70
+ if (warmTokens === undefined) {
71
+ warmTokens = new Set();
72
+ warmedTokensByTransport.set(transportKey, warmTokens);
73
+ }
74
+ return warmTokens;
75
+ };
76
+ const pinSab = (sab) => {
77
+ let pin = pinnedBySab.get(sab);
78
+ if (pin === undefined) {
79
+ const produced = getBufferReferenceCapabilities().produceShared(sab);
80
+ pin = {
81
+ token: produced.token,
82
+ pointer: produced.pointer,
83
+ byteLength: produced.byteLength,
84
+ };
85
+ pinnedBySab.set(sab, pin);
86
+ pinFinalizer?.register(sab, pin.token);
87
+ }
88
+ return pin;
89
+ };
90
+ const makeMetadata = (pin) => ({
91
+ kind: SHARED_ARRAY_BUFFER_CODEC_ID,
92
+ origin: PROCESS_ORIGIN,
93
+ runtime: RUNTIME,
94
+ pointer: pin.pointer.toString(),
95
+ token: pin.token.toString(),
96
+ byteLength: pin.byteLength,
97
+ });
98
+ const makeFullNumericMetadata = (pin) => {
99
+ if (pin.byteLength > 0xffffffff)
100
+ return undefined;
101
+ const [tokenLow, tokenHigh] = splitU64(pin.token);
102
+ const [pointerLow, pointerHigh] = splitU64(pin.pointer);
103
+ return [
104
+ tokenLow,
105
+ tokenHigh,
106
+ pointerLow,
107
+ pointerHigh,
108
+ pin.byteLength >>> 0,
109
+ encodeRuntime(RUNTIME),
110
+ getProcessId() >>> 0,
111
+ 0,
112
+ ];
113
+ };
114
+ const makeTokenNumericMetadata = (pin) => {
115
+ const [tokenLow, tokenHigh] = splitU64(pin.token);
116
+ return [tokenLow, tokenHigh];
117
+ };
118
+ /** Wrap a SAB as external payload; GC-managed pins mean no settle finalizer. */
119
+ export const wrapSharedArrayBufferPayload = (sab) => {
120
+ let payload = payloadBySharedBuffer.get(sab);
121
+ if (payload !== undefined)
122
+ return payload;
123
+ const pin = pinSab(sab);
124
+ payload = createSharedArrayBufferPayload(sab, pin, makeMetadata(pin));
125
+ return payload;
126
+ };
127
+ const createSharedArrayBufferPayload = (buffer, pin, metadata) => {
128
+ let payload = payloadBySharedBuffer.get(buffer);
129
+ if (payload !== undefined)
130
+ return payload;
131
+ const fullNumeric = makeFullNumericMetadata(pin);
132
+ const tokenOnlyNumeric = makeTokenNumericMetadata(pin);
133
+ payload = {
134
+ [EXTERNAL_PAYLOAD_BRAND]: SHARED_ARRAY_BUFFER_CODEC_ID,
135
+ toMetadata: () => metadata,
136
+ [SHARED_ARRAY_BUFFER_NUMERIC_TRANSFER]: (transportKey) => {
137
+ if (fullNumeric === undefined) {
138
+ return undefined;
139
+ }
140
+ const warmTokens = getWarmTokens(transportKey);
141
+ if (warmTokens === undefined)
142
+ return fullNumeric;
143
+ if (warmTokens.has(pin.token))
144
+ return tokenOnlyNumeric;
145
+ warmTokens.add(pin.token);
146
+ return fullNumeric;
147
+ },
148
+ };
149
+ payloadBySharedBuffer.set(buffer, payload);
150
+ return payload;
151
+ };
152
+ export const getSharedArrayBufferPayload = (value) => {
153
+ if (isSharedArrayBufferValue(value))
154
+ return wrapSharedArrayBufferPayload(value);
155
+ return payloadBySharedBuffer.get(value);
156
+ };
157
+ const isSharedArrayBufferMetadata = (value) => {
158
+ if (value === null || typeof value !== "object")
159
+ return false;
160
+ const meta = value;
161
+ return (meta.kind === SHARED_ARRAY_BUFFER_CODEC_ID &&
162
+ typeof meta.origin === "string" &&
163
+ typeof meta.runtime === "string" &&
164
+ typeof meta.pointer === "string" &&
165
+ typeof meta.token === "string" &&
166
+ typeof meta.byteLength === "number" &&
167
+ Number.isInteger(meta.byteLength) &&
168
+ meta.byteLength >= 0);
169
+ };
170
+ const materializeSharedBuffer = (metadata, warmOnly) => {
171
+ if (metadata.origin !== PROCESS_ORIGIN) {
172
+ throw new Error(`SharedArrayBuffer cannot cross a process boundary (origin ${metadata.origin} ` +
173
+ `!= ${PROCESS_ORIGIN}); it is shared by reference to thread workers only.`);
174
+ }
175
+ const token = BigInt(metadata.token);
176
+ const cached = cachedSharedBuffersByToken.get(token);
177
+ if (cached !== undefined)
178
+ return cached;
179
+ if (warmOnly) {
180
+ throw new TypeError("SharedArrayBuffer cache miss for warm token payload");
181
+ }
182
+ const region = getBufferReferenceCapabilities().adoptShared({
183
+ token,
184
+ pointer: BigInt(metadata.pointer),
185
+ byteOffset: 0,
186
+ byteLength: metadata.byteLength,
187
+ });
188
+ cachedSharedBuffersByToken.set(token, region.buffer);
189
+ createSharedArrayBufferPayload(region.buffer, {
190
+ token,
191
+ pointer: BigInt(metadata.pointer),
192
+ byteLength: metadata.byteLength,
193
+ }, metadata);
194
+ return region.buffer;
195
+ };
196
+ const decode = (metadata) => {
197
+ if (!isSharedArrayBufferMetadata(metadata)) {
198
+ throw new TypeError("Invalid SharedArrayBuffer payload metadata");
199
+ }
200
+ return materializeSharedBuffer(metadata, false);
201
+ };
202
+ const decodeNumeric = (words) => {
203
+ if (words.length === SHARED_ARRAY_BUFFER_TOKEN_NUMERIC_WORDS) {
204
+ const token = joinU64(words[0] ?? 0, words[1] ?? 0);
205
+ const cached = cachedSharedBuffersByToken.get(token);
206
+ if (cached !== undefined)
207
+ return cached;
208
+ throw new TypeError("SharedArrayBuffer cache miss for warm token payload");
209
+ }
210
+ const runtime = decodeRuntime(words[5] ?? 0);
211
+ if (runtime === undefined) {
212
+ throw new TypeError("Invalid SharedArrayBuffer numeric runtime");
213
+ }
214
+ const originPid = words[6];
215
+ if (originPid === undefined ||
216
+ !Number.isInteger(originPid) ||
217
+ originPid < 0) {
218
+ throw new TypeError("Invalid SharedArrayBuffer numeric origin");
219
+ }
220
+ if (words.length !== SHARED_ARRAY_BUFFER_NUMERIC_WORDS) {
221
+ throw new TypeError("Invalid SharedArrayBuffer numeric word count");
222
+ }
223
+ const metadata = {
224
+ kind: SHARED_ARRAY_BUFFER_CODEC_ID,
225
+ origin: `${runtime}:${originPid >>> 0}`,
226
+ runtime,
227
+ pointer: joinU64(words[2] ?? 0, words[3] ?? 0).toString(),
228
+ token: joinU64(words[0] ?? 0, words[1] ?? 0).toString(),
229
+ byteLength: words[4] ?? 0,
230
+ };
231
+ return materializeSharedBuffer(metadata, false);
232
+ };
233
+ const codecGlobal = globalThis;
234
+ const codecs = codecGlobal.__KNITTING_PAYLOAD_CODECS__ ??= Object.create(null);
235
+ codecs[SHARED_ARRAY_BUFFER_CODEC_ID] = { decode, decodeNumeric };
@@ -0,0 +1,425 @@
1
+ #if !defined(__linux__) && !defined(__APPLE__) && !defined(_WIN32)
2
+ #error "knitting_buffer_pointer.cc currently supports Linux, macOS, and Windows."
3
+ #endif
4
+
5
+ #include <node.h>
6
+ #include <v8.h>
7
+
8
+ #include <cstdint>
9
+ #include <memory>
10
+ #include <mutex>
11
+ #include <unordered_map>
12
+
13
+ namespace knitting_buffer_pointer {
14
+
15
+ struct RetainedReference {
16
+ v8::Isolate* isolate;
17
+ v8::Global<v8::Value> value;
18
+ };
19
+
20
+ std::mutex retained_mutex;
21
+ uint64_t next_retained_id = 1;
22
+ std::unordered_map<uint64_t, std::unique_ptr<RetainedReference>> retained_refs;
23
+
24
+ // BackingStore is isolate-independent, unlike v8::Global<Value>.
25
+ // Consumers can co-own moved bytes while the registry keeps producer liveness.
26
+ struct RetainedBackingStore {
27
+ std::shared_ptr<v8::BackingStore> store;
28
+ size_t byte_offset;
29
+ size_t byte_length;
30
+ };
31
+
32
+ std::mutex backing_mutex;
33
+ uint64_t next_backing_id = 1;
34
+ std::unordered_map<uint64_t, RetainedBackingStore> retained_backings;
35
+
36
+ void ThrowType(v8::Isolate* isolate, const char* message) {
37
+ isolate->ThrowException(v8::Exception::TypeError(
38
+ v8::String::NewFromUtf8(isolate, message).ToLocalChecked()
39
+ ));
40
+ }
41
+
42
+ v8::Local<v8::String> DetachKey(v8::Isolate* isolate) {
43
+ return v8::String::NewFromUtf8Literal(
44
+ isolate,
45
+ "knitting.bufferReference.detachKey"
46
+ );
47
+ }
48
+
49
+ bool DetachWithKey(
50
+ v8::Local<v8::ArrayBuffer> buffer,
51
+ v8::Local<v8::Value> key
52
+ ) {
53
+ v8::Maybe<bool> detached = buffer->Detach(key);
54
+ return (detached.IsJust() && detached.FromJust()) || buffer->WasDetached();
55
+ }
56
+
57
+ bool DetachDefaultArrayBuffer(v8::Local<v8::ArrayBuffer> buffer) {
58
+ if (buffer->WasDetached()) return true;
59
+ if (!buffer->IsDetachable()) return false;
60
+ return DetachWithKey(buffer, v8::Local<v8::Value>());
61
+ }
62
+
63
+ bool DetachKnittingArrayBuffer(
64
+ v8::Isolate* isolate,
65
+ v8::Local<v8::ArrayBuffer> buffer
66
+ ) {
67
+ if (buffer->WasDetached()) return true;
68
+ if (!buffer->IsDetachable()) return false;
69
+ if (DetachWithKey(buffer, DetachKey(isolate))) return true;
70
+ return DetachWithKey(buffer, v8::Local<v8::Value>());
71
+ }
72
+
73
+ bool ReadPointerInfo(
74
+ v8::Isolate* isolate,
75
+ v8::Local<v8::Value> value,
76
+ void** data,
77
+ size_t* byte_length
78
+ ) {
79
+ size_t byte_offset = 0;
80
+
81
+ if (value->IsArrayBufferView()) {
82
+ v8::Local<v8::ArrayBufferView> view = value.As<v8::ArrayBufferView>();
83
+ *data = view->Buffer()->Data();
84
+ byte_offset = view->ByteOffset();
85
+ *byte_length = view->ByteLength();
86
+ } else if (value->IsArrayBuffer()) {
87
+ v8::Local<v8::ArrayBuffer> buffer = value.As<v8::ArrayBuffer>();
88
+ *data = buffer->Data();
89
+ *byte_length = buffer->ByteLength();
90
+ } else if (value->IsSharedArrayBuffer()) {
91
+ v8::Local<v8::SharedArrayBuffer> buffer = value.As<v8::SharedArrayBuffer>();
92
+ *data = buffer->Data();
93
+ *byte_length = buffer->ByteLength();
94
+ } else {
95
+ ThrowType(
96
+ isolate,
97
+ "getPointer expects an ArrayBuffer, SharedArrayBuffer, or typed array"
98
+ );
99
+ return false;
100
+ }
101
+
102
+ *data = static_cast<uint8_t*>(*data) + byte_offset;
103
+ return true;
104
+ }
105
+
106
+ uint64_t ReadToken(
107
+ const v8::FunctionCallbackInfo<v8::Value>& args,
108
+ int index,
109
+ const char* message
110
+ ) {
111
+ v8::Isolate* isolate = args.GetIsolate();
112
+ if (args.Length() <= index || !args[index]->IsBigInt()) {
113
+ ThrowType(isolate, message);
114
+ return 0;
115
+ }
116
+
117
+ bool lossless = false;
118
+ uint64_t token = args[index].As<v8::BigInt>()->Uint64Value(&lossless);
119
+ if (token == 0) {
120
+ ThrowType(isolate, "retained pointer token must be non-zero");
121
+ return 0;
122
+ }
123
+ return token;
124
+ }
125
+
126
+ void GetPointer(const v8::FunctionCallbackInfo<v8::Value>& args) {
127
+ v8::Isolate* isolate = args.GetIsolate();
128
+
129
+ if (args.Length() < 1) {
130
+ ThrowType(isolate, "getPointer(buffer) requires an ArrayBuffer or view");
131
+ return;
132
+ }
133
+
134
+ void* data = nullptr;
135
+ size_t byte_length = 0;
136
+ if (!ReadPointerInfo(isolate, args[0], &data, &byte_length)) return;
137
+
138
+ uintptr_t address = reinterpret_cast<uintptr_t>(data);
139
+ args.GetReturnValue().Set(
140
+ v8::BigInt::NewFromUnsigned(isolate, static_cast<uint64_t>(address))
141
+ );
142
+ }
143
+
144
+ void RetainPointer(const v8::FunctionCallbackInfo<v8::Value>& args) {
145
+ v8::Isolate* isolate = args.GetIsolate();
146
+ v8::Local<v8::Context> context = isolate->GetCurrentContext();
147
+
148
+ if (args.Length() < 1) {
149
+ ThrowType(isolate, "retainPointer(buffer) requires an ArrayBuffer or view");
150
+ return;
151
+ }
152
+
153
+ void* data = nullptr;
154
+ size_t byte_length = 0;
155
+ if (!ReadPointerInfo(isolate, args[0], &data, &byte_length)) return;
156
+
157
+ auto retained = std::make_unique<RetainedReference>();
158
+ retained->isolate = isolate;
159
+ retained->value.Reset(isolate, args[0]);
160
+
161
+ uint64_t token = 0;
162
+ {
163
+ std::lock_guard<std::mutex> lock(retained_mutex);
164
+ token = next_retained_id++;
165
+ if (token == 0) token = next_retained_id++;
166
+ retained_refs.emplace(token, std::move(retained));
167
+ }
168
+
169
+ v8::Local<v8::Object> result = v8::Object::New(isolate);
170
+ result->Set(
171
+ context,
172
+ v8::String::NewFromUtf8Literal(isolate, "pointer"),
173
+ v8::BigInt::NewFromUnsigned(
174
+ isolate,
175
+ static_cast<uint64_t>(reinterpret_cast<uintptr_t>(data))
176
+ )
177
+ ).ToChecked();
178
+ result->Set(
179
+ context,
180
+ v8::String::NewFromUtf8Literal(isolate, "byteLength"),
181
+ v8::Number::New(isolate, static_cast<double>(byte_length))
182
+ ).ToChecked();
183
+ result->Set(
184
+ context,
185
+ v8::String::NewFromUtf8Literal(isolate, "token"),
186
+ v8::BigInt::NewFromUnsigned(isolate, token)
187
+ ).ToChecked();
188
+ args.GetReturnValue().Set(result);
189
+ }
190
+
191
+ void ReleasePointer(const v8::FunctionCallbackInfo<v8::Value>& args) {
192
+ uint64_t token = ReadToken(args, 0, "releasePointer(token) requires a bigint");
193
+ if (token == 0) return;
194
+
195
+ std::unique_ptr<RetainedReference> retained;
196
+ {
197
+ std::lock_guard<std::mutex> lock(retained_mutex);
198
+ auto found = retained_refs.find(token);
199
+ if (found == retained_refs.end()) {
200
+ args.GetReturnValue().Set(false);
201
+ return;
202
+ }
203
+ retained = std::move(found->second);
204
+ retained_refs.erase(found);
205
+ }
206
+
207
+ retained->value.Reset();
208
+ args.GetReturnValue().Set(true);
209
+ }
210
+
211
+ void NoopDeleter(void*, size_t, void*) {}
212
+
213
+ void WrapPointer(const v8::FunctionCallbackInfo<v8::Value>& args) {
214
+ v8::Isolate* isolate = args.GetIsolate();
215
+ v8::Local<v8::Context> context = isolate->GetCurrentContext();
216
+
217
+ if (args.Length() < 2 || !args[0]->IsBigInt() || !args[1]->IsNumber()) {
218
+ ThrowType(
219
+ isolate,
220
+ "wrapPointer(pointer: bigint, byteLength: number) requires both arguments"
221
+ );
222
+ return;
223
+ }
224
+
225
+ bool lossless = false;
226
+ uint64_t address = args[0].As<v8::BigInt>()->Uint64Value(&lossless);
227
+ if (address == 0) {
228
+ ThrowType(isolate, "wrapPointer received a null pointer");
229
+ return;
230
+ }
231
+
232
+ v8::Maybe<int64_t> maybe_length = args[1]->IntegerValue(context);
233
+ if (maybe_length.IsNothing() || maybe_length.FromJust() < 0) {
234
+ ThrowType(isolate, "byteLength must be a non-negative integer");
235
+ return;
236
+ }
237
+ size_t byte_length = static_cast<size_t>(maybe_length.FromJust());
238
+
239
+ void* data = reinterpret_cast<void*>(static_cast<uintptr_t>(address));
240
+ std::unique_ptr<v8::BackingStore> backing = v8::ArrayBuffer::NewBackingStore(
241
+ data,
242
+ byte_length,
243
+ NoopDeleter,
244
+ nullptr
245
+ );
246
+ std::shared_ptr<v8::BackingStore> shared(std::move(backing));
247
+ v8::Local<v8::ArrayBuffer> buffer = v8::ArrayBuffer::New(isolate, shared);
248
+ buffer->SetDetachKey(DetachKey(isolate));
249
+ args.GetReturnValue().Set(buffer);
250
+ }
251
+
252
+ void DetachArrayBuffer(const v8::FunctionCallbackInfo<v8::Value>& args) {
253
+ v8::Isolate* isolate = args.GetIsolate();
254
+
255
+ if (args.Length() < 1 || !args[0]->IsArrayBuffer()) {
256
+ ThrowType(isolate, "detachArrayBuffer(buffer) requires an ArrayBuffer");
257
+ return;
258
+ }
259
+
260
+ v8::Local<v8::ArrayBuffer> buffer = args[0].As<v8::ArrayBuffer>();
261
+ if (buffer->WasDetached()) {
262
+ args.GetReturnValue().Set(true);
263
+ return;
264
+ }
265
+ args.GetReturnValue().Set(DetachKnittingArrayBuffer(isolate, buffer));
266
+ }
267
+
268
+ // Read the underlying ArrayBuffer + region of an ArrayBuffer or typed-array view.
269
+ // SharedArrayBuffer is rejected: it is already shareable and must not be detached.
270
+ bool ReadArrayBufferRegion(
271
+ v8::Isolate* isolate,
272
+ v8::Local<v8::Value> value,
273
+ v8::Local<v8::ArrayBuffer>* buffer,
274
+ size_t* byte_offset,
275
+ size_t* byte_length
276
+ ) {
277
+ if (value->IsArrayBufferView()) {
278
+ v8::Local<v8::ArrayBufferView> view = value.As<v8::ArrayBufferView>();
279
+ *buffer = view->Buffer();
280
+ *byte_offset = view->ByteOffset();
281
+ *byte_length = view->ByteLength();
282
+ return true;
283
+ }
284
+ if (value->IsArrayBuffer()) {
285
+ v8::Local<v8::ArrayBuffer> ab = value.As<v8::ArrayBuffer>();
286
+ *buffer = ab;
287
+ *byte_offset = 0;
288
+ *byte_length = ab->ByteLength();
289
+ return true;
290
+ }
291
+ ThrowType(
292
+ isolate,
293
+ "retainBackingStore expects an ArrayBuffer or typed array (not a SharedArrayBuffer)"
294
+ );
295
+ return false;
296
+ }
297
+
298
+ // retainBackingStore(buffer) -> { pointer, byteOffset, byteLength, token }
299
+ // Registry-pins the backing store, then detaches the source for the move.
300
+ void RetainBackingStore(const v8::FunctionCallbackInfo<v8::Value>& args) {
301
+ v8::Isolate* isolate = args.GetIsolate();
302
+ v8::Local<v8::Context> context = isolate->GetCurrentContext();
303
+
304
+ if (args.Length() < 1) {
305
+ ThrowType(isolate, "retainBackingStore(buffer) requires an ArrayBuffer or view");
306
+ return;
307
+ }
308
+
309
+ v8::Local<v8::ArrayBuffer> buffer;
310
+ size_t byte_offset = 0;
311
+ size_t byte_length = 0;
312
+ if (!ReadArrayBufferRegion(isolate, args[0], &buffer, &byte_offset, &byte_length)) {
313
+ return;
314
+ }
315
+
316
+ std::shared_ptr<v8::BackingStore> store = buffer->GetBackingStore();
317
+ uint8_t* base = static_cast<uint8_t*>(store->Data());
318
+ uintptr_t address = reinterpret_cast<uintptr_t>(base + byte_offset);
319
+
320
+ // Detach only after taking the shared_ptr; ordinary JS buffers use no key.
321
+ DetachDefaultArrayBuffer(buffer);
322
+
323
+ uint64_t token = 0;
324
+ {
325
+ std::lock_guard<std::mutex> lock(backing_mutex);
326
+ token = next_backing_id++;
327
+ if (token == 0) token = next_backing_id++;
328
+ retained_backings.emplace(
329
+ token,
330
+ RetainedBackingStore{ std::move(store), byte_offset, byte_length }
331
+ );
332
+ }
333
+
334
+ v8::Local<v8::Object> result = v8::Object::New(isolate);
335
+ result->Set(
336
+ context,
337
+ v8::String::NewFromUtf8Literal(isolate, "pointer"),
338
+ v8::BigInt::NewFromUnsigned(isolate, static_cast<uint64_t>(address))
339
+ ).ToChecked();
340
+ result->Set(
341
+ context,
342
+ v8::String::NewFromUtf8Literal(isolate, "byteOffset"),
343
+ v8::Number::New(isolate, static_cast<double>(byte_offset))
344
+ ).ToChecked();
345
+ result->Set(
346
+ context,
347
+ v8::String::NewFromUtf8Literal(isolate, "byteLength"),
348
+ v8::Number::New(isolate, static_cast<double>(byte_length))
349
+ ).ToChecked();
350
+ result->Set(
351
+ context,
352
+ v8::String::NewFromUtf8Literal(isolate, "token"),
353
+ v8::BigInt::NewFromUnsigned(isolate, token)
354
+ ).ToChecked();
355
+ args.GetReturnValue().Set(result);
356
+ }
357
+
358
+ // adoptBackingStore(token) -> ArrayBuffer
359
+ // Returns an ArrayBuffer in the caller's isolate that co-owns the registry store.
360
+ // The registry entry remains until producer release.
361
+ void AdoptBackingStore(const v8::FunctionCallbackInfo<v8::Value>& args) {
362
+ v8::Isolate* isolate = args.GetIsolate();
363
+ uint64_t token = ReadToken(args, 0, "adoptBackingStore(token) requires a bigint");
364
+ if (token == 0) return;
365
+
366
+ std::shared_ptr<v8::BackingStore> store;
367
+ {
368
+ std::lock_guard<std::mutex> lock(backing_mutex);
369
+ auto found = retained_backings.find(token);
370
+ if (found == retained_backings.end()) {
371
+ ThrowType(isolate, "adoptBackingStore: unknown or released token");
372
+ return;
373
+ }
374
+ store = found->second.store;
375
+ }
376
+
377
+ v8::Local<v8::ArrayBuffer> buffer = v8::ArrayBuffer::New(isolate, store);
378
+ buffer->SetDetachKey(DetachKey(isolate));
379
+ args.GetReturnValue().Set(buffer);
380
+ }
381
+
382
+ // releaseBackingStore(token) -> boolean
383
+ // Drops the registry's reference. The store is freed only if no consumer adopted.
384
+ void ReleaseBackingStore(const v8::FunctionCallbackInfo<v8::Value>& args) {
385
+ uint64_t token =
386
+ ReadToken(args, 0, "releaseBackingStore(token) requires a bigint");
387
+ if (token == 0) return;
388
+
389
+ RetainedBackingStore released;
390
+ bool found = false;
391
+ {
392
+ std::lock_guard<std::mutex> lock(backing_mutex);
393
+ auto it = retained_backings.find(token);
394
+ if (it != retained_backings.end()) {
395
+ released = std::move(it->second);
396
+ retained_backings.erase(it);
397
+ found = true;
398
+ }
399
+ }
400
+ // released.store drops here, outside the lock.
401
+ args.GetReturnValue().Set(found);
402
+ }
403
+
404
+ // SAB transport avoids SharedArrayBuffer::New across isolates: worker teardown
405
+ // can corrupt the shared store. JS pins plus non-owning aliases keep native
406
+ // handles from outliving their isolate.
407
+
408
+ void Initialize(
409
+ v8::Local<v8::Object> exports,
410
+ v8::Local<v8::Value>,
411
+ v8::Local<v8::Context>
412
+ ) {
413
+ NODE_SET_METHOD(exports, "getPointer", GetPointer);
414
+ NODE_SET_METHOD(exports, "retainPointer", RetainPointer);
415
+ NODE_SET_METHOD(exports, "releasePointer", ReleasePointer);
416
+ NODE_SET_METHOD(exports, "wrapPointer", WrapPointer);
417
+ NODE_SET_METHOD(exports, "detachArrayBuffer", DetachArrayBuffer);
418
+ NODE_SET_METHOD(exports, "retainBackingStore", RetainBackingStore);
419
+ NODE_SET_METHOD(exports, "adoptBackingStore", AdoptBackingStore);
420
+ NODE_SET_METHOD(exports, "releaseBackingStore", ReleaseBackingStore);
421
+ }
422
+
423
+ NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize)
424
+
425
+ }
@@ -49,7 +49,13 @@ export declare enum PayloadBuffer {
49
49
  EnvelopeDynamicHeaderString = 43,
50
50
  ExternalPayload = 44,
51
51
  StaticExternalPayload = 45,
52
- ProcessSharedBuffer = 46
52
+ ProcessSharedBuffer = 46,
53
+ BufferReference = 47,
54
+ SharedArrayBuffer = 48,
55
+ EnvelopeStaticHeaderExternal = 49,
56
+ EnvelopeDynamicHeaderExternal = 50,
57
+ EnvelopeStaticHeaderStringExternal = 51,
58
+ EnvelopeDynamicHeaderStringExternal = 52
53
59
  }
54
60
  export declare enum LockBound {
55
61
  paddingLock = 0,
@@ -70,15 +76,20 @@ export type Task = [
70
76
  number
71
77
  ] & {
72
78
  value: unknown;
79
+ finalize?: (() => void) | undefined;
73
80
  resolve: (value?: unknown) => void;
74
81
  reject: (reason?: unknown) => void;
75
82
  };
83
+ export declare const PayloadTransportFinalizer: unique symbol;
76
84
  export declare const PromisePayloadMarker: unique symbol;
77
85
  export type PromisePayloadHandler = (task: Task, isRejected: boolean, value: unknown) => void;
78
86
  export declare const beginPromisePayload: (task: Task) => boolean;
79
87
  export declare const finishPromisePayload: (task: Task) => void;
80
88
  export declare const isPromisePayloadPending: (task: Task) => boolean;
81
89
  export declare const resetTaskLocalFlags: (task: Task) => void;
90
+ export declare const addTaskFinalizer: (task: Task, finalizer: () => void) => void;
91
+ export declare const attachPayloadTransportFinalizer: (task: Task, value: unknown) => void;
92
+ export declare const runTaskFinalizers: (task: Task) => void;
82
93
  export declare enum TaskIndex {
83
94
  /**
84
95
  * Worker -> host response flags word.