knitting 0.1.60 → 0.1.62
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 +94 -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 +136 -25
- package/src/common/runtime.d.ts +2 -1
- package/src/common/runtime.js +8 -1
- package/src/common/task-source.d.ts +9 -0
- package/src/common/task-source.js +15 -0
- package/src/memory/lock.d.ts +37 -1
- package/src/memory/lock.js +262 -5
- 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.d.ts +11 -8
- package/src/runtime/dispatcher.js +163 -21
- package/src/runtime/inline-executor.js +6 -1
- package/src/runtime/pool.d.ts +127 -4
- package/src/runtime/pool.js +262 -73
- package/src/runtime/process-worker.d.ts +24 -12
- package/src/runtime/process-worker.js +185 -115
- package/src/runtime/tx-queue.d.ts +17 -1
- package/src/runtime/tx-queue.js +120 -15
- package/src/runtime/worker-common.d.ts +13 -0
- package/src/runtime/worker-common.js +103 -0
- package/src/types.d.ts +75 -4
- package/src/worker/loop.js +34 -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
|
@@ -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
|
];
|
|
@@ -236,6 +191,106 @@ export const createProcessWorkerMemoryLayout = ({ signalBytes, abortBytes, paylo
|
|
|
236
191
|
},
|
|
237
192
|
};
|
|
238
193
|
};
|
|
194
|
+
export const createProcessStealMemoryLayout = ({ threads, signalBytes, abortBytes, abortSignalMax, payloadBytes, sharedMemory, }) => {
|
|
195
|
+
if (!Number.isInteger(threads) || threads < 2) {
|
|
196
|
+
throw new RangeError("process stealing needs at least two workers");
|
|
197
|
+
}
|
|
198
|
+
const carpet = createByteCarpet();
|
|
199
|
+
const requestLockSlice = carpet.take("requestLockSector", LOCK_SECTOR_BYTE_LENGTH);
|
|
200
|
+
const requestHeadersSlice = carpet.take("requestHeaders", getHeaderBlockByteLength({
|
|
201
|
+
slotCount: LockBound.slots,
|
|
202
|
+
slotStrideU32: HEADER_SLOT_STRIDE_U32,
|
|
203
|
+
alignTo: 64,
|
|
204
|
+
}));
|
|
205
|
+
const abortSignalsSlice = carpet.take("abortSignals", abortBytes);
|
|
206
|
+
const requestPayloadSlice = carpet.take("requestPayload", payloadBytes);
|
|
207
|
+
const workerSlices = Array.from({ length: threads }, (_, thread) => ({
|
|
208
|
+
signal: carpet.take(`signals-${thread}`, signalBytes),
|
|
209
|
+
returnLock: carpet.take(`returnLockSector-${thread}`, LOCK_SECTOR_BYTE_LENGTH),
|
|
210
|
+
returnHeaders: carpet.take(`returnHeaders-${thread}`, getHeaderBlockByteLength({
|
|
211
|
+
slotCount: LockBound.slots,
|
|
212
|
+
slotStrideU32: HEADER_SLOT_STRIDE_U32,
|
|
213
|
+
alignTo: 64,
|
|
214
|
+
})),
|
|
215
|
+
returnPayload: carpet.take(`returnPayload-${thread}`, payloadBytes),
|
|
216
|
+
}));
|
|
217
|
+
const primitives = getProcessWorkerSharedMemoryPrimitives();
|
|
218
|
+
const forceNamed = sharedMemory.mode === "named" || isWindowsRuntimeHost();
|
|
219
|
+
const mapping = primitives.createSharedMemory(forceNamed
|
|
220
|
+
? {
|
|
221
|
+
size: carpet.byteLength(),
|
|
222
|
+
mode: "create",
|
|
223
|
+
name: makeProcessWorkerMemoryName(0, sharedMemory.namePrefix),
|
|
224
|
+
}
|
|
225
|
+
: { size: carpet.byteLength() });
|
|
226
|
+
const descriptor = FileDescriptor.fromMapping(mapping);
|
|
227
|
+
if (isWindowsRuntimeHost() && descriptor.name === undefined) {
|
|
228
|
+
throw new Error("Windows process worker shared memory must use a named mapping");
|
|
229
|
+
}
|
|
230
|
+
if (sharedMemory.mode === "named" && descriptor.name === undefined) {
|
|
231
|
+
throw new Error("processSharedMemory mode named needs a named shared-memory backend");
|
|
232
|
+
}
|
|
233
|
+
const buffer = mapping.buffer;
|
|
234
|
+
const bind = (slice) => makeSharedBufferRegion(buffer, slice.byteOffset, slice.byteLength);
|
|
235
|
+
const abortSignals = bind(abortSignalsSlice);
|
|
236
|
+
const requestLock = {
|
|
237
|
+
headers: bind(requestHeadersSlice),
|
|
238
|
+
headerSlotStrideU32: HEADER_SLOT_STRIDE_U32,
|
|
239
|
+
lockSector: bind(requestLockSlice),
|
|
240
|
+
payloadSector: bind(requestLockSlice),
|
|
241
|
+
};
|
|
242
|
+
// Each worker calls cleanup, so named-memory unlink happens only after every
|
|
243
|
+
// endpoint has released its view. The per-worker mapping wrappers below do
|
|
244
|
+
// not expose close(); the host views remain attached until host exit because
|
|
245
|
+
// Bun's external ArrayBuffer finalizer can race an explicit munmap.
|
|
246
|
+
let remaining = threads;
|
|
247
|
+
let cleaned = false;
|
|
248
|
+
const cleanupSharedMapping = () => {
|
|
249
|
+
if (cleaned)
|
|
250
|
+
return;
|
|
251
|
+
remaining--;
|
|
252
|
+
if (remaining > 0)
|
|
253
|
+
return;
|
|
254
|
+
cleaned = true;
|
|
255
|
+
const name = descriptor.name;
|
|
256
|
+
if (name !== undefined && sharedMemory.unlinkOnShutdown) {
|
|
257
|
+
primitives.unlinkSharedMemory?.(name);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const workerMapping = {
|
|
261
|
+
...mapping,
|
|
262
|
+
close: undefined,
|
|
263
|
+
};
|
|
264
|
+
const workers = workerSlices.map((slices) => {
|
|
265
|
+
const returnLock = {
|
|
266
|
+
headers: bind(slices.returnHeaders),
|
|
267
|
+
headerSlotStrideU32: HEADER_SLOT_STRIDE_U32,
|
|
268
|
+
lockSector: bind(slices.returnLock),
|
|
269
|
+
payloadSector: bind(slices.returnLock),
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
mapping: workerMapping,
|
|
273
|
+
descriptor,
|
|
274
|
+
controlLayout: {
|
|
275
|
+
controlSAB: buffer,
|
|
276
|
+
signals: bind(slices.signal),
|
|
277
|
+
abortSignals,
|
|
278
|
+
lock: requestLock,
|
|
279
|
+
returnLock,
|
|
280
|
+
slices: carpet.slices,
|
|
281
|
+
},
|
|
282
|
+
lockPayload: bind(requestPayloadSlice),
|
|
283
|
+
returnPayload: bind(slices.returnPayload),
|
|
284
|
+
cleanup: cleanupSharedMapping,
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
mapping,
|
|
289
|
+
descriptor,
|
|
290
|
+
workers,
|
|
291
|
+
abortSignalMax,
|
|
292
|
+
};
|
|
293
|
+
};
|
|
239
294
|
const toChildProcessSharedBufferMetadata = (source, descriptor) => {
|
|
240
295
|
const region = toSharedBufferRegion(source);
|
|
241
296
|
const metadata = descriptor.toMetadata();
|
|
@@ -254,49 +309,6 @@ const toProcessWorkerWireLockBuffers = (lock, descriptor) => ({
|
|
|
254
309
|
payload: toChildProcessSharedBufferMetadata(lock.payload, descriptor),
|
|
255
310
|
payloadSector: toChildProcessSharedBufferMetadata(lock.payloadSector, descriptor),
|
|
256
311
|
});
|
|
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
312
|
export const toProcessWorkerBootPayload = (workerData, memory) => ({
|
|
301
313
|
version: RUNTIME_PROCESS_WORKER_BOOT_VERSION,
|
|
302
314
|
workerData: {
|
|
@@ -417,10 +429,62 @@ const processWorkerNodeBinary = (commandPrefix) => {
|
|
|
417
429
|
: undefined) ??
|
|
418
430
|
DEFAULT_NODE_BINARY;
|
|
419
431
|
};
|
|
420
|
-
const
|
|
432
|
+
const processWorkerNodeMajorCache = new Map();
|
|
433
|
+
const resolveProcessWorkerNodeMajor = (commandPrefix) => {
|
|
434
|
+
const nodeProcess = getNodeProcess();
|
|
435
|
+
if (commandPrefix === undefined &&
|
|
436
|
+
nodeProcess !== undefined &&
|
|
437
|
+
nodeProcess?.env?.NODE_BINARY === undefined &&
|
|
438
|
+
RUNTIME === "node") {
|
|
439
|
+
return parseNodeMajorVersion(nodeProcess.versions?.node);
|
|
440
|
+
}
|
|
441
|
+
const nodeBinary = processWorkerNodeBinary(commandPrefix);
|
|
442
|
+
const cacheKey = JSON.stringify([commandPrefix ?? null, nodeBinary]);
|
|
443
|
+
if (processWorkerNodeMajorCache.has(cacheKey)) {
|
|
444
|
+
return processWorkerNodeMajorCache.get(cacheKey);
|
|
445
|
+
}
|
|
446
|
+
let major;
|
|
447
|
+
try {
|
|
448
|
+
const childProcess = getNodeBuiltinModule("node:child_process");
|
|
449
|
+
if (typeof childProcess?.spawnSync === "function") {
|
|
450
|
+
const command = commandPrefix?.[0] ?? nodeBinary;
|
|
451
|
+
const args = commandPrefix === undefined
|
|
452
|
+
? ["--version"]
|
|
453
|
+
: [...commandPrefix.slice(1), nodeBinary, "--version"];
|
|
454
|
+
const probe = childProcess.spawnSync(command, args, {
|
|
455
|
+
encoding: "utf8",
|
|
456
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
457
|
+
timeout: 2_000,
|
|
458
|
+
windowsHide: true,
|
|
459
|
+
});
|
|
460
|
+
if (probe.status === 0) {
|
|
461
|
+
major = parseNodeMajorVersion(probe.stdout);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// Unknown targets conservatively use the Node 22/24 addon transport.
|
|
467
|
+
}
|
|
468
|
+
processWorkerNodeMajorCache.set(cacheKey, major);
|
|
469
|
+
return major;
|
|
470
|
+
};
|
|
471
|
+
export const readProcessWorkerNodeMajor = (options) => resolveProcessWorkerNodeMajor(readProcessWorkerCommandPrefix(options));
|
|
472
|
+
const nodeProcessWorkerFlagIsSupported = (flag, nodeMajor) => {
|
|
473
|
+
const key = flag.split("=", 1)[0];
|
|
474
|
+
if (key === "--experimental-ffi" || key === "--allow-ffi") {
|
|
475
|
+
return nodeProcessWorkerUsesFfi(nodeMajor);
|
|
476
|
+
}
|
|
477
|
+
if (key === "--allow-net") {
|
|
478
|
+
return nodeMajor !== undefined && nodeMajor >= 25;
|
|
479
|
+
}
|
|
480
|
+
return true;
|
|
481
|
+
};
|
|
482
|
+
const processWorkerNodeExecArgv = (permission, nodeMajor) => {
|
|
421
483
|
const out = [];
|
|
422
484
|
const seen = new Set();
|
|
423
485
|
const add = (flag) => {
|
|
486
|
+
if (!nodeProcessWorkerFlagIsSupported(flag, nodeMajor))
|
|
487
|
+
return;
|
|
424
488
|
if (seen.has(flag))
|
|
425
489
|
return;
|
|
426
490
|
seen.add(flag);
|
|
@@ -429,16 +493,17 @@ const processWorkerNodeExecArgv = (permission) => {
|
|
|
429
493
|
for (const flag of toWorkerCompatExecArgv(getNodeProcess()?.execArgv) ?? []) {
|
|
430
494
|
add(flag);
|
|
431
495
|
}
|
|
432
|
-
for (const flag of nodeProcessWorkerInternalExecArgv())
|
|
496
|
+
for (const flag of nodeProcessWorkerInternalExecArgv(nodeMajor))
|
|
433
497
|
add(flag);
|
|
434
498
|
if (permission?.enabled === true && permission.unsafe !== true) {
|
|
435
499
|
for (const flag of permission.node.flags)
|
|
436
500
|
add(flag);
|
|
437
|
-
if (permission.netAll &&
|
|
501
|
+
if (permission.netAll && nodeMajor !== undefined && nodeMajor >= 25) {
|
|
438
502
|
add("--allow-net");
|
|
503
|
+
}
|
|
439
504
|
// Node process workers need one native transport capability regardless of
|
|
440
505
|
// task permissions: addons on Node 22/24, node:ffi on supported Node 26+ LTS.
|
|
441
|
-
add(nodeProcessWorkerUsesFfi() ? "--allow-ffi" : "--allow-addons");
|
|
506
|
+
add(nodeProcessWorkerUsesFfi(nodeMajor) ? "--allow-ffi" : "--allow-addons");
|
|
442
507
|
}
|
|
443
508
|
return out;
|
|
444
509
|
};
|
|
@@ -454,9 +519,10 @@ const processWorkerCommand = ({ processRuntime, workerUrl, bun, deno, commandPre
|
|
|
454
519
|
];
|
|
455
520
|
}
|
|
456
521
|
else if (processRuntime === "node") {
|
|
522
|
+
const nodeMajor = resolveProcessWorkerNodeMajor(commandPrefix);
|
|
457
523
|
command = [
|
|
458
524
|
processWorkerNodeBinary(commandPrefix),
|
|
459
|
-
...processWorkerNodeExecArgv(permission),
|
|
525
|
+
...processWorkerNodeExecArgv(permission, nodeMajor),
|
|
460
526
|
workerPath,
|
|
461
527
|
];
|
|
462
528
|
}
|
|
@@ -596,9 +662,23 @@ const spawnNodeHostedProcessWorker = ({ workerUrl, bootPayload, memory, processR
|
|
|
596
662
|
queueMicrotask(() => child.send?.(bootPayload));
|
|
597
663
|
}
|
|
598
664
|
child.on("error", events.emitError);
|
|
599
|
-
|
|
665
|
+
let resolveExit = () => { };
|
|
666
|
+
const exited = new Promise((resolve) => {
|
|
667
|
+
resolveExit = resolve;
|
|
668
|
+
});
|
|
669
|
+
child.on("exit", (code) => {
|
|
670
|
+
resolveExit();
|
|
671
|
+
events.emitExit(code ?? -1);
|
|
672
|
+
});
|
|
600
673
|
return {
|
|
601
|
-
terminate: () =>
|
|
674
|
+
terminate: () => {
|
|
675
|
+
try {
|
|
676
|
+
child.kill();
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
}
|
|
680
|
+
return exited;
|
|
681
|
+
},
|
|
602
682
|
unref: () => child.unref?.(),
|
|
603
683
|
on: events.on,
|
|
604
684
|
};
|
|
@@ -687,16 +767,6 @@ export const spawnProcessWorker = (options) => {
|
|
|
687
767
|
throw new Error("process worker runtime is only available in Node, Deno, or Bun");
|
|
688
768
|
}
|
|
689
769
|
};
|
|
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
770
|
export const cleanupProcessWorkerMemoryQuietly = (memory) => {
|
|
701
771
|
try {
|
|
702
772
|
memory?.cleanup();
|
|
@@ -11,15 +11,31 @@ 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
|
+
waitForCompletion: (onWake: () => void, timeoutMs?: number) => boolean;
|
|
37
|
+
setCompletionWaiterArmed: (armed: boolean) => void;
|
|
38
|
+
setReturnHooks: (lane: number, hooks: ReturnHooks | undefined) => void;
|
|
23
39
|
enqueue: (functionID: FunctionID, timeout?: TaskTimeout, abortSignal?: AbortSignalOption) => (rawArgs: RawArguments) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
|
|
24
40
|
flushToWorker: () => boolean;
|
|
25
41
|
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,119 @@ 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
|
+
const returnWaiters = [
|
|
58
|
+
returnLock,
|
|
59
|
+
...(extraReturnLocks ?? []),
|
|
60
|
+
].map((each) => typeof each.waitForHostChange === "function"
|
|
61
|
+
? each.waitForHostChange
|
|
62
|
+
: () => undefined);
|
|
63
|
+
const returnArmers = [
|
|
64
|
+
returnLock,
|
|
65
|
+
...(extraReturnLocks ?? []),
|
|
66
|
+
].map((each) => typeof each.setHostWaiterArmed === "function"
|
|
67
|
+
? each.setHostWaiterArmed
|
|
68
|
+
: (_armed) => { });
|
|
69
|
+
// A stealing queue drains one private return lock per worker. Borrowed
|
|
70
|
+
// BufferReferences must therefore be claimed with the hooks belonging to the
|
|
71
|
+
// worker that produced that particular return. The hooks are bound after the
|
|
72
|
+
// worker context exists, while the pool-global queue is built before workers
|
|
73
|
+
// spawn, so keep one mutable hook slot per return lane.
|
|
74
|
+
const returnHooks = new Array(returnResolvers.length);
|
|
75
|
+
if (releaseBufferReferenceReturn !== undefined) {
|
|
76
|
+
returnHooks[0] = releaseBufferReferenceReturn;
|
|
77
|
+
}
|
|
78
|
+
const resolveReturnAt = (index) => {
|
|
79
|
+
const resolve = returnResolvers[index];
|
|
80
|
+
const hooks = returnHooks[index];
|
|
81
|
+
return hooks === undefined
|
|
82
|
+
? resolve()
|
|
83
|
+
: withBufferReferenceReturnReleaser(hooks, resolve);
|
|
84
|
+
};
|
|
85
|
+
// Preserve the original one-lane fast path exactly: no wrapper, array lookup,
|
|
86
|
+
// or hook branch is paid by the default one-worker transport. Mutable
|
|
87
|
+
// per-return-lane hooks are needed only by a multi-worker stealing queue.
|
|
88
|
+
const completeFrame = returnResolvers.length === 1
|
|
89
|
+
? releaseBufferReferenceReturn === undefined
|
|
90
|
+
? returnResolvers[0]
|
|
91
|
+
: () => withBufferReferenceReturnReleaser(releaseBufferReferenceReturn, returnResolvers[0])
|
|
92
|
+
: () => {
|
|
93
|
+
let resolved = 0 | 0;
|
|
94
|
+
for (let i = 0; i < returnResolvers.length; i++) {
|
|
95
|
+
resolved = (resolved + resolveReturnAt(i)) | 0;
|
|
96
|
+
}
|
|
97
|
+
return resolved;
|
|
98
|
+
};
|
|
99
|
+
// A stealing queue has one private return lock per worker. Keep at most one
|
|
100
|
+
// waitAsync waiter per lane: after lane A rings, lane B's unresolved waiter
|
|
101
|
+
// must survive the next arm or Atomics.notify(..., 1) could wake an obsolete
|
|
102
|
+
// waiter and starve the live one until the watchdog timeout.
|
|
103
|
+
const completionArmed = new Uint8Array(returnWaiters.length);
|
|
104
|
+
let completionWake;
|
|
105
|
+
let completionGeneration = 0 | 0;
|
|
106
|
+
const setCompletionWaiterArmed = (armed) => {
|
|
107
|
+
for (const setArmed of returnArmers)
|
|
108
|
+
setArmed(armed);
|
|
109
|
+
};
|
|
110
|
+
const waitForCompletion = (onWake, timeoutMs) => {
|
|
111
|
+
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
|
+
let supported = true;
|
|
116
|
+
for (let index = 0; index < returnWaiters.length; index++) {
|
|
117
|
+
if (completionArmed[index] !== 0)
|
|
118
|
+
continue;
|
|
119
|
+
let wait;
|
|
120
|
+
try {
|
|
121
|
+
wait = returnWaiters[index](timeoutMs);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
wait = undefined;
|
|
125
|
+
}
|
|
126
|
+
if (wait === undefined) {
|
|
127
|
+
supported = false;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
completionArmed[index] = 1;
|
|
131
|
+
const generation = completionGeneration;
|
|
132
|
+
const wakeLane = () => {
|
|
133
|
+
if (completionArmed[index] === 0)
|
|
134
|
+
return;
|
|
135
|
+
completionArmed[index] = 0;
|
|
136
|
+
returnArmers[index](false);
|
|
137
|
+
if (generation !== completionGeneration)
|
|
138
|
+
return;
|
|
139
|
+
completionWake?.();
|
|
140
|
+
};
|
|
141
|
+
if (!wait.async)
|
|
142
|
+
wakeLane();
|
|
143
|
+
else
|
|
144
|
+
Promise.resolve(wait.value).then(wakeLane, wakeLane);
|
|
145
|
+
}
|
|
146
|
+
if (!supported) {
|
|
147
|
+
completionWake = undefined;
|
|
148
|
+
setCompletionWaiterArmed(false);
|
|
149
|
+
completionGeneration = (completionGeneration + 1) | 0;
|
|
150
|
+
}
|
|
151
|
+
return supported;
|
|
152
|
+
};
|
|
56
153
|
const hasActiveTasks = () => {
|
|
57
154
|
const count = (inUsed - getPendingPromiseCount()) | 0;
|
|
58
155
|
return count > 0;
|
|
@@ -85,6 +182,14 @@ export function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenc
|
|
|
85
182
|
hasPendingFrames,
|
|
86
183
|
txIdle,
|
|
87
184
|
completeFrame,
|
|
185
|
+
waitForCompletion,
|
|
186
|
+
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
|
+
},
|
|
88
193
|
enqueue: (functionID, timeout, abortSignal) => {
|
|
89
194
|
const HAS_TIMER = timeout !== undefined;
|
|
90
195
|
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) => Promise<void>;
|