knitting 0.1.56 → 0.1.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -40
- package/knitting.d.ts +3 -2
- package/knitting.js +3 -3
- package/map.md +21 -9
- package/package.json +7 -1
- 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 +199 -20
- 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 +2 -1
- package/src/common/runtime.js +18 -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 +87 -77
- package/src/memory/lock.js +103 -84
- package/src/memory/payloadCodec.d.ts +11 -2
- package/src/memory/payloadCodec.js +127 -31
- 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 +54 -14
- 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/inline-executor.js +27 -15
- package/src/runtime/pool.d.ts +7 -2
- package/src/runtime/pool.js +74 -6
- package/src/runtime/process-worker.d.ts +1 -1
- package/src/runtime/process-worker.js +48 -8
- package/src/runtime/tx-queue.d.ts +3 -1
- package/src/runtime/tx-queue.js +4 -1
- package/src/types.d.ts +48 -6
- package/src/worker/composable-runners.js +3 -2
- package/src/worker/loop.d.ts +1 -0
- package/src/worker/loop.js +25 -12
- package/src/worker/task-loader.d.ts +5 -4
- package/src/worker/task-loader.js +13 -7
- package/src/worker/timers.js +8 -6
package/src/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
@@ -7,8 +7,10 @@ import { spawnWorkerContext } from "./runtime/pool.js";
|
|
|
7
7
|
import { ChannelHandler } from "./runtime/dispatcher.js";
|
|
8
8
|
import { 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 { readProcessWorkerCommandPrefix, 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);
|
|
@@ -44,12 +46,39 @@ const readHostCwd = () => {
|
|
|
44
46
|
}
|
|
45
47
|
return undefined;
|
|
46
48
|
};
|
|
49
|
+
const readKnownProcessWorkerNodeMajor = (worker) => {
|
|
50
|
+
if (RUNTIME !== "node")
|
|
51
|
+
return undefined;
|
|
52
|
+
if (readProcessWorkerCommandPrefix(worker) !== undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
const nodeProcess = getNodeProcess();
|
|
55
|
+
if (nodeProcess?.env?.NODE_BINARY !== undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
const raw = nodeProcess?.versions?.node;
|
|
58
|
+
const major = Number.parseInt(String(raw).split(".", 1)[0] ?? "", 10);
|
|
59
|
+
return Number.isInteger(major) && major > 0 ? major : undefined;
|
|
60
|
+
};
|
|
47
61
|
const formatDebugList = (values, empty = "(none)") => values && values.length > 0 ? values.join(",") : empty;
|
|
48
62
|
const MAX_FUNCTION_ID = 0xFFFF;
|
|
49
63
|
const MAX_FUNCTION_COUNT = MAX_FUNCTION_ID + 1;
|
|
50
64
|
const DEFAULT_IMPORT_EXPORT_NAME = "default";
|
|
51
65
|
export const isMain = RUNTIME_IS_MAIN_THREAD;
|
|
52
66
|
export { endpointSymbol as endpointSymbol };
|
|
67
|
+
/**
|
|
68
|
+
* Declare the task module's URL for runtimes without stack traces (e.g.
|
|
69
|
+
* Andromeda) that can't auto-discover the caller. Call once at module top
|
|
70
|
+
* level, before defining tasks:
|
|
71
|
+
*
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { setModuleUrl, task } from "knitting";
|
|
74
|
+
* setModuleUrl(import.meta.url);
|
|
75
|
+
* export const add = task({ f: ([a, b]: [number, number]) => a + b });
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* Runs in both host and worker (same module re-imported), so both agree on the
|
|
79
|
+
* path. No-op-safe on Node/Deno/Bun, where stack discovery already works.
|
|
80
|
+
*/
|
|
81
|
+
export { setModuleUrl as setModuleUrl };
|
|
53
82
|
/**
|
|
54
83
|
* Reconstructs stable task order from top-level exports before names are bound.
|
|
55
84
|
*/
|
|
@@ -80,25 +109,112 @@ const resolveImportHref = (href, callerHref) => {
|
|
|
80
109
|
return toModuleUrl(href);
|
|
81
110
|
}
|
|
82
111
|
};
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
throw new
|
|
112
|
+
const resolveWorkerSettings = (worker, callerHref) => {
|
|
113
|
+
if (worker === undefined)
|
|
114
|
+
return undefined;
|
|
115
|
+
const usingPorffor = worker.processRuntime === "porffor";
|
|
116
|
+
if (usingPorffor && worker.runtime !== undefined &&
|
|
117
|
+
worker.runtime !== "compiled") {
|
|
118
|
+
throw new Error('worker.processRuntime "porffor" requires worker.runtime to be compiled or omitted');
|
|
90
119
|
}
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
const forcePorfforBuild = usingPorffor && worker.runtime === undefined;
|
|
121
|
+
let resolved = usingPorffor
|
|
122
|
+
? {
|
|
123
|
+
...worker,
|
|
124
|
+
runtime: "compiled",
|
|
125
|
+
processRuntime: undefined,
|
|
126
|
+
compiled: forcePorfforBuild
|
|
127
|
+
? { ...worker.compiled, build: "always" }
|
|
128
|
+
: worker.compiled,
|
|
129
|
+
}
|
|
130
|
+
: worker;
|
|
131
|
+
const bootstrap = resolved.bootstrap;
|
|
132
|
+
if (bootstrap !== undefined) {
|
|
133
|
+
const name = bootstrap.name ?? DEFAULT_IMPORT_EXPORT_NAME;
|
|
134
|
+
if (typeof bootstrap.href !== "string" || bootstrap.href.length === 0) {
|
|
135
|
+
throw new TypeError("worker.bootstrap.href must be a non-empty string");
|
|
136
|
+
}
|
|
137
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
138
|
+
throw new TypeError("worker.bootstrap.name must be a non-empty string");
|
|
139
|
+
}
|
|
140
|
+
resolved = {
|
|
141
|
+
...resolved,
|
|
142
|
+
bootstrap: {
|
|
143
|
+
...bootstrap,
|
|
144
|
+
href: resolveImportHref(bootstrap.href, callerHref),
|
|
145
|
+
name,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
93
148
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
149
|
+
const compiled = resolved.compiled;
|
|
150
|
+
if (compiled !== undefined) {
|
|
151
|
+
if (compiled.build !== undefined && typeof compiled.build !== "boolean" &&
|
|
152
|
+
compiled.build !== "always") {
|
|
153
|
+
throw new TypeError('worker.compiled.build must be a boolean or "always"');
|
|
154
|
+
}
|
|
155
|
+
for (const [name, value] of Object.entries({
|
|
156
|
+
artifact: compiled.artifact,
|
|
157
|
+
manifest: compiled.manifest,
|
|
158
|
+
compiler: compiled.compiler,
|
|
159
|
+
})) {
|
|
160
|
+
if (value !== undefined &&
|
|
161
|
+
(typeof value !== "string" || value.length === 0)) {
|
|
162
|
+
throw new TypeError("worker.compiled." + name + " must be a non-empty string");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
resolved = {
|
|
166
|
+
...resolved,
|
|
167
|
+
compiled: {
|
|
168
|
+
build: compiled.build,
|
|
169
|
+
compiler: compiled.compiler === undefined
|
|
170
|
+
? undefined
|
|
171
|
+
: compiled.compiler.startsWith(".") ||
|
|
172
|
+
compiled.compiler.startsWith("/") ||
|
|
173
|
+
compiled.compiler.startsWith("file:") ||
|
|
174
|
+
/^[A-Za-z]:[\\/]/.test(compiled.compiler)
|
|
175
|
+
? resolveImportHref(compiled.compiler, callerHref)
|
|
176
|
+
: compiled.compiler,
|
|
177
|
+
artifact: compiled.artifact === undefined
|
|
178
|
+
? undefined
|
|
179
|
+
: resolveImportHref(compiled.artifact, callerHref),
|
|
180
|
+
manifest: compiled.manifest === undefined
|
|
181
|
+
? undefined
|
|
182
|
+
: resolveImportHref(compiled.manifest, callerHref),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return resolved;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Check whether a task module has a compatible prebuilt native worker.
|
|
190
|
+
*
|
|
191
|
+
* By default tasks.ts resolves to tasks.knt plus tasks.knt.json.
|
|
192
|
+
* This only inspects the artifact; it never executes it.
|
|
193
|
+
*/
|
|
194
|
+
export const checkCompiledWorker = (source, options) => {
|
|
195
|
+
const moduleSource = typeof source === "string" || source instanceof URL
|
|
196
|
+
? source
|
|
197
|
+
: source?.importedFrom;
|
|
198
|
+
if (typeof moduleSource !== "string" && !(moduleSource instanceof URL)) {
|
|
199
|
+
throw new TypeError("checkCompiledWorker expects a task definition, module path, or URL");
|
|
200
|
+
}
|
|
201
|
+
const callerHref = getCallerHref(2);
|
|
202
|
+
const resolvedOptions = options === undefined ? undefined : {
|
|
203
|
+
build: options.build,
|
|
204
|
+
compiler: options.compiler,
|
|
205
|
+
artifact: options.artifact === undefined
|
|
206
|
+
? undefined
|
|
207
|
+
: resolveImportHref(options.artifact, callerHref),
|
|
208
|
+
manifest: options.manifest === undefined
|
|
209
|
+
? undefined
|
|
210
|
+
: resolveImportHref(options.manifest, callerHref),
|
|
101
211
|
};
|
|
212
|
+
const inspection = inspectCompiledWorkerArtifact({
|
|
213
|
+
source: moduleSource,
|
|
214
|
+
options: resolvedOptions,
|
|
215
|
+
});
|
|
216
|
+
const { artifactHref: _artifactHref, manifestHref: _manifestHref, taskEntries: _taskEntries, ...result } = inspection;
|
|
217
|
+
return result;
|
|
102
218
|
};
|
|
103
219
|
const isTaskDefinition = (value) => value != null &&
|
|
104
220
|
typeof value === "object" &&
|
|
@@ -206,7 +322,9 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
206
322
|
key === "--allow-fs-write" ||
|
|
207
323
|
key === "--allow-worker" ||
|
|
208
324
|
key === "--allow-child-process" ||
|
|
325
|
+
key === "--allow-net" ||
|
|
209
326
|
key === "--allow-addons" ||
|
|
327
|
+
key === "--allow-ffi" ||
|
|
210
328
|
key === "--allow-wasi";
|
|
211
329
|
};
|
|
212
330
|
const stripNodePermissionFlags = (flags) => flags?.filter((flag) => !isNodePermissionFlag(flag));
|
|
@@ -256,7 +374,54 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
256
374
|
hostDebug?.log(`modules=${formatDebugList(list)}`);
|
|
257
375
|
hostDebug?.log(`permission=${permissionProtocol?.mode ?? "off"} execArgv=${formatDebugList(execArgv)}`);
|
|
258
376
|
const usesAbortSignal = listOfFunctions.some((fn) => fn.abortSignal !== undefined);
|
|
259
|
-
const resolvedWorker =
|
|
377
|
+
const resolvedWorker = resolveWorkerSettings(worker, callerHref);
|
|
378
|
+
const usingCompiledWorker = resolvedWorker?.runtime === "compiled";
|
|
379
|
+
if (resolvedWorker?.compiled !== undefined && !usingCompiledWorker) {
|
|
380
|
+
throw new Error("worker.compiled requires worker.runtime to be compiled");
|
|
381
|
+
}
|
|
382
|
+
if (usingCompiledWorker) {
|
|
383
|
+
const unsupported = [];
|
|
384
|
+
if (usingInliner)
|
|
385
|
+
unsupported.push("inliner");
|
|
386
|
+
if (payload !== undefined)
|
|
387
|
+
unsupported.push("payload");
|
|
388
|
+
if (unsafe !== undefined)
|
|
389
|
+
unsupported.push("unsafe");
|
|
390
|
+
if (source !== undefined)
|
|
391
|
+
unsupported.push("source");
|
|
392
|
+
if (workerExecArgv !== undefined)
|
|
393
|
+
unsupported.push("workerExecArgv");
|
|
394
|
+
if (permission !== undefined)
|
|
395
|
+
unsupported.push("permission");
|
|
396
|
+
if (host !== undefined)
|
|
397
|
+
unsupported.push("host");
|
|
398
|
+
if (resolvedWorker.bootstrap !== undefined) {
|
|
399
|
+
unsupported.push("worker.bootstrap");
|
|
400
|
+
}
|
|
401
|
+
if (resolvedWorker.timers !== undefined)
|
|
402
|
+
unsupported.push("worker.timers");
|
|
403
|
+
if (resolvedWorker.processRuntime !== undefined) {
|
|
404
|
+
unsupported.push("worker.processRuntime");
|
|
405
|
+
}
|
|
406
|
+
if (resolvedWorker.processCommandPrefix !== undefined) {
|
|
407
|
+
unsupported.push("worker.processCommandPrefix");
|
|
408
|
+
}
|
|
409
|
+
if (resolvedWorker.processSharedMemory !== undefined) {
|
|
410
|
+
unsupported.push("worker.processSharedMemory");
|
|
411
|
+
}
|
|
412
|
+
if (resolvedWorker.resolveAfterFinishingAll !== undefined) {
|
|
413
|
+
unsupported.push("worker.resolveAfterFinishingAll");
|
|
414
|
+
}
|
|
415
|
+
if (listOfFunctions.some((fn) => fn.timeout !== undefined)) {
|
|
416
|
+
unsupported.push("task timeout");
|
|
417
|
+
}
|
|
418
|
+
if (listOfFunctions.some((fn) => fn.imported === true)) {
|
|
419
|
+
unsupported.push("importTask");
|
|
420
|
+
}
|
|
421
|
+
if (unsupported.length > 0) {
|
|
422
|
+
throw new Error("Compiled workers do not support: " + unsupported.join(", "));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
260
425
|
if (resolvedWorker?.bootstrap !== undefined) {
|
|
261
426
|
hostDebug?.log(`bootstrap href=${resolvedWorker.bootstrap.href}` +
|
|
262
427
|
` name=${resolvedWorker.bootstrap.name}`);
|
|
@@ -264,6 +429,19 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
264
429
|
if (usingInliner && resolvedWorker?.bootstrap !== undefined) {
|
|
265
430
|
throw new Error("worker.bootstrap cannot be used with the inliner");
|
|
266
431
|
}
|
|
432
|
+
if (resolvedWorker?.runtime === "process") {
|
|
433
|
+
const processRuntime = readProcessWorkerRuntime(resolvedWorker);
|
|
434
|
+
enforceProcessPermissionCompatibility(classifyProcessPermissionCompatibility({
|
|
435
|
+
permission,
|
|
436
|
+
resolved: permissionProtocol,
|
|
437
|
+
target: {
|
|
438
|
+
runtime: processRuntime,
|
|
439
|
+
nodeMajor: processRuntime === "node"
|
|
440
|
+
? readKnownProcessWorkerNodeMajor(resolvedWorker)
|
|
441
|
+
: undefined,
|
|
442
|
+
},
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
267
445
|
const hardTimeoutMs = Number.isFinite(resolvedWorker?.hardTimeoutMs)
|
|
268
446
|
? Math.max(1, Math.floor(resolvedWorker?.hardTimeoutMs))
|
|
269
447
|
: undefined;
|
|
@@ -292,7 +470,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
292
470
|
return "serial-channel";
|
|
293
471
|
})();
|
|
294
472
|
const dispatcher = explicitDispatcher ?? autoDispatcher;
|
|
295
|
-
const serialChannel =
|
|
473
|
+
const serialChannel = !usingCompiledWorker &&
|
|
474
|
+
dispatcher === "serial-channel";
|
|
296
475
|
const serialDispatcherChannel = serialChannel
|
|
297
476
|
? new ChannelHandler()
|
|
298
477
|
: undefined;
|
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,8 @@
|
|
|
1
|
-
type RuntimeName = "deno" | "bun" | "node" | "unknown";
|
|
1
|
+
type RuntimeName = "deno" | "bun" | "node" | "andromeda" | "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;
|
|
5
6
|
export declare const RUNTIME: RuntimeName;
|
|
6
7
|
export declare const SET_IMMEDIATE: typeof setImmediate & ((cb: () => void) => void);
|
|
7
8
|
export declare const HAS_SHARED_WASM_MEMORY: boolean;
|
package/src/common/runtime.js
CHANGED
|
@@ -4,16 +4,30 @@ 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
|
+
export const RUNTIME = (IS_DENO
|
|
11
|
+
? "deno"
|
|
12
|
+
: IS_BUN
|
|
13
|
+
? "bun"
|
|
14
|
+
: IS_NODE
|
|
15
|
+
? "node"
|
|
16
|
+
: IS_ANDROMEDA
|
|
17
|
+
? "andromeda"
|
|
18
|
+
: "unknown");
|
|
19
|
+
// `WebAssembly` may be undeclared (not just undefined) on Andromeda, where a
|
|
20
|
+
// bare reference throws; capture it through `globalThis` so access is safe.
|
|
21
|
+
const WebAssemblyRef = globals.WebAssembly;
|
|
8
22
|
export const SET_IMMEDIATE = typeof globals.setImmediate === "function" ? globals.setImmediate : undefined;
|
|
9
23
|
const WASM_MEMORY_PAGE_BYTES = 64 * 1024;
|
|
10
24
|
const wasmSharedBufferMemory = new WeakMap();
|
|
11
25
|
const wasmSharedBufferMaxByteLength = new WeakMap();
|
|
12
26
|
const hasSharedWasmMemory = (() => {
|
|
13
|
-
if (typeof
|
|
27
|
+
if (typeof WebAssemblyRef?.Memory !== "function")
|
|
14
28
|
return false;
|
|
15
29
|
try {
|
|
16
|
-
void new
|
|
30
|
+
void new WebAssemblyRef.Memory({ initial: 0, maximum: 1, shared: true });
|
|
17
31
|
return true;
|
|
18
32
|
}
|
|
19
33
|
catch {
|
|
@@ -23,7 +37,7 @@ const hasSharedWasmMemory = (() => {
|
|
|
23
37
|
export const HAS_SHARED_WASM_MEMORY = hasSharedWasmMemory;
|
|
24
38
|
const roundupWasmPages = (byteLength) => Math.ceil(Math.max(0, byteLength) / WASM_MEMORY_PAGE_BYTES);
|
|
25
39
|
const createSharedWasmBuffer = (byteLength, maxByteLength) => {
|
|
26
|
-
const memory = new
|
|
40
|
+
const memory = new WebAssemblyRef.Memory({
|
|
27
41
|
initial: roundupWasmPages(byteLength),
|
|
28
42
|
maximum: Math.max(roundupWasmPages(byteLength), roundupWasmPages(maxByteLength)),
|
|
29
43
|
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
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getNodeBuiltinModule, getNodeProcess } from "./node-compat.js";
|
|
2
|
+
import { IS_ANDROMEDA } from "./runtime.js";
|
|
2
3
|
export const RUNTIME_PROCESS_WORKER_ENV = "KNITTING_PROCESS_WORKER";
|
|
3
4
|
export const RUNTIME_PROCESS_WORKER_BOOT_ENV = "KNITTING_PROCESS_WORKER_BOOT";
|
|
4
5
|
export const RUNTIME_PROCESS_WORKER_BOOT_VERSION = 1;
|
|
@@ -16,15 +17,23 @@ export const RUNTIME_POOL_DEPTH = readPoolDepth();
|
|
|
16
17
|
const workerThreads = getNodeBuiltinModule("node:worker_threads");
|
|
17
18
|
const isWorkerGlobalScope = () => {
|
|
18
19
|
const scopeCtor = globalThis.WorkerGlobalScope;
|
|
19
|
-
if (typeof scopeCtor
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
if (typeof scopeCtor === "function") {
|
|
21
|
+
try {
|
|
22
|
+
if (globalThis instanceof scopeCtor) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// fall through to runtime-specific checks
|
|
28
|
+
}
|
|
24
29
|
}
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
// Andromeda has no `WorkerGlobalScope`; its worker scope is distinguished by a
|
|
31
|
+
// `self` global that the main thread lacks.
|
|
32
|
+
if (IS_ANDROMEDA &&
|
|
33
|
+
typeof globalThis.self !== "undefined") {
|
|
34
|
+
return true;
|
|
27
35
|
}
|
|
36
|
+
return false;
|
|
28
37
|
};
|
|
29
38
|
export const RUNTIME_WORKER = workerThreads?.Worker ??
|
|
30
39
|
(globalThis.Worker);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RUNTIME } from "../common/runtime.js";
|
|
2
2
|
import { loadNodeBufferPointerAddon } from "./node-buffer-pointer.js";
|
|
3
|
+
import { getNodeFfi, isNodeFfiTarget } from "./node-ffi.js";
|
|
3
4
|
const isSharedArrayBufferInstance = (value) => typeof SharedArrayBuffer === "function" && value instanceof SharedArrayBuffer;
|
|
4
5
|
const assertMovableSource = (source) => {
|
|
5
6
|
const buffer = ArrayBuffer.isView(source) ? source.buffer : source;
|
|
@@ -132,6 +133,27 @@ const createBunCapabilities = () => {
|
|
|
132
133
|
releaseShared: releaseFfi,
|
|
133
134
|
};
|
|
134
135
|
};
|
|
136
|
+
const createNodeFfiCapabilities = () => {
|
|
137
|
+
const ffi = getNodeFfi();
|
|
138
|
+
const readPointer = (region) => ffi.getRawPointer(region);
|
|
139
|
+
return {
|
|
140
|
+
runtime: "node",
|
|
141
|
+
supportsOwningAdopt: false,
|
|
142
|
+
produce: (view) => produceFfi(view, readPointer),
|
|
143
|
+
adopt: ({ pointer, byteLength }, opts) => {
|
|
144
|
+
const alias = ffi.toArrayBuffer(pointer, byteLength, false);
|
|
145
|
+
return finishAlias(alias, opts?.copy);
|
|
146
|
+
},
|
|
147
|
+
release: releaseFfi,
|
|
148
|
+
supportsSharedAdopt: false,
|
|
149
|
+
produceShared: (sab) => produceSharedFfi(sab, readPointer),
|
|
150
|
+
adoptShared: ({ pointer, byteLength }) => {
|
|
151
|
+
const alias = ffi.toArrayBuffer(pointer, byteLength, false);
|
|
152
|
+
return { buffer: alias, byteOffset: 0, byteLength, isShared: false };
|
|
153
|
+
},
|
|
154
|
+
releaseShared: releaseFfi,
|
|
155
|
+
};
|
|
156
|
+
};
|
|
135
157
|
// ---------------------------------------------------------------------------
|
|
136
158
|
// Node: addon-backed owning move via shared_ptr<BackingStore>.
|
|
137
159
|
// Older prebuilds fall back to non-owning retain/wrap.
|
|
@@ -206,7 +228,9 @@ export const getBufferReferenceCapabilities = () => {
|
|
|
206
228
|
return cached;
|
|
207
229
|
switch (RUNTIME) {
|
|
208
230
|
case "node":
|
|
209
|
-
return cached =
|
|
231
|
+
return cached = isNodeFfiTarget()
|
|
232
|
+
? createNodeFfiCapabilities()
|
|
233
|
+
: createNodeCapabilities();
|
|
210
234
|
case "deno":
|
|
211
235
|
return cached = createDenoCapabilities();
|
|
212
236
|
case "bun":
|
|
@@ -38,11 +38,24 @@ export type BufferReferenceReturnReleaseMessage = {
|
|
|
38
38
|
readonly [BUFFER_REFERENCE_RETURN_RELEASE_MESSAGE_KEY]: string;
|
|
39
39
|
};
|
|
40
40
|
type BufferReferenceReturnReleaser = (token: bigint) => void;
|
|
41
|
+
/** Host-side hooks for borrowed returns: release channel plus shutdown tracking. */
|
|
42
|
+
export type BufferReferenceReturnHooks = {
|
|
43
|
+
/** Ask the producing worker to drop its pin for `token`. */
|
|
44
|
+
release: BufferReferenceReturnReleaser;
|
|
45
|
+
/**
|
|
46
|
+
* Record a borrowed reference so the pool can revoke it before its worker
|
|
47
|
+
* dies. Called at claim time without `aliasBuffer`, then again on first
|
|
48
|
+
* materialize with the alias buffer once it exists.
|
|
49
|
+
*/
|
|
50
|
+
track?: (ref: BufferReference, token: bigint, aliasBuffer?: ArrayBuffer) => void;
|
|
51
|
+
};
|
|
41
52
|
type BufferSource = ArrayBufferView | ArrayBuffer;
|
|
42
53
|
declare const PAYLOAD_TRANSPORT_FINALIZER: unique symbol;
|
|
43
|
-
export declare const withBufferReferenceReturnReleaser: <T>(releaser: BufferReferenceReturnReleaser | undefined, run: () => T) => T;
|
|
54
|
+
export declare const withBufferReferenceReturnReleaser: <T>(releaser: BufferReferenceReturnReleaser | BufferReferenceReturnHooks | undefined, run: () => T) => T;
|
|
44
55
|
export declare const createBufferReferenceReturnReleaseMessage: (token: bigint) => BufferReferenceReturnReleaseMessage;
|
|
45
56
|
export declare const readBufferReferenceReturnReleaseMessage: (value: unknown) => bigint | undefined;
|
|
57
|
+
/** Revocation primitive: detach `buffer` so every view over it is neutralized. */
|
|
58
|
+
export declare const detachArrayBufferBestEffort: (runtime: BufferReferenceRuntime, buffer: ArrayBuffer) => boolean;
|
|
46
59
|
export declare const isBufferReferenceMetadata: (value: unknown) => value is BufferReferenceMetadata;
|
|
47
60
|
/**
|
|
48
61
|
* Zero-copy handle for moving ArrayBuffer bytes to/from **thread** workers.
|
|
@@ -67,12 +80,20 @@ export declare class BufferReference {
|
|
|
67
80
|
[BUFFER_REFERENCE_NUMERIC_TRANSFER](): BufferReferenceNumericMetadata | undefined;
|
|
68
81
|
get isLocal(): boolean;
|
|
69
82
|
/** Prepare a returned reference before the worker-side producer hold drains. */
|
|
70
|
-
claimOwnership(releaser?: BufferReferenceReturnReleaser): this;
|
|
83
|
+
claimOwnership(releaser?: BufferReferenceReturnReleaser | BufferReferenceReturnHooks | undefined): this;
|
|
71
84
|
toArrayBuffer(): ArrayBuffer;
|
|
72
85
|
toUint8Array(): Uint8Array;
|
|
73
86
|
/** The source is moved on construction, so it is never retained here. */
|
|
74
87
|
get source(): undefined;
|
|
75
88
|
release(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Revoke a borrowed return before its producing worker goes away: detach
|
|
91
|
+
* the borrowed alias so no view can dangle. With `copyBytes` the reference
|
|
92
|
+
* itself survives on an owned copy; without it (worker bytes already gone)
|
|
93
|
+
* later reads fail loud instead of aliasing freed memory.
|
|
94
|
+
*/
|
|
95
|
+
revokeBorrow(copyBytes: boolean): void;
|
|
76
96
|
[Symbol.dispose](): void;
|
|
77
97
|
[PAYLOAD_TRANSPORT_FINALIZER](): (() => void) | undefined;
|
|
78
98
|
}
|
|
99
|
+
export declare const isBufferReferenceValue: (value: unknown) => value is BufferReference;
|