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.
Files changed (69) 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 +29 -16
  20. package/src/api.js +98 -28
  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 +78 -0
  28. package/src/connections/buffer-reference.js +461 -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/process-shared-buffer.d.ts +6 -0
  35. package/src/connections/process-shared-buffer.js +6 -0
  36. package/src/connections/shared-array-buffer-payload.d.ts +36 -0
  37. package/src/connections/shared-array-buffer-payload.js +235 -0
  38. package/src/debug/env-diff.d.ts +26 -0
  39. package/src/debug/env-diff.js +49 -0
  40. package/src/debug/gate.d.ts +18 -0
  41. package/src/debug/gate.js +69 -0
  42. package/src/debug/handle.d.ts +23 -0
  43. package/src/debug/handle.js +48 -0
  44. package/src/ipc/transport/shared-memory.d.ts +1 -3
  45. package/src/knitting_buffer_pointer.cc +425 -0
  46. package/src/memory/lock.d.ts +12 -1
  47. package/src/memory/lock.js +47 -4
  48. package/src/memory/payload-config.d.ts +9 -0
  49. package/src/memory/payloadCodec.js +220 -37
  50. package/src/permission/protocol.d.ts +1 -1
  51. package/src/permission/protocol.js +30 -20
  52. package/src/runtime/pool.d.ts +3 -5
  53. package/src/runtime/pool.js +18 -18
  54. package/src/runtime/process-worker.js +5 -2
  55. package/src/runtime/tx-queue.d.ts +3 -2
  56. package/src/runtime/tx-queue.js +18 -13
  57. package/src/types.d.ts +54 -50
  58. package/src/utils/http.d.ts +29 -0
  59. package/src/utils/http.js +100 -0
  60. package/src/worker/loop.js +76 -14
  61. package/src/worker/rx-queue.d.ts +4 -1
  62. package/src/worker/rx-queue.js +53 -4
  63. package/src/worker/safety/startup.d.ts +2 -3
  64. package/src/worker/safety/startup.js +1 -4
  65. package/src/worker/timers.js +7 -2
  66. package/unsafe.d.ts +1 -0
  67. package/unsafe.js +1 -0
  68. package/utils.d.ts +1 -0
  69. 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 { RUNTIME_IS_MAIN_THREAD, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
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
- * With this information we can recreate the logical order of
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
- export const createPool = ({ threads, debug, inliner, balancer, payload, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, source, worker, workerExecArgv, permission, dispatcher, host, }) => (tasks) => {
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 ((debug?.extras === true)) {
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
- const hostDispatcher = host ?? dispatcher;
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: hostDispatcher,
300
+ host,
226
301
  payload,
227
- payloadInitialBytes,
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
- ? handlers[0]
382
- : managerMethod({
383
- contexts: workers,
384
- balancer,
385
- handlers,
386
- inlinerGate: usingInliner
387
- ? {
388
- index: inlinerIndex,
389
- threshold: inlinerDispatchThreshold,
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 {
@@ -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 declare class Envelope<H extends EnvelopeHeader = EnvelopeHeader> {
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: ArrayBuffer;
9
- constructor(header: H, payload: ArrayBuffer);
12
+ readonly payload: B;
13
+ constructor(header: H, payload: B);
14
+ [Symbol.dispose](): void;
15
+ [PayloadTransportFinalizer](): (() => void) | undefined;
10
16
  }
11
17
  export {};
@@ -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
+ }