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.
Files changed (101) hide show
  1. package/BROWSER.md +134 -0
  2. package/README.md +347 -46
  3. package/knitting.browser.d.ts +6 -0
  4. package/knitting.browser.js +1 -0
  5. package/knitting.d.ts +3 -2
  6. package/knitting.js +3 -3
  7. package/map.md +21 -9
  8. package/package.json +17 -2
  9. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
  11. package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
  12. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
  14. package/prebuilds/darwin-arm64-node-137/knitting_shm.node +0 -0
  15. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  16. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  18. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  19. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  20. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  21. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  22. package/scripts/build-compiled-worker.ts +276 -0
  23. package/scripts/compiled-worker/porffor.d.ts +5 -0
  24. package/scripts/compiled-worker/runtime.ts +529 -0
  25. package/scripts/compiled-worker/task-shim.ts +32 -0
  26. package/src/api.d.ts +24 -1
  27. package/src/api.js +311 -27
  28. package/src/common/module-url.d.ts +1 -0
  29. package/src/common/module-url.js +30 -2
  30. package/src/common/node-compat.d.ts +2 -0
  31. package/src/common/path-canonical.js +20 -6
  32. package/src/common/runtime.d.ts +3 -1
  33. package/src/common/runtime.js +25 -4
  34. package/src/common/task-source.d.ts +2 -0
  35. package/src/common/task-source.js +17 -1
  36. package/src/common/worker-runtime.js +16 -7
  37. package/src/connections/buffer-reference-native.js +25 -1
  38. package/src/connections/buffer-reference.d.ts +23 -2
  39. package/src/connections/buffer-reference.js +143 -29
  40. package/src/connections/bun.d.ts +1 -1
  41. package/src/connections/bun.js +25 -14
  42. package/src/connections/deno.d.ts +3 -1
  43. package/src/connections/deno.js +35 -14
  44. package/src/connections/external-array-buffer.d.ts +2 -0
  45. package/src/connections/external-array-buffer.js +33 -0
  46. package/src/connections/node-addons.d.ts +11 -0
  47. package/src/connections/node-addons.js +49 -1
  48. package/src/connections/node-ffi-api.d.ts +19 -0
  49. package/src/connections/node-ffi-api.js +28 -0
  50. package/src/connections/node-ffi.d.ts +27 -0
  51. package/src/connections/node-ffi.js +262 -0
  52. package/src/connections/node.d.ts +1 -1
  53. package/src/connections/node.js +20 -20
  54. package/src/connections/package-assets.js +25 -15
  55. package/src/connections/posix.d.ts +42 -0
  56. package/src/connections/posix.js +101 -0
  57. package/src/connections/process-shared-buffer.d.ts +1 -0
  58. package/src/connections/process-shared-buffer.js +11 -45
  59. package/src/connections/types.d.ts +4 -0
  60. package/src/connections/windows.d.ts +19 -10
  61. package/src/connections/windows.js +87 -66
  62. package/src/error.d.ts +7 -6
  63. package/src/error.js +8 -7
  64. package/src/memory/lock.d.ts +112 -77
  65. package/src/memory/lock.js +314 -87
  66. package/src/memory/payloadCodec.d.ts +11 -2
  67. package/src/memory/payloadCodec.js +129 -37
  68. package/src/memory/regionRegistry.d.ts +1 -3
  69. package/src/memory/regionRegistry.js +16 -14
  70. package/src/permission/compatibility.d.ts +28 -0
  71. package/src/permission/compatibility.js +239 -0
  72. package/src/permission/index.d.ts +2 -0
  73. package/src/permission/index.js +1 -0
  74. package/src/permission/protocol.d.ts +1 -0
  75. package/src/permission/protocol.js +59 -15
  76. package/src/runtime/compiled-artifact.d.ts +21 -0
  77. package/src/runtime/compiled-artifact.js +260 -0
  78. package/src/runtime/compiled-builder.d.ts +8 -0
  79. package/src/runtime/compiled-builder.js +42 -0
  80. package/src/runtime/compiled-worker.d.ts +27 -0
  81. package/src/runtime/compiled-worker.js +562 -0
  82. package/src/runtime/dispatcher.js +9 -0
  83. package/src/runtime/inline-executor.js +27 -15
  84. package/src/runtime/pool.d.ts +121 -5
  85. package/src/runtime/pool.js +263 -49
  86. package/src/runtime/process-worker.d.ts +3 -13
  87. package/src/runtime/process-worker.js +97 -101
  88. package/src/runtime/tx-queue.d.ts +18 -2
  89. package/src/runtime/tx-queue.js +56 -16
  90. package/src/runtime/worker-common.d.ts +13 -0
  91. package/src/runtime/worker-common.js +102 -0
  92. package/src/types.d.ts +89 -9
  93. package/src/worker/composable-runners.js +3 -2
  94. package/src/worker/loop.d.ts +1 -0
  95. package/src/worker/loop.js +52 -17
  96. package/src/worker/rx-queue.d.ts +11 -1
  97. package/src/worker/rx-queue.js +4 -1
  98. package/src/worker/task-loader.d.ts +5 -4
  99. package/src/worker/task-loader.js +13 -7
  100. package/src/worker/timers.d.ts +8 -1
  101. 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 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");
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
- if (typeof name !== "string" || name.length === 0) {
92
- throw new TypeError("worker.bootstrap.name must be a non-empty string");
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
- return {
95
- ...worker,
96
- bootstrap: {
97
- ...bootstrap,
98
- href: resolveImportHref(bootstrap.href, callerHref),
99
- name,
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 = resolveWorkerBootstrapSettings(worker, callerHref);
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
- const serialChannel = dispatcher === "serial-channel";
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
- for (let index = 0; index < checks.length; index++) {
338
- const check = checks[index];
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
  };
@@ -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,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;
@@ -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
- 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
+ // 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 WebAssembly?.Memory !== "function")
34
+ if (typeof WebAssemblyRef?.Memory !== "function")
14
35
  return false;
15
36
  try {
16
- void new WebAssembly.Memory({ initial: 0, maximum: 1, shared: true });
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 WebAssembly.Memory({
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
  };