knitting 0.1.52 → 0.1.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -335,7 +335,7 @@ Common options you might tweak:
335
335
  | `worker.runtime` | Choose `"thread"` or `"process"` workers. |
336
336
  | `worker.processSharedMemory` | Process-worker memory discovery: `"inherit"` by default on POSIX, or `"named"` for wrappers/containers that cannot preserve fd 0. |
337
337
  | `permission` | Runtime permission policy for workers. |
338
- | `debug` | Enable extra diagnostics. |
338
+ | `debug` | Enable diagnostics (`host`, `globals`, `signals`, `imports`, `lifecycle`) or use `KNITTING_DEBUG`. |
339
339
  | `source` | Worker source override for advanced runtimes. |
340
340
 
341
341
  ### Worker bootstrap
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knitting",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
4
4
  "description": "Shared-memory IPC runtime for Node.js, Deno, and Bun.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/api.d.ts CHANGED
@@ -25,12 +25,31 @@ export { endpointSymbol as endpointSymbol };
25
25
  * Reconstructs stable task order from top-level exports before names are bound.
26
26
  */
27
27
  export declare const toListAndIds: ToListAndIdsFn;
28
+ /**
29
+ * Create a typed worker pool from module-scope exported tasks/functions.
30
+ *
31
+ * Install/import as `knitting` on npm or `@vixeny/knitting` on JSR. Requires
32
+ * Node 22+, Deno 2+, or Bun 1+.
33
+ *
34
+ * Use `createPool(options)({ taskA })`, call `await pool.call.taskA(arg)`, and
35
+ * clean up with `using pool = ...` or `await pool.shutdown()`.
36
+ *
37
+ * Guard host-only pool setup with `isMain`: workers re-import task modules, and
38
+ * top-level imports in those modules run in every worker. Keep task modules
39
+ * lean and separate from server/framework setup. Each task receives one
40
+ * argument; use an object or tuple for multiple values.
41
+ */
28
42
  export declare const createPool: CreatePoolFactory;
29
43
  /**
30
- * Define a worker task.
44
+ * Define a worker task with options.
45
+ *
46
+ * Pass raw module-scope exported functions to `createPool` directly when you do
47
+ * not need options. Use `task({ f })` for timeouts, abort signals, or the
48
+ * single-task `.createPool()` shorthand.
31
49
  *
32
- * Input may be a direct value or a native Promise of that value.
33
- * Thenables/PromiseLike values are treated as plain values.
50
+ * The function receives one argument; use a tuple/object for multiple values.
51
+ * Inputs may be direct values or native Promises, so `request.arrayBuffer()` can
52
+ * be forwarded without awaiting on the host.
34
53
  */
35
54
  export declare function task<F extends InferredTaskFunction>(I: InferredTaskShape<F, undefined>): ReturnFixed<InferredTaskInput<F, undefined>, InferredTaskOutput<F>, undefined>;
36
55
  export declare function task<F extends InferredTaskFunction>(I: InferredTaskShape<F, true>): ReturnFixed<InferredTaskInput<F, true>, InferredTaskOutput<F>, true>;
@@ -39,10 +58,11 @@ export declare function task<A extends TaskInput = void, B extends Args = void>(
39
58
  export declare function task<A extends TaskInput = void, B extends Args = void, AS extends AbortSignalConfig = AbortSignalConfig>(I: FixPoint<A, B, AS>): ReturnFixed<A, B, AS>;
40
59
  export declare function task<A extends TaskInput = void, B extends Args = void>(I: FixPoint<A, B, undefined>): ReturnFixed<A, B, undefined>;
41
60
  /**
42
- * Define a task whose worker-side function is imported dynamically from `href`.
61
+ * Define a task whose worker-side code is imported only by workers.
43
62
  *
44
- * This keeps module import/evaluation inside the worker, so worker permission
45
- * policies apply to that import path.
63
+ * Use this for untrusted or security-sensitive code: the host keeps a typed
64
+ * wrapper and does not import/evaluate `href`. The target export must be a
65
+ * plain function, not a `task()` wrapper.
46
66
  */
47
67
  export declare function importTask<A extends TaskInput = void, B extends Args = void>(options: ImportTaskOptions<A, B, true>): ReturnFixed<A, B, true>;
48
68
  export declare function importTask<A extends TaskInput = void, B extends Args = void, AS extends AbortSignalConfig = AbortSignalConfig>(options: ImportTaskOptions<A, B, AS>): ReturnFixed<A, B, AS>;
package/src/api.js CHANGED
@@ -7,15 +7,51 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
7
7
  return path;
8
8
  };
9
9
  import { getCallerFilePath, getCallerHref } from "./common/task-source.js";
10
+ import { DEBUG_ENABLED, resolveDebugNamespaces } from "./debug/gate.js";
10
11
  import { genTaskID } from "./common/task-source.js";
11
12
  import { toModuleUrl } from "./common/module-url.js";
12
13
  import { endpointSymbol } from "./common/task-symbol.js";
13
14
  import { spawnWorkerContext } from "./runtime/pool.js";
15
+ import { RUNTIME } from "./common/runtime.js";
14
16
  import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
15
17
  import { resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
16
18
  import { getNodeProcess } from "./common/node-compat.js";
17
19
  import { managerMethod } from "./runtime/balancer.js";
18
20
  import { createInlineExecutor } from "./runtime/inline-executor.js";
21
+ const hasDebugNamespace = (namespaces, namespace) => namespaces.has("*") || namespaces.has(namespace);
22
+ const createHostDebug = (namespaces) => {
23
+ const enabled = (namespace) => hasDebugNamespace(namespaces, namespace);
24
+ if (!enabled("host"))
25
+ return undefined;
26
+ const base = performance.now();
27
+ const tag = `host·${RUNTIME}`;
28
+ const log = (message) => {
29
+ const elapsed = (performance.now() - base).toFixed(1);
30
+ console.error(`[${tag}·+${elapsed}ms] host: ${message}`);
31
+ };
32
+ return { log };
33
+ };
34
+ const readHostCwd = () => {
35
+ const denoCwd = globalThis.Deno?.cwd;
36
+ if (typeof denoCwd === "function") {
37
+ try {
38
+ return denoCwd();
39
+ }
40
+ catch {
41
+ }
42
+ }
43
+ const nodeProcess = getNodeProcess();
44
+ if (typeof nodeProcess?.cwd === "function") {
45
+ try {
46
+ return nodeProcess.cwd();
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ }
52
+ return undefined;
53
+ };
54
+ const formatDebugList = (values, empty = "(none)") => values && values.length > 0 ? values.join(",") : empty;
19
55
  const MAX_FUNCTION_ID = 0xFFFF;
20
56
  const MAX_FUNCTION_COUNT = MAX_FUNCTION_ID + 1;
21
57
  const DEFAULT_IMPORT_EXPORT_NAME = "default";
@@ -93,14 +129,36 @@ const toPoolTaskEntries = (input, callerHref) => Object.entries(input).map(([nam
93
129
  }
94
130
  throw new TypeError(`createPool task "${name}" must be a task definition or exported function`);
95
131
  });
96
- export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, source, worker, workerExecArgv, permission, dispatcher, host, }) => (tasks) => {
132
+ /**
133
+ * Create a typed worker pool from module-scope exported tasks/functions.
134
+ *
135
+ * Install/import as `knitting` on npm or `@vixeny/knitting` on JSR. Requires
136
+ * Node 22+, Deno 2+, or Bun 1+.
137
+ *
138
+ * Use `createPool(options)({ taskA })`, call `await pool.call.taskA(arg)`, and
139
+ * clean up with `using pool = ...` or `await pool.shutdown()`.
140
+ *
141
+ * Guard host-only pool setup with `isMain`: workers re-import task modules, and
142
+ * top-level imports in those modules run in every worker. Keep task modules
143
+ * lean and separate from server/framework setup. Each task receives one
144
+ * argument; use an object or tuple for multiple values.
145
+ */
146
+ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe, abortSignalCapacity, source, worker, workerExecArgv, permission, host, }) => (tasks) => {
97
147
  const bufferReferenceReturn = unsafe?.BufferReferenceReturn;
148
+ const debugRequested = DEBUG_ENABLED ||
149
+ (debug !== undefined && debug !== false);
150
+ let debugNamespaces;
151
+ const getDebugNamespaces = () => debugNamespaces ??= resolveDebugNamespaces(debug);
152
+ const hostDebug = debugRequested
153
+ ? createHostDebug(getDebugNamespaces())
154
+ : undefined;
155
+ const debugEnabled = (namespace) => debugRequested && hasDebugNamespace(getDebugNamespaces(), namespace);
98
156
  /**
99
157
  * This functions is only available in the main thread.
100
158
  * Also triggers when debug extra is enabled.
101
159
  */
102
160
  if (RUNTIME_IS_MAIN_THREAD === false) {
103
- if ((debug?.extras === true)) {
161
+ if (debugEnabled("lifecycle")) {
104
162
  console.warn("createPool has been called with : " + JSON.stringify(RUNTIME_WORKER_DATA));
105
163
  }
106
164
  const notMainThreadError = () => {
@@ -126,6 +184,10 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
126
184
  const listOfFunctions = toPoolTaskEntries(tasks, callerHref)
127
185
  .sort((a, b) => a.name.localeCompare(b.name));
128
186
  const { list, ids, names, at } = toListAndIds(listOfFunctions);
187
+ hostDebug?.log(`cwd=${readHostCwd() ?? "(unknown)"} caller=${callerHref}`);
188
+ listOfFunctions.forEach((fn) => {
189
+ hostDebug?.log(`task name=${fn.name} id=${fn.id} from=${fn.importedFrom}`);
190
+ });
129
191
  if (listOfFunctions.length > MAX_FUNCTION_COUNT) {
130
192
  throw new RangeError(`Too many tasks: received ${listOfFunctions.length}. ` +
131
193
  `Maximum is ${MAX_FUNCTION_COUNT} (Uint16 function IDs: 0..${MAX_FUNCTION_ID}).`);
@@ -196,9 +258,16 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
196
258
  ...(defaultExecArgv ?? []),
197
259
  ]);
198
260
  const execArgv = sanitizeExecArgv(combinedExecArgv.length > 0 ? combinedExecArgv : undefined);
199
- const hostDispatcher = host ?? dispatcher;
261
+ hostDebug?.log(`pool runtime=${RUNTIME} workers=${threads ?? 1}` +
262
+ ` lanes=${totalNumberOfThread} inliner=${usingInliner ? "on" : "off"}`);
263
+ hostDebug?.log(`modules=${formatDebugList(list)}`);
264
+ hostDebug?.log(`permission=${permissionProtocol?.mode ?? "off"} execArgv=${formatDebugList(execArgv)}`);
200
265
  const usesAbortSignal = listOfFunctions.some((fn) => fn.abortSignal !== undefined);
201
266
  const resolvedWorker = resolveWorkerBootstrapSettings(worker, callerHref);
267
+ if (resolvedWorker?.bootstrap !== undefined) {
268
+ hostDebug?.log(`bootstrap href=${resolvedWorker.bootstrap.href}` +
269
+ ` name=${resolvedWorker.bootstrap.name}`);
270
+ }
202
271
  if (usingInliner && resolvedWorker?.bootstrap !== undefined) {
203
272
  throw new Error("worker.bootstrap cannot be used with the inliner");
204
273
  }
@@ -223,17 +292,14 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
223
292
  at,
224
293
  thread,
225
294
  debug,
295
+ hostDebug: hostDebug?.log,
226
296
  totalNumberOfThread,
227
297
  source,
228
298
  workerOptions: resolvedWorker,
229
299
  workerExecArgv: execArgv,
230
- host: hostDispatcher,
300
+ host,
231
301
  payload,
232
302
  bufferReferenceReturn,
233
- payloadInitialBytes,
234
- payloadMaxBytes,
235
- bufferMode,
236
- maxPayloadBytes,
237
303
  abortSignalCapacity,
238
304
  usesAbortSignal,
239
305
  permission: permissionProtocol,
@@ -45,10 +45,12 @@ export declare const createBufferReferenceReturnReleaseMessage: (token: bigint)
45
45
  export declare const readBufferReferenceReturnReleaseMessage: (value: unknown) => bigint | undefined;
46
46
  export declare const isBufferReferenceMetadata: (value: unknown) => value is BufferReferenceMetadata;
47
47
  /**
48
- * Zero-copy handle for moving ArrayBuffer bytes to **thread** workers.
48
+ * Zero-copy handle for moving ArrayBuffer bytes to/from **thread** workers.
49
49
  *
50
50
  * Construction detaches the source. Consumers materialize the moved region in
51
- * their isolate: owning on Node, alias/copy on Deno/Bun.
51
+ * their isolate: owning on Node, alias/copy on Deno/Bun. Return a
52
+ * `BufferReference` for large binary results to avoid copying back through the
53
+ * transport. Use `ProcessSharedBuffer` across process boundaries.
52
54
  */
53
55
  export declare class BufferReference {
54
56
  #private;
@@ -216,10 +216,12 @@ export const isBufferReferenceMetadata = (value) => {
216
216
  meta.byteLength >= 0);
217
217
  };
218
218
  /**
219
- * Zero-copy handle for moving ArrayBuffer bytes to **thread** workers.
219
+ * Zero-copy handle for moving ArrayBuffer bytes to/from **thread** workers.
220
220
  *
221
221
  * Construction detaches the source. Consumers materialize the moved region in
222
- * their isolate: owning on Node, alias/copy on Deno/Bun.
222
+ * their isolate: owning on Node, alias/copy on Deno/Bun. Return a
223
+ * `BufferReference` for large binary results to avoid copying back through the
224
+ * transport. Use `ProcessSharedBuffer` across process boundaries.
223
225
  */
224
226
  export class BufferReference {
225
227
  [EXTERNAL_PAYLOAD_BRAND] = BUFFER_REFERENCE_CODEC_ID;
@@ -36,6 +36,12 @@ export type ProcessSharedBufferViewConstructor<View extends ProcessSharedBufferV
36
36
  };
37
37
  export declare const setDefaultProcessSharedBufferPrimitives: (primitives: ProcessSharedBufferPrimitives | undefined) => void;
38
38
  export declare const getDefaultProcessSharedBufferPrimitives: () => ProcessSharedBufferPrimitives;
39
+ /**
40
+ * Zero-copy shared bytes for process workers.
41
+ *
42
+ * Use this when the boundary is a separate process, container, or sandbox.
43
+ * For thread workers, `SharedArrayBuffer` or `BufferReference` can be cheaper.
44
+ */
39
45
  export declare class ProcessSharedBuffer {
40
46
  readonly [PROCESS_SHARED_BUFFER_BRAND] = true;
41
47
  readonly [EXTERNAL_PAYLOAD_BRAND] = "knitting.processSharedBuffer";
@@ -116,6 +116,12 @@ const expectRange = (byteOffset, byteLength, availableByteLength) => {
116
116
  throw new RangeError("process shared buffer byteLength is out of bounds");
117
117
  }
118
118
  };
119
+ /**
120
+ * Zero-copy shared bytes for process workers.
121
+ *
122
+ * Use this when the boundary is a separate process, container, or sandbox.
123
+ * For thread workers, `SharedArrayBuffer` or `BufferReference` can be cheaper.
124
+ */
119
125
  export class ProcessSharedBuffer {
120
126
  [PROCESS_SHARED_BUFFER_BRAND] = true;
121
127
  [EXTERNAL_PAYLOAD_BRAND] = PROCESS_SHARED_BUFFER_CODEC_ID;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Environment-diff primitive: snapshot the keys present on `globalThis`, then
3
+ * later compare to see what the loaded code added, removed, or redefined.
4
+ *
5
+ * This is the core of "check what happens" debugging — a concrete before/after
6
+ * of the real runtime a worker's modules created, not an aggregate metric. The
7
+ * same snapshot/diff shape generalises to other ambient state (process
8
+ * listeners, open handles, prototype patches); `globalThis` keys are the first
9
+ * instance.
10
+ */
11
+ export type EnvSnapshot = {
12
+ readonly keys: ReadonlySet<string | symbol>;
13
+ };
14
+ /** Capture every own key on `globalThis`, including symbols. */
15
+ export declare const snapshotGlobals: () => EnvSnapshot;
16
+ export type GlobalsDiff = {
17
+ readonly added: (string | symbol)[];
18
+ readonly removed: (string | symbol)[];
19
+ };
20
+ export declare const diffGlobals: (before: EnvSnapshot, after: EnvSnapshot) => GlobalsDiff;
21
+ /**
22
+ * Render a key with enough provenance to tell a fresh global from a
23
+ * monkeypatch: its `typeof`/accessor kind plus writable/configurable/enumerable
24
+ * flags (`w`/`c`/`e`).
25
+ */
26
+ export declare const describeGlobalKey: (key: string | symbol) => string;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Environment-diff primitive: snapshot the keys present on `globalThis`, then
3
+ * later compare to see what the loaded code added, removed, or redefined.
4
+ *
5
+ * This is the core of "check what happens" debugging — a concrete before/after
6
+ * of the real runtime a worker's modules created, not an aggregate metric. The
7
+ * same snapshot/diff shape generalises to other ambient state (process
8
+ * listeners, open handles, prototype patches); `globalThis` keys are the first
9
+ * instance.
10
+ */
11
+ /** Capture every own key on `globalThis`, including symbols. */
12
+ export const snapshotGlobals = () => ({
13
+ keys: new Set(Reflect.ownKeys(globalThis)),
14
+ });
15
+ export const diffGlobals = (before, after) => {
16
+ const added = [];
17
+ const removed = [];
18
+ for (const key of after.keys) {
19
+ if (!before.keys.has(key))
20
+ added.push(key);
21
+ }
22
+ for (const key of before.keys) {
23
+ if (!after.keys.has(key))
24
+ removed.push(key);
25
+ }
26
+ return { added, removed };
27
+ };
28
+ /**
29
+ * Render a key with enough provenance to tell a fresh global from a
30
+ * monkeypatch: its `typeof`/accessor kind plus writable/configurable/enumerable
31
+ * flags (`w`/`c`/`e`).
32
+ */
33
+ export const describeGlobalKey = (key) => {
34
+ const name = typeof key === "symbol" ? key.toString() : key;
35
+ let descriptor;
36
+ try {
37
+ descriptor = Object.getOwnPropertyDescriptor(globalThis, key);
38
+ }
39
+ catch {
40
+ return name;
41
+ }
42
+ if (descriptor === undefined)
43
+ return name;
44
+ const kind = descriptor.get !== undefined || descriptor.set !== undefined
45
+ ? "accessor"
46
+ : typeof descriptor.value;
47
+ const flags = `${descriptor.writable === false ? "" : "w"}${descriptor.configurable ? "c" : ""}${descriptor.enumerable ? "e" : ""}`;
48
+ return flags.length > 0 ? `${name} (${kind} ${flags})` : `${name} (${kind})`;
49
+ };
@@ -0,0 +1,18 @@
1
+ import type { DebugOptions } from "../types.js";
2
+ /** Namespaces explicitly requested via `KNITTING_DEBUG`. */
3
+ export declare const DEBUG_NAMESPACES: ReadonlySet<string>;
4
+ /**
5
+ * True when at least one namespace is requested via the env var alone. Guards
6
+ * the env-only paths; callers that also accept a `debug` option should use
7
+ * {@link resolveDebugNamespaces} instead.
8
+ */
9
+ export declare const DEBUG_ENABLED: boolean;
10
+ /**
11
+ * Merge the namespaces requested through the `debug` pool option with those from
12
+ * `KNITTING_DEBUG`; either source can enable a namespace. Returns an empty set
13
+ * when nothing is requested, so callers use `.size === 0` to keep the rest of
14
+ * `src/debug` unloaded — zero cost when off.
15
+ */
16
+ export declare const resolveDebugNamespaces: (debug?: DebugOptions) => Set<string>;
17
+ /** True when `namespace` (or `"*"`) is active for the given `debug` config + env. */
18
+ export declare const debugHas: (debug: DebugOptions | undefined, namespace: string) => boolean;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Zero-cost debug gate.
3
+ *
4
+ * Read once at module load from `KNITTING_DEBUG`. This module is deliberately
5
+ * tiny and dependency-light: importing it must never pull in the logger or the
6
+ * environment-diff machinery. Callers branch on {@link DEBUG_ENABLED} and only
7
+ * then `await import("./handle.ts")`, so when debug is off nothing else under
8
+ * `src/debug` is ever loaded — literally zero cost, not merely cheap.
9
+ *
10
+ * `KNITTING_DEBUG` is a comma-separated list of namespaces:
11
+ * KNITTING_DEBUG=host,imports # only those
12
+ * KNITTING_DEBUG=* # everything
13
+ */
14
+ import { getNodeProcess } from "../common/node-compat.js";
15
+ const readEnv = (key) => {
16
+ // Deno: `process.env` exists under node-compat, but reading it may require
17
+ // --allow-env. Prefer the typed `Deno.env` and swallow permission errors.
18
+ const denoEnv = globalThis.Deno?.env;
19
+ if (typeof denoEnv?.get === "function") {
20
+ try {
21
+ return denoEnv.get(key);
22
+ }
23
+ catch {
24
+ /* env permission denied — fall through to node/bun */
25
+ }
26
+ }
27
+ try {
28
+ return getNodeProcess()?.env?.[key];
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ };
34
+ const raw = readEnv("KNITTING_DEBUG");
35
+ /** Namespaces explicitly requested via `KNITTING_DEBUG`. */
36
+ export const DEBUG_NAMESPACES = new Set((raw ?? "")
37
+ .split(",")
38
+ .map((part) => part.trim())
39
+ .filter((part) => part.length > 0));
40
+ /**
41
+ * True when at least one namespace is requested via the env var alone. Guards
42
+ * the env-only paths; callers that also accept a `debug` option should use
43
+ * {@link resolveDebugNamespaces} instead.
44
+ */
45
+ export const DEBUG_ENABLED = DEBUG_NAMESPACES.size > 0;
46
+ /**
47
+ * Merge the namespaces requested through the `debug` pool option with those from
48
+ * `KNITTING_DEBUG`; either source can enable a namespace. Returns an empty set
49
+ * when nothing is requested, so callers use `.size === 0` to keep the rest of
50
+ * `src/debug` unloaded — zero cost when off.
51
+ */
52
+ export const resolveDebugNamespaces = (debug) => {
53
+ const namespaces = new Set(DEBUG_NAMESPACES);
54
+ if (debug === true) {
55
+ namespaces.add("*");
56
+ }
57
+ else if (debug !== undefined && debug !== false) {
58
+ for (const [key, value] of Object.entries(debug)) {
59
+ if (value === true)
60
+ namespaces.add(key);
61
+ }
62
+ }
63
+ return namespaces;
64
+ };
65
+ /** True when `namespace` (or `"*"`) is active for the given `debug` config + env. */
66
+ export const debugHas = (debug, namespace) => {
67
+ const namespaces = resolveDebugNamespaces(debug);
68
+ return namespaces.has("*") || namespaces.has(namespace);
69
+ };
@@ -0,0 +1,23 @@
1
+ export type DebugInit = {
2
+ /** Identity prefix for log tags, e.g. `"w0"` for a worker or `"main"`. */
3
+ readonly name: string;
4
+ readonly runtime: string;
5
+ readonly namespaces: ReadonlySet<string>;
6
+ };
7
+ export type Debug = {
8
+ /**
9
+ * Is a namespace active? Capture this once before a hot loop and branch on
10
+ * the boolean — never call per-iteration.
11
+ */
12
+ enabled: (namespace: string) => boolean;
13
+ /** Emit a tagged line to stderr when `namespace` is active. */
14
+ log: (namespace: string, message: string) => void;
15
+ /**
16
+ * Re-snapshot `globalThis` and report what changed since the previous phase.
17
+ * Drives two-phase pollution attribution (e.g. `"bootstrap"` then
18
+ * `"tasks"`), so you can see which loader injected which global. No-op unless
19
+ * the `globals` namespace is active.
20
+ */
21
+ envPhase: (label: string) => void;
22
+ };
23
+ export declare const initDebug: ({ name, runtime, namespaces }: DebugInit) => Debug;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Worker-side debug handle. Lazily imported (see {@link ./gate.ts}) only when
3
+ * `KNITTING_DEBUG` names at least one namespace, so neither this code nor the
4
+ * baseline snapshot it takes exists when debug is off.
5
+ *
6
+ * Diagnostics go to stderr so they never corrupt a worker's stdout, and every
7
+ * line is tagged with the worker id, runtime, and a clock relative to when this
8
+ * worker's debug initialised. The clock is worker-local on purpose: a main-thread
9
+ * timestamp can't be compared against `performance.now()` here because the time
10
+ * origins differ across the thread/process boundary (it would read negative).
11
+ */
12
+ import { describeGlobalKey, diffGlobals, snapshotGlobals, } from "./env-diff.js";
13
+ export const initDebug = ({ name, runtime, namespaces }) => {
14
+ const all = namespaces.has("*");
15
+ const enabled = (namespace) => all || namespaces.has(namespace);
16
+ const base = performance.now();
17
+ const tag = `${name}·${runtime}`;
18
+ const log = (namespace, message) => {
19
+ if (!enabled(namespace))
20
+ return;
21
+ const elapsed = (performance.now() - base).toFixed(1);
22
+ console.error(`[${tag}·+${elapsed}ms] ${namespace}: ${message}`);
23
+ };
24
+ // Baseline for the environment diff, taken the moment debug initialises:
25
+ // before worker bootstrap and before any task module is imported. Only paid
26
+ // when `globals` tracing is actually on.
27
+ let previous = enabled("globals")
28
+ ? snapshotGlobals()
29
+ : undefined;
30
+ const envPhase = (label) => {
31
+ if (previous === undefined)
32
+ return;
33
+ const current = snapshotGlobals();
34
+ const { added, removed } = diffGlobals(previous, current);
35
+ previous = current;
36
+ if (added.length === 0 && removed.length === 0) {
37
+ log("globals", `${label}: no new globals`);
38
+ return;
39
+ }
40
+ if (added.length > 0) {
41
+ log("globals", `${label} +${added.length}: ${added.map(describeGlobalKey).join(" ")}`);
42
+ }
43
+ if (removed.length > 0) {
44
+ log("globals", `${label} -${removed.length}: ${removed.map(String).join(" ")}`);
45
+ }
46
+ };
47
+ return { enabled, log, envPhase };
48
+ };
@@ -1,6 +1,5 @@
1
1
  export type SignalArguments = ReturnType<typeof createSharedMemoryTransport>;
2
2
  import { type SharedBufferSource } from "../../common/shared-buffer-region.js";
3
- import { type DebugOptions } from "../../types.js";
4
3
  export declare const TRANSPORT_SIGNAL_BYTES: number;
5
4
  export type Sab = {
6
5
  size?: number;
@@ -10,11 +9,10 @@ type SignalForWorker = {
10
9
  sabObject?: Sab;
11
10
  isMain: boolean;
12
11
  thread: number;
13
- debug?: DebugOptions;
14
12
  startTime?: number;
15
13
  };
16
14
  export declare const createSharedMemoryTransport: ({ sabObject, isMain, startTime }: SignalForWorker) => {
17
- sab: import("../../types.js").SharedBufferRegion;
15
+ sab: import("../../common/shared-buffer-region.js").SharedBufferRegion;
18
16
  op: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
19
17
  startAt: number;
20
18
  opView: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
@@ -1,11 +1,20 @@
1
1
  import { type SharedBufferSource } from "../common/shared-buffer-region.js";
2
2
  export declare const PAYLOAD_DEFAULT_MAX_BYTE_LENGTH: number;
3
3
  export declare const PAYLOAD_DEFAULT_INITIAL_BYTES: number;
4
+ /** Payload backing mode. Growable uses SAB growth when the runtime supports it. */
4
5
  export type PayloadBufferMode = "growable" | "fixed";
5
6
  export type PayloadBufferOptions = {
7
+ /** Shared payload buffer mode. Defaults to growable when available. */
6
8
  mode?: PayloadBufferMode;
9
+ /** Initial shared payload bytes per direction. Defaults to 4 MiB when growable. */
7
10
  payloadInitialBytes?: number;
11
+ /** Shared payload growth cap. Defaults to 64 MiB. */
8
12
  payloadMaxByteLength?: number;
13
+ /**
14
+ * Max dynamic payload bytes for one call. Defaults to `payloadMaxByteLength >> 3`
15
+ * (about 8 MiB by default) and must stay `<= payloadMaxByteLength >> 3`.
16
+ * Over-cap payloads reject with `KNT_ERROR_3`.
17
+ */
9
18
  maxPayloadBytes?: number;
10
19
  };
11
20
  export type ResolvedPayloadBufferOptions = {
@@ -163,4 +163,4 @@ export declare const resolvePermissionProtocol: ({ permission, modules, }: {
163
163
  modules?: string[];
164
164
  }) => ResolvedPermissionProtocol | undefined;
165
165
  export declare const toRuntimePermissionFlags: (protocol: ResolvedPermissionProtocol | undefined) => string[];
166
- export type { PermissionPath, PermissionMode, PermissionLegacyMode, SysApiName, NodePermissionSettings, DenoPermissionSettings, PermissionEnvironment, PermissionProtocol, PermissionProtocolInput, ResolvedPermissionProtocol, };
166
+ export type { DenoPermissionSettings, NodePermissionSettings, PermissionEnvironment, PermissionLegacyMode, PermissionMode, PermissionPath, PermissionProtocol, PermissionProtocolInput, ResolvedPermissionProtocol, SysApiName, };
@@ -107,11 +107,14 @@ const normalizeSysApiList = (values) => {
107
107
  return out;
108
108
  };
109
109
  const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
110
- const normalizeProtocolInput = (input) => !input ? undefined : (typeof input === "string" ? { mode: input } : input);
110
+ const normalizeProtocolInput = (input) => !input
111
+ ? undefined
112
+ : (typeof input === "string" ? { mode: input } : input);
111
113
  const isWindows = () => {
112
114
  const nodeProcess = getNodeProcess();
113
- if (typeof nodeProcess?.platform === "string")
115
+ if (typeof nodeProcess?.platform === "string") {
114
116
  return nodeProcess.platform === "win32";
117
+ }
115
118
  const g = globalThis;
116
119
  return g.Deno?.build?.os === "windows";
117
120
  };
@@ -146,7 +149,8 @@ const getHome = () => {
146
149
  }
147
150
  const g = globalThis;
148
151
  try {
149
- const home = g.Deno?.env?.get?.("HOME") ?? g.Deno?.env?.get?.("USERPROFILE");
152
+ const home = g.Deno?.env?.get?.("HOME") ??
153
+ g.Deno?.env?.get?.("USERPROFILE");
150
154
  if (typeof home === "string" && home.length > 0)
151
155
  return home;
152
156
  }
@@ -198,7 +202,11 @@ const toPathList = (values, cwd, home) => {
198
202
  };
199
203
  const toUniquePathList = (values, cwd, home) => normalizeList(toPathList(values, cwd, home));
200
204
  const toEnvFiles = (input, cwd, home) => {
201
- const values = Array.isArray(input) ? input : input ? [input] : [DEFAULT_ENV_FILE];
205
+ const values = Array.isArray(input)
206
+ ? input
207
+ : input
208
+ ? [input]
209
+ : [DEFAULT_ENV_FILE];
202
210
  return toUniquePathList(values, cwd, home);
203
211
  };
204
212
  const rawRealpathSync = realpathSyncCompat.native ?? realpathSyncCompat;
@@ -212,7 +220,8 @@ const isPathWithin = (base, candidate) => {
212
220
  const canonicalBase = toCanonicalPath(base);
213
221
  const canonicalCandidate = toCanonicalPath(candidate);
214
222
  const relative = pathRelative(canonicalBase, canonicalCandidate);
215
- return relative === "" || (!relative.startsWith("..") && !pathIsAbsolute(relative));
223
+ return relative === "" ||
224
+ (!relative.startsWith("..") && !pathIsAbsolute(relative));
216
225
  };
217
226
  const defaultSensitiveProjectAndHomePaths = (cwd, home) => {
218
227
  const projectSensitive = DEFAULT_DENY_RELATIVE.map((entry) => pathResolve(cwd, entry));
@@ -343,6 +352,13 @@ const toDenoFlags = ({ read, readAll, write, writeAll, denyRead, denyWrite, net,
343
352
  flags.push(`--deny-env=${envDeny.join(",")}`);
344
353
  }
345
354
  for (const file of envFiles) {
355
+ try {
356
+ if (!existsSyncCompat(file))
357
+ continue;
358
+ }
359
+ catch {
360
+ continue;
361
+ }
346
362
  flags.push(`--env-file=${file}`);
347
363
  }
348
364
  if (runAll) {
@@ -476,9 +492,7 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
476
492
  const configuredRead = readAll
477
493
  ? []
478
494
  : toPathList(Array.isArray(input.read) ? input.read : undefined, cwd, home);
479
- const configuredWrite = writeAll
480
- ? []
481
- : toPathList(Array.isArray(input.write) ? input.write : undefined, cwd, home);
495
+ const configuredWrite = writeAll ? [] : toPathList(Array.isArray(input.write) ? input.write : undefined, cwd, home);
482
496
  const resolvedRead = readAll
483
497
  ? []
484
498
  : hasExplicitRead
@@ -501,19 +515,17 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
501
515
  : normalizeStringList(Array.isArray(input.net) ? input.net : []);
502
516
  const denyNet = normalizeStringList(input.denyNet);
503
517
  const allowImportAll = input.allowImport === true;
504
- const allowImport = allowImportAll
505
- ? []
506
- : normalizeStringList(Array.isArray(input.allowImport)
507
- ? input.allowImport
508
- : [...DEFAULT_ALLOW_IMPORT_HOSTS]);
518
+ const allowImport = allowImportAll ? [] : normalizeStringList(Array.isArray(input.allowImport)
519
+ ? input.allowImport
520
+ : [...DEFAULT_ALLOW_IMPORT_HOSTS]);
509
521
  const envAllowAll = input.env?.allow === true;
510
- const envAllow = envAllowAll
511
- ? []
512
- : normalizeStringList(Array.isArray(input.env?.allow) ? input.env.allow : []);
522
+ const envAllow = envAllowAll ? [] : normalizeStringList(Array.isArray(input.env?.allow) ? input.env.allow : []);
513
523
  const envDeny = normalizeStringList(input.env?.deny);
514
524
  const legacyRunEnabled = input.node?.allowChildProcess === true ||
515
525
  input.deno?.allowRun === true;
516
- const runSource = hasOwn(input, "run") ? input.run : (legacyRunEnabled ? true : []);
526
+ const runSource = hasOwn(input, "run")
527
+ ? input.run
528
+ : (legacyRunEnabled ? true : []);
517
529
  const runAll = runSource === true;
518
530
  const run = runAll
519
531
  ? []
@@ -526,9 +538,7 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
526
538
  ? input.ffi
527
539
  : (input.node?.allowAddons === true ? true : false);
528
540
  const ffiAll = ffiSource === true;
529
- const ffi = ffiAll
530
- ? []
531
- : toUniquePathList(Array.isArray(ffiSource) ? ffiSource : undefined, cwd, home);
541
+ const ffi = ffiAll ? [] : toUniquePathList(Array.isArray(ffiSource) ? ffiSource : undefined, cwd, home);
532
542
  const denyFfi = toUniquePathList(input.denyFfi, cwd, home);
533
543
  const sysSource = input.sys;
534
544
  const sysAll = sysSource === true;
@@ -4,7 +4,7 @@ import { lock2 } from "../memory/lock.js";
4
4
  import type { DebugOptions, DispatcherSettings, WorkerContext, WorkerData, WorkerSettings } from "../types.js";
5
5
  import "../worker/loop.js";
6
6
  import { type PayloadBufferOptions } from "../memory/payload-config.js";
7
- export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, usesAbortSignal, }: {
7
+ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, }: {
8
8
  list: string[];
9
9
  ids: number[];
10
10
  names: string[];
@@ -12,6 +12,7 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
12
12
  sab?: Sab;
13
13
  thread: number;
14
14
  debug?: DebugOptions;
15
+ hostDebug?: (message: string) => void;
15
16
  totalNumberOfThread: number;
16
17
  source?: string;
17
18
  workerOptions?: WorkerSettings;
@@ -20,10 +21,6 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
20
21
  host?: DispatcherSettings;
21
22
  payload?: PayloadBufferOptions;
22
23
  bufferReferenceReturn?: "copy" | "borrow";
23
- payloadInitialBytes?: number;
24
- payloadMaxBytes?: number;
25
- bufferMode?: PayloadBufferOptions["mode"];
26
- maxPayloadBytes?: number;
27
24
  abortSignalCapacity?: number;
28
25
  usesAbortSignal?: boolean;
29
26
  }) => WorkerContext & {
@@ -34,7 +34,7 @@ const withFixedPayloadConfig = (config) => ({
34
34
  mode: "fixed",
35
35
  payloadInitialBytes: config.payloadMaxByteLength,
36
36
  });
37
- export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, usesAbortSignal, }) => {
37
+ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, }) => {
38
38
  const tsFileUrl = new URL(import.meta.url);
39
39
  const poliWorker = RUNTIME_WORKER;
40
40
  const resolvedWorkerOptions = serializeWorkerBootstrapData(withDefaultWorkerTimers(workerOptions));
@@ -48,9 +48,6 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, total
48
48
  const processSharedMemorySettings = useProcessWorkerRuntime
49
49
  ? readProcessSharedMemorySettings(resolvedWorkerOptions)
50
50
  : undefined;
51
- if (debug?.logHref === true) {
52
- console.log(tsFileUrl);
53
- }
54
51
  if (!useProcessWorkerRuntime && typeof poliWorker !== "function") {
55
52
  throw new Error("Worker is not available in this runtime");
56
53
  }
@@ -63,15 +60,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, total
63
60
  return bytes > 0 ? bytes : undefined;
64
61
  };
65
62
  const basePayloadConfig = resolvePayloadBufferOptions({
66
- options: {
67
- ...payload,
68
- mode: payload?.mode ?? bufferMode,
69
- maxPayloadBytes: payload?.maxPayloadBytes ?? maxPayloadBytes,
70
- payloadInitialBytes: payload?.payloadInitialBytes ??
71
- sanitizeBytes(payloadInitialBytes),
72
- payloadMaxByteLength: payload?.payloadMaxByteLength ??
73
- sanitizeBytes(payloadMaxBytes),
74
- },
63
+ options: payload,
75
64
  });
76
65
  const resolvedPayloadConfig = useProcessWorkerRuntime
77
66
  ? withFixedPayloadConfig(basePayloadConfig)
@@ -183,7 +172,6 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, total
183
172
  : sab,
184
173
  isMain: true,
185
174
  thread,
186
- debug,
187
175
  });
188
176
  const signalBox = signals;
189
177
  const nativeNotifySignal = createProcessWorkerNativeSignalNotifier({
@@ -208,13 +196,18 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, total
208
196
  channelHandler,
209
197
  dispatcherOptions: host,
210
198
  notifySignal: nativeNotifySignal,
211
- //thread,
212
- //debugSignal: debug?.logMain ?? false,
213
- //perf,
214
199
  });
215
200
  channelHandler.open(check);
216
201
  let worker;
217
202
  const workerUrl = source ?? tsFileUrl;
203
+ const workerMode = useProcessWorkerRuntime
204
+ ? "process"
205
+ : HAS_NODE_WORKER_THREADS
206
+ ? "worker_threads"
207
+ : "worker";
208
+ hostDebug?.(`worker thread=${thread} mode=${workerMode}` +
209
+ `${processWorkerRuntime ? ` runtime=${processWorkerRuntime}` : ""}` +
210
+ ` url=${String(workerUrl)}`);
218
211
  const workerDataPayload = {
219
212
  sab: signals.sab,
220
213
  abortSignalSAB,
@@ -365,7 +358,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, total
365
358
  Promise.resolve().then(check);
366
359
  // Macro lane: dispatcher check is driven by the channel callback.
367
360
  // channelHandler.notify();
368
- // Use opView as a wake counter in lock2 mode to avoid lost wakeups.
361
+ // Use opView as a wake token in lock2 mode to avoid lost wakeups.
369
362
  if (a_load(signalBox.rxStatus, 0) === 0) {
370
363
  a_add(thisSignal, 0, 1);
371
364
  notifySignal?.();
@@ -2,6 +2,7 @@ import { fileURLToPath as fileURLToPathCompat } from "node:url";
2
2
  import { HEADER_SLOT_STRIDE_U32, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
3
3
  import { createByteCarpet, getHeaderBlockByteLength, makeSharedBufferRegion, } from "../memory/byte-carpet.js";
4
4
  import { RUNTIME } from "../common/runtime.js";
5
+ import { debugHas } from "../debug/gate.js";
5
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";
6
7
  import { getNodeBuiltinModule, getNodeProcess } from "../common/node-compat.js";
7
8
  import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
@@ -72,7 +73,7 @@ export const createProcessSharedMemoryAllocator = (debug) => {
72
73
  addon = loadNodeNativeAddon(require, "knitting_shared_memory");
73
74
  }
74
75
  catch (error) {
75
- if (debug?.extras === true) {
76
+ if (debugHas(debug, "lifecycle")) {
76
77
  console.warn("Process-shared memory allocator unavailable; falling back to SharedArrayBuffer.", error);
77
78
  }
78
79
  return undefined;
package/src/types.d.ts CHANGED
@@ -62,8 +62,11 @@ type Serializable = string | object | number | boolean | bigint;
62
62
  type ValidInput = bigint | void | JSONValue | symbol | ArrayBuffer | Uint8Array | Int32Array | Float64Array | BigInt64Array | BigUint64Array | DataView | Error | Date | Envelope<EnvelopeHeader, EnvelopeBody>;
63
63
  type Args = ValidInput | Serializable;
64
64
  type MaybePromise<T> = T | Promise<T>;
65
+ /** Blob payloads are not supported; pass ArrayBuffer/typed arrays instead. */
65
66
  type NoBlob<T> = T extends Blob ? never : T;
67
+ /** Task input may be a direct value or native Promise; thenables are plain values. */
66
68
  type TaskInput = NoBlob<Args> | Promise<NoBlob<Args>>;
69
+ /** Per-call timeout. A number is milliseconds; object form can return a default. */
67
70
  type TaskTimeout = number | {
68
71
  time: number;
69
72
  maybe?: true;
@@ -76,6 +79,7 @@ type BivariantCallback<Args extends unknown[], R> = {
76
79
  type AbortSignalConfig = {
77
80
  readonly hasAborted: true;
78
81
  };
82
+ /** `true` or config injects an abort toolkit as the task's second parameter. */
79
83
  type AbortSignalOption = true | AbortSignalConfig | undefined;
80
84
  type AbortSignalMethods<AS extends AbortSignalOption> = AS extends undefined ? never : {
81
85
  hasAborted: () => boolean;
@@ -118,7 +122,9 @@ type FunctionMapType<T extends Record<string, TaskLike<any> | TaskFunctionLike>>
118
122
  [K in keyof T]: PromiseWrapped<TaskCallable<T[K]>, AbortSignalOfTask<T[K]>>;
119
123
  };
120
124
  interface FixPointBase<A extends TaskInput, B extends Args, AS extends AbortSignalOption = undefined> {
125
+ /** Worker function. It receives one value; use a tuple/object for many inputs. */
121
126
  readonly f: TaskFn<A, B, AS>;
127
+ /** Soft call timeout. Use `worker.hardTimeoutMs` for runaway CPU walls. */
122
128
  readonly timeout?: TaskTimeout;
123
129
  }
124
130
  type FixPoint<A extends TaskInput, B extends Args, AS extends AbortSignalOption = undefined> = FixPointBase<A, B, AS> & (AS extends undefined ? {
@@ -127,7 +133,9 @@ type FixPoint<A extends TaskInput, B extends Args, AS extends AbortSignalOption
127
133
  readonly abortSignal: AS;
128
134
  });
129
135
  type ImportTaskOptions<A extends TaskInput = void, B extends Args = void, AS extends AbortSignalOption = undefined> = Omit<FixPoint<A, B, AS>, "f"> & {
136
+ /** Module imported by the worker only. Relative paths resolve from caller. */
130
137
  readonly href: string;
138
+ /** Plain function export name. Defaults to `"default"`; do not target `task()`. */
131
139
  readonly name?: string;
132
140
  };
133
141
  type SecondPart = {
@@ -142,13 +150,22 @@ type SecondPart = {
142
150
  readonly imported?: boolean;
143
151
  };
144
152
  type SingleTaskPool<A extends TaskInput = Args, B extends Args = Args, AS extends AbortSignalOption = undefined> = {
153
+ /** Invoke the single task. Arguments may be native Promises. */
145
154
  call: PromiseWrapped<TaskFn<A, B, AS>, AS>;
155
+ /** Await worker teardown now. `using` disposes at scope exit without awaiting. */
146
156
  shutdown: (delayMs?: number) => Promise<void>;
157
+ /** Starts shutdown at scope exit. Use `shutdown()` when you must await it. */
147
158
  [Symbol.dispose]: () => void;
148
159
  };
149
160
  type Pool<T extends Record<string, TaskLike<any> | TaskFunctionLike>> = {
161
+ /** Await worker teardown now. `using` disposes at scope exit without awaiting. */
150
162
  shutdown: (delayMs?: number) => Promise<void>;
163
+ /** Starts shutdown at scope exit. Use `shutdown()` when you must await it. */
151
164
  [Symbol.dispose]: () => void;
165
+ /**
166
+ * Typed task callers. Each call accepts the task input or a native Promise.
167
+ * Thrown errors/rejections reject here as Error objects with cause chains.
168
+ */
152
169
  call: FunctionMapType<T>;
153
170
  };
154
171
  type ReturnFixed<A extends TaskInput = undefined, B extends Args = undefined, AS extends AbortSignalOption = undefined> = FixPoint<A, B, AS> & SecondPart & {
@@ -175,12 +192,13 @@ type Balancer = BalancerStrategy | {
175
192
  */
176
193
  strategy?: BalancerStrategy;
177
194
  };
178
- type DebugOptions = {
179
- extras?: boolean;
180
- logMain?: boolean;
181
- logHref?: boolean;
182
- logImportedUrl?: boolean;
195
+ /** Debug namespaces for host setup, worker state, imports, globals, and lifecycle. */
196
+ type DebugNamespace = "host" | "globals" | "signals" | "imports" | "lifecycle";
197
+ type DebugFlags = {
198
+ [Namespace in DebugNamespace]?: boolean;
183
199
  };
200
+ /** Pass `true` for all debug, or enable namespaces by name. */
201
+ type DebugOptions = boolean | DebugFlags;
184
202
  type WorkerBootstrapContext = {
185
203
  readonly thread: number;
186
204
  readonly totalNumberOfThread: number;
@@ -212,7 +230,7 @@ type WorkerSettings = {
212
230
  /**
213
231
  * Experimental worker runtime.
214
232
  * "thread" uses Worker/worker_threads. "process" spawns another JavaScript
215
- * runtime and shares one inherited fd-backed memory mapping.
233
+ * runtime, useful for process isolation, bwrap, and containers.
216
234
  */
217
235
  runtime?: "thread" | "process";
218
236
  /**
@@ -280,42 +298,21 @@ type DispatcherSettings = {
280
298
  maxBackoffMs?: number;
281
299
  };
282
300
  type CreatePool = {
301
+ /** Number of workers. Default: 1. */
283
302
  threads?: number;
284
- /**
285
- * @deprecated Too risky with processes, need to rewrite or delete.
286
- */
303
+ /** Add a host inline lane for regular tasks. Imported tasks still use workers. */
287
304
  inliner?: Inliner;
288
305
  balancer?: Balancer;
289
306
  worker?: WorkerSettings;
290
307
  /**
291
- * Payload transport settings.
308
+ * Payload transport settings. Default dynamic payload cap is about 8 MiB
309
+ * (`payloadMaxByteLength >> 3`, with a 64 MiB growth cap).
292
310
  */
293
311
  payload?: PayloadBufferOptions;
294
312
  /**
295
313
  * Experimental unsafe options.
296
314
  */
297
315
  unsafe?: UnsafeOptions;
298
- /**
299
- * Initial payload SharedArrayBuffer size (bytes) per worker direction.
300
- * Defaults to 4 MiB when growable SAB is available, otherwise defaults to
301
- * `payloadMaxBytes`.
302
- * @deprecated Use `payload.payloadInitialBytes`.
303
- */
304
- payloadInitialBytes?: number;
305
- /**
306
- * Maximum payload SharedArrayBuffer size (bytes) per worker direction.
307
- * Defaults to 64 MiB.
308
- * @deprecated Use `payload.payloadMaxByteLength`.
309
- */
310
- payloadMaxBytes?: number;
311
- /**
312
- * @deprecated Use `payload.mode`.
313
- */
314
- bufferMode?: PayloadBufferMode;
315
- /**
316
- * @deprecated Use `payload.maxPayloadBytes`.
317
- */
318
- maxPayloadBytes?: number;
319
316
  /**
320
317
  * Abort-aware signal pool capacity.
321
318
  * Defaults to `258`.
@@ -332,19 +329,18 @@ type CreatePool = {
332
329
  workerExecArgv?: string[];
333
330
  /**
334
331
  * Runtime permission protocol.
335
- * Omit to use strict defaults with `allowImport: true`.
332
+ * Omit to use strict defaults with `allowImport: true`; worker console is
333
+ * quiet unless `permission: { console: true }`.
334
+ *
335
+ * Task code cannot terminate the host: process/Deno exit APIs are blocked.
336
336
  * Use `"strict"` (default for object mode) or `"unsafe"`.
337
337
  * Accepts object form for fine-grained permission controls.
338
338
  */
339
339
  permission?: PermissionProtocolInput;
340
- /**
341
- * @deprecated Use `host` instead.
342
- */
343
- dispatcher?: DispatcherSettings;
344
340
  debug?: DebugOptions;
345
341
  source?: string;
346
342
  };
347
- 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, 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, };
343
+ 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, };
348
344
  export type { Task as Task } from "./memory/lock.js";
349
345
  export { LockBound as LockBound, PayloadBuffer as PayloadBuffer, PayloadSignal as PayloadSignal, TaskIndex as TaskIndex, } from "./memory/lock.js";
350
346
  export type { RegisterMalloc as RegisterMalloc } from "./memory/regionRegistry.js";
@@ -3,13 +3,21 @@ export type NumberFormat = "f64" | "f32" | "i32";
3
3
  export type NumberBufferOptions = {
4
4
  format?: NumberFormat;
5
5
  };
6
+ /** View ArrayBuffer/SharedArrayBuffer/typed-array bytes as Uint8Array. */
6
7
  export declare const bufferToBytes: (source: BufferLike) => Uint8Array;
8
+ /** Copy bytes into a SharedArrayBuffer for zero-copy worker inputs. */
7
9
  export declare const bytesToBuffer: (source: BufferLike) => SharedArrayBuffer;
10
+ /** Decode UTF-8 bytes from ArrayBuffer/SharedArrayBuffer/typed-array input. */
8
11
  export declare const bufferToString: (source: BufferLike) => string;
12
+ /** Encode UTF-8 text into a SharedArrayBuffer. */
9
13
  export declare const stringToBuffer: (text: string) => SharedArrayBuffer;
14
+ /** Decode JSON from UTF-8 bytes. */
10
15
  export declare const bufferToJson: <T = unknown>(source: BufferLike) => T;
16
+ /** Encode JSON into a SharedArrayBuffer. */
11
17
  export declare const jsonToBuffer: (value: unknown) => SharedArrayBuffer;
18
+ /** Encode numbers into a SharedArrayBuffer (`f64` by default). */
12
19
  export declare const numbersToBuffer: (numbers: ArrayLike<number>, options?: NumberBufferOptions) => SharedArrayBuffer;
20
+ /** Decode numeric typed-array views from worker-friendly byte buffers. */
13
21
  export declare function bufferToNumbers(source: BufferLike, options?: {
14
22
  format?: "f64";
15
23
  }): Float64Array;
package/src/utils/http.js CHANGED
@@ -21,7 +21,9 @@ const toBytes = (source) => {
21
21
  }
22
22
  throw new TypeError("Expected an ArrayBuffer, SharedArrayBuffer, or typed-array view.");
23
23
  };
24
+ /** View ArrayBuffer/SharedArrayBuffer/typed-array bytes as Uint8Array. */
24
25
  export const bufferToBytes = (source) => toBytes(source);
26
+ /** Copy bytes into a SharedArrayBuffer for zero-copy worker inputs. */
25
27
  export const bytesToBuffer = (source) => {
26
28
  const bytes = toBytes(source);
27
29
  const sab = new SharedArrayBuffer(bytes.byteLength);
@@ -38,6 +40,7 @@ const makeNumberView = (format, buffer, byteOffset, length) => {
38
40
  return new Int32Array(buffer, byteOffset, length);
39
41
  }
40
42
  };
43
+ /** Decode UTF-8 bytes from ArrayBuffer/SharedArrayBuffer/typed-array input. */
41
44
  export const bufferToString = (source) => {
42
45
  const bytes = toBytes(source);
43
46
  if (hasNodeBuffer) {
@@ -47,6 +50,7 @@ export const bufferToString = (source) => {
47
50
  }
48
51
  return textDecoder.decode(bytes);
49
52
  };
53
+ /** Encode UTF-8 text into a SharedArrayBuffer. */
50
54
  export const stringToBuffer = (text) => {
51
55
  if (typeof text !== "string") {
52
56
  throw new TypeError("stringToBuffer expects a string.");
@@ -62,7 +66,9 @@ export const stringToBuffer = (text) => {
62
66
  new Uint8Array(sab).set(encoded);
63
67
  return sab;
64
68
  };
69
+ /** Decode JSON from UTF-8 bytes. */
65
70
  export const bufferToJson = (source) => JSON.parse(bufferToString(source));
71
+ /** Encode JSON into a SharedArrayBuffer. */
66
72
  export const jsonToBuffer = (value) => {
67
73
  const text = JSON.stringify(value);
68
74
  if (typeof text !== "string") {
@@ -70,6 +76,7 @@ export const jsonToBuffer = (value) => {
70
76
  }
71
77
  return stringToBuffer(text);
72
78
  };
79
+ /** Encode numbers into a SharedArrayBuffer (`f64` by default). */
73
80
  export const numbersToBuffer = (numbers, options) => {
74
81
  const format = options?.format ?? "f64";
75
82
  const sab = new SharedArrayBuffer(numbers.length * BYTES_PER_ELEMENT[format]);
@@ -13,6 +13,7 @@ import { signalAbortFactory } from "../shared/abortSignal.js";
13
13
  import { runWorkerBootstrap } from "./bootstrap.js";
14
14
  import { getProcessWorkerNativeWaitU32, installProcessWorkerBootstrap, } from "./process-worker-bootstrap.js";
15
15
  import { readBufferReferenceReturnReleaseMessage, } from "../connections/buffer-reference.js";
16
+ import { resolveDebugNamespaces } from "../debug/gate.js";
16
17
  const WORKER_FATAL_MESSAGE_KEY = "__knittingWorkerFatal";
17
18
  const reportWorkerStartupFatal = (error) => {
18
19
  const message = String(error?.message ?? error);
@@ -72,6 +73,14 @@ export const workerMainLoop = async (startupData) => {
72
73
  const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, bufferReferenceReturn, permission, totalNumberOfThread, list, ids, names, at, } = startupData;
73
74
  scrubWorkerDataSensitiveBuffers(startupData);
74
75
  assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
76
+ const debugNamespaces = resolveDebugNamespaces(debug);
77
+ const dbg = debugNamespaces.size > 0
78
+ ? await import("../debug/handle.js").then((module) => module.initDebug({
79
+ name: `w${thread}`,
80
+ runtime: RUNTIME,
81
+ namespaces: debugNamespaces,
82
+ }))
83
+ : undefined;
75
84
  let Comment;
76
85
  (function (Comment) {
77
86
  Comment[Comment["thisIsAHint"] = 0] = "thisIsAHint";
@@ -82,7 +91,6 @@ export const workerMainLoop = async (startupData) => {
82
91
  },
83
92
  isMain: false,
84
93
  thread,
85
- debug,
86
94
  startTime: startAt,
87
95
  });
88
96
  const lockState = lock2({
@@ -106,8 +114,10 @@ export const workerMainLoop = async (startupData) => {
106
114
  const timers = workerOptions?.timers;
107
115
  const spinMicroseconds = timers?.spinMicroseconds ??
108
116
  Math.max(1, totalNumberOfThread) * 50;
109
- const parkMs = timers?.parkMs ??
110
- Math.max(1, totalNumberOfThread) * 50;
117
+ const parkMs = dbg !== undefined
118
+ ? Number.POSITIVE_INFINITY
119
+ : (timers?.parkMs ??
120
+ Math.max(1, totalNumberOfThread) * 50);
111
121
  const pauseSpin = (() => {
112
122
  const fn = typeof timers?.pauseNanoseconds === "number"
113
123
  ? whilePausing({ pauseInNanoseconds: timers.pauseNanoseconds })
@@ -123,6 +133,7 @@ export const workerMainLoop = async (startupData) => {
123
133
  thread,
124
134
  totalNumberOfThread,
125
135
  });
136
+ dbg?.envPhase("bootstrap");
126
137
  const listOfFunctions = await getFunctions({
127
138
  list,
128
139
  isWorker: true,
@@ -131,7 +142,9 @@ export const workerMainLoop = async (startupData) => {
131
142
  at,
132
143
  permission,
133
144
  });
134
- assertWorkerImportsResolved({ debug, list, ids, names, listOfFunctions });
145
+ dbg?.envPhase("tasks");
146
+ dbg?.log("imports", `${listOfFunctions.length} task(s) from ${list.map((spec) => spec.split(/[\\/]/).pop() || spec).join(", ")}`);
147
+ assertWorkerImportsResolved({ list, ids, names, listOfFunctions });
135
148
  const abortSignals = abortSignalSAB
136
149
  ? signalAbortFactory({
137
150
  sab: abortSignalSAB,
@@ -170,7 +183,7 @@ export const workerMainLoop = async (startupData) => {
170
183
  let awaitingSpins = 0;
171
184
  let lastAwaiting = 0;
172
185
  const MAX_AWAITING_MS = 10;
173
- let wakeSeq = a_load(opView, 0);
186
+ let wakeToken = a_load(opView, 0);
174
187
  const scheduleMacro = () => {
175
188
  if (isInMacro)
176
189
  return;
@@ -195,15 +208,43 @@ export const workerMainLoop = async (startupData) => {
195
208
  }
196
209
  post2(null);
197
210
  };
198
- const _enqueueLock = enqueueLock;
211
+ const traceSignals = dbg?.enabled("signals") === true;
199
212
  const _hasCompleted = hasCompleted;
200
- const _writeBatch = writeBatch;
201
213
  const _hasPending = hasPending;
202
- const _serviceBatchImmediate = serviceBatchImmediate;
203
214
  const _getAwaiting = getAwaiting;
204
215
  const _drainReturnReleases = drainReturnReleases;
205
216
  const _pauseSpin = pauseSpin;
206
- const _pauseUntil = pauseUntil;
217
+ const _enqueueLock = traceSignals
218
+ ? () => {
219
+ const progressed = enqueueLock();
220
+ if (progressed) {
221
+ dbg.log("signals", "work from=host");
222
+ }
223
+ return progressed;
224
+ }
225
+ : enqueueLock;
226
+ const _writeBatch = traceSignals
227
+ ? (max) => {
228
+ const wrote = writeBatch(max);
229
+ if (wrote > 0)
230
+ dbg.log("signals", `result count=${wrote}`);
231
+ return wrote;
232
+ }
233
+ : writeBatch;
234
+ const _serviceBatchImmediate = traceSignals
235
+ ? () => {
236
+ const ran = serviceBatchImmediate();
237
+ if (ran > 0)
238
+ dbg.log("signals", `run count=${ran}`);
239
+ return ran;
240
+ }
241
+ : serviceBatchImmediate;
242
+ const _pauseUntil = traceSignals
243
+ ? (value, spinMicroseconds, parkMs) => {
244
+ dbg.log("signals", `idle token=${value}`);
245
+ pauseUntil(value, spinMicroseconds, parkMs);
246
+ }
247
+ : pauseUntil;
207
248
  const loop = () => {
208
249
  isInMacro = false;
209
250
  let progressed = true;
@@ -234,8 +275,8 @@ export const workerMainLoop = async (startupData) => {
234
275
  _pauseSpin();
235
276
  continue;
236
277
  }
237
- _pauseUntil(wakeSeq, spinMicroseconds, parkMs);
238
- wakeSeq = a_load(opView, 0);
278
+ _pauseUntil(wakeToken, spinMicroseconds, parkMs);
279
+ wakeToken = a_load(opView, 0);
239
280
  }
240
281
  }
241
282
  };
@@ -249,6 +290,7 @@ export const workerMainLoop = async (startupData) => {
249
290
  }
250
291
  port1Any.start?.();
251
292
  port2.start?.();
293
+ dbg?.log("lifecycle", `ready: ${listOfFunctions.length} task(s) on thread ${thread}/${totalNumberOfThread}, entering dispatch loop`);
252
294
  scheduleMacro();
253
295
  };
254
296
  const isWorkerGlobalScope = () => {
@@ -1,17 +1,16 @@
1
1
  import { type SharedBufferSource } from "../../common/shared-buffer-region.js";
2
- import type { DebugOptions, LockBuffers } from "../../types.js";
2
+ import type { LockBuffers } from "../../types.js";
3
3
  type SharedMemoryBootData = {
4
4
  sab: SharedBufferSource | undefined;
5
5
  lock: LockBuffers | undefined;
6
6
  returnLock: LockBuffers | undefined;
7
7
  };
8
8
  type ImportedFunctionsState = {
9
- debug: DebugOptions | undefined;
10
9
  list: string[];
11
10
  ids: number[];
12
11
  names?: string[];
13
12
  listOfFunctions: readonly unknown[];
14
13
  };
15
14
  export declare const assertWorkerSharedMemoryBootData: ({ sab, lock, returnLock }: SharedMemoryBootData) => void;
16
- export declare const assertWorkerImportsResolved: ({ debug, list, ids, names, listOfFunctions }: ImportedFunctionsState) => void;
15
+ export declare const assertWorkerImportsResolved: ({ list, ids, names, listOfFunctions }: ImportedFunctionsState) => void;
17
16
  export {};
@@ -17,10 +17,7 @@ export const assertWorkerSharedMemoryBootData = ({ sab, lock, returnLock }) => {
17
17
  throw new Error("worker missing return lock SABs");
18
18
  }
19
19
  };
20
- export const assertWorkerImportsResolved = ({ debug, list, ids, names, listOfFunctions }) => {
21
- if (debug?.logImportedUrl === true) {
22
- console.log(list);
23
- }
20
+ export const assertWorkerImportsResolved = ({ list, ids, names, listOfFunctions }) => {
24
21
  if (listOfFunctions.length > 0 &&
25
22
  (names === undefined || listOfFunctions.length === names.length))
26
23
  return;
@@ -50,7 +50,7 @@ const isPlainNodeWindows = runtimeGlobals.process?.platform === "win32" &&
50
50
  // full parkMs (up to seconds); the long park with no wake is what made CI
51
51
  // runners appear to hang. The native wait already polls internally, so this is
52
52
  // just bounding each poll slice.
53
- const nativeWaitTimeoutMs = (parkMs) => isPlainNodeWindows ? 1 : parkMs ?? 60;
53
+ const nativeWaitTimeoutMs = (parkMs) => isPlainNodeWindows ? 1 : Number.isFinite(parkMs) ? parkMs : 60;
54
54
  export const whilePausing = ({ pauseInNanoseconds }) => {
55
55
  const forNanoseconds = pauseInNanoseconds ?? DEFAULT_PAUSE_TIME;
56
56
  if (!a_pause || forNanoseconds <= 0)
@@ -101,10 +101,15 @@ export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, tx
101
101
  else if (useSharedMemoryWait &&
102
102
  a_wait &&
103
103
  opView.buffer instanceof SharedArrayBuffer) {
104
+ // This is the notify-able path: a host Atomics.notify on opView wakes it.
105
+ // An infinite parkMs (debug) is intentional here — the worker blocks until
106
+ // a real signal instead of re-polling on a timeout.
104
107
  a_wait(opView, at, value, parkMs ?? 60);
105
108
  }
106
109
  else if (a_wait && waitFallbackView) {
107
- a_wait(waitFallbackView, 0, 0, parkMs ?? 1);
110
+ // waitFallbackView is never notified — this wait only ever times out and
111
+ // re-polls, so it must stay finite or the worker would hang forever.
112
+ a_wait(waitFallbackView, 0, 0, Number.isFinite(parkMs) ? parkMs : 1);
108
113
  }
109
114
  a_store(rxStatus, 0, 1);
110
115
  };