knitting 0.1.63 → 0.1.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +623 -342
  2. package/knitting.browser.d.ts +3 -1
  3. package/knitting.browser.js +1 -1
  4. package/knitting.d.ts +3 -1
  5. package/knitting.js +2 -1
  6. package/map.md +0 -6
  7. package/package.json +10 -5
  8. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  9. package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
  10. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  11. package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
  12. package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
  14. package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
  15. package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
  16. package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
  18. package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
  19. package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
  20. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  21. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  22. package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
  23. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  24. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  25. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  26. package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
  27. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  28. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  29. package/scripts/build-native-addons.ts +5 -0
  30. package/shared-memory.d.ts +3 -0
  31. package/shared-memory.js +3 -0
  32. package/src/api.js +158 -71
  33. package/src/common/with-resolvers.js +2 -5
  34. package/src/common/worker-runtime.d.ts +7 -0
  35. package/src/common/worker-runtime.js +7 -0
  36. package/src/connections/buffer-reference.d.ts +10 -36
  37. package/src/connections/buffer-reference.js +15 -170
  38. package/src/connections/node-addons.d.ts +1 -1
  39. package/src/connections/node-addons.js +11 -1
  40. package/src/connections/shared-array-buffer-payload.d.ts +7 -0
  41. package/src/connections/shared-array-buffer-payload.js +27 -11
  42. package/src/debug/gate.js +1 -1
  43. package/src/debug/handle.d.ts +6 -1
  44. package/src/debug/handle.js +14 -6
  45. package/src/error.d.ts +9 -0
  46. package/src/error.js +16 -2
  47. package/src/knitting_buffer_pointer.cc +57 -2
  48. package/src/knitting_doorbell.cc +220 -0
  49. package/src/memory/knitting-body.d.ts +44 -0
  50. package/src/memory/knitting-body.js +51 -0
  51. package/src/memory/knitting-buffer-http.d.ts +116 -0
  52. package/src/memory/knitting-buffer-http.js +255 -0
  53. package/src/memory/knitting-buffer.d.ts +250 -0
  54. package/src/memory/knitting-buffer.js +695 -0
  55. package/src/memory/lazy-region-registry.d.ts +83 -0
  56. package/src/memory/lazy-region-registry.js +355 -0
  57. package/src/memory/lock.d.ts +80 -15
  58. package/src/memory/lock.js +473 -139
  59. package/src/memory/payloadCodec.d.ts +18 -2
  60. package/src/memory/payloadCodec.js +340 -76
  61. package/src/memory/regionRegistry.d.ts +6 -0
  62. package/src/memory/regionRegistry.js +125 -240
  63. package/src/memory/shared-buffer-io.d.ts +7 -0
  64. package/src/memory/shared-buffer-io.js +34 -8
  65. package/src/permission/protocol.d.ts +1 -0
  66. package/src/permission/protocol.js +8 -3
  67. package/src/runtime/deno-doorbell.d.ts +26 -0
  68. package/src/runtime/deno-doorbell.js +117 -0
  69. package/src/runtime/dispatcher.d.ts +13 -6
  70. package/src/runtime/dispatcher.js +101 -63
  71. package/src/runtime/host-arg-arena.d.ts +3 -0
  72. package/src/runtime/host-arg-arena.js +16 -0
  73. package/src/runtime/inline-executor.js +2 -1
  74. package/src/runtime/node-doorbell.d.ts +14 -0
  75. package/src/runtime/node-doorbell.js +84 -0
  76. package/src/runtime/pool.d.ts +30 -15
  77. package/src/runtime/pool.js +199 -151
  78. package/src/runtime/process-worker.d.ts +9 -0
  79. package/src/runtime/process-worker.js +32 -3
  80. package/src/runtime/tx-queue.d.ts +4 -6
  81. package/src/runtime/tx-queue.js +63 -48
  82. package/src/runtime/worker-common.d.ts +7 -0
  83. package/src/runtime/worker-common.js +28 -2
  84. package/src/types.d.ts +66 -78
  85. package/src/worker/loop.js +95 -60
  86. package/src/worker/rx-queue.d.ts +2 -3
  87. package/src/worker/rx-queue.js +34 -40
  88. package/src/worker/safety/index.d.ts +1 -1
  89. package/src/worker/safety/index.js +1 -1
  90. package/src/worker/safety/process.d.ts +2 -0
  91. package/src/worker/safety/process.js +8 -1
  92. package/src/worker/safety/startup.js +11 -6
  93. package/src/worker/shared-return.d.ts +9 -0
  94. package/src/worker/shared-return.js +22 -0
  95. package/src/worker/task-loader.js +1 -2
  96. package/src/worker/timers.d.ts +2 -6
  97. package/src/worker/timers.js +39 -22
  98. package/unsafe.d.ts +2 -1
  99. package/unsafe.js +2 -1
package/knitting.js CHANGED
@@ -2,5 +2,6 @@
2
2
  import { workerMainLoop } from "./src/worker/loop.js";
3
3
  import { checkCompiledWorker, createPool, importTask, isMain, setModuleUrl, task, } from "./src/api.js";
4
4
  import { Envelope } from "./src/common/envelope.js";
5
+ import { KnittingError } from "./src/error.js";
5
6
  import { isNumericArray, NumericArray, } from "./src/connections/numeric-array.js";
6
- export { checkCompiledWorker as checkCompiledWorker, createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, isNumericArray as isNumericArray, NumericArray as NumericArray, setModuleUrl as setModuleUrl, task as task, workerMainLoop as workerMainLoop, };
7
+ export { checkCompiledWorker as checkCompiledWorker, createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, isNumericArray as isNumericArray, KnittingError as KnittingError, NumericArray as NumericArray, setModuleUrl as setModuleUrl, task as task, workerMainLoop as workerMainLoop, };
package/map.md CHANGED
@@ -237,12 +237,8 @@ The core flow is:
237
237
 
238
238
  - `bench/latency.ts`: Compares basic task-call latency against a direct worker
239
239
  postMessage baseline.
240
- - `bench/types.ts`: Compares payload-type behavior between knitting and worker
241
- messaging.
242
240
  - `bench/types_knitting.ts`: Knitting-only payload-type benchmark with broader
243
241
  type coverage.
244
- - `bench/ipc.ts`: Larger IPC comparison benchmark covering local calls,
245
- postMessage, HTTP, WebSocket, and knitting.
246
242
  - `bench/withload.ts`: Measures behavior under main-thread load.
247
243
  - `bench/call-growth.ts`: Measures call cost as payload size grows.
248
244
  - `bench/call-growth-batch.ts`: Batch-focused version of call-growth tests.
@@ -254,8 +250,6 @@ The core flow is:
254
250
  - `bench/payload-sweep.ts`: Uint8Array payload-size sweep promoted from the old
255
251
  scratch file. Supports table output and `--json`.
256
252
  - `bench/postmessage/single.ts`: Worker-thread postMessage baseline helper.
257
- - `bench/postmessage/test.go`: Go comparison/experiment for postMessage-style
258
- IPC.
259
253
  - `bench/util/json-parse.ts`: Mitata JSON output reducer used by benchmark
260
254
  scripts.
261
255
  - `bench/util/type-payloads.ts`: Shared payload cases and size estimation for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knitting",
3
- "version": "0.1.63",
3
+ "version": "0.1.73",
4
4
  "description": "Shared-memory IPC runtime for Node.js, Deno, and Bun.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -83,11 +83,12 @@
83
83
  "release:npm": "npm run test:all && npm publish",
84
84
  "release:npm:dry-run": "npm publish --dry-run",
85
85
  "test": "npm run test:deno",
86
- "test:deno": "deno test -A --ignore=test/runtime.node.test.ts --ignore=test/runtime.process.test.ts",
86
+ "test:deno": "deno test -A --ignore=test/runtime.node.test.ts --ignore=test/runtime.process.test.ts test",
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\"",
@@ -101,13 +102,17 @@
101
102
  "bun": ">=1.0.0"
102
103
  },
103
104
  "peerDependencies": {
104
- "typescript": "^5.2.0"
105
+ "typescript": ">=5.2.0"
106
+ },
107
+ "peerDependenciesMeta": {
108
+ "typescript": {
109
+ "optional": true
110
+ }
105
111
  },
106
112
  "devDependencies": {
107
113
  "@types/bun": "latest",
108
114
  "@types/deno": "latest",
109
115
  "@types/node": "latest",
110
- "dts-bundle-generator": "^9.5.1",
111
116
  "mitata": "^1.0.34",
112
117
  "typescript": "^5.2.0"
113
118
  }
@@ -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
@@ -4,22 +4,28 @@ import { genTaskID, stableTaskID } from "./common/task-source.js";
4
4
  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
+ import { isNodePermissionExecFlag } from "./runtime/worker-common.js";
7
8
  import { ChannelHandler, hostDispatcherLoop } from "./runtime/dispatcher.js";
9
+ import { createDenoCompletionDoorbell } from "./runtime/deno-doorbell.js";
8
10
  import { createSharedArrayBuffer, RUNTIME } from "./common/runtime.js";
9
11
  import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
10
12
  import { classifyProcessPermissionCompatibility, enforceProcessPermissionCompatibility, resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
11
13
  import { getNodeProcess } from "./common/node-compat.js";
12
14
  import { readProcessSharedMemorySettings, readProcessWorkerNodeMajor, readProcessWorkerRuntime, } from "./runtime/process-worker.js";
15
+ import { assertStealClaim, DEFAULT_STEAL_CLAIM, } from "./memory/lock.js";
13
16
  import { TRANSPORT_SIGNAL_BYTES } from "./ipc/transport/shared-memory.js";
14
17
  import { inspectCompiledWorkerArtifact } from "./runtime/compiled-artifact.js";
15
18
  import { managerMethod } from "./runtime/balancer.js";
16
19
  import { createInlineExecutor } from "./runtime/inline-executor.js";
20
+ import { createHostArgAllocator } from "./runtime/host-arg-arena.js";
17
21
  const hasDebugNamespace = (namespaces, namespace) => namespaces.has("*") || namespaces.has(namespace);
18
- const createHostDebug = (namespaces) => {
22
+ const createHostDebug = (namespaces, epoch) => {
19
23
  const enabled = (namespace) => hasDebugNamespace(namespaces, namespace);
20
24
  if (!enabled("host"))
21
25
  return undefined;
22
- const base = performance.now();
26
+ // Same rebase the workers do (see debug/handle.ts), so host and worker lines
27
+ // share one zero.
28
+ const base = epoch - performance.timeOrigin;
23
29
  const tag = `host·${RUNTIME}`;
24
30
  const log = (message) => {
25
31
  const elapsed = (performance.now() - base).toFixed(1);
@@ -242,13 +248,20 @@ const toPoolTaskEntries = (input, callerHref) => Object.entries(input).map(([nam
242
248
  * argument; use an object or tuple for multiple values.
243
249
  */
244
250
  export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe, abortSignalCapacity, source, worker, workerExecArgv, permission, host, }) => (tasks) => {
245
- const bufferReferenceReturn = unsafe?.BufferReferenceReturn;
251
+ const sharedBytesEnabled = unsafe?.SharedBytes === true;
252
+ const sharedArgsEnabled = unsafe?.SharedArgs === true;
246
253
  const debugRequested = DEBUG_ENABLED ||
247
254
  (debug !== undefined && debug !== false);
248
255
  let debugNamespaces;
249
256
  const getDebugNamespaces = () => debugNamespaces ??= resolveDebugNamespaces(debug);
250
- const hostDebug = debugRequested
251
- ? createHostDebug(getDebugNamespaces())
257
+ // Absolute (Unix-epoch ms) zero for every debug clock in this pool. Raw
258
+ // `performance.now()` is relative to each thread/process's own time origin;
259
+ // `timeOrigin + now()` is not, so workers can rebase onto this value.
260
+ const debugEpoch = debugRequested
261
+ ? performance.timeOrigin + performance.now()
262
+ : undefined;
263
+ const hostDebug = debugEpoch !== undefined
264
+ ? createHostDebug(getDebugNamespaces(), debugEpoch)
252
265
  : undefined;
253
266
  const debugEnabled = (namespace) => debugRequested && hasDebugNamespace(getDebugNamespaces(), namespace);
254
267
  /**
@@ -275,6 +288,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
275
288
  return {
276
289
  shutdown: mainThreadOnlyProxy,
277
290
  [Symbol.dispose]: () => { },
291
+ [Symbol.asyncDispose]: async () => { },
278
292
  call: mainThreadOnlyProxy,
279
293
  };
280
294
  }
@@ -303,20 +317,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
303
317
  const permissionExecArgv = toRuntimePermissionFlags(permissionProtocol);
304
318
  const nodeProcess = getNodeProcess();
305
319
  const allowedFlags = nodeProcess?.allowedNodeEnvironmentFlags ?? null;
306
- const isNodePermissionFlag = (flag) => {
307
- const key = flag.split("=", 1)[0];
308
- return key === "--permission" ||
309
- key === "--experimental-permission" ||
310
- key === "--allow-fs-read" ||
311
- key === "--allow-fs-write" ||
312
- key === "--allow-worker" ||
313
- key === "--allow-child-process" ||
314
- key === "--allow-net" ||
315
- key === "--allow-addons" ||
316
- key === "--allow-ffi" ||
317
- key === "--allow-wasi";
318
- };
319
- const stripNodePermissionFlags = (flags) => flags?.filter((flag) => !isNodePermissionFlag(flag));
320
+ const stripNodePermissionFlags = (flags) => flags?.filter((flag) => !isNodePermissionExecFlag(flag));
320
321
  const dedupeFlags = (flags) => {
321
322
  const out = [];
322
323
  const seen = new Set();
@@ -343,16 +344,11 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
343
344
  ? nodeProcess.execArgv
344
345
  : undefined;
345
346
  const defaultExecArgvCandidate = workerExecArgv ??
346
- (inheritedExecArgv
347
- ? (allowedFlags?.has("--expose-gc") === true
348
- ? (inheritedExecArgv.includes("--expose-gc")
349
- ? inheritedExecArgv
350
- : [...inheritedExecArgv, "--expose-gc"])
351
- : inheritedExecArgv)
352
- : undefined);
353
- const defaultExecArgv = permissionProtocol?.unsafe === true
354
- ? stripNodePermissionFlags(defaultExecArgvCandidate)
355
- : defaultExecArgvCandidate;
347
+ inheritedExecArgv;
348
+ // Knitting resolves the worker's permission policy itself. Do not inherit
349
+ // the host's Node grants (or caller-supplied permission flags), which could
350
+ // broaden that policy when Node combines repeated --allow-* options.
351
+ const defaultExecArgv = stripNodePermissionFlags(defaultExecArgvCandidate);
356
352
  const combinedExecArgv = dedupeFlags([
357
353
  ...permissionExecArgv,
358
354
  ...(defaultExecArgv ?? []),
@@ -364,6 +360,16 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
364
360
  hostDebug?.log(`permission=${permissionProtocol?.mode ?? "off"} execArgv=${formatDebugList(execArgv)}`);
365
361
  const usesAbortSignal = listOfFunctions.some((fn) => fn.abortSignal !== undefined);
366
362
  const resolvedWorker = resolveWorkerSettings(worker, callerHref);
363
+ if (RUNTIME === "node" &&
364
+ resolvedWorker?.runtime !== "process" &&
365
+ resolvedWorker?.runtime !== "compiled") {
366
+ const missingPermissionFlags = permissionExecArgv.filter((flag) => isNodePermissionExecFlag(flag) && !execArgv?.includes(flag));
367
+ if (missingPermissionFlags.length > 0) {
368
+ throw new Error("Node cannot apply the resolved permission policy to this thread " +
369
+ `worker (unsupported flags: ${missingPermissionFlags.join(", ")}). ` +
370
+ "Refusing to start the worker without those permissions.");
371
+ }
372
+ }
367
373
  const dispatcherEnv = nodeProcess?.env?.KNITTING_DISPATCHER;
368
374
  const stealEnvRaw = nodeProcess?.env?.KNITTING_STEAL?.trim().toLowerCase();
369
375
  const stealEnv = stealEnvRaw === "1" || stealEnvRaw === "true"
@@ -371,14 +377,24 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
371
377
  : stealEnvRaw === "0" || stealEnvRaw === "false"
372
378
  ? false
373
379
  : 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.
380
+ // Shared-submit stealing is the default for compatible multi-worker pools;
381
+ // explicit options take precedence over the environment.
377
382
  const dispatcherExplicitlySelected = host?.dispatcher !== undefined ||
378
383
  dispatcherEnv === "serial-channel" || dispatcherEnv === "per-thread";
379
384
  const stealDefaultCompatible = balancer === undefined &&
380
385
  !dispatcherExplicitlySelected && (threads ?? 1) <= MAX_STEAL_CONSUMERS;
381
386
  const stealRequested = host?.steal ?? stealEnv ?? stealDefaultCompatible;
387
+ const stealClaimEnvRaw = nodeProcess?.env?.KNITTING_STEAL_CLAIM?.trim()
388
+ .toLowerCase();
389
+ // An unrecognised discipline is an error, not a fallback: `cas-mask` and
390
+ // typos used to select Dekker silently and run under the wrong name.
391
+ const stealClaimEnv = stealClaimEnvRaw === undefined || stealClaimEnvRaw === ""
392
+ ? undefined
393
+ : assertStealClaim(stealClaimEnvRaw, "KNITTING_STEAL_CLAIM");
394
+ // Select the stealing claim discipline, preferring the explicit option.
395
+ const stealClaim = host?.stealClaim === undefined
396
+ ? stealClaimEnv ?? DEFAULT_STEAL_CLAIM
397
+ : assertStealClaim(host.stealClaim, "host.stealClaim");
382
398
  const usingCompiledWorker = resolvedWorker?.runtime === "compiled";
383
399
  if (resolvedWorker?.compiled !== undefined && !usingCompiledWorker) {
384
400
  throw new Error("worker.compiled requires worker.runtime to be compiled");
@@ -463,9 +479,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
463
479
  ? dispatcherEnv
464
480
  : undefined);
465
481
  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.
482
+ // Bun and single-worker pools use per-thread dispatch; other pools share a
483
+ // channel across their private lanes.
469
484
  if (RUNTIME === "bun")
470
485
  return "per-thread";
471
486
  if ((threads ?? 1) <= 1)
@@ -473,10 +488,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
473
488
  return "serial-channel";
474
489
  })();
475
490
  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.
491
+ // Stealing uses one submit region and private return lanes. Explicit balancer
492
+ // or dispatcher settings retain private lanes unless stealing is requested.
480
493
  const useSteal = stealRequested &&
481
494
  !usingCompiledWorker &&
482
495
  (threads ?? 1) > 1 &&
@@ -485,7 +498,9 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
485
498
  ? createStealPoolBuffers({
486
499
  threads: threads ?? 1,
487
500
  payload,
501
+ sharedArgs: sharedArgsEnabled,
488
502
  regionLanes: host?.stealRegionLanes,
503
+ stealClaim,
489
504
  abortSignalCapacity,
490
505
  usesAbortSignal,
491
506
  processWorker: resolvedWorker?.runtime === "process"
@@ -498,13 +513,16 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
498
513
  : undefined;
499
514
  const serialChannel = !usingCompiledWorker && !useSteal &&
500
515
  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.
516
+ // The serial topology relies on channel delivery to run each lane check.
505
517
  const serialDispatcherChannel = serialChannel
506
518
  ? new ChannelHandler("channel")
507
519
  : undefined;
520
+ // Deno uses a threadSafe FFI callback because waitAsync does not wake an idle
521
+ // event loop; process workers cannot use the process-local pointer.
522
+ const denoCompletionDoorbell = !usingCompiledWorker &&
523
+ resolvedWorker?.runtime !== "process" && host?.doorbell !== false
524
+ ? createDenoCompletionDoorbell()
525
+ : undefined;
508
526
  let workers = Array.from({
509
527
  length: threads ?? 1,
510
528
  }).map((_, thread) => spawnWorkerContext({
@@ -514,6 +532,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
514
532
  at,
515
533
  thread,
516
534
  debug,
535
+ debugEpoch,
517
536
  hostDebug: hostDebug?.log,
518
537
  totalNumberOfThread,
519
538
  // Worker count without the inline lane. The inliner runs on the host
@@ -522,13 +541,15 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
522
541
  source,
523
542
  workerOptions: resolvedWorker,
524
543
  workerExecArgv: execArgv,
544
+ requestedExecArgv: workerExecArgv,
525
545
  host,
526
546
  payload,
527
- bufferReferenceReturn,
547
+ sharedBytesEnabled,
528
548
  abortSignalCapacity,
529
549
  usesAbortSignal,
530
550
  permission: permissionProtocol,
531
551
  sharedChannelHandler: serialDispatcherChannel,
552
+ denoCompletionDoorbell,
532
553
  stealPool: stealBuffers === undefined ? undefined : {
533
554
  submitBuffers: stealBuffers.submitBuffers,
534
555
  returnBuffers: stealBuffers.returnBuffers[thread],
@@ -538,6 +559,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
538
559
  consumers: threads ?? 1,
539
560
  consumerId: thread,
540
561
  regionLanes: stealBuffers.regionLanes,
562
+ stealClaim: stealBuffers.stealClaim,
541
563
  abortSignalSAB: stealBuffers.abortSignalSAB,
542
564
  abortSignalMax: stealBuffers.abortSignalMax,
543
565
  processMemory: stealBuffers.processMemory,
@@ -547,23 +569,34 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
547
569
  if (useSteal) {
548
570
  const channel = stealChannel;
549
571
  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.
572
+ // Round-robin wake keeps stealing at one notify per publish; any idle worker
573
+ // can claim the region.
554
574
  let wakeCursor = 0;
575
+ let lastWoken = 0;
555
576
  const wakeOne = () => {
556
577
  const lanes = workers.length;
557
578
  if (lanes === 0)
558
579
  return;
559
- const lane = wakeCursor;
580
+ const lane = lastWoken = wakeCursor;
560
581
  wakeCursor = wakeCursor + 1 < lanes ? wakeCursor + 1 : 0;
561
582
  workers[lane].laneWake?.();
562
583
  };
584
+ // Any awake worker will claim what was just published, so a lost wake
585
+ // only strands work when every worker is parked. Ring the lane this pass
586
+ // already rang: one still waking up absorbs it, and a second lane would
587
+ // be one more thread woken per call.
588
+ const wakeIfAllParked = () => {
589
+ for (let lane = 0; lane < workers.length; lane++) {
590
+ const context = workers[lane];
591
+ if (context.laneAwake?.() !== false)
592
+ return;
593
+ }
594
+ workers[lastWoken]?.laneWake?.();
595
+ };
563
596
  // The shared dispatcher drives one queue, so it needs signal words of its
564
597
  // own rather than any single lane's; waking is delegated to `wakeOne`.
565
598
  const signalWords = new Int32Array(createSharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT));
566
- const { check } = hostDispatcherLoop({
599
+ const { check, wakeCompletion } = hostDispatcherLoop({
567
600
  signalBox: {
568
601
  opView: signalWords.subarray(0, 1),
569
602
  txStatus: signalWords.subarray(1, 2),
@@ -573,19 +606,32 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
573
606
  channelHandler: channel,
574
607
  dispatcherOptions: host,
575
608
  notifySignal: wakeOne,
609
+ wakeAfterPass: wakeIfAllParked,
576
610
  crossProcess: resolvedWorker?.runtime === "process",
611
+ nativeCompletionDoorbell: denoCompletionDoorbell !== undefined ||
612
+ workers.some((context) => context
613
+ .nodeCompletionDoorbell === true),
614
+ processCompletionDoorbell: workers.some((context) => context.processCompletionDoorbell === true),
577
615
  });
578
616
  channel.open(check);
617
+ if (denoCompletionDoorbell !== undefined) {
618
+ workers.forEach((_, thread) => {
619
+ denoCompletionDoorbell.listen(thread, wakeCompletion);
620
+ });
621
+ }
622
+ workers.forEach((context) => {
623
+ context.bindCompletionWake?.(wakeCompletion);
624
+ });
579
625
  workers.forEach((context) => {
580
626
  context.bindSend(() => {
581
627
  if (check.isRunning !== true) {
582
628
  check.isRunning = true;
583
- Promise.resolve().then(check);
629
+ queueMicrotask(check);
584
630
  }
585
631
  wakeOne();
586
632
  });
587
633
  });
588
- hostDebug?.log(`dispatcher=steal lanes=${workers.length} g=${stealBuffers.regionLanes}`);
634
+ hostDebug?.log(`dispatcher=steal lanes=${workers.length} g=${stealBuffers.regionLanes} claim=${stealClaim}`);
589
635
  }
590
636
  const sharedDispatcherChannel = serialDispatcherChannel;
591
637
  if (serialChannel) {
@@ -595,11 +641,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
595
641
  let serialScheduled = false;
596
642
  let serialInFlight = false;
597
643
  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.
644
+ // Visit only active private lanes; inactive lanes stay out of the tick.
603
645
  const active = [];
604
646
  const isActive = new Uint8Array(checks.length);
605
647
  const markActive = (lane) => {
@@ -642,7 +684,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
642
684
  if (serialScheduled)
643
685
  return;
644
686
  serialScheduled = true;
645
- Promise.resolve().then(runSerialChecks);
687
+ queueMicrotask(runSerialChecks);
646
688
  };
647
689
  channel.open(runSerialChecks);
648
690
  workers.forEach((context, lane) => {
@@ -690,11 +732,29 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
690
732
  if (closePromise)
691
733
  return closePromise;
692
734
  closing = true;
735
+ let canCloseNativeDoorbells = denoCompletionDoorbell === undefined;
693
736
  closePromise = Promise.allSettled(workers.map((context) => context.requestStop?.()))
694
- .then(() => Promise.allSettled(workers.map((context) => context.kills())))
737
+ .then((stops) => {
738
+ if (denoCompletionDoorbell !== undefined) {
739
+ canCloseNativeDoorbells = stops.every((result, index) => {
740
+ if (workers[index].requestStop === undefined)
741
+ return true;
742
+ return result.status === "fulfilled" && result.value === true;
743
+ });
744
+ }
745
+ return Promise.allSettled(workers.map((context) => context.kills()));
746
+ })
695
747
  .then(() => {
696
748
  sharedDispatcherChannel?.close();
697
749
  stealChannel?.close();
750
+ })
751
+ .finally(() => {
752
+ if (canCloseNativeDoorbells) {
753
+ denoCompletionDoorbell?.close();
754
+ return;
755
+ }
756
+ // Keep the callback alive if a worker failed to acknowledge shutdown.
757
+ denoCompletionDoorbell?.unref();
698
758
  });
699
759
  return closePromise;
700
760
  };
@@ -763,23 +823,41 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
763
823
  for (const { name } of indexedFunctions) {
764
824
  callHandlers.set(name, []);
765
825
  }
766
- for (const worker of workers) {
826
+ if (useSteal) {
827
+ // Every worker context points at the same pool-global submit queue in the
828
+ // stealing topology. There is therefore no worker to assign here: the
829
+ // claimant is selected later by decodeSteal(). Keep one call closure per
830
+ // task and avoid paying N equivalent wrapper allocations at pool setup.
831
+ const sharedWorker = workers[0];
767
832
  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,
833
+ callHandlers.set(name, [
834
+ wrapGuardedInvoke({
835
+ taskName: name,
836
+ invoke: sharedWorker.call({
837
+ fnNumber: index,
838
+ timeout,
839
+ abortSignal,
840
+ }),
774
841
  }),
775
- }));
842
+ ]);
843
+ }
844
+ }
845
+ else {
846
+ for (const worker of workers) {
847
+ for (const { name, index, timeout, abortSignal } of indexedFunctions) {
848
+ callHandlers.get(name).push(wrapGuardedInvoke({
849
+ taskName: name,
850
+ invoke: worker.call({
851
+ fnNumber: index,
852
+ timeout,
853
+ abortSignal,
854
+ }),
855
+ }));
856
+ }
776
857
  }
777
858
  }
778
859
  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.
860
+ // Imported tasks must run on a worker so worker-side permission policies apply.
783
861
  const buildImportedInvoker = (handlers) => {
784
862
  const workerHandlers = [];
785
863
  const workerContexts = [];
@@ -804,6 +882,11 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
804
882
  });
805
883
  };
806
884
  const buildInvoker = (handlers, imported) => {
885
+ // Stealing owns one shared submit queue. The worker that runs a call is
886
+ // deliberately chosen by the worker-side claim protocol, not by the host
887
+ // balancer, so round-robin/first-idle selection would only add overhead.
888
+ if (useSteal)
889
+ return handlers[0];
807
890
  if (imported && usingInliner) {
808
891
  return buildImportedInvoker(handlers);
809
892
  }
@@ -835,7 +918,10 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
835
918
  return {
836
919
  shutdown: shutdownWithDelay,
837
920
  [Symbol.dispose]: disposePool,
921
+ [Symbol.asyncDispose]: () => shutdownWithDelay(),
838
922
  call: Object.fromEntries(callEntries),
923
+ // Only the shared submit queue has a single arena to build arguments in.
924
+ sharedArgBytes: createHostArgAllocator(sharedArgsEnabled ? stealBuffers?.submitBuffers.payload : undefined),
839
925
  };
840
926
  };
841
927
  const SINGLE_TASK_KEY = "__task__";
@@ -847,6 +933,7 @@ const createSingleTaskPool = (single, options) => {
847
933
  call: pool.call[SINGLE_TASK_KEY],
848
934
  shutdown: pool.shutdown,
849
935
  [Symbol.dispose]: pool[Symbol.dispose],
936
+ [Symbol.asyncDispose]: pool[Symbol.asyncDispose],
850
937
  };
851
938
  };
852
939
  const buildTaskDefinitionFromCaller = (input, callerHref, at, imported = false) => {
@@ -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;