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
@@ -109,6 +109,13 @@ const isWindowsRuntimeHost = () => {
109
109
  return denoOs === "windows";
110
110
  return globalThis.process?.platform === "win32";
111
111
  };
112
+ const processWorkerNeedsNamedMemory = (sharedMemory) =>
113
+ // `Deno.Command` does not reliably carry an anonymous memfd/shm fd into a
114
+ // child as stdin: Linux can hand the child a non-mappable descriptor and
115
+ // macOS cannot always reopen the host fd through `/dev/fd`. Named mappings
116
+ // are reopened by the worker instead, which works across Deno's supported
117
+ // hosts and process-worker runtimes.
118
+ sharedMemory.mode === "named" || isWindowsRuntimeHost() || RUNTIME === "deno";
112
119
  let processWorkerMemoryNameCounter = 0;
113
120
  const makeProcessWorkerMemoryName = (thread, prefix = "kpw") => {
114
121
  const processId = globalThis.process?.pid ??
@@ -142,7 +149,7 @@ export const createProcessWorkerMemoryLayout = ({ signalBytes, abortBytes, paylo
142
149
  const requestPayloadSlice = carpet.take("requestPayload", payloadBytes);
143
150
  const returnPayloadSlice = carpet.take("returnPayload", payloadBytes);
144
151
  const primitives = getProcessWorkerSharedMemoryPrimitives();
145
- const forceNamed = sharedMemory.mode === "named" || isWindowsRuntimeHost();
152
+ const forceNamed = processWorkerNeedsNamedMemory(sharedMemory);
146
153
  const mapping = primitives.createSharedMemory(forceNamed
147
154
  ? {
148
155
  size: carpet.byteLength(),
@@ -215,7 +222,7 @@ export const createProcessStealMemoryLayout = ({ threads, signalBytes, abortByte
215
222
  returnPayload: carpet.take(`returnPayload-${thread}`, payloadBytes),
216
223
  }));
217
224
  const primitives = getProcessWorkerSharedMemoryPrimitives();
218
- const forceNamed = sharedMemory.mode === "named" || isWindowsRuntimeHost();
225
+ const forceNamed = processWorkerNeedsNamedMemory(sharedMemory);
219
226
  const mapping = primitives.createSharedMemory(forceNamed
220
227
  ? {
221
228
  size: carpet.byteLength(),
@@ -546,6 +553,19 @@ export const createProcessWorkerNativeSignalNotifier = ({ processRuntime, signal
546
553
  return undefined;
547
554
  }
548
555
  };
556
+ /**
557
+ * Whether this host/child pair has the Node-compatible IPC channel that the
558
+ * process completion-doorbell prototype uses. Deno process workers currently
559
+ * boot through environment data and do not have that channel.
560
+ */
561
+ export const processWorkerUsesIpc = ({ processRuntime, commandPrefix, }) => {
562
+ if (processRuntime === undefined || commandPrefix !== undefined)
563
+ return false;
564
+ if (RUNTIME === "node") {
565
+ return processRuntime === "node" || processRuntime === "bun";
566
+ }
567
+ return RUNTIME === "bun" && processRuntime === "bun";
568
+ };
549
569
  const createProcessWorkerEventHub = () => {
550
570
  const messageHandlers = [];
551
571
  const errorHandlers = [];
@@ -661,7 +681,16 @@ const spawnNodeHostedProcessWorker = ({ workerUrl, bootPayload, memory, processR
661
681
  child.on("message", events.emitMessage);
662
682
  queueMicrotask(() => child.send?.(bootPayload));
663
683
  }
664
- child.on("error", events.emitError);
684
+ child.on("error", (error) => {
685
+ if (error?.code !== "ENOENT") {
686
+ events.emitError(error);
687
+ return;
688
+ }
689
+ // A bare "spawn deno ENOENT" does not say which option chose the binary.
690
+ events.emitError(new Error(`process worker binary "${command}" was not found (spawn ENOENT). ` +
691
+ `Install ${processRuntime} or set worker.processRuntime to "node", ` +
692
+ `"deno" or "bun"; it defaults to "deno".`, { cause: error }));
693
+ });
665
694
  let resolveExit = () => { };
666
695
  const exited = new Promise((resolve) => {
667
696
  resolveExit = resolve;
@@ -2,7 +2,6 @@ import "../memory/payloadCodec.js";
2
2
  import { type Lock2, type Task } from "../memory/lock.js";
3
3
  import type { AbortSignalOption, TaskTimeout } from "../types.js";
4
4
  import { type SignalAbortStore } from "../shared/abortSignal.js";
5
- import { type BufferReferenceReturnHooks } from "../connections/buffer-reference.js";
6
5
  type RawArguments = unknown;
7
6
  type FunctionID = number;
8
7
  type QueueTask = Task;
@@ -23,19 +22,18 @@ type CreateHostTxQueueArgs = {
23
22
  * When omitted, only `returnLock` is drained (the classic one-lane shape).
24
23
  */
25
24
  extraReturnLocks?: readonly Lock2[];
26
- releaseBufferReferenceReturn?: ((token: bigint) => void) | BufferReferenceReturnHooks;
27
25
  abortSignals?: Pick<SignalAbortStore, "getSignal" | "setSignal" | "resetSignal" | "closeNow">;
28
26
  now?: () => number;
29
27
  };
30
- type ReturnHooks = ((token: bigint) => void) | BufferReferenceReturnHooks;
31
- export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, releaseBufferReferenceReturn, abortSignals, now, }: CreateHostTxQueueArgs): {
32
- rejectAll: (reason: string) => void;
28
+ export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abortSignals, now, }: CreateHostTxQueueArgs): {
29
+ close: (reason: unknown) => void;
30
+ rejectAll: (reason: unknown) => void;
33
31
  hasPendingFrames: () => boolean;
34
32
  txIdle: () => boolean;
35
33
  completeFrame: () => number;
36
34
  waitForCompletion: (onWake: () => void, timeoutMs?: number) => boolean;
35
+ armCompletionNotifier: () => boolean;
37
36
  setCompletionWaiterArmed: (armed: boolean) => void;
38
- setReturnHooks: (lane: number, hooks: ReturnHooks | undefined) => void;
39
37
  enqueue: (functionID: FunctionID, timeout?: TaskTimeout, abortSignal?: AbortSignalOption) => (rawArgs: RawArguments) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
40
38
  flushToWorker: () => boolean;
41
39
  enqueueKnown: (task: QueueTask) => boolean;
@@ -4,7 +4,6 @@ import "../memory/payloadCodec.js";
4
4
  import { makeTask, resetTaskLocalFlags, runTaskFinalizers, TaskIndex, } from "../memory/lock.js";
5
5
  import { withResolvers } from "../common/with-resolvers.js";
6
6
  import { AbortSignalPoolExhausted, OneShotDeferred, } from "../shared/abortSignal.js";
7
- import { withBufferReferenceReturnReleaser, } from "../connections/buffer-reference.js";
8
7
  const SLOT_INDEX_MASK = 31;
9
8
  const SLOT_META_MASK = 0x07ffffff;
10
9
  const SLOT_META_SHIFT = 5;
@@ -14,7 +13,7 @@ const FUNCTION_META_SHIFT = 16;
14
13
  const ABORT_SIGNAL_META_OFFSET = 1;
15
14
  const NO_ABORT_SIGNAL = -1;
16
15
  const p_now = performance.now.bind(performance);
17
- export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, releaseBufferReferenceReturn, abortSignals, now, }) {
16
+ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abortSignals, now, }) {
18
17
  const PLACE_HOLDER = (_) => {
19
18
  throw ("UNREACHABLE FROM PLACE HOLDER (main)");
20
19
  };
@@ -35,6 +34,7 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
35
34
  const queuePush = (task) => queue.push(task);
36
35
  const { publish, flushPending, hasPendingFrames, getPendingFrameCount, getPendingPromiseCount, resetPendingState, } = lock;
37
36
  let inUsed = 0 | 0;
37
+ let closedReason;
38
38
  const resetSignal = abortSignals?.resetSignal;
39
39
  const nowTime = now ?? p_now;
40
40
  const onReturnResolved = (task) => {
@@ -66,33 +66,18 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
66
66
  ].map((each) => typeof each.setHostWaiterArmed === "function"
67
67
  ? each.setHostWaiterArmed
68
68
  : (_armed) => { });
69
- // A stealing queue drains one private return lock per worker. Borrowed
70
- // BufferReferences must therefore be claimed with the hooks belonging to the
71
- // worker that produced that particular return. The hooks are bound after the
72
- // worker context exists, while the pool-global queue is built before workers
73
- // spawn, so keep one mutable hook slot per return lane.
74
- const returnHooks = new Array(returnResolvers.length);
75
- if (releaseBufferReferenceReturn !== undefined) {
76
- returnHooks[0] = releaseBufferReferenceReturn;
77
- }
78
- const resolveReturnAt = (index) => {
79
- const resolve = returnResolvers[index];
80
- const hooks = returnHooks[index];
81
- return hooks === undefined
82
- ? resolve()
83
- : withBufferReferenceReturnReleaser(hooks, resolve);
84
- };
85
- // Preserve the original one-lane fast path exactly: no wrapper, array lookup,
86
- // or hook branch is paid by the default one-worker transport. Mutable
87
- // per-return-lane hooks are needed only by a multi-worker stealing queue.
69
+ const returnNativeArmers = [
70
+ returnLock,
71
+ ...(extraReturnLocks ?? []),
72
+ ].map((each) => typeof each.armHostNotifier === "function"
73
+ ? each.armHostNotifier
74
+ : () => false);
88
75
  const completeFrame = returnResolvers.length === 1
89
- ? releaseBufferReferenceReturn === undefined
90
- ? returnResolvers[0]
91
- : () => withBufferReferenceReturnReleaser(releaseBufferReferenceReturn, returnResolvers[0])
76
+ ? returnResolvers[0]
92
77
  : () => {
93
78
  let resolved = 0 | 0;
94
79
  for (let i = 0; i < returnResolvers.length; i++) {
95
- resolved = (resolved + resolveReturnAt(i)) | 0;
80
+ resolved = (resolved + returnResolvers[i]()) | 0;
96
81
  }
97
82
  return resolved;
98
83
  };
@@ -103,19 +88,35 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
103
88
  const completionArmed = new Uint8Array(returnWaiters.length);
104
89
  let completionWake;
105
90
  let completionGeneration = 0 | 0;
91
+ const completionGenerations = new Int32Array(returnWaiters.length);
92
+ // Reuse one callback per lane and capture the generation before re-arming.
93
+ const completionCallbacks = returnWaiters.map((_, index) => () => {
94
+ if (completionArmed[index] === 0)
95
+ return;
96
+ const generation = completionGenerations[index];
97
+ completionArmed[index] = 0;
98
+ returnArmers[index](false);
99
+ if (generation !== completionGeneration)
100
+ return;
101
+ completionWake?.();
102
+ });
106
103
  const setCompletionWaiterArmed = (armed) => {
107
104
  for (const setArmed of returnArmers)
108
105
  setArmed(armed);
109
106
  };
110
107
  const waitForCompletion = (onWake, timeoutMs) => {
111
108
  completionWake = onWake;
112
- // A persistent waiter may still be pending after a send preempted the
113
- // dispatcher. Re-arm its shared gate before relying on that waiter again.
114
- setCompletionWaiterArmed(true);
115
109
  let supported = true;
116
110
  for (let index = 0; index < returnWaiters.length; index++) {
117
- if (completionArmed[index] !== 0)
111
+ if (completionArmed[index] !== 0) {
112
+ // Re-arm persistent waiters with an atomic check; a result may have
113
+ // arrived while the gate was off, and setting ARMED alone can strand it.
114
+ if (!returnNativeArmers[index]()) {
115
+ onWake();
116
+ return true;
117
+ }
118
118
  continue;
119
+ }
119
120
  let wait;
120
121
  try {
121
122
  wait = returnWaiters[index](timeoutMs);
@@ -128,20 +129,14 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
128
129
  break;
129
130
  }
130
131
  completionArmed[index] = 1;
131
- const generation = completionGeneration;
132
- const wakeLane = () => {
133
- if (completionArmed[index] === 0)
134
- return;
135
- completionArmed[index] = 0;
136
- returnArmers[index](false);
137
- if (generation !== completionGeneration)
138
- return;
139
- completionWake?.();
140
- };
141
- if (!wait.async)
132
+ completionGenerations[index] = completionGeneration;
133
+ const wakeLane = completionCallbacks[index];
134
+ if (!wait.async) {
142
135
  wakeLane();
143
- else
144
- Promise.resolve(wait.value).then(wakeLane, wakeLane);
136
+ // A synchronous wake may disarm the whole queue, so stop here.
137
+ return true;
138
+ }
139
+ Promise.resolve(wait.value).then(wakeLane, wakeLane);
145
140
  }
146
141
  if (!supported) {
147
142
  completionWake = undefined;
@@ -150,6 +145,21 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
150
145
  }
151
146
  return supported;
152
147
  };
148
+ /**
149
+ * Native callbacks (Deno's threadSafe UnsafeCallback) cannot use waitAsync,
150
+ * but they use the same shared arm word. Each lane is armed and checked for
151
+ * a publication in one operation; a false return means the dispatcher must
152
+ * drain again rather than sleep waiting for a ring that already happened.
153
+ */
154
+ const armCompletionNotifier = () => {
155
+ for (const arm of returnNativeArmers) {
156
+ if (arm())
157
+ continue;
158
+ setCompletionWaiterArmed(false);
159
+ return false;
160
+ }
161
+ return true;
162
+ };
153
163
  const hasActiveTasks = () => {
154
164
  const count = (inUsed - getPendingPromiseCount()) | 0;
155
165
  return count > 0;
@@ -173,29 +183,34 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
173
183
  resetPendingState();
174
184
  inUsed = 0 | 0;
175
185
  };
186
+ /** Reject everything in flight, and every later call, with `reason`. */
187
+ const close = (reason) => {
188
+ if (closedReason !== undefined)
189
+ return;
190
+ closedReason = reason;
191
+ rejectAll(reason);
192
+ };
176
193
  const flushToWorker = () => flushPending();
177
194
  const enqueueKnown = (task) => {
178
195
  return publish(task);
179
196
  };
180
197
  return {
198
+ close,
181
199
  rejectAll,
182
200
  hasPendingFrames,
183
201
  txIdle,
184
202
  completeFrame,
185
203
  waitForCompletion,
204
+ armCompletionNotifier,
186
205
  setCompletionWaiterArmed,
187
- setReturnHooks: (lane, hooks) => {
188
- if (!Number.isInteger(lane) || lane < 0 || lane >= returnHooks.length) {
189
- throw new RangeError(`return lane ${lane} out of range`);
190
- }
191
- returnHooks[lane] = hooks;
192
- },
193
206
  enqueue: (functionID, timeout, abortSignal) => {
194
207
  const HAS_TIMER = timeout !== undefined;
195
208
  const functionIDMasked = functionID & FUNCTION_ID_MASK;
196
209
  const USE_SIGNAL = abortSignal !== undefined &&
197
210
  abortSignals !== undefined;
198
211
  return (rawArgs) => {
212
+ if (closedReason !== undefined)
213
+ return Promise.reject(closedReason);
199
214
  if (inUsed === queue.length) {
200
215
  const newSize = inUsed + 32;
201
216
  let current = queue.length;
@@ -7,8 +7,15 @@ export type SpawnedWorker = {
7
7
  export type NodeWorkerLike = {
8
8
  on?: (event: "error" | "exit" | "message", listener: (...args: unknown[]) => void) => void;
9
9
  };
10
+ export declare const isNodePermissionExecFlag: (flag: string) => boolean;
10
11
  export declare const toWorkerSafeExecArgv: (flags: string[] | undefined) => string[] | undefined;
12
+ /** Preserve process-worker runtime flags while replacing inherited permissions. */
11
13
  export declare const toWorkerCompatExecArgv: (flags: string[] | undefined) => string[] | undefined;
14
+ /**
15
+ * Warn once per flag set when a `workerExecArgv` flag the caller asked for did
16
+ * not reach the worker thread, instead of dropping it silently.
17
+ */
18
+ export declare const warnDroppedWorkerExecArgv: (requested: string[] | undefined, applied: string[] | undefined) => void;
12
19
  export declare const serializeWorkerBootstrapData: (options: WorkerSettings) => WorkerSettings;
13
20
  /** Terminate a worker, optionally waiting for it to exit. */
14
21
  export declare const terminateWorkerQuietly: (worker: SpawnedWorker, awaitExit?: boolean) => Promise<void>;
@@ -6,24 +6,29 @@ const execFlagKey = (flag) => flag.split("=", 1)[0];
6
6
  const NODE_PERMISSION_EXEC_FLAGS = new Set([
7
7
  "--permission",
8
8
  "--experimental-permission",
9
+ "--experimental-config-file",
10
+ "--experimental-default-config-file",
9
11
  "--allow-fs-read",
10
12
  "--allow-fs-write",
13
+ "--allow-fs-vfs",
11
14
  "--allow-worker",
12
15
  "--allow-child-process",
16
+ "--allow-env",
13
17
  "--allow-net",
14
18
  "--allow-addons",
15
19
  "--allow-ffi",
16
20
  "--allow-wasi",
21
+ "--allow-inspector",
22
+ "--allow-openssl-store",
17
23
  ]);
18
24
  const NODE_WORKER_SAFE_EXEC_FLAGS = new Set([
19
25
  "--experimental-ffi",
20
26
  "--experimental-transform-types",
21
- "--expose-gc",
22
27
  "--no-warnings",
23
28
  ...NODE_PERMISSION_EXEC_FLAGS,
24
29
  ]);
25
30
  const isNodeWorkerSafeExecFlag = (flag) => NODE_WORKER_SAFE_EXEC_FLAGS.has(execFlagKey(flag));
26
- const isNodePermissionExecFlag = (flag) => NODE_PERMISSION_EXEC_FLAGS.has(execFlagKey(flag));
31
+ export const isNodePermissionExecFlag = (flag) => NODE_PERMISSION_EXEC_FLAGS.has(execFlagKey(flag));
27
32
  export const toWorkerSafeExecArgv = (flags) => {
28
33
  if (!flags || flags.length === 0)
29
34
  return undefined;
@@ -40,6 +45,7 @@ export const toWorkerSafeExecArgv = (flags) => {
40
45
  }
41
46
  return deduped;
42
47
  };
48
+ /** Preserve process-worker runtime flags while replacing inherited permissions. */
43
49
  export const toWorkerCompatExecArgv = (flags) => {
44
50
  const safe = toWorkerSafeExecArgv(flags);
45
51
  if (!safe || safe.length === 0)
@@ -47,6 +53,26 @@ export const toWorkerCompatExecArgv = (flags) => {
47
53
  const compat = safe.filter((flag) => !isNodePermissionExecFlag(flag));
48
54
  return compat.length > 0 ? compat : undefined;
49
55
  };
56
+ const droppedExecArgvWarnings = new Set();
57
+ /**
58
+ * Warn once per flag set when a `workerExecArgv` flag the caller asked for did
59
+ * not reach the worker thread, instead of dropping it silently.
60
+ */
61
+ export const warnDroppedWorkerExecArgv = (requested, applied) => {
62
+ if (!requested || requested.length === 0)
63
+ return;
64
+ const kept = new Set(applied ?? []);
65
+ const dropped = requested.filter((flag) => !kept.has(flag));
66
+ if (dropped.length === 0)
67
+ return;
68
+ const key = dropped.join(" ");
69
+ if (droppedExecArgvWarnings.has(key))
70
+ return;
71
+ droppedExecArgvWarnings.add(key);
72
+ console.warn(`knitting: workerExecArgv ${dropped.join(", ")} cannot be applied to a ` +
73
+ `worker thread and was dropped. V8 and process-wide flags must be ` +
74
+ `passed to the host process instead.`);
75
+ };
50
76
  const isPlainRecord = (value) => {
51
77
  if (value === null || typeof value !== "object")
52
78
  return false;
package/src/types.d.ts CHANGED
@@ -14,7 +14,7 @@ interface WorkerContext {
14
14
  txIdle(): boolean;
15
15
  call(descriptor: WorkerCall): WorkerInvoke;
16
16
  /** Ask the worker to leave its dispatch loop before termination. */
17
- requestStop?(): Promise<void>;
17
+ requestStop?(): Promise<boolean>;
18
18
  kills(): Promise<void>;
19
19
  }
20
20
  type CreateContext = WorkerContext;
@@ -28,36 +28,43 @@ type WorkerData = {
28
28
  thread: number;
29
29
  totalNumberOfThread: number;
30
30
  debug?: DebugOptions;
31
+ /**
32
+ * Host's debug zero as `performance.timeOrigin + performance.now()` (Unix
33
+ * epoch ms), so worker debug clocks line up with the host's. Set only when
34
+ * debug is requested.
35
+ */
36
+ debugEpoch?: number;
31
37
  startAt: number;
32
38
  workerOptions?: WorkerSettings;
33
39
  at: number[];
34
40
  lock: LockBuffers;
35
41
  returnLock: LockBuffers;
36
42
  payloadConfig?: PayloadBufferOptions;
37
- bufferReferenceReturn?: "copy" | "borrow";
43
+ /** Enable borrowed returns for this worker's return lane. */
44
+ sharedReturn?: boolean;
38
45
  permission?: ResolvedPermissionProtocol;
39
46
  /** Whether this host can arm an async completion waiter on the return lock. */
40
47
  notifyOnHostPublish?: boolean;
41
- /**
42
- * Work stealing. When present, `lock` is a submit region shared by every
43
- * worker and this worker claims from it as consumer `consumerId` of
44
- * `consumers`, rather than owning a private request lane. `returnLock` stays
45
- * private — the endpoint that claims a task owns its response.
46
- */
48
+ /** Process-local IPC channel can carry coalesced completion doorbells. */
49
+ processCompletionDoorbell?: boolean;
50
+ /** Process-local Deno callback pointer for waking the host completion pump. */
51
+ denoCompletionDoorbell?: bigint;
52
+ /** Process-local Node uv_async handle for waking the host completion pump. */
53
+ nodeCompletionDoorbell?: bigint;
54
+ /** Shared submit lock and private return lock for a stealing worker. */
47
55
  steal?: {
48
56
  consumers: number;
49
57
  consumerId: number;
50
58
  regionLanes: number;
59
+ /** Claim discipline; see `DispatcherSettings.stealClaim`. */
60
+ claim?: "dekker" | "ticket";
51
61
  };
52
62
  };
53
63
  type UnsafeOptions = {
54
- /**
55
- * Experimental `BufferReference` return lifetime.
56
- *
57
- * `"copy"` is safe after worker release. `"borrow"` skips the Deno/Bun copy,
58
- * but must be released before producer shutdown and must not outlive its ref.
59
- */
60
- BufferReferenceReturn?: "copy" | "borrow";
64
+ /** Enable borrowed large returns and the zero-copy `sharedBytes()` path. */
65
+ SharedBytes?: boolean;
66
+ /** Enable borrowed large arguments and `pool.sharedArgBytes()`. */
67
+ SharedArgs?: boolean;
61
68
  };
62
69
  type LockBuffers = {
63
70
  headers: SharedBufferSource;
@@ -172,17 +179,26 @@ type SingleTaskPool<A extends TaskInput = Args, B extends Args = Args, AS extend
172
179
  shutdown: (delayMs?: number) => Promise<void>;
173
180
  /** Starts shutdown at scope exit. Use `shutdown()` when you must await it. */
174
181
  [Symbol.dispose]: () => void;
182
+ /** `await using` awaits worker teardown at scope exit. */
183
+ [Symbol.asyncDispose]: () => Promise<void>;
175
184
  };
176
185
  type Pool<T extends Record<string, TaskLike<any> | TaskFunctionLike>> = {
177
186
  /** Await worker teardown now. `using` disposes at scope exit without awaiting. */
178
187
  shutdown: (delayMs?: number) => Promise<void>;
179
188
  /** Starts shutdown at scope exit. Use `shutdown()` when you must await it. */
180
189
  [Symbol.dispose]: () => void;
190
+ /** `await using` awaits worker teardown at scope exit. */
191
+ [Symbol.asyncDispose]: () => Promise<void>;
181
192
  /**
182
193
  * Typed task callers. Each call accepts the task input or a native Promise.
183
194
  * Thrown errors/rejections reject here as Error objects with cause chains.
184
195
  */
185
196
  call: FunctionMapType<T>;
197
+ /**
198
+ * Allocate an argument buffer. Borrowed buffers are recycled after 32 later
199
+ * large arguments, so the receiving task must consume them before awaiting.
200
+ */
201
+ sharedArgBytes: (byteLength: number) => Uint8Array;
186
202
  };
187
203
  type ReturnFixed<A extends TaskInput = undefined, B extends Args = undefined, AS extends AbortSignalOption = undefined> = FixPoint<A, B, AS> & SecondPart & {
188
204
  createPool: (options?: CreatePool) => SingleTaskPool<A, B, AS>;
@@ -208,8 +224,8 @@ type Balancer = BalancerStrategy | {
208
224
  */
209
225
  strategy?: BalancerStrategy;
210
226
  };
211
- /** Debug namespaces for host setup, worker state, imports, globals, and lifecycle. */
212
- type DebugNamespace = "host" | "globals" | "signals" | "imports" | "lifecycle";
227
+ /** Debug namespaces for host setup, worker state, imports, globals, lifecycle, and steal claims. */
228
+ type DebugNamespace = "host" | "globals" | "signals" | "imports" | "lifecycle" | "steal";
213
229
  type DebugFlags = {
214
230
  [Namespace in DebugNamespace]?: boolean;
215
231
  };
@@ -309,10 +325,10 @@ type WorkerSettings = {
309
325
  /**
310
326
  * How process workers discover their shared-memory control channel.
311
327
  *
312
- * "inherit" keeps the POSIX fd-inheritance path and is the default outside
313
- * Windows. "named" creates an OS-named shared-memory object that wrappers
314
- * such as containers can reopen by name when they share the same IPC
315
- * namespace.
328
+ * "inherit" keeps the POSIX fd-inheritance path. "named" creates an
329
+ * OS-named shared-memory object that wrappers such as containers can reopen
330
+ * by name when they share the same IPC namespace. Deno-hosted pools and
331
+ * Windows use a named mapping automatically.
316
332
  */
317
333
  processSharedMemory?: ProcessSharedMemoryMode | ProcessSharedMemorySettings;
318
334
  timers?: WorkerTimers;
@@ -334,7 +350,10 @@ type WorkerTimers = {
334
350
  */
335
351
  spinMicroseconds?: number;
336
352
  /**
337
- * Atomics.wait timeout when parked (milliseconds).
353
+ * Atomics.wait timeout when parked (milliseconds). Defaults to 1000 for
354
+ * thread workers, which the host wakes on every publish, so the timeout is
355
+ * only a safety net; 1 for process workers, most of which only rediscover
356
+ * work when it expires.
338
357
  */
339
358
  parkMs?: number;
340
359
  /**
@@ -344,47 +363,19 @@ type WorkerTimers = {
344
363
  pauseNanoseconds?: number;
345
364
  };
346
365
  type DispatcherSettings = {
347
- /**
348
- * How many immediate notify loops before the dispatcher stops re-arming the
349
- * pump for free.
350
- *
351
- * The default depends on what the dispatcher escalates *to*. Polling
352
- * escalates to a `setTimeout` ladder costing ~1.1ms even at delay 0, so it
353
- * defaults to 128 — escalating is expensive and the wide window also batches
354
- * completions. A doorbell escalates to `Atomics.waitAsync` at roughly the
355
- * price of one hop, so pools that have one default to 1. Pools without a
356
- * doorbell — Deno, or `doorbell: false` — keep 128.
357
- *
358
- * Setting this explicitly opts out of that coupling for every pool shape.
359
- */
366
+ /** Number of immediate notify loops before backoff starts. */
360
367
  stallFreeLoops?: number;
361
368
  /**
362
369
  * Max backoff delay (milliseconds).
363
370
  */
364
371
  maxBackoffMs?: number;
365
372
  /**
366
- * Replace idle completion polling with an `Atomics.waitAsync` doorbell when
367
- * the host runtime supports it.
368
- *
369
- * Defaults to enabled on Node and Bun at any worker count, where it costs
370
- * 1.6-3.9x less host CPU per completed call. Under HTTP load, where the host
371
- * has real work of its own, that converts to +13% to +36% throughput.
372
- *
373
- * Turn it off for a pool that oversubscribes its machine. The doorbell only
374
- * progresses when the host gets scheduled, so once workers occupy every core
375
- * a wake must preempt one: measured +5% to +12.6% rps while workers+host fit
376
- * within the cores, -22% to -32% once they do not. That is not gated
377
- * automatically because the core count cannot be probed portably.
378
- *
379
- * Forced off, overriding an explicit `true`, where a doorbell cannot work:
380
- * on Deno, whose `waitAsync` does not wake an idle event loop, and for
381
- * process workers, which live in another process and so cannot ring a host
382
- * waiter at all — V8 keeps its Atomics waiter list per isolate, which is why
383
- * they wake through a native futex addon instead. Compiled (Porffor) workers
384
- * never reach this path: they reject `host` outright and use pipes rather
385
- * than shared memory.
373
+ * Use a completion doorbell when supported. Deno uses a thread-safe FFI
374
+ * callback; process and compiled workers use their own completion transport.
386
375
  */
387
376
  doorbell?: boolean;
377
+ /** Use Node's native `uv_async_t` completion bridge when available. */
378
+ nativeDoorbell?: boolean;
388
379
  /**
389
380
  * Host dispatcher topology.
390
381
  * - `"per-thread"`: each worker owns its dispatcher and macro channel.
@@ -398,29 +389,20 @@ type DispatcherSettings = {
398
389
  * `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
399
390
  */
400
391
  dispatcher?: "per-thread" | "serial-channel";
401
- /**
402
- * Work stealing: one shared submit region that any worker may
403
- * claim from, private return lanes, and a pool-global pending registry. The
404
- * endpoint that claims a task owns its response.
405
- *
406
- * Enabled by default for compatible multi-worker thread and process pools
407
- * unless a balancer or private-lane dispatcher was explicitly selected.
408
- * One-worker pools, inliners, compiled/Porffor workers, and pools above the
409
- * current 31-claimant protocol limit retain their existing transport. Set
410
- * `false` (or `KNITTING_STEAL=0`) to opt out; `KNITTING_STEAL=1` explicitly
411
- * opts in and overrides a balancer/dispatcher selection.
412
- */
392
+ /** Use a shared submit region with private return lanes. */
413
393
  steal?: boolean;
414
394
  /**
415
- * Lanes claimed per stealing handshake (a power of two, `slots / g >=
416
- * workers + 1`). Defaults to the widest region the lane budget allows, which
417
- * amortises arbitration best for cheap tasks.
418
- *
419
- * **A region is a batch.** For expensive tasks, a wide region lets one worker
420
- * claim work the others could have run in parallel; set this to `1` (or a
421
- * small value) when per-task cost dominates arbitration cost.
395
+ * Number of lanes claimed per handshake. Use smaller regions for expensive
396
+ * tasks; Dekker requires at least one spare region per live consumer.
422
397
  */
423
398
  stealRegionLanes?: number;
399
+ /**
400
+ * Publication-ordered tickets (`"ticket"`, the default) or Dekker regions
401
+ * (`"dekker"`). Unrecognised values are rejected rather than defaulted, so a
402
+ * removed discipline such as `cas-mask` fails at pool creation. Ticket pools
403
+ * reject pending and future calls on worker failure.
404
+ */
405
+ stealClaim?: "dekker" | "ticket";
424
406
  };
425
407
  type CreatePool = {
426
408
  /** Number of workers. Default: 1. */
@@ -448,14 +430,20 @@ type CreatePool = {
448
430
  */
449
431
  host?: DispatcherSettings;
450
432
  /**
451
- * Extra Node.js execArgv flags for worker threads (e.g. ["--expose-gc"]).
452
- * Defaults to process.execArgv plus "--expose-gc" when allowed.
433
+ * Extra Node.js execArgv flags for worker threads (e.g. ["--no-warnings"]).
434
+ * Defaults to compatible flags from process.execArgv. Node permission flags
435
+ * are replaced by the resolved `permission` policy.
436
+ *
437
+ * Node rejects V8 and process-wide flags (`--expose-gc`,
438
+ * `--max-old-space-size`, ...). Unsupported caller flags are dropped with a
439
+ * warning. Permission flags are never dropped to start an unpermissioned
440
+ * thread worker; pool creation fails if Node cannot apply them.
453
441
  */
454
442
  workerExecArgv?: string[];
455
443
  /**
456
444
  * Runtime permission protocol.
457
- * Omit to use strict defaults with `allowImport: true`; worker console is
458
- * quiet unless `permission: { console: true }`.
445
+ * Omit to use strict defaults with `allowImport: true`. `console` is
446
+ * accepted but not enforced: worker console output is always forwarded.
459
447
  *
460
448
  * Task code cannot terminate the host: process/Deno exit APIs are blocked.
461
449
  * Use `"strict"` (default for object mode) or `"unsafe"`.