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
@@ -1,8 +1,8 @@
1
- import { fileURLToPath as fileURLToPathCompat } from "node:url";
2
1
  import { HEADER_SLOT_STRIDE_U32, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
3
2
  import { createByteCarpet, getHeaderBlockByteLength, makeSharedBufferRegion, } from "../memory/byte-carpet.js";
4
3
  import { RUNTIME } from "../common/runtime.js";
5
4
  import { debugHas } from "../debug/gate.js";
5
+ import { toWorkerCompatExecArgv, } from "./worker-common.js";
6
6
  import { RUNTIME_POOL_DEPTH, RUNTIME_POOL_DEPTH_ENV, RUNTIME_PROCESS_WORKER_BOOT_ENV, RUNTIME_PROCESS_WORKER_BOOT_VERSION, RUNTIME_PROCESS_WORKER_ENV, } from "../common/worker-runtime.js";
7
7
  import { getNodeBuiltinModule, getNodeProcess } from "../common/node-compat.js";
8
8
  import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
@@ -12,47 +12,14 @@ import { FileDescriptor, ProcessSharedBuffer, } from "../connections/index.js";
12
12
  import { createNodeConnectionPrimitives, loadNodeFutexAddon, } from "../connections/node.js";
13
13
  import { loadNodeNativeAddon } from "../connections/node-addons.js";
14
14
  import { detectPosixPlatform } from "../connections/posix.js";
15
- const execFlagKey = (flag) => flag.split("=", 1)[0];
16
- const NODE_PERMISSION_EXEC_FLAGS = new Set([
17
- "--permission",
18
- "--experimental-permission",
19
- "--allow-fs-read",
20
- "--allow-fs-write",
21
- "--allow-worker",
22
- "--allow-child-process",
23
- "--allow-addons",
24
- "--allow-wasi",
25
- ]);
26
- const NODE_WORKER_SAFE_EXEC_FLAGS = new Set([
27
- "--experimental-transform-types",
28
- "--expose-gc",
29
- "--no-warnings",
30
- ...NODE_PERMISSION_EXEC_FLAGS,
31
- ]);
32
- const isNodeWorkerSafeExecFlag = (flag) => NODE_WORKER_SAFE_EXEC_FLAGS.has(execFlagKey(flag));
33
- const isNodePermissionExecFlag = (flag) => NODE_PERMISSION_EXEC_FLAGS.has(execFlagKey(flag));
34
- export const toWorkerSafeExecArgv = (flags) => {
35
- if (!flags || flags.length === 0)
36
- return undefined;
37
- const filtered = flags.filter(isNodeWorkerSafeExecFlag);
38
- if (filtered.length === 0)
39
- return undefined;
40
- const seen = new Set();
41
- const deduped = [];
42
- for (const flag of filtered) {
43
- if (seen.has(flag))
44
- continue;
45
- seen.add(flag);
46
- deduped.push(flag);
15
+ // `node:url` resolved lazily so this module evaluates on runtimes without it
16
+ // (e.g. Andromeda); process workers aren't used there, so it never runs.
17
+ const fileURLToPathCompat = (value) => {
18
+ const url = getNodeBuiltinModule("node:url");
19
+ if (url === undefined) {
20
+ throw new Error("node:url is not available in this runtime");
47
21
  }
48
- return deduped;
49
- };
50
- export const toWorkerCompatExecArgv = (flags) => {
51
- const safe = toWorkerSafeExecArgv(flags);
52
- if (!safe || safe.length === 0)
53
- return undefined;
54
- const compat = safe.filter((flag) => !isNodePermissionExecFlag(flag));
55
- return compat.length > 0 ? compat : undefined;
22
+ return url.fileURLToPath(value);
56
23
  };
57
24
  const toProcessSharedMemorySize = (byteLength) => {
58
25
  if (!Number.isFinite(byteLength) || byteLength <= 0) {
@@ -107,10 +74,22 @@ const DENO_PROCESS_WORKER_INTERNAL_FLAGS = [
107
74
  `--allow-env=${DENO_PROCESS_WORKER_BOOT_ENV_ALLOW}`,
108
75
  "--allow-ffi",
109
76
  ];
110
- const NODE_PROCESS_WORKER_EXEC_ARGV = [
111
- "--no-warnings",
112
- "--experimental-transform-types",
113
- ];
77
+ const parseNodeMajorVersion = (version) => {
78
+ const match = /^v?(\d+)/.exec(String(version).trim());
79
+ if (match === null)
80
+ return undefined;
81
+ const major = Number.parseInt(match[1], 10);
82
+ return Number.isInteger(major) && major > 0 ? major : undefined;
83
+ };
84
+ const nodeProcessWorkerUsesFfi = (nodeMajor) => nodeMajor !== undefined && nodeMajor >= 26 && nodeMajor % 2 === 0;
85
+ const nodeProcessWorkerInternalExecArgv = (nodeMajor) => {
86
+ return [
87
+ "--no-warnings",
88
+ ...(nodeProcessWorkerUsesFfi(nodeMajor)
89
+ ? ["--experimental-ffi"]
90
+ : ["--experimental-transform-types"]),
91
+ ];
92
+ };
114
93
  const getProcessWorkerSharedMemoryPrimitives = () => {
115
94
  switch (RUNTIME) {
116
95
  case "bun":
@@ -230,49 +209,6 @@ const toProcessWorkerWireLockBuffers = (lock, descriptor) => ({
230
209
  payload: toChildProcessSharedBufferMetadata(lock.payload, descriptor),
231
210
  payloadSector: toChildProcessSharedBufferMetadata(lock.payloadSector, descriptor),
232
211
  });
233
- const isPlainRecord = (value) => {
234
- if (value === null || typeof value !== "object")
235
- return false;
236
- const prototype = Object.getPrototypeOf(value);
237
- return prototype === Object.prototype || prototype === null;
238
- };
239
- const serializeWorkerBootstrapValue = (value, seen = new WeakMap()) => {
240
- if (value instanceof ProcessSharedBuffer)
241
- return value.toMetadata();
242
- if (value === null || typeof value !== "object")
243
- return value;
244
- const existing = seen.get(value);
245
- if (existing !== undefined)
246
- return existing;
247
- if (Array.isArray(value)) {
248
- const out = [];
249
- seen.set(value, out);
250
- for (const item of value) {
251
- out.push(serializeWorkerBootstrapValue(item, seen));
252
- }
253
- return out;
254
- }
255
- if (!isPlainRecord(value))
256
- return value;
257
- const out = {};
258
- seen.set(value, out);
259
- for (const [key, item] of Object.entries(value)) {
260
- out[key] = serializeWorkerBootstrapValue(item, seen);
261
- }
262
- return out;
263
- };
264
- export const serializeWorkerBootstrapData = (options) => {
265
- const bootstrap = options.bootstrap;
266
- if (bootstrap === undefined || bootstrap.data === undefined)
267
- return options;
268
- return {
269
- ...options,
270
- bootstrap: {
271
- ...bootstrap,
272
- data: serializeWorkerBootstrapValue(bootstrap.data),
273
- },
274
- };
275
- };
276
212
  export const toProcessWorkerBootPayload = (workerData, memory) => ({
277
213
  version: RUNTIME_PROCESS_WORKER_BOOT_VERSION,
278
214
  workerData: {
@@ -393,10 +329,62 @@ const processWorkerNodeBinary = (commandPrefix) => {
393
329
  : undefined) ??
394
330
  DEFAULT_NODE_BINARY;
395
331
  };
396
- const processWorkerNodeExecArgv = () => {
332
+ const processWorkerNodeMajorCache = new Map();
333
+ const resolveProcessWorkerNodeMajor = (commandPrefix) => {
334
+ const nodeProcess = getNodeProcess();
335
+ if (commandPrefix === undefined &&
336
+ nodeProcess !== undefined &&
337
+ nodeProcess?.env?.NODE_BINARY === undefined &&
338
+ RUNTIME === "node") {
339
+ return parseNodeMajorVersion(nodeProcess.versions?.node);
340
+ }
341
+ const nodeBinary = processWorkerNodeBinary(commandPrefix);
342
+ const cacheKey = JSON.stringify([commandPrefix ?? null, nodeBinary]);
343
+ if (processWorkerNodeMajorCache.has(cacheKey)) {
344
+ return processWorkerNodeMajorCache.get(cacheKey);
345
+ }
346
+ let major;
347
+ try {
348
+ const childProcess = getNodeBuiltinModule("node:child_process");
349
+ if (typeof childProcess?.spawnSync === "function") {
350
+ const command = commandPrefix?.[0] ?? nodeBinary;
351
+ const args = commandPrefix === undefined
352
+ ? ["--version"]
353
+ : [...commandPrefix.slice(1), nodeBinary, "--version"];
354
+ const probe = childProcess.spawnSync(command, args, {
355
+ encoding: "utf8",
356
+ stdio: ["ignore", "pipe", "ignore"],
357
+ timeout: 2_000,
358
+ windowsHide: true,
359
+ });
360
+ if (probe.status === 0) {
361
+ major = parseNodeMajorVersion(probe.stdout);
362
+ }
363
+ }
364
+ }
365
+ catch {
366
+ // Unknown targets conservatively use the Node 22/24 addon transport.
367
+ }
368
+ processWorkerNodeMajorCache.set(cacheKey, major);
369
+ return major;
370
+ };
371
+ export const readProcessWorkerNodeMajor = (options) => resolveProcessWorkerNodeMajor(readProcessWorkerCommandPrefix(options));
372
+ const nodeProcessWorkerFlagIsSupported = (flag, nodeMajor) => {
373
+ const key = flag.split("=", 1)[0];
374
+ if (key === "--experimental-ffi" || key === "--allow-ffi") {
375
+ return nodeProcessWorkerUsesFfi(nodeMajor);
376
+ }
377
+ if (key === "--allow-net") {
378
+ return nodeMajor !== undefined && nodeMajor >= 25;
379
+ }
380
+ return true;
381
+ };
382
+ const processWorkerNodeExecArgv = (permission, nodeMajor) => {
397
383
  const out = [];
398
384
  const seen = new Set();
399
385
  const add = (flag) => {
386
+ if (!nodeProcessWorkerFlagIsSupported(flag, nodeMajor))
387
+ return;
400
388
  if (seen.has(flag))
401
389
  return;
402
390
  seen.add(flag);
@@ -405,8 +393,18 @@ const processWorkerNodeExecArgv = () => {
405
393
  for (const flag of toWorkerCompatExecArgv(getNodeProcess()?.execArgv) ?? []) {
406
394
  add(flag);
407
395
  }
408
- for (const flag of NODE_PROCESS_WORKER_EXEC_ARGV)
396
+ for (const flag of nodeProcessWorkerInternalExecArgv(nodeMajor))
409
397
  add(flag);
398
+ if (permission?.enabled === true && permission.unsafe !== true) {
399
+ for (const flag of permission.node.flags)
400
+ add(flag);
401
+ if (permission.netAll && nodeMajor !== undefined && nodeMajor >= 25) {
402
+ add("--allow-net");
403
+ }
404
+ // Node process workers need one native transport capability regardless of
405
+ // task permissions: addons on Node 22/24, node:ffi on supported Node 26+ LTS.
406
+ add(nodeProcessWorkerUsesFfi(nodeMajor) ? "--allow-ffi" : "--allow-addons");
407
+ }
410
408
  return out;
411
409
  };
412
410
  const processWorkerCommand = ({ processRuntime, workerUrl, bun, deno, commandPrefix, permission, }) => {
@@ -421,9 +419,10 @@ const processWorkerCommand = ({ processRuntime, workerUrl, bun, deno, commandPre
421
419
  ];
422
420
  }
423
421
  else if (processRuntime === "node") {
422
+ const nodeMajor = resolveProcessWorkerNodeMajor(commandPrefix);
424
423
  command = [
425
424
  processWorkerNodeBinary(commandPrefix),
426
- ...processWorkerNodeExecArgv(),
425
+ ...processWorkerNodeExecArgv(permission, nodeMajor),
427
426
  workerPath,
428
427
  ];
429
428
  }
@@ -654,20 +653,17 @@ export const spawnProcessWorker = (options) => {
654
653
  throw new Error("process worker runtime is only available in Node, Deno, or Bun");
655
654
  }
656
655
  };
657
- export const terminateWorkerQuietly = (worker) => {
658
- try {
659
- // Runaway worker termination can be slow or stuck on some runtimes; once the
660
- // pool is closing it must not keep the host process alive.
661
- worker.unref?.();
662
- void Promise.resolve(worker.terminate()).catch(() => { });
663
- }
664
- catch {
665
- }
666
- };
667
656
  export const cleanupProcessWorkerMemoryQuietly = (memory) => {
668
657
  try {
669
658
  memory?.cleanup();
670
659
  }
671
660
  catch {
672
661
  }
662
+ finally {
663
+ try {
664
+ memory?.mapping.close?.();
665
+ }
666
+ catch {
667
+ }
668
+ }
673
669
  };
@@ -1,6 +1,8 @@
1
+ import "../memory/payloadCodec.js";
1
2
  import { type Lock2, type Task } from "../memory/lock.js";
2
3
  import type { AbortSignalOption, TaskTimeout } from "../types.js";
3
4
  import { type SignalAbortStore } from "../shared/abortSignal.js";
5
+ import { type BufferReferenceReturnHooks } from "../connections/buffer-reference.js";
4
6
  type RawArguments = unknown;
5
7
  type FunctionID = number;
6
8
  type QueueTask = Task;
@@ -9,15 +11,29 @@ type CreateHostTxQueueArgs = {
9
11
  max?: number;
10
12
  lock: Lock2;
11
13
  returnLock: Lock2;
12
- releaseBufferReferenceReturn?: (token: bigint) => void;
14
+ /**
15
+ * Extra return locks to drain into the same pending registry.
16
+ *
17
+ * Under work stealing the host publishes into one shared submit lock and any
18
+ * worker may claim the task, so the response comes back on whichever lane
19
+ * executed it. Task IDs index this queue's `queue` array, which is
20
+ * pool-global, so every lane's `resolveHost` can settle into it — the
21
+ * stealing worker owns the response and the host demultiplexes by ID.
22
+ *
23
+ * When omitted, only `returnLock` is drained (the classic one-lane shape).
24
+ */
25
+ extraReturnLocks?: readonly Lock2[];
26
+ releaseBufferReferenceReturn?: ((token: bigint) => void) | BufferReferenceReturnHooks;
13
27
  abortSignals?: Pick<SignalAbortStore, "getSignal" | "setSignal" | "resetSignal" | "closeNow">;
14
28
  now?: () => number;
15
29
  };
16
- export declare function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenceReturn, abortSignals, now, }: CreateHostTxQueueArgs): {
30
+ type ReturnHooks = ((token: bigint) => void) | BufferReferenceReturnHooks;
31
+ export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, releaseBufferReferenceReturn, abortSignals, now, }: CreateHostTxQueueArgs): {
17
32
  rejectAll: (reason: string) => void;
18
33
  hasPendingFrames: () => boolean;
19
34
  txIdle: () => boolean;
20
35
  completeFrame: () => number;
36
+ setReturnHooks: (lane: number, hooks: ReturnHooks | undefined) => void;
21
37
  enqueue: (functionID: FunctionID, timeout?: TaskTimeout, abortSignal?: AbortSignalOption) => (rawArgs: RawArguments) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
22
38
  flushToWorker: () => boolean;
23
39
  enqueueKnown: (task: QueueTask) => boolean;
@@ -1,7 +1,10 @@
1
+ // Side-effect import: registers the payload codec (cycle break for Andromeda;
2
+ // see lock.ts), ensuring registration before any lock2() call.
3
+ import "../memory/payloadCodec.js";
1
4
  import { makeTask, resetTaskLocalFlags, runTaskFinalizers, TaskIndex, } from "../memory/lock.js";
2
5
  import { withResolvers } from "../common/with-resolvers.js";
3
6
  import { AbortSignalPoolExhausted, OneShotDeferred, } from "../shared/abortSignal.js";
4
- import { withBufferReferenceReturnReleaser } from "../connections/buffer-reference.js";
7
+ import { withBufferReferenceReturnReleaser, } from "../connections/buffer-reference.js";
5
8
  const SLOT_INDEX_MASK = 31;
6
9
  const SLOT_META_MASK = 0x07ffffff;
7
10
  const SLOT_META_SHIFT = 5;
@@ -11,7 +14,7 @@ const FUNCTION_META_SHIFT = 16;
11
14
  const ABORT_SIGNAL_META_OFFSET = 1;
12
15
  const NO_ABORT_SIGNAL = -1;
13
16
  const p_now = performance.now.bind(performance);
14
- export function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenceReturn, abortSignals, now, }) {
17
+ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, releaseBufferReferenceReturn, abortSignals, now, }) {
15
18
  const PLACE_HOLDER = (_) => {
16
19
  throw ("UNREACHABLE FROM PLACE HOLDER (main)");
17
20
  };
@@ -34,22 +37,53 @@ export function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenc
34
37
  let inUsed = 0 | 0;
35
38
  const resetSignal = abortSignals?.resetSignal;
36
39
  const nowTime = now ?? p_now;
37
- const resolveReturn = returnLock.resolveHost({
40
+ const onReturnResolved = (task) => {
41
+ inUsed = (inUsed - 1) | 0;
42
+ resetTaskLocalFlags(task);
43
+ runTaskFinalizers(task);
44
+ task.value = null;
45
+ task.resolve = PLACE_HOLDER;
46
+ task.reject = PLACE_HOLDER;
47
+ freePush(task[TaskIndex.ID]);
48
+ };
49
+ const returnResolvers = [
50
+ returnLock,
51
+ ...(extraReturnLocks ?? []),
52
+ ].map((each) => each.resolveHost({
38
53
  queue,
39
54
  activeRejectPlaceholder: PLACE_HOLDER,
40
- onResolved: (task) => {
41
- inUsed = (inUsed - 1) | 0;
42
- resetTaskLocalFlags(task);
43
- runTaskFinalizers(task);
44
- task.value = null;
45
- task.resolve = PLACE_HOLDER;
46
- task.reject = PLACE_HOLDER;
47
- freePush(task[TaskIndex.ID]);
48
- },
49
- });
50
- const completeFrame = releaseBufferReferenceReturn === undefined
51
- ? resolveReturn
52
- : () => withBufferReferenceReturnReleaser(releaseBufferReferenceReturn, resolveReturn);
55
+ onResolved: onReturnResolved,
56
+ }));
57
+ // A stealing queue drains one private return lock per worker. Borrowed
58
+ // BufferReferences must therefore be claimed with the hooks belonging to the
59
+ // worker that produced that particular return. The hooks are bound after the
60
+ // worker context exists, while the pool-global queue is built before workers
61
+ // spawn, so keep one mutable hook slot per return lane.
62
+ const returnHooks = new Array(returnResolvers.length);
63
+ if (releaseBufferReferenceReturn !== undefined) {
64
+ returnHooks[0] = releaseBufferReferenceReturn;
65
+ }
66
+ const resolveReturnAt = (index) => {
67
+ const resolve = returnResolvers[index];
68
+ const hooks = returnHooks[index];
69
+ return hooks === undefined
70
+ ? resolve()
71
+ : withBufferReferenceReturnReleaser(hooks, resolve);
72
+ };
73
+ // Preserve the original one-lane fast path exactly: no wrapper, array lookup,
74
+ // or hook branch is paid by the default one-worker transport. Mutable
75
+ // per-return-lane hooks are needed only by a multi-worker stealing queue.
76
+ const completeFrame = returnResolvers.length === 1
77
+ ? releaseBufferReferenceReturn === undefined
78
+ ? returnResolvers[0]
79
+ : () => withBufferReferenceReturnReleaser(releaseBufferReferenceReturn, returnResolvers[0])
80
+ : () => {
81
+ let resolved = 0 | 0;
82
+ for (let i = 0; i < returnResolvers.length; i++) {
83
+ resolved = (resolved + resolveReturnAt(i)) | 0;
84
+ }
85
+ return resolved;
86
+ };
53
87
  const hasActiveTasks = () => {
54
88
  const count = (inUsed - getPendingPromiseCount()) | 0;
55
89
  return count > 0;
@@ -82,6 +116,12 @@ export function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenc
82
116
  hasPendingFrames,
83
117
  txIdle,
84
118
  completeFrame,
119
+ setReturnHooks: (lane, hooks) => {
120
+ if (!Number.isInteger(lane) || lane < 0 || lane >= returnHooks.length) {
121
+ throw new RangeError(`return lane ${lane} out of range`);
122
+ }
123
+ returnHooks[lane] = hooks;
124
+ },
85
125
  enqueue: (functionID, timeout, abortSignal) => {
86
126
  const HAS_TIMER = timeout !== undefined;
87
127
  const functionIDMasked = functionID & FUNCTION_ID_MASK;
@@ -0,0 +1,13 @@
1
+ import type { WorkerSettings } from "../types.js";
2
+ export type SpawnedWorker = {
3
+ terminate: () => unknown;
4
+ unref?: () => unknown;
5
+ postMessage?: (message: unknown) => void;
6
+ };
7
+ export type NodeWorkerLike = {
8
+ on?: (event: "error" | "exit" | "message", listener: (...args: unknown[]) => void) => void;
9
+ };
10
+ export declare const toWorkerSafeExecArgv: (flags: string[] | undefined) => string[] | undefined;
11
+ export declare const toWorkerCompatExecArgv: (flags: string[] | undefined) => string[] | undefined;
12
+ export declare const serializeWorkerBootstrapData: (options: WorkerSettings) => WorkerSettings;
13
+ export declare const terminateWorkerQuietly: (worker: SpawnedWorker) => void;
@@ -0,0 +1,102 @@
1
+ // Worker helpers shared by every worker mode. Kept out of `process-worker.ts`
2
+ // so the thread and web-worker paths do not pull the process-worker module —
3
+ // and, through it, the whole native connections tree — into their bundles.
4
+ import { ProcessSharedBuffer } from "../connections/process-shared-buffer.js";
5
+ const execFlagKey = (flag) => flag.split("=", 1)[0];
6
+ const NODE_PERMISSION_EXEC_FLAGS = new Set([
7
+ "--permission",
8
+ "--experimental-permission",
9
+ "--allow-fs-read",
10
+ "--allow-fs-write",
11
+ "--allow-worker",
12
+ "--allow-child-process",
13
+ "--allow-net",
14
+ "--allow-addons",
15
+ "--allow-ffi",
16
+ "--allow-wasi",
17
+ ]);
18
+ const NODE_WORKER_SAFE_EXEC_FLAGS = new Set([
19
+ "--experimental-ffi",
20
+ "--experimental-transform-types",
21
+ "--expose-gc",
22
+ "--no-warnings",
23
+ ...NODE_PERMISSION_EXEC_FLAGS,
24
+ ]);
25
+ const isNodeWorkerSafeExecFlag = (flag) => NODE_WORKER_SAFE_EXEC_FLAGS.has(execFlagKey(flag));
26
+ const isNodePermissionExecFlag = (flag) => NODE_PERMISSION_EXEC_FLAGS.has(execFlagKey(flag));
27
+ export const toWorkerSafeExecArgv = (flags) => {
28
+ if (!flags || flags.length === 0)
29
+ return undefined;
30
+ const filtered = flags.filter(isNodeWorkerSafeExecFlag);
31
+ if (filtered.length === 0)
32
+ return undefined;
33
+ const seen = new Set();
34
+ const deduped = [];
35
+ for (const flag of filtered) {
36
+ if (seen.has(flag))
37
+ continue;
38
+ seen.add(flag);
39
+ deduped.push(flag);
40
+ }
41
+ return deduped;
42
+ };
43
+ export const toWorkerCompatExecArgv = (flags) => {
44
+ const safe = toWorkerSafeExecArgv(flags);
45
+ if (!safe || safe.length === 0)
46
+ return undefined;
47
+ const compat = safe.filter((flag) => !isNodePermissionExecFlag(flag));
48
+ return compat.length > 0 ? compat : undefined;
49
+ };
50
+ const isPlainRecord = (value) => {
51
+ if (value === null || typeof value !== "object")
52
+ return false;
53
+ const prototype = Object.getPrototypeOf(value);
54
+ return prototype === Object.prototype || prototype === null;
55
+ };
56
+ const serializeWorkerBootstrapValue = (value, seen = new WeakMap()) => {
57
+ if (value instanceof ProcessSharedBuffer)
58
+ return value.toMetadata();
59
+ if (value === null || typeof value !== "object")
60
+ return value;
61
+ const existing = seen.get(value);
62
+ if (existing !== undefined)
63
+ return existing;
64
+ if (Array.isArray(value)) {
65
+ const out = [];
66
+ seen.set(value, out);
67
+ for (const item of value) {
68
+ out.push(serializeWorkerBootstrapValue(item, seen));
69
+ }
70
+ return out;
71
+ }
72
+ if (!isPlainRecord(value))
73
+ return value;
74
+ const out = {};
75
+ seen.set(value, out);
76
+ for (const [key, item] of Object.entries(value)) {
77
+ out[key] = serializeWorkerBootstrapValue(item, seen);
78
+ }
79
+ return out;
80
+ };
81
+ export const serializeWorkerBootstrapData = (options) => {
82
+ const bootstrap = options.bootstrap;
83
+ if (bootstrap === undefined || bootstrap.data === undefined)
84
+ return options;
85
+ return {
86
+ ...options,
87
+ bootstrap: {
88
+ ...bootstrap,
89
+ data: serializeWorkerBootstrapValue(bootstrap.data),
90
+ },
91
+ };
92
+ };
93
+ export const terminateWorkerQuietly = (worker) => {
94
+ try {
95
+ // Runaway worker termination can be slow or stuck on some runtimes; once the
96
+ // pool is closing it must not keep the host process alive.
97
+ worker.unref?.();
98
+ void Promise.resolve(worker.terminate()).catch(() => { });
99
+ }
100
+ catch {
101
+ }
102
+ };
package/src/types.d.ts CHANGED
@@ -34,6 +34,17 @@ type WorkerData = {
34
34
  payloadConfig?: PayloadBufferOptions;
35
35
  bufferReferenceReturn?: "copy" | "borrow";
36
36
  permission?: ResolvedPermissionProtocol;
37
+ /**
38
+ * Work stealing. When present, `lock` is a submit region shared by every
39
+ * worker and this worker claims from it as consumer `consumerId` of
40
+ * `consumers`, rather than owning a private request lane. `returnLock` stays
41
+ * private — the endpoint that claims a task owns its response.
42
+ */
43
+ steal?: {
44
+ consumers: number;
45
+ consumerId: number;
46
+ regionLanes: number;
47
+ };
37
48
  };
38
49
  type UnsafeOptions = {
39
50
  /**
@@ -83,6 +94,7 @@ type AbortSignalConfig = {
83
94
  type AbortSignalOption = true | AbortSignalConfig | undefined;
84
95
  type AbortSignalMethods<AS extends AbortSignalOption> = AS extends undefined ? never : {
85
96
  hasAborted: () => boolean;
97
+ now: () => number;
86
98
  };
87
99
  type AbortSignalToolkit<AS extends AbortSignalOption> = AbortSignalMethods<AS>;
88
100
  type TaskFn<A extends TaskInput, B extends Args, AS extends AbortSignalOption = undefined> = BivariantCallback<AS extends undefined ? [NoBlob<Awaited<A>>] : [NoBlob<Awaited<A>>, AbortSignalToolkit<AS>], MaybePromise<NoBlob<B>>>;
@@ -220,6 +232,41 @@ type WorkerBootstrapOptions<Data = unknown> = {
220
232
  */
221
233
  data?: Data;
222
234
  };
235
+ /** Native worker artifact and on-demand compiler settings. */
236
+ type CompiledWorkerOptions = {
237
+ /**
238
+ * Native executable path. Relative paths resolve from the createPool caller.
239
+ * Defaults to the task module path with its extension replaced by `.knt`.
240
+ */
241
+ artifact?: string;
242
+ /**
243
+ * Versioned metadata sidecar. Defaults to `<artifact>.json`.
244
+ */
245
+ manifest?: string;
246
+ /**
247
+ * Build a missing/incompatible artifact, or use `"always"` to rebuild once
248
+ * whenever a pool is created. Defaults to true.
249
+ */
250
+ build?: boolean | "always";
251
+ /**
252
+ * Porffor main executable or checkout. Defaults to PORFFOR_MAIN/PORF, then
253
+ * `porf` on PATH; a pinned compiler is cached locally when none is present.
254
+ */
255
+ compiler?: string;
256
+ };
257
+ type CompiledWorkerSource = string | URL | {
258
+ readonly importedFrom: string;
259
+ };
260
+ type CompiledWorkerCheck = {
261
+ /** True only when the executable and compatible manifest both validate. */
262
+ compiled: boolean;
263
+ artifact: string;
264
+ manifest: string;
265
+ reason?: string;
266
+ compiler?: string;
267
+ protocol?: string;
268
+ tasks?: readonly string[];
269
+ };
223
270
  type WorkerSettings = {
224
271
  resolveAfterFinishingAll?: true;
225
272
  /**
@@ -230,13 +277,18 @@ type WorkerSettings = {
230
277
  /**
231
278
  * Experimental worker runtime.
232
279
  * "thread" uses Worker/worker_threads. "process" spawns another JavaScript
233
- * runtime, useful for process isolation, bwrap, and containers.
280
+ * runtime, useful for process isolation, bwrap, and containers. "compiled"
281
+ * validates and runs a `.knt` native worker, building it on first use.
234
282
  */
235
- runtime?: "thread" | "process";
283
+ runtime?: "thread" | "process" | "compiled";
284
+ /** Artifact settings used when runtime is "compiled". */
285
+ compiled?: CompiledWorkerOptions;
236
286
  /**
237
- * Runtime executable to use when runtime is "process". Defaults to "deno".
287
+ * Runtime executable for process workers. Standalone `"porffor"` selects the
288
+ * compiled backend and rebuilds once per pool; with `runtime: "compiled"` it
289
+ * reuses the validated artifact.
238
290
  */
239
- processRuntime?: "bun" | "deno" | "node";
291
+ processRuntime?: "bun" | "deno" | "node" | "porffor";
240
292
  /**
241
293
  * Command argv to prepend before the process worker runtime command.
242
294
  * Useful for wrappers such as systemd-run, cgexec, nice, taskset, or
@@ -302,11 +354,38 @@ type DispatcherSettings = {
302
354
  * - `"serial-channel"`: each worker keeps its own dispatcher check state, but
303
355
  * one shared channel runs all lane checks from first to last.
304
356
  *
305
- * Experimental default: `"per-thread"` on Bun or with one worker, otherwise
306
- * `"serial-channel"` for multi-worker Node/Deno pools. Can also be forced
307
- * with the `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
357
+ * Used by private-lane pools. Its experimental default is `"per-thread"` on
358
+ * Bun or with one worker, otherwise `"serial-channel"` for multi-worker
359
+ * Node/Deno pools. Selecting it explicitly keeps private lanes unless
360
+ * `steal: true` is also explicit. Can also be forced with the
361
+ * `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
308
362
  */
309
363
  dispatcher?: "per-thread" | "serial-channel";
364
+ /**
365
+ * Work stealing: one shared submit region that any worker may
366
+ * claim from, private return lanes, and a pool-global pending registry. The
367
+ * endpoint that claims a task owns its response.
368
+ *
369
+ * Enabled by default for multi-worker thread pools unless a balancer or
370
+ * private-lane dispatcher was explicitly selected. One-worker pools,
371
+ * inliners, compiled/Porffor workers, process workers, and pools above the
372
+ * current 31-claimant protocol limit retain their existing transport.
373
+ * Explicitly requesting `true` with process workers is rejected because
374
+ * their transport uses separately mapped process-shared memory. Set `false`
375
+ * (or `KNITTING_STEAL=0`) to opt out; `KNITTING_STEAL=1` explicitly opts in
376
+ * and overrides a balancer/dispatcher selection.
377
+ */
378
+ steal?: boolean;
379
+ /**
380
+ * Lanes claimed per stealing handshake (a power of two, `slots / g >=
381
+ * workers + 1`). Defaults to the widest region the lane budget allows, which
382
+ * amortises arbitration best for cheap tasks.
383
+ *
384
+ * **A region is a batch.** For expensive tasks, a wide region lets one worker
385
+ * claim work the others could have run in parallel; set this to `1` (or a
386
+ * small value) when per-task cost dominates arbitration cost.
387
+ */
388
+ stealRegionLanes?: number;
310
389
  };
311
390
  type CreatePool = {
312
391
  /** Number of workers. Default: 1. */
@@ -351,7 +430,8 @@ type CreatePool = {
351
430
  debug?: DebugOptions;
352
431
  source?: string;
353
432
  };
354
- export type { AbortSignalConfig as AbortSignalConfig, AbortSignalMethods as AbortSignalMethods, AbortSignalOption as AbortSignalOption, AbortSignalToolkit as AbortSignalToolkit, Args as Args, Balancer as Balancer, BalancerStrategy as BalancerStrategy, Composed as Composed, ComposedWithKey as ComposedWithKey, CreateContext as CreateContext, CreatePool as CreatePool, DebugNamespace as DebugNamespace, DebugOptions as DebugOptions, DispatcherSettings as DispatcherSettings, Envelope as Envelope, EnvelopeBody as EnvelopeBody, EnvelopeHeader as EnvelopeHeader, External as External, FixPoint as FixPoint, FunctionMapType as FunctionMapType, ImportTaskOptions as ImportTaskOptions, Inliner as Inliner, LockBuffers as LockBuffers, LockBufferTextCompat as LockBufferTextCompat, MaybePromise as MaybePromise, PayloadBufferMode as PayloadBufferMode, PayloadBufferOptions as PayloadBufferOptions, PermissionProtocol as PermissionProtocol, PermissionProtocolInput as PermissionProtocolInput, Pool as Pool, ProcessSharedMemoryMode as ProcessSharedMemoryMode, ProcessSharedMemorySettings as ProcessSharedMemorySettings, ResolvedPermissionProtocol as ResolvedPermissionProtocol, ReturnFixed as ReturnFixed, SecondPart as SecondPart, SharedBufferRegion as SharedBufferRegion, SharedBufferSource as SharedBufferSource, SharedBufferTextCompat as SharedBufferTextCompat, SingleTaskPool as SingleTaskPool, TaskFn as TaskFn, TaskInput as TaskInput, tasks as tasks, TaskTimeout as TaskTimeout, ValidInput as ValidInput, WorkerBootstrapContext as WorkerBootstrapContext, WorkerBootstrapFunction as WorkerBootstrapFunction, WorkerBootstrapOptions as WorkerBootstrapOptions, WorkerCall as WorkerCall, WorkerContext as WorkerContext, WorkerData as WorkerData, WorkerInvoke as WorkerInvoke, WorkerSettings as WorkerSettings, WorkerTimers as WorkerTimers, };
355
- export type { Task as Task } from "./memory/lock.js";
433
+ export type { AbortSignalConfig as AbortSignalConfig, AbortSignalMethods as AbortSignalMethods, AbortSignalOption as AbortSignalOption, AbortSignalToolkit as AbortSignalToolkit, Args as Args, Balancer as Balancer, BalancerStrategy as BalancerStrategy, CompiledWorkerCheck as CompiledWorkerCheck, CompiledWorkerOptions as CompiledWorkerOptions, CompiledWorkerSource as CompiledWorkerSource, Composed as Composed, ComposedWithKey as ComposedWithKey, CreateContext as CreateContext, CreatePool as CreatePool, DebugNamespace as DebugNamespace, DebugOptions as DebugOptions, DispatcherSettings as DispatcherSettings, Envelope as Envelope, EnvelopeBody as EnvelopeBody, EnvelopeHeader as EnvelopeHeader, External as External, FixPoint as FixPoint, FunctionMapType as FunctionMapType, ImportTaskOptions as ImportTaskOptions, Inliner as Inliner, LockBuffers as LockBuffers, LockBufferTextCompat as LockBufferTextCompat, MaybePromise as MaybePromise, PayloadBufferMode as PayloadBufferMode, PayloadBufferOptions as PayloadBufferOptions, PermissionProtocol as PermissionProtocol, PermissionProtocolInput as PermissionProtocolInput, Pool as Pool, ProcessSharedMemoryMode as ProcessSharedMemoryMode, ProcessSharedMemorySettings as ProcessSharedMemorySettings, ResolvedPermissionProtocol as ResolvedPermissionProtocol, ReturnFixed as ReturnFixed, SecondPart as SecondPart, SharedBufferRegion as SharedBufferRegion, SharedBufferSource as SharedBufferSource, SharedBufferTextCompat as SharedBufferTextCompat, SingleTaskPool as SingleTaskPool, TaskFn as TaskFn, TaskInput as TaskInput, tasks as tasks, TaskTimeout as TaskTimeout, ValidInput as ValidInput, WorkerBootstrapContext as WorkerBootstrapContext, WorkerBootstrapFunction as WorkerBootstrapFunction, WorkerBootstrapOptions as WorkerBootstrapOptions, WorkerCall as WorkerCall, WorkerContext as WorkerContext, WorkerData as WorkerData, WorkerInvoke as WorkerInvoke, WorkerSettings as WorkerSettings, WorkerTimers as WorkerTimers, };
434
+ import type { Task as TaskType } from "./memory/lock.js";
356
435
  export { LockBound as LockBound, PayloadBuffer as PayloadBuffer, PayloadSignal as PayloadSignal, TaskIndex as TaskIndex, } from "./memory/lock.js";
436
+ export type Task = TaskType;
357
437
  export type { RegisterMalloc as RegisterMalloc } from "./memory/regionRegistry.js";