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 +36 -25
- package/package.json +1 -1
- package/src/api.d.ts +26 -6
- package/src/api.js +147 -9
- package/src/connections/buffer-reference.d.ts +4 -2
- package/src/connections/buffer-reference.js +4 -2
- package/src/connections/process-shared-buffer.d.ts +6 -0
- package/src/connections/process-shared-buffer.js +6 -0
- package/src/debug/env-diff.d.ts +26 -0
- package/src/debug/env-diff.js +49 -0
- package/src/debug/gate.d.ts +18 -0
- package/src/debug/gate.js +69 -0
- package/src/debug/handle.d.ts +23 -0
- package/src/debug/handle.js +48 -0
- package/src/ipc/transport/shared-memory.d.ts +1 -3
- package/src/memory/payload-config.d.ts +9 -0
- package/src/permission/protocol.d.ts +1 -1
- package/src/permission/protocol.js +30 -20
- package/src/runtime/dispatcher.d.ts +6 -0
- package/src/runtime/pool.d.ts +11 -5
- package/src/runtime/pool.js +50 -44
- package/src/runtime/process-worker.js +2 -1
- package/src/runtime/tx-queue.js +5 -1
- package/src/types.d.ts +45 -38
- package/src/utils/http.d.ts +8 -0
- package/src/utils/http.js +7 -0
- package/src/worker/loop.js +53 -11
- package/src/worker/safety/startup.d.ts +2 -3
- package/src/worker/safety/startup.js +1 -4
- package/src/worker/timers.js +8 -3
package/README.md
CHANGED
|
@@ -11,18 +11,22 @@
|
|
|
11
11
|
[](https://deno.com/)
|
|
12
12
|
[](https://bun.sh/)
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
`
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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,
|
|
42
|
-
|
|
43
|
-
|
|
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:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
-
|
|
56
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
| `
|
|
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
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
|
-
*
|
|
33
|
-
*
|
|
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
|
|
61
|
+
* Define a task whose worker-side code is imported only by workers.
|
|
43
62
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
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 ((
|
|
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
|
-
|
|
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
|
|
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(() =>
|
|
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;
|