knitting 0.1.60 → 0.1.61

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 (42) hide show
  1. package/BROWSER.md +134 -0
  2. package/README.md +93 -6
  3. package/knitting.browser.d.ts +6 -0
  4. package/knitting.browser.js +1 -0
  5. package/package.json +11 -2
  6. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  7. package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
  8. package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
  9. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
  11. package/prebuilds/darwin-arm64-node-137/knitting_shm.node +0 -0
  12. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  13. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  14. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  15. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  16. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  18. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  19. package/src/api.js +127 -22
  20. package/src/common/runtime.d.ts +2 -1
  21. package/src/common/runtime.js +8 -1
  22. package/src/memory/lock.d.ts +26 -1
  23. package/src/memory/lock.js +212 -4
  24. package/src/memory/payloadCodec.js +2 -6
  25. package/src/memory/regionRegistry.d.ts +1 -3
  26. package/src/memory/regionRegistry.js +16 -14
  27. package/src/permission/protocol.js +5 -1
  28. package/src/runtime/dispatcher.js +9 -0
  29. package/src/runtime/pool.d.ts +114 -3
  30. package/src/runtime/pool.js +209 -63
  31. package/src/runtime/process-worker.d.ts +2 -12
  32. package/src/runtime/process-worker.js +69 -113
  33. package/src/runtime/tx-queue.d.ts +15 -1
  34. package/src/runtime/tx-queue.js +52 -15
  35. package/src/runtime/worker-common.d.ts +13 -0
  36. package/src/runtime/worker-common.js +102 -0
  37. package/src/types.d.ts +41 -3
  38. package/src/worker/loop.js +27 -5
  39. package/src/worker/rx-queue.d.ts +11 -1
  40. package/src/worker/rx-queue.js +4 -1
  41. package/src/worker/timers.d.ts +8 -1
  42. package/src/worker/timers.js +20 -17
@@ -1,17 +1,10 @@
1
1
  import { type LockControlCarpet } from "../memory/byte-carpet.js";
2
2
  import type { DebugOptions, LockBuffers, WorkerData, WorkerSettings } from "../types.js";
3
+ import { type NodeWorkerLike, type SpawnedWorker } from "./worker-common.js";
3
4
  import { RUNTIME_PROCESS_WORKER_BOOT_VERSION } from "../common/worker-runtime.js";
4
5
  import { type SharedBufferSource } from "../common/shared-buffer-region.js";
5
6
  import { FileDescriptor, type ProcessSharedBufferMetadata } from "../connections/index.js";
6
7
  import type { SharedMemoryBuffer, SharedMemoryMapping } from "../connections/types.js";
7
- export type SpawnedWorker = {
8
- terminate: () => unknown;
9
- unref?: () => unknown;
10
- postMessage?: (message: unknown) => void;
11
- };
12
- export type NodeWorkerLike = {
13
- on?: (event: "error" | "exit" | "message", listener: (...args: unknown[]) => void) => void;
14
- };
15
8
  type ProcessWorkerWireLockBuffers = Omit<LockBuffers, "headers" | "lockSector" | "payload" | "payloadSector"> & {
16
9
  headers: ProcessSharedBufferMetadata;
17
10
  lockSector: ProcessSharedBufferMetadata;
@@ -60,8 +53,6 @@ type ProcessSharedMemoryAllocator = {
60
53
  createBuffer: (byteLength: number) => SharedArrayBuffer;
61
54
  backings: ProcessSharedMemoryBacking[];
62
55
  };
63
- export declare const toWorkerSafeExecArgv: (flags: string[] | undefined) => string[] | undefined;
64
- export declare const toWorkerCompatExecArgv: (flags: string[] | undefined) => string[] | undefined;
65
56
  export declare const createProcessSharedMemoryAllocator: (debug: DebugOptions | undefined) => ProcessSharedMemoryAllocator | undefined;
66
57
  export declare const createProcessWorkerMemoryLayout: ({ signalBytes, abortBytes, payloadBytes, thread, sharedMemory, }: {
67
58
  signalBytes: number;
@@ -70,11 +61,11 @@ export declare const createProcessWorkerMemoryLayout: ({ signalBytes, abortBytes
70
61
  thread: number;
71
62
  sharedMemory: ResolvedProcessSharedMemorySettings;
72
63
  }) => ProcessWorkerMemoryLayout;
73
- export declare const serializeWorkerBootstrapData: (options: WorkerSettings) => WorkerSettings;
74
64
  export declare const toProcessWorkerBootPayload: (workerData: WorkerData, memory: ProcessWorkerMemoryLayout) => ProcessWorkerBootPayload;
75
65
  export declare const readProcessWorkerRuntime: (options: WorkerSettings | undefined) => ProcessWorkerRuntime;
76
66
  export declare const readProcessWorkerCommandPrefix: (options: WorkerSettings | undefined) => ProcessWorkerCommandPrefix | undefined;
77
67
  export declare const readProcessSharedMemorySettings: (options: WorkerSettings | undefined) => ResolvedProcessSharedMemorySettings;
68
+ export declare const readProcessWorkerNodeMajor: (options: WorkerSettings | undefined) => number | undefined;
78
69
  export declare const createProcessWorkerNativeSignalNotifier: ({ processRuntime, signal, }: {
79
70
  processRuntime: ProcessWorkerRuntime | undefined;
80
71
  signal: Int32Array;
@@ -87,6 +78,5 @@ export declare const spawnProcessWorker: (options: {
87
78
  commandPrefix?: ProcessWorkerCommandPrefix;
88
79
  permission?: WorkerData["permission"];
89
80
  }) => SpawnedWorker & NodeWorkerLike;
90
- export declare const terminateWorkerQuietly: (worker: SpawnedWorker) => void;
91
81
  export declare const cleanupProcessWorkerMemoryQuietly: (memory: ProcessWorkerMemoryLayout | undefined) => void;
92
82
  export {};
@@ -2,6 +2,7 @@ import { HEADER_SLOT_STRIDE_U32, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../
2
2
  import { createByteCarpet, getHeaderBlockByteLength, makeSharedBufferRegion, } from "../memory/byte-carpet.js";
3
3
  import { RUNTIME } from "../common/runtime.js";
4
4
  import { debugHas } from "../debug/gate.js";
5
+ import { toWorkerCompatExecArgv, } from "./worker-common.js";
5
6
  import { RUNTIME_POOL_DEPTH, RUNTIME_POOL_DEPTH_ENV, RUNTIME_PROCESS_WORKER_BOOT_ENV, RUNTIME_PROCESS_WORKER_BOOT_VERSION, RUNTIME_PROCESS_WORKER_ENV, } from "../common/worker-runtime.js";
6
7
  import { getNodeBuiltinModule, getNodeProcess } from "../common/node-compat.js";
7
8
  import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
@@ -20,51 +21,6 @@ const fileURLToPathCompat = (value) => {
20
21
  }
21
22
  return url.fileURLToPath(value);
22
23
  };
23
- const execFlagKey = (flag) => flag.split("=", 1)[0];
24
- const NODE_PERMISSION_EXEC_FLAGS = new Set([
25
- "--permission",
26
- "--experimental-permission",
27
- "--allow-fs-read",
28
- "--allow-fs-write",
29
- "--allow-worker",
30
- "--allow-child-process",
31
- "--allow-net",
32
- "--allow-addons",
33
- "--allow-ffi",
34
- "--allow-wasi",
35
- ]);
36
- const NODE_WORKER_SAFE_EXEC_FLAGS = new Set([
37
- "--experimental-ffi",
38
- "--experimental-transform-types",
39
- "--expose-gc",
40
- "--no-warnings",
41
- ...NODE_PERMISSION_EXEC_FLAGS,
42
- ]);
43
- const isNodeWorkerSafeExecFlag = (flag) => NODE_WORKER_SAFE_EXEC_FLAGS.has(execFlagKey(flag));
44
- const isNodePermissionExecFlag = (flag) => NODE_PERMISSION_EXEC_FLAGS.has(execFlagKey(flag));
45
- export const toWorkerSafeExecArgv = (flags) => {
46
- if (!flags || flags.length === 0)
47
- return undefined;
48
- const filtered = flags.filter(isNodeWorkerSafeExecFlag);
49
- if (filtered.length === 0)
50
- return undefined;
51
- const seen = new Set();
52
- const deduped = [];
53
- for (const flag of filtered) {
54
- if (seen.has(flag))
55
- continue;
56
- seen.add(flag);
57
- deduped.push(flag);
58
- }
59
- return deduped;
60
- };
61
- export const toWorkerCompatExecArgv = (flags) => {
62
- const safe = toWorkerSafeExecArgv(flags);
63
- if (!safe || safe.length === 0)
64
- return undefined;
65
- const compat = safe.filter((flag) => !isNodePermissionExecFlag(flag));
66
- return compat.length > 0 ? compat : undefined;
67
- };
68
24
  const toProcessSharedMemorySize = (byteLength) => {
69
25
  if (!Number.isFinite(byteLength) || byteLength <= 0) {
70
26
  throw new RangeError("process shared memory byteLength must be positive");
@@ -118,19 +74,18 @@ const DENO_PROCESS_WORKER_INTERNAL_FLAGS = [
118
74
  `--allow-env=${DENO_PROCESS_WORKER_BOOT_ENV_ALLOW}`,
119
75
  "--allow-ffi",
120
76
  ];
121
- const nodeMajorVersion = () => {
122
- const version = getNodeProcess()?.versions?.node ?? "";
123
- const major = Number.parseInt(version.split(".", 1)[0] ?? "", 10);
124
- return Number.isInteger(major) ? major : 0;
125
- };
126
- const nodeProcessWorkerUsesFfi = () => {
127
- const major = nodeMajorVersion();
128
- return major >= 26 && major % 2 === 0;
77
+ const parseNodeMajorVersion = (version) => {
78
+ const match = /^v?(\d+)/.exec(String(version).trim());
79
+ if (match === null)
80
+ return undefined;
81
+ const major = Number.parseInt(match[1], 10);
82
+ return Number.isInteger(major) && major > 0 ? major : undefined;
129
83
  };
130
- const nodeProcessWorkerInternalExecArgv = () => {
84
+ const nodeProcessWorkerUsesFfi = (nodeMajor) => nodeMajor !== undefined && nodeMajor >= 26 && nodeMajor % 2 === 0;
85
+ const nodeProcessWorkerInternalExecArgv = (nodeMajor) => {
131
86
  return [
132
87
  "--no-warnings",
133
- ...(nodeProcessWorkerUsesFfi()
88
+ ...(nodeProcessWorkerUsesFfi(nodeMajor)
134
89
  ? ["--experimental-ffi"]
135
90
  : ["--experimental-transform-types"]),
136
91
  ];
@@ -254,49 +209,6 @@ const toProcessWorkerWireLockBuffers = (lock, descriptor) => ({
254
209
  payload: toChildProcessSharedBufferMetadata(lock.payload, descriptor),
255
210
  payloadSector: toChildProcessSharedBufferMetadata(lock.payloadSector, descriptor),
256
211
  });
257
- const isPlainRecord = (value) => {
258
- if (value === null || typeof value !== "object")
259
- return false;
260
- const prototype = Object.getPrototypeOf(value);
261
- return prototype === Object.prototype || prototype === null;
262
- };
263
- const serializeWorkerBootstrapValue = (value, seen = new WeakMap()) => {
264
- if (value instanceof ProcessSharedBuffer)
265
- return value.toMetadata();
266
- if (value === null || typeof value !== "object")
267
- return value;
268
- const existing = seen.get(value);
269
- if (existing !== undefined)
270
- return existing;
271
- if (Array.isArray(value)) {
272
- const out = [];
273
- seen.set(value, out);
274
- for (const item of value) {
275
- out.push(serializeWorkerBootstrapValue(item, seen));
276
- }
277
- return out;
278
- }
279
- if (!isPlainRecord(value))
280
- return value;
281
- const out = {};
282
- seen.set(value, out);
283
- for (const [key, item] of Object.entries(value)) {
284
- out[key] = serializeWorkerBootstrapValue(item, seen);
285
- }
286
- return out;
287
- };
288
- export const serializeWorkerBootstrapData = (options) => {
289
- const bootstrap = options.bootstrap;
290
- if (bootstrap === undefined || bootstrap.data === undefined)
291
- return options;
292
- return {
293
- ...options,
294
- bootstrap: {
295
- ...bootstrap,
296
- data: serializeWorkerBootstrapValue(bootstrap.data),
297
- },
298
- };
299
- };
300
212
  export const toProcessWorkerBootPayload = (workerData, memory) => ({
301
213
  version: RUNTIME_PROCESS_WORKER_BOOT_VERSION,
302
214
  workerData: {
@@ -417,10 +329,62 @@ const processWorkerNodeBinary = (commandPrefix) => {
417
329
  : undefined) ??
418
330
  DEFAULT_NODE_BINARY;
419
331
  };
420
- const processWorkerNodeExecArgv = (permission) => {
332
+ const processWorkerNodeMajorCache = new Map();
333
+ const resolveProcessWorkerNodeMajor = (commandPrefix) => {
334
+ const nodeProcess = getNodeProcess();
335
+ if (commandPrefix === undefined &&
336
+ nodeProcess !== undefined &&
337
+ nodeProcess?.env?.NODE_BINARY === undefined &&
338
+ RUNTIME === "node") {
339
+ return parseNodeMajorVersion(nodeProcess.versions?.node);
340
+ }
341
+ const nodeBinary = processWorkerNodeBinary(commandPrefix);
342
+ const cacheKey = JSON.stringify([commandPrefix ?? null, nodeBinary]);
343
+ if (processWorkerNodeMajorCache.has(cacheKey)) {
344
+ return processWorkerNodeMajorCache.get(cacheKey);
345
+ }
346
+ let major;
347
+ try {
348
+ const childProcess = getNodeBuiltinModule("node:child_process");
349
+ if (typeof childProcess?.spawnSync === "function") {
350
+ const command = commandPrefix?.[0] ?? nodeBinary;
351
+ const args = commandPrefix === undefined
352
+ ? ["--version"]
353
+ : [...commandPrefix.slice(1), nodeBinary, "--version"];
354
+ const probe = childProcess.spawnSync(command, args, {
355
+ encoding: "utf8",
356
+ stdio: ["ignore", "pipe", "ignore"],
357
+ timeout: 2_000,
358
+ windowsHide: true,
359
+ });
360
+ if (probe.status === 0) {
361
+ major = parseNodeMajorVersion(probe.stdout);
362
+ }
363
+ }
364
+ }
365
+ catch {
366
+ // Unknown targets conservatively use the Node 22/24 addon transport.
367
+ }
368
+ processWorkerNodeMajorCache.set(cacheKey, major);
369
+ return major;
370
+ };
371
+ export const readProcessWorkerNodeMajor = (options) => resolveProcessWorkerNodeMajor(readProcessWorkerCommandPrefix(options));
372
+ const nodeProcessWorkerFlagIsSupported = (flag, nodeMajor) => {
373
+ const key = flag.split("=", 1)[0];
374
+ if (key === "--experimental-ffi" || key === "--allow-ffi") {
375
+ return nodeProcessWorkerUsesFfi(nodeMajor);
376
+ }
377
+ if (key === "--allow-net") {
378
+ return nodeMajor !== undefined && nodeMajor >= 25;
379
+ }
380
+ return true;
381
+ };
382
+ const processWorkerNodeExecArgv = (permission, nodeMajor) => {
421
383
  const out = [];
422
384
  const seen = new Set();
423
385
  const add = (flag) => {
386
+ if (!nodeProcessWorkerFlagIsSupported(flag, nodeMajor))
387
+ return;
424
388
  if (seen.has(flag))
425
389
  return;
426
390
  seen.add(flag);
@@ -429,16 +393,17 @@ const processWorkerNodeExecArgv = (permission) => {
429
393
  for (const flag of toWorkerCompatExecArgv(getNodeProcess()?.execArgv) ?? []) {
430
394
  add(flag);
431
395
  }
432
- for (const flag of nodeProcessWorkerInternalExecArgv())
396
+ for (const flag of nodeProcessWorkerInternalExecArgv(nodeMajor))
433
397
  add(flag);
434
398
  if (permission?.enabled === true && permission.unsafe !== true) {
435
399
  for (const flag of permission.node.flags)
436
400
  add(flag);
437
- if (permission.netAll && nodeMajorVersion() >= 25)
401
+ if (permission.netAll && nodeMajor !== undefined && nodeMajor >= 25) {
438
402
  add("--allow-net");
403
+ }
439
404
  // Node process workers need one native transport capability regardless of
440
405
  // task permissions: addons on Node 22/24, node:ffi on supported Node 26+ LTS.
441
- add(nodeProcessWorkerUsesFfi() ? "--allow-ffi" : "--allow-addons");
406
+ add(nodeProcessWorkerUsesFfi(nodeMajor) ? "--allow-ffi" : "--allow-addons");
442
407
  }
443
408
  return out;
444
409
  };
@@ -454,9 +419,10 @@ const processWorkerCommand = ({ processRuntime, workerUrl, bun, deno, commandPre
454
419
  ];
455
420
  }
456
421
  else if (processRuntime === "node") {
422
+ const nodeMajor = resolveProcessWorkerNodeMajor(commandPrefix);
457
423
  command = [
458
424
  processWorkerNodeBinary(commandPrefix),
459
- ...processWorkerNodeExecArgv(permission),
425
+ ...processWorkerNodeExecArgv(permission, nodeMajor),
460
426
  workerPath,
461
427
  ];
462
428
  }
@@ -687,16 +653,6 @@ export const spawnProcessWorker = (options) => {
687
653
  throw new Error("process worker runtime is only available in Node, Deno, or Bun");
688
654
  }
689
655
  };
690
- export const terminateWorkerQuietly = (worker) => {
691
- try {
692
- // Runaway worker termination can be slow or stuck on some runtimes; once the
693
- // pool is closing it must not keep the host process alive.
694
- worker.unref?.();
695
- void Promise.resolve(worker.terminate()).catch(() => { });
696
- }
697
- catch {
698
- }
699
- };
700
656
  export const cleanupProcessWorkerMemoryQuietly = (memory) => {
701
657
  try {
702
658
  memory?.cleanup();
@@ -11,15 +11,29 @@ type CreateHostTxQueueArgs = {
11
11
  max?: number;
12
12
  lock: Lock2;
13
13
  returnLock: Lock2;
14
+ /**
15
+ * Extra return locks to drain into the same pending registry.
16
+ *
17
+ * Under work stealing the host publishes into one shared submit lock and any
18
+ * worker may claim the task, so the response comes back on whichever lane
19
+ * executed it. Task IDs index this queue's `queue` array, which is
20
+ * pool-global, so every lane's `resolveHost` can settle into it — the
21
+ * stealing worker owns the response and the host demultiplexes by ID.
22
+ *
23
+ * When omitted, only `returnLock` is drained (the classic one-lane shape).
24
+ */
25
+ extraReturnLocks?: readonly Lock2[];
14
26
  releaseBufferReferenceReturn?: ((token: bigint) => void) | BufferReferenceReturnHooks;
15
27
  abortSignals?: Pick<SignalAbortStore, "getSignal" | "setSignal" | "resetSignal" | "closeNow">;
16
28
  now?: () => number;
17
29
  };
18
- export declare function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenceReturn, abortSignals, now, }: CreateHostTxQueueArgs): {
30
+ type ReturnHooks = ((token: bigint) => void) | BufferReferenceReturnHooks;
31
+ export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, releaseBufferReferenceReturn, abortSignals, now, }: CreateHostTxQueueArgs): {
19
32
  rejectAll: (reason: string) => void;
20
33
  hasPendingFrames: () => boolean;
21
34
  txIdle: () => boolean;
22
35
  completeFrame: () => number;
36
+ setReturnHooks: (lane: number, hooks: ReturnHooks | undefined) => void;
23
37
  enqueue: (functionID: FunctionID, timeout?: TaskTimeout, abortSignal?: AbortSignalOption) => (rawArgs: RawArguments) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
24
38
  flushToWorker: () => boolean;
25
39
  enqueueKnown: (task: QueueTask) => boolean;
@@ -14,7 +14,7 @@ const FUNCTION_META_SHIFT = 16;
14
14
  const ABORT_SIGNAL_META_OFFSET = 1;
15
15
  const NO_ABORT_SIGNAL = -1;
16
16
  const p_now = performance.now.bind(performance);
17
- export function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenceReturn, abortSignals, now, }) {
17
+ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, releaseBufferReferenceReturn, abortSignals, now, }) {
18
18
  const PLACE_HOLDER = (_) => {
19
19
  throw ("UNREACHABLE FROM PLACE HOLDER (main)");
20
20
  };
@@ -37,22 +37,53 @@ export function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenc
37
37
  let inUsed = 0 | 0;
38
38
  const resetSignal = abortSignals?.resetSignal;
39
39
  const nowTime = now ?? p_now;
40
- const resolveReturn = returnLock.resolveHost({
40
+ const onReturnResolved = (task) => {
41
+ inUsed = (inUsed - 1) | 0;
42
+ resetTaskLocalFlags(task);
43
+ runTaskFinalizers(task);
44
+ task.value = null;
45
+ task.resolve = PLACE_HOLDER;
46
+ task.reject = PLACE_HOLDER;
47
+ freePush(task[TaskIndex.ID]);
48
+ };
49
+ const returnResolvers = [
50
+ returnLock,
51
+ ...(extraReturnLocks ?? []),
52
+ ].map((each) => each.resolveHost({
41
53
  queue,
42
54
  activeRejectPlaceholder: PLACE_HOLDER,
43
- onResolved: (task) => {
44
- inUsed = (inUsed - 1) | 0;
45
- resetTaskLocalFlags(task);
46
- runTaskFinalizers(task);
47
- task.value = null;
48
- task.resolve = PLACE_HOLDER;
49
- task.reject = PLACE_HOLDER;
50
- freePush(task[TaskIndex.ID]);
51
- },
52
- });
53
- const completeFrame = releaseBufferReferenceReturn === undefined
54
- ? resolveReturn
55
- : () => withBufferReferenceReturnReleaser(releaseBufferReferenceReturn, resolveReturn);
55
+ onResolved: onReturnResolved,
56
+ }));
57
+ // A stealing queue drains one private return lock per worker. Borrowed
58
+ // BufferReferences must therefore be claimed with the hooks belonging to the
59
+ // worker that produced that particular return. The hooks are bound after the
60
+ // worker context exists, while the pool-global queue is built before workers
61
+ // spawn, so keep one mutable hook slot per return lane.
62
+ const returnHooks = new Array(returnResolvers.length);
63
+ if (releaseBufferReferenceReturn !== undefined) {
64
+ returnHooks[0] = releaseBufferReferenceReturn;
65
+ }
66
+ const resolveReturnAt = (index) => {
67
+ const resolve = returnResolvers[index];
68
+ const hooks = returnHooks[index];
69
+ return hooks === undefined
70
+ ? resolve()
71
+ : withBufferReferenceReturnReleaser(hooks, resolve);
72
+ };
73
+ // Preserve the original one-lane fast path exactly: no wrapper, array lookup,
74
+ // or hook branch is paid by the default one-worker transport. Mutable
75
+ // per-return-lane hooks are needed only by a multi-worker stealing queue.
76
+ const completeFrame = returnResolvers.length === 1
77
+ ? releaseBufferReferenceReturn === undefined
78
+ ? returnResolvers[0]
79
+ : () => withBufferReferenceReturnReleaser(releaseBufferReferenceReturn, returnResolvers[0])
80
+ : () => {
81
+ let resolved = 0 | 0;
82
+ for (let i = 0; i < returnResolvers.length; i++) {
83
+ resolved = (resolved + resolveReturnAt(i)) | 0;
84
+ }
85
+ return resolved;
86
+ };
56
87
  const hasActiveTasks = () => {
57
88
  const count = (inUsed - getPendingPromiseCount()) | 0;
58
89
  return count > 0;
@@ -85,6 +116,12 @@ export function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenc
85
116
  hasPendingFrames,
86
117
  txIdle,
87
118
  completeFrame,
119
+ setReturnHooks: (lane, hooks) => {
120
+ if (!Number.isInteger(lane) || lane < 0 || lane >= returnHooks.length) {
121
+ throw new RangeError(`return lane ${lane} out of range`);
122
+ }
123
+ returnHooks[lane] = hooks;
124
+ },
88
125
  enqueue: (functionID, timeout, abortSignal) => {
89
126
  const HAS_TIMER = timeout !== undefined;
90
127
  const functionIDMasked = functionID & FUNCTION_ID_MASK;
@@ -0,0 +1,13 @@
1
+ import type { WorkerSettings } from "../types.js";
2
+ export type SpawnedWorker = {
3
+ terminate: () => unknown;
4
+ unref?: () => unknown;
5
+ postMessage?: (message: unknown) => void;
6
+ };
7
+ export type NodeWorkerLike = {
8
+ on?: (event: "error" | "exit" | "message", listener: (...args: unknown[]) => void) => void;
9
+ };
10
+ export declare const toWorkerSafeExecArgv: (flags: string[] | undefined) => string[] | undefined;
11
+ export declare const toWorkerCompatExecArgv: (flags: string[] | undefined) => string[] | undefined;
12
+ export declare const serializeWorkerBootstrapData: (options: WorkerSettings) => WorkerSettings;
13
+ export declare const terminateWorkerQuietly: (worker: SpawnedWorker) => void;
@@ -0,0 +1,102 @@
1
+ // Worker helpers shared by every worker mode. Kept out of `process-worker.ts`
2
+ // so the thread and web-worker paths do not pull the process-worker module —
3
+ // and, through it, the whole native connections tree — into their bundles.
4
+ import { ProcessSharedBuffer } from "../connections/process-shared-buffer.js";
5
+ const execFlagKey = (flag) => flag.split("=", 1)[0];
6
+ const NODE_PERMISSION_EXEC_FLAGS = new Set([
7
+ "--permission",
8
+ "--experimental-permission",
9
+ "--allow-fs-read",
10
+ "--allow-fs-write",
11
+ "--allow-worker",
12
+ "--allow-child-process",
13
+ "--allow-net",
14
+ "--allow-addons",
15
+ "--allow-ffi",
16
+ "--allow-wasi",
17
+ ]);
18
+ const NODE_WORKER_SAFE_EXEC_FLAGS = new Set([
19
+ "--experimental-ffi",
20
+ "--experimental-transform-types",
21
+ "--expose-gc",
22
+ "--no-warnings",
23
+ ...NODE_PERMISSION_EXEC_FLAGS,
24
+ ]);
25
+ const isNodeWorkerSafeExecFlag = (flag) => NODE_WORKER_SAFE_EXEC_FLAGS.has(execFlagKey(flag));
26
+ const isNodePermissionExecFlag = (flag) => NODE_PERMISSION_EXEC_FLAGS.has(execFlagKey(flag));
27
+ export const toWorkerSafeExecArgv = (flags) => {
28
+ if (!flags || flags.length === 0)
29
+ return undefined;
30
+ const filtered = flags.filter(isNodeWorkerSafeExecFlag);
31
+ if (filtered.length === 0)
32
+ return undefined;
33
+ const seen = new Set();
34
+ const deduped = [];
35
+ for (const flag of filtered) {
36
+ if (seen.has(flag))
37
+ continue;
38
+ seen.add(flag);
39
+ deduped.push(flag);
40
+ }
41
+ return deduped;
42
+ };
43
+ export const toWorkerCompatExecArgv = (flags) => {
44
+ const safe = toWorkerSafeExecArgv(flags);
45
+ if (!safe || safe.length === 0)
46
+ return undefined;
47
+ const compat = safe.filter((flag) => !isNodePermissionExecFlag(flag));
48
+ return compat.length > 0 ? compat : undefined;
49
+ };
50
+ const isPlainRecord = (value) => {
51
+ if (value === null || typeof value !== "object")
52
+ return false;
53
+ const prototype = Object.getPrototypeOf(value);
54
+ return prototype === Object.prototype || prototype === null;
55
+ };
56
+ const serializeWorkerBootstrapValue = (value, seen = new WeakMap()) => {
57
+ if (value instanceof ProcessSharedBuffer)
58
+ return value.toMetadata();
59
+ if (value === null || typeof value !== "object")
60
+ return value;
61
+ const existing = seen.get(value);
62
+ if (existing !== undefined)
63
+ return existing;
64
+ if (Array.isArray(value)) {
65
+ const out = [];
66
+ seen.set(value, out);
67
+ for (const item of value) {
68
+ out.push(serializeWorkerBootstrapValue(item, seen));
69
+ }
70
+ return out;
71
+ }
72
+ if (!isPlainRecord(value))
73
+ return value;
74
+ const out = {};
75
+ seen.set(value, out);
76
+ for (const [key, item] of Object.entries(value)) {
77
+ out[key] = serializeWorkerBootstrapValue(item, seen);
78
+ }
79
+ return out;
80
+ };
81
+ export const serializeWorkerBootstrapData = (options) => {
82
+ const bootstrap = options.bootstrap;
83
+ if (bootstrap === undefined || bootstrap.data === undefined)
84
+ return options;
85
+ return {
86
+ ...options,
87
+ bootstrap: {
88
+ ...bootstrap,
89
+ data: serializeWorkerBootstrapValue(bootstrap.data),
90
+ },
91
+ };
92
+ };
93
+ export const terminateWorkerQuietly = (worker) => {
94
+ try {
95
+ // Runaway worker termination can be slow or stuck on some runtimes; once the
96
+ // pool is closing it must not keep the host process alive.
97
+ worker.unref?.();
98
+ void Promise.resolve(worker.terminate()).catch(() => { });
99
+ }
100
+ catch {
101
+ }
102
+ };
package/src/types.d.ts CHANGED
@@ -34,6 +34,17 @@ type WorkerData = {
34
34
  payloadConfig?: PayloadBufferOptions;
35
35
  bufferReferenceReturn?: "copy" | "borrow";
36
36
  permission?: ResolvedPermissionProtocol;
37
+ /**
38
+ * Work stealing. When present, `lock` is a submit region shared by every
39
+ * worker and this worker claims from it as consumer `consumerId` of
40
+ * `consumers`, rather than owning a private request lane. `returnLock` stays
41
+ * private — the endpoint that claims a task owns its response.
42
+ */
43
+ steal?: {
44
+ consumers: number;
45
+ consumerId: number;
46
+ regionLanes: number;
47
+ };
37
48
  };
38
49
  type UnsafeOptions = {
39
50
  /**
@@ -343,11 +354,38 @@ type DispatcherSettings = {
343
354
  * - `"serial-channel"`: each worker keeps its own dispatcher check state, but
344
355
  * one shared channel runs all lane checks from first to last.
345
356
  *
346
- * Experimental default: `"per-thread"` on Bun or with one worker, otherwise
347
- * `"serial-channel"` for multi-worker Node/Deno pools. Can also be forced
348
- * with the `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
357
+ * Used by private-lane pools. Its experimental default is `"per-thread"` on
358
+ * Bun or with one worker, otherwise `"serial-channel"` for multi-worker
359
+ * Node/Deno pools. Selecting it explicitly keeps private lanes unless
360
+ * `steal: true` is also explicit. Can also be forced with the
361
+ * `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
349
362
  */
350
363
  dispatcher?: "per-thread" | "serial-channel";
364
+ /**
365
+ * Work stealing: one shared submit region that any worker may
366
+ * claim from, private return lanes, and a pool-global pending registry. The
367
+ * endpoint that claims a task owns its response.
368
+ *
369
+ * Enabled by default for multi-worker thread pools unless a balancer or
370
+ * private-lane dispatcher was explicitly selected. One-worker pools,
371
+ * inliners, compiled/Porffor workers, process workers, and pools above the
372
+ * current 31-claimant protocol limit retain their existing transport.
373
+ * Explicitly requesting `true` with process workers is rejected because
374
+ * their transport uses separately mapped process-shared memory. Set `false`
375
+ * (or `KNITTING_STEAL=0`) to opt out; `KNITTING_STEAL=1` explicitly opts in
376
+ * and overrides a balancer/dispatcher selection.
377
+ */
378
+ steal?: boolean;
379
+ /**
380
+ * Lanes claimed per stealing handshake (a power of two, `slots / g >=
381
+ * workers + 1`). Defaults to the widest region the lane budget allows, which
382
+ * amortises arbitration best for cheap tasks.
383
+ *
384
+ * **A region is a batch.** For expensive tasks, a wide region lets one worker
385
+ * claim work the others could have run in parallel; set this to `1` (or a
386
+ * small value) when per-task cost dominates arbitration cost.
387
+ */
388
+ stealRegionLanes?: number;
351
389
  };
352
390
  type CreatePool = {
353
391
  /** Number of workers. Default: 1. */
@@ -73,7 +73,7 @@ export const workerMainLoop = async (startupData) => {
73
73
  installTerminationGuard();
74
74
  installUnhandledRejectionSilencer();
75
75
  installPerformanceNowGuard();
76
- const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, bufferReferenceReturn, permission, totalNumberOfThread, list, ids, names, at, } = startupData;
76
+ const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, bufferReferenceReturn, permission, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
77
77
  scrubWorkerDataSensitiveBuffers(startupData);
78
78
  assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
79
79
  const debugNamespaces = resolveDebugNamespaces(debug);
@@ -105,6 +105,11 @@ export const workerMainLoop = async (startupData) => {
105
105
  payloadConfig,
106
106
  textCompat: lock.textCompat,
107
107
  processBoundary: RUNTIME_IS_PROCESS_WORKER,
108
+ // Under stealing the request region is shared, so decode() becomes a
109
+ // region-Dekker claim against the other endpoints.
110
+ consumers: steal?.consumers,
111
+ consumerId: steal?.consumerId,
112
+ regionLanes: steal?.regionLanes,
108
113
  });
109
114
  const returnLockState = lock2({
110
115
  headers: returnLock.headers,
@@ -163,6 +168,7 @@ export const workerMainLoop = async (startupData) => {
163
168
  returnLock: returnLockState,
164
169
  borrowReturnedBufferReferences: bufferReferenceReturn === "borrow",
165
170
  hasAborted: abortSignals?.hasAborted,
171
+ stealing: steal !== undefined,
166
172
  });
167
173
  installBufferReferenceReleaseListener(releaseReturnedBufferReference);
168
174
  a_store(rxStatus, 0, 1);
@@ -175,6 +181,7 @@ export const workerMainLoop = async (startupData) => {
175
181
  pauseInNanoseconds: timers?.pauseNanoseconds,
176
182
  enqueueLock,
177
183
  write: () => hasCompleted() ? writeBatch(WRITE_MAX) : 0,
184
+ flushBeforeClaim: steal !== undefined,
178
185
  nativeWaitU32,
179
186
  useSharedMemoryWait: !(RUNTIME_IS_PROCESS_WORKER &&
180
187
  RUNTIME === "node" &&
@@ -250,15 +257,30 @@ export const workerMainLoop = async (startupData) => {
250
257
  pauseUntil(value, spinMicroseconds, parkMs);
251
258
  }
252
259
  : pauseUntil;
260
+ const flushBeforeClaim = steal !== undefined;
253
261
  const loop = () => {
254
262
  isInMacro = false;
255
263
  let progressed = true;
256
264
  let awaiting = 0;
257
265
  while (true) {
258
- progressed = _enqueueLock();
259
- if (_hasCompleted()) {
260
- if (_writeBatch(WRITE_MAX) > 0)
261
- progressed = true;
266
+ if (flushBeforeClaim) {
267
+ // Stealing only. Flush finished work before taking more on: a computed
268
+ // response should not wait behind a claim, and that claim can block on
269
+ // a peer withdrawing its intent. Measured to hurt the per-lane path,
270
+ // where a claim is a cheap private decode and picking work up promptly
271
+ // matters more, so the classic order is kept below.
272
+ if (_hasCompleted()) {
273
+ if (_writeBatch(WRITE_MAX) > 0)
274
+ progressed = true;
275
+ }
276
+ progressed = _enqueueLock() || progressed;
277
+ }
278
+ else {
279
+ progressed = _enqueueLock();
280
+ if (_hasCompleted()) {
281
+ if (_writeBatch(WRITE_MAX) > 0)
282
+ progressed = true;
283
+ }
262
284
  }
263
285
  _drainReturnReleases();
264
286
  if (_hasPending()) {