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.
- package/README.md +623 -342
- package/knitting.browser.d.ts +3 -1
- package/knitting.browser.js +1 -1
- package/knitting.d.ts +3 -1
- package/knitting.js +2 -1
- package/map.md +0 -6
- package/package.json +10 -5
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/scripts/build-native-addons.ts +5 -0
- package/shared-memory.d.ts +3 -0
- package/shared-memory.js +3 -0
- package/src/api.js +158 -71
- package/src/common/with-resolvers.js +2 -5
- package/src/common/worker-runtime.d.ts +7 -0
- package/src/common/worker-runtime.js +7 -0
- package/src/connections/buffer-reference.d.ts +10 -36
- package/src/connections/buffer-reference.js +15 -170
- package/src/connections/node-addons.d.ts +1 -1
- package/src/connections/node-addons.js +11 -1
- package/src/connections/shared-array-buffer-payload.d.ts +7 -0
- package/src/connections/shared-array-buffer-payload.js +27 -11
- package/src/debug/gate.js +1 -1
- package/src/debug/handle.d.ts +6 -1
- package/src/debug/handle.js +14 -6
- package/src/error.d.ts +9 -0
- package/src/error.js +16 -2
- package/src/knitting_buffer_pointer.cc +57 -2
- package/src/knitting_doorbell.cc +220 -0
- package/src/memory/knitting-body.d.ts +44 -0
- package/src/memory/knitting-body.js +51 -0
- package/src/memory/knitting-buffer-http.d.ts +116 -0
- package/src/memory/knitting-buffer-http.js +255 -0
- package/src/memory/knitting-buffer.d.ts +250 -0
- package/src/memory/knitting-buffer.js +695 -0
- package/src/memory/lazy-region-registry.d.ts +83 -0
- package/src/memory/lazy-region-registry.js +355 -0
- package/src/memory/lock.d.ts +80 -15
- package/src/memory/lock.js +473 -139
- package/src/memory/payloadCodec.d.ts +18 -2
- package/src/memory/payloadCodec.js +340 -76
- package/src/memory/regionRegistry.d.ts +6 -0
- package/src/memory/regionRegistry.js +125 -240
- package/src/memory/shared-buffer-io.d.ts +7 -0
- package/src/memory/shared-buffer-io.js +34 -8
- package/src/permission/protocol.d.ts +1 -0
- package/src/permission/protocol.js +8 -3
- package/src/runtime/deno-doorbell.d.ts +26 -0
- package/src/runtime/deno-doorbell.js +117 -0
- package/src/runtime/dispatcher.d.ts +13 -6
- package/src/runtime/dispatcher.js +101 -63
- package/src/runtime/host-arg-arena.d.ts +3 -0
- package/src/runtime/host-arg-arena.js +16 -0
- package/src/runtime/inline-executor.js +2 -1
- package/src/runtime/node-doorbell.d.ts +14 -0
- package/src/runtime/node-doorbell.js +84 -0
- package/src/runtime/pool.d.ts +30 -15
- package/src/runtime/pool.js +199 -151
- package/src/runtime/process-worker.d.ts +9 -0
- package/src/runtime/process-worker.js +32 -3
- package/src/runtime/tx-queue.d.ts +4 -6
- package/src/runtime/tx-queue.js +63 -48
- package/src/runtime/worker-common.d.ts +7 -0
- package/src/runtime/worker-common.js +28 -2
- package/src/types.d.ts +66 -78
- package/src/worker/loop.js +95 -60
- package/src/worker/rx-queue.d.ts +2 -3
- package/src/worker/rx-queue.js +34 -40
- package/src/worker/safety/index.d.ts +1 -1
- package/src/worker/safety/index.js +1 -1
- package/src/worker/safety/process.d.ts +2 -0
- package/src/worker/safety/process.js +8 -1
- package/src/worker/safety/startup.js +11 -6
- package/src/worker/shared-return.d.ts +9 -0
- package/src/worker/shared-return.js +22 -0
- package/src/worker/task-loader.js +1 -2
- package/src/worker/timers.d.ts +2 -6
- package/src/worker/timers.js +39 -22
- package/unsafe.d.ts +2 -1
- 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
|
|
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
|
|
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",
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
rejectAll: (reason:
|
|
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;
|
package/src/runtime/tx-queue.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
?
|
|
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 +
|
|
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
|
-
|
|
132
|
-
const wakeLane =
|
|
133
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
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
|
-
*
|
|
367
|
-
*
|
|
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
|
-
*
|
|
416
|
-
*
|
|
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. ["--
|
|
452
|
-
* Defaults to process.execArgv
|
|
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
|
|
458
|
-
*
|
|
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"`.
|