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
|
@@ -4,6 +4,13 @@ export const RUNTIME_PROCESS_WORKER_ENV = "KNITTING_PROCESS_WORKER";
|
|
|
4
4
|
export const RUNTIME_PROCESS_WORKER_BOOT_ENV = "KNITTING_PROCESS_WORKER_BOOT";
|
|
5
5
|
export const RUNTIME_PROCESS_WORKER_BOOT_VERSION = 1;
|
|
6
6
|
export const RUNTIME_POOL_DEPTH_ENV = "KNITTING_POOL_DEPTH";
|
|
7
|
+
/**
|
|
8
|
+
* A coalesced completion wake sent over a process worker's existing IPC
|
|
9
|
+
* channel. The result itself remains in shared memory; this is only the
|
|
10
|
+
* doorbell byte.
|
|
11
|
+
*/
|
|
12
|
+
export const PROCESS_COMPLETION_DOORBELL = "__knittingCompletionDoorbell";
|
|
13
|
+
export const isProcessCompletionDoorbell = (value) => value === PROCESS_COMPLETION_DOORBELL;
|
|
7
14
|
const nodeProcess = getNodeProcess();
|
|
8
15
|
export const RUNTIME_IS_PROCESS_WORKER = nodeProcess?.env?.[RUNTIME_PROCESS_WORKER_ENV] === "1";
|
|
9
16
|
const readPoolDepth = () => {
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import { type BufferReferenceRuntime } from "./buffer-reference-native.js";
|
|
2
2
|
export type { BufferReferenceRuntime } from "./buffer-reference-native.js";
|
|
3
3
|
export declare const BUFFER_REFERENCE_KIND: "knitting.bufferReference";
|
|
4
|
-
/** Named values for the `unsafe.BufferReferenceReturn` pool option. */
|
|
5
|
-
export declare const BufferReferenceReturn: {
|
|
6
|
-
/** Safe default: Deno/Bun copy the returned bytes so they outlive the worker. */
|
|
7
|
-
readonly Copy: "copy";
|
|
8
|
-
/** Zero-copy: borrow the worker's backing store until the reference is released. */
|
|
9
|
-
readonly Borrow: "borrow";
|
|
10
|
-
};
|
|
11
|
-
export type BufferReferenceReturn = (typeof BufferReferenceReturn)[keyof typeof BufferReferenceReturn];
|
|
12
4
|
export declare const BUFFER_REFERENCE_NUMERIC_TRANSFER: unique symbol;
|
|
13
|
-
export declare const BUFFER_REFERENCE_RETURN_RELEASE_TOKEN: unique symbol;
|
|
14
5
|
declare const EXTERNAL_PAYLOAD_BRAND: unique symbol;
|
|
15
6
|
export declare const BUFFER_REFERENCE_CODEC_ID: "knitting.bufferReference";
|
|
16
|
-
declare const BUFFER_REFERENCE_RETURN_RELEASE_MESSAGE_KEY = "__knittingBufferReferenceRelease";
|
|
17
7
|
export type BufferReferenceNumericMetadata = readonly [
|
|
18
8
|
pointerLow: number,
|
|
19
9
|
pointerHigh: number,
|
|
@@ -34,26 +24,17 @@ export type BufferReferenceMetadata = {
|
|
|
34
24
|
readonly byteOffset: number;
|
|
35
25
|
readonly byteLength: number;
|
|
36
26
|
};
|
|
37
|
-
export type BufferReferenceReturnReleaseMessage = {
|
|
38
|
-
readonly [BUFFER_REFERENCE_RETURN_RELEASE_MESSAGE_KEY]: string;
|
|
39
|
-
};
|
|
40
|
-
type BufferReferenceReturnReleaser = (token: bigint) => void;
|
|
41
|
-
/** Host-side hooks for borrowed returns: release channel plus shutdown tracking. */
|
|
42
|
-
export type BufferReferenceReturnHooks = {
|
|
43
|
-
/** Ask the producing worker to drop its pin for `token`. */
|
|
44
|
-
release: BufferReferenceReturnReleaser;
|
|
45
|
-
/**
|
|
46
|
-
* Record a borrowed reference so the pool can revoke it before its worker
|
|
47
|
-
* dies. Called at claim time without `aliasBuffer`, then again on first
|
|
48
|
-
* materialize with the alias buffer once it exists.
|
|
49
|
-
*/
|
|
50
|
-
track?: (ref: BufferReference, token: bigint, aliasBuffer?: ArrayBuffer) => void;
|
|
51
|
-
};
|
|
52
27
|
type BufferSource = ArrayBufferView | ArrayBuffer;
|
|
53
28
|
declare const PAYLOAD_TRANSPORT_FINALIZER: unique symbol;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Whether a buffer has given up its bytes.
|
|
31
|
+
*
|
|
32
|
+
* Exported so a caller that *moved* a source can confirm the move actually
|
|
33
|
+
* happened. A runtime that cannot detach a buffer -- WASM memory, anything an
|
|
34
|
+
* external API has pinned -- otherwise leaves the producer still able to write
|
|
35
|
+
* memory the consumer has been told it owns.
|
|
36
|
+
*/
|
|
37
|
+
export declare const isArrayBufferDetached: (buffer: ArrayBuffer) => boolean;
|
|
57
38
|
/** Revocation primitive: detach `buffer` so every view over it is neutralized. */
|
|
58
39
|
export declare const detachArrayBufferBestEffort: (runtime: BufferReferenceRuntime, buffer: ArrayBuffer) => boolean;
|
|
59
40
|
export declare const isBufferReferenceMetadata: (value: unknown) => value is BufferReferenceMetadata;
|
|
@@ -80,19 +61,12 @@ export declare class BufferReference {
|
|
|
80
61
|
[BUFFER_REFERENCE_NUMERIC_TRANSFER](): BufferReferenceNumericMetadata | undefined;
|
|
81
62
|
get isLocal(): boolean;
|
|
82
63
|
/** Prepare a returned reference before the worker-side producer hold drains. */
|
|
83
|
-
claimOwnership(
|
|
64
|
+
claimOwnership(): this;
|
|
84
65
|
toArrayBuffer(): ArrayBuffer;
|
|
85
66
|
toUint8Array(): Uint8Array;
|
|
86
67
|
/** The source is moved on construction, so it is never retained here. */
|
|
87
68
|
get source(): undefined;
|
|
88
69
|
release(): void;
|
|
89
|
-
/**
|
|
90
|
-
* Revoke a borrowed return before its producing worker goes away: detach
|
|
91
|
-
* the borrowed alias so no view can dangle. With `copyBytes` the reference
|
|
92
|
-
* itself survives on an owned copy; without it (worker bytes already gone)
|
|
93
|
-
* later reads fail loud instead of aliasing freed memory.
|
|
94
|
-
*/
|
|
95
|
-
revokeBorrow(copyBytes: boolean): void;
|
|
96
70
|
[Symbol.dispose](): void;
|
|
97
71
|
[PAYLOAD_TRANSPORT_FINALIZER](): (() => void) | undefined;
|
|
98
72
|
}
|
|
@@ -4,18 +4,9 @@ import { RUNTIME_IS_MAIN_THREAD } from "../common/worker-runtime.js";
|
|
|
4
4
|
import { loadNodeBufferPointerAddon } from "./node-buffer-pointer.js";
|
|
5
5
|
import { getBufferReferenceCapabilities, } from "./buffer-reference-native.js";
|
|
6
6
|
export const BUFFER_REFERENCE_KIND = "knitting.bufferReference";
|
|
7
|
-
/** Named values for the `unsafe.BufferReferenceReturn` pool option. */
|
|
8
|
-
export const BufferReferenceReturn = {
|
|
9
|
-
/** Safe default: Deno/Bun copy the returned bytes so they outlive the worker. */
|
|
10
|
-
Copy: "copy",
|
|
11
|
-
/** Zero-copy: borrow the worker's backing store until the reference is released. */
|
|
12
|
-
Borrow: "borrow",
|
|
13
|
-
};
|
|
14
7
|
export const BUFFER_REFERENCE_NUMERIC_TRANSFER = Symbol.for("knitting.bufferReference.numericTransfer");
|
|
15
|
-
export const BUFFER_REFERENCE_RETURN_RELEASE_TOKEN = Symbol.for("knitting.bufferReference.returnReleaseToken");
|
|
16
8
|
const EXTERNAL_PAYLOAD_BRAND = Symbol.for("knitting.payloadCodec");
|
|
17
9
|
export const BUFFER_REFERENCE_CODEC_ID = BUFFER_REFERENCE_KIND;
|
|
18
|
-
const BUFFER_REFERENCE_RETURN_RELEASE_MESSAGE_KEY = "__knittingBufferReferenceRelease";
|
|
19
10
|
const getRuntime = () => {
|
|
20
11
|
if (RUNTIME === "deno" || RUNTIME === "bun" || RUNTIME === "node") {
|
|
21
12
|
return RUNTIME;
|
|
@@ -37,7 +28,6 @@ const RUNTIME_NODE = 1;
|
|
|
37
28
|
const RUNTIME_DENO = 2;
|
|
38
29
|
const RUNTIME_BUN = 3;
|
|
39
30
|
const PAYLOAD_TRANSPORT_FINALIZER = Symbol.for("knitting.payloadCodec.transportFinalizer");
|
|
40
|
-
let currentReturnHooks;
|
|
41
31
|
const bufferReferenceInstances = new WeakSet();
|
|
42
32
|
const encodeRuntime = (runtime) => {
|
|
43
33
|
switch (runtime) {
|
|
@@ -96,36 +86,6 @@ const numericMetadataToMetadata = (words) => {
|
|
|
96
86
|
byteLength: words[5] ?? 0,
|
|
97
87
|
};
|
|
98
88
|
};
|
|
99
|
-
const toReturnHooks = (releaser) => typeof releaser === "function" ? { release: releaser } : releaser;
|
|
100
|
-
export const withBufferReferenceReturnReleaser = (releaser, run) => {
|
|
101
|
-
if (releaser === undefined)
|
|
102
|
-
return run();
|
|
103
|
-
const previous = currentReturnHooks;
|
|
104
|
-
currentReturnHooks = toReturnHooks(releaser);
|
|
105
|
-
try {
|
|
106
|
-
return run();
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
currentReturnHooks = previous;
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
export const createBufferReferenceReturnReleaseMessage = (token) => ({
|
|
113
|
-
[BUFFER_REFERENCE_RETURN_RELEASE_MESSAGE_KEY]: token.toString(),
|
|
114
|
-
});
|
|
115
|
-
export const readBufferReferenceReturnReleaseMessage = (value) => {
|
|
116
|
-
if (value === null || typeof value !== "object")
|
|
117
|
-
return undefined;
|
|
118
|
-
const raw = value[BUFFER_REFERENCE_RETURN_RELEASE_MESSAGE_KEY];
|
|
119
|
-
if (typeof raw !== "string")
|
|
120
|
-
return undefined;
|
|
121
|
-
try {
|
|
122
|
-
const token = BigInt(raw);
|
|
123
|
-
return token > 0n ? token : undefined;
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
89
|
const producerFinalizer = typeof FinalizationRegistry === "function"
|
|
130
90
|
? new FinalizationRegistry((backstop) => {
|
|
131
91
|
backstop.count -= 1;
|
|
@@ -139,16 +99,6 @@ const producerFinalizer = typeof FinalizationRegistry === "function"
|
|
|
139
99
|
}
|
|
140
100
|
})
|
|
141
101
|
: undefined;
|
|
142
|
-
const borrowedReturnFinalizer = typeof FinalizationRegistry === "function"
|
|
143
|
-
? new FinalizationRegistry(({ token, release }) => {
|
|
144
|
-
try {
|
|
145
|
-
release(token);
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
// best effort
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
: undefined;
|
|
152
102
|
const isSharedArrayBufferInstance = (value) => typeof SharedArrayBuffer === "function" && value instanceof SharedArrayBuffer;
|
|
153
103
|
const assertMovableSource = (source) => {
|
|
154
104
|
if (isSharedArrayBufferInstance(source)) {
|
|
@@ -166,6 +116,15 @@ const assertMovableSource = (source) => {
|
|
|
166
116
|
};
|
|
167
117
|
const isDetached = (buffer) => buffer.detached === true ||
|
|
168
118
|
buffer.byteLength === 0;
|
|
119
|
+
/**
|
|
120
|
+
* Whether a buffer has given up its bytes.
|
|
121
|
+
*
|
|
122
|
+
* Exported so a caller that *moved* a source can confirm the move actually
|
|
123
|
+
* happened. A runtime that cannot detach a buffer -- WASM memory, anything an
|
|
124
|
+
* external API has pinned -- otherwise leaves the producer still able to write
|
|
125
|
+
* memory the consumer has been told it owns.
|
|
126
|
+
*/
|
|
127
|
+
export const isArrayBufferDetached = (buffer) => isDetached(buffer);
|
|
169
128
|
/** Revocation primitive: detach `buffer` so every view over it is neutralized. */
|
|
170
129
|
export const detachArrayBufferBestEffort = (runtime, buffer) => {
|
|
171
130
|
if (isDetached(buffer))
|
|
@@ -241,9 +200,6 @@ export class BufferReference {
|
|
|
241
200
|
#producerBackstop;
|
|
242
201
|
#materializedRegions;
|
|
243
202
|
#owned;
|
|
244
|
-
#borrowedReturnReleaser;
|
|
245
|
-
#borrowedReturnTrack;
|
|
246
|
-
#borrowedReturnFinalizerToken;
|
|
247
203
|
#transportRefs = 0;
|
|
248
204
|
#released = false;
|
|
249
205
|
constructor(source, meta) {
|
|
@@ -341,9 +297,8 @@ export class BufferReference {
|
|
|
341
297
|
}
|
|
342
298
|
}
|
|
343
299
|
/** Prepare a returned reference before the worker-side producer hold drains. */
|
|
344
|
-
claimOwnership(
|
|
345
|
-
if (this.#owned !== undefined || this.#
|
|
346
|
-
this.#released)
|
|
300
|
+
claimOwnership() {
|
|
301
|
+
if (this.#owned !== undefined || this.#released)
|
|
347
302
|
return this;
|
|
348
303
|
this.#assertLocal();
|
|
349
304
|
const caps = getBufferReferenceCapabilities();
|
|
@@ -353,21 +308,6 @@ export class BufferReference {
|
|
|
353
308
|
byteOffset: this.byteOffset,
|
|
354
309
|
byteLength: this.byteLength,
|
|
355
310
|
};
|
|
356
|
-
if (releaser !== undefined) {
|
|
357
|
-
const hooks = toReturnHooks(releaser);
|
|
358
|
-
// Borrow: adopt lazily — the alias costs GC-visible external memory on
|
|
359
|
-
// FFI runtimes, so it is only created when the bytes are actually read.
|
|
360
|
-
// Until then no view can exist, so the GC backstop safely watches the
|
|
361
|
-
// reference itself; first materialize migrates it to the alias buffer
|
|
362
|
-
// (views strongly reference their buffer, so the worker pin can only
|
|
363
|
-
// drop once no view can reach the borrowed bytes).
|
|
364
|
-
this.#borrowedReturnReleaser = hooks.release;
|
|
365
|
-
this.#borrowedReturnTrack = hooks.track;
|
|
366
|
-
this.#borrowedReturnFinalizerToken = {};
|
|
367
|
-
borrowedReturnFinalizer?.register(this, { token: this.#token, release: hooks.release }, this.#borrowedReturnFinalizerToken);
|
|
368
|
-
hooks.track?.(this, this.#token);
|
|
369
|
-
return this;
|
|
370
|
-
}
|
|
371
311
|
this.#owned = caps.adopt(input, caps.supportsOwningAdopt ? undefined : { copy: true });
|
|
372
312
|
return this;
|
|
373
313
|
}
|
|
@@ -382,18 +322,6 @@ export class BufferReference {
|
|
|
382
322
|
byteOffset: this.byteOffset,
|
|
383
323
|
byteLength: this.byteLength,
|
|
384
324
|
});
|
|
385
|
-
if (this.#borrowedReturnReleaser !== undefined) {
|
|
386
|
-
// First borrowed read: views over this alias exist from here on, so
|
|
387
|
-
// move the GC backstop from the reference onto the alias buffer.
|
|
388
|
-
this.#owned = region;
|
|
389
|
-
if (this.#borrowedReturnFinalizerToken !== undefined &&
|
|
390
|
-
borrowedReturnFinalizer !== undefined) {
|
|
391
|
-
borrowedReturnFinalizer.unregister(this.#borrowedReturnFinalizerToken);
|
|
392
|
-
borrowedReturnFinalizer.register(region.buffer, { token: this.#token, release: this.#borrowedReturnReleaser }, this.#borrowedReturnFinalizerToken);
|
|
393
|
-
}
|
|
394
|
-
this.#borrowedReturnTrack?.(this, this.#token, region.buffer);
|
|
395
|
-
return region;
|
|
396
|
-
}
|
|
397
325
|
(this.#materializedRegions ??= []).push(region);
|
|
398
326
|
if (this.#producerBackstop !== undefined && producerFinalizer !== undefined) {
|
|
399
327
|
this.#producerBackstop.count += 1;
|
|
@@ -420,6 +348,10 @@ export class BufferReference {
|
|
|
420
348
|
if (this.#released)
|
|
421
349
|
return;
|
|
422
350
|
this.#released = true;
|
|
351
|
+
// A claimed Node return is an owning ArrayBuffer, not a borrow. Drop this
|
|
352
|
+
// wrapper's ownership immediately; any ArrayBuffer or typed-array the
|
|
353
|
+
// caller kept still co-owns the backing store and stays valid.
|
|
354
|
+
this.#owned = undefined;
|
|
423
355
|
// Revoke every alias this reference handed out before any pin can drop.
|
|
424
356
|
// Leak-not-UAF: if a detach fails, the pin must stay held — the GC
|
|
425
357
|
// backstops remain registered and retry once the buffers are unreachable.
|
|
@@ -433,31 +365,6 @@ export class BufferReference {
|
|
|
433
365
|
allDetached;
|
|
434
366
|
}
|
|
435
367
|
}
|
|
436
|
-
const borrowedReturnReleaser = this.#borrowedReturnReleaser;
|
|
437
|
-
this.#borrowedReturnReleaser = undefined;
|
|
438
|
-
this.#borrowedReturnTrack = undefined;
|
|
439
|
-
if (borrowedReturnReleaser !== undefined) {
|
|
440
|
-
const owned = this.#owned;
|
|
441
|
-
this.#owned = undefined;
|
|
442
|
-
if (owned !== undefined) {
|
|
443
|
-
allDetached = detachArrayBufferBestEffort(this.runtime, owned.buffer) &&
|
|
444
|
-
allDetached;
|
|
445
|
-
}
|
|
446
|
-
if (allDetached) {
|
|
447
|
-
if (this.#borrowedReturnFinalizerToken !== undefined) {
|
|
448
|
-
borrowedReturnFinalizer?.unregister(this.#borrowedReturnFinalizerToken);
|
|
449
|
-
this.#borrowedReturnFinalizerToken = undefined;
|
|
450
|
-
}
|
|
451
|
-
try {
|
|
452
|
-
borrowedReturnReleaser(this.#token);
|
|
453
|
-
}
|
|
454
|
-
catch {
|
|
455
|
-
// best effort
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
// else: the finalizer stays registered on the alias buffer and sends
|
|
459
|
-
// the release once every escaped view is gone.
|
|
460
|
-
}
|
|
461
368
|
// Only the producer owns the registry hold; consumers must not drop it.
|
|
462
369
|
if (this.#isProducer && allDetached) {
|
|
463
370
|
if (this.#finalizerToken !== undefined) {
|
|
@@ -475,65 +382,6 @@ export class BufferReference {
|
|
|
475
382
|
// else on failed detach: the countdown backstop drops the pin when the
|
|
476
383
|
// reference and every materialized alias are unreachable.
|
|
477
384
|
}
|
|
478
|
-
/**
|
|
479
|
-
* Revoke a borrowed return before its producing worker goes away: detach
|
|
480
|
-
* the borrowed alias so no view can dangle. With `copyBytes` the reference
|
|
481
|
-
* itself survives on an owned copy; without it (worker bytes already gone)
|
|
482
|
-
* later reads fail loud instead of aliasing freed memory.
|
|
483
|
-
*/
|
|
484
|
-
revokeBorrow(copyBytes) {
|
|
485
|
-
const releaser = this.#borrowedReturnReleaser;
|
|
486
|
-
if (releaser === undefined || this.#released)
|
|
487
|
-
return;
|
|
488
|
-
const owned = this.#owned;
|
|
489
|
-
let nextOwned;
|
|
490
|
-
let detached = true;
|
|
491
|
-
if (owned !== undefined) {
|
|
492
|
-
if (copyBytes) {
|
|
493
|
-
nextOwned = {
|
|
494
|
-
buffer: owned.buffer.slice(owned.byteOffset, owned.byteOffset + owned.byteLength),
|
|
495
|
-
byteOffset: 0,
|
|
496
|
-
byteLength: owned.byteLength,
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
// Without a copy, keep the (now detached) region so reads throw
|
|
500
|
-
// instead of re-materializing an alias over freed worker memory.
|
|
501
|
-
detached = detachArrayBufferBestEffort(this.runtime, owned.buffer);
|
|
502
|
-
}
|
|
503
|
-
else if (copyBytes) {
|
|
504
|
-
// Never materialized: take the bytes now, while the worker still pins
|
|
505
|
-
// them, so the reference stays readable after its worker dies.
|
|
506
|
-
const caps = getBufferReferenceCapabilities();
|
|
507
|
-
nextOwned = caps.adopt({
|
|
508
|
-
token: this.#token,
|
|
509
|
-
pointer: this.pointer,
|
|
510
|
-
byteOffset: this.byteOffset,
|
|
511
|
-
byteLength: this.byteLength,
|
|
512
|
-
}, { copy: !caps.supportsOwningAdopt });
|
|
513
|
-
}
|
|
514
|
-
else {
|
|
515
|
-
// Never materialized and the worker bytes are already gone: no view
|
|
516
|
-
// exists, so poison future reads instead of aliasing freed memory.
|
|
517
|
-
this.#released = true;
|
|
518
|
-
}
|
|
519
|
-
if (!detached)
|
|
520
|
-
return;
|
|
521
|
-
if (nextOwned !== undefined)
|
|
522
|
-
this.#owned = nextOwned;
|
|
523
|
-
this.#borrowedReturnReleaser = undefined;
|
|
524
|
-
this.#borrowedReturnTrack = undefined;
|
|
525
|
-
if (this.#borrowedReturnFinalizerToken !== undefined) {
|
|
526
|
-
borrowedReturnFinalizer?.unregister(this.#borrowedReturnFinalizerToken);
|
|
527
|
-
this.#borrowedReturnFinalizerToken = undefined;
|
|
528
|
-
}
|
|
529
|
-
// Best effort: if the worker is still alive it can drop its pin early.
|
|
530
|
-
try {
|
|
531
|
-
releaser(this.#token);
|
|
532
|
-
}
|
|
533
|
-
catch {
|
|
534
|
-
// best effort
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
385
|
[Symbol.dispose]() {
|
|
538
386
|
this.release();
|
|
539
387
|
}
|
|
@@ -550,9 +398,6 @@ export class BufferReference {
|
|
|
550
398
|
if (this.#transportRefs <= 0)
|
|
551
399
|
this.release();
|
|
552
400
|
});
|
|
553
|
-
if (this.#isProducer) {
|
|
554
|
-
finalizer[BUFFER_REFERENCE_RETURN_RELEASE_TOKEN] = this.#token;
|
|
555
|
-
}
|
|
556
401
|
return finalizer;
|
|
557
402
|
}
|
|
558
403
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
type NodeRequire = (specifier: string) => unknown;
|
|
2
|
-
export type NodeNativeAddonName = "knitting_shared_memory" | "knitting_shm" | "knitting_buffer_pointer";
|
|
2
|
+
export type NodeNativeAddonName = "knitting_shared_memory" | "knitting_shm" | "knitting_buffer_pointer" | "knitting_doorbell";
|
|
3
3
|
export type NodePlatformInfo = {
|
|
4
4
|
arch: string;
|
|
5
5
|
modules?: string;
|
|
@@ -34,6 +34,14 @@ export const formatNodeNativeAddonLoadError = (name, platformInfo, errors) => {
|
|
|
34
34
|
? "an unknown version"
|
|
35
35
|
: `v${platformInfo.version}`;
|
|
36
36
|
const target = `${platformInfo.platform}-${platformInfo.arch}`;
|
|
37
|
+
if (errors.some((error) => error.includes("ERR_DLOPEN_DISABLED"))) {
|
|
38
|
+
return (`knitting: Node.js ${version} blocked loading native addon ${name} ` +
|
|
39
|
+
`because --allow-addons is not enabled. For a thread worker pool, ` +
|
|
40
|
+
`set permission.node.allowAddons to true; if the host itself runs ` +
|
|
41
|
+
`with --permission, start it with --allow-addons too. This permits ` +
|
|
42
|
+
`task code to load Node native addons; only enable it for trusted ` +
|
|
43
|
+
`tasks.${attempts}`);
|
|
44
|
+
}
|
|
37
45
|
if (abi === NODE_26_MODULE_ABI) {
|
|
38
46
|
return (`knitting: Node.js 26 uses node:ffi instead of ABI-specific addons. ` +
|
|
39
47
|
`Restart Node with the --experimental-ffi flag` +
|
|
@@ -85,7 +93,9 @@ export const loadNodeNativeAddon = (require, name, specifier) => {
|
|
|
85
93
|
return require(candidate);
|
|
86
94
|
}
|
|
87
95
|
catch (error) {
|
|
88
|
-
|
|
96
|
+
const code = error?.code;
|
|
97
|
+
errors.push(`${candidate}: ${typeof code === "string" ? `${code}: ` : ""}` +
|
|
98
|
+
String(error));
|
|
89
99
|
}
|
|
90
100
|
}
|
|
91
101
|
throw new Error(formatNodeNativeAddonLoadError(name, readNodePlatformInfo(), errors));
|
|
@@ -30,6 +30,13 @@ type SharedArrayBufferPayload = {
|
|
|
30
30
|
readonly [SHARED_ARRAY_BUFFER_NUMERIC_TRANSFER]: (transportKey?: object) => SharedArrayBufferNumericMetadata | SharedArrayBufferTokenNumericMetadata | undefined;
|
|
31
31
|
};
|
|
32
32
|
export declare const isSharedArrayBufferValue: (value: unknown) => value is SharedArrayBuffer;
|
|
33
|
+
type SharedPin = {
|
|
34
|
+
token: bigint;
|
|
35
|
+
pointer: bigint;
|
|
36
|
+
byteLength: number;
|
|
37
|
+
};
|
|
38
|
+
/** The pin backing `buffer`, when it has been shared at least once. */
|
|
39
|
+
export declare const getSharedPin: (buffer: object) => SharedPin | undefined;
|
|
33
40
|
/** Wrap a SAB as external payload; GC-managed pins mean no settle finalizer. */
|
|
34
41
|
export declare const wrapSharedArrayBufferPayload: (sab: SharedArrayBuffer) => SharedArrayBufferPayload;
|
|
35
42
|
export declare const getSharedArrayBufferPayload: (value: object) => SharedArrayBufferPayload | undefined;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { RUNTIME } from "../common/runtime.js";
|
|
2
2
|
import { getNodeProcess } from "../common/node-compat.js";
|
|
3
3
|
import { getBufferReferenceCapabilities } from "./buffer-reference-native.js";
|
|
4
|
-
//
|
|
5
|
-
// SABs are
|
|
4
|
+
// SharedArrayBuffer transport uses a process-local pointer and is limited to
|
|
5
|
+
// thread workers; SABs are not moved or detached.
|
|
6
6
|
export const SHARED_ARRAY_BUFFER_CODEC_ID = "knitting.sharedArrayBuffer";
|
|
7
7
|
export const SHARED_ARRAY_BUFFER_NUMERIC_TRANSFER = Symbol.for("knitting.sharedArrayBuffer.numericTransfer");
|
|
8
8
|
export const SHARED_ARRAY_BUFFER_NUMERIC_WORDS = 8;
|
|
9
9
|
const SHARED_ARRAY_BUFFER_TOKEN_NUMERIC_WORDS = 2;
|
|
10
|
+
// Slice frames carry an explicit length; warm frames carry only the token.
|
|
10
11
|
const EXTERNAL_PAYLOAD_BRAND = Symbol.for("knitting.payloadCodec");
|
|
11
12
|
const getProcessId = () => {
|
|
12
13
|
const proc = getNodeProcess();
|
|
@@ -23,7 +24,19 @@ export const isSharedArrayBufferValue = (value) => hasSharedArrayBuffer && value
|
|
|
23
24
|
const pinnedBySab = new WeakMap();
|
|
24
25
|
const payloadBySharedBuffer = new WeakMap();
|
|
25
26
|
const warmedTokensByTransport = new WeakMap();
|
|
26
|
-
|
|
27
|
+
// Tokens are isolate-local, so adopted aliases are cached per transport lane.
|
|
28
|
+
const adoptedByTransport = new WeakMap();
|
|
29
|
+
const adoptedWithoutTransport = new Map();
|
|
30
|
+
const adoptedCacheFor = (transportKey) => {
|
|
31
|
+
if (transportKey === undefined)
|
|
32
|
+
return adoptedWithoutTransport;
|
|
33
|
+
let cache = adoptedByTransport.get(transportKey);
|
|
34
|
+
if (cache === undefined) {
|
|
35
|
+
cache = new Map();
|
|
36
|
+
adoptedByTransport.set(transportKey, cache);
|
|
37
|
+
}
|
|
38
|
+
return cache;
|
|
39
|
+
};
|
|
27
40
|
const pinFinalizer = typeof FinalizationRegistry === "function"
|
|
28
41
|
? new FinalizationRegistry((token) => {
|
|
29
42
|
try {
|
|
@@ -87,6 +100,8 @@ const pinSab = (sab) => {
|
|
|
87
100
|
}
|
|
88
101
|
return pin;
|
|
89
102
|
};
|
|
103
|
+
/** The pin backing `buffer`, when it has been shared at least once. */
|
|
104
|
+
export const getSharedPin = (buffer) => pinnedBySab.get(buffer);
|
|
90
105
|
const makeMetadata = (pin) => ({
|
|
91
106
|
kind: SHARED_ARRAY_BUFFER_CODEC_ID,
|
|
92
107
|
origin: PROCESS_ORIGIN,
|
|
@@ -167,13 +182,14 @@ const isSharedArrayBufferMetadata = (value) => {
|
|
|
167
182
|
Number.isInteger(meta.byteLength) &&
|
|
168
183
|
meta.byteLength >= 0);
|
|
169
184
|
};
|
|
170
|
-
const materializeSharedBuffer = (metadata, warmOnly) => {
|
|
185
|
+
const materializeSharedBuffer = (metadata, warmOnly, transportKey) => {
|
|
171
186
|
if (metadata.origin !== PROCESS_ORIGIN) {
|
|
172
187
|
throw new Error(`SharedArrayBuffer cannot cross a process boundary (origin ${metadata.origin} ` +
|
|
173
188
|
`!= ${PROCESS_ORIGIN}); it is shared by reference to thread workers only.`);
|
|
174
189
|
}
|
|
175
190
|
const token = BigInt(metadata.token);
|
|
176
|
-
const
|
|
191
|
+
const cache = adoptedCacheFor(transportKey);
|
|
192
|
+
const cached = cache.get(token);
|
|
177
193
|
if (cached !== undefined)
|
|
178
194
|
return cached;
|
|
179
195
|
if (warmOnly) {
|
|
@@ -185,7 +201,7 @@ const materializeSharedBuffer = (metadata, warmOnly) => {
|
|
|
185
201
|
byteOffset: 0,
|
|
186
202
|
byteLength: metadata.byteLength,
|
|
187
203
|
});
|
|
188
|
-
|
|
204
|
+
cache.set(token, region.buffer);
|
|
189
205
|
createSharedArrayBufferPayload(region.buffer, {
|
|
190
206
|
token,
|
|
191
207
|
pointer: BigInt(metadata.pointer),
|
|
@@ -193,16 +209,16 @@ const materializeSharedBuffer = (metadata, warmOnly) => {
|
|
|
193
209
|
}, metadata);
|
|
194
210
|
return region.buffer;
|
|
195
211
|
};
|
|
196
|
-
const decode = (metadata) => {
|
|
212
|
+
const decode = (metadata, transportKey) => {
|
|
197
213
|
if (!isSharedArrayBufferMetadata(metadata)) {
|
|
198
214
|
throw new TypeError("Invalid SharedArrayBuffer payload metadata");
|
|
199
215
|
}
|
|
200
|
-
return materializeSharedBuffer(metadata, false);
|
|
216
|
+
return materializeSharedBuffer(metadata, false, transportKey);
|
|
201
217
|
};
|
|
202
|
-
const decodeNumeric = (words) => {
|
|
218
|
+
const decodeNumeric = (words, transportKey) => {
|
|
203
219
|
if (words.length === SHARED_ARRAY_BUFFER_TOKEN_NUMERIC_WORDS) {
|
|
204
220
|
const token = joinU64(words[0] ?? 0, words[1] ?? 0);
|
|
205
|
-
const cached =
|
|
221
|
+
const cached = adoptedCacheFor(transportKey).get(token);
|
|
206
222
|
if (cached !== undefined)
|
|
207
223
|
return cached;
|
|
208
224
|
throw new TypeError("SharedArrayBuffer cache miss for warm token payload");
|
|
@@ -228,7 +244,7 @@ const decodeNumeric = (words) => {
|
|
|
228
244
|
token: joinU64(words[0] ?? 0, words[1] ?? 0).toString(),
|
|
229
245
|
byteLength: words[4] ?? 0,
|
|
230
246
|
};
|
|
231
|
-
return materializeSharedBuffer(metadata, false);
|
|
247
|
+
return materializeSharedBuffer(metadata, false, transportKey);
|
|
232
248
|
};
|
|
233
249
|
const codecGlobal = globalThis;
|
|
234
250
|
const codecs = codecGlobal.__KNITTING_PAYLOAD_CODECS__ ??= Object.create(null);
|
package/src/debug/gate.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Read once at module load from `KNITTING_DEBUG`. This module is deliberately
|
|
5
5
|
* tiny and dependency-light: importing it must never pull in the logger or the
|
|
6
6
|
* environment-diff machinery. Callers branch on {@link DEBUG_ENABLED} and only
|
|
7
|
-
* then `await import("./handle.
|
|
7
|
+
* then `await import("./handle.js")`, so when debug is off nothing else under
|
|
8
8
|
* `src/debug` is ever loaded — literally zero cost, not merely cheap.
|
|
9
9
|
*
|
|
10
10
|
* `KNITTING_DEBUG` is a comma-separated list of namespaces:
|
package/src/debug/handle.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ export type DebugInit = {
|
|
|
3
3
|
readonly name: string;
|
|
4
4
|
readonly runtime: string;
|
|
5
5
|
readonly namespaces: ReadonlySet<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Host debug epoch as `timeOrigin + now()`. When absent (debug enabled only
|
|
8
|
+
* inside the worker), the clock starts when this handle initialises.
|
|
9
|
+
*/
|
|
10
|
+
readonly epoch?: number;
|
|
6
11
|
};
|
|
7
12
|
export type Debug = {
|
|
8
13
|
/**
|
|
@@ -20,4 +25,4 @@ export type Debug = {
|
|
|
20
25
|
*/
|
|
21
26
|
envPhase: (label: string) => void;
|
|
22
27
|
};
|
|
23
|
-
export declare const initDebug: ({ name, runtime, namespaces }: DebugInit) => Debug;
|
|
28
|
+
export declare const initDebug: ({ name, runtime, namespaces, epoch }: DebugInit) => Debug;
|
package/src/debug/handle.js
CHANGED
|
@@ -4,16 +4,24 @@
|
|
|
4
4
|
* baseline snapshot it takes exists when debug is off.
|
|
5
5
|
*
|
|
6
6
|
* Diagnostics go to stderr so they never corrupt a worker's stdout, and every
|
|
7
|
-
* line is tagged with the worker id, runtime, and a clock
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* line is tagged with the worker id, runtime, and a clock in milliseconds since
|
|
8
|
+
* the host's debug epoch, so host and worker lines interleave on one timeline.
|
|
9
|
+
*
|
|
10
|
+
* Raw `performance.now()` can't be compared across the thread/process boundary:
|
|
11
|
+
* bun and deno give each worker its own time origin, and a process worker's
|
|
12
|
+
* origin is its own process start. `performance.timeOrigin + performance.now()`
|
|
13
|
+
* is comparable (measured within a few µs on bun, node and deno, for both
|
|
14
|
+
* threads and processes), so the host sends that absolute value and each worker
|
|
15
|
+
* rebases onto it once. Staying in the local `now()` frame afterwards keeps
|
|
16
|
+
* full precision; the absolute sum alone only resolves ~0.24µs.
|
|
11
17
|
*/
|
|
12
18
|
import { describeGlobalKey, diffGlobals, snapshotGlobals, } from "./env-diff.js";
|
|
13
|
-
export const initDebug = ({ name, runtime, namespaces }) => {
|
|
19
|
+
export const initDebug = ({ name, runtime, namespaces, epoch }) => {
|
|
14
20
|
const all = namespaces.has("*");
|
|
15
21
|
const enabled = (namespace) => all || namespaces.has(namespace);
|
|
16
|
-
const base =
|
|
22
|
+
const base = epoch === undefined
|
|
23
|
+
? performance.now()
|
|
24
|
+
: epoch - performance.timeOrigin;
|
|
17
25
|
const tag = `${name}·${runtime}`;
|
|
18
26
|
const log = (namespace, message) => {
|
|
19
27
|
if (!enabled(namespace))
|
package/src/error.d.ts
CHANGED
|
@@ -6,6 +6,15 @@ export declare const ErrorKnitting: {
|
|
|
6
6
|
readonly Serializable: 3;
|
|
7
7
|
};
|
|
8
8
|
export type ErrorKnitting = typeof ErrorKnitting[keyof typeof ErrorKnitting];
|
|
9
|
+
export type KnittingErrorCode = "KNT_ERROR_0" | "KNT_ERROR_1" | "KNT_ERROR_2" | "KNT_ERROR_3" | "THREAD_CLOSED" | "WORKER_CRASHED" | "WORKER_EXITED" | "WORKER_STARTUP_FAILED";
|
|
10
|
+
/**
|
|
11
|
+
* Rejection raised by Knitting itself rather than by task code. Branch on
|
|
12
|
+
* `code`; `message` keeps the human-readable text.
|
|
13
|
+
*/
|
|
14
|
+
export declare class KnittingError extends Error {
|
|
15
|
+
readonly code: KnittingErrorCode;
|
|
16
|
+
constructor(code: KnittingErrorCode, message: string, cause?: unknown);
|
|
17
|
+
}
|
|
9
18
|
export declare const encoderError: ({ task, type, onPromise, detail, }: {
|
|
10
19
|
task: Task;
|
|
11
20
|
type: ErrorKnitting;
|
package/src/error.js
CHANGED
|
@@ -8,6 +8,18 @@ export const ErrorKnitting = {
|
|
|
8
8
|
Json: 2,
|
|
9
9
|
Serializable: 3,
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Rejection raised by Knitting itself rather than by task code. Branch on
|
|
13
|
+
* `code`; `message` keeps the human-readable text.
|
|
14
|
+
*/
|
|
15
|
+
export class KnittingError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code, message, cause) {
|
|
18
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
19
|
+
this.name = "KnittingError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
11
23
|
const reasonFrom = (task, type, detail) => {
|
|
12
24
|
switch (type) {
|
|
13
25
|
case ErrorKnitting.Function: {
|
|
@@ -41,10 +53,12 @@ export const encoderError = ({ task, type, onPromise, detail, }) => {
|
|
|
41
53
|
}
|
|
42
54
|
if (!beginPromisePayload(task))
|
|
43
55
|
return false;
|
|
56
|
+
// Built here, not in the microtask, so the stack still names the caller.
|
|
57
|
+
const error = new KnittingError(`KNT_ERROR_${type}`, reason);
|
|
44
58
|
queueMicrotask(() => {
|
|
45
59
|
finishPromisePayload(task);
|
|
46
|
-
task.value =
|
|
47
|
-
onPromise(task, true,
|
|
60
|
+
task.value = error;
|
|
61
|
+
onPromise(task, true, error);
|
|
48
62
|
});
|
|
49
63
|
return false;
|
|
50
64
|
};
|