knitting 0.1.70 → 0.1.73
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 +101 -10
- package/knitting.browser.d.ts +3 -1
- package/knitting.browser.js +1 -1
- package/knitting.d.ts +3 -1
- package/knitting.js +2 -1
- package/package.json +8 -3
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/src/api.js +58 -34
- package/src/connections/node-addons.js +11 -1
- package/src/debug/gate.js +1 -1
- package/src/debug/handle.d.ts +6 -1
- package/src/debug/handle.js +14 -6
- package/src/error.d.ts +9 -0
- package/src/error.js +16 -2
- package/src/memory/lock.d.ts +48 -6
- package/src/memory/lock.js +350 -142
- package/src/memory/payloadCodec.js +31 -11
- package/src/permission/protocol.d.ts +1 -0
- package/src/permission/protocol.js +8 -3
- package/src/runtime/dispatcher.d.ts +6 -1
- package/src/runtime/dispatcher.js +24 -8
- package/src/runtime/inline-executor.js +2 -1
- package/src/runtime/pool.d.ts +13 -4
- package/src/runtime/pool.js +107 -47
- package/src/runtime/process-worker.js +10 -1
- package/src/runtime/tx-queue.d.ts +2 -1
- package/src/runtime/tx-queue.js +11 -0
- package/src/runtime/worker-common.d.ts +7 -0
- package/src/runtime/worker-common.js +28 -2
- package/src/types.d.ts +35 -11
- package/src/worker/loop.js +17 -4
- package/src/worker/safety/index.d.ts +1 -1
- package/src/worker/safety/index.js +1 -1
- package/src/worker/safety/process.d.ts +2 -0
- package/src/worker/safety/process.js +8 -1
- package/src/worker/safety/startup.js +11 -6
- package/src/worker/timers.js +25 -3
package/knitting.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { workerMainLoop } from "./src/worker/loop.js";
|
|
2
2
|
import { checkCompiledWorker, createPool, importTask, isMain, setModuleUrl, task } from "./src/api.js";
|
|
3
3
|
import { Envelope } from "./src/common/envelope.js";
|
|
4
|
+
import { KnittingError } from "./src/error.js";
|
|
4
5
|
import { isNumericArray, NumericArray } from "./src/connections/numeric-array.js";
|
|
5
|
-
export { checkCompiledWorker as checkCompiledWorker, createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, isNumericArray as isNumericArray, NumericArray as NumericArray, setModuleUrl as setModuleUrl, task as task, workerMainLoop as workerMainLoop, };
|
|
6
|
+
export { checkCompiledWorker as checkCompiledWorker, createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, isNumericArray as isNumericArray, KnittingError as KnittingError, NumericArray as NumericArray, setModuleUrl as setModuleUrl, task as task, workerMainLoop as workerMainLoop, };
|
|
6
7
|
export type { EnvelopeBody as EnvelopeBody, EnvelopeHeader as EnvelopeHeader, } from "./src/common/envelope.js";
|
|
8
|
+
export type { KnittingErrorCode } from "./src/error.js";
|
|
7
9
|
export type { CompiledWorkerCheck, CompiledWorkerOptions, CompiledWorkerSource, } from "./src/types.js";
|
package/knitting.js
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
import { workerMainLoop } from "./src/worker/loop.js";
|
|
3
3
|
import { checkCompiledWorker, createPool, importTask, isMain, setModuleUrl, task, } from "./src/api.js";
|
|
4
4
|
import { Envelope } from "./src/common/envelope.js";
|
|
5
|
+
import { KnittingError } from "./src/error.js";
|
|
5
6
|
import { isNumericArray, NumericArray, } from "./src/connections/numeric-array.js";
|
|
6
|
-
export { checkCompiledWorker as checkCompiledWorker, createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, isNumericArray as isNumericArray, NumericArray as NumericArray, setModuleUrl as setModuleUrl, task as task, workerMainLoop as workerMainLoop, };
|
|
7
|
+
export { checkCompiledWorker as checkCompiledWorker, createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, isNumericArray as isNumericArray, KnittingError as KnittingError, NumericArray as NumericArray, setModuleUrl as setModuleUrl, task as task, workerMainLoop as workerMainLoop, };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knitting",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.73",
|
|
4
4
|
"description": "Shared-memory IPC runtime for Node.js, Deno, and Bun.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"release:npm": "npm run test:all && npm publish",
|
|
84
84
|
"release:npm:dry-run": "npm publish --dry-run",
|
|
85
85
|
"test": "npm run test:deno",
|
|
86
|
-
"test:deno": "deno test -A --ignore=test/runtime.node.test.ts --ignore=test/runtime.process.test.ts",
|
|
86
|
+
"test:deno": "deno test -A --ignore=test/runtime.node.test.ts --ignore=test/runtime.process.test.ts test",
|
|
87
87
|
"test:node": "node --no-warnings --experimental-transform-types --test \"./test/*.test.ts\"",
|
|
88
88
|
"test:node:26": "node --no-warnings --experimental-ffi test/node26-ffi.integration.mjs",
|
|
89
89
|
"test:bun": "bun test",
|
|
@@ -102,7 +102,12 @@
|
|
|
102
102
|
"bun": ">=1.0.0"
|
|
103
103
|
},
|
|
104
104
|
"peerDependencies": {
|
|
105
|
-
"typescript": "
|
|
105
|
+
"typescript": ">=5.2.0"
|
|
106
|
+
},
|
|
107
|
+
"peerDependenciesMeta": {
|
|
108
|
+
"typescript": {
|
|
109
|
+
"optional": true
|
|
110
|
+
}
|
|
106
111
|
},
|
|
107
112
|
"devDependencies": {
|
|
108
113
|
"@types/bun": "latest",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/api.js
CHANGED
|
@@ -4,6 +4,7 @@ import { genTaskID, stableTaskID } from "./common/task-source.js";
|
|
|
4
4
|
import { toModuleUrl } from "./common/module-url.js";
|
|
5
5
|
import { endpointSymbol } from "./common/task-symbol.js";
|
|
6
6
|
import { createStealPoolBuffers, MAX_STEAL_CONSUMERS, spawnWorkerContext, } from "./runtime/pool.js";
|
|
7
|
+
import { isNodePermissionExecFlag } from "./runtime/worker-common.js";
|
|
7
8
|
import { ChannelHandler, hostDispatcherLoop } from "./runtime/dispatcher.js";
|
|
8
9
|
import { createDenoCompletionDoorbell } from "./runtime/deno-doorbell.js";
|
|
9
10
|
import { createSharedArrayBuffer, RUNTIME } from "./common/runtime.js";
|
|
@@ -11,17 +12,20 @@ import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from
|
|
|
11
12
|
import { classifyProcessPermissionCompatibility, enforceProcessPermissionCompatibility, resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
|
|
12
13
|
import { getNodeProcess } from "./common/node-compat.js";
|
|
13
14
|
import { readProcessSharedMemorySettings, readProcessWorkerNodeMajor, readProcessWorkerRuntime, } from "./runtime/process-worker.js";
|
|
15
|
+
import { assertStealClaim, DEFAULT_STEAL_CLAIM, } from "./memory/lock.js";
|
|
14
16
|
import { TRANSPORT_SIGNAL_BYTES } from "./ipc/transport/shared-memory.js";
|
|
15
17
|
import { inspectCompiledWorkerArtifact } from "./runtime/compiled-artifact.js";
|
|
16
18
|
import { managerMethod } from "./runtime/balancer.js";
|
|
17
19
|
import { createInlineExecutor } from "./runtime/inline-executor.js";
|
|
18
20
|
import { createHostArgAllocator } from "./runtime/host-arg-arena.js";
|
|
19
21
|
const hasDebugNamespace = (namespaces, namespace) => namespaces.has("*") || namespaces.has(namespace);
|
|
20
|
-
const createHostDebug = (namespaces) => {
|
|
22
|
+
const createHostDebug = (namespaces, epoch) => {
|
|
21
23
|
const enabled = (namespace) => hasDebugNamespace(namespaces, namespace);
|
|
22
24
|
if (!enabled("host"))
|
|
23
25
|
return undefined;
|
|
24
|
-
|
|
26
|
+
// Same rebase the workers do (see debug/handle.ts), so host and worker lines
|
|
27
|
+
// share one zero.
|
|
28
|
+
const base = epoch - performance.timeOrigin;
|
|
25
29
|
const tag = `host·${RUNTIME}`;
|
|
26
30
|
const log = (message) => {
|
|
27
31
|
const elapsed = (performance.now() - base).toFixed(1);
|
|
@@ -250,8 +254,14 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
250
254
|
(debug !== undefined && debug !== false);
|
|
251
255
|
let debugNamespaces;
|
|
252
256
|
const getDebugNamespaces = () => debugNamespaces ??= resolveDebugNamespaces(debug);
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
// Absolute (Unix-epoch ms) zero for every debug clock in this pool. Raw
|
|
258
|
+
// `performance.now()` is relative to each thread/process's own time origin;
|
|
259
|
+
// `timeOrigin + now()` is not, so workers can rebase onto this value.
|
|
260
|
+
const debugEpoch = debugRequested
|
|
261
|
+
? performance.timeOrigin + performance.now()
|
|
262
|
+
: undefined;
|
|
263
|
+
const hostDebug = debugEpoch !== undefined
|
|
264
|
+
? createHostDebug(getDebugNamespaces(), debugEpoch)
|
|
255
265
|
: undefined;
|
|
256
266
|
const debugEnabled = (namespace) => debugRequested && hasDebugNamespace(getDebugNamespaces(), namespace);
|
|
257
267
|
/**
|
|
@@ -278,6 +288,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
278
288
|
return {
|
|
279
289
|
shutdown: mainThreadOnlyProxy,
|
|
280
290
|
[Symbol.dispose]: () => { },
|
|
291
|
+
[Symbol.asyncDispose]: async () => { },
|
|
281
292
|
call: mainThreadOnlyProxy,
|
|
282
293
|
};
|
|
283
294
|
}
|
|
@@ -306,20 +317,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
306
317
|
const permissionExecArgv = toRuntimePermissionFlags(permissionProtocol);
|
|
307
318
|
const nodeProcess = getNodeProcess();
|
|
308
319
|
const allowedFlags = nodeProcess?.allowedNodeEnvironmentFlags ?? null;
|
|
309
|
-
const
|
|
310
|
-
const key = flag.split("=", 1)[0];
|
|
311
|
-
return key === "--permission" ||
|
|
312
|
-
key === "--experimental-permission" ||
|
|
313
|
-
key === "--allow-fs-read" ||
|
|
314
|
-
key === "--allow-fs-write" ||
|
|
315
|
-
key === "--allow-worker" ||
|
|
316
|
-
key === "--allow-child-process" ||
|
|
317
|
-
key === "--allow-net" ||
|
|
318
|
-
key === "--allow-addons" ||
|
|
319
|
-
key === "--allow-ffi" ||
|
|
320
|
-
key === "--allow-wasi";
|
|
321
|
-
};
|
|
322
|
-
const stripNodePermissionFlags = (flags) => flags?.filter((flag) => !isNodePermissionFlag(flag));
|
|
320
|
+
const stripNodePermissionFlags = (flags) => flags?.filter((flag) => !isNodePermissionExecFlag(flag));
|
|
323
321
|
const dedupeFlags = (flags) => {
|
|
324
322
|
const out = [];
|
|
325
323
|
const seen = new Set();
|
|
@@ -346,16 +344,11 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
346
344
|
? nodeProcess.execArgv
|
|
347
345
|
: undefined;
|
|
348
346
|
const defaultExecArgvCandidate = workerExecArgv ??
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
: inheritedExecArgv)
|
|
355
|
-
: undefined);
|
|
356
|
-
const defaultExecArgv = permissionProtocol?.unsafe === true
|
|
357
|
-
? stripNodePermissionFlags(defaultExecArgvCandidate)
|
|
358
|
-
: defaultExecArgvCandidate;
|
|
347
|
+
inheritedExecArgv;
|
|
348
|
+
// Knitting resolves the worker's permission policy itself. Do not inherit
|
|
349
|
+
// the host's Node grants (or caller-supplied permission flags), which could
|
|
350
|
+
// broaden that policy when Node combines repeated --allow-* options.
|
|
351
|
+
const defaultExecArgv = stripNodePermissionFlags(defaultExecArgvCandidate);
|
|
359
352
|
const combinedExecArgv = dedupeFlags([
|
|
360
353
|
...permissionExecArgv,
|
|
361
354
|
...(defaultExecArgv ?? []),
|
|
@@ -367,6 +360,16 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
367
360
|
hostDebug?.log(`permission=${permissionProtocol?.mode ?? "off"} execArgv=${formatDebugList(execArgv)}`);
|
|
368
361
|
const usesAbortSignal = listOfFunctions.some((fn) => fn.abortSignal !== undefined);
|
|
369
362
|
const resolvedWorker = resolveWorkerSettings(worker, callerHref);
|
|
363
|
+
if (RUNTIME === "node" &&
|
|
364
|
+
resolvedWorker?.runtime !== "process" &&
|
|
365
|
+
resolvedWorker?.runtime !== "compiled") {
|
|
366
|
+
const missingPermissionFlags = permissionExecArgv.filter((flag) => isNodePermissionExecFlag(flag) && !execArgv?.includes(flag));
|
|
367
|
+
if (missingPermissionFlags.length > 0) {
|
|
368
|
+
throw new Error("Node cannot apply the resolved permission policy to this thread " +
|
|
369
|
+
`worker (unsupported flags: ${missingPermissionFlags.join(", ")}). ` +
|
|
370
|
+
"Refusing to start the worker without those permissions.");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
370
373
|
const dispatcherEnv = nodeProcess?.env?.KNITTING_DISPATCHER;
|
|
371
374
|
const stealEnvRaw = nodeProcess?.env?.KNITTING_STEAL?.trim().toLowerCase();
|
|
372
375
|
const stealEnv = stealEnvRaw === "1" || stealEnvRaw === "true"
|
|
@@ -383,12 +386,15 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
383
386
|
const stealRequested = host?.steal ?? stealEnv ?? stealDefaultCompatible;
|
|
384
387
|
const stealClaimEnvRaw = nodeProcess?.env?.KNITTING_STEAL_CLAIM?.trim()
|
|
385
388
|
.toLowerCase();
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
389
|
+
// An unrecognised discipline is an error, not a fallback: `cas-mask` and
|
|
390
|
+
// typos used to select Dekker silently and run under the wrong name.
|
|
391
|
+
const stealClaimEnv = stealClaimEnvRaw === undefined || stealClaimEnvRaw === ""
|
|
392
|
+
? undefined
|
|
393
|
+
: assertStealClaim(stealClaimEnvRaw, "KNITTING_STEAL_CLAIM");
|
|
390
394
|
// Select the stealing claim discipline, preferring the explicit option.
|
|
391
|
-
const stealClaim = host?.stealClaim
|
|
395
|
+
const stealClaim = host?.stealClaim === undefined
|
|
396
|
+
? stealClaimEnv ?? DEFAULT_STEAL_CLAIM
|
|
397
|
+
: assertStealClaim(host.stealClaim, "host.stealClaim");
|
|
392
398
|
const usingCompiledWorker = resolvedWorker?.runtime === "compiled";
|
|
393
399
|
if (resolvedWorker?.compiled !== undefined && !usingCompiledWorker) {
|
|
394
400
|
throw new Error("worker.compiled requires worker.runtime to be compiled");
|
|
@@ -526,6 +532,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
526
532
|
at,
|
|
527
533
|
thread,
|
|
528
534
|
debug,
|
|
535
|
+
debugEpoch,
|
|
529
536
|
hostDebug: hostDebug?.log,
|
|
530
537
|
totalNumberOfThread,
|
|
531
538
|
// Worker count without the inline lane. The inliner runs on the host
|
|
@@ -534,6 +541,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
534
541
|
source,
|
|
535
542
|
workerOptions: resolvedWorker,
|
|
536
543
|
workerExecArgv: execArgv,
|
|
544
|
+
requestedExecArgv: workerExecArgv,
|
|
537
545
|
host,
|
|
538
546
|
payload,
|
|
539
547
|
sharedBytesEnabled,
|
|
@@ -564,14 +572,27 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
564
572
|
// Round-robin wake keeps stealing at one notify per publish; any idle worker
|
|
565
573
|
// can claim the region.
|
|
566
574
|
let wakeCursor = 0;
|
|
575
|
+
let lastWoken = 0;
|
|
567
576
|
const wakeOne = () => {
|
|
568
577
|
const lanes = workers.length;
|
|
569
578
|
if (lanes === 0)
|
|
570
579
|
return;
|
|
571
|
-
const lane = wakeCursor;
|
|
580
|
+
const lane = lastWoken = wakeCursor;
|
|
572
581
|
wakeCursor = wakeCursor + 1 < lanes ? wakeCursor + 1 : 0;
|
|
573
582
|
workers[lane].laneWake?.();
|
|
574
583
|
};
|
|
584
|
+
// Any awake worker will claim what was just published, so a lost wake
|
|
585
|
+
// only strands work when every worker is parked. Ring the lane this pass
|
|
586
|
+
// already rang: one still waking up absorbs it, and a second lane would
|
|
587
|
+
// be one more thread woken per call.
|
|
588
|
+
const wakeIfAllParked = () => {
|
|
589
|
+
for (let lane = 0; lane < workers.length; lane++) {
|
|
590
|
+
const context = workers[lane];
|
|
591
|
+
if (context.laneAwake?.() !== false)
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
workers[lastWoken]?.laneWake?.();
|
|
595
|
+
};
|
|
575
596
|
// The shared dispatcher drives one queue, so it needs signal words of its
|
|
576
597
|
// own rather than any single lane's; waking is delegated to `wakeOne`.
|
|
577
598
|
const signalWords = new Int32Array(createSharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT));
|
|
@@ -585,6 +606,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
585
606
|
channelHandler: channel,
|
|
586
607
|
dispatcherOptions: host,
|
|
587
608
|
notifySignal: wakeOne,
|
|
609
|
+
wakeAfterPass: wakeIfAllParked,
|
|
588
610
|
crossProcess: resolvedWorker?.runtime === "process",
|
|
589
611
|
nativeCompletionDoorbell: denoCompletionDoorbell !== undefined ||
|
|
590
612
|
workers.some((context) => context
|
|
@@ -896,6 +918,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
896
918
|
return {
|
|
897
919
|
shutdown: shutdownWithDelay,
|
|
898
920
|
[Symbol.dispose]: disposePool,
|
|
921
|
+
[Symbol.asyncDispose]: () => shutdownWithDelay(),
|
|
899
922
|
call: Object.fromEntries(callEntries),
|
|
900
923
|
// Only the shared submit queue has a single arena to build arguments in.
|
|
901
924
|
sharedArgBytes: createHostArgAllocator(sharedArgsEnabled ? stealBuffers?.submitBuffers.payload : undefined),
|
|
@@ -910,6 +933,7 @@ const createSingleTaskPool = (single, options) => {
|
|
|
910
933
|
call: pool.call[SINGLE_TASK_KEY],
|
|
911
934
|
shutdown: pool.shutdown,
|
|
912
935
|
[Symbol.dispose]: pool[Symbol.dispose],
|
|
936
|
+
[Symbol.asyncDispose]: pool[Symbol.asyncDispose],
|
|
913
937
|
};
|
|
914
938
|
};
|
|
915
939
|
const buildTaskDefinitionFromCaller = (input, callerHref, at, imported = false) => {
|
|
@@ -34,6 +34,14 @@ export const formatNodeNativeAddonLoadError = (name, platformInfo, errors) => {
|
|
|
34
34
|
? "an unknown version"
|
|
35
35
|
: `v${platformInfo.version}`;
|
|
36
36
|
const target = `${platformInfo.platform}-${platformInfo.arch}`;
|
|
37
|
+
if (errors.some((error) => error.includes("ERR_DLOPEN_DISABLED"))) {
|
|
38
|
+
return (`knitting: Node.js ${version} blocked loading native addon ${name} ` +
|
|
39
|
+
`because --allow-addons is not enabled. For a thread worker pool, ` +
|
|
40
|
+
`set permission.node.allowAddons to true; if the host itself runs ` +
|
|
41
|
+
`with --permission, start it with --allow-addons too. This permits ` +
|
|
42
|
+
`task code to load Node native addons; only enable it for trusted ` +
|
|
43
|
+
`tasks.${attempts}`);
|
|
44
|
+
}
|
|
37
45
|
if (abi === NODE_26_MODULE_ABI) {
|
|
38
46
|
return (`knitting: Node.js 26 uses node:ffi instead of ABI-specific addons. ` +
|
|
39
47
|
`Restart Node with the --experimental-ffi flag` +
|
|
@@ -85,7 +93,9 @@ export const loadNodeNativeAddon = (require, name, specifier) => {
|
|
|
85
93
|
return require(candidate);
|
|
86
94
|
}
|
|
87
95
|
catch (error) {
|
|
88
|
-
|
|
96
|
+
const code = error?.code;
|
|
97
|
+
errors.push(`${candidate}: ${typeof code === "string" ? `${code}: ` : ""}` +
|
|
98
|
+
String(error));
|
|
89
99
|
}
|
|
90
100
|
}
|
|
91
101
|
throw new Error(formatNodeNativeAddonLoadError(name, readNodePlatformInfo(), errors));
|
package/src/debug/gate.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Read once at module load from `KNITTING_DEBUG`. This module is deliberately
|
|
5
5
|
* tiny and dependency-light: importing it must never pull in the logger or the
|
|
6
6
|
* environment-diff machinery. Callers branch on {@link DEBUG_ENABLED} and only
|
|
7
|
-
* then `await import("./handle.
|
|
7
|
+
* then `await import("./handle.js")`, so when debug is off nothing else under
|
|
8
8
|
* `src/debug` is ever loaded — literally zero cost, not merely cheap.
|
|
9
9
|
*
|
|
10
10
|
* `KNITTING_DEBUG` is a comma-separated list of namespaces:
|
package/src/debug/handle.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ export type DebugInit = {
|
|
|
3
3
|
readonly name: string;
|
|
4
4
|
readonly runtime: string;
|
|
5
5
|
readonly namespaces: ReadonlySet<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Host debug epoch as `timeOrigin + now()`. When absent (debug enabled only
|
|
8
|
+
* inside the worker), the clock starts when this handle initialises.
|
|
9
|
+
*/
|
|
10
|
+
readonly epoch?: number;
|
|
6
11
|
};
|
|
7
12
|
export type Debug = {
|
|
8
13
|
/**
|
|
@@ -20,4 +25,4 @@ export type Debug = {
|
|
|
20
25
|
*/
|
|
21
26
|
envPhase: (label: string) => void;
|
|
22
27
|
};
|
|
23
|
-
export declare const initDebug: ({ name, runtime, namespaces }: DebugInit) => Debug;
|
|
28
|
+
export declare const initDebug: ({ name, runtime, namespaces, epoch }: DebugInit) => Debug;
|
package/src/debug/handle.js
CHANGED
|
@@ -4,16 +4,24 @@
|
|
|
4
4
|
* baseline snapshot it takes exists when debug is off.
|
|
5
5
|
*
|
|
6
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* line is tagged with the worker id, runtime, and a clock in milliseconds since
|
|
8
|
+
* the host's debug epoch, so host and worker lines interleave on one timeline.
|
|
9
|
+
*
|
|
10
|
+
* Raw `performance.now()` can't be compared across the thread/process boundary:
|
|
11
|
+
* bun and deno give each worker its own time origin, and a process worker's
|
|
12
|
+
* origin is its own process start. `performance.timeOrigin + performance.now()`
|
|
13
|
+
* is comparable (measured within a few µs on bun, node and deno, for both
|
|
14
|
+
* threads and processes), so the host sends that absolute value and each worker
|
|
15
|
+
* rebases onto it once. Staying in the local `now()` frame afterwards keeps
|
|
16
|
+
* full precision; the absolute sum alone only resolves ~0.24µs.
|
|
11
17
|
*/
|
|
12
18
|
import { describeGlobalKey, diffGlobals, snapshotGlobals, } from "./env-diff.js";
|
|
13
|
-
export const initDebug = ({ name, runtime, namespaces }) => {
|
|
19
|
+
export const initDebug = ({ name, runtime, namespaces, epoch }) => {
|
|
14
20
|
const all = namespaces.has("*");
|
|
15
21
|
const enabled = (namespace) => all || namespaces.has(namespace);
|
|
16
|
-
const base =
|
|
22
|
+
const base = epoch === undefined
|
|
23
|
+
? performance.now()
|
|
24
|
+
: epoch - performance.timeOrigin;
|
|
17
25
|
const tag = `${name}·${runtime}`;
|
|
18
26
|
const log = (namespace, message) => {
|
|
19
27
|
if (!enabled(namespace))
|
package/src/error.d.ts
CHANGED
|
@@ -6,6 +6,15 @@ export declare const ErrorKnitting: {
|
|
|
6
6
|
readonly Serializable: 3;
|
|
7
7
|
};
|
|
8
8
|
export type ErrorKnitting = typeof ErrorKnitting[keyof typeof ErrorKnitting];
|
|
9
|
+
export type KnittingErrorCode = "KNT_ERROR_0" | "KNT_ERROR_1" | "KNT_ERROR_2" | "KNT_ERROR_3" | "THREAD_CLOSED" | "WORKER_CRASHED" | "WORKER_EXITED" | "WORKER_STARTUP_FAILED";
|
|
10
|
+
/**
|
|
11
|
+
* Rejection raised by Knitting itself rather than by task code. Branch on
|
|
12
|
+
* `code`; `message` keeps the human-readable text.
|
|
13
|
+
*/
|
|
14
|
+
export declare class KnittingError extends Error {
|
|
15
|
+
readonly code: KnittingErrorCode;
|
|
16
|
+
constructor(code: KnittingErrorCode, message: string, cause?: unknown);
|
|
17
|
+
}
|
|
9
18
|
export declare const encoderError: ({ task, type, onPromise, detail, }: {
|
|
10
19
|
task: Task;
|
|
11
20
|
type: ErrorKnitting;
|
package/src/error.js
CHANGED
|
@@ -8,6 +8,18 @@ export const ErrorKnitting = {
|
|
|
8
8
|
Json: 2,
|
|
9
9
|
Serializable: 3,
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Rejection raised by Knitting itself rather than by task code. Branch on
|
|
13
|
+
* `code`; `message` keeps the human-readable text.
|
|
14
|
+
*/
|
|
15
|
+
export class KnittingError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code, message, cause) {
|
|
18
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
19
|
+
this.name = "KnittingError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
11
23
|
const reasonFrom = (task, type, detail) => {
|
|
12
24
|
switch (type) {
|
|
13
25
|
case ErrorKnitting.Function: {
|
|
@@ -41,10 +53,12 @@ export const encoderError = ({ task, type, onPromise, detail, }) => {
|
|
|
41
53
|
}
|
|
42
54
|
if (!beginPromisePayload(task))
|
|
43
55
|
return false;
|
|
56
|
+
// Built here, not in the microtask, so the stack still names the caller.
|
|
57
|
+
const error = new KnittingError(`KNT_ERROR_${type}`, reason);
|
|
44
58
|
queueMicrotask(() => {
|
|
45
59
|
finishPromisePayload(task);
|
|
46
|
-
task.value =
|
|
47
|
-
onPromise(task, true,
|
|
60
|
+
task.value = error;
|
|
61
|
+
onPromise(task, true, error);
|
|
48
62
|
});
|
|
49
63
|
return false;
|
|
50
64
|
};
|
package/src/memory/lock.d.ts
CHANGED
|
@@ -181,8 +181,26 @@ export declare const STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32: number;
|
|
|
181
181
|
export declare const STEAL_LIVE_SLOT_OFFSET_U32: number;
|
|
182
182
|
/** Host-owned arm word for the return-lock completion doorbell. */
|
|
183
183
|
export declare const DOORBELL_ARMED_SLOT_OFFSET_U32: number;
|
|
184
|
-
/**
|
|
185
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Non-wrapping 64-bit claim head for the `ticket` discipline. The producer
|
|
186
|
+
* publishes only the low 32 bits of the tail. A successful head CAS validates
|
|
187
|
+
* the snapshot: with that head unchanged, at most 32 tickets can be pending,
|
|
188
|
+
* so unsigned subtraction recovers the exact distance even across tail wrap.
|
|
189
|
+
* Head and tail occupy different slots' header lines (slots 0 and 1).
|
|
190
|
+
*/
|
|
191
|
+
export declare const STEAL_TICKET_HEAD_SLOT_OFFSET_U32: number;
|
|
192
|
+
/** Negative head permanently closes a failed ticket queue. */
|
|
193
|
+
export declare const STEAL_TICKET_FAILED = -1n;
|
|
194
|
+
/**
|
|
195
|
+
* Publication-order ring. `order[t & 31]` is the slot the producer filled for
|
|
196
|
+
* ticket `t`, so a ticket names a lane without the ticket having to *be* the
|
|
197
|
+
* lane: the producer keeps its free-bit allocation and claims follow publication
|
|
198
|
+
* order. Cell `i` uses the former ACK word; words 14..15
|
|
199
|
+
* are reserved for the head in slot 0 and the tail in slot 1.
|
|
200
|
+
*/
|
|
201
|
+
export declare const STEAL_TICKET_ORDER_SLOT_OFFSET_U32: number;
|
|
202
|
+
/** Ring cells, and therefore the wrap mask, follow the slot count. */
|
|
203
|
+
export declare const STEAL_TICKET_RING_MASK: number;
|
|
186
204
|
export declare const HEADER_U32_LENGTH: number;
|
|
187
205
|
export declare const HEADER_BYTE_LENGTH: number;
|
|
188
206
|
export declare const makeTask: () => Task;
|
|
@@ -194,18 +212,37 @@ type ResolveHostOptions = {
|
|
|
194
212
|
};
|
|
195
213
|
export type Lock2 = ReturnType<typeof lock2>;
|
|
196
214
|
/**
|
|
197
|
-
*
|
|
215
|
+
* Claim discipline for stealing consumers.
|
|
198
216
|
*
|
|
199
217
|
* - `dekker`: per-consumer intent words and an O(N) peer survey.
|
|
200
|
-
* - `
|
|
218
|
+
* - `ticket`: one monotonic counter; a CAS on the head claims lanes in
|
|
219
|
+
* publication order, with no regions and no per-consumer state.
|
|
201
220
|
*/
|
|
202
|
-
export
|
|
221
|
+
export declare const STEAL_CLAIM_DISCIPLINES: readonly ["dekker", "ticket"];
|
|
222
|
+
export type StealClaimDiscipline = typeof STEAL_CLAIM_DISCIPLINES[number];
|
|
223
|
+
/**
|
|
224
|
+
* Validate a claim discipline coming from an untrusted source — an env var, or
|
|
225
|
+
* a JS caller the type system never saw.
|
|
226
|
+
*
|
|
227
|
+
* This throws rather than falling back, because a silent fallback is worse than
|
|
228
|
+
* a crash here: a stale `cas-mask` setting or a typo would quietly run Dekker,
|
|
229
|
+
* and every benchmark and test built on it would report Dekker's numbers under
|
|
230
|
+
* another name. Both sides of a lock must also agree, so an unrecognised value
|
|
231
|
+
* that resolved differently on host and worker would corrupt the protocol.
|
|
232
|
+
*/
|
|
233
|
+
export declare const assertStealClaim: (value: unknown, source: string) => StealClaimDiscipline;
|
|
234
|
+
/**
|
|
235
|
+
* The discipline chosen when nothing selects one. Host and worker default
|
|
236
|
+
* independently, so this must be a single shared constant: two sides landing on
|
|
237
|
+
* different disciplines would read the same buffer under different protocols.
|
|
238
|
+
*/
|
|
239
|
+
export declare const DEFAULT_STEAL_CLAIM: StealClaimDiscipline;
|
|
203
240
|
export type WaitAsyncState = "not-equal" | "ok" | "timed-out";
|
|
204
241
|
export type WaitAsyncResult = {
|
|
205
242
|
async: boolean;
|
|
206
243
|
value: WaitAsyncState | PromiseLike<WaitAsyncState>;
|
|
207
244
|
};
|
|
208
|
-
export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, sharedReturn, moveReturn, consumers, consumerId, regionLanes, stealClaim, notifyOnHostPublish, notifyHostPublish, }: {
|
|
245
|
+
export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, sharedReturn, moveReturn, consumers, consumerId, regionLanes, stealClaim, notifyOnHostPublish, notifyHostPublish, traceClaim, }: {
|
|
209
246
|
headers?: SharedBufferSource;
|
|
210
247
|
headerSlotStrideU32?: number;
|
|
211
248
|
LockBoundSector?: SharedBufferSource;
|
|
@@ -244,6 +281,11 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
|
|
|
244
281
|
notifyOnHostPublish?: boolean;
|
|
245
282
|
/** Runtime-native host wake used when Atomics.waitAsync cannot wake it. */
|
|
246
283
|
notifyHostPublish?: () => void;
|
|
284
|
+
/**
|
|
285
|
+
* Debug sink for steal claims. Only consulted when `decode` is chosen, so an
|
|
286
|
+
* untraced lock returns the bare claim function; see {@link traceStealClaims}.
|
|
287
|
+
*/
|
|
288
|
+
traceClaim?: (message: string) => void;
|
|
247
289
|
}) => {
|
|
248
290
|
enlist: (task: Task) => true;
|
|
249
291
|
encode: (task: Task, state?: number) => boolean;
|