knitting 0.1.62 → 0.1.70

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 (84) hide show
  1. package/README.md +525 -335
  2. package/knitting.browser.js +1 -1
  3. package/map.md +0 -6
  4. package/package.json +3 -3
  5. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  6. package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
  7. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  8. package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
  9. package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
  11. package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
  12. package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
  13. package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
  14. package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
  15. package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
  16. package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
  17. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  18. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  19. package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
  20. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  21. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  22. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  23. package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
  24. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  25. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  26. package/scripts/build-native-addons.ts +5 -0
  27. package/shared-memory.d.ts +3 -0
  28. package/shared-memory.js +3 -0
  29. package/src/api.js +109 -42
  30. package/src/common/with-resolvers.js +2 -5
  31. package/src/common/worker-runtime.d.ts +7 -0
  32. package/src/common/worker-runtime.js +7 -0
  33. package/src/connections/buffer-reference.d.ts +10 -36
  34. package/src/connections/buffer-reference.js +15 -170
  35. package/src/connections/node-addons.d.ts +1 -1
  36. package/src/connections/shared-array-buffer-payload.d.ts +7 -0
  37. package/src/connections/shared-array-buffer-payload.js +27 -11
  38. package/src/ipc/transport/shared-memory.d.ts +9 -1
  39. package/src/ipc/transport/shared-memory.js +13 -1
  40. package/src/knitting_buffer_pointer.cc +57 -2
  41. package/src/knitting_doorbell.cc +220 -0
  42. package/src/memory/knitting-body.d.ts +44 -0
  43. package/src/memory/knitting-body.js +51 -0
  44. package/src/memory/knitting-buffer-http.d.ts +116 -0
  45. package/src/memory/knitting-buffer-http.js +255 -0
  46. package/src/memory/knitting-buffer.d.ts +250 -0
  47. package/src/memory/knitting-buffer.js +695 -0
  48. package/src/memory/lazy-region-registry.d.ts +83 -0
  49. package/src/memory/lazy-region-registry.js +355 -0
  50. package/src/memory/lock.d.ts +38 -15
  51. package/src/memory/lock.js +227 -109
  52. package/src/memory/payloadCodec.d.ts +18 -2
  53. package/src/memory/payloadCodec.js +309 -65
  54. package/src/memory/regionRegistry.d.ts +6 -0
  55. package/src/memory/regionRegistry.js +125 -240
  56. package/src/memory/shared-buffer-io.d.ts +7 -0
  57. package/src/memory/shared-buffer-io.js +34 -8
  58. package/src/runtime/deno-doorbell.d.ts +26 -0
  59. package/src/runtime/deno-doorbell.js +117 -0
  60. package/src/runtime/dispatcher.d.ts +8 -6
  61. package/src/runtime/dispatcher.js +80 -58
  62. package/src/runtime/host-arg-arena.d.ts +3 -0
  63. package/src/runtime/host-arg-arena.js +16 -0
  64. package/src/runtime/node-doorbell.d.ts +14 -0
  65. package/src/runtime/node-doorbell.js +84 -0
  66. package/src/runtime/pool.d.ts +27 -15
  67. package/src/runtime/pool.js +138 -116
  68. package/src/runtime/process-worker.d.ts +9 -0
  69. package/src/runtime/process-worker.js +22 -2
  70. package/src/runtime/tx-queue.d.ts +2 -5
  71. package/src/runtime/tx-queue.js +52 -48
  72. package/src/runtime/worker-common.d.ts +2 -1
  73. package/src/runtime/worker-common.js +19 -5
  74. package/src/types.d.ts +36 -70
  75. package/src/worker/loop.js +95 -62
  76. package/src/worker/rx-queue.d.ts +2 -3
  77. package/src/worker/rx-queue.js +34 -40
  78. package/src/worker/shared-return.d.ts +9 -0
  79. package/src/worker/shared-return.js +22 -0
  80. package/src/worker/task-loader.js +1 -2
  81. package/src/worker/timers.d.ts +2 -6
  82. package/src/worker/timers.js +14 -19
  83. package/unsafe.d.ts +2 -1
  84. package/unsafe.js +2 -1
@@ -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;
@@ -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);
@@ -1,6 +1,13 @@
1
1
  export type SignalArguments = ReturnType<typeof createSharedMemoryTransport>;
2
2
  import { type SharedBufferSource } from "../../common/shared-buffer-region.js";
3
3
  export declare const TRANSPORT_SIGNAL_BYTES: number;
4
+ /** States of the worker-stop word. */
5
+ export declare const WORKER_STOP: {
6
+ readonly running: 0;
7
+ readonly requested: 1;
8
+ /** The worker has left its dispatch loop. */
9
+ readonly acknowledged: 2;
10
+ };
4
11
  export type Sab = {
5
12
  size?: number;
6
13
  sharedSab?: SharedBufferSource;
@@ -18,6 +25,7 @@ export declare const createSharedMemoryTransport: ({ sabObject, isMain, startTim
18
25
  opView: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
19
26
  rxStatus: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
20
27
  txStatus: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
28
+ stopView: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
21
29
  };
22
- export type MainSignal = Pick<SignalArguments, "opView" | "startAt" | "rxStatus" | "txStatus">;
30
+ export type MainSignal = Pick<SignalArguments, "opView" | "startAt" | "rxStatus" | "txStatus" | "stopView">;
23
31
  export {};
@@ -7,8 +7,16 @@ const SIGNAL_OFFSETS = {
7
7
  op: 0,
8
8
  rxStatus: CACHE_LINE_BYTES,
9
9
  txStatus: CACHE_LINE_BYTES * 2,
10
+ stop: CACHE_LINE_BYTES * 3,
11
+ };
12
+ export const TRANSPORT_SIGNAL_BYTES = CACHE_LINE_BYTES * 4;
13
+ /** States of the worker-stop word. */
14
+ export const WORKER_STOP = {
15
+ running: 0,
16
+ requested: 1,
17
+ /** The worker has left its dispatch loop. */
18
+ acknowledged: 2,
10
19
  };
11
- export const TRANSPORT_SIGNAL_BYTES = CACHE_LINE_BYTES * 3;
12
20
  const a_store = Atomics.store;
13
21
  export const createSharedMemoryTransport = ({ sabObject, isMain, startTime }) => {
14
22
  const toGrow = sabObject?.size ?? page;
@@ -24,6 +32,9 @@ export const createSharedMemoryTransport = ({ sabObject, isMain, startTime }) =>
24
32
  a_store(opView, 0, 0);
25
33
  const rxStatus = new Int32Array(sab, baseByteOffset + SIGNAL_OFFSETS.rxStatus, 1);
26
34
  a_store(rxStatus, 0, 1);
35
+ const stopView = new Int32Array(sab, baseByteOffset + SIGNAL_OFFSETS.stop, 1);
36
+ if (isMain)
37
+ a_store(stopView, 0, WORKER_STOP.running);
27
38
  return {
28
39
  sab: signalRegion,
29
40
  op: opView,
@@ -31,5 +42,6 @@ export const createSharedMemoryTransport = ({ sabObject, isMain, startTime }) =>
31
42
  opView,
32
43
  rxStatus,
33
44
  txStatus: new Int32Array(sab, baseByteOffset + SIGNAL_OFFSETS.txStatus, 1),
45
+ stopView,
34
46
  };
35
47
  };
@@ -9,6 +9,7 @@
9
9
  #include <memory>
10
10
  #include <mutex>
11
11
  #include <unordered_map>
12
+ #include <vector>
12
13
 
13
14
  namespace knitting_buffer_pointer {
14
15
 
@@ -27,12 +28,51 @@ struct RetainedBackingStore {
27
28
  std::shared_ptr<v8::BackingStore> store;
28
29
  size_t byte_offset;
29
30
  size_t byte_length;
31
+ // The isolate that produced the move. This is only a registry hold: a
32
+ // consumer's adopted ArrayBuffer owns its own shared_ptr.
33
+ v8::Isolate* producer_isolate;
30
34
  };
31
35
 
32
36
  std::mutex backing_mutex;
33
37
  uint64_t next_backing_id = 1;
34
38
  std::unordered_map<uint64_t, RetainedBackingStore> retained_backings;
35
39
 
40
+ // Node tears a worker down without running its JavaScript finalizers. Both
41
+ // registries are process-global, so discard holds produced by that environment
42
+ // during teardown. Any consumer that adopted a backing store already has an
43
+ // independent shared_ptr and is unaffected.
44
+ void CleanupEnvironment(void* data) {
45
+ v8::Isolate* isolate = static_cast<v8::Isolate*>(data);
46
+ // Collect first, destroy on scope exit: releasing a hold must not run under
47
+ // the registry lock that found it.
48
+ std::vector<std::unique_ptr<RetainedReference>> released_refs;
49
+ std::vector<RetainedBackingStore> released_backings;
50
+
51
+ {
52
+ std::lock_guard<std::mutex> lock(retained_mutex);
53
+ for (auto it = retained_refs.begin(); it != retained_refs.end();) {
54
+ if (it->second->isolate != isolate) {
55
+ ++it;
56
+ continue;
57
+ }
58
+ released_refs.push_back(std::move(it->second));
59
+ it = retained_refs.erase(it);
60
+ }
61
+ }
62
+
63
+ {
64
+ std::lock_guard<std::mutex> lock(backing_mutex);
65
+ for (auto it = retained_backings.begin(); it != retained_backings.end();) {
66
+ if (it->second.producer_isolate != isolate) {
67
+ ++it;
68
+ continue;
69
+ }
70
+ released_backings.push_back(std::move(it->second));
71
+ it = retained_backings.erase(it);
72
+ }
73
+ }
74
+ }
75
+
36
76
  void ThrowType(v8::Isolate* isolate, const char* message) {
37
77
  isolate->ThrowException(v8::Exception::TypeError(
38
78
  v8::String::NewFromUtf8(isolate, message).ToLocalChecked()
@@ -318,7 +358,18 @@ void RetainBackingStore(const v8::FunctionCallbackInfo<v8::Value>& args) {
318
358
  uintptr_t address = reinterpret_cast<uintptr_t>(base + byte_offset);
319
359
 
320
360
  // Detach only after taking the shared_ptr; ordinary JS buffers use no key.
321
- DetachDefaultArrayBuffer(buffer);
361
+ // A failed detach must not be ignored: retaining the store while the source
362
+ // stays live hands the consumer an "owned" buffer that still aliases memory
363
+ // the producer can write. WASM memory and externally pinned buffers are the
364
+ // ones that land here, and the caller falls back to copying.
365
+ if (!DetachDefaultArrayBuffer(buffer)) {
366
+ ThrowType(
367
+ isolate,
368
+ "retainBackingStore requires a detachable ArrayBuffer; this buffer "
369
+ "cannot be moved without leaving the source aliasing it"
370
+ );
371
+ return;
372
+ }
322
373
 
323
374
  uint64_t token = 0;
324
375
  {
@@ -327,7 +378,9 @@ void RetainBackingStore(const v8::FunctionCallbackInfo<v8::Value>& args) {
327
378
  if (token == 0) token = next_backing_id++;
328
379
  retained_backings.emplace(
329
380
  token,
330
- RetainedBackingStore{ std::move(store), byte_offset, byte_length }
381
+ RetainedBackingStore{
382
+ std::move(store), byte_offset, byte_length, isolate
383
+ }
331
384
  );
332
385
  }
333
386
 
@@ -410,6 +463,8 @@ void Initialize(
410
463
  v8::Local<v8::Value>,
411
464
  v8::Local<v8::Context>
412
465
  ) {
466
+ v8::Isolate* isolate = exports->GetIsolate();
467
+ node::AddEnvironmentCleanupHook(isolate, CleanupEnvironment, isolate);
413
468
  NODE_SET_METHOD(exports, "getPointer", GetPointer);
414
469
  NODE_SET_METHOD(exports, "retainPointer", RetainPointer);
415
470
  NODE_SET_METHOD(exports, "releasePointer", ReleasePointer);