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.
Files changed (77) hide show
  1. package/README.md +254 -40
  2. package/knitting.d.ts +3 -2
  3. package/knitting.js +3 -3
  4. package/map.md +21 -9
  5. package/package.json +7 -1
  6. package/scripts/build-compiled-worker.ts +276 -0
  7. package/scripts/compiled-worker/porffor.d.ts +5 -0
  8. package/scripts/compiled-worker/runtime.ts +529 -0
  9. package/scripts/compiled-worker/task-shim.ts +32 -0
  10. package/src/api.d.ts +24 -1
  11. package/src/api.js +199 -20
  12. package/src/common/module-url.d.ts +1 -0
  13. package/src/common/module-url.js +30 -2
  14. package/src/common/node-compat.d.ts +2 -0
  15. package/src/common/path-canonical.js +20 -6
  16. package/src/common/runtime.d.ts +2 -1
  17. package/src/common/runtime.js +18 -4
  18. package/src/common/task-source.d.ts +2 -0
  19. package/src/common/task-source.js +17 -1
  20. package/src/common/worker-runtime.js +16 -7
  21. package/src/connections/buffer-reference-native.js +25 -1
  22. package/src/connections/buffer-reference.d.ts +23 -2
  23. package/src/connections/buffer-reference.js +143 -29
  24. package/src/connections/bun.d.ts +1 -1
  25. package/src/connections/bun.js +25 -14
  26. package/src/connections/deno.d.ts +3 -1
  27. package/src/connections/deno.js +35 -14
  28. package/src/connections/external-array-buffer.d.ts +2 -0
  29. package/src/connections/external-array-buffer.js +33 -0
  30. package/src/connections/node-addons.d.ts +11 -0
  31. package/src/connections/node-addons.js +49 -1
  32. package/src/connections/node-ffi-api.d.ts +19 -0
  33. package/src/connections/node-ffi-api.js +28 -0
  34. package/src/connections/node-ffi.d.ts +27 -0
  35. package/src/connections/node-ffi.js +262 -0
  36. package/src/connections/node.d.ts +1 -1
  37. package/src/connections/node.js +20 -20
  38. package/src/connections/package-assets.js +25 -15
  39. package/src/connections/posix.d.ts +42 -0
  40. package/src/connections/posix.js +101 -0
  41. package/src/connections/process-shared-buffer.d.ts +1 -0
  42. package/src/connections/process-shared-buffer.js +11 -45
  43. package/src/connections/types.d.ts +4 -0
  44. package/src/connections/windows.d.ts +19 -10
  45. package/src/connections/windows.js +87 -66
  46. package/src/error.d.ts +7 -6
  47. package/src/error.js +8 -7
  48. package/src/memory/lock.d.ts +87 -77
  49. package/src/memory/lock.js +103 -84
  50. package/src/memory/payloadCodec.d.ts +11 -2
  51. package/src/memory/payloadCodec.js +127 -31
  52. package/src/permission/compatibility.d.ts +28 -0
  53. package/src/permission/compatibility.js +239 -0
  54. package/src/permission/index.d.ts +2 -0
  55. package/src/permission/index.js +1 -0
  56. package/src/permission/protocol.d.ts +1 -0
  57. package/src/permission/protocol.js +54 -14
  58. package/src/runtime/compiled-artifact.d.ts +21 -0
  59. package/src/runtime/compiled-artifact.js +260 -0
  60. package/src/runtime/compiled-builder.d.ts +8 -0
  61. package/src/runtime/compiled-builder.js +42 -0
  62. package/src/runtime/compiled-worker.d.ts +27 -0
  63. package/src/runtime/compiled-worker.js +562 -0
  64. package/src/runtime/inline-executor.js +27 -15
  65. package/src/runtime/pool.d.ts +7 -2
  66. package/src/runtime/pool.js +74 -6
  67. package/src/runtime/process-worker.d.ts +1 -1
  68. package/src/runtime/process-worker.js +48 -8
  69. package/src/runtime/tx-queue.d.ts +3 -1
  70. package/src/runtime/tx-queue.js +4 -1
  71. package/src/types.d.ts +48 -6
  72. package/src/worker/composable-runners.js +3 -2
  73. package/src/worker/loop.d.ts +1 -0
  74. package/src/worker/loop.js +25 -12
  75. package/src/worker/task-loader.d.ts +5 -4
  76. package/src/worker/task-loader.js +13 -7
  77. 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 resolveWorkerBootstrapSettings = (worker, callerHref) => {
84
- const bootstrap = worker?.bootstrap;
85
- if (bootstrap === undefined)
86
- return worker;
87
- const name = bootstrap.name ?? DEFAULT_IMPORT_EXPORT_NAME;
88
- if (typeof bootstrap.href !== "string" || bootstrap.href.length === 0) {
89
- throw new TypeError("worker.bootstrap.href must be a non-empty string");
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
- if (typeof name !== "string" || name.length === 0) {
92
- throw new TypeError("worker.bootstrap.name must be a non-empty string");
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
- return {
95
- ...worker,
96
- bootstrap: {
97
- ...bootstrap,
98
- href: resolveImportHref(bootstrap.href, callerHref),
99
- name,
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 = resolveWorkerBootstrapSettings(worker, callerHref);
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 = dispatcher === "serial-channel";
473
+ const serialChannel = !usingCompiledWorker &&
474
+ dispatcher === "serial-channel";
296
475
  const serialDispatcherChannel = serialChannel
297
476
  ? new ChannelHandler()
298
477
  : undefined;
@@ -1 +1,2 @@
1
1
  export declare const toModuleUrl: (specifier: string) => string;
2
+ export declare const toImportSpecifier: (moduleUrl: string) => string;
@@ -1,9 +1,18 @@
1
- import { pathToFileURL } from "node:url";
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
- return pathToFileURL(specifier).href;
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,8 +1,10 @@
1
1
  export type NodeProcessLike = {
2
2
  getBuiltinModule?: (id: string) => unknown;
3
3
  versions?: {
4
+ modules?: string;
4
5
  node?: string;
5
6
  };
7
+ pid?: number;
6
8
  platform?: string;
7
9
  allowedNodeEnvironmentFlags?: ReadonlySet<string>;
8
10
  execArgv?: string[];
@@ -1,9 +1,23 @@
1
- import { existsSync, realpathSync, } from "node:fs";
2
- import { basename as pathBasename, dirname as pathDirname, join as pathJoin, resolve as pathResolve, } from "node:path";
3
- export const toCanonicalPath = (candidate, fsApi = {
4
- existsSync,
5
- realpathSync: realpathSync.native ?? realpathSync,
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") {
@@ -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;
@@ -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
- export const RUNTIME = (IS_DENO ? "deno" : IS_BUN ? "bun" : IS_NODE ? "node" : "unknown");
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 WebAssembly?.Memory !== "function")
27
+ if (typeof WebAssemblyRef?.Memory !== "function")
14
28
  return false;
15
29
  try {
16
- void new WebAssembly.Memory({ initial: 0, maximum: 1, shared: true });
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 WebAssembly.Memory({
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 !== "function")
20
- return false;
21
- try {
22
- return globalThis instanceof
23
- scopeCtor;
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
- catch {
26
- return false;
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 = createNodeCapabilities();
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;