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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knitting",
3
- "version": "0.1.62",
3
+ "version": "0.1.70",
4
4
  "description": "Shared-memory IPC runtime for Node.js, Deno, and Bun.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -86,8 +86,9 @@
86
86
  "test:deno": "deno test -A --ignore=test/runtime.node.test.ts --ignore=test/runtime.process.test.ts",
87
87
  "test:node": "node --no-warnings --experimental-transform-types --test \"./test/*.test.ts\"",
88
88
  "test:node:26": "node --no-warnings --experimental-ffi test/node26-ffi.integration.mjs",
89
- "test:bun": "bun test ./test/*.test.ts --path-ignore-patterns=**/tx-queue.test.ts",
89
+ "test:bun": "bun test",
90
90
  "test:browser": "bun run scripts/test-browser.ts",
91
+ "check:windows-park": "bun run scripts/check-windows-park.ts",
91
92
  "test:runtimes": "npm run test:node && npm run test:bun",
92
93
  "test:all": "npm run test:deno && npm run test:runtimes && npm run test:browser",
93
94
  "coverage:node": "node --no-warnings --experimental-transform-types --experimental-test-coverage --test \"./test/*.test.ts\"",
@@ -107,7 +108,6 @@
107
108
  "@types/bun": "latest",
108
109
  "@types/deno": "latest",
109
110
  "@types/node": "latest",
110
- "dts-bundle-generator": "^9.5.1",
111
111
  "mitata": "^1.0.34",
112
112
  "typescript": "^5.2.0"
113
113
  }
@@ -237,6 +237,11 @@ const addons = [
237
237
  source: "src/knitting_buffer_pointer.cc",
238
238
  output: "build/Release/knitting_buffer_pointer.node",
239
239
  },
240
+ {
241
+ name: "knitting_doorbell",
242
+ source: "src/knitting_doorbell.cc",
243
+ output: "build/Release/knitting_doorbell.node",
244
+ },
240
245
  ];
241
246
  const ffiLibraries = isWindows
242
247
  ? [
@@ -1 +1,4 @@
1
1
  export { getDefaultProcessSharedBufferPrimitives, parseProcessSharedBufferMetadata, PROCESS_SHARED_BUFFER_BRAND, ProcessSharedBuffer, setDefaultProcessSharedBufferPrimitives, type ProcessSharedBufferCreator, type ProcessSharedBufferMapper, type ProcessSharedBufferMetadata, type ProcessSharedBufferPrimitives, type ProcessSharedBufferRange, type ProcessSharedBufferView, type ProcessSharedBufferViewConstructor, } from "./src/connections/process-shared-buffer.js";
2
+ export { attachKnittingAllocator, createKnittingAllocator, DEFAULT_ARENA_BYTE_LENGTH, detectRegion, type KnittingAllocator, type KnittingAllocatorOptions, type KnittingBufferDescriptor, KnittingSharedBuffer, } from "./src/memory/knitting-buffer.js";
3
+ export { createBodyReader, type KnittingBody, type KnittingBodyWire, type KnittingTransport, } from "./src/memory/knitting-body.js";
4
+ export { HTTP_BODY_STREAM_THRESHOLD_BYTES, HTTP_BODY_REFERENCE_THRESHOLD_BYTES, type ReadBodyIntoBytesOptions, type ReadBodyOptions, type ReadBodyOrReferOptions, type ReadBodyPayload, readBodyIntoBytes, readBodyIntoRegion, readBodyOrRefer, type RegionAllocator, } from "./src/memory/knitting-buffer-http.js";
package/shared-memory.js CHANGED
@@ -1 +1,4 @@
1
1
  export { getDefaultProcessSharedBufferPrimitives, parseProcessSharedBufferMetadata, PROCESS_SHARED_BUFFER_BRAND, ProcessSharedBuffer, setDefaultProcessSharedBufferPrimitives, } from "./src/connections/process-shared-buffer.js";
2
+ export { attachKnittingAllocator, createKnittingAllocator, DEFAULT_ARENA_BYTE_LENGTH, detectRegion, KnittingSharedBuffer, } from "./src/memory/knitting-buffer.js";
3
+ export { createBodyReader, } from "./src/memory/knitting-body.js";
4
+ export { HTTP_BODY_STREAM_THRESHOLD_BYTES, HTTP_BODY_REFERENCE_THRESHOLD_BYTES, readBodyIntoBytes, readBodyIntoRegion, readBodyOrRefer, } from "./src/memory/knitting-buffer-http.js";
package/src/api.js CHANGED
@@ -5,6 +5,7 @@ import { toModuleUrl } from "./common/module-url.js";
5
5
  import { endpointSymbol } from "./common/task-symbol.js";
6
6
  import { createStealPoolBuffers, MAX_STEAL_CONSUMERS, spawnWorkerContext, } from "./runtime/pool.js";
7
7
  import { ChannelHandler, hostDispatcherLoop } from "./runtime/dispatcher.js";
8
+ import { createDenoCompletionDoorbell } from "./runtime/deno-doorbell.js";
8
9
  import { createSharedArrayBuffer, RUNTIME } from "./common/runtime.js";
9
10
  import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
10
11
  import { classifyProcessPermissionCompatibility, enforceProcessPermissionCompatibility, resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
@@ -14,6 +15,7 @@ import { TRANSPORT_SIGNAL_BYTES } from "./ipc/transport/shared-memory.js";
14
15
  import { inspectCompiledWorkerArtifact } from "./runtime/compiled-artifact.js";
15
16
  import { managerMethod } from "./runtime/balancer.js";
16
17
  import { createInlineExecutor } from "./runtime/inline-executor.js";
18
+ import { createHostArgAllocator } from "./runtime/host-arg-arena.js";
17
19
  const hasDebugNamespace = (namespaces, namespace) => namespaces.has("*") || namespaces.has(namespace);
18
20
  const createHostDebug = (namespaces) => {
19
21
  const enabled = (namespace) => hasDebugNamespace(namespaces, namespace);
@@ -242,7 +244,8 @@ const toPoolTaskEntries = (input, callerHref) => Object.entries(input).map(([nam
242
244
  * argument; use an object or tuple for multiple values.
243
245
  */
244
246
  export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe, abortSignalCapacity, source, worker, workerExecArgv, permission, host, }) => (tasks) => {
245
- const bufferReferenceReturn = unsafe?.BufferReferenceReturn;
247
+ const sharedBytesEnabled = unsafe?.SharedBytes === true;
248
+ const sharedArgsEnabled = unsafe?.SharedArgs === true;
246
249
  const debugRequested = DEBUG_ENABLED ||
247
250
  (debug !== undefined && debug !== false);
248
251
  let debugNamespaces;
@@ -371,14 +374,21 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
371
374
  : stealEnvRaw === "0" || stealEnvRaw === "false"
372
375
  ? false
373
376
  : undefined;
374
- // Shared-submit stealing is the default for compatible multi-worker pools.
375
- // An explicit option wins over the environment; compiled and inline
376
- // transports retain their existing topology.
377
+ // Shared-submit stealing is the default for compatible multi-worker pools;
378
+ // explicit options take precedence over the environment.
377
379
  const dispatcherExplicitlySelected = host?.dispatcher !== undefined ||
378
380
  dispatcherEnv === "serial-channel" || dispatcherEnv === "per-thread";
379
381
  const stealDefaultCompatible = balancer === undefined &&
380
382
  !dispatcherExplicitlySelected && (threads ?? 1) <= MAX_STEAL_CONSUMERS;
381
383
  const stealRequested = host?.steal ?? stealEnv ?? stealDefaultCompatible;
384
+ const stealClaimEnvRaw = nodeProcess?.env?.KNITTING_STEAL_CLAIM?.trim()
385
+ .toLowerCase();
386
+ const stealClaimEnv = stealClaimEnvRaw === "cas-mask" ||
387
+ stealClaimEnvRaw === "dekker"
388
+ ? stealClaimEnvRaw
389
+ : undefined;
390
+ // Select the stealing claim discipline, preferring the explicit option.
391
+ const stealClaim = host?.stealClaim ?? stealClaimEnv ?? "dekker";
382
392
  const usingCompiledWorker = resolvedWorker?.runtime === "compiled";
383
393
  if (resolvedWorker?.compiled !== undefined && !usingCompiledWorker) {
384
394
  throw new Error("worker.compiled requires worker.runtime to be compiled");
@@ -463,9 +473,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
463
473
  ? dispatcherEnv
464
474
  : undefined);
465
475
  const autoDispatcher = (() => {
466
- // Experimental default for HTTP-style bursts: Bun still favors direct
467
- // per-thread channels, while Node/Deno multi-worker pools favor one shared
468
- // macro channel over the per-lane dispatcher checks.
476
+ // Bun and single-worker pools use per-thread dispatch; other pools share a
477
+ // channel across their private lanes.
469
478
  if (RUNTIME === "bun")
470
479
  return "per-thread";
471
480
  if ((threads ?? 1) <= 1)
@@ -473,10 +482,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
473
482
  return "serial-channel";
474
483
  })();
475
484
  const dispatcher = explicitDispatcher ?? autoDispatcher;
476
- // Work stealing: one shared submit region, private return lanes, one
477
- // pool-global pending registry. It is the compatible multi-worker default;
478
- // an explicit balancer/dispatcher keeps the private-lane topology unless
479
- // stealing itself was explicitly requested.
485
+ // Stealing uses one submit region and private return lanes. Explicit balancer
486
+ // or dispatcher settings retain private lanes unless stealing is requested.
480
487
  const useSteal = stealRequested &&
481
488
  !usingCompiledWorker &&
482
489
  (threads ?? 1) > 1 &&
@@ -485,7 +492,9 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
485
492
  ? createStealPoolBuffers({
486
493
  threads: threads ?? 1,
487
494
  payload,
495
+ sharedArgs: sharedArgsEnabled,
488
496
  regionLanes: host?.stealRegionLanes,
497
+ stealClaim,
489
498
  abortSignalCapacity,
490
499
  usesAbortSignal,
491
500
  processWorker: resolvedWorker?.runtime === "process"
@@ -498,13 +507,16 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
498
507
  : undefined;
499
508
  const serialChannel = !usingCompiledWorker && !useSteal &&
500
509
  dispatcher === "serial-channel";
501
- // One serial-channel hop drives every lane check in turn, and it depends on
502
- // the channel's delivery to do that: swapping in the cheaper `setImmediate`
503
- // pump cost 25% throughput and tripled p99 here, where every other topology
504
- // gained. See ChannelHandler's note.
510
+ // The serial topology relies on channel delivery to run each lane check.
505
511
  const serialDispatcherChannel = serialChannel
506
512
  ? new ChannelHandler("channel")
507
513
  : undefined;
514
+ // Deno uses a threadSafe FFI callback because waitAsync does not wake an idle
515
+ // event loop; process workers cannot use the process-local pointer.
516
+ const denoCompletionDoorbell = !usingCompiledWorker &&
517
+ resolvedWorker?.runtime !== "process" && host?.doorbell !== false
518
+ ? createDenoCompletionDoorbell()
519
+ : undefined;
508
520
  let workers = Array.from({
509
521
  length: threads ?? 1,
510
522
  }).map((_, thread) => spawnWorkerContext({
@@ -516,16 +528,20 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
516
528
  debug,
517
529
  hostDebug: hostDebug?.log,
518
530
  totalNumberOfThread,
531
+ // Worker count without the inline lane. The inliner runs on the host
532
+ // thread and never spins, so it must not count towards the spin policy.
533
+ workerCount: threads ?? 1,
519
534
  source,
520
535
  workerOptions: resolvedWorker,
521
536
  workerExecArgv: execArgv,
522
537
  host,
523
538
  payload,
524
- bufferReferenceReturn,
539
+ sharedBytesEnabled,
525
540
  abortSignalCapacity,
526
541
  usesAbortSignal,
527
542
  permission: permissionProtocol,
528
543
  sharedChannelHandler: serialDispatcherChannel,
544
+ denoCompletionDoorbell,
529
545
  stealPool: stealBuffers === undefined ? undefined : {
530
546
  submitBuffers: stealBuffers.submitBuffers,
531
547
  returnBuffers: stealBuffers.returnBuffers[thread],
@@ -535,6 +551,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
535
551
  consumers: threads ?? 1,
536
552
  consumerId: thread,
537
553
  regionLanes: stealBuffers.regionLanes,
554
+ stealClaim: stealBuffers.stealClaim,
538
555
  abortSignalSAB: stealBuffers.abortSignalSAB,
539
556
  abortSignalMax: stealBuffers.abortSignalMax,
540
557
  processMemory: stealBuffers.processMemory,
@@ -544,10 +561,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
544
561
  if (useSteal) {
545
562
  const channel = stealChannel;
546
563
  const queue = stealBuffers.sharedQueue;
547
- // Wake exactly one worker per drain, round-robin. Stealing decouples the
548
- // wake target from the assignment: if the woken worker is busy, any other
549
- // idle endpoint claims the region instead. So this costs one notify per
550
- // publish regardless of pool size, rather than one per lane.
564
+ // Round-robin wake keeps stealing at one notify per publish; any idle worker
565
+ // can claim the region.
551
566
  let wakeCursor = 0;
552
567
  const wakeOne = () => {
553
568
  const lanes = workers.length;
@@ -560,7 +575,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
560
575
  // The shared dispatcher drives one queue, so it needs signal words of its
561
576
  // own rather than any single lane's; waking is delegated to `wakeOne`.
562
577
  const signalWords = new Int32Array(createSharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT));
563
- const { check } = hostDispatcherLoop({
578
+ const { check, wakeCompletion } = hostDispatcherLoop({
564
579
  signalBox: {
565
580
  opView: signalWords.subarray(0, 1),
566
581
  txStatus: signalWords.subarray(1, 2),
@@ -571,18 +586,30 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
571
586
  dispatcherOptions: host,
572
587
  notifySignal: wakeOne,
573
588
  crossProcess: resolvedWorker?.runtime === "process",
589
+ nativeCompletionDoorbell: denoCompletionDoorbell !== undefined ||
590
+ workers.some((context) => context
591
+ .nodeCompletionDoorbell === true),
592
+ processCompletionDoorbell: workers.some((context) => context.processCompletionDoorbell === true),
574
593
  });
575
594
  channel.open(check);
595
+ if (denoCompletionDoorbell !== undefined) {
596
+ workers.forEach((_, thread) => {
597
+ denoCompletionDoorbell.listen(thread, wakeCompletion);
598
+ });
599
+ }
600
+ workers.forEach((context) => {
601
+ context.bindCompletionWake?.(wakeCompletion);
602
+ });
576
603
  workers.forEach((context) => {
577
604
  context.bindSend(() => {
578
605
  if (check.isRunning !== true) {
579
606
  check.isRunning = true;
580
- Promise.resolve().then(check);
607
+ queueMicrotask(check);
581
608
  }
582
609
  wakeOne();
583
610
  });
584
611
  });
585
- hostDebug?.log(`dispatcher=steal lanes=${workers.length} g=${stealBuffers.regionLanes}`);
612
+ hostDebug?.log(`dispatcher=steal lanes=${workers.length} g=${stealBuffers.regionLanes} claim=${stealClaim}`);
586
613
  }
587
614
  const sharedDispatcherChannel = serialDispatcherChannel;
588
615
  if (serialChannel) {
@@ -592,11 +619,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
592
619
  let serialScheduled = false;
593
620
  let serialInFlight = false;
594
621
  let serialRerun = false;
595
- // Busy-lane set: a tick walks only the lanes that have work, not all of
596
- // them. `active` is a dense list of lane indices; `isActive` keeps the
597
- // membership test O(1). A lane joins on send() and leaves once its queue
598
- // reports idle, so an N-worker pool with one busy lane costs one check per
599
- // tick instead of N.
622
+ // Visit only active private lanes; inactive lanes stay out of the tick.
600
623
  const active = [];
601
624
  const isActive = new Uint8Array(checks.length);
602
625
  const markActive = (lane) => {
@@ -639,7 +662,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
639
662
  if (serialScheduled)
640
663
  return;
641
664
  serialScheduled = true;
642
- Promise.resolve().then(runSerialChecks);
665
+ queueMicrotask(runSerialChecks);
643
666
  };
644
667
  channel.open(runSerialChecks);
645
668
  workers.forEach((context, lane) => {
@@ -687,10 +710,29 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
687
710
  if (closePromise)
688
711
  return closePromise;
689
712
  closing = true;
690
- closePromise = Promise.allSettled(workers.map((context) => context.kills()))
713
+ let canCloseNativeDoorbells = denoCompletionDoorbell === undefined;
714
+ closePromise = Promise.allSettled(workers.map((context) => context.requestStop?.()))
715
+ .then((stops) => {
716
+ if (denoCompletionDoorbell !== undefined) {
717
+ canCloseNativeDoorbells = stops.every((result, index) => {
718
+ if (workers[index].requestStop === undefined)
719
+ return true;
720
+ return result.status === "fulfilled" && result.value === true;
721
+ });
722
+ }
723
+ return Promise.allSettled(workers.map((context) => context.kills()));
724
+ })
691
725
  .then(() => {
692
726
  sharedDispatcherChannel?.close();
693
727
  stealChannel?.close();
728
+ })
729
+ .finally(() => {
730
+ if (canCloseNativeDoorbells) {
731
+ denoCompletionDoorbell?.close();
732
+ return;
733
+ }
734
+ // Keep the callback alive if a worker failed to acknowledge shutdown.
735
+ denoCompletionDoorbell?.unref();
694
736
  });
695
737
  return closePromise;
696
738
  };
@@ -759,23 +801,41 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
759
801
  for (const { name } of indexedFunctions) {
760
802
  callHandlers.set(name, []);
761
803
  }
762
- for (const worker of workers) {
804
+ if (useSteal) {
805
+ // Every worker context points at the same pool-global submit queue in the
806
+ // stealing topology. There is therefore no worker to assign here: the
807
+ // claimant is selected later by decodeSteal(). Keep one call closure per
808
+ // task and avoid paying N equivalent wrapper allocations at pool setup.
809
+ const sharedWorker = workers[0];
763
810
  for (const { name, index, timeout, abortSignal } of indexedFunctions) {
764
- callHandlers.get(name).push(wrapGuardedInvoke({
765
- taskName: name,
766
- invoke: worker.call({
767
- fnNumber: index,
768
- timeout,
769
- abortSignal,
811
+ callHandlers.set(name, [
812
+ wrapGuardedInvoke({
813
+ taskName: name,
814
+ invoke: sharedWorker.call({
815
+ fnNumber: index,
816
+ timeout,
817
+ abortSignal,
818
+ }),
770
819
  }),
771
- }));
820
+ ]);
821
+ }
822
+ }
823
+ else {
824
+ for (const worker of workers) {
825
+ for (const { name, index, timeout, abortSignal } of indexedFunctions) {
826
+ callHandlers.get(name).push(wrapGuardedInvoke({
827
+ taskName: name,
828
+ invoke: worker.call({
829
+ fnNumber: index,
830
+ timeout,
831
+ abortSignal,
832
+ }),
833
+ }));
834
+ }
772
835
  }
773
836
  }
774
837
  const useDirectHandler = (threads ?? 1) === 1 && !usingInliner;
775
- // Imported tasks must never execute on the host inliner lane: their module
776
- // import is meant to happen inside the worker so worker permission policies
777
- // apply. When the inliner is active we strip the inline lane from their
778
- // handler set so they only ever reach real worker lanes.
838
+ // Imported tasks must run on a worker so worker-side permission policies apply.
779
839
  const buildImportedInvoker = (handlers) => {
780
840
  const workerHandlers = [];
781
841
  const workerContexts = [];
@@ -800,6 +860,11 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
800
860
  });
801
861
  };
802
862
  const buildInvoker = (handlers, imported) => {
863
+ // Stealing owns one shared submit queue. The worker that runs a call is
864
+ // deliberately chosen by the worker-side claim protocol, not by the host
865
+ // balancer, so round-robin/first-idle selection would only add overhead.
866
+ if (useSteal)
867
+ return handlers[0];
803
868
  if (imported && usingInliner) {
804
869
  return buildImportedInvoker(handlers);
805
870
  }
@@ -832,6 +897,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
832
897
  shutdown: shutdownWithDelay,
833
898
  [Symbol.dispose]: disposePool,
834
899
  call: Object.fromEntries(callEntries),
900
+ // Only the shared submit queue has a single arena to build arguments in.
901
+ sharedArgBytes: createHostArgAllocator(sharedArgsEnabled ? stealBuffers?.submitBuffers.payload : undefined),
835
902
  };
836
903
  };
837
904
  const SINGLE_TASK_KEY = "__task__";
@@ -7,11 +7,8 @@ export const withResolvers = () => {
7
7
  const native = Promise.withResolvers;
8
8
  if (typeof native === "function") {
9
9
  const deferred = native.call(Promise);
10
- return {
11
- promise: attachReject(deferred.promise, deferred.reject),
12
- resolve: deferred.resolve,
13
- reject: deferred.reject,
14
- };
10
+ attachReject(deferred.promise, deferred.reject);
11
+ return deferred;
15
12
  }
16
13
  let resolve;
17
14
  let reject;
@@ -29,6 +29,13 @@ 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
31
  export declare const RUNTIME_POOL_DEPTH_ENV = "KNITTING_POOL_DEPTH";
32
+ /**
33
+ * A coalesced completion wake sent over a process worker's existing IPC
34
+ * channel. The result itself remains in shared memory; this is only the
35
+ * doorbell byte.
36
+ */
37
+ export declare const PROCESS_COMPLETION_DOORBELL = "__knittingCompletionDoorbell";
38
+ export declare const isProcessCompletionDoorbell: (value: unknown) => boolean;
32
39
  export declare const RUNTIME_IS_PROCESS_WORKER: boolean;
33
40
  export declare const RUNTIME_POOL_DEPTH: number;
34
41
  export declare const RUNTIME_WORKER: new (specifier: string | URL, options?: Record<string, unknown>) => RuntimeWorkerLike;
@@ -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
  }