@retaw106/mmc-oc 2.3.0 → 2.5.0
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/dist/cli.js +12 -1
- package/dist/config/schema/magic-context.d.ts +33 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config-init.js +12 -1
- package/dist/{context-wt7g4f8k.js → context-xx09sh1g.js} +1985 -603
- package/dist/features/magic-context/memory-maintainer/launcher.d.ts +13 -4
- package/dist/features/magic-context/memory-maintainer/launcher.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/manual-commands.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/model-chain.d.ts +47 -0
- package/dist/features/magic-context/memory-maintainer/model-chain.d.ts.map +1 -0
- package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts +17 -4
- package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/pipeline.d.ts +6 -0
- package/dist/features/magic-context/memory-maintainer/pipeline.d.ts.map +1 -1
- package/dist/features/magic-context/message-index-async.d.ts +6 -2
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +5 -8
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +5 -1
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +2 -0
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-host-instruction-retention.d.ts +4 -0
- package/dist/features/magic-context/storage-host-instruction-retention.d.ts.map +1 -0
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts +2 -2
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts +9 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +4 -0
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +57 -2
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts +34 -0
- package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts.map +1 -0
- package/dist/hooks/magic-context/apply-operations.d.ts +3 -1
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-hint.d.ts +3 -0
- package/dist/hooks/magic-context/auto-search-hint.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -2
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +2 -2
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts +4 -0
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger-error.d.ts +11 -0
- package/dist/hooks/magic-context/compartment-trigger-error.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +1 -0
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
- package/dist/hooks/magic-context/degraded-pass-refusal.d.ts +2 -0
- package/dist/hooks/magic-context/degraded-pass-refusal.d.ts.map +1 -1
- package/dist/hooks/magic-context/diagnostic-log-state.d.ts +4 -0
- package/dist/hooks/magic-context/diagnostic-log-state.d.ts.map +1 -0
- package/dist/hooks/magic-context/edit-marker.d.ts +2 -0
- package/dist/hooks/magic-context/edit-marker.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
- package/dist/hooks/magic-context/file-token-coverage.d.ts +11 -0
- package/dist/hooks/magic-context/file-token-coverage.d.ts.map +1 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/host-served-rows.d.ts +10 -0
- package/dist/hooks/magic-context/host-served-rows.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/nudge-evaluation-log.d.ts +3 -0
- package/dist/hooks/magic-context/nudge-evaluation-log.d.ts.map +1 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts +2 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
- package/dist/hooks/magic-context/pending-drop-eligibility.d.ts +14 -0
- package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -0
- package/dist/hooks/magic-context/pending-materialization.d.ts +2 -0
- package/dist/hooks/magic-context/pending-materialization.d.ts.map +1 -1
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts +2 -0
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/provider-visible-parts.d.ts +10 -4
- package/dist/hooks/magic-context/provider-visible-parts.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +3 -3
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/render-options.d.ts +19 -0
- package/dist/hooks/magic-context/render-options.d.ts.map +1 -0
- package/dist/hooks/magic-context/supersession-reclaim.d.ts +5 -3
- package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-date-refresh.d.ts +3 -2
- package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts +4 -0
- package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts.map +1 -0
- package/dist/hooks/magic-context/tag-content-primitives.d.ts +1 -1
- package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +15 -2
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts +18 -0
- package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts.map +1 -0
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +8 -4
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/text-complete.d.ts +1 -1
- package/dist/hooks/magic-context/text-complete.d.ts.map +1 -1
- package/dist/hooks/magic-context/todo-view.d.ts +2 -2
- package/dist/hooks/magic-context/tool-drop-target.d.ts +3 -2
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-reclaim.d.ts +5 -2
- package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +1 -0
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +8 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-stage-logger.d.ts +3 -0
- package/dist/hooks/magic-context/transform-stage-logger.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +4 -9
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/unresolved-history-boundary.d.ts +5 -1
- package/dist/hooks/magic-context/unresolved-history-boundary.d.ts.map +1 -1
- package/dist/{index-kw21z5pe.js → index-0d40b4q8.js} +17119 -16569
- package/dist/{index-z2w1eq3y.js → index-1vxffj5j.js} +558 -211
- package/dist/{index-z2g20msd.js → index-azbmnekv.js} +4 -4
- package/dist/{index-18yrysyj.js → index-bpjjcdy9.js} +1 -1
- package/dist/{index-a5mw0xp3.js → index-k25thxq7.js} +1 -1
- package/dist/{index-3gj1ak4c.js → index-mjf0p7yq.js} +1 -1
- package/dist/{index-czba8tw3.js → index-pte3cfs9.js} +226 -14
- package/dist/index.js +2 -2
- package/dist/{message-time-backfill-sykevdax.js → message-time-backfill-g9py24yt.js} +1 -1
- package/dist/model-resolution-cvyrrcjw.js +8 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/subagent-tool-policy.d.ts +4 -0
- package/dist/plugin/subagent-tool-policy.d.ts.map +1 -1
- package/dist/{read-session-chunk-863d6c06.js → read-session-chunk-eb3fy45y.js} +3 -3
- package/dist/{recomp-orchestrator-wrjy3qym.js → recomp-orchestrator-mmpy5cb7.js} +6 -6
- package/dist/{send-session-notification-cz6ev088.js → send-session-notification-zpqs213d.js} +3 -3
- package/dist/shared/model-suggestion-retry.d.ts +23 -0
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/prompt-surface-runtime.d.ts +5 -0
- package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
- package/dist/shared/reclaim-distance.d.ts +33 -0
- package/dist/shared/reclaim-distance.d.ts.map +1 -0
- package/dist/shared/rpc-discovery-cleanup.d.ts +3 -0
- package/dist/shared/rpc-discovery-cleanup.d.ts.map +1 -0
- package/dist/shared/rpc-server.d.ts +2 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/search-text.d.ts +8 -0
- package/dist/shared/search-text.d.ts.map +1 -0
- package/dist/shared/sqlite.d.ts +8 -1
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/stable-json.d.ts +7 -0
- package/dist/shared/stable-json.d.ts.map +1 -1
- package/dist/shared/storage-file-mutex.d.ts.map +1 -1
- package/dist/shared/system-directive.d.ts +2 -0
- package/dist/shared/system-directive.d.ts.map +1 -1
- package/dist/shared/tail-hygiene-json.d.ts +22 -0
- package/dist/shared/tail-hygiene-json.d.ts.map +1 -0
- package/dist/shared/user-facing-codes.d.ts +2 -2
- package/dist/v2/fold/native-replay.d.ts +11 -2
- package/dist/v2/fold/native-replay.d.ts.map +1 -1
- package/dist/v2/fold/policy.d.ts.map +1 -1
- package/dist/v2/fold/restore.d.ts.map +1 -1
- package/dist/v2/hidden-completion.d.ts +20 -1
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/generate.d.ts +36 -1
- package/dist/v2/hooks/generate.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +22 -1
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/lkg-system.d.ts +35 -4
- package/dist/v2/hooks/lkg-system.d.ts.map +1 -1
- package/dist/v2/hooks/payload.d.ts +10 -1
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/hooks/store.d.ts +2 -0
- package/dist/v2/hooks/store.d.ts.map +1 -1
- package/dist/v2/hooks/types.d.ts +12 -0
- package/dist/v2/hooks/types.d.ts.map +1 -1
- package/dist/v2/server.js +16272 -13742
- package/dist/v2/store-reader.d.ts +38 -1
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/web/main.js +262 -17
- package/dist/{wrapup-orchestrator-bmybc5p5.js → wrapup-orchestrator-jq589pgr.js} +6 -6
- package/package.json +1 -1
- package/src/config/latch-permanence-guard.test.ts +8 -0
- package/src/config/project-security.test.ts +17 -0
- package/src/config/schema/magic-context.test.ts +50 -0
- package/src/config/schema/magic-context.ts +34 -1
- package/src/shared/model-suggestion-retry.test.ts +52 -0
- package/src/shared/model-suggestion-retry.ts +13 -4
- package/src/shared/prompt-surface-runtime.ts +12 -0
- package/src/shared/reclaim-distance.test.ts +111 -0
- package/src/shared/reclaim-distance.ts +91 -0
- package/src/shared/root-async-context.probe.mjs +36 -0
- package/src/shared/root-async-context.test.ts +49 -0
- package/src/shared/rpc-discovery-cleanup.test.ts +125 -0
- package/src/shared/rpc-discovery-cleanup.ts +57 -0
- package/src/shared/rpc-server.ts +36 -13
- package/src/shared/search-text.test.ts +30 -0
- package/src/shared/search-text.ts +62 -0
- package/src/shared/sqlite-busy-budget-review.test.ts +291 -0
- package/src/shared/sqlite-foreground-wait.test.ts +237 -1
- package/src/shared/sqlite-turn-deadline.test.ts +286 -0
- package/src/shared/sqlite.ts +292 -23
- package/src/shared/stable-json.ts +56 -14
- package/src/shared/storage-file-mutex.test.ts +51 -2
- package/src/shared/storage-file-mutex.ts +11 -1
- package/src/shared/system-directive.test.ts +37 -0
- package/src/shared/system-directive.ts +19 -1
- package/src/shared/tag-transcript.ts +16 -4
- package/src/shared/tail-hygiene-json.test.ts +117 -0
- package/src/shared/tail-hygiene-json.ts +217 -0
- package/src/shared/user-facing-codes.ts +2 -2
- package/dist/model-resolution-wazv26n0.js +0 -6
package/src/shared/sqlite.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* `run()` → {changes,lastInsertRowid}) is identical and was verified directly.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
42
|
+
import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
|
|
43
43
|
import { statSync } from "node:fs";
|
|
44
44
|
// Type import only — runtime is loaded dynamically below. @types/better-sqlite3
|
|
45
45
|
// has the richest definitions and is a structural superset of the API surface
|
|
@@ -47,6 +47,16 @@ import { statSync } from "node:fs";
|
|
|
47
47
|
// node:sqlite at runtime (both expose prepare/run/get/all/exec/close).
|
|
48
48
|
import type BetterSqlite3 from "better-sqlite3";
|
|
49
49
|
|
|
50
|
+
// Capture the root before creating request-local AsyncLocalStorage scopes.
|
|
51
|
+
// Shared workers and timers must not inherit callbacks that reject stale
|
|
52
|
+
// database writes: those callbacks can retain an obsolete request's inputs.
|
|
53
|
+
const resourceRoot = new AsyncResource("magic-context:root");
|
|
54
|
+
|
|
55
|
+
/** Create host-owned resources without inheriting request-local state. */
|
|
56
|
+
export function inRootAsyncContext<T>(operation: () => T): T {
|
|
57
|
+
return resourceRoot.runInAsyncScope(operation);
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
/**
|
|
51
61
|
* Slow-write reporting is injected rather than imported: this module is also
|
|
52
62
|
* executed directly by Node (the `node:sqlite` smoke in CI, Pi, Desktop), and
|
|
@@ -383,11 +393,187 @@ export function getTagIdentityWriteGeneration(): number {
|
|
|
383
393
|
return tagIdentityWriteGeneration;
|
|
384
394
|
}
|
|
385
395
|
|
|
386
|
-
/**
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
396
|
+
/** Only Pi serving connections need a mirror of their native busy handler. */
|
|
397
|
+
const busyTimeoutStates = new WeakMap<Database, { current: number }>();
|
|
398
|
+
const BUSY_TIMEOUT_QUERY =
|
|
399
|
+
/^\s*PRAGMA\s+(?:(?:\w+|"[^"]+"|`[^`]+`|\[[^\]]+\])\s*\.\s*)?(?:busy_timeout|"busy_timeout"|`busy_timeout`|\[busy_timeout\])\s*;?\s*$/i;
|
|
400
|
+
const BUSY_TIMEOUT_SETTER =
|
|
401
|
+
/^\s*PRAGMA\s+(?:(?:\w+|"[^"]+"|`[^`]+`|\[[^\]]+\])\s*\.\s*)?(?:busy_timeout|"busy_timeout"|`busy_timeout`|\[busy_timeout\])\s*(?==|\()/i;
|
|
402
|
+
|
|
403
|
+
function literalBusyTimeout(sql: string): number | undefined {
|
|
404
|
+
const head = BUSY_TIMEOUT_SETTER.exec(sql);
|
|
405
|
+
if (!head) return undefined;
|
|
406
|
+
const match = /^(?:=\s*|\(\s*)(?:'(-?\d+)'|"(-?\d+)"|(-?\d+))\s*\)?\s*;?\s*$/.exec(
|
|
407
|
+
sql.slice(head[0].length),
|
|
408
|
+
);
|
|
409
|
+
if (!match) return undefined;
|
|
410
|
+
const value = Number(match[1] ?? match[2] ?? match[3]);
|
|
411
|
+
return value < 0 ? 0 : value <= 0x7fffffff ? value : undefined;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Split timeout-setting scripts without mistaking quoted text or comments for pragmas. */
|
|
415
|
+
function busyTimeoutStatements(sql: string): string[] {
|
|
416
|
+
const statements: string[] = [];
|
|
417
|
+
let statement = "";
|
|
418
|
+
let quote = "";
|
|
419
|
+
for (let i = 0; i < sql.length; i++) {
|
|
420
|
+
const char = sql[i];
|
|
421
|
+
if (quote) {
|
|
422
|
+
statement += char;
|
|
423
|
+
if (char === quote) {
|
|
424
|
+
if (sql[i + 1] === quote) statement += sql[++i];
|
|
425
|
+
else quote = "";
|
|
426
|
+
}
|
|
427
|
+
} else if (char === "'" || char === '"' || char === "`" || char === "[") {
|
|
428
|
+
quote = char === "[" ? "]" : char;
|
|
429
|
+
statement += char;
|
|
430
|
+
} else if (char === "-" && sql[i + 1] === "-") {
|
|
431
|
+
while (i + 1 < sql.length && sql[i + 1] !== "\n") i++;
|
|
432
|
+
statement += " ";
|
|
433
|
+
} else if (char === "/" && sql[i + 1] === "*") {
|
|
434
|
+
i += 2;
|
|
435
|
+
while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/")) i++;
|
|
436
|
+
i++;
|
|
437
|
+
statement += " ";
|
|
438
|
+
} else if (char === ";") {
|
|
439
|
+
statements.push(statement);
|
|
440
|
+
statement = "";
|
|
441
|
+
} else statement += char;
|
|
442
|
+
}
|
|
443
|
+
statements.push(statement);
|
|
444
|
+
return statements.filter((part) => part.trim().length > 0);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Install the native deadline layer only on a connection that will serve Pi turns. */
|
|
448
|
+
function installNativeDeadlineRouting(db: BetterSqlite3.Database): void {
|
|
449
|
+
const rawExec = db.exec.bind(db);
|
|
450
|
+
const rawPrepare = db.prepare.bind(db);
|
|
451
|
+
const readTimeout = (): number => {
|
|
452
|
+
const statement = rawPrepare("PRAGMA busy_timeout");
|
|
453
|
+
try {
|
|
454
|
+
return Object.values(statement.get() as Record<string, number>)[0];
|
|
455
|
+
} finally {
|
|
456
|
+
(statement as Partial<FinalizableStatement>).finalize?.();
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const timeoutState = { current: readTimeout() };
|
|
460
|
+
busyTimeoutStates.set(db, timeoutState);
|
|
461
|
+
const setBusyTimeout = (timeout: number): void => {
|
|
462
|
+
rawExec(`PRAGMA busy_timeout=${timeout}`);
|
|
463
|
+
timeoutState.current = timeout;
|
|
464
|
+
};
|
|
465
|
+
const withinDeadline = <T>(operation: () => T): T => {
|
|
466
|
+
const deadline = nativeWaitScope?.getStore();
|
|
467
|
+
if (!deadline?.active || !deadline.remainingMs) return operation();
|
|
468
|
+
const remaining = deadline.remainingMs();
|
|
469
|
+
if (remaining === Infinity) return operation();
|
|
470
|
+
const previous = timeoutState.current;
|
|
471
|
+
const timeout = Math.min(previous, Math.max(0, Math.floor(remaining)));
|
|
472
|
+
if (timeout === previous) return operation();
|
|
473
|
+
setBusyTimeout(timeout);
|
|
474
|
+
try {
|
|
475
|
+
return operation();
|
|
476
|
+
} finally {
|
|
477
|
+
setBusyTimeout(previous);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
Object.defineProperty(db, "exec", {
|
|
481
|
+
configurable: true,
|
|
482
|
+
writable: true,
|
|
483
|
+
value: (sql: string) => {
|
|
484
|
+
const parts = /\bbusy_timeout\b/i.test(sql) ? busyTimeoutStatements(sql) : [];
|
|
485
|
+
if (parts.length === 1 && BUSY_TIMEOUT_QUERY.test(parts[0])) return rawExec(sql);
|
|
486
|
+
const singleSetter = parts.length === 1 && BUSY_TIMEOUT_SETTER.test(parts[0]);
|
|
487
|
+
const setting = singleSetter ? literalBusyTimeout(parts[0]) : undefined;
|
|
488
|
+
if (singleSetter) {
|
|
489
|
+
const result = rawExec(sql);
|
|
490
|
+
timeoutState.current = setting ?? readTimeout();
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
if (!/\bbusy_timeout\b/i.test(sql)) return withinDeadline(() => rawExec(sql));
|
|
494
|
+
const setters = parts.filter((part) => BUSY_TIMEOUT_SETTER.test(part));
|
|
495
|
+
return withinDeadline(() => {
|
|
496
|
+
try {
|
|
497
|
+
const result = rawExec(sql);
|
|
498
|
+
if (setters.length)
|
|
499
|
+
timeoutState.current =
|
|
500
|
+
literalBusyTimeout(setters[setters.length - 1]) ?? readTimeout();
|
|
501
|
+
return result;
|
|
502
|
+
} catch (error) {
|
|
503
|
+
// exec can apply a timeout-setting prefix before a later statement
|
|
504
|
+
// fails. Only an ambiguous setting needs a native readback.
|
|
505
|
+
if (setters.length) timeoutState.current = readTimeout();
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
// Timeout introspection cannot wait on a database lock. Its getter must
|
|
512
|
+
// report the configured timeout, not a temporary cap that a caller might save.
|
|
513
|
+
const nativePrepare = (sql: string) =>
|
|
514
|
+
BUSY_TIMEOUT_QUERY.test(sql) || BUSY_TIMEOUT_SETTER.test(sql)
|
|
515
|
+
? rawPrepare(sql)
|
|
516
|
+
: withinDeadline(() => rawPrepare(sql));
|
|
517
|
+
Object.defineProperty(db, "prepare", {
|
|
518
|
+
configurable: true,
|
|
519
|
+
writable: true,
|
|
520
|
+
value: (sql: string) => {
|
|
521
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun adds values() to the common statement interface.
|
|
522
|
+
const statement: any = nativePrepare(sql);
|
|
523
|
+
const normalized = /\bbusy_timeout\b/i.test(sql)
|
|
524
|
+
? (busyTimeoutStatements(sql)[0] ?? sql)
|
|
525
|
+
: sql;
|
|
526
|
+
const setting = literalBusyTimeout(normalized);
|
|
527
|
+
const timeoutSetter = BUSY_TIMEOUT_SETTER.test(normalized);
|
|
528
|
+
const timeoutQuery = BUSY_TIMEOUT_QUERY.test(normalized);
|
|
529
|
+
// SQLite applies this PRAGMA while preparing it, before get/run.
|
|
530
|
+
if (timeoutSetter) timeoutState.current = setting ?? readTimeout();
|
|
531
|
+
for (const method of ["run", "get", "all", "values"] as const) {
|
|
532
|
+
const execute = statement[method]?.bind(statement);
|
|
533
|
+
if (!execute) continue;
|
|
534
|
+
Object.defineProperty(statement, method, {
|
|
535
|
+
configurable: true,
|
|
536
|
+
// biome-ignore lint/suspicious/noExplicitAny: retain native binding overloads.
|
|
537
|
+
value: (...args: any[]) => {
|
|
538
|
+
if (timeoutQuery) return execute(...args);
|
|
539
|
+
if (!timeoutSetter) return withinDeadline(() => execute(...args));
|
|
540
|
+
const result = execute(...args);
|
|
541
|
+
timeoutState.current = readTimeout();
|
|
542
|
+
return result;
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
if (typeof statement.iterate === "function") {
|
|
547
|
+
const iterate = statement.iterate.bind(statement);
|
|
548
|
+
Object.defineProperty(statement, "iterate", {
|
|
549
|
+
configurable: true,
|
|
550
|
+
// biome-ignore lint/suspicious/noExplicitAny: retain native binding overloads.
|
|
551
|
+
value: (...args: any[]) => {
|
|
552
|
+
const iterator =
|
|
553
|
+
!timeoutSetter && !timeoutQuery
|
|
554
|
+
? withinDeadline(() => iterate(...args))
|
|
555
|
+
: iterate(...args);
|
|
556
|
+
const next = iterator.next.bind(iterator);
|
|
557
|
+
if (timeoutSetter) timeoutState.current = readTimeout();
|
|
558
|
+
iterator.next = () => {
|
|
559
|
+
if (timeoutQuery) return next();
|
|
560
|
+
if (!timeoutSetter) return withinDeadline(next);
|
|
561
|
+
const result = next();
|
|
562
|
+
timeoutState.current = readTimeout();
|
|
563
|
+
return result;
|
|
564
|
+
};
|
|
565
|
+
return iterator;
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
return statement;
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Route native Bun and Node transactions through the same acquisition retry.
|
|
575
|
+
* Native Bun wrappers execute BEGIN internally, so both runtimes use this wrapper.
|
|
576
|
+
* Deadline routing, when selected, is installed underneath it at connection open. */
|
|
391
577
|
function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean): void {
|
|
392
578
|
const nativeExec = db.exec.bind(db);
|
|
393
579
|
Object.defineProperty(db, "exec", {
|
|
@@ -419,7 +605,9 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
|
|
|
419
605
|
!/^\s*(?:SELECT|EXPLAIN|PRAGMA|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i.test(sql)
|
|
420
606
|
) {
|
|
421
607
|
try {
|
|
422
|
-
|
|
608
|
+
// exec can commit a script prefix before BUSY. Run it once;
|
|
609
|
+
// only prepared statements are safe to probe and retry.
|
|
610
|
+
return withAutocommitTimeout(db, () => nativeExec(sql), false);
|
|
423
611
|
} finally {
|
|
424
612
|
if (mayChangeTagIdentity(sql)) tagIdentityWriteGeneration += 1;
|
|
425
613
|
}
|
|
@@ -543,6 +731,7 @@ function trackSqliteConnection(
|
|
|
543
731
|
db: BetterSqlite3.Database,
|
|
544
732
|
filename: unknown,
|
|
545
733
|
options: unknown,
|
|
734
|
+
turnDeadlines = false,
|
|
546
735
|
): BetterSqlite3.Database {
|
|
547
736
|
const originalClose = db.close.bind(db) as (...args: unknown[]) => unknown;
|
|
548
737
|
const sequence = nextSqliteConnectionSequence++;
|
|
@@ -560,6 +749,7 @@ function trackSqliteConnection(
|
|
|
560
749
|
((options as { readonly?: unknown }).readonly === true ||
|
|
561
750
|
(options as { readOnly?: unknown }).readOnly === true),
|
|
562
751
|
};
|
|
752
|
+
if (turnDeadlines) installNativeDeadlineRouting(db);
|
|
563
753
|
installTransactionRouting(db, metadata.readonly);
|
|
564
754
|
Object.defineProperty(db, "close", {
|
|
565
755
|
configurable: true,
|
|
@@ -586,7 +776,9 @@ function trackSqliteConnection(
|
|
|
586
776
|
|
|
587
777
|
const TrackedDatabase = new Proxy(DatabaseImpl, {
|
|
588
778
|
construct(target, args) {
|
|
589
|
-
const db =
|
|
779
|
+
const db = inRootAsyncContext(
|
|
780
|
+
() => Reflect.construct(target, args, target) as BetterSqlite3.Database,
|
|
781
|
+
);
|
|
590
782
|
return trackSqliteConnection(db, args[0], args[1]);
|
|
591
783
|
},
|
|
592
784
|
}) as typeof BetterSqlite3;
|
|
@@ -640,6 +832,18 @@ function buildNodeSqliteDatabaseClass(DatabaseSync: any): typeof BetterSqlite3 {
|
|
|
640
832
|
|
|
641
833
|
export const Database: typeof BetterSqlite3 = TrackedDatabase;
|
|
642
834
|
|
|
835
|
+
/** Open a Pi foreground connection with native-deadline routing before statements are prepared. */
|
|
836
|
+
export function createSqliteTurnDatabase(
|
|
837
|
+
filename?: string | Buffer,
|
|
838
|
+
options?: BetterSqlite3.Options,
|
|
839
|
+
): Database {
|
|
840
|
+
return trackSqliteConnection(new DatabaseImpl(filename, options), filename, options, true);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export function hasSqliteTurnDeadlineRouting(db: Database): boolean {
|
|
844
|
+
return busyTimeoutStates.has(db);
|
|
845
|
+
}
|
|
846
|
+
|
|
643
847
|
/** The constructor path, without querying SQLite on the prompt thread. */
|
|
644
848
|
export function getSqliteDatabasePath(db: Database): string | null {
|
|
645
849
|
for (const connection of trackedSqliteConnections.values()) {
|
|
@@ -664,6 +868,10 @@ function pragmaValue(db: BetterSqlite3.Database, name: string): unknown {
|
|
|
664
868
|
}
|
|
665
869
|
|
|
666
870
|
function pragmaNumber(db: BetterSqlite3.Database, name: string): number | null {
|
|
871
|
+
if (name === "busy_timeout") {
|
|
872
|
+
const state = busyTimeoutStates.get(db);
|
|
873
|
+
if (state) return state.current;
|
|
874
|
+
}
|
|
667
875
|
try {
|
|
668
876
|
const value = pragmaValue(db, name);
|
|
669
877
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
@@ -870,6 +1078,38 @@ export function prepareCachedStatement(db: Database, sql: string): Statement {
|
|
|
870
1078
|
}
|
|
871
1079
|
|
|
872
1080
|
const privilegeDepth = new WeakMap<Database, number>();
|
|
1081
|
+
interface NativeWaitLease {
|
|
1082
|
+
active: boolean;
|
|
1083
|
+
remainingMs?: () => number;
|
|
1084
|
+
}
|
|
1085
|
+
let nativeWaitScope: AsyncLocalStorage<NativeWaitLease | undefined> | undefined;
|
|
1086
|
+
|
|
1087
|
+
function releaseNativeWaitLease(lease: NativeWaitLease): void {
|
|
1088
|
+
lease.active = false;
|
|
1089
|
+
// Detached descendants can retain their async-local lease after the turn.
|
|
1090
|
+
// Do not retain the completed request through its deadline callback.
|
|
1091
|
+
lease.remainingMs = undefined;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** Bound native lock waits for an awaited turn, without changing background timeouts. */
|
|
1095
|
+
export function withSqliteTurnDeadline<T>(remainingMs: () => number, operation: () => T): T {
|
|
1096
|
+
const scope = (nativeWaitScope ??= new AsyncLocalStorage<NativeWaitLease | undefined>());
|
|
1097
|
+
const lease: NativeWaitLease = { active: true, remainingMs };
|
|
1098
|
+
return scope.run(lease, () => {
|
|
1099
|
+
try {
|
|
1100
|
+
const result = operation();
|
|
1101
|
+
if (result && typeof (result as { then?: unknown }).then === "function")
|
|
1102
|
+
return Promise.resolve(result).finally(
|
|
1103
|
+
releaseNativeWaitLease.bind(null, lease),
|
|
1104
|
+
) as T;
|
|
1105
|
+
releaseNativeWaitLease(lease);
|
|
1106
|
+
return result;
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
releaseNativeWaitLease(lease);
|
|
1109
|
+
throw error;
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
873
1113
|
const transformPassScope = new AsyncLocalStorage<
|
|
874
1114
|
| {
|
|
875
1115
|
active: boolean;
|
|
@@ -883,7 +1123,8 @@ const backgroundWriterScope = new AsyncLocalStorage<boolean>();
|
|
|
883
1123
|
|
|
884
1124
|
/** Opt in only a writer that can safely defer its work after a busy acquisition. */
|
|
885
1125
|
export function withSqliteBackgroundWriter<T>(operation: () => T): T {
|
|
886
|
-
return backgroundWriterScope.run(true, operation);
|
|
1126
|
+
if (!nativeWaitScope?.getStore()) return backgroundWriterScope.run(true, operation);
|
|
1127
|
+
return nativeWaitScope.run(undefined, () => backgroundWriterScope.run(true, operation));
|
|
887
1128
|
}
|
|
888
1129
|
|
|
889
1130
|
/** Only an awaited foreground transform may spend the extra acquisition budget.
|
|
@@ -920,7 +1161,8 @@ export function withSqliteTransformPass<T>(operation: () => T): T {
|
|
|
920
1161
|
|
|
921
1162
|
/** Start detached maintenance work without borrowing a foreground pass's budget. */
|
|
922
1163
|
export function withoutSqliteTransformPass<T>(operation: () => T): T {
|
|
923
|
-
return transformPassScope.run(undefined, operation);
|
|
1164
|
+
if (!nativeWaitScope?.getStore()) return transformPassScope.run(undefined, operation);
|
|
1165
|
+
return nativeWaitScope.run(undefined, () => transformPassScope.run(undefined, operation));
|
|
924
1166
|
}
|
|
925
1167
|
|
|
926
1168
|
/** Attach the adapter's existing generation/cancellation/deadline guard to writes. */
|
|
@@ -999,33 +1241,60 @@ function foregroundWaitLease() {
|
|
|
999
1241
|
: undefined;
|
|
1000
1242
|
}
|
|
1001
1243
|
|
|
1002
|
-
/**
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
function withAutocommitTimeout<T>(db: Database, operation: () => T): T {
|
|
1244
|
+
/** Probe a prepared statement's writer lock without charging uncontended work
|
|
1245
|
+
* against the foreground lock-wait budget. An exec script may commit some of
|
|
1246
|
+
* its statements before failing, so it cannot be retried without duplicating
|
|
1247
|
+
* writes; conservatively charge its single attempt's full execution time. */
|
|
1248
|
+
function withAutocommitTimeout<T>(db: Database, operation: () => T, singleStatement = true): T {
|
|
1007
1249
|
assertTransformWrite();
|
|
1008
1250
|
const lease = foregroundWaitLease();
|
|
1009
1251
|
if (isInTransaction(db)) return operation();
|
|
1010
1252
|
const scoped = lease || backgroundWriterScope.getStore();
|
|
1011
1253
|
const previous = scoped ? pragmaNumber(db, "busy_timeout") : null;
|
|
1012
|
-
if (scoped)
|
|
1013
|
-
db.exec(
|
|
1014
|
-
`PRAGMA busy_timeout=${lease ? Math.floor(Math.min(lease.remainingWaitMs, lease.guard?.remainingMs() ?? Infinity)) : SHORT_BUSY_TIMEOUT_MS}`,
|
|
1015
|
-
);
|
|
1016
1254
|
const started = performance.now();
|
|
1017
1255
|
let completed = false;
|
|
1018
|
-
|
|
1256
|
+
const execute = () => {
|
|
1019
1257
|
const result = operation();
|
|
1020
1258
|
completed = true;
|
|
1021
1259
|
return result;
|
|
1260
|
+
};
|
|
1261
|
+
try {
|
|
1262
|
+
if (!scoped) return execute();
|
|
1263
|
+
if (singleStatement) {
|
|
1264
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
1265
|
+
try {
|
|
1266
|
+
return execute();
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
if (!isTransientSqliteError(error) || isInTransaction(db)) throw error;
|
|
1269
|
+
}
|
|
1270
|
+
assertTransformWrite();
|
|
1271
|
+
}
|
|
1272
|
+
const timeout = Math.floor(
|
|
1273
|
+
Math.min(
|
|
1274
|
+
lease?.remainingWaitMs ?? SHORT_BUSY_TIMEOUT_MS,
|
|
1275
|
+
lease?.guard?.remainingMs() ?? Infinity,
|
|
1276
|
+
),
|
|
1277
|
+
);
|
|
1278
|
+
db.exec(`PRAGMA busy_timeout=${timeout}`);
|
|
1279
|
+
const waitStarted = performance.now();
|
|
1280
|
+
try {
|
|
1281
|
+
return execute();
|
|
1282
|
+
} finally {
|
|
1283
|
+
// Deduct the prepared statement's contended retry or the exec
|
|
1284
|
+
// script's execution from the remaining foreground wait budget;
|
|
1285
|
+
// the native API cannot separate lock waiting from execution.
|
|
1286
|
+
if (lease)
|
|
1287
|
+
lease.remainingWaitMs = Math.max(
|
|
1288
|
+
0,
|
|
1289
|
+
lease.remainingWaitMs - (performance.now() - waitStarted),
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1022
1292
|
} catch (error) {
|
|
1023
1293
|
if (lease && isTransientSqliteError(error))
|
|
1024
1294
|
throw new SqliteAcquisitionBusyError(error, "autocommit");
|
|
1025
1295
|
throw error;
|
|
1026
1296
|
} finally {
|
|
1027
1297
|
const elapsed = performance.now() - started;
|
|
1028
|
-
if (lease) lease.remainingWaitMs = Math.max(0, lease.remainingWaitMs - elapsed);
|
|
1029
1298
|
if (previous !== null) db.exec(`PRAGMA busy_timeout=${previous}`);
|
|
1030
1299
|
// Native autocommit does not expose the instant its lock was acquired.
|
|
1031
1300
|
// Label the upper bound so a waiting statement is not blamed as a holder.
|
|
@@ -1192,7 +1461,7 @@ export async function withAsyncPrivilegedWriter<T>(
|
|
|
1192
1461
|
operation: () => T,
|
|
1193
1462
|
options: {
|
|
1194
1463
|
signal?: AbortSignal;
|
|
1195
|
-
beforeRetry?: (error: SqliteAcquisitionBusyError) => void
|
|
1464
|
+
beforeRetry?: (error: SqliteAcquisitionBusyError) => void | Promise<void>;
|
|
1196
1465
|
jitter?: boolean;
|
|
1197
1466
|
budgetMs?: number;
|
|
1198
1467
|
assertCurrentPass?: () => void;
|
|
@@ -1235,7 +1504,7 @@ export async function withAsyncPrivilegedWriter<T>(
|
|
|
1235
1504
|
if (entered || !(error instanceof SqliteAcquisitionBusyError)) throw error;
|
|
1236
1505
|
// Adapters may serve a validated saved request instead of waiting.
|
|
1237
1506
|
// This runs only after a failed BEGIN, never after callback writes.
|
|
1238
|
-
options.beforeRetry?.(error);
|
|
1507
|
+
await options.beforeRetry?.(error);
|
|
1239
1508
|
const elapsed = performance.now() - started;
|
|
1240
1509
|
if (elapsed >= budgetMs) {
|
|
1241
1510
|
reportSqliteDiagnostic(
|
|
@@ -22,33 +22,33 @@ export function stableStringify(value: unknown, seen = new WeakSet<object>()): s
|
|
|
22
22
|
// value's text once per enclosing level, so a large value N levels deep cost
|
|
23
23
|
// N copies of itself.
|
|
24
24
|
const pieces: string[] = [];
|
|
25
|
-
writeStable(value, seen, pieces);
|
|
25
|
+
writeStable(value, seen, (piece) => pieces.push(piece));
|
|
26
26
|
return pieces.join("");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function writeStable(value: unknown, seen: WeakSet<object>,
|
|
29
|
+
function writeStable(value: unknown, seen: WeakSet<object>, write: (piece: string) => void): void {
|
|
30
30
|
if (value === undefined) {
|
|
31
|
-
|
|
31
|
+
write("undefined");
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
if (value === null || typeof value !== "object") {
|
|
35
|
-
|
|
35
|
+
write(JSON.stringify(value) ?? String(value));
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
// Any object met a second time, shared or cyclic, is written as "[Circular]".
|
|
39
39
|
if (seen.has(value)) {
|
|
40
|
-
|
|
40
|
+
write('"[Circular]"');
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
seen.add(value);
|
|
44
44
|
if (Array.isArray(value)) {
|
|
45
|
-
|
|
45
|
+
write("[");
|
|
46
46
|
for (let index = 0; index < value.length; index += 1) {
|
|
47
|
-
if (index > 0)
|
|
47
|
+
if (index > 0) write(",");
|
|
48
48
|
// A hole writes nothing between its commas, as joining a mapped array did.
|
|
49
|
-
if (index in value) writeStable(value[index], seen,
|
|
49
|
+
if (index in value) writeStable(value[index], seen, write);
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
write("]");
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
// Code-point sort (NOT localeCompare). Stable across runtimes/locales.
|
|
@@ -57,11 +57,53 @@ function writeStable(value: unknown, seen: WeakSet<object>, pieces: string[]): v
|
|
|
57
57
|
if (a > b) return 1;
|
|
58
58
|
return 0;
|
|
59
59
|
});
|
|
60
|
-
|
|
60
|
+
// Native JSON can emit a small scalar-only object in one operation. Larger
|
|
61
|
+
// containers still stream; undefined and integer keys need the writer's
|
|
62
|
+
// non-native spelling/order. Bound chunks so hashing never builds a large JSON string.
|
|
63
|
+
if (
|
|
64
|
+
entries.length > 0 &&
|
|
65
|
+
entries.length <= 16 &&
|
|
66
|
+
entries.every(
|
|
67
|
+
([key, child]) =>
|
|
68
|
+
key.length <= 4096 &&
|
|
69
|
+
!/^(0|[1-9]\d*)$/.test(key) &&
|
|
70
|
+
(child === null ||
|
|
71
|
+
typeof child === "number" ||
|
|
72
|
+
typeof child === "boolean" ||
|
|
73
|
+
(typeof child === "string" && child.length <= 4096)),
|
|
74
|
+
)
|
|
75
|
+
) {
|
|
76
|
+
write(JSON.stringify(Object.fromEntries(entries)));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
write("{");
|
|
61
80
|
entries.forEach(([key, child], index) => {
|
|
62
|
-
if (index > 0)
|
|
63
|
-
|
|
64
|
-
|
|
81
|
+
if (index > 0) write(",");
|
|
82
|
+
write(JSON.stringify(key));
|
|
83
|
+
write(":");
|
|
84
|
+
writeStable(child, seen, write);
|
|
65
85
|
});
|
|
66
|
-
|
|
86
|
+
write("}");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The existing UTF-16 FNV fingerprint, without retaining the serialized JSON. */
|
|
90
|
+
export function stableStringifyHash(value: unknown, prefix = ""): string {
|
|
91
|
+
const sink = jsonHashSink(prefix);
|
|
92
|
+
writeStable(value, new WeakSet(), sink.write);
|
|
93
|
+
return sink.digest();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Hash the same code units as the content memo, including lone surrogates. */
|
|
97
|
+
export function jsonHashSink(prefix = ""): {
|
|
98
|
+
write: (piece: string) => void;
|
|
99
|
+
digest: () => string;
|
|
100
|
+
} {
|
|
101
|
+
let hash = 0x811c9dc5;
|
|
102
|
+
const write = (piece: string): void => {
|
|
103
|
+
for (let index = 0; index < piece.length; index++) {
|
|
104
|
+
hash = Math.imul(hash ^ piece.charCodeAt(index), 0x01000193);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
write(prefix);
|
|
108
|
+
return { write, digest: () => (hash >>> 0).toString(16).padStart(8, "0") };
|
|
67
109
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { afterAll, describe, expect, test } from "bun:test";
|
|
1
|
+
import { afterAll, describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, readdirSync, readFileSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { withStorageFileMutex } from "./storage-file-mutex";
|
|
7
|
+
import * as permissions from "./storage-permissions";
|
|
7
8
|
|
|
8
9
|
const directories: string[] = [];
|
|
9
10
|
const newDirectory = () => {
|
|
@@ -16,6 +17,54 @@ afterAll(() => {
|
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
describe("withStorageFileMutex", () => {
|
|
20
|
+
for (const removals of [1, 2, 3]) {
|
|
21
|
+
test(`ticket creation tolerates at most two directory cleanup races (${removals})`, () => {
|
|
22
|
+
const directory = newDirectory();
|
|
23
|
+
const write = permissions.writeStorageFileSync;
|
|
24
|
+
let attempts = 0;
|
|
25
|
+
let worked = false;
|
|
26
|
+
const writer = spyOn(permissions, "writeStorageFileSync").mockImplementation(
|
|
27
|
+
(...args) => {
|
|
28
|
+
if (attempts++ < removals) rmdirSync(directory);
|
|
29
|
+
return write(...args);
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
try {
|
|
33
|
+
const acquire = () =>
|
|
34
|
+
withStorageFileMutex(directory, () => {
|
|
35
|
+
worked = true;
|
|
36
|
+
return "acquired";
|
|
37
|
+
});
|
|
38
|
+
if (removals <= 2) {
|
|
39
|
+
expect(acquire()).toBe("acquired");
|
|
40
|
+
// Includes the successful contender's atomic ticket rewrite.
|
|
41
|
+
expect(attempts).toBe(removals + 2);
|
|
42
|
+
expect(readdirSync(directory)).toEqual([]);
|
|
43
|
+
} else {
|
|
44
|
+
expect(acquire).toThrow("ENOENT");
|
|
45
|
+
expect(attempts).toBe(3);
|
|
46
|
+
}
|
|
47
|
+
expect(worked).toBe(removals <= 2);
|
|
48
|
+
} finally {
|
|
49
|
+
writer.mockRestore();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test("does not retry other ticket write failures", () => {
|
|
55
|
+
const directory = newDirectory();
|
|
56
|
+
const error = Object.assign(new Error("denied"), { code: "EACCES" });
|
|
57
|
+
const writer = spyOn(permissions, "writeStorageFileSync").mockImplementation(() => {
|
|
58
|
+
throw error;
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
expect(() => withStorageFileMutex(directory, () => 42)).toThrow(error);
|
|
62
|
+
expect(writer).toHaveBeenCalledTimes(1);
|
|
63
|
+
} finally {
|
|
64
|
+
writer.mockRestore();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
19
68
|
test("serializes two critical sections", () => {
|
|
20
69
|
const directory = newDirectory();
|
|
21
70
|
const orderPath = join(directory, "order");
|
|
@@ -68,7 +68,17 @@ export function withStorageFileMutex<T>(directory: string, work: () => T, timeou
|
|
|
68
68
|
const ticket: Ticket = { choosing: true, number: 0, birth, checksum: "" };
|
|
69
69
|
const encodedTicket = () =>
|
|
70
70
|
JSON.stringify({ ...ticket, checksum: ticketChecksum(name, ticket) });
|
|
71
|
-
|
|
71
|
+
for (let attempt = 0; ; attempt++) {
|
|
72
|
+
try {
|
|
73
|
+
writeStorageFileSync(ownPath, encodedTicket(), { flag: "wx", forcePrivate: true });
|
|
74
|
+
break;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// A session cleanup can remove an empty directory between mkdir and
|
|
77
|
+
// ticket creation. No ticket exists yet, so retry before contending.
|
|
78
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" || attempt >= 2) throw error;
|
|
79
|
+
ensurePrivateStorageDirectorySync(directory);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
72
82
|
const deadline = performance.now() + timeoutMs;
|
|
73
83
|
const peers = (): [string, Ticket | null][] => {
|
|
74
84
|
const found: [string, Ticket | null][] = [];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { removeNestedBlocks, removeSystemReminders } from "./system-directive";
|
|
3
|
+
|
|
4
|
+
describe("removeSystemReminders", () => {
|
|
5
|
+
test("removes nested reminders without leaking the outer tail", () => {
|
|
6
|
+
expect(
|
|
7
|
+
removeSystemReminders(
|
|
8
|
+
"before <system-reminder>outer <system-reminder>inner</system-reminder> tail</system-reminder> after",
|
|
9
|
+
),
|
|
10
|
+
).toBe("before after");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("removes unclosed reminders through the end", () => {
|
|
14
|
+
expect(
|
|
15
|
+
removeSystemReminders(
|
|
16
|
+
" keep <system-reminder>hidden <system-reminder>nested</system-reminder> tail",
|
|
17
|
+
),
|
|
18
|
+
).toBe("keep");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("handles attributes, case, self-closing tags and adjacent blocks", () => {
|
|
22
|
+
expect(
|
|
23
|
+
removeSystemReminders(
|
|
24
|
+
'a<SYSTEM-REMINDER id="x">hidden</SYSTEM-REMINDER><system-reminder/>b<system-reminder>hidden</system-reminder>c',
|
|
25
|
+
),
|
|
26
|
+
).toBe("abc");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("preserves ordinary content and other tags", () => {
|
|
30
|
+
expect(removeSystemReminders(" <other>visible</other> ")).toBe("<other>visible</other>");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("removeNestedBlocks tracks multiple tag names and only closes the matching top", () => {
|
|
35
|
+
expect(removeNestedBlocks("a<one>x<two>y</one>z</two>tail</one>b", ["one", "two"])).toBe("ab");
|
|
36
|
+
expect(removeNestedBlocks(" a<one/>b ", ["one"])).toBe(" ab ");
|
|
37
|
+
});
|