knitting 0.1.63 → 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 (80) 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 +105 -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/knitting_buffer_pointer.cc +57 -2
  39. package/src/knitting_doorbell.cc +220 -0
  40. package/src/memory/knitting-body.d.ts +44 -0
  41. package/src/memory/knitting-body.js +51 -0
  42. package/src/memory/knitting-buffer-http.d.ts +116 -0
  43. package/src/memory/knitting-buffer-http.js +255 -0
  44. package/src/memory/knitting-buffer.d.ts +250 -0
  45. package/src/memory/knitting-buffer.js +695 -0
  46. package/src/memory/lazy-region-registry.d.ts +83 -0
  47. package/src/memory/lazy-region-registry.js +355 -0
  48. package/src/memory/lock.d.ts +38 -15
  49. package/src/memory/lock.js +205 -79
  50. package/src/memory/payloadCodec.d.ts +18 -2
  51. package/src/memory/payloadCodec.js +309 -65
  52. package/src/memory/regionRegistry.d.ts +6 -0
  53. package/src/memory/regionRegistry.js +125 -240
  54. package/src/memory/shared-buffer-io.d.ts +7 -0
  55. package/src/memory/shared-buffer-io.js +34 -8
  56. package/src/runtime/deno-doorbell.d.ts +26 -0
  57. package/src/runtime/deno-doorbell.js +117 -0
  58. package/src/runtime/dispatcher.d.ts +8 -6
  59. package/src/runtime/dispatcher.js +80 -58
  60. package/src/runtime/host-arg-arena.d.ts +3 -0
  61. package/src/runtime/host-arg-arena.js +16 -0
  62. package/src/runtime/node-doorbell.d.ts +14 -0
  63. package/src/runtime/node-doorbell.js +84 -0
  64. package/src/runtime/pool.d.ts +21 -15
  65. package/src/runtime/pool.js +104 -116
  66. package/src/runtime/process-worker.d.ts +9 -0
  67. package/src/runtime/process-worker.js +22 -2
  68. package/src/runtime/tx-queue.d.ts +2 -5
  69. package/src/runtime/tx-queue.js +52 -48
  70. package/src/types.d.ts +35 -71
  71. package/src/worker/loop.js +79 -57
  72. package/src/worker/rx-queue.d.ts +2 -3
  73. package/src/worker/rx-queue.js +34 -40
  74. package/src/worker/shared-return.d.ts +9 -0
  75. package/src/worker/shared-return.js +22 -0
  76. package/src/worker/task-loader.js +1 -2
  77. package/src/worker/timers.d.ts +2 -6
  78. package/src/worker/timers.js +14 -19
  79. package/unsafe.d.ts +2 -1
  80. package/unsafe.js +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knitting",
3
- "version": "0.1.63",
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({
@@ -524,11 +536,12 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
524
536
  workerExecArgv: execArgv,
525
537
  host,
526
538
  payload,
527
- bufferReferenceReturn,
539
+ sharedBytesEnabled,
528
540
  abortSignalCapacity,
529
541
  usesAbortSignal,
530
542
  permission: permissionProtocol,
531
543
  sharedChannelHandler: serialDispatcherChannel,
544
+ denoCompletionDoorbell,
532
545
  stealPool: stealBuffers === undefined ? undefined : {
533
546
  submitBuffers: stealBuffers.submitBuffers,
534
547
  returnBuffers: stealBuffers.returnBuffers[thread],
@@ -538,6 +551,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
538
551
  consumers: threads ?? 1,
539
552
  consumerId: thread,
540
553
  regionLanes: stealBuffers.regionLanes,
554
+ stealClaim: stealBuffers.stealClaim,
541
555
  abortSignalSAB: stealBuffers.abortSignalSAB,
542
556
  abortSignalMax: stealBuffers.abortSignalMax,
543
557
  processMemory: stealBuffers.processMemory,
@@ -547,10 +561,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
547
561
  if (useSteal) {
548
562
  const channel = stealChannel;
549
563
  const queue = stealBuffers.sharedQueue;
550
- // Wake exactly one worker per drain, round-robin. Stealing decouples the
551
- // wake target from the assignment: if the woken worker is busy, any other
552
- // idle endpoint claims the region instead. So this costs one notify per
553
- // 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.
554
566
  let wakeCursor = 0;
555
567
  const wakeOne = () => {
556
568
  const lanes = workers.length;
@@ -563,7 +575,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
563
575
  // The shared dispatcher drives one queue, so it needs signal words of its
564
576
  // own rather than any single lane's; waking is delegated to `wakeOne`.
565
577
  const signalWords = new Int32Array(createSharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT));
566
- const { check } = hostDispatcherLoop({
578
+ const { check, wakeCompletion } = hostDispatcherLoop({
567
579
  signalBox: {
568
580
  opView: signalWords.subarray(0, 1),
569
581
  txStatus: signalWords.subarray(1, 2),
@@ -574,18 +586,30 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
574
586
  dispatcherOptions: host,
575
587
  notifySignal: wakeOne,
576
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),
577
593
  });
578
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
+ });
579
603
  workers.forEach((context) => {
580
604
  context.bindSend(() => {
581
605
  if (check.isRunning !== true) {
582
606
  check.isRunning = true;
583
- Promise.resolve().then(check);
607
+ queueMicrotask(check);
584
608
  }
585
609
  wakeOne();
586
610
  });
587
611
  });
588
- hostDebug?.log(`dispatcher=steal lanes=${workers.length} g=${stealBuffers.regionLanes}`);
612
+ hostDebug?.log(`dispatcher=steal lanes=${workers.length} g=${stealBuffers.regionLanes} claim=${stealClaim}`);
589
613
  }
590
614
  const sharedDispatcherChannel = serialDispatcherChannel;
591
615
  if (serialChannel) {
@@ -595,11 +619,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
595
619
  let serialScheduled = false;
596
620
  let serialInFlight = false;
597
621
  let serialRerun = false;
598
- // Busy-lane set: a tick walks only the lanes that have work, not all of
599
- // them. `active` is a dense list of lane indices; `isActive` keeps the
600
- // membership test O(1). A lane joins on send() and leaves once its queue
601
- // reports idle, so an N-worker pool with one busy lane costs one check per
602
- // tick instead of N.
622
+ // Visit only active private lanes; inactive lanes stay out of the tick.
603
623
  const active = [];
604
624
  const isActive = new Uint8Array(checks.length);
605
625
  const markActive = (lane) => {
@@ -642,7 +662,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
642
662
  if (serialScheduled)
643
663
  return;
644
664
  serialScheduled = true;
645
- Promise.resolve().then(runSerialChecks);
665
+ queueMicrotask(runSerialChecks);
646
666
  };
647
667
  channel.open(runSerialChecks);
648
668
  workers.forEach((context, lane) => {
@@ -690,11 +710,29 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
690
710
  if (closePromise)
691
711
  return closePromise;
692
712
  closing = true;
713
+ let canCloseNativeDoorbells = denoCompletionDoorbell === undefined;
693
714
  closePromise = Promise.allSettled(workers.map((context) => context.requestStop?.()))
694
- .then(() => Promise.allSettled(workers.map((context) => context.kills())))
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
+ })
695
725
  .then(() => {
696
726
  sharedDispatcherChannel?.close();
697
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();
698
736
  });
699
737
  return closePromise;
700
738
  };
@@ -763,23 +801,41 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
763
801
  for (const { name } of indexedFunctions) {
764
802
  callHandlers.set(name, []);
765
803
  }
766
- 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];
767
810
  for (const { name, index, timeout, abortSignal } of indexedFunctions) {
768
- callHandlers.get(name).push(wrapGuardedInvoke({
769
- taskName: name,
770
- invoke: worker.call({
771
- fnNumber: index,
772
- timeout,
773
- abortSignal,
811
+ callHandlers.set(name, [
812
+ wrapGuardedInvoke({
813
+ taskName: name,
814
+ invoke: sharedWorker.call({
815
+ fnNumber: index,
816
+ timeout,
817
+ abortSignal,
818
+ }),
774
819
  }),
775
- }));
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
+ }
776
835
  }
777
836
  }
778
837
  const useDirectHandler = (threads ?? 1) === 1 && !usingInliner;
779
- // Imported tasks must never execute on the host inliner lane: their module
780
- // import is meant to happen inside the worker so worker permission policies
781
- // apply. When the inliner is active we strip the inline lane from their
782
- // handler set so they only ever reach real worker lanes.
838
+ // Imported tasks must run on a worker so worker-side permission policies apply.
783
839
  const buildImportedInvoker = (handlers) => {
784
840
  const workerHandlers = [];
785
841
  const workerContexts = [];
@@ -804,6 +860,11 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
804
860
  });
805
861
  };
806
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];
807
868
  if (imported && usingInliner) {
808
869
  return buildImportedInvoker(handlers);
809
870
  }
@@ -836,6 +897,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
836
897
  shutdown: shutdownWithDelay,
837
898
  [Symbol.dispose]: disposePool,
838
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),
839
902
  };
840
903
  };
841
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
  }