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