knitting 0.1.56 → 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 +347 -46
- package/knitting.browser.d.ts +6 -0
- package/knitting.browser.js +1 -0
- package/knitting.d.ts +3 -2
- package/knitting.js +3 -3
- package/map.md +21 -9
- package/package.json +17 -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/scripts/build-compiled-worker.ts +276 -0
- package/scripts/compiled-worker/porffor.d.ts +5 -0
- package/scripts/compiled-worker/runtime.ts +529 -0
- package/scripts/compiled-worker/task-shim.ts +32 -0
- package/src/api.d.ts +24 -1
- package/src/api.js +311 -27
- package/src/common/module-url.d.ts +1 -0
- package/src/common/module-url.js +30 -2
- package/src/common/node-compat.d.ts +2 -0
- package/src/common/path-canonical.js +20 -6
- package/src/common/runtime.d.ts +3 -1
- package/src/common/runtime.js +25 -4
- package/src/common/task-source.d.ts +2 -0
- package/src/common/task-source.js +17 -1
- package/src/common/worker-runtime.js +16 -7
- package/src/connections/buffer-reference-native.js +25 -1
- package/src/connections/buffer-reference.d.ts +23 -2
- package/src/connections/buffer-reference.js +143 -29
- package/src/connections/bun.d.ts +1 -1
- package/src/connections/bun.js +25 -14
- package/src/connections/deno.d.ts +3 -1
- package/src/connections/deno.js +35 -14
- package/src/connections/external-array-buffer.d.ts +2 -0
- package/src/connections/external-array-buffer.js +33 -0
- package/src/connections/node-addons.d.ts +11 -0
- package/src/connections/node-addons.js +49 -1
- package/src/connections/node-ffi-api.d.ts +19 -0
- package/src/connections/node-ffi-api.js +28 -0
- package/src/connections/node-ffi.d.ts +27 -0
- package/src/connections/node-ffi.js +262 -0
- package/src/connections/node.d.ts +1 -1
- package/src/connections/node.js +20 -20
- package/src/connections/package-assets.js +25 -15
- package/src/connections/posix.d.ts +42 -0
- package/src/connections/posix.js +101 -0
- package/src/connections/process-shared-buffer.d.ts +1 -0
- package/src/connections/process-shared-buffer.js +11 -45
- package/src/connections/types.d.ts +4 -0
- package/src/connections/windows.d.ts +19 -10
- package/src/connections/windows.js +87 -66
- package/src/error.d.ts +7 -6
- package/src/error.js +8 -7
- package/src/memory/lock.d.ts +112 -77
- package/src/memory/lock.js +314 -87
- package/src/memory/payloadCodec.d.ts +11 -2
- package/src/memory/payloadCodec.js +129 -37
- package/src/memory/regionRegistry.d.ts +1 -3
- package/src/memory/regionRegistry.js +16 -14
- package/src/permission/compatibility.d.ts +28 -0
- package/src/permission/compatibility.js +239 -0
- package/src/permission/index.d.ts +2 -0
- package/src/permission/index.js +1 -0
- package/src/permission/protocol.d.ts +1 -0
- package/src/permission/protocol.js +59 -15
- package/src/runtime/compiled-artifact.d.ts +21 -0
- package/src/runtime/compiled-artifact.js +260 -0
- package/src/runtime/compiled-builder.d.ts +8 -0
- package/src/runtime/compiled-builder.js +42 -0
- package/src/runtime/compiled-worker.d.ts +27 -0
- package/src/runtime/compiled-worker.js +562 -0
- package/src/runtime/dispatcher.js +9 -0
- package/src/runtime/inline-executor.js +27 -15
- package/src/runtime/pool.d.ts +121 -5
- package/src/runtime/pool.js +263 -49
- package/src/runtime/process-worker.d.ts +3 -13
- package/src/runtime/process-worker.js +97 -101
- package/src/runtime/tx-queue.d.ts +18 -2
- package/src/runtime/tx-queue.js +56 -16
- package/src/runtime/worker-common.d.ts +13 -0
- package/src/runtime/worker-common.js +102 -0
- package/src/types.d.ts +89 -9
- package/src/worker/composable-runners.js +3 -2
- package/src/worker/loop.d.ts +1 -0
- package/src/worker/loop.js +52 -17
- package/src/worker/rx-queue.d.ts +11 -1
- package/src/worker/rx-queue.js +4 -1
- package/src/worker/task-loader.d.ts +5 -4
- package/src/worker/task-loader.js +13 -7
- package/src/worker/timers.d.ts +8 -1
- package/src/worker/timers.js +28 -23
package/src/api.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { getCallerFilePath, getCallerHref } from "./common/task-source.js";
|
|
1
|
+
import { getCallerFilePath, getCallerHref, setModuleUrl, } from "./common/task-source.js";
|
|
2
2
|
import { DEBUG_ENABLED, resolveDebugNamespaces } from "./debug/gate.js";
|
|
3
3
|
import { genTaskID } from "./common/task-source.js";
|
|
4
4
|
import { toModuleUrl } from "./common/module-url.js";
|
|
5
5
|
import { endpointSymbol } from "./common/task-symbol.js";
|
|
6
|
-
import { spawnWorkerContext } from "./runtime/pool.js";
|
|
7
|
-
import { ChannelHandler } from "./runtime/dispatcher.js";
|
|
8
|
-
import { RUNTIME } from "./common/runtime.js";
|
|
6
|
+
import { createStealPoolBuffers, MAX_STEAL_CONSUMERS, spawnWorkerContext, } from "./runtime/pool.js";
|
|
7
|
+
import { ChannelHandler, hostDispatcherLoop } from "./runtime/dispatcher.js";
|
|
8
|
+
import { createSharedArrayBuffer, RUNTIME } from "./common/runtime.js";
|
|
9
9
|
import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
|
|
10
|
-
import { resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
|
|
10
|
+
import { classifyProcessPermissionCompatibility, enforceProcessPermissionCompatibility, resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
|
|
11
11
|
import { getNodeProcess } from "./common/node-compat.js";
|
|
12
|
+
import { readProcessWorkerNodeMajor, readProcessWorkerRuntime, } from "./runtime/process-worker.js";
|
|
13
|
+
import { inspectCompiledWorkerArtifact } from "./runtime/compiled-artifact.js";
|
|
12
14
|
import { managerMethod } from "./runtime/balancer.js";
|
|
13
15
|
import { createInlineExecutor } from "./runtime/inline-executor.js";
|
|
14
16
|
const hasDebugNamespace = (namespaces, namespace) => namespaces.has("*") || namespaces.has(namespace);
|
|
@@ -50,6 +52,21 @@ const MAX_FUNCTION_COUNT = MAX_FUNCTION_ID + 1;
|
|
|
50
52
|
const DEFAULT_IMPORT_EXPORT_NAME = "default";
|
|
51
53
|
export const isMain = RUNTIME_IS_MAIN_THREAD;
|
|
52
54
|
export { endpointSymbol as endpointSymbol };
|
|
55
|
+
/**
|
|
56
|
+
* Declare the task module's URL for runtimes without stack traces (e.g.
|
|
57
|
+
* Andromeda) that can't auto-discover the caller. Call once at module top
|
|
58
|
+
* level, before defining tasks:
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { setModuleUrl, task } from "knitting";
|
|
62
|
+
* setModuleUrl(import.meta.url);
|
|
63
|
+
* export const add = task({ f: ([a, b]: [number, number]) => a + b });
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* Runs in both host and worker (same module re-imported), so both agree on the
|
|
67
|
+
* path. No-op-safe on Node/Deno/Bun, where stack discovery already works.
|
|
68
|
+
*/
|
|
69
|
+
export { setModuleUrl as setModuleUrl };
|
|
53
70
|
/**
|
|
54
71
|
* Reconstructs stable task order from top-level exports before names are bound.
|
|
55
72
|
*/
|
|
@@ -80,25 +97,112 @@ const resolveImportHref = (href, callerHref) => {
|
|
|
80
97
|
return toModuleUrl(href);
|
|
81
98
|
}
|
|
82
99
|
};
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
throw new
|
|
100
|
+
const resolveWorkerSettings = (worker, callerHref) => {
|
|
101
|
+
if (worker === undefined)
|
|
102
|
+
return undefined;
|
|
103
|
+
const usingPorffor = worker.processRuntime === "porffor";
|
|
104
|
+
if (usingPorffor && worker.runtime !== undefined &&
|
|
105
|
+
worker.runtime !== "compiled") {
|
|
106
|
+
throw new Error('worker.processRuntime "porffor" requires worker.runtime to be compiled or omitted');
|
|
90
107
|
}
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
const forcePorfforBuild = usingPorffor && worker.runtime === undefined;
|
|
109
|
+
let resolved = usingPorffor
|
|
110
|
+
? {
|
|
111
|
+
...worker,
|
|
112
|
+
runtime: "compiled",
|
|
113
|
+
processRuntime: undefined,
|
|
114
|
+
compiled: forcePorfforBuild
|
|
115
|
+
? { ...worker.compiled, build: "always" }
|
|
116
|
+
: worker.compiled,
|
|
117
|
+
}
|
|
118
|
+
: worker;
|
|
119
|
+
const bootstrap = resolved.bootstrap;
|
|
120
|
+
if (bootstrap !== undefined) {
|
|
121
|
+
const name = bootstrap.name ?? DEFAULT_IMPORT_EXPORT_NAME;
|
|
122
|
+
if (typeof bootstrap.href !== "string" || bootstrap.href.length === 0) {
|
|
123
|
+
throw new TypeError("worker.bootstrap.href must be a non-empty string");
|
|
124
|
+
}
|
|
125
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
126
|
+
throw new TypeError("worker.bootstrap.name must be a non-empty string");
|
|
127
|
+
}
|
|
128
|
+
resolved = {
|
|
129
|
+
...resolved,
|
|
130
|
+
bootstrap: {
|
|
131
|
+
...bootstrap,
|
|
132
|
+
href: resolveImportHref(bootstrap.href, callerHref),
|
|
133
|
+
name,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
93
136
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
137
|
+
const compiled = resolved.compiled;
|
|
138
|
+
if (compiled !== undefined) {
|
|
139
|
+
if (compiled.build !== undefined && typeof compiled.build !== "boolean" &&
|
|
140
|
+
compiled.build !== "always") {
|
|
141
|
+
throw new TypeError('worker.compiled.build must be a boolean or "always"');
|
|
142
|
+
}
|
|
143
|
+
for (const [name, value] of Object.entries({
|
|
144
|
+
artifact: compiled.artifact,
|
|
145
|
+
manifest: compiled.manifest,
|
|
146
|
+
compiler: compiled.compiler,
|
|
147
|
+
})) {
|
|
148
|
+
if (value !== undefined &&
|
|
149
|
+
(typeof value !== "string" || value.length === 0)) {
|
|
150
|
+
throw new TypeError("worker.compiled." + name + " must be a non-empty string");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
resolved = {
|
|
154
|
+
...resolved,
|
|
155
|
+
compiled: {
|
|
156
|
+
build: compiled.build,
|
|
157
|
+
compiler: compiled.compiler === undefined
|
|
158
|
+
? undefined
|
|
159
|
+
: compiled.compiler.startsWith(".") ||
|
|
160
|
+
compiled.compiler.startsWith("/") ||
|
|
161
|
+
compiled.compiler.startsWith("file:") ||
|
|
162
|
+
/^[A-Za-z]:[\\/]/.test(compiled.compiler)
|
|
163
|
+
? resolveImportHref(compiled.compiler, callerHref)
|
|
164
|
+
: compiled.compiler,
|
|
165
|
+
artifact: compiled.artifact === undefined
|
|
166
|
+
? undefined
|
|
167
|
+
: resolveImportHref(compiled.artifact, callerHref),
|
|
168
|
+
manifest: compiled.manifest === undefined
|
|
169
|
+
? undefined
|
|
170
|
+
: resolveImportHref(compiled.manifest, callerHref),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return resolved;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Check whether a task module has a compatible prebuilt native worker.
|
|
178
|
+
*
|
|
179
|
+
* By default tasks.ts resolves to tasks.knt plus tasks.knt.json.
|
|
180
|
+
* This only inspects the artifact; it never executes it.
|
|
181
|
+
*/
|
|
182
|
+
export const checkCompiledWorker = (source, options) => {
|
|
183
|
+
const moduleSource = typeof source === "string" || source instanceof URL
|
|
184
|
+
? source
|
|
185
|
+
: source?.importedFrom;
|
|
186
|
+
if (typeof moduleSource !== "string" && !(moduleSource instanceof URL)) {
|
|
187
|
+
throw new TypeError("checkCompiledWorker expects a task definition, module path, or URL");
|
|
188
|
+
}
|
|
189
|
+
const callerHref = getCallerHref(2);
|
|
190
|
+
const resolvedOptions = options === undefined ? undefined : {
|
|
191
|
+
build: options.build,
|
|
192
|
+
compiler: options.compiler,
|
|
193
|
+
artifact: options.artifact === undefined
|
|
194
|
+
? undefined
|
|
195
|
+
: resolveImportHref(options.artifact, callerHref),
|
|
196
|
+
manifest: options.manifest === undefined
|
|
197
|
+
? undefined
|
|
198
|
+
: resolveImportHref(options.manifest, callerHref),
|
|
101
199
|
};
|
|
200
|
+
const inspection = inspectCompiledWorkerArtifact({
|
|
201
|
+
source: moduleSource,
|
|
202
|
+
options: resolvedOptions,
|
|
203
|
+
});
|
|
204
|
+
const { artifactHref: _artifactHref, manifestHref: _manifestHref, taskEntries: _taskEntries, ...result } = inspection;
|
|
205
|
+
return result;
|
|
102
206
|
};
|
|
103
207
|
const isTaskDefinition = (value) => value != null &&
|
|
104
208
|
typeof value === "object" &&
|
|
@@ -206,7 +310,9 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
206
310
|
key === "--allow-fs-write" ||
|
|
207
311
|
key === "--allow-worker" ||
|
|
208
312
|
key === "--allow-child-process" ||
|
|
313
|
+
key === "--allow-net" ||
|
|
209
314
|
key === "--allow-addons" ||
|
|
315
|
+
key === "--allow-ffi" ||
|
|
210
316
|
key === "--allow-wasi";
|
|
211
317
|
};
|
|
212
318
|
const stripNodePermissionFlags = (flags) => flags?.filter((flag) => !isNodePermissionFlag(flag));
|
|
@@ -256,7 +362,72 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
256
362
|
hostDebug?.log(`modules=${formatDebugList(list)}`);
|
|
257
363
|
hostDebug?.log(`permission=${permissionProtocol?.mode ?? "off"} execArgv=${formatDebugList(execArgv)}`);
|
|
258
364
|
const usesAbortSignal = listOfFunctions.some((fn) => fn.abortSignal !== undefined);
|
|
259
|
-
const resolvedWorker =
|
|
365
|
+
const resolvedWorker = resolveWorkerSettings(worker, callerHref);
|
|
366
|
+
const dispatcherEnv = nodeProcess?.env?.KNITTING_DISPATCHER;
|
|
367
|
+
const stealEnvRaw = nodeProcess?.env?.KNITTING_STEAL?.trim().toLowerCase();
|
|
368
|
+
const stealEnv = stealEnvRaw === "1" || stealEnvRaw === "true"
|
|
369
|
+
? true
|
|
370
|
+
: stealEnvRaw === "0" || stealEnvRaw === "false"
|
|
371
|
+
? false
|
|
372
|
+
: undefined;
|
|
373
|
+
// Shared-submit stealing is the default for compatible multi-worker thread
|
|
374
|
+
// pools. An explicit option wins over the environment; process/compiled/
|
|
375
|
+
// inline transports retain their existing topology unless the caller
|
|
376
|
+
// explicitly requests an unsupported combination.
|
|
377
|
+
const dispatcherExplicitlySelected = host?.dispatcher !== undefined ||
|
|
378
|
+
dispatcherEnv === "serial-channel" || dispatcherEnv === "per-thread";
|
|
379
|
+
const stealDefaultCompatible = balancer === undefined &&
|
|
380
|
+
!dispatcherExplicitlySelected && (threads ?? 1) <= MAX_STEAL_CONSUMERS;
|
|
381
|
+
const stealRequested = host?.steal ?? stealEnv ?? stealDefaultCompatible;
|
|
382
|
+
const stealExplicitlyEnabled = host?.steal === true ||
|
|
383
|
+
(host?.steal === undefined && stealEnv === true);
|
|
384
|
+
const usingCompiledWorker = resolvedWorker?.runtime === "compiled";
|
|
385
|
+
if (resolvedWorker?.compiled !== undefined && !usingCompiledWorker) {
|
|
386
|
+
throw new Error("worker.compiled requires worker.runtime to be compiled");
|
|
387
|
+
}
|
|
388
|
+
if (usingCompiledWorker) {
|
|
389
|
+
const unsupported = [];
|
|
390
|
+
if (usingInliner)
|
|
391
|
+
unsupported.push("inliner");
|
|
392
|
+
if (payload !== undefined)
|
|
393
|
+
unsupported.push("payload");
|
|
394
|
+
if (unsafe !== undefined)
|
|
395
|
+
unsupported.push("unsafe");
|
|
396
|
+
if (source !== undefined)
|
|
397
|
+
unsupported.push("source");
|
|
398
|
+
if (workerExecArgv !== undefined)
|
|
399
|
+
unsupported.push("workerExecArgv");
|
|
400
|
+
if (permission !== undefined)
|
|
401
|
+
unsupported.push("permission");
|
|
402
|
+
if (host !== undefined)
|
|
403
|
+
unsupported.push("host");
|
|
404
|
+
if (resolvedWorker.bootstrap !== undefined) {
|
|
405
|
+
unsupported.push("worker.bootstrap");
|
|
406
|
+
}
|
|
407
|
+
if (resolvedWorker.timers !== undefined)
|
|
408
|
+
unsupported.push("worker.timers");
|
|
409
|
+
if (resolvedWorker.processRuntime !== undefined) {
|
|
410
|
+
unsupported.push("worker.processRuntime");
|
|
411
|
+
}
|
|
412
|
+
if (resolvedWorker.processCommandPrefix !== undefined) {
|
|
413
|
+
unsupported.push("worker.processCommandPrefix");
|
|
414
|
+
}
|
|
415
|
+
if (resolvedWorker.processSharedMemory !== undefined) {
|
|
416
|
+
unsupported.push("worker.processSharedMemory");
|
|
417
|
+
}
|
|
418
|
+
if (resolvedWorker.resolveAfterFinishingAll !== undefined) {
|
|
419
|
+
unsupported.push("worker.resolveAfterFinishingAll");
|
|
420
|
+
}
|
|
421
|
+
if (listOfFunctions.some((fn) => fn.timeout !== undefined)) {
|
|
422
|
+
unsupported.push("task timeout");
|
|
423
|
+
}
|
|
424
|
+
if (listOfFunctions.some((fn) => fn.imported === true)) {
|
|
425
|
+
unsupported.push("importTask");
|
|
426
|
+
}
|
|
427
|
+
if (unsupported.length > 0) {
|
|
428
|
+
throw new Error("Compiled workers do not support: " + unsupported.join(", "));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
260
431
|
if (resolvedWorker?.bootstrap !== undefined) {
|
|
261
432
|
hostDebug?.log(`bootstrap href=${resolvedWorker.bootstrap.href}` +
|
|
262
433
|
` name=${resolvedWorker.bootstrap.name}`);
|
|
@@ -264,6 +435,22 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
264
435
|
if (usingInliner && resolvedWorker?.bootstrap !== undefined) {
|
|
265
436
|
throw new Error("worker.bootstrap cannot be used with the inliner");
|
|
266
437
|
}
|
|
438
|
+
if (stealExplicitlyEnabled && resolvedWorker?.runtime === "process") {
|
|
439
|
+
throw new Error("host.steal does not support process workers; use worker threads or disable stealing");
|
|
440
|
+
}
|
|
441
|
+
if (resolvedWorker?.runtime === "process") {
|
|
442
|
+
const processRuntime = readProcessWorkerRuntime(resolvedWorker);
|
|
443
|
+
enforceProcessPermissionCompatibility(classifyProcessPermissionCompatibility({
|
|
444
|
+
permission,
|
|
445
|
+
resolved: permissionProtocol,
|
|
446
|
+
target: {
|
|
447
|
+
runtime: processRuntime,
|
|
448
|
+
nodeMajor: processRuntime === "node"
|
|
449
|
+
? readProcessWorkerNodeMajor(resolvedWorker)
|
|
450
|
+
: undefined,
|
|
451
|
+
},
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
267
454
|
const hardTimeoutMs = Number.isFinite(resolvedWorker?.hardTimeoutMs)
|
|
268
455
|
? Math.max(1, Math.floor(resolvedWorker?.hardTimeoutMs))
|
|
269
456
|
: undefined;
|
|
@@ -276,7 +463,6 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
276
463
|
`(import { isMain } from "knitting") so only the main program starts ` +
|
|
277
464
|
`the pool.`);
|
|
278
465
|
}
|
|
279
|
-
const dispatcherEnv = nodeProcess?.env?.KNITTING_DISPATCHER;
|
|
280
466
|
const explicitDispatcher = host?.dispatcher ??
|
|
281
467
|
(dispatcherEnv === "serial-channel" || dispatcherEnv === "per-thread"
|
|
282
468
|
? dispatcherEnv
|
|
@@ -292,7 +478,26 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
292
478
|
return "serial-channel";
|
|
293
479
|
})();
|
|
294
480
|
const dispatcher = explicitDispatcher ?? autoDispatcher;
|
|
295
|
-
|
|
481
|
+
// Work stealing: one shared submit region, private return lanes, one
|
|
482
|
+
// pool-global pending registry. It is the compatible multi-thread default;
|
|
483
|
+
// an explicit balancer/dispatcher keeps the private-lane topology unless
|
|
484
|
+
// stealing itself was explicitly requested.
|
|
485
|
+
const useSteal = stealRequested &&
|
|
486
|
+
!usingCompiledWorker &&
|
|
487
|
+
resolvedWorker?.runtime !== "process" &&
|
|
488
|
+
(threads ?? 1) > 1 &&
|
|
489
|
+
!usingInliner;
|
|
490
|
+
const stealBuffers = useSteal
|
|
491
|
+
? createStealPoolBuffers({
|
|
492
|
+
threads: threads ?? 1,
|
|
493
|
+
payload,
|
|
494
|
+
regionLanes: host?.stealRegionLanes,
|
|
495
|
+
abortSignalCapacity,
|
|
496
|
+
usesAbortSignal,
|
|
497
|
+
})
|
|
498
|
+
: undefined;
|
|
499
|
+
const serialChannel = !usingCompiledWorker && !useSteal &&
|
|
500
|
+
dispatcher === "serial-channel";
|
|
296
501
|
const serialDispatcherChannel = serialChannel
|
|
297
502
|
? new ChannelHandler()
|
|
298
503
|
: undefined;
|
|
@@ -317,14 +522,83 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
317
522
|
usesAbortSignal,
|
|
318
523
|
permission: permissionProtocol,
|
|
319
524
|
sharedChannelHandler: serialDispatcherChannel,
|
|
525
|
+
stealPool: stealBuffers === undefined ? undefined : {
|
|
526
|
+
submitBuffers: stealBuffers.submitBuffers,
|
|
527
|
+
returnBuffers: stealBuffers.returnBuffers[thread],
|
|
528
|
+
hostSubmitLock: stealBuffers.hostSubmitLock,
|
|
529
|
+
hostReturnLock: stealBuffers.hostReturnLocks[thread],
|
|
530
|
+
sharedQueue: stealBuffers.sharedQueue,
|
|
531
|
+
consumers: threads ?? 1,
|
|
532
|
+
consumerId: thread,
|
|
533
|
+
regionLanes: stealBuffers.regionLanes,
|
|
534
|
+
abortSignalSAB: stealBuffers.abortSignalSAB,
|
|
535
|
+
abortSignalMax: stealBuffers.abortSignalMax,
|
|
536
|
+
},
|
|
320
537
|
}));
|
|
538
|
+
const stealChannel = useSteal ? new ChannelHandler() : undefined;
|
|
539
|
+
if (useSteal) {
|
|
540
|
+
const channel = stealChannel;
|
|
541
|
+
const queue = stealBuffers.sharedQueue;
|
|
542
|
+
// Wake exactly one worker per drain, round-robin. Stealing decouples the
|
|
543
|
+
// wake target from the assignment: if the woken worker is busy, any other
|
|
544
|
+
// idle endpoint claims the region instead. So this costs one notify per
|
|
545
|
+
// publish regardless of pool size, rather than one per lane.
|
|
546
|
+
let wakeCursor = 0;
|
|
547
|
+
const wakeOne = () => {
|
|
548
|
+
const lanes = workers.length;
|
|
549
|
+
if (lanes === 0)
|
|
550
|
+
return;
|
|
551
|
+
const lane = wakeCursor;
|
|
552
|
+
wakeCursor = wakeCursor + 1 < lanes ? wakeCursor + 1 : 0;
|
|
553
|
+
workers[lane].laneWake?.();
|
|
554
|
+
};
|
|
555
|
+
// The shared dispatcher drives one queue, so it needs signal words of its
|
|
556
|
+
// own rather than any single lane's; waking is delegated to `wakeOne`.
|
|
557
|
+
const signalWords = new Int32Array(createSharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT));
|
|
558
|
+
const { check } = hostDispatcherLoop({
|
|
559
|
+
signalBox: {
|
|
560
|
+
opView: signalWords.subarray(0, 1),
|
|
561
|
+
txStatus: signalWords.subarray(1, 2),
|
|
562
|
+
rxStatus: signalWords.subarray(2, 3),
|
|
563
|
+
},
|
|
564
|
+
queue,
|
|
565
|
+
channelHandler: channel,
|
|
566
|
+
dispatcherOptions: host,
|
|
567
|
+
notifySignal: wakeOne,
|
|
568
|
+
});
|
|
569
|
+
channel.open(check);
|
|
570
|
+
workers.forEach((context) => {
|
|
571
|
+
context.bindSend(() => {
|
|
572
|
+
if (check.isRunning !== true) {
|
|
573
|
+
check.isRunning = true;
|
|
574
|
+
Promise.resolve().then(check);
|
|
575
|
+
}
|
|
576
|
+
wakeOne();
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
hostDebug?.log(`dispatcher=steal lanes=${workers.length} g=${stealBuffers.regionLanes}`);
|
|
580
|
+
}
|
|
321
581
|
const sharedDispatcherChannel = serialDispatcherChannel;
|
|
322
582
|
if (serialChannel) {
|
|
323
583
|
const channel = serialDispatcherChannel;
|
|
324
584
|
const checks = workers.map((context) => context.dispatcherCheck);
|
|
585
|
+
const laneIdle = workers.map((context) => context.txIdle);
|
|
325
586
|
let serialScheduled = false;
|
|
326
587
|
let serialInFlight = false;
|
|
327
588
|
let serialRerun = false;
|
|
589
|
+
// Busy-lane set: a tick walks only the lanes that have work, not all of
|
|
590
|
+
// them. `active` is a dense list of lane indices; `isActive` keeps the
|
|
591
|
+
// membership test O(1). A lane joins on send() and leaves once its queue
|
|
592
|
+
// reports idle, so an N-worker pool with one busy lane costs one check per
|
|
593
|
+
// tick instead of N.
|
|
594
|
+
const active = [];
|
|
595
|
+
const isActive = new Uint8Array(checks.length);
|
|
596
|
+
const markActive = (lane) => {
|
|
597
|
+
if (isActive[lane] === 1)
|
|
598
|
+
return;
|
|
599
|
+
isActive[lane] = 1;
|
|
600
|
+
active.push(lane);
|
|
601
|
+
};
|
|
328
602
|
const runSerialChecks = () => {
|
|
329
603
|
if (serialInFlight) {
|
|
330
604
|
serialRerun = true;
|
|
@@ -334,11 +608,19 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
334
608
|
do {
|
|
335
609
|
serialRerun = false;
|
|
336
610
|
serialScheduled = false;
|
|
337
|
-
|
|
338
|
-
|
|
611
|
+
// Walk backwards so dropping an idle lane is a swap-with-last pop.
|
|
612
|
+
for (let index = active.length - 1; index >= 0; index--) {
|
|
613
|
+
const lane = active[index];
|
|
614
|
+
const check = checks[lane];
|
|
339
615
|
if (check.isRunning !== true)
|
|
340
616
|
check.isRunning = true;
|
|
341
617
|
check();
|
|
618
|
+
if (laneIdle[lane]()) {
|
|
619
|
+
isActive[lane] = 0;
|
|
620
|
+
const last = active.pop();
|
|
621
|
+
if (index < active.length)
|
|
622
|
+
active[index] = last;
|
|
623
|
+
}
|
|
342
624
|
}
|
|
343
625
|
} while (serialRerun);
|
|
344
626
|
serialInFlight = false;
|
|
@@ -354,9 +636,10 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
354
636
|
Promise.resolve().then(runSerialChecks);
|
|
355
637
|
};
|
|
356
638
|
channel.open(runSerialChecks);
|
|
357
|
-
workers.forEach((context) => {
|
|
639
|
+
workers.forEach((context, lane) => {
|
|
358
640
|
const wake = context.laneWake;
|
|
359
641
|
context.bindSend(() => {
|
|
642
|
+
markActive(lane);
|
|
360
643
|
scheduleSerialCheck();
|
|
361
644
|
wake();
|
|
362
645
|
});
|
|
@@ -401,6 +684,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
401
684
|
closePromise = Promise.allSettled(workers.map((context) => context.kills()))
|
|
402
685
|
.then(() => {
|
|
403
686
|
sharedDispatcherChannel?.close();
|
|
687
|
+
stealChannel?.close();
|
|
404
688
|
});
|
|
405
689
|
return closePromise;
|
|
406
690
|
};
|
package/src/common/module-url.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getNodeBuiltinModule } from "./node-compat.js";
|
|
2
|
+
import { IS_ANDROMEDA } from "./runtime.js";
|
|
2
3
|
const WINDOWS_DRIVE_PATH = /^[A-Za-z]:[\\/]/;
|
|
3
4
|
const WINDOWS_UNC_PATH = /^\\\\[^\\/?]+\\[^\\/?]+/;
|
|
4
5
|
const encodeFilePath = (path) => encodeURI(path)
|
|
5
6
|
.replace(/\?/g, "%3F")
|
|
6
7
|
.replace(/#/g, "%23");
|
|
8
|
+
// `node:url` resolved lazily (absent on Andromeda); fall back to a pure
|
|
9
|
+
// path->file-URL conversion when it is missing.
|
|
10
|
+
const nodePathToFileURL = () => getNodeBuiltinModule("node:url")
|
|
11
|
+
?.pathToFileURL;
|
|
12
|
+
const pathToFileUrlFallback = (specifier) => {
|
|
13
|
+
const absolute = specifier.startsWith("/") ? specifier : `/${specifier}`;
|
|
14
|
+
return `file://${encodeFilePath(absolute)}`;
|
|
15
|
+
};
|
|
7
16
|
export const toModuleUrl = (specifier) => {
|
|
8
17
|
if (WINDOWS_DRIVE_PATH.test(specifier)) {
|
|
9
18
|
const normalized = specifier.replace(/\\/g, "/");
|
|
@@ -19,6 +28,25 @@ export const toModuleUrl = (specifier) => {
|
|
|
19
28
|
return new URL(specifier).href;
|
|
20
29
|
}
|
|
21
30
|
catch {
|
|
22
|
-
|
|
31
|
+
const pathToFileURL = nodePathToFileURL();
|
|
32
|
+
return pathToFileURL
|
|
33
|
+
? pathToFileURL(specifier).href
|
|
34
|
+
: pathToFileUrlFallback(specifier);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
// Andromeda's loader treats `file://` import specifiers as relative paths;
|
|
38
|
+
// convert back to a plain filesystem path there. No-op elsewhere.
|
|
39
|
+
export const toImportSpecifier = (moduleUrl) => {
|
|
40
|
+
if (!IS_ANDROMEDA || !moduleUrl.startsWith("file://"))
|
|
41
|
+
return moduleUrl;
|
|
42
|
+
const withoutScheme = moduleUrl.slice("file://".length);
|
|
43
|
+
const path = withoutScheme.startsWith("/")
|
|
44
|
+
? withoutScheme
|
|
45
|
+
: `/${withoutScheme}`;
|
|
46
|
+
try {
|
|
47
|
+
return decodeURIComponent(path);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return path;
|
|
23
51
|
}
|
|
24
52
|
};
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { getNodeBuiltinModule } from "./node-compat.js";
|
|
2
|
+
const nodeFs = () => getNodeBuiltinModule("node:fs");
|
|
3
|
+
const nodePath = () => {
|
|
4
|
+
const module = getNodeBuiltinModule("node:path");
|
|
5
|
+
if (module === undefined) {
|
|
6
|
+
throw new Error("node:path is not available in this runtime");
|
|
7
|
+
}
|
|
8
|
+
return module;
|
|
9
|
+
};
|
|
10
|
+
const defaultFsApi = () => {
|
|
11
|
+
const fs = nodeFs();
|
|
12
|
+
if (fs === undefined)
|
|
13
|
+
return {};
|
|
14
|
+
return {
|
|
15
|
+
existsSync: fs.existsSync,
|
|
16
|
+
realpathSync: fs.realpathSync.native ?? fs.realpathSync,
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export const toCanonicalPath = (candidate, fsApi = defaultFsApi()) => {
|
|
20
|
+
const { basename: pathBasename, dirname: pathDirname, join: pathJoin, resolve: pathResolve, } = nodePath();
|
|
7
21
|
const absolute = pathResolve(candidate);
|
|
8
22
|
const { existsSync, realpathSync } = fsApi;
|
|
9
23
|
if (typeof realpathSync === "function") {
|
package/src/common/runtime.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
type RuntimeName = "deno" | "bun" | "node" | "unknown";
|
|
1
|
+
type RuntimeName = "deno" | "bun" | "node" | "andromeda" | "browser" | "unknown";
|
|
2
2
|
export declare const IS_DENO: boolean;
|
|
3
3
|
export declare const IS_BUN: boolean;
|
|
4
4
|
export declare const IS_NODE: boolean;
|
|
5
|
+
export declare const IS_ANDROMEDA: boolean;
|
|
6
|
+
export declare const IS_BROWSER: boolean;
|
|
5
7
|
export declare const RUNTIME: RuntimeName;
|
|
6
8
|
export declare const SET_IMMEDIATE: typeof setImmediate & ((cb: () => void) => void);
|
|
7
9
|
export declare const HAS_SHARED_WASM_MEMORY: boolean;
|
package/src/common/runtime.js
CHANGED
|
@@ -4,16 +4,37 @@ const nodeProcess = getNodeProcess();
|
|
|
4
4
|
export const IS_DENO = typeof globals.Deno?.version?.deno === "string";
|
|
5
5
|
export const IS_BUN = typeof globals.Bun?.version === "string";
|
|
6
6
|
export const IS_NODE = typeof nodeProcess?.versions?.node === "string";
|
|
7
|
-
|
|
7
|
+
// Andromeda (Nova engine): no `node:` builtins or `WebAssembly`; detect via
|
|
8
|
+
// its private bootstrap global.
|
|
9
|
+
export const IS_ANDROMEDA = typeof globals.__andromeda__ !== "undefined";
|
|
10
|
+
// A page or a web worker: no `node:` builtins, but `Worker`, `MessageChannel`,
|
|
11
|
+
// and a cross-origin isolated `SharedArrayBuffer` are all the pool needs.
|
|
12
|
+
export const IS_BROWSER = !IS_DENO && !IS_BUN && !IS_NODE && !IS_ANDROMEDA &&
|
|
13
|
+
(typeof globals.document !== "undefined" ||
|
|
14
|
+
typeof globals.WorkerGlobalScope === "function");
|
|
15
|
+
export const RUNTIME = (IS_DENO
|
|
16
|
+
? "deno"
|
|
17
|
+
: IS_BUN
|
|
18
|
+
? "bun"
|
|
19
|
+
: IS_NODE
|
|
20
|
+
? "node"
|
|
21
|
+
: IS_ANDROMEDA
|
|
22
|
+
? "andromeda"
|
|
23
|
+
: IS_BROWSER
|
|
24
|
+
? "browser"
|
|
25
|
+
: "unknown");
|
|
26
|
+
// `WebAssembly` may be undeclared (not just undefined) on Andromeda, where a
|
|
27
|
+
// bare reference throws; capture it through `globalThis` so access is safe.
|
|
28
|
+
const WebAssemblyRef = globals.WebAssembly;
|
|
8
29
|
export const SET_IMMEDIATE = typeof globals.setImmediate === "function" ? globals.setImmediate : undefined;
|
|
9
30
|
const WASM_MEMORY_PAGE_BYTES = 64 * 1024;
|
|
10
31
|
const wasmSharedBufferMemory = new WeakMap();
|
|
11
32
|
const wasmSharedBufferMaxByteLength = new WeakMap();
|
|
12
33
|
const hasSharedWasmMemory = (() => {
|
|
13
|
-
if (typeof
|
|
34
|
+
if (typeof WebAssemblyRef?.Memory !== "function")
|
|
14
35
|
return false;
|
|
15
36
|
try {
|
|
16
|
-
void new
|
|
37
|
+
void new WebAssemblyRef.Memory({ initial: 0, maximum: 1, shared: true });
|
|
17
38
|
return true;
|
|
18
39
|
}
|
|
19
40
|
catch {
|
|
@@ -23,7 +44,7 @@ const hasSharedWasmMemory = (() => {
|
|
|
23
44
|
export const HAS_SHARED_WASM_MEMORY = hasSharedWasmMemory;
|
|
24
45
|
const roundupWasmPages = (byteLength) => Math.ceil(Math.max(0, byteLength) / WASM_MEMORY_PAGE_BYTES);
|
|
25
46
|
const createSharedWasmBuffer = (byteLength, maxByteLength) => {
|
|
26
|
-
const memory = new
|
|
47
|
+
const memory = new WebAssemblyRef.Memory({
|
|
27
48
|
initial: roundupWasmPages(byteLength),
|
|
28
49
|
maximum: Math.max(roundupWasmPages(byteLength), roundupWasmPages(maxByteLength)),
|
|
29
50
|
shared: true,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export declare const genTaskID: () => number;
|
|
2
|
+
export declare const setModuleUrl: (url: string | undefined) => void;
|
|
3
|
+
export declare const getModuleUrlOverride: () => string | undefined;
|
|
2
4
|
export declare const getCallerHref: (offset?: number) => string;
|
|
3
5
|
export declare const getCallerFilePath: (offset?: number) => [string, number];
|
|
@@ -61,7 +61,21 @@ const collectStackFrames = () => {
|
|
|
61
61
|
const isInternalFrame = (frame) => isRuntimeInternalFrame(frame.file) ||
|
|
62
62
|
isInternalCallerFrame(frame.file) ||
|
|
63
63
|
isInternalCallerFunction(frame.functionName, frame.methodName);
|
|
64
|
+
// Module-URL override for runtimes without stack traces (e.g. Andromeda, where
|
|
65
|
+
// `new Error().stack` is undefined). When set, caller discovery skips stack
|
|
66
|
+
// inspection and attributes every task here. Set via
|
|
67
|
+
// `setModuleUrl(import.meta.url)`, which runs in both host and worker copies so
|
|
68
|
+
// both agree on the path.
|
|
69
|
+
let moduleUrlOverride;
|
|
70
|
+
export const setModuleUrl = (url) => {
|
|
71
|
+
moduleUrlOverride = typeof url === "string" && url.length > 0
|
|
72
|
+
? toModuleUrl(url)
|
|
73
|
+
: undefined;
|
|
74
|
+
};
|
|
75
|
+
export const getModuleUrlOverride = () => moduleUrlOverride;
|
|
64
76
|
const resolveCallerHref = (offset) => {
|
|
77
|
+
if (moduleUrlOverride !== undefined)
|
|
78
|
+
return moduleUrlOverride;
|
|
65
79
|
const frames = collectStackFrames();
|
|
66
80
|
const direct = frames[offset];
|
|
67
81
|
const caller = (direct && !isInternalFrame(direct)
|
|
@@ -70,7 +84,9 @@ const resolveCallerHref = (offset) => {
|
|
|
70
84
|
frames.find((frame) => !isInternalFrame(frame))?.file ??
|
|
71
85
|
frames.find((frame) => !isRuntimeInternalFrame(frame.file))?.file;
|
|
72
86
|
if (!caller) {
|
|
73
|
-
throw new Error("Unable to determine caller file."
|
|
87
|
+
throw new Error("Unable to determine caller file. This runtime exposes no stack traces " +
|
|
88
|
+
"(e.g. Andromeda); call setModuleUrl(import.meta.url) at the top of " +
|
|
89
|
+
"the module that defines your tasks before creating a pool.");
|
|
74
90
|
}
|
|
75
91
|
return toModuleUrl(caller);
|
|
76
92
|
};
|