knitting 0.1.51 → 0.1.53
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 +213 -54
- package/knitting.d.ts +1 -0
- package/map.md +15 -3
- package/package.json +14 -2
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.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_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_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/src/api.d.ts +29 -16
- package/src/api.js +98 -28
- package/src/common/envelope.d.ts +9 -3
- package/src/common/envelope.js +14 -0
- package/src/common/worker-runtime.d.ts +2 -0
- package/src/common/worker-runtime.js +9 -0
- package/src/connections/buffer-reference-native.d.ts +56 -0
- package/src/connections/buffer-reference-native.js +217 -0
- package/src/connections/buffer-reference.d.ts +78 -0
- package/src/connections/buffer-reference.js +461 -0
- package/src/connections/index.d.ts +1 -0
- package/src/connections/index.js +1 -0
- package/src/connections/node-addons.d.ts +1 -1
- package/src/connections/node-buffer-pointer.d.ts +20 -0
- package/src/connections/node-buffer-pointer.js +16 -0
- package/src/connections/process-shared-buffer.d.ts +6 -0
- package/src/connections/process-shared-buffer.js +6 -0
- package/src/connections/shared-array-buffer-payload.d.ts +36 -0
- package/src/connections/shared-array-buffer-payload.js +235 -0
- package/src/debug/env-diff.d.ts +26 -0
- package/src/debug/env-diff.js +49 -0
- package/src/debug/gate.d.ts +18 -0
- package/src/debug/gate.js +69 -0
- package/src/debug/handle.d.ts +23 -0
- package/src/debug/handle.js +48 -0
- package/src/ipc/transport/shared-memory.d.ts +1 -3
- package/src/knitting_buffer_pointer.cc +425 -0
- package/src/memory/lock.d.ts +12 -1
- package/src/memory/lock.js +47 -4
- package/src/memory/payload-config.d.ts +9 -0
- package/src/memory/payloadCodec.js +220 -37
- package/src/permission/protocol.d.ts +1 -1
- package/src/permission/protocol.js +30 -20
- package/src/runtime/pool.d.ts +3 -5
- package/src/runtime/pool.js +18 -18
- package/src/runtime/process-worker.js +5 -2
- package/src/runtime/tx-queue.d.ts +3 -2
- package/src/runtime/tx-queue.js +18 -13
- package/src/types.d.ts +54 -50
- package/src/utils/http.d.ts +29 -0
- package/src/utils/http.js +100 -0
- package/src/worker/loop.js +76 -14
- package/src/worker/rx-queue.d.ts +4 -1
- package/src/worker/rx-queue.js +53 -4
- package/src/worker/safety/startup.d.ts +2 -3
- package/src/worker/safety/startup.js +1 -4
- package/src/worker/timers.js +7 -2
- package/unsafe.d.ts +1 -0
- package/unsafe.js +1 -0
- package/utils.d.ts +1 -0
- package/utils.js +1 -0
package/src/api.js
CHANGED
|
@@ -7,27 +7,58 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
9
|
import { getCallerFilePath, getCallerHref } from "./common/task-source.js";
|
|
10
|
+
import { DEBUG_ENABLED, resolveDebugNamespaces } from "./debug/gate.js";
|
|
10
11
|
import { genTaskID } from "./common/task-source.js";
|
|
11
12
|
import { toModuleUrl } from "./common/module-url.js";
|
|
12
13
|
import { endpointSymbol } from "./common/task-symbol.js";
|
|
13
14
|
import { spawnWorkerContext } from "./runtime/pool.js";
|
|
14
|
-
import {
|
|
15
|
+
import { RUNTIME } from "./common/runtime.js";
|
|
16
|
+
import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
|
|
15
17
|
import { resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
|
|
16
18
|
import { getNodeProcess } from "./common/node-compat.js";
|
|
17
19
|
import { managerMethod } from "./runtime/balancer.js";
|
|
18
20
|
import { createInlineExecutor } from "./runtime/inline-executor.js";
|
|
21
|
+
const hasDebugNamespace = (namespaces, namespace) => namespaces.has("*") || namespaces.has(namespace);
|
|
22
|
+
const createHostDebug = (namespaces) => {
|
|
23
|
+
const enabled = (namespace) => hasDebugNamespace(namespaces, namespace);
|
|
24
|
+
if (!enabled("host"))
|
|
25
|
+
return undefined;
|
|
26
|
+
const base = performance.now();
|
|
27
|
+
const tag = `host·${RUNTIME}`;
|
|
28
|
+
const log = (message) => {
|
|
29
|
+
const elapsed = (performance.now() - base).toFixed(1);
|
|
30
|
+
console.error(`[${tag}·+${elapsed}ms] host: ${message}`);
|
|
31
|
+
};
|
|
32
|
+
return { log };
|
|
33
|
+
};
|
|
34
|
+
const readHostCwd = () => {
|
|
35
|
+
const denoCwd = globalThis.Deno?.cwd;
|
|
36
|
+
if (typeof denoCwd === "function") {
|
|
37
|
+
try {
|
|
38
|
+
return denoCwd();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const nodeProcess = getNodeProcess();
|
|
44
|
+
if (typeof nodeProcess?.cwd === "function") {
|
|
45
|
+
try {
|
|
46
|
+
return nodeProcess.cwd();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
};
|
|
54
|
+
const formatDebugList = (values, empty = "(none)") => values && values.length > 0 ? values.join(",") : empty;
|
|
19
55
|
const MAX_FUNCTION_ID = 0xFFFF;
|
|
20
56
|
const MAX_FUNCTION_COUNT = MAX_FUNCTION_ID + 1;
|
|
21
57
|
const DEFAULT_IMPORT_EXPORT_NAME = "default";
|
|
22
58
|
export const isMain = RUNTIME_IS_MAIN_THREAD;
|
|
23
59
|
export { endpointSymbol as endpointSymbol };
|
|
24
60
|
/**
|
|
25
|
-
*
|
|
26
|
-
* relevant exported functions from a file, also it helps to
|
|
27
|
-
* track a task before naming, ` export ` elements have to be declared
|
|
28
|
-
* at top level and without branching, we take advantage of this to
|
|
29
|
-
* correctly map them.
|
|
30
|
-
*
|
|
61
|
+
* Reconstructs stable task order from top-level exports before names are bound.
|
|
31
62
|
*/
|
|
32
63
|
export const toListAndIds = (args) => {
|
|
33
64
|
const result = args
|
|
@@ -98,13 +129,36 @@ const toPoolTaskEntries = (input, callerHref) => Object.entries(input).map(([nam
|
|
|
98
129
|
}
|
|
99
130
|
throw new TypeError(`createPool task "${name}" must be a task definition or exported function`);
|
|
100
131
|
});
|
|
101
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Create a typed worker pool from module-scope exported tasks/functions.
|
|
134
|
+
*
|
|
135
|
+
* Install/import as `knitting` on npm or `@vixeny/knitting` on JSR. Requires
|
|
136
|
+
* Node 22+, Deno 2+, or Bun 1+.
|
|
137
|
+
*
|
|
138
|
+
* Use `createPool(options)({ taskA })`, call `await pool.call.taskA(arg)`, and
|
|
139
|
+
* clean up with `using pool = ...` or `await pool.shutdown()`.
|
|
140
|
+
*
|
|
141
|
+
* Guard host-only pool setup with `isMain`: workers re-import task modules, and
|
|
142
|
+
* top-level imports in those modules run in every worker. Keep task modules
|
|
143
|
+
* lean and separate from server/framework setup. Each task receives one
|
|
144
|
+
* argument; use an object or tuple for multiple values.
|
|
145
|
+
*/
|
|
146
|
+
export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe, abortSignalCapacity, source, worker, workerExecArgv, permission, host, }) => (tasks) => {
|
|
147
|
+
const bufferReferenceReturn = unsafe?.BufferReferenceReturn;
|
|
148
|
+
const debugRequested = DEBUG_ENABLED ||
|
|
149
|
+
(debug !== undefined && debug !== false);
|
|
150
|
+
let debugNamespaces;
|
|
151
|
+
const getDebugNamespaces = () => debugNamespaces ??= resolveDebugNamespaces(debug);
|
|
152
|
+
const hostDebug = debugRequested
|
|
153
|
+
? createHostDebug(getDebugNamespaces())
|
|
154
|
+
: undefined;
|
|
155
|
+
const debugEnabled = (namespace) => debugRequested && hasDebugNamespace(getDebugNamespaces(), namespace);
|
|
102
156
|
/**
|
|
103
157
|
* This functions is only available in the main thread.
|
|
104
158
|
* Also triggers when debug extra is enabled.
|
|
105
159
|
*/
|
|
106
160
|
if (RUNTIME_IS_MAIN_THREAD === false) {
|
|
107
|
-
if ((
|
|
161
|
+
if (debugEnabled("lifecycle")) {
|
|
108
162
|
console.warn("createPool has been called with : " + JSON.stringify(RUNTIME_WORKER_DATA));
|
|
109
163
|
}
|
|
110
164
|
const notMainThreadError = () => {
|
|
@@ -130,6 +184,10 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
|
|
|
130
184
|
const listOfFunctions = toPoolTaskEntries(tasks, callerHref)
|
|
131
185
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
132
186
|
const { list, ids, names, at } = toListAndIds(listOfFunctions);
|
|
187
|
+
hostDebug?.log(`cwd=${readHostCwd() ?? "(unknown)"} caller=${callerHref}`);
|
|
188
|
+
listOfFunctions.forEach((fn) => {
|
|
189
|
+
hostDebug?.log(`task name=${fn.name} id=${fn.id} from=${fn.importedFrom}`);
|
|
190
|
+
});
|
|
133
191
|
if (listOfFunctions.length > MAX_FUNCTION_COUNT) {
|
|
134
192
|
throw new RangeError(`Too many tasks: received ${listOfFunctions.length}. ` +
|
|
135
193
|
`Maximum is ${MAX_FUNCTION_COUNT} (Uint16 function IDs: 0..${MAX_FUNCTION_ID}).`);
|
|
@@ -200,15 +258,31 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
|
|
|
200
258
|
...(defaultExecArgv ?? []),
|
|
201
259
|
]);
|
|
202
260
|
const execArgv = sanitizeExecArgv(combinedExecArgv.length > 0 ? combinedExecArgv : undefined);
|
|
203
|
-
|
|
261
|
+
hostDebug?.log(`pool runtime=${RUNTIME} workers=${threads ?? 1}` +
|
|
262
|
+
` lanes=${totalNumberOfThread} inliner=${usingInliner ? "on" : "off"}`);
|
|
263
|
+
hostDebug?.log(`modules=${formatDebugList(list)}`);
|
|
264
|
+
hostDebug?.log(`permission=${permissionProtocol?.mode ?? "off"} execArgv=${formatDebugList(execArgv)}`);
|
|
204
265
|
const usesAbortSignal = listOfFunctions.some((fn) => fn.abortSignal !== undefined);
|
|
205
266
|
const resolvedWorker = resolveWorkerBootstrapSettings(worker, callerHref);
|
|
267
|
+
if (resolvedWorker?.bootstrap !== undefined) {
|
|
268
|
+
hostDebug?.log(`bootstrap href=${resolvedWorker.bootstrap.href}` +
|
|
269
|
+
` name=${resolvedWorker.bootstrap.name}`);
|
|
270
|
+
}
|
|
206
271
|
if (usingInliner && resolvedWorker?.bootstrap !== undefined) {
|
|
207
272
|
throw new Error("worker.bootstrap cannot be used with the inliner");
|
|
208
273
|
}
|
|
209
274
|
const hardTimeoutMs = Number.isFinite(resolvedWorker?.hardTimeoutMs)
|
|
210
275
|
? Math.max(1, Math.floor(resolvedWorker?.hardTimeoutMs))
|
|
211
276
|
: undefined;
|
|
277
|
+
if (RUNTIME_POOL_DEPTH >= 1) {
|
|
278
|
+
throw new Error(`createPool() tried to spawn workers from inside a worker process ` +
|
|
279
|
+
`(pool depth ${RUNTIME_POOL_DEPTH}). This usually means a pool is ` +
|
|
280
|
+
`created at module scope in a module your workers import, so every ` +
|
|
281
|
+
`worker spawns its own pool recursively. Is your createPool protected ` +
|
|
282
|
+
`by isMain? Guard pool creation behind \`if (isMain) { ... }\` ` +
|
|
283
|
+
`(import { isMain } from "knitting") so only the main program starts ` +
|
|
284
|
+
`the pool.`);
|
|
285
|
+
}
|
|
212
286
|
let workers = Array.from({
|
|
213
287
|
length: threads ?? 1,
|
|
214
288
|
}).map((_, thread) => spawnWorkerContext({
|
|
@@ -218,16 +292,14 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
|
|
|
218
292
|
at,
|
|
219
293
|
thread,
|
|
220
294
|
debug,
|
|
295
|
+
hostDebug: hostDebug?.log,
|
|
221
296
|
totalNumberOfThread,
|
|
222
297
|
source,
|
|
223
298
|
workerOptions: resolvedWorker,
|
|
224
299
|
workerExecArgv: execArgv,
|
|
225
|
-
host
|
|
300
|
+
host,
|
|
226
301
|
payload,
|
|
227
|
-
|
|
228
|
-
payloadMaxBytes,
|
|
229
|
-
bufferMode,
|
|
230
|
-
maxPayloadBytes,
|
|
302
|
+
bufferReferenceReturn,
|
|
231
303
|
abortSignalCapacity,
|
|
232
304
|
usesAbortSignal,
|
|
233
305
|
permission: permissionProtocol,
|
|
@@ -377,19 +449,17 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
|
|
|
377
449
|
if (imported && usingInliner) {
|
|
378
450
|
return buildImportedInvoker(handlers);
|
|
379
451
|
}
|
|
380
|
-
return useDirectHandler
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
: undefined,
|
|
392
|
-
});
|
|
452
|
+
return useDirectHandler ? handlers[0] : managerMethod({
|
|
453
|
+
contexts: workers,
|
|
454
|
+
balancer,
|
|
455
|
+
handlers,
|
|
456
|
+
inlinerGate: usingInliner
|
|
457
|
+
? {
|
|
458
|
+
index: inlinerIndex,
|
|
459
|
+
threshold: inlinerDispatchThreshold,
|
|
460
|
+
}
|
|
461
|
+
: undefined,
|
|
462
|
+
});
|
|
393
463
|
};
|
|
394
464
|
let callEntries;
|
|
395
465
|
try {
|
package/src/common/envelope.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import type { BufferReference } from "../connections/buffer-reference.js";
|
|
2
|
+
import type { ProcessSharedBuffer } from "../connections/process-shared-buffer.js";
|
|
1
3
|
type EnvelopeHeaderPrimitive = string | number | boolean | null;
|
|
2
4
|
type EnvelopeHeaderValue = EnvelopeHeaderPrimitive | EnvelopeHeaderValue[] | {
|
|
3
5
|
[key: string]: EnvelopeHeaderValue;
|
|
4
6
|
};
|
|
5
7
|
export type EnvelopeHeader = EnvelopeHeaderValue;
|
|
6
|
-
export
|
|
8
|
+
export type EnvelopeBody = ArrayBuffer | SharedArrayBuffer | BufferReference | ProcessSharedBuffer;
|
|
9
|
+
declare const PayloadTransportFinalizer: unique symbol;
|
|
10
|
+
export declare class Envelope<H extends EnvelopeHeader = EnvelopeHeader, B extends EnvelopeBody = ArrayBuffer> {
|
|
7
11
|
readonly header: H;
|
|
8
|
-
readonly payload:
|
|
9
|
-
constructor(header: H, payload:
|
|
12
|
+
readonly payload: B;
|
|
13
|
+
constructor(header: H, payload: B);
|
|
14
|
+
[Symbol.dispose](): void;
|
|
15
|
+
[PayloadTransportFinalizer](): (() => void) | undefined;
|
|
10
16
|
}
|
|
11
17
|
export {};
|
package/src/common/envelope.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const PayloadTransportFinalizer = Symbol.for("knitting.payloadCodec.transportFinalizer");
|
|
1
2
|
export class Envelope {
|
|
2
3
|
header;
|
|
3
4
|
payload;
|
|
@@ -5,4 +6,17 @@ export class Envelope {
|
|
|
5
6
|
this.header = header;
|
|
6
7
|
this.payload = payload;
|
|
7
8
|
}
|
|
9
|
+
[Symbol.dispose]() {
|
|
10
|
+
const body = this.payload;
|
|
11
|
+
if (body !== null && typeof body === "object") {
|
|
12
|
+
body[Symbol.dispose]?.();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
[PayloadTransportFinalizer]() {
|
|
16
|
+
const body = this.payload;
|
|
17
|
+
if (body === null || typeof body !== "object")
|
|
18
|
+
return undefined;
|
|
19
|
+
const finalizer = body[PayloadTransportFinalizer];
|
|
20
|
+
return typeof finalizer === "function" ? finalizer.call(body) : undefined;
|
|
21
|
+
}
|
|
8
22
|
}
|
|
@@ -28,7 +28,9 @@ export type RuntimeMessageChannelLike = {
|
|
|
28
28
|
export declare const RUNTIME_PROCESS_WORKER_ENV = "KNITTING_PROCESS_WORKER";
|
|
29
29
|
export declare const RUNTIME_PROCESS_WORKER_BOOT_ENV = "KNITTING_PROCESS_WORKER_BOOT";
|
|
30
30
|
export declare const RUNTIME_PROCESS_WORKER_BOOT_VERSION = 1;
|
|
31
|
+
export declare const RUNTIME_POOL_DEPTH_ENV = "KNITTING_POOL_DEPTH";
|
|
31
32
|
export declare const RUNTIME_IS_PROCESS_WORKER: boolean;
|
|
33
|
+
export declare const RUNTIME_POOL_DEPTH: number;
|
|
32
34
|
export declare const RUNTIME_WORKER: new (specifier: string | URL, options?: Record<string, unknown>) => RuntimeWorkerLike;
|
|
33
35
|
export declare const RUNTIME_MESSAGE_CHANNEL: new () => RuntimeMessageChannelLike;
|
|
34
36
|
export declare const HAS_NODE_WORKER_THREADS: boolean;
|
|
@@ -2,8 +2,17 @@ import { getNodeBuiltinModule, getNodeProcess } from "./node-compat.js";
|
|
|
2
2
|
export const RUNTIME_PROCESS_WORKER_ENV = "KNITTING_PROCESS_WORKER";
|
|
3
3
|
export const RUNTIME_PROCESS_WORKER_BOOT_ENV = "KNITTING_PROCESS_WORKER_BOOT";
|
|
4
4
|
export const RUNTIME_PROCESS_WORKER_BOOT_VERSION = 1;
|
|
5
|
+
export const RUNTIME_POOL_DEPTH_ENV = "KNITTING_POOL_DEPTH";
|
|
5
6
|
const nodeProcess = getNodeProcess();
|
|
6
7
|
export const RUNTIME_IS_PROCESS_WORKER = nodeProcess?.env?.[RUNTIME_PROCESS_WORKER_ENV] === "1";
|
|
8
|
+
const readPoolDepth = () => {
|
|
9
|
+
const raw = nodeProcess?.env?.[RUNTIME_POOL_DEPTH_ENV];
|
|
10
|
+
if (typeof raw !== "string")
|
|
11
|
+
return 0;
|
|
12
|
+
const parsed = Number.parseInt(raw, 10);
|
|
13
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
14
|
+
};
|
|
15
|
+
export const RUNTIME_POOL_DEPTH = readPoolDepth();
|
|
7
16
|
const workerThreads = getNodeBuiltinModule("node:worker_threads");
|
|
8
17
|
const isWorkerGlobalScope = () => {
|
|
9
18
|
const scopeCtor = globalThis.WorkerGlobalScope;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type BufferReferenceRuntime = "node" | "deno" | "bun";
|
|
2
|
+
/** A backing store the producer has taken exclusive ownership of (source detached). */
|
|
3
|
+
export type ProducedBuffer = {
|
|
4
|
+
/** Producer-side release handle. */
|
|
5
|
+
token: bigint;
|
|
6
|
+
/** Address of the region start (view offset already applied). */
|
|
7
|
+
pointer: bigint;
|
|
8
|
+
/** Offset of the region inside the backing store (used by the Node owning adopt). */
|
|
9
|
+
byteOffset: number;
|
|
10
|
+
byteLength: number;
|
|
11
|
+
};
|
|
12
|
+
export type AdoptInput = {
|
|
13
|
+
token: bigint;
|
|
14
|
+
pointer: bigint;
|
|
15
|
+
byteOffset: number;
|
|
16
|
+
byteLength: number;
|
|
17
|
+
};
|
|
18
|
+
/** The materialized region: bytes live in `buffer` at `[byteOffset, byteOffset+byteLength)`. */
|
|
19
|
+
export type AdoptedRegion = {
|
|
20
|
+
buffer: ArrayBuffer;
|
|
21
|
+
byteOffset: number;
|
|
22
|
+
byteLength: number;
|
|
23
|
+
};
|
|
24
|
+
/** Materialized SAB region; `isShared` means `buffer` is a real SharedArrayBuffer. */
|
|
25
|
+
export type SharedAdoptedRegion = {
|
|
26
|
+
buffer: SharedArrayBuffer | ArrayBuffer;
|
|
27
|
+
byteOffset: number;
|
|
28
|
+
byteLength: number;
|
|
29
|
+
isShared: boolean;
|
|
30
|
+
};
|
|
31
|
+
export type AdoptOptions = {
|
|
32
|
+
/** Copy when bytes must survive producer release on non-owning runtimes. */
|
|
33
|
+
copy?: boolean;
|
|
34
|
+
};
|
|
35
|
+
type MovableBufferSource = ArrayBuffer | ArrayBufferView;
|
|
36
|
+
export type BufferReferenceCapabilities = {
|
|
37
|
+
readonly runtime: BufferReferenceRuntime;
|
|
38
|
+
/** True when `adopt` returns a buffer that co-owns the store and survives `release`. */
|
|
39
|
+
readonly supportsOwningAdopt: boolean;
|
|
40
|
+
/** Take exclusive ownership of `source`'s bytes, detaching the source. */
|
|
41
|
+
produce(source: MovableBufferSource): ProducedBuffer;
|
|
42
|
+
/** Materialize the region in the current isolate (owning on Node, alias/copy elsewhere). */
|
|
43
|
+
adopt(input: AdoptInput, opts?: AdoptOptions): AdoptedRegion;
|
|
44
|
+
/** Drop the producer's hold on the backing store. */
|
|
45
|
+
release(token: bigint): void;
|
|
46
|
+
/** True when `adoptShared` returns a real SharedArrayBuffer (Node). */
|
|
47
|
+
readonly supportsSharedAdopt: boolean;
|
|
48
|
+
/** Share a SharedArrayBuffer by reference (no detach — SABs persist). */
|
|
49
|
+
produceShared(sab: SharedArrayBuffer): ProducedBuffer;
|
|
50
|
+
/** Materialize a shared region as a non-owning alias over the same bytes. */
|
|
51
|
+
adoptShared(input: AdoptInput): SharedAdoptedRegion;
|
|
52
|
+
/** Drop the producer's pin on a shared buffer (JS-side; teardown-safe). */
|
|
53
|
+
releaseShared(token: bigint): void;
|
|
54
|
+
};
|
|
55
|
+
export declare const getBufferReferenceCapabilities: () => BufferReferenceCapabilities;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { RUNTIME } from "../common/runtime.js";
|
|
2
|
+
import { loadNodeBufferPointerAddon } from "./node-buffer-pointer.js";
|
|
3
|
+
const isSharedArrayBufferInstance = (value) => typeof SharedArrayBuffer === "function" && value instanceof SharedArrayBuffer;
|
|
4
|
+
const assertMovableSource = (source) => {
|
|
5
|
+
const buffer = ArrayBuffer.isView(source) ? source.buffer : source;
|
|
6
|
+
if (isSharedArrayBufferInstance(buffer)) {
|
|
7
|
+
throw new TypeError("BufferReference expects ArrayBuffer-backed views; SharedArrayBuffer is already " +
|
|
8
|
+
"shared and cannot be moved");
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
const readSourceRegion = (source) => {
|
|
12
|
+
if (ArrayBuffer.isView(source)) {
|
|
13
|
+
return {
|
|
14
|
+
buffer: source.buffer,
|
|
15
|
+
byteOffset: source.byteOffset,
|
|
16
|
+
byteLength: source.byteLength,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { buffer: source, byteOffset: 0, byteLength: source.byteLength };
|
|
20
|
+
};
|
|
21
|
+
/** Detach `buffer`, returning a fixed-length buffer over the same bytes (the move). */
|
|
22
|
+
const detachIntoFixedLength = (buffer) => {
|
|
23
|
+
const transferable = buffer;
|
|
24
|
+
if (typeof transferable.transferToFixedLength === "function") {
|
|
25
|
+
return transferable.transferToFixedLength();
|
|
26
|
+
}
|
|
27
|
+
if (typeof transferable.transfer === "function") {
|
|
28
|
+
return transferable.transfer();
|
|
29
|
+
}
|
|
30
|
+
throw new TypeError("BufferReference requires ArrayBuffer.prototype.transfer to move a buffer in this runtime");
|
|
31
|
+
};
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Deno/Bun pin moved buffers in JS and expose non-owning FFI aliases.
|
|
34
|
+
// Tokens stay producer-isolate-local; consumers materialize from the pointer.
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
const ffiPinned = new Map();
|
|
37
|
+
let nextFfiToken = 1n;
|
|
38
|
+
const pinTransferred = (buffer) => {
|
|
39
|
+
const token = nextFfiToken++;
|
|
40
|
+
ffiPinned.set(token, buffer);
|
|
41
|
+
return token;
|
|
42
|
+
};
|
|
43
|
+
const pinShared = (sab) => {
|
|
44
|
+
const token = nextFfiToken++;
|
|
45
|
+
ffiPinned.set(token, sab);
|
|
46
|
+
return token;
|
|
47
|
+
};
|
|
48
|
+
const releaseFfi = (token) => {
|
|
49
|
+
ffiPinned.delete(token);
|
|
50
|
+
};
|
|
51
|
+
const produceSharedFfi = (sab, readPointer) => {
|
|
52
|
+
// SABs are shared, not detached; pin keeps producer bytes alive.
|
|
53
|
+
const pointer = readPointer(new Uint8Array(sab));
|
|
54
|
+
const token = pinShared(sab);
|
|
55
|
+
return { token, pointer, byteOffset: 0, byteLength: sab.byteLength };
|
|
56
|
+
};
|
|
57
|
+
const getDeno = () => {
|
|
58
|
+
const deno = globalThis.Deno;
|
|
59
|
+
if (deno?.UnsafePointer === undefined || deno.UnsafePointerView === undefined) {
|
|
60
|
+
throw new Error("Deno FFI (UnsafePointer) is not available in this runtime");
|
|
61
|
+
}
|
|
62
|
+
return deno;
|
|
63
|
+
};
|
|
64
|
+
const getBunFFI = () => {
|
|
65
|
+
const ffi = globalThis.Bun?.FFI;
|
|
66
|
+
if (typeof ffi?.ptr !== "function" || typeof ffi?.toArrayBuffer !== "function") {
|
|
67
|
+
throw new Error("Bun FFI (ptr/toArrayBuffer) is not available in this runtime");
|
|
68
|
+
}
|
|
69
|
+
return ffi;
|
|
70
|
+
};
|
|
71
|
+
const produceFfi = (source, readPointer) => {
|
|
72
|
+
assertMovableSource(source);
|
|
73
|
+
const { buffer, byteOffset, byteLength } = readSourceRegion(source);
|
|
74
|
+
const moved = detachIntoFixedLength(buffer);
|
|
75
|
+
// Region view over the moved buffer; the pointer captures the offset.
|
|
76
|
+
const region = new Uint8Array(moved, byteOffset, byteLength);
|
|
77
|
+
const pointer = readPointer(region);
|
|
78
|
+
const token = pinTransferred(moved);
|
|
79
|
+
return { token, pointer, byteOffset, byteLength };
|
|
80
|
+
};
|
|
81
|
+
const finishAlias = (alias, copy) => {
|
|
82
|
+
if (copy) {
|
|
83
|
+
const owned = alias.slice(0);
|
|
84
|
+
return { buffer: owned, byteOffset: 0, byteLength: owned.byteLength };
|
|
85
|
+
}
|
|
86
|
+
return { buffer: alias, byteOffset: 0, byteLength: alias.byteLength };
|
|
87
|
+
};
|
|
88
|
+
const createDenoCapabilities = () => {
|
|
89
|
+
const deno = getDeno();
|
|
90
|
+
return {
|
|
91
|
+
runtime: "deno",
|
|
92
|
+
supportsOwningAdopt: false,
|
|
93
|
+
produce: (view) => produceFfi(view, (region) => deno.UnsafePointer.value(deno.UnsafePointer.of(region))),
|
|
94
|
+
adopt: ({ pointer, byteLength }, opts) => {
|
|
95
|
+
const alias = denoAlias(deno, pointer, byteLength);
|
|
96
|
+
return finishAlias(alias, opts?.copy);
|
|
97
|
+
},
|
|
98
|
+
release: releaseFfi,
|
|
99
|
+
supportsSharedAdopt: false,
|
|
100
|
+
produceShared: (sab) => produceSharedFfi(sab, (region) => deno.UnsafePointer.value(deno.UnsafePointer.of(region))),
|
|
101
|
+
adoptShared: ({ pointer, byteLength }) => {
|
|
102
|
+
const alias = denoAlias(deno, pointer, byteLength);
|
|
103
|
+
return { buffer: alias, byteOffset: 0, byteLength, isShared: false };
|
|
104
|
+
},
|
|
105
|
+
releaseShared: releaseFfi,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
const denoAlias = (deno, pointer, byteLength) => {
|
|
109
|
+
const handle = deno.UnsafePointer.create(pointer);
|
|
110
|
+
if (handle === null || handle === undefined) {
|
|
111
|
+
throw new Error("Deno.UnsafePointer.create returned null");
|
|
112
|
+
}
|
|
113
|
+
return new deno.UnsafePointerView(handle).getArrayBuffer(byteLength);
|
|
114
|
+
};
|
|
115
|
+
const createBunCapabilities = () => {
|
|
116
|
+
const ffi = getBunFFI();
|
|
117
|
+
return {
|
|
118
|
+
runtime: "bun",
|
|
119
|
+
supportsOwningAdopt: false,
|
|
120
|
+
produce: (view) => produceFfi(view, (region) => BigInt(ffi.ptr(region))),
|
|
121
|
+
adopt: ({ pointer, byteLength }, opts) => {
|
|
122
|
+
const alias = ffi.toArrayBuffer(Number(pointer), 0, byteLength);
|
|
123
|
+
return finishAlias(alias, opts?.copy);
|
|
124
|
+
},
|
|
125
|
+
release: releaseFfi,
|
|
126
|
+
supportsSharedAdopt: false,
|
|
127
|
+
produceShared: (sab) => produceSharedFfi(sab, (region) => BigInt(ffi.ptr(region))),
|
|
128
|
+
adoptShared: ({ pointer, byteLength }) => {
|
|
129
|
+
const alias = ffi.toArrayBuffer(Number(pointer), 0, byteLength);
|
|
130
|
+
return { buffer: alias, byteOffset: 0, byteLength, isShared: false };
|
|
131
|
+
},
|
|
132
|
+
releaseShared: releaseFfi,
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Node: addon-backed owning move via shared_ptr<BackingStore>.
|
|
137
|
+
// Older prebuilds fall back to non-owning retain/wrap.
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
const createNodeCapabilities = () => {
|
|
140
|
+
const addon = loadNodeBufferPointerAddon();
|
|
141
|
+
const owning = typeof addon.retainBackingStore === "function" &&
|
|
142
|
+
typeof addon.adoptBackingStore === "function" &&
|
|
143
|
+
typeof addon.releaseBackingStore === "function";
|
|
144
|
+
// SABs use JS pins plus non-owning aliases so no native handle survives teardown.
|
|
145
|
+
const sharedCapabilities = {
|
|
146
|
+
supportsSharedAdopt: false,
|
|
147
|
+
produceShared: (sab) => produceSharedFfi(sab, (region) => addon.getPointer(region)),
|
|
148
|
+
adoptShared: ({ pointer, byteLength }) => {
|
|
149
|
+
const alias = addon.wrapPointer(pointer, byteLength);
|
|
150
|
+
return { buffer: alias, byteOffset: 0, byteLength, isShared: false };
|
|
151
|
+
},
|
|
152
|
+
releaseShared: releaseFfi,
|
|
153
|
+
};
|
|
154
|
+
if (owning) {
|
|
155
|
+
return {
|
|
156
|
+
runtime: "node",
|
|
157
|
+
supportsOwningAdopt: true,
|
|
158
|
+
produce: (source) => {
|
|
159
|
+
assertMovableSource(source);
|
|
160
|
+
const r = addon.retainBackingStore(source);
|
|
161
|
+
return {
|
|
162
|
+
token: r.token,
|
|
163
|
+
pointer: r.pointer,
|
|
164
|
+
byteOffset: r.byteOffset,
|
|
165
|
+
byteLength: r.byteLength,
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
adopt: ({ token, byteOffset, byteLength }, opts) => {
|
|
169
|
+
const buffer = addon.adoptBackingStore(token);
|
|
170
|
+
if (opts?.copy) {
|
|
171
|
+
const owned = buffer.slice(byteOffset, byteOffset + byteLength);
|
|
172
|
+
return { buffer: owned, byteOffset: 0, byteLength: owned.byteLength };
|
|
173
|
+
}
|
|
174
|
+
return { buffer, byteOffset, byteLength };
|
|
175
|
+
},
|
|
176
|
+
release: (token) => void addon.releaseBackingStore(token),
|
|
177
|
+
...sharedCapabilities,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Legacy fallback: producer pins via retainPointer, consumer wraps non-owning.
|
|
181
|
+
return {
|
|
182
|
+
runtime: "node",
|
|
183
|
+
supportsOwningAdopt: false,
|
|
184
|
+
produce: (source) => {
|
|
185
|
+
assertMovableSource(source);
|
|
186
|
+
const retained = addon.retainPointer(source);
|
|
187
|
+
const byteOffset = ArrayBuffer.isView(source) ? source.byteOffset : 0;
|
|
188
|
+
return {
|
|
189
|
+
token: retained.token,
|
|
190
|
+
pointer: retained.pointer,
|
|
191
|
+
byteOffset,
|
|
192
|
+
byteLength: retained.byteLength,
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
adopt: ({ pointer, byteLength }, opts) => {
|
|
196
|
+
const alias = addon.wrapPointer(pointer, byteLength);
|
|
197
|
+
return finishAlias(alias, opts?.copy);
|
|
198
|
+
},
|
|
199
|
+
release: (token) => void addon.releasePointer(token),
|
|
200
|
+
...sharedCapabilities,
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
let cached;
|
|
204
|
+
export const getBufferReferenceCapabilities = () => {
|
|
205
|
+
if (cached !== undefined)
|
|
206
|
+
return cached;
|
|
207
|
+
switch (RUNTIME) {
|
|
208
|
+
case "node":
|
|
209
|
+
return cached = createNodeCapabilities();
|
|
210
|
+
case "deno":
|
|
211
|
+
return cached = createDenoCapabilities();
|
|
212
|
+
case "bun":
|
|
213
|
+
return cached = createBunCapabilities();
|
|
214
|
+
default:
|
|
215
|
+
throw new Error(`BufferReference cannot run in runtime "${RUNTIME}"`);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type BufferReferenceRuntime } from "./buffer-reference-native.js";
|
|
2
|
+
export type { BufferReferenceRuntime } from "./buffer-reference-native.js";
|
|
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
|
+
export declare const BUFFER_REFERENCE_NUMERIC_TRANSFER: unique symbol;
|
|
13
|
+
export declare const BUFFER_REFERENCE_RETURN_RELEASE_TOKEN: unique symbol;
|
|
14
|
+
declare const EXTERNAL_PAYLOAD_BRAND: unique symbol;
|
|
15
|
+
export declare const BUFFER_REFERENCE_CODEC_ID: "knitting.bufferReference";
|
|
16
|
+
declare const BUFFER_REFERENCE_RETURN_RELEASE_MESSAGE_KEY = "__knittingBufferReferenceRelease";
|
|
17
|
+
export type BufferReferenceNumericMetadata = readonly [
|
|
18
|
+
pointerLow: number,
|
|
19
|
+
pointerHigh: number,
|
|
20
|
+
tokenLow: number,
|
|
21
|
+
tokenHigh: number,
|
|
22
|
+
byteOffset: number,
|
|
23
|
+
byteLength: number,
|
|
24
|
+
runtime: number,
|
|
25
|
+
originPid: number
|
|
26
|
+
];
|
|
27
|
+
export type BufferReferenceMetadata = {
|
|
28
|
+
readonly kind: typeof BUFFER_REFERENCE_KIND;
|
|
29
|
+
readonly origin: string;
|
|
30
|
+
readonly runtime: BufferReferenceRuntime;
|
|
31
|
+
readonly pointer: string;
|
|
32
|
+
/** Producer-side release handle (process-local). */
|
|
33
|
+
readonly token: string;
|
|
34
|
+
readonly byteOffset: number;
|
|
35
|
+
readonly byteLength: number;
|
|
36
|
+
};
|
|
37
|
+
export type BufferReferenceReturnReleaseMessage = {
|
|
38
|
+
readonly [BUFFER_REFERENCE_RETURN_RELEASE_MESSAGE_KEY]: string;
|
|
39
|
+
};
|
|
40
|
+
type BufferReferenceReturnReleaser = (token: bigint) => void;
|
|
41
|
+
type BufferSource = ArrayBufferView | ArrayBuffer;
|
|
42
|
+
declare const PAYLOAD_TRANSPORT_FINALIZER: unique symbol;
|
|
43
|
+
export declare const withBufferReferenceReturnReleaser: <T>(releaser: BufferReferenceReturnReleaser | undefined, run: () => T) => T;
|
|
44
|
+
export declare const createBufferReferenceReturnReleaseMessage: (token: bigint) => BufferReferenceReturnReleaseMessage;
|
|
45
|
+
export declare const readBufferReferenceReturnReleaseMessage: (value: unknown) => bigint | undefined;
|
|
46
|
+
export declare const isBufferReferenceMetadata: (value: unknown) => value is BufferReferenceMetadata;
|
|
47
|
+
/**
|
|
48
|
+
* Zero-copy handle for moving ArrayBuffer bytes to/from **thread** workers.
|
|
49
|
+
*
|
|
50
|
+
* Construction detaches the source. Consumers materialize the moved region in
|
|
51
|
+
* their isolate: owning on Node, alias/copy on Deno/Bun. Return a
|
|
52
|
+
* `BufferReference` for large binary results to avoid copying back through the
|
|
53
|
+
* transport. Use `ProcessSharedBuffer` across process boundaries.
|
|
54
|
+
*/
|
|
55
|
+
export declare class BufferReference {
|
|
56
|
+
#private;
|
|
57
|
+
readonly [EXTERNAL_PAYLOAD_BRAND]: "knitting.bufferReference";
|
|
58
|
+
readonly runtime: BufferReferenceRuntime;
|
|
59
|
+
readonly origin: string;
|
|
60
|
+
readonly pointer: bigint;
|
|
61
|
+
readonly byteOffset: number;
|
|
62
|
+
readonly byteLength: number;
|
|
63
|
+
constructor(source: BufferSource);
|
|
64
|
+
constructor(source: BufferSource | undefined, meta: BufferReferenceMetadata);
|
|
65
|
+
static fromMetadata(meta: BufferReferenceMetadata): BufferReference;
|
|
66
|
+
toMetadata(): BufferReferenceMetadata;
|
|
67
|
+
[BUFFER_REFERENCE_NUMERIC_TRANSFER](): BufferReferenceNumericMetadata | undefined;
|
|
68
|
+
get isLocal(): boolean;
|
|
69
|
+
/** Prepare a returned reference before the worker-side producer hold drains. */
|
|
70
|
+
claimOwnership(releaser?: BufferReferenceReturnReleaser): this;
|
|
71
|
+
toArrayBuffer(): ArrayBuffer;
|
|
72
|
+
toUint8Array(): Uint8Array;
|
|
73
|
+
/** The source is moved on construction, so it is never retained here. */
|
|
74
|
+
get source(): undefined;
|
|
75
|
+
release(): void;
|
|
76
|
+
[Symbol.dispose](): void;
|
|
77
|
+
[PAYLOAD_TRANSPORT_FINALIZER](): (() => void) | undefined;
|
|
78
|
+
}
|