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 +1 -1
- package/package.json +1 -1
- package/src/api.d.ts +26 -6
- package/src/api.js +74 -8
- 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/pool.d.ts +2 -5
- package/src/runtime/pool.js +11 -18
- package/src/runtime/process-worker.js +2 -1
- package/src/types.d.ts +33 -37
- 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 +7 -2
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
|
|
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
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,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
|
-
|
|
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 ((
|
|
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
|
-
|
|
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
|
|
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("../../
|
|
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 {
|
|
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
|
|
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") ??
|
|
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)
|
|
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 === "" ||
|
|
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
|
-
:
|
|
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")
|
|
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;
|
package/src/runtime/pool.d.ts
CHANGED
|
@@ -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,
|
|
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 & {
|
package/src/runtime/pool.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
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";
|
package/src/utils/http.d.ts
CHANGED
|
@@ -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]);
|
package/src/worker/loop.js
CHANGED
|
@@ -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 =
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
238
|
-
|
|
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 {
|
|
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: ({
|
|
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 = ({
|
|
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;
|
package/src/worker/timers.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
};
|