knitting 0.1.70 → 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 (45) hide show
  1. package/README.md +101 -10
  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/package.json +8 -3
  7. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  8. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  9. package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
  10. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  11. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  12. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
  14. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  15. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  16. package/src/api.js +58 -34
  17. package/src/connections/node-addons.js +11 -1
  18. package/src/debug/gate.js +1 -1
  19. package/src/debug/handle.d.ts +6 -1
  20. package/src/debug/handle.js +14 -6
  21. package/src/error.d.ts +9 -0
  22. package/src/error.js +16 -2
  23. package/src/memory/lock.d.ts +48 -6
  24. package/src/memory/lock.js +350 -142
  25. package/src/memory/payloadCodec.js +31 -11
  26. package/src/permission/protocol.d.ts +1 -0
  27. package/src/permission/protocol.js +8 -3
  28. package/src/runtime/dispatcher.d.ts +6 -1
  29. package/src/runtime/dispatcher.js +24 -8
  30. package/src/runtime/inline-executor.js +2 -1
  31. package/src/runtime/pool.d.ts +13 -4
  32. package/src/runtime/pool.js +107 -47
  33. package/src/runtime/process-worker.js +10 -1
  34. package/src/runtime/tx-queue.d.ts +2 -1
  35. package/src/runtime/tx-queue.js +11 -0
  36. package/src/runtime/worker-common.d.ts +7 -0
  37. package/src/runtime/worker-common.js +28 -2
  38. package/src/types.d.ts +35 -11
  39. package/src/worker/loop.js +17 -4
  40. package/src/worker/safety/index.d.ts +1 -1
  41. package/src/worker/safety/index.js +1 -1
  42. package/src/worker/safety/process.d.ts +2 -0
  43. package/src/worker/safety/process.js +8 -1
  44. package/src/worker/safety/startup.js +11 -6
  45. package/src/worker/timers.js +25 -3
@@ -40,7 +40,7 @@ const objectGetPrototypeOf = Object.getPrototypeOf;
40
40
  const objectHasOwn = Object.prototype.hasOwnProperty;
41
41
  const arrayIsArray = Array.isArray;
42
42
  const objectPrototype = Object.prototype;
43
- const UNSUPPORTED_OBJECT_DETAIL = "Unsupported object type. Allowed: plain object, array, Error, Date, Envelope, Buffer, ArrayBuffer, DataView, typed arrays, and registered external payloads. Serialize it yourself.";
43
+ const UNSUPPORTED_OBJECT_DETAIL = "Unsupported object type. Allowed: plain object, array, Error, Date, Envelope, Buffer, ArrayBuffer, DataView, Uint8Array, Int32Array, Float64Array, BigInt64Array, BigUint64Array, and registered external payloads. Serialize it yourself.";
44
44
  const ENVELOPE_PAYLOAD_DETAIL = "Envelope payload must be an ArrayBuffer, SharedArrayBuffer, " +
45
45
  "ProcessSharedBuffer, or BufferReference.";
46
46
  const ENVELOPE_HEADER_DETAIL = "Envelope header must be a JSON-like value or string.";
@@ -252,18 +252,23 @@ const toErrorPayload = (error) => {
252
252
  }
253
253
  return payload;
254
254
  };
255
- const parseErrorPayload = (raw) => {
256
- let parsed;
257
- try {
258
- parsed = parseJSON(raw);
255
+ // The shape `toErrorCause` writes for an Error cause. A plain-object cause
256
+ // with any other key is left as the object it was.
257
+ const isErrorPayload = (value) => {
258
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
259
+ return false;
259
260
  }
260
- catch {
261
- return new Error(raw);
261
+ const payload = value;
262
+ if (typeof payload.name !== "string" || typeof payload.message !== "string") {
263
+ return false;
262
264
  }
263
- if (parsed == null || typeof parsed !== "object") {
264
- return new Error(String(parsed));
265
+ for (const key in payload) {
266
+ if (key !== "name" && key !== "message" && key !== "stack" && key !== "cause")
267
+ return false;
265
268
  }
266
- const payload = parsed;
269
+ return true;
270
+ };
271
+ const reviveErrorPayload = (payload) => {
267
272
  const err = new Error(typeof payload.message === "string" ? payload.message : "");
268
273
  if (typeof payload.name === "string" && payload.name.length > 0) {
269
274
  err.name = payload.name;
@@ -276,10 +281,25 @@ const parseErrorPayload = (raw) => {
276
281
  }
277
282
  }
278
283
  if (objectHasOwn.call(payload, "cause")) {
279
- err.cause = payload.cause;
284
+ err.cause = isErrorPayload(payload.cause)
285
+ ? reviveErrorPayload(payload.cause)
286
+ : payload.cause;
280
287
  }
281
288
  return err;
282
289
  };
290
+ const parseErrorPayload = (raw) => {
291
+ let parsed;
292
+ try {
293
+ parsed = parseJSON(raw);
294
+ }
295
+ catch {
296
+ return new Error(raw);
297
+ }
298
+ if (parsed == null || typeof parsed !== "object") {
299
+ return new Error(String(parsed));
300
+ }
301
+ return reviveErrorPayload(parsed);
302
+ };
283
303
  const decodeBigIntBinary = (bytes) => {
284
304
  const sign = bytes[0];
285
305
  let value = 0n;
@@ -2,6 +2,7 @@ type PermissionPath = string | URL;
2
2
  type NodePermissionSettings = {
3
3
  allowWorker?: boolean;
4
4
  allowChildProcess?: boolean;
5
+ /** Allows task code to load Node native addons inside worker threads. */
5
6
  allowAddons?: boolean;
6
7
  allowFfi?: boolean;
7
8
  allowWasi?: boolean;
@@ -281,9 +281,14 @@ const resolveDenoLock = (input, cwd, home) => {
281
281
  };
282
282
  const resolveNodePermissionActivationFlag = () => {
283
283
  try {
284
- const raw = getNodeProcess()?.versions?.node;
285
- const major = Number.parseInt(String(raw).split(".", 1)[0] ?? "", 10);
286
- if (Number.isFinite(major) && major > 0 && major < 22) {
284
+ const version = String(getNodeProcess()?.versions?.node ?? "");
285
+ const [majorPart, minorPart] = version.split(".");
286
+ const major = Number.parseInt(majorPart ?? "", 10);
287
+ const minor = Number.parseInt(minorPart ?? "", 10);
288
+ const permissionIsStable = major > 23 ||
289
+ (major === 23 && minor >= 5) ||
290
+ (major === 22 && minor >= 13);
291
+ if (Number.isFinite(major) && major > 0 && !permissionIsStable) {
287
292
  return "--experimental-permission";
288
293
  }
289
294
  }
@@ -2,12 +2,17 @@ import { type MultiQueue } from "./tx-queue.js";
2
2
  import { type MainSignal } from "../ipc/transport/shared-memory.js";
3
3
  import { type RuntimeMessageChannelLike, type RuntimeMessagePortLike } from "../common/worker-runtime.js";
4
4
  import type { DispatcherSettings } from "../types.js";
5
- export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }: {
5
+ export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, wakeAfterPass, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }: {
6
6
  queue: MultiQueue;
7
7
  signalBox: MainSignal;
8
8
  channelHandler: ChannelHandler;
9
9
  dispatcherOptions?: DispatcherSettings;
10
10
  notifySignal?: () => void;
11
+ /**
12
+ * Runs at the end of every drain pass. Defaults to waking this lane's worker
13
+ * if it is parked; a shared queue supplies its own policy.
14
+ */
15
+ wakeAfterPass?: () => void;
11
16
  /** Workers live in other processes; disables only the atomic doorbell. */
12
17
  crossProcess?: boolean;
13
18
  /** A runtime-native completion ring that wakes the host event loop. */
@@ -7,9 +7,9 @@ const IMMEDIATE_PUMP = RUNTIME === "deno" || RUNTIME === "node"
7
7
  // Polling tolerates a wider free window; a doorbell should arm promptly.
8
8
  const POLL_STALL_FREE_LOOPS = 128;
9
9
  const DOORBELL_STALL_FREE_LOOPS = 1;
10
- export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }) => {
10
+ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, wakeAfterPass, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }) => {
11
+ const a_add = Atomics.add;
11
12
  const a_load = Atomics.load;
12
- const a_store = Atomics.store;
13
13
  const a_notify = Atomics.notify;
14
14
  const canNotifySignal = opView.buffer instanceof SharedArrayBuffer;
15
15
  const wakeSignal = notifySignal ??
@@ -17,6 +17,16 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
17
17
  if (canNotifySignal)
18
18
  a_notify(opView, 0, 1);
19
19
  });
20
+ // Bump rather than store a constant: the worker parks on the value it last
21
+ // read, so only a changed word makes a ring that lands before its wait count.
22
+ const wakeLane = () => {
23
+ a_add(opView, 0, 1);
24
+ wakeSignal();
25
+ };
26
+ const wakeIfParked = wakeAfterPass ?? (() => {
27
+ if (a_load(rxStatus, 0) === 0)
28
+ wakeLane();
29
+ });
20
30
  const notify = () => channelHandler.notify();
21
31
  // Atomics waiters are process-local, so cross-process workers cannot use this
22
32
  // doorbell.
@@ -149,12 +159,11 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
149
159
  do {
150
160
  check.rerun = false;
151
161
  txStatus[0] = 1;
152
- // Wake a parked worker whenever its receive status is clear. Pending
153
- // frames are not a reliable gate because the queue may have room.
154
- if (a_load(rxStatus, 0) === 0) {
155
- a_store(opView, 0, 1);
156
- wakeSignal();
157
- }
162
+ // Wake a parked worker whenever its receive status is clear, so it is
163
+ // up by the time the frames land. Pending frames are not a reliable gate
164
+ // because the queue may have room.
165
+ if (a_load(rxStatus, 0) === 0)
166
+ wakeLane();
158
167
  // Only completed frames count as progress for backoff purposes.
159
168
  let completed = false;
160
169
  let progressed = true;
@@ -170,6 +179,13 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
170
179
  progressed = true;
171
180
  }
172
181
  }
182
+ // The ring above was decided before the frames flushed here existed, and
183
+ // calls publish straight from enqueue without passing through this loop.
184
+ // A worker that parked in between saw neither the frames nor a ring and
185
+ // slept out its whole parkMs. Publishing ends in a seq-cst store and the
186
+ // worker re-checks after clearing rxStatus, so re-reading rxStatus after
187
+ // the pass means either the worker sees the frames or this sees it parked.
188
+ wakeIfParked();
173
189
  txStatus[0] = 0;
174
190
  if (!txIdle()) {
175
191
  // Queued frames indicate back-pressure, not progress; only a completion
@@ -1,4 +1,5 @@
1
1
  import { withResolvers } from "../common/with-resolvers.js";
2
+ import { KnittingError } from "../error.js";
2
3
  import RingQueue from "../ipc/tools/ring-queue.js";
3
4
  import { createRuntimeMessageChannel } from "../common/worker-runtime.js";
4
5
  // const objects, not `enum`s: Andromeda's Nova engine can't parse `enum`. Same
@@ -270,7 +271,7 @@ export const createInlineExecutor = ({ tasks, genTaskID, batchSize, }) => {
270
271
  if (stateByIndex[index] !== SlotStateMacro.Pending)
271
272
  continue;
272
273
  try {
273
- deferredByIndex[index]?.reject("Thread closed");
274
+ deferredByIndex[index]?.reject(new KnittingError("THREAD_CLOSED", "Thread closed"));
274
275
  }
275
276
  catch {
276
277
  }
@@ -10,7 +10,11 @@ import "../worker/loop.js";
10
10
  import { type PayloadBufferOptions } from "../memory/payload-config.js";
11
11
  /** Build the shared submit and private return buffers for a stealing pool. */
12
12
  export declare const resolveStealRegionLanes: (consumers: number) => number;
13
- /** Return the widest region allowed by the selected claim discipline. */
13
+ /**
14
+ * Return the widest region allowed by the selected claim discipline. Only
15
+ * Dekker needs a region per consumer; `ticket` has no regions at all and uses
16
+ * the width purely as a claim batch size.
17
+ */
14
18
  export declare const resolveMaxStealRegionLanes: (consumers: number, stealClaim?: StealClaimDiscipline) => number;
15
19
  /** Maximum claimants that leave the protocol's required spare region. */
16
20
  export declare const MAX_STEAL_CONSUMERS: number;
@@ -91,7 +95,8 @@ export declare const createStealPoolBuffers: ({ threads, payload, sharedArgs, re
91
95
  setPromiseHandler: (handler?: import("../memory/lock.js").PromisePayloadHandler) => void;
92
96
  }[];
93
97
  sharedQueue: {
94
- rejectAll: (reason: string) => void;
98
+ close: (reason: unknown) => void;
99
+ rejectAll: (reason: unknown) => void;
95
100
  hasPendingFrames: () => boolean;
96
101
  txIdle: () => boolean;
97
102
  completeFrame: () => number;
@@ -104,12 +109,12 @@ export declare const createStealPoolBuffers: ({ threads, payload, sharedArgs, re
104
109
  settlePromisePayload: (task: Task, isRejected: boolean, value: unknown) => boolean;
105
110
  };
106
111
  regionLanes: number;
107
- stealClaim: StealClaimDiscipline;
112
+ stealClaim: "dekker" | "ticket";
108
113
  processMemory: ProcessStealMemoryLayout;
109
114
  abortSignalSAB: SharedArrayBuffer | import("../types.js").SharedBufferRegion;
110
115
  abortSignalMax: number;
111
116
  };
112
- export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }: {
117
+ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, debugEpoch, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, requestedExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }: {
113
118
  list: string[];
114
119
  ids: number[];
115
120
  names: string[];
@@ -117,6 +122,7 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
117
122
  sab?: Sab;
118
123
  thread: number;
119
124
  debug?: DebugOptions;
125
+ debugEpoch?: number;
120
126
  hostDebug?: (message: string) => void;
121
127
  totalNumberOfThread: number;
122
128
  /**
@@ -128,6 +134,8 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
128
134
  source?: string;
129
135
  workerOptions?: WorkerSettings;
130
136
  workerExecArgv?: string[];
137
+ /** The caller's own `workerExecArgv`, before defaults and filtering. */
138
+ requestedExecArgv?: string[];
131
139
  permission?: WorkerData["permission"];
132
140
  host?: DispatcherSettings;
133
141
  payload?: PayloadBufferOptions;
@@ -171,6 +179,7 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
171
179
  processSharedMemoryBackings?: readonly ProcessSharedMemoryBacking[];
172
180
  dispatcherCheck?: DispatcherCheck;
173
181
  laneWake?: () => void;
182
+ laneAwake?: () => boolean;
174
183
  bindSend?: (fn: () => void) => void;
175
184
  bindCompletionWake?: (fn: () => void) => void;
176
185
  processCompletionDoorbell?: boolean;
@@ -1,10 +1,11 @@
1
1
  import { createHostTxQueue } from "./tx-queue.js";
2
2
  import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessStealMemoryLayout, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, processWorkerUsesIpc, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
3
- import { serializeWorkerBootstrapData, terminateWorkerQuietly, toWorkerCompatExecArgv, toWorkerSafeExecArgv, } from "./worker-common.js";
3
+ import { serializeWorkerBootstrapData, terminateWorkerQuietly, isNodePermissionExecFlag, toWorkerSafeExecArgv, warnDroppedWorkerExecArgv, } from "./worker-common.js";
4
4
  import { createSharedMemoryTransport, TRANSPORT_SIGNAL_BYTES, WORKER_STOP, } from "../ipc/transport/shared-memory.js";
5
5
  import { ChannelHandler, hostDispatcherLoop, } from "./dispatcher.js";
6
+ import { KnittingError } from "../error.js";
6
7
  import { createNodeCompletionDoorbell } from "./node-doorbell.js";
7
- import { HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
8
+ import { DEFAULT_STEAL_CLAIM, HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
8
9
  // Registers the payload codec before any lock is built.
9
10
  import "../memory/payloadCodec.js";
10
11
  import "../worker/loop.js";
@@ -25,8 +26,12 @@ const WORKER_STOP_ACK_TIMEOUT_MS = 50;
25
26
  // registry. Give its worker loop a chance to drain deferred producer pins
26
27
  // before the host falls back to terminate().
27
28
  const WORKER_STOP_NEEDS_ACK = RUNTIME === "deno" || RUNTIME === "node";
28
- // Keep idle workers self-healing if an Atomics.notify wake is missed.
29
- const DEFAULT_WORKER_PARK_MS = 1;
29
+ // The park timeout is only a safety net: the host wakes a parked thread worker
30
+ // after every publish, so a long park costs no latency and idles at ~0 CPU.
31
+ const DEFAULT_WORKER_PARK_MS = 1000;
32
+ // A process worker can only be rung across processes by a Node host waking a
33
+ // Node child; every other pairing rediscovers work on this timeout.
34
+ const DEFAULT_PROCESS_WORKER_PARK_MS = 1;
30
35
  const SINGLE_WORKER_SPIN_US = 50;
31
36
  const DEFAULT_ABORT_SIGNAL_CAPACITY = 258;
32
37
  const sanitizePositiveInteger = (value) => {
@@ -38,7 +43,10 @@ const sanitizePositiveInteger = (value) => {
38
43
  const resolveAbortSignalCapacity = (value) => sanitizePositiveInteger(value) ?? DEFAULT_ABORT_SIGNAL_CAPACITY;
39
44
  const abortSignalByteLength = (capacity) => Math.max(1, Math.ceil(capacity / 32)) * Uint32Array.BYTES_PER_ELEMENT;
40
45
  const withDefaultWorkerTimers = (options, workerCount) => {
41
- const parkMs = options?.timers?.parkMs ?? DEFAULT_WORKER_PARK_MS;
46
+ const parkMs = options?.timers?.parkMs ??
47
+ (options?.runtime === "process"
48
+ ? DEFAULT_PROCESS_WORKER_PARK_MS
49
+ : DEFAULT_WORKER_PARK_MS);
42
50
  // `??` and not `||`: an explicit `spinMicroseconds: 0` is the whole point of
43
51
  // the multi-worker default and must not fall through to it.
44
52
  const spinMicroseconds = options?.timers?.spinMicroseconds ??
@@ -67,10 +75,14 @@ export const resolveStealRegionLanes = (consumers) => {
67
75
  }
68
76
  throw new RangeError(`${consumers} stealing workers need more than ${LockBound.slots} lanes`);
69
77
  };
70
- /** Return the widest region allowed by the selected claim discipline. */
71
- export const resolveMaxStealRegionLanes = (consumers, stealClaim) => stealClaim !== undefined && stealClaim !== "dekker"
72
- ? LockBound.slots
73
- : resolveStealRegionLanes(consumers);
78
+ /**
79
+ * Return the widest region allowed by the selected claim discipline. Only
80
+ * Dekker needs a region per consumer; `ticket` has no regions at all and uses
81
+ * the width purely as a claim batch size.
82
+ */
83
+ export const resolveMaxStealRegionLanes = (consumers, stealClaim = DEFAULT_STEAL_CLAIM) => stealClaim === "dekker"
84
+ ? resolveStealRegionLanes(consumers)
85
+ : LockBound.slots;
74
86
  /** Maximum claimants that leave the protocol's required spare region. */
75
87
  export const MAX_STEAL_CONSUMERS = LockBound.slots - 1;
76
88
  export const createStealPoolBuffers = ({ threads, payload, sharedArgs, regionLanes, stealClaim, abortSignalCapacity, usesAbortSignal, processWorker, }) => {
@@ -97,7 +109,7 @@ export const createStealPoolBuffers = ({ threads, payload, sharedArgs, regionLan
97
109
  payload: payloadSab,
98
110
  }),
99
111
  });
100
- // Keep the default width valid for Dekker; wider CAS-mask regions are explicit.
112
+ // Keep the default width valid for Dekker; wider regions are explicit.
101
113
  const maxLanes = resolveMaxStealRegionLanes(threads, stealClaim);
102
114
  const lanes = regionLanes === undefined
103
115
  ? resolveStealRegionLanes(threads)
@@ -188,7 +200,7 @@ export const createStealPoolBuffers = ({ threads, payload, sharedArgs, regionLan
188
200
  (abortSignalSAB === undefined ? undefined : resolvedAbortSignalCapacity),
189
201
  };
190
202
  };
191
- export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }) => {
203
+ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, debugEpoch, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, requestedExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }) => {
192
204
  if (workerOptions?.runtime === "compiled") {
193
205
  return spawnCompiledWorkerContext({
194
206
  list,
@@ -380,10 +392,20 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
380
392
  const ownsChannel = sharedChannelHandler === undefined &&
381
393
  stealPool === undefined;
382
394
  const ownChannel = sharedChannelHandler ?? new ChannelHandler();
395
+ const threadCanLoadNodeAddons = permission?.enabled !== true ||
396
+ permission.unsafe === true || permission.node.allowAddons === true;
397
+ const useNativeNodeDoorbell = RUNTIME === "node" &&
398
+ !useProcessWorkerRuntime && host?.doorbell !== false &&
399
+ host?.nativeDoorbell === true && threadCanLoadNodeAddons;
400
+ if (RUNTIME === "node" && !useProcessWorkerRuntime &&
401
+ host?.doorbell !== false && host?.nativeDoorbell === true &&
402
+ !threadCanLoadNodeAddons) {
403
+ hostDebug?.("native Node doorbell disabled by worker permissions; using the " +
404
+ "portable wake path (set permission.node.allowAddons=true to enable)");
405
+ }
383
406
  // Create the Node doorbell before bootstrapping the worker.
384
407
  let nodeCompletionWake;
385
- const nodeCompletionDoorbell = !useProcessWorkerRuntime &&
386
- host?.doorbell !== false && host?.nativeDoorbell === true
408
+ const nodeCompletionDoorbell = useNativeNodeDoorbell
387
409
  ? createNodeCompletionDoorbell(() => nodeCompletionWake?.())
388
410
  : undefined;
389
411
  const canUseNodeCompletionDoorbell = nodeCompletionDoorbell !== undefined;
@@ -438,6 +460,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
438
460
  at,
439
461
  thread,
440
462
  debug,
463
+ debugEpoch,
441
464
  workerOptions: resolvedWorkerOptions,
442
465
  totalNumberOfThread,
443
466
  startAt: signalBox.startAt,
@@ -476,6 +499,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
476
499
  const withExecArgv = workerExecArgv && workerExecArgv.length > 0
477
500
  ? { ...baseWorkerOptions, execArgv: workerExecArgv }
478
501
  : baseWorkerOptions;
502
+ const requiredNodePermissionFlags = RUNTIME === "node" &&
503
+ permission?.enabled === true &&
504
+ permission.unsafe !== true
505
+ ? permission.node.flags.filter(isNodePermissionExecFlag)
506
+ : [];
479
507
  if (processWorkerMemory !== undefined) {
480
508
  worker = spawnProcessWorker({
481
509
  workerUrl,
@@ -488,43 +516,59 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
488
516
  }
489
517
  else if (HAS_NODE_WORKER_THREADS) {
490
518
  try {
491
- worker = new WorkerCtor(workerUrl, withExecArgv);
492
- }
493
- catch (error) {
494
- if (error?.code === "ERR_WORKER_INVALID_EXEC_ARGV") {
495
- const fallbackExecArgv = toWorkerSafeExecArgv(withExecArgv.execArgv);
496
- if (fallbackExecArgv && fallbackExecArgv.length > 0) {
497
- try {
498
- worker = new WorkerCtor(workerUrl, { ...baseWorkerOptions, execArgv: fallbackExecArgv });
519
+ // The flags the worker actually started with.
520
+ let appliedExecArgv;
521
+ try {
522
+ worker = new WorkerCtor(workerUrl, withExecArgv);
523
+ appliedExecArgv = withExecArgv.execArgv;
524
+ }
525
+ catch (error) {
526
+ if (error?.code === "ERR_WORKER_INVALID_EXEC_ARGV") {
527
+ const fallbackExecArgv = toWorkerSafeExecArgv(withExecArgv.execArgv);
528
+ const missingPermissionFlags = requiredNodePermissionFlags.filter((flag) => !fallbackExecArgv?.includes(flag));
529
+ if (missingPermissionFlags.length > 0) {
530
+ throw new Error("Node rejected the permission flags required by Knitting's " +
531
+ `thread worker policy (${missingPermissionFlags.join(", ")}). ` +
532
+ "Refusing to retry without them.");
499
533
  }
500
- catch (fallbackError) {
501
- if (fallbackError?.code ===
502
- "ERR_WORKER_INVALID_EXEC_ARGV") {
503
- const compatExecArgv = toWorkerCompatExecArgv(fallbackExecArgv);
504
- if (compatExecArgv && compatExecArgv.length > 0) {
505
- try {
506
- worker = new WorkerCtor(workerUrl, { ...baseWorkerOptions, execArgv: compatExecArgv });
507
- }
508
- catch {
509
- worker = new WorkerCtor(workerUrl, baseWorkerOptions);
510
- }
534
+ if (fallbackExecArgv && fallbackExecArgv.length > 0) {
535
+ try {
536
+ worker = new WorkerCtor(workerUrl, { ...baseWorkerOptions, execArgv: fallbackExecArgv });
537
+ appliedExecArgv = fallbackExecArgv;
538
+ }
539
+ catch (fallbackError) {
540
+ if (requiredNodePermissionFlags.length > 0) {
541
+ throw new Error("Node could not start a thread worker with Knitting's " +
542
+ "permission policy. Refusing to start it without those " +
543
+ `permissions. ${String(fallbackError)}`);
511
544
  }
512
- else {
545
+ if (fallbackError?.code ===
546
+ "ERR_WORKER_INVALID_EXEC_ARGV") {
513
547
  worker = new WorkerCtor(workerUrl, baseWorkerOptions);
514
548
  }
549
+ else {
550
+ throw fallbackError;
551
+ }
515
552
  }
516
- else {
517
- throw fallbackError;
518
- }
553
+ }
554
+ else if (requiredNodePermissionFlags.length > 0) {
555
+ throw new Error("Node rejected the permission flags required by Knitting's " +
556
+ "thread worker policy. Refusing to start the worker without " +
557
+ "those permissions.");
558
+ }
559
+ else {
560
+ worker = new WorkerCtor(workerUrl, baseWorkerOptions);
519
561
  }
520
562
  }
521
563
  else {
522
- worker = new WorkerCtor(workerUrl, baseWorkerOptions);
564
+ throw error;
523
565
  }
524
566
  }
525
- else {
526
- throw error;
527
- }
567
+ warnDroppedWorkerExecArgv(requestedExecArgv, appliedExecArgv);
568
+ }
569
+ catch (error) {
570
+ nodeCompletionDoorbell?.close();
571
+ throw error;
528
572
  }
529
573
  }
530
574
  else {
@@ -553,11 +597,24 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
553
597
  deactivateStealConsumer();
554
598
  }
555
599
  };
556
- const markWorkerClosed = (reason) => {
600
+ const markWorkerClosed = (code, reason, cause) => {
557
601
  if (closedReason)
558
602
  return;
559
603
  closedReason = reason;
560
- rejectAll(reason);
604
+ const error = new KnittingError(code, reason, cause);
605
+ if (stealPool !== undefined && stealPool.stealClaim !== "ticket") {
606
+ // Dekker's shared queue outlives one consumer: the surviving workers
607
+ // keep claiming from it, so only the calls in flight are rejected.
608
+ rejectAll(error);
609
+ }
610
+ else {
611
+ // A lane's own queue has no other consumer, so a call accepted after
612
+ // this point would stay pending forever: close it and reject them all.
613
+ // Under ticket stealing a claimed ticket may be lost at any point
614
+ // between CAS and retirement, so the whole shared registry closes and
615
+ // task-id/slot reuse is prohibited.
616
+ queue.close(error);
617
+ }
561
618
  channelHandler?.close();
562
619
  };
563
620
  const onWorkerMessage = (message) => {
@@ -567,12 +624,12 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
567
624
  }
568
625
  if (!isWorkerFatalMessage(message))
569
626
  return;
570
- markWorkerClosed(`Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`);
627
+ markWorkerClosed("WORKER_STARTUP_FAILED", `Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`);
571
628
  terminateFailedWorker();
572
629
  };
573
630
  const onWorkerError = (error) => {
574
631
  const message = String(error?.message ?? error);
575
- markWorkerClosed(`Worker crashed: ${message}`);
632
+ markWorkerClosed("WORKER_CRASHED", `Worker crashed: ${message}`, error);
576
633
  terminateFailedWorker();
577
634
  };
578
635
  const nodeWorker = worker;
@@ -585,11 +642,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
585
642
  if (closedReason !== undefined)
586
643
  return;
587
644
  if (stopRequested) {
588
- markWorkerClosed("Thread closed");
645
+ markWorkerClosed("THREAD_CLOSED", "Thread closed");
589
646
  return;
590
647
  }
591
648
  const normalized = typeof code === "number" ? code : -1;
592
- markWorkerClosed(`Worker exited with code ${normalized}`);
649
+ markWorkerClosed("WORKER_EXITED", `Worker exited with code ${normalized}`);
593
650
  });
594
651
  }
595
652
  else {
@@ -651,7 +708,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
651
708
  call,
652
709
  requestStop: requestWorkerStop,
653
710
  kills: async () => {
654
- markWorkerClosed("Thread closed");
711
+ markWorkerClosed("THREAD_CLOSED", "Thread closed");
655
712
  // Process workers must exit before their shared memory is unmapped.
656
713
  const awaitExit = processWorkerMemory !== undefined;
657
714
  const termination = terminateWorkerQuietly(worker, awaitExit);
@@ -665,6 +722,9 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
665
722
  laneWake: sharedChannelHandler !== undefined || stealPool !== undefined
666
723
  ? laneWake
667
724
  : undefined,
725
+ laneAwake: stealPool !== undefined
726
+ ? () => a_load(signalBox.rxStatus, 0) !== 0
727
+ : undefined,
668
728
  bindSend: sharedChannelHandler !== undefined || stealPool !== undefined
669
729
  ? ((fn) => void (dispatchSend = fn))
670
730
  : undefined,
@@ -681,7 +681,16 @@ const spawnNodeHostedProcessWorker = ({ workerUrl, bootPayload, memory, processR
681
681
  child.on("message", events.emitMessage);
682
682
  queueMicrotask(() => child.send?.(bootPayload));
683
683
  }
684
- 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
+ });
685
694
  let resolveExit = () => { };
686
695
  const exited = new Promise((resolve) => {
687
696
  resolveExit = resolve;
@@ -26,7 +26,8 @@ type CreateHostTxQueueArgs = {
26
26
  now?: () => number;
27
27
  };
28
28
  export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abortSignals, now, }: CreateHostTxQueueArgs): {
29
- rejectAll: (reason: string) => void;
29
+ close: (reason: unknown) => void;
30
+ rejectAll: (reason: unknown) => void;
30
31
  hasPendingFrames: () => boolean;
31
32
  txIdle: () => boolean;
32
33
  completeFrame: () => number;
@@ -34,6 +34,7 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abo
34
34
  const queuePush = (task) => queue.push(task);
35
35
  const { publish, flushPending, hasPendingFrames, getPendingFrameCount, getPendingPromiseCount, resetPendingState, } = lock;
36
36
  let inUsed = 0 | 0;
37
+ let closedReason;
37
38
  const resetSignal = abortSignals?.resetSignal;
38
39
  const nowTime = now ?? p_now;
39
40
  const onReturnResolved = (task) => {
@@ -182,11 +183,19 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abo
182
183
  resetPendingState();
183
184
  inUsed = 0 | 0;
184
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
+ };
185
193
  const flushToWorker = () => flushPending();
186
194
  const enqueueKnown = (task) => {
187
195
  return publish(task);
188
196
  };
189
197
  return {
198
+ close,
190
199
  rejectAll,
191
200
  hasPendingFrames,
192
201
  txIdle,
@@ -200,6 +209,8 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abo
200
209
  const USE_SIGNAL = abortSignal !== undefined &&
201
210
  abortSignals !== undefined;
202
211
  return (rawArgs) => {
212
+ if (closedReason !== undefined)
213
+ return Promise.reject(closedReason);
203
214
  if (inUsed === queue.length) {
204
215
  const newSize = inUsed + 32;
205
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>;