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.
Files changed (99) hide show
  1. package/README.md +623 -342
  2. package/knitting.browser.d.ts +3 -1
  3. package/knitting.browser.js +1 -1
  4. package/knitting.d.ts +3 -1
  5. package/knitting.js +2 -1
  6. package/map.md +0 -6
  7. package/package.json +10 -5
  8. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  9. package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
  10. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  11. package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
  12. package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
  14. package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
  15. package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
  16. package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
  18. package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
  19. package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
  20. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  21. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  22. package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
  23. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  24. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  25. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  26. package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
  27. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  28. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  29. package/scripts/build-native-addons.ts +5 -0
  30. package/shared-memory.d.ts +3 -0
  31. package/shared-memory.js +3 -0
  32. package/src/api.js +158 -71
  33. package/src/common/with-resolvers.js +2 -5
  34. package/src/common/worker-runtime.d.ts +7 -0
  35. package/src/common/worker-runtime.js +7 -0
  36. package/src/connections/buffer-reference.d.ts +10 -36
  37. package/src/connections/buffer-reference.js +15 -170
  38. package/src/connections/node-addons.d.ts +1 -1
  39. package/src/connections/node-addons.js +11 -1
  40. package/src/connections/shared-array-buffer-payload.d.ts +7 -0
  41. package/src/connections/shared-array-buffer-payload.js +27 -11
  42. package/src/debug/gate.js +1 -1
  43. package/src/debug/handle.d.ts +6 -1
  44. package/src/debug/handle.js +14 -6
  45. package/src/error.d.ts +9 -0
  46. package/src/error.js +16 -2
  47. package/src/knitting_buffer_pointer.cc +57 -2
  48. package/src/knitting_doorbell.cc +220 -0
  49. package/src/memory/knitting-body.d.ts +44 -0
  50. package/src/memory/knitting-body.js +51 -0
  51. package/src/memory/knitting-buffer-http.d.ts +116 -0
  52. package/src/memory/knitting-buffer-http.js +255 -0
  53. package/src/memory/knitting-buffer.d.ts +250 -0
  54. package/src/memory/knitting-buffer.js +695 -0
  55. package/src/memory/lazy-region-registry.d.ts +83 -0
  56. package/src/memory/lazy-region-registry.js +355 -0
  57. package/src/memory/lock.d.ts +80 -15
  58. package/src/memory/lock.js +473 -139
  59. package/src/memory/payloadCodec.d.ts +18 -2
  60. package/src/memory/payloadCodec.js +340 -76
  61. package/src/memory/regionRegistry.d.ts +6 -0
  62. package/src/memory/regionRegistry.js +125 -240
  63. package/src/memory/shared-buffer-io.d.ts +7 -0
  64. package/src/memory/shared-buffer-io.js +34 -8
  65. package/src/permission/protocol.d.ts +1 -0
  66. package/src/permission/protocol.js +8 -3
  67. package/src/runtime/deno-doorbell.d.ts +26 -0
  68. package/src/runtime/deno-doorbell.js +117 -0
  69. package/src/runtime/dispatcher.d.ts +13 -6
  70. package/src/runtime/dispatcher.js +101 -63
  71. package/src/runtime/host-arg-arena.d.ts +3 -0
  72. package/src/runtime/host-arg-arena.js +16 -0
  73. package/src/runtime/inline-executor.js +2 -1
  74. package/src/runtime/node-doorbell.d.ts +14 -0
  75. package/src/runtime/node-doorbell.js +84 -0
  76. package/src/runtime/pool.d.ts +30 -15
  77. package/src/runtime/pool.js +199 -151
  78. package/src/runtime/process-worker.d.ts +9 -0
  79. package/src/runtime/process-worker.js +32 -3
  80. package/src/runtime/tx-queue.d.ts +4 -6
  81. package/src/runtime/tx-queue.js +63 -48
  82. package/src/runtime/worker-common.d.ts +7 -0
  83. package/src/runtime/worker-common.js +28 -2
  84. package/src/types.d.ts +66 -78
  85. package/src/worker/loop.js +95 -60
  86. package/src/worker/rx-queue.d.ts +2 -3
  87. package/src/worker/rx-queue.js +34 -40
  88. package/src/worker/safety/index.d.ts +1 -1
  89. package/src/worker/safety/index.js +1 -1
  90. package/src/worker/safety/process.d.ts +2 -0
  91. package/src/worker/safety/process.js +8 -1
  92. package/src/worker/safety/startup.js +11 -6
  93. package/src/worker/shared-return.d.ts +9 -0
  94. package/src/worker/shared-return.js +22 -0
  95. package/src/worker/task-loader.js +1 -2
  96. package/src/worker/timers.d.ts +2 -6
  97. package/src/worker/timers.js +39 -22
  98. package/unsafe.d.ts +2 -1
  99. 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
- export declare const withBufferReferenceReturnReleaser: <T>(releaser: BufferReferenceReturnReleaser | BufferReferenceReturnHooks | undefined, run: () => T) => T;
55
- export declare const createBufferReferenceReturnReleaseMessage: (token: bigint) => BufferReferenceReturnReleaseMessage;
56
- export declare const readBufferReferenceReturnReleaseMessage: (value: unknown) => bigint | undefined;
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(releaser?: BufferReferenceReturnReleaser | BufferReferenceReturnHooks | undefined): this;
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(releaser = currentReturnHooks) {
345
- if (this.#owned !== undefined || this.#borrowedReturnReleaser !== undefined ||
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
- errors.push(`${candidate}: ${String(error)}`);
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
- // Thread-worker SharedArrayBuffer transport by process-local pointer.
5
- // SABs are shared, not moved or detached; process workers reject them.
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
- const cachedSharedBuffersByToken = new Map();
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 cached = cachedSharedBuffersByToken.get(token);
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
- cachedSharedBuffersByToken.set(token, region.buffer);
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 = cachedSharedBuffersByToken.get(token);
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.ts")`, so when debug is off nothing else under
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:
@@ -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;
@@ -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 relative to when this
8
- * worker's debug initialised. The clock is worker-local on purpose: a main-thread
9
- * timestamp can't be compared against `performance.now()` here because the time
10
- * origins differ across the thread/process boundary (it would read negative).
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 = performance.now();
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 = reason;
47
- onPromise(task, true, reason);
60
+ task.value = error;
61
+ onPromise(task, true, error);
48
62
  });
49
63
  return false;
50
64
  };