knitting 0.1.52 → 0.1.54

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
@@ -11,18 +11,22 @@
11
11
  [![Deno](https://img.shields.io/badge/deno-2%2B-000000?logo=deno&logoColor=white)](https://deno.com/)
12
12
  [![Bun](https://img.shields.io/badge/bun-1%2B-f472b6?logo=bun&logoColor=white)](https://bun.sh/)
13
13
 
14
- Knitting is a worker pool over a shared-memory IPC runtime for Node.js, Deno,
15
- and Bun. Our mission is to make JavaScript a multicore language with real
16
- inter-runtime communication.
14
+ Website: [knittingdocs.netlify.app](https://knittingdocs.netlify.app/)
17
15
 
18
- Thanks to its memory design, it can be 5x to 25x faster than using
19
- `postMessages` , bypassing OS socket communication entirely with a novel
20
- protocol written from scratch.
16
+ If you are an agent trying to understand the project, the website also serves an
17
+ [`llms.txt`](https://knittingdocs.netlify.app/llms.txt) file with a compact map
18
+ of the docs.
21
19
 
22
- Use it for parts of your program that should run in different environments, such
23
- as CPU-intensive tasks, small jobs, runtime-isolated tasks, custom isolation for
24
- workers in Docker or bwrap environments, long-running tools, or any processes
25
- that require speed and type flexibility.
20
+ Knitting is a worker pool built on shared-memory IPC for Node.js, Deno, and Bun.
21
+ It lets you call work running on other threads or processes as if it were a
22
+ normal async function.
23
+
24
+ Because calls move through shared memory instead of `postMessage` or sockets,
25
+ some workloads can be 5x to 25x faster than the usual worker-message path.
26
+
27
+ Use it when part of your program should run somewhere else: CPU-heavy work,
28
+ bursty small jobs, runtime-isolated code, Docker or bwrap workers, long-running
29
+ tools, or cross-runtime process work that still needs to be fast and typed.
26
30
 
27
31
  You export a function or task, spin up a pool, and call it like a normal async
28
32
  function:
@@ -31,29 +35,28 @@ function:
31
35
  const result = await pool.call.resizeImage(file);
32
36
  ```
33
37
 
34
- So you only have to take care of 4 things:
38
+ Most of the time, you only have to take care of four things:
35
39
 
36
40
  - Export a function or task
37
41
  - Create a pool
38
42
  - Call it
39
43
  - Let `using` or `shutdown()` close the pool
40
44
 
41
- Under the hood, we take care of scheduling and orchestration across worker
42
- threads or separate processes, also handling signals, timeouts, life cycles,
43
- memory allocation, garbage collection, and cross-runtime memory over the
44
- processes.
45
+ Under the hood, Knitting handles scheduling across worker threads or separate
46
+ processes, plus signals, timeouts, lifecycles, memory allocation, cleanup, and
47
+ cross-runtime shared memory.
45
48
 
46
49
  ## Why use it?
47
50
 
48
- - Easy to use: Have a multithreaded environment or process with few lines of
49
- code.
50
- - Great type support: pass primitives, JSON, Promise of these, and special types
51
- (typed arrays, `Node Buffer`, `Envelope`, and `ProcessSharedBuffer`).
51
+ - Easy to use: spin up threads or processes with a small API.
52
+ - Great type support: pass primitives, JSON, promises of those values, and
53
+ special types like typed arrays, `Node Buffer`, `Envelope`, and
54
+ `ProcessSharedBuffer`.
52
55
  - Runtime flexibility: the same API across Node.js, Deno, and Bun.
53
56
  - Worker choices: use threads for fast pools or processes for stronger
54
57
  isolation.
55
- - All out-of-the-box experiences: strict-by-default permissions, payload-size
56
- limits, task timeouts, abort-aware tasks, and worker hard timeouts.
58
+ - Practical defaults: strict worker permissions, payload-size limits, task
59
+ timeouts, abort-aware tasks, and worker hard timeouts.
57
60
 
58
61
  ## Requirements
59
62
 
@@ -96,9 +99,9 @@ if (isMain) {
96
99
  }
97
100
  ```
98
101
 
99
- The `isMain` guard when the same module is loaded by workers or process. Export
100
- exposes the tasks or functions at module scope, so knitting maps down the
101
- imports, then use and use the pool only from the main program.
102
+ Use the `isMain` guard when a module can be loaded by both the host and its
103
+ workers. Export tasks at module scope so Knitting can find them, then create and
104
+ use the pool only from the main program.
102
105
 
103
106
  ## The Mental Model
104
107
 
@@ -335,9 +338,17 @@ Common options you might tweak:
335
338
  | `worker.runtime` | Choose `"thread"` or `"process"` workers. |
336
339
  | `worker.processSharedMemory` | Process-worker memory discovery: `"inherit"` by default on POSIX, or `"named"` for wrappers/containers that cannot preserve fd 0. |
337
340
  | `permission` | Runtime permission policy for workers. |
338
- | `debug` | Enable extra diagnostics. |
341
+ | `host.dispatcher` | Experimental host dispatcher topology: `"per-thread"` or `"serial-channel"`. |
342
+ | `debug` | Enable diagnostics (`host`, `globals`, `signals`, `imports`, `lifecycle`) or use `KNITTING_DEBUG`. |
339
343
  | `source` | Worker source override for advanced runtimes. |
340
344
 
345
+ Most users can leave `host.dispatcher` alone. The current default is
346
+ experimental: Bun and single-worker pools use `"per-thread"`, while multi-worker
347
+ Node/Deno pools use `"serial-channel"` because it tends to behave well for
348
+ bursty HTTP-style fan-out. If you are tuning a server or comparing runtimes, you
349
+ can force either mode with `KNITTING_DISPATCHER=per-thread` or
350
+ `KNITTING_DISPATCHER=serial-channel`.
351
+
341
352
  ### Worker bootstrap
342
353
 
343
354
  Use `worker.bootstrap` when a worker needs privileged setup before task modules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knitting",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
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,52 @@ 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 { ChannelHandler } from "./runtime/dispatcher.js";
16
+ import { RUNTIME } from "./common/runtime.js";
14
17
  import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
15
18
  import { resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
16
19
  import { getNodeProcess } from "./common/node-compat.js";
17
20
  import { managerMethod } from "./runtime/balancer.js";
18
21
  import { createInlineExecutor } from "./runtime/inline-executor.js";
22
+ const hasDebugNamespace = (namespaces, namespace) => namespaces.has("*") || namespaces.has(namespace);
23
+ const createHostDebug = (namespaces) => {
24
+ const enabled = (namespace) => hasDebugNamespace(namespaces, namespace);
25
+ if (!enabled("host"))
26
+ return undefined;
27
+ const base = performance.now();
28
+ const tag = `host·${RUNTIME}`;
29
+ const log = (message) => {
30
+ const elapsed = (performance.now() - base).toFixed(1);
31
+ console.error(`[${tag}·+${elapsed}ms] host: ${message}`);
32
+ };
33
+ return { log };
34
+ };
35
+ const readHostCwd = () => {
36
+ const denoCwd = globalThis.Deno?.cwd;
37
+ if (typeof denoCwd === "function") {
38
+ try {
39
+ return denoCwd();
40
+ }
41
+ catch {
42
+ }
43
+ }
44
+ const nodeProcess = getNodeProcess();
45
+ if (typeof nodeProcess?.cwd === "function") {
46
+ try {
47
+ return nodeProcess.cwd();
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ return undefined;
54
+ };
55
+ const formatDebugList = (values, empty = "(none)") => values && values.length > 0 ? values.join(",") : empty;
19
56
  const MAX_FUNCTION_ID = 0xFFFF;
20
57
  const MAX_FUNCTION_COUNT = MAX_FUNCTION_ID + 1;
21
58
  const DEFAULT_IMPORT_EXPORT_NAME = "default";
@@ -93,14 +130,36 @@ const toPoolTaskEntries = (input, callerHref) => Object.entries(input).map(([nam
93
130
  }
94
131
  throw new TypeError(`createPool task "${name}" must be a task definition or exported function`);
95
132
  });
96
- export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, source, worker, workerExecArgv, permission, dispatcher, host, }) => (tasks) => {
133
+ /**
134
+ * Create a typed worker pool from module-scope exported tasks/functions.
135
+ *
136
+ * Install/import as `knitting` on npm or `@vixeny/knitting` on JSR. Requires
137
+ * Node 22+, Deno 2+, or Bun 1+.
138
+ *
139
+ * Use `createPool(options)({ taskA })`, call `await pool.call.taskA(arg)`, and
140
+ * clean up with `using pool = ...` or `await pool.shutdown()`.
141
+ *
142
+ * Guard host-only pool setup with `isMain`: workers re-import task modules, and
143
+ * top-level imports in those modules run in every worker. Keep task modules
144
+ * lean and separate from server/framework setup. Each task receives one
145
+ * argument; use an object or tuple for multiple values.
146
+ */
147
+ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe, abortSignalCapacity, source, worker, workerExecArgv, permission, host, }) => (tasks) => {
97
148
  const bufferReferenceReturn = unsafe?.BufferReferenceReturn;
149
+ const debugRequested = DEBUG_ENABLED ||
150
+ (debug !== undefined && debug !== false);
151
+ let debugNamespaces;
152
+ const getDebugNamespaces = () => debugNamespaces ??= resolveDebugNamespaces(debug);
153
+ const hostDebug = debugRequested
154
+ ? createHostDebug(getDebugNamespaces())
155
+ : undefined;
156
+ const debugEnabled = (namespace) => debugRequested && hasDebugNamespace(getDebugNamespaces(), namespace);
98
157
  /**
99
158
  * This functions is only available in the main thread.
100
159
  * Also triggers when debug extra is enabled.
101
160
  */
102
161
  if (RUNTIME_IS_MAIN_THREAD === false) {
103
- if ((debug?.extras === true)) {
162
+ if (debugEnabled("lifecycle")) {
104
163
  console.warn("createPool has been called with : " + JSON.stringify(RUNTIME_WORKER_DATA));
105
164
  }
106
165
  const notMainThreadError = () => {
@@ -126,6 +185,10 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
126
185
  const listOfFunctions = toPoolTaskEntries(tasks, callerHref)
127
186
  .sort((a, b) => a.name.localeCompare(b.name));
128
187
  const { list, ids, names, at } = toListAndIds(listOfFunctions);
188
+ hostDebug?.log(`cwd=${readHostCwd() ?? "(unknown)"} caller=${callerHref}`);
189
+ listOfFunctions.forEach((fn) => {
190
+ hostDebug?.log(`task name=${fn.name} id=${fn.id} from=${fn.importedFrom}`);
191
+ });
129
192
  if (listOfFunctions.length > MAX_FUNCTION_COUNT) {
130
193
  throw new RangeError(`Too many tasks: received ${listOfFunctions.length}. ` +
131
194
  `Maximum is ${MAX_FUNCTION_COUNT} (Uint16 function IDs: 0..${MAX_FUNCTION_ID}).`);
@@ -196,9 +259,16 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
196
259
  ...(defaultExecArgv ?? []),
197
260
  ]);
198
261
  const execArgv = sanitizeExecArgv(combinedExecArgv.length > 0 ? combinedExecArgv : undefined);
199
- const hostDispatcher = host ?? dispatcher;
262
+ hostDebug?.log(`pool runtime=${RUNTIME} workers=${threads ?? 1}` +
263
+ ` lanes=${totalNumberOfThread} inliner=${usingInliner ? "on" : "off"}`);
264
+ hostDebug?.log(`modules=${formatDebugList(list)}`);
265
+ hostDebug?.log(`permission=${permissionProtocol?.mode ?? "off"} execArgv=${formatDebugList(execArgv)}`);
200
266
  const usesAbortSignal = listOfFunctions.some((fn) => fn.abortSignal !== undefined);
201
267
  const resolvedWorker = resolveWorkerBootstrapSettings(worker, callerHref);
268
+ if (resolvedWorker?.bootstrap !== undefined) {
269
+ hostDebug?.log(`bootstrap href=${resolvedWorker.bootstrap.href}` +
270
+ ` name=${resolvedWorker.bootstrap.name}`);
271
+ }
202
272
  if (usingInliner && resolvedWorker?.bootstrap !== undefined) {
203
273
  throw new Error("worker.bootstrap cannot be used with the inliner");
204
274
  }
@@ -214,6 +284,26 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
214
284
  `(import { isMain } from "knitting") so only the main program starts ` +
215
285
  `the pool.`);
216
286
  }
287
+ const dispatcherEnv = nodeProcess?.env?.KNITTING_DISPATCHER;
288
+ const explicitDispatcher = host?.dispatcher ??
289
+ (dispatcherEnv === "serial-channel" || dispatcherEnv === "per-thread"
290
+ ? dispatcherEnv
291
+ : undefined);
292
+ const autoDispatcher = (() => {
293
+ // Experimental default for HTTP-style bursts: Bun still favors direct
294
+ // per-thread channels, while Node/Deno multi-worker pools favor one shared
295
+ // macro channel over the per-lane dispatcher checks.
296
+ if (RUNTIME === "bun")
297
+ return "per-thread";
298
+ if ((threads ?? 1) <= 1)
299
+ return "per-thread";
300
+ return "serial-channel";
301
+ })();
302
+ const dispatcher = explicitDispatcher ?? autoDispatcher;
303
+ const serialChannel = dispatcher === "serial-channel";
304
+ const serialDispatcherChannel = serialChannel
305
+ ? new ChannelHandler()
306
+ : undefined;
217
307
  let workers = Array.from({
218
308
  length: threads ?? 1,
219
309
  }).map((_, thread) => spawnWorkerContext({
@@ -223,21 +313,67 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
223
313
  at,
224
314
  thread,
225
315
  debug,
316
+ hostDebug: hostDebug?.log,
226
317
  totalNumberOfThread,
227
318
  source,
228
319
  workerOptions: resolvedWorker,
229
320
  workerExecArgv: execArgv,
230
- host: hostDispatcher,
321
+ host,
231
322
  payload,
232
323
  bufferReferenceReturn,
233
- payloadInitialBytes,
234
- payloadMaxBytes,
235
- bufferMode,
236
- maxPayloadBytes,
237
324
  abortSignalCapacity,
238
325
  usesAbortSignal,
239
326
  permission: permissionProtocol,
327
+ sharedChannelHandler: serialDispatcherChannel,
240
328
  }));
329
+ const sharedDispatcherChannel = serialDispatcherChannel;
330
+ if (serialChannel) {
331
+ const channel = serialDispatcherChannel;
332
+ const checks = workers.map((context) => context.dispatcherCheck);
333
+ let serialScheduled = false;
334
+ let serialInFlight = false;
335
+ let serialRerun = false;
336
+ const runSerialChecks = () => {
337
+ if (serialInFlight) {
338
+ serialRerun = true;
339
+ return;
340
+ }
341
+ serialInFlight = true;
342
+ do {
343
+ serialRerun = false;
344
+ serialScheduled = false;
345
+ for (let index = 0; index < checks.length; index++) {
346
+ const check = checks[index];
347
+ if (check.isRunning !== true)
348
+ check.isRunning = true;
349
+ check();
350
+ }
351
+ } while (serialRerun);
352
+ serialInFlight = false;
353
+ };
354
+ const scheduleSerialCheck = () => {
355
+ if (serialInFlight) {
356
+ serialRerun = true;
357
+ return;
358
+ }
359
+ if (serialScheduled)
360
+ return;
361
+ serialScheduled = true;
362
+ Promise.resolve().then(runSerialChecks);
363
+ };
364
+ channel.open(runSerialChecks);
365
+ workers.forEach((context) => {
366
+ const wake = context.laneWake;
367
+ context.bindSend(() => {
368
+ scheduleSerialCheck();
369
+ wake();
370
+ });
371
+ });
372
+ hostDebug?.log(`dispatcher=serial-channel lanes=${checks.length}`);
373
+ }
374
+ else {
375
+ hostDebug?.log(`dispatcher=per-thread lanes=${workers.length}`);
376
+ }
241
377
  if (usingInliner) {
242
378
  const mainThread = createInlineExecutor({
243
379
  tasks: listOfFunctions,
@@ -271,7 +407,9 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
271
407
  return closePromise;
272
408
  closing = true;
273
409
  closePromise = Promise.allSettled(workers.map((context) => context.kills()))
274
- .then(() => undefined);
410
+ .then(() => {
411
+ sharedDispatcherChannel?.close();
412
+ });
275
413
  return closePromise;
276
414
  };
277
415
  const wrapGuardedInvoke = ({ invoke, taskName, }) => (args) => {
@@ -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;