@cortexkit/opencode-magic-context 0.44.1 → 0.44.2
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 +6 -0
- package/dist/agents/dreamer.d.ts +4 -6
- package/dist/agents/dreamer.d.ts.map +1 -1
- package/dist/config/live-child-output-cap.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +63 -10
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-d14ah73v.js → conflict-warning-hook-1msyvb51.js} +3 -3
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts +11 -0
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/docs-proposals.d.ts +2 -0
- package/dist/features/magic-context/dreamer/docs-proposals.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/manual-summary.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts +5 -0
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts +5 -0
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +2 -2
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +8 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts +5 -0
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts +18 -2
- package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts +12 -0
- package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/token-budget.d.ts +33 -0
- package/dist/features/magic-context/dreamer/token-budget.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify-diff.d.ts +15 -0
- package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify-gate.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-read-cap.d.ts +4 -0
- package/dist/features/magic-context/dreamer/verify-read-cap.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify.d.ts +5 -0
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity-cache.d.ts +6 -0
- package/dist/features/magic-context/memory/project-identity-cache.d.ts.map +1 -0
- package/dist/features/magic-context/memory/project-identity.d.ts +8 -22
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -0
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
- package/dist/features/magic-context/memory/verification-paths.d.ts +1 -0
- package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +5 -0
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/session-activity.d.ts +10 -0
- package/dist/features/magic-context/session-activity.d.ts.map +1 -0
- package/dist/features/magic-context/session-project-backfill.d.ts +4 -1
- package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts +5 -0
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +1 -0
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/storage.d.ts +1 -1
- package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/types.d.ts +2 -0
- package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +2 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-identity-merge.d.ts +4 -1
- package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/dropped-input-guard.d.ts.map +1 -1
- package/dist/hooks/magic-context/embed-session-state.d.ts +3 -0
- package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts +1 -0
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-payloads.d.ts +1 -0
- package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts +6 -4
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts +7 -0
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-d107az47.js → index-18tjha8g.js} +1 -1
- package/dist/{index-0rp6pj54.js → index-3x9caerx.js} +7 -5
- package/dist/index-40s1xz22.js +125 -0
- package/dist/{index-cz88kw0n.js → index-egkfvhms.js} +117 -39
- package/dist/{index-s6b7h12g.js → index-esxfcz5z.js} +73 -2
- package/dist/index-gtzcmv90.js +7260 -0
- package/dist/{index-6w9p73jz.js → index-jqcbs2k1.js} +10 -0
- package/dist/{index-a9vbtdzv.js → index-r9vkp4n9.js} +1 -1
- package/dist/{index-jtdrvmcn.js → index-ttdn47aq.js} +37431 -37044
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8195 -14449
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +10 -9
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +1 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-fkdv2g5b.js → read-session-chunk-wmvgnd24.js} +2 -2
- package/dist/{recomp-orchestrator-k4h5n0p3.js → recomp-orchestrator-rk40ff15.js} +5 -4
- package/dist/{send-session-notification-g2k03zyn.js → send-session-notification-6xsrcxnp.js} +2 -2
- package/dist/shared/model-suggestion-retry.d.ts +1 -1
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/prompt-async-transport.d.ts +7 -7
- package/dist/shared/prompt-async-transport.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +21 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts +12 -0
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/subagent-runner.d.ts +3 -1
- package/dist/shared/subagent-runner.d.ts.map +1 -1
- package/dist/shared/user-facing-codes.d.ts +5 -0
- package/dist/shared/user-facing-codes.d.ts.map +1 -1
- package/dist/{task-config-mgf5nsh3.js → task-config-2bm68628.js} +1 -1
- package/dist/transform-postprocess-phase-t7xgdhdf.js +36 -0
- package/dist/v2/hidden-completion.d.ts +2 -0
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +1 -0
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/lkg-system.d.ts +11 -0
- package/dist/v2/hooks/lkg-system.d.ts.map +1 -0
- package/dist/v2/hooks/session-project-backfill.d.ts +1 -1
- package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -1
- package/dist/v2/hooks/storage-gate.d.ts +11 -5
- package/dist/v2/hooks/storage-gate.d.ts.map +1 -1
- package/dist/v2/host-service.d.ts +2 -1
- package/dist/v2/host-service.d.ts.map +1 -1
- package/dist/v2/server.js +24481 -22623
- package/dist/v2/store-reader.d.ts +2 -0
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/{wrapup-orchestrator-35y56636.js → wrapup-orchestrator-h8tr10c6.js} +6 -5
- package/package.json +1 -1
- package/src/agents/dreamer.ts +4 -6
- package/src/agents/hidden-agent-registrations.ts +3 -3
- package/src/agents/permissions.test.ts +3 -4
- package/src/config/latch-permanence-guard.test.ts +9 -0
- package/src/config/live-child-output-cap.test.ts +26 -0
- package/src/config/live-child-output-cap.ts +6 -2
- package/src/config/schema/magic-context.ts +21 -6
- package/src/features/magic-context/dreamer/task-registry.ts +13 -0
- package/src/shared/logger.test.ts +1 -0
- package/src/shared/model-suggestion-retry.ts +9 -2
- package/src/shared/prompt-async-transport.test.ts +254 -1
- package/src/shared/prompt-async-transport.ts +153 -44
- package/src/shared/rpc-async-probes.test.ts +230 -0
- package/src/shared/rpc-server.ts +6 -4
- package/src/shared/rpc-utils.test.ts +27 -1
- package/src/shared/rpc-utils.ts +161 -9
- package/src/shared/sqlite-acquisition-fence.test.ts +132 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +94 -0
- package/src/shared/sqlite-pass-scope.test.ts +182 -0
- package/src/shared/sqlite-transaction-mode-guard.test.ts +42 -10
- package/src/shared/sqlite-write-locker-test-support.ts +1 -0
- package/src/shared/sqlite.ts +142 -56
- package/src/shared/storage-busy.test.ts +95 -0
- package/src/shared/subagent-runner.ts +4 -0
- package/src/shared/user-facing-codes.ts +6 -0
- package/src/shared/windows-hidden-spawns.test.ts +132 -0
- package/src/shared/write-transaction-attribution.test.ts +1 -0
package/src/shared/rpc-utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
1
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -118,12 +118,13 @@ const RPC_IDENTITY_SKEW_TOLERANCE_MS = 120_000;
|
|
|
118
118
|
const LINUX_CLOCK_TICKS_PER_SECOND = 100;
|
|
119
119
|
const PS_PROBE_TIMEOUT_MS = 1_000;
|
|
120
120
|
const WINDOWS_CIM_PROBE_TIMEOUT_MS = 5_000;
|
|
121
|
+
const WINDOWS_PROCESS_SNAPSHOT_TTL_MS = 2_000;
|
|
121
122
|
const MAX_ANCESTOR_WALK_DEPTH = 16;
|
|
122
123
|
const OPEN_CODE_COMMAND_MARKERS = ["opencode", "node", "bun", "electron"];
|
|
123
124
|
const TASKLIST_NO_TASKS_PATTERN =
|
|
124
125
|
/^INFO:\s+No tasks are running which match the specified criteria\.?$/im;
|
|
125
126
|
const WINDOWS_CIM_COMMAND =
|
|
126
|
-
"Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress";
|
|
127
|
+
"Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine,CreationDate | ConvertTo-Json -Compress";
|
|
127
128
|
const PI_HARNESS_ARC_MARKERS = [
|
|
128
129
|
"pi-coding-agent",
|
|
129
130
|
"oh-my-pi",
|
|
@@ -184,6 +185,7 @@ function readPsProcessStartTime(pid: number): number | null {
|
|
|
184
185
|
encoding: "utf8",
|
|
185
186
|
timeout: PS_PROBE_TIMEOUT_MS,
|
|
186
187
|
stdio: ["ignore", "pipe", "pipe"],
|
|
188
|
+
windowsHide: true,
|
|
187
189
|
});
|
|
188
190
|
const processStartTime = Date.parse(String(output).trim());
|
|
189
191
|
return Number.isFinite(processStartTime) ? processStartTime : null;
|
|
@@ -232,6 +234,7 @@ interface ProcessSnapshot {
|
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
let windowsProcessFactsCache: Map<number, ProcessFacts> | null = null;
|
|
237
|
+
let windowsProcessSnapshotCache: { snapshot: ProcessSnapshot; at: number } | null = null;
|
|
235
238
|
|
|
236
239
|
function rememberWindowsProcessFacts(facts: ProcessFacts[]): void {
|
|
237
240
|
windowsProcessFactsCache = new Map(facts.map((fact) => [fact.pid, fact]));
|
|
@@ -239,6 +242,7 @@ function rememberWindowsProcessFacts(facts: ProcessFacts[]): void {
|
|
|
239
242
|
|
|
240
243
|
function clearWindowsProcessFactsCache(): void {
|
|
241
244
|
windowsProcessFactsCache = null;
|
|
245
|
+
windowsProcessSnapshotCache = null;
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
function parseCsvLine(line: string): string[] | null {
|
|
@@ -292,6 +296,7 @@ function readWindowsProcess(pid: number): { state: PidLiveness; command?: string
|
|
|
292
296
|
encoding: "utf8",
|
|
293
297
|
timeout: PS_PROBE_TIMEOUT_MS,
|
|
294
298
|
stdio: ["ignore", "pipe", "pipe"],
|
|
299
|
+
windowsHide: true,
|
|
295
300
|
});
|
|
296
301
|
const entries = parseTasklistOutput(String(output));
|
|
297
302
|
if (entries === null) return { state: "inconclusive" };
|
|
@@ -325,6 +330,7 @@ function readPsProcessCommand(pid: number): string | null {
|
|
|
325
330
|
encoding: "utf8",
|
|
326
331
|
timeout: PS_PROBE_TIMEOUT_MS,
|
|
327
332
|
stdio: ["ignore", "pipe", "pipe"],
|
|
333
|
+
windowsHide: true,
|
|
328
334
|
});
|
|
329
335
|
return String(output);
|
|
330
336
|
} catch {
|
|
@@ -521,6 +527,7 @@ function execProcessList(
|
|
|
521
527
|
encoding: "utf8",
|
|
522
528
|
timeout,
|
|
523
529
|
stdio: ["ignore", "pipe", "pipe"],
|
|
530
|
+
windowsHide: true,
|
|
524
531
|
}),
|
|
525
532
|
);
|
|
526
533
|
}
|
|
@@ -588,7 +595,12 @@ function parseWindowsCimOutput(output: string): ProcessFacts[] | null {
|
|
|
588
595
|
pid,
|
|
589
596
|
parentPid: Number.isInteger(parentPid) && parentPid > 0 ? parentPid : null,
|
|
590
597
|
commandLine,
|
|
591
|
-
imageName:
|
|
598
|
+
imageName:
|
|
599
|
+
typeof record.Name === "string"
|
|
600
|
+
? record.Name
|
|
601
|
+
: commandLine
|
|
602
|
+
? executableName(commandTokens(commandLine)[0])
|
|
603
|
+
: null,
|
|
592
604
|
startTime: parseWindowsCreationDate(record.CreationDate),
|
|
593
605
|
});
|
|
594
606
|
}
|
|
@@ -679,14 +691,18 @@ function readPosixParentPid(pid: number): number | null {
|
|
|
679
691
|
* plugin runs. Bound the walk and stop on cycles so a broken parent map
|
|
680
692
|
* cannot loop.
|
|
681
693
|
*/
|
|
682
|
-
function collectAncestorPids(
|
|
694
|
+
function collectAncestorPids(
|
|
695
|
+
selfPid: number,
|
|
696
|
+
parentByPid: Map<number, number>,
|
|
697
|
+
probeParents = true,
|
|
698
|
+
): Set<number> {
|
|
683
699
|
const ancestors = new Set<number>();
|
|
684
700
|
let current = selfPid;
|
|
685
701
|
for (let depth = 0; depth < MAX_ANCESTOR_WALK_DEPTH; depth += 1) {
|
|
686
702
|
let ppid: number | null = null;
|
|
687
703
|
if (parentByPid.has(current)) {
|
|
688
704
|
ppid = parentByPid.get(current) ?? null;
|
|
689
|
-
} else if (rpcIdentityPlatform !== "win32") {
|
|
705
|
+
} else if (rpcIdentityPlatform !== "win32" && probeParents) {
|
|
690
706
|
ppid = readPosixParentPid(current);
|
|
691
707
|
if (ppid == null && current === process.pid && process.ppid > 0) {
|
|
692
708
|
ppid = process.ppid;
|
|
@@ -703,8 +719,11 @@ function collectAncestorPids(selfPid: number, parentByPid: Map<number, number>):
|
|
|
703
719
|
return ancestors;
|
|
704
720
|
}
|
|
705
721
|
|
|
706
|
-
function classifyLivePiSnapshot(
|
|
707
|
-
|
|
722
|
+
function classifyLivePiSnapshot(
|
|
723
|
+
snapshot: ProcessSnapshot,
|
|
724
|
+
probeParents = true,
|
|
725
|
+
): PiProcessDiscovery {
|
|
726
|
+
const ancestors = collectAncestorPids(process.pid, snapshot.parentByPid, probeParents);
|
|
708
727
|
const processIds = new Set<number>();
|
|
709
728
|
const inconclusivePids = new Set<number>();
|
|
710
729
|
const skippedAncestorPids: number[] = [];
|
|
@@ -773,8 +792,14 @@ export function inspectLivePiProcesses(): PiProcessDiscovery {
|
|
|
773
792
|
// Prefer one CIM query (pid, parent, command line, start time).
|
|
774
793
|
// tasklist is image-name-only, so it is a fallback and never a
|
|
775
794
|
// verified live-harness source.
|
|
776
|
-
const
|
|
777
|
-
const
|
|
795
|
+
const now = rpcIdentityNowMs();
|
|
796
|
+
const cached = windowsProcessSnapshotCache;
|
|
797
|
+
// A brief reuse avoids spawning PowerShell on every blocker scan.
|
|
798
|
+
const snapshot =
|
|
799
|
+
cached && now >= cached.at && now - cached.at < WINDOWS_PROCESS_SNAPSHOT_TTL_MS
|
|
800
|
+
? cached.snapshot
|
|
801
|
+
: (tryReadWindowsCimSnapshot(rpcProcessListExecFileSync) ??
|
|
802
|
+
tryReadWindowsTasklistSnapshot());
|
|
778
803
|
if (!snapshot) {
|
|
779
804
|
return {
|
|
780
805
|
state: "unreadable",
|
|
@@ -782,6 +807,7 @@ export function inspectLivePiProcesses(): PiProcessDiscovery {
|
|
|
782
807
|
error: "process list unavailable",
|
|
783
808
|
};
|
|
784
809
|
}
|
|
810
|
+
if (snapshot !== cached?.snapshot) windowsProcessSnapshotCache = { snapshot, at: now };
|
|
785
811
|
rememberWindowsProcessFacts(snapshot.facts);
|
|
786
812
|
return classifyLivePiSnapshot(snapshot);
|
|
787
813
|
}
|
|
@@ -834,3 +860,129 @@ export function parseRpcPortFile(content: string, fallbackPid = 0): RpcPortFileR
|
|
|
834
860
|
function isValidPort(port: number): boolean {
|
|
835
861
|
return Number.isInteger(port) && port > 0 && port <= 65535;
|
|
836
862
|
}
|
|
863
|
+
|
|
864
|
+
/** One bounded, shared process scan for asynchronous storage opens. */
|
|
865
|
+
export interface AsyncProcessInspection {
|
|
866
|
+
pi: PiProcessDiscovery;
|
|
867
|
+
/** Includes process names and commands so offline maintenance can rule out live store users. */
|
|
868
|
+
processSnapshot?: {
|
|
869
|
+
source: ProcessSnapshotSource;
|
|
870
|
+
facts: Array<{ pid: number; imageName: string | null; commandLine: string | null }>;
|
|
871
|
+
};
|
|
872
|
+
evidence(pid: number): ProcessProbeEvidence;
|
|
873
|
+
liveness(pid: number): PidLiveness;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function execProcessListAsync(file: string, args: string[], timeout: number): Promise<string> {
|
|
877
|
+
return new Promise((resolve, reject) => {
|
|
878
|
+
execFile(
|
|
879
|
+
file,
|
|
880
|
+
args,
|
|
881
|
+
{ encoding: "utf8", timeout, windowsHide: true, maxBuffer: 8 * 1024 * 1024 },
|
|
882
|
+
(error, stdout) => {
|
|
883
|
+
if (error) reject(error);
|
|
884
|
+
else resolve(stdout);
|
|
885
|
+
},
|
|
886
|
+
);
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
let asyncProcessExec = execProcessListAsync;
|
|
891
|
+
let asyncInspection: Promise<AsyncProcessInspection> | undefined;
|
|
892
|
+
let asyncInspectionExpires = 0;
|
|
893
|
+
let lastAsyncSnapshot: ProcessSnapshot | null = null;
|
|
894
|
+
|
|
895
|
+
/** Inject only the asynchronous transport; legacy synchronous probes stay isolated. */
|
|
896
|
+
export function __setAsyncProcessProbeForTests(probe?: typeof execProcessListAsync): void {
|
|
897
|
+
asyncProcessExec = probe ?? execProcessListAsync;
|
|
898
|
+
asyncInspection = undefined;
|
|
899
|
+
asyncInspectionExpires = 0;
|
|
900
|
+
lastAsyncSnapshot = null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export function inspectProcessesAsync(forceFresh = false): Promise<AsyncProcessInspection> {
|
|
904
|
+
if (!forceFresh && asyncInspection && rpcIdentityNowMs() < asyncInspectionExpires)
|
|
905
|
+
return asyncInspection;
|
|
906
|
+
asyncInspectionExpires = Number.POSITIVE_INFINITY;
|
|
907
|
+
asyncInspection = (async () => {
|
|
908
|
+
let snapshot: ProcessSnapshot | null = null;
|
|
909
|
+
try {
|
|
910
|
+
if (rpcIdentityPlatform === "win32") {
|
|
911
|
+
try {
|
|
912
|
+
const output = await asyncProcessExec(
|
|
913
|
+
"powershell",
|
|
914
|
+
["-NoProfile", "-Command", WINDOWS_CIM_COMMAND],
|
|
915
|
+
WINDOWS_CIM_PROBE_TIMEOUT_MS,
|
|
916
|
+
);
|
|
917
|
+
const facts = parseWindowsCimOutput(output);
|
|
918
|
+
if (facts) snapshot = snapshotFromFacts(facts, "cim");
|
|
919
|
+
} catch {
|
|
920
|
+
/* tasklist is the bounded fallback when CIM is unavailable. */
|
|
921
|
+
}
|
|
922
|
+
if (!snapshot) {
|
|
923
|
+
const entries = parseTasklistOutput(
|
|
924
|
+
await asyncProcessExec("tasklist", ["/FO", "CSV"], PS_PROBE_TIMEOUT_MS),
|
|
925
|
+
);
|
|
926
|
+
if (entries)
|
|
927
|
+
snapshot = snapshotFromFacts(
|
|
928
|
+
entries.map(({ pid, command }) => ({
|
|
929
|
+
pid,
|
|
930
|
+
parentPid: null,
|
|
931
|
+
commandLine: null,
|
|
932
|
+
imageName: command,
|
|
933
|
+
startTime: null,
|
|
934
|
+
})),
|
|
935
|
+
"tasklist",
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
const output = await asyncProcessExec(
|
|
940
|
+
"ps",
|
|
941
|
+
["-axo", "pid=,ppid=,lstart=,command="],
|
|
942
|
+
PS_PROBE_TIMEOUT_MS,
|
|
943
|
+
);
|
|
944
|
+
const facts: ProcessFacts[] = [];
|
|
945
|
+
for (const line of output.split(/\r?\n/)) {
|
|
946
|
+
const match =
|
|
947
|
+
/^\s*(\d+)\s+(\d+)\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(.+)$/.exec(
|
|
948
|
+
line,
|
|
949
|
+
);
|
|
950
|
+
if (!match) continue;
|
|
951
|
+
const startTime = Date.parse(match[3]);
|
|
952
|
+
facts.push({
|
|
953
|
+
pid: Number(match[1]),
|
|
954
|
+
parentPid: Number(match[2]),
|
|
955
|
+
commandLine: match[4],
|
|
956
|
+
imageName: null,
|
|
957
|
+
startTime: Number.isFinite(startTime) ? startTime : null,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
snapshot = facts.length > 0 ? snapshotFromFacts(facts, "ps") : null;
|
|
961
|
+
}
|
|
962
|
+
} catch {
|
|
963
|
+
/* Missing process evidence is inconclusive, never proof of death. */
|
|
964
|
+
}
|
|
965
|
+
// A timed-out refresh cannot prove that a process blocking migration has
|
|
966
|
+
// exited. Keep its evidence until a successful scan can establish death.
|
|
967
|
+
const fresh = snapshot !== null;
|
|
968
|
+
if (snapshot) lastAsyncSnapshot = snapshot;
|
|
969
|
+
else snapshot = lastAsyncSnapshot;
|
|
970
|
+
const byPid = new Map(snapshot?.facts.map((fact) => [fact.pid, fact]));
|
|
971
|
+
return {
|
|
972
|
+
pi: snapshot
|
|
973
|
+
? classifyLivePiSnapshot(snapshot, false)
|
|
974
|
+
: { state: "unreadable", processIds: [] },
|
|
975
|
+
...(fresh && snapshot
|
|
976
|
+
? { processSnapshot: { source: snapshot.source, facts: snapshot.facts } }
|
|
977
|
+
: {}),
|
|
978
|
+
evidence: (pid: number) => ({
|
|
979
|
+
startTime: byPid.get(pid)?.startTime ?? null,
|
|
980
|
+
commandLine: byPid.get(pid)?.commandLine ?? byPid.get(pid)?.imageName ?? null,
|
|
981
|
+
}),
|
|
982
|
+
liveness: (pid: number) => (byPid.has(pid) ? "alive" : fresh ? "dead" : "inconclusive"),
|
|
983
|
+
} satisfies AsyncProcessInspection;
|
|
984
|
+
})().finally(() => {
|
|
985
|
+
asyncInspectionExpires = rpcIdentityNowMs() + WINDOWS_PROCESS_SNAPSHOT_TTL_MS;
|
|
986
|
+
});
|
|
987
|
+
return asyncInspection;
|
|
988
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
|
|
6
|
+
const root = resolve(import.meta.dir, "../../../..");
|
|
7
|
+
const shared = "packages/plugin/src/shared/sqlite.ts";
|
|
8
|
+
function files(dir: string): string[] {
|
|
9
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
10
|
+
const path = join(dir, entry.name);
|
|
11
|
+
if (entry.isDirectory()) return files(path);
|
|
12
|
+
return /\.tsx?$/.test(path) && !/\.test\.|test-support|\.d\.ts$/.test(path) ? [path] : [];
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function scan(
|
|
17
|
+
path: string,
|
|
18
|
+
text: string,
|
|
19
|
+
): { violations: string[]; transactions: number; literals: number } {
|
|
20
|
+
const violations: string[] = [];
|
|
21
|
+
let transactions = 0;
|
|
22
|
+
let literals = 0;
|
|
23
|
+
const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
|
|
24
|
+
const visit = (node: ts.Node) => {
|
|
25
|
+
if (
|
|
26
|
+
ts.isCallExpression(node) &&
|
|
27
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
28
|
+
node.expression.name.text === "transaction"
|
|
29
|
+
)
|
|
30
|
+
transactions++;
|
|
31
|
+
if (
|
|
32
|
+
path !== shared &&
|
|
33
|
+
ts.isImportDeclaration(node) &&
|
|
34
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
35
|
+
!node.importClause?.isTypeOnly &&
|
|
36
|
+
/^(bun:sqlite|node:sqlite|better-sqlite3)$/.test(node.moduleSpecifier.text)
|
|
37
|
+
) {
|
|
38
|
+
violations.push(`${path}: native SQLite import bypasses shared acquisition`);
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
path !== shared &&
|
|
42
|
+
ts.isExportDeclaration(node) &&
|
|
43
|
+
!node.isTypeOnly &&
|
|
44
|
+
node.moduleSpecifier &&
|
|
45
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
46
|
+
/^(bun:sqlite|node:sqlite|better-sqlite3)$/.test(node.moduleSpecifier.text)
|
|
47
|
+
) {
|
|
48
|
+
violations.push(`${path}: native SQLite re-export bypasses shared acquisition`);
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
path !== shared &&
|
|
52
|
+
ts.isCallExpression(node) &&
|
|
53
|
+
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
54
|
+
(ts.isIdentifier(node.expression) && node.expression.text === "require")) &&
|
|
55
|
+
/(?:bun:sqlite|node:sqlite|better-sqlite3)/.test(
|
|
56
|
+
node.arguments.map((arg) => arg.getText(source)).join(""),
|
|
57
|
+
)
|
|
58
|
+
) {
|
|
59
|
+
violations.push(`${path}: dynamic native SQLite import bypasses shared acquisition`);
|
|
60
|
+
}
|
|
61
|
+
const sqlText = ts.isStringLiteralLike(node)
|
|
62
|
+
? node.text
|
|
63
|
+
: ts.isTemplateExpression(node)
|
|
64
|
+
? node.head.text + node.templateSpans.map((span) => span.literal.text).join("")
|
|
65
|
+
: undefined;
|
|
66
|
+
if (
|
|
67
|
+
sqlText !== undefined &&
|
|
68
|
+
/\bBEGIN\s+(?:IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*(?:;|$)/i.test(sqlText)
|
|
69
|
+
) {
|
|
70
|
+
literals++;
|
|
71
|
+
if (path !== shared) {
|
|
72
|
+
const call = node.parent;
|
|
73
|
+
const routed =
|
|
74
|
+
!ts.isTemplateExpression(node) &&
|
|
75
|
+
ts.isCallExpression(call) &&
|
|
76
|
+
ts.isPropertyAccessExpression(call.expression) &&
|
|
77
|
+
call.expression.name.text === "exec" &&
|
|
78
|
+
/^\s*BEGIN\s+(IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*;?\s*$/i.test(sqlText);
|
|
79
|
+
if (!routed)
|
|
80
|
+
violations.push(
|
|
81
|
+
`${path}:${source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1}: BEGIN acquisition must be a standalone shared Database.exec call`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
ts.forEachChild(node, visit);
|
|
86
|
+
};
|
|
87
|
+
visit(source);
|
|
88
|
+
return { violations, transactions, literals };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
test("all production transaction acquisition routes through the shared SQLite runtime", () => {
|
|
92
|
+
const scans = ["packages/plugin/src", "packages/pi-plugin/src", "packages/cli/src"]
|
|
93
|
+
.flatMap((directory) => files(join(root, directory)))
|
|
94
|
+
.map((path) => scan(relative(root, path), readFileSync(path, "utf8")));
|
|
95
|
+
expect(scans.reduce((sum, scan) => sum + scan.transactions, 0)).toBeGreaterThan(50);
|
|
96
|
+
expect(scans.reduce((sum, scan) => sum + scan.literals, 0)).toBeGreaterThan(20);
|
|
97
|
+
expect(scans.flatMap((scan) => scan.violations)).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("acquisition fence rejects raw native transactions and unrouted BEGIN shapes", () => {
|
|
101
|
+
expect(
|
|
102
|
+
scan(
|
|
103
|
+
"new-writer.ts",
|
|
104
|
+
'import { Database } from "bun:sqlite"; new Database(":memory:").transaction(() => write())();',
|
|
105
|
+
).violations,
|
|
106
|
+
).toHaveLength(1);
|
|
107
|
+
expect(
|
|
108
|
+
scan(
|
|
109
|
+
"new-writer.ts",
|
|
110
|
+
'const { DatabaseSync } = require("node:sqlite"); new DatabaseSync(":memory:").transaction(() => write())();',
|
|
111
|
+
).violations,
|
|
112
|
+
).toHaveLength(1);
|
|
113
|
+
expect(scan("new-writer.ts", 'export { Database } from "bun:sqlite";').violations).toHaveLength(
|
|
114
|
+
1,
|
|
115
|
+
);
|
|
116
|
+
expect(scan("new-writer.ts", 'db.prepare("BEGIN IMMEDIATE").run();').violations).toHaveLength(
|
|
117
|
+
1,
|
|
118
|
+
);
|
|
119
|
+
expect(
|
|
120
|
+
scan("new-writer.ts", 'db.exec("BEGIN IMMEDIATE; INSERT INTO result VALUES (1)");')
|
|
121
|
+
.violations,
|
|
122
|
+
).toHaveLength(1);
|
|
123
|
+
expect(scan("new-writer.ts", "db.exec(`BEGIN IMMEDIATE; ${extra}`);").violations).toHaveLength(
|
|
124
|
+
1,
|
|
125
|
+
);
|
|
126
|
+
expect(
|
|
127
|
+
scan(
|
|
128
|
+
"new-writer.ts",
|
|
129
|
+
'import { Database } from "./shared/sqlite"; db.exec("BEGIN IMMEDIATE"); db.transaction(() => write()).immediate();',
|
|
130
|
+
).violations,
|
|
131
|
+
).toEqual([]);
|
|
132
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Database, withPrivilegedWriter, withSqliteTransformPass } from "./sqlite";
|
|
6
|
+
import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
|
|
7
|
+
|
|
8
|
+
for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
|
|
9
|
+
test(`shared SQLite retries ${mode} acquisition before running any writes`, async () => {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-routed-acquisition-"));
|
|
11
|
+
const path = join(dir, "context.db");
|
|
12
|
+
const db = new Database(path);
|
|
13
|
+
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=50; CREATE TABLE result(value TEXT)");
|
|
14
|
+
const locker = await startSqliteWriteLocker(path, 650);
|
|
15
|
+
let calls = 0;
|
|
16
|
+
const write = () => {
|
|
17
|
+
calls++;
|
|
18
|
+
db.prepare("INSERT INTO result VALUES (?)").run("once");
|
|
19
|
+
return "managed";
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
withSqliteTransformPass(() => {
|
|
23
|
+
if (mode === "literal") {
|
|
24
|
+
db.exec("BEGIN IMMEDIATE");
|
|
25
|
+
write();
|
|
26
|
+
db.exec("COMMIT");
|
|
27
|
+
} else {
|
|
28
|
+
const transaction = db.transaction(write);
|
|
29
|
+
expect(mode === "default" ? transaction() : transaction[mode]()).toBe(
|
|
30
|
+
"managed",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
expect(calls).toBe(1);
|
|
35
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([{ value: "once" }]);
|
|
36
|
+
} finally {
|
|
37
|
+
await locker.exited;
|
|
38
|
+
db.close();
|
|
39
|
+
rmSync(dir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
}, 30000);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("routed transactions preserve nesting, receiver, arguments and rollback without callback retries", () => {
|
|
45
|
+
const db = new Database(":memory:");
|
|
46
|
+
db.exec("CREATE TABLE result(value TEXT)");
|
|
47
|
+
let calls = 0;
|
|
48
|
+
const transaction = db.transaction(function (this: { prefix: string }, value: string) {
|
|
49
|
+
calls++;
|
|
50
|
+
db.prepare("INSERT INTO result VALUES (?)").run(this.prefix + value);
|
|
51
|
+
db.transaction(() => {
|
|
52
|
+
db.exec("INSERT INTO result VALUES ('inner')");
|
|
53
|
+
}).immediate();
|
|
54
|
+
throw Object.assign(new Error("busy after writes"), { code: "SQLITE_BUSY" });
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
expect(() =>
|
|
58
|
+
withSqliteTransformPass(() => transaction.call({ prefix: "outer-" }, "value")),
|
|
59
|
+
).toThrow("busy after writes");
|
|
60
|
+
expect(calls).toBe(1);
|
|
61
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
|
|
62
|
+
} finally {
|
|
63
|
+
db.close();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("exhausted routed acquisition never enters the callback or multiplies privileged retries", () => {
|
|
68
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-routed-exhaustion-"));
|
|
69
|
+
const path = join(dir, "context.db");
|
|
70
|
+
const blocker = new Database(path);
|
|
71
|
+
const db = new Database(path);
|
|
72
|
+
blocker.exec("PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT)");
|
|
73
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
74
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
75
|
+
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
76
|
+
let callbacks = 0;
|
|
77
|
+
try {
|
|
78
|
+
for (const run of [
|
|
79
|
+
() => db.transaction(() => callbacks++)(),
|
|
80
|
+
() => withPrivilegedWriter(db, () => callbacks++),
|
|
81
|
+
]) {
|
|
82
|
+
wait.mockClear();
|
|
83
|
+
expect(() => withSqliteTransformPass(run)).toThrow("after 3 attempts");
|
|
84
|
+
expect(wait).toHaveBeenCalledTimes(2);
|
|
85
|
+
expect(callbacks).toBe(0);
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
wait.mockRestore();
|
|
89
|
+
blocker.exec("ROLLBACK");
|
|
90
|
+
blocker.close();
|
|
91
|
+
db.close();
|
|
92
|
+
rmSync(dir, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
Database,
|
|
7
|
+
withoutSqliteTransformPass,
|
|
8
|
+
withPrivilegedWriter,
|
|
9
|
+
withSqliteTransformPass,
|
|
10
|
+
} from "./sqlite";
|
|
11
|
+
|
|
12
|
+
test("background privileged acquisition attempts once without backoff", () => {
|
|
13
|
+
const db = new Database(":memory:");
|
|
14
|
+
const error = Object.assign(new Error("original busy"), { code: "SQLITE_BUSY" });
|
|
15
|
+
const exec = spyOn(db, "exec").mockImplementation(() => {
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
19
|
+
let callbacks = 0;
|
|
20
|
+
try {
|
|
21
|
+
expect(() => withPrivilegedWriter(db, () => callbacks++)).toThrow("original busy");
|
|
22
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
23
|
+
expect(wait).not.toHaveBeenCalled();
|
|
24
|
+
expect(callbacks).toBe(0);
|
|
25
|
+
} finally {
|
|
26
|
+
exec.mockRestore();
|
|
27
|
+
wait.mockRestore();
|
|
28
|
+
db.close();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("background transaction exhausts one five-second busy timeout", () => {
|
|
33
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-background-busy-"));
|
|
34
|
+
const path = join(dir, "context.db");
|
|
35
|
+
const blocker = new Database(path);
|
|
36
|
+
const writer = new Database(path);
|
|
37
|
+
blocker.exec("PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT)");
|
|
38
|
+
writer.exec("PRAGMA busy_timeout=5000");
|
|
39
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
40
|
+
let callbacks = 0;
|
|
41
|
+
try {
|
|
42
|
+
const start = performance.now();
|
|
43
|
+
expect(() => writer.transaction(() => callbacks++)()).toThrow();
|
|
44
|
+
const elapsed = performance.now() - start;
|
|
45
|
+
expect(callbacks).toBe(0);
|
|
46
|
+
expect(elapsed).toBeGreaterThanOrEqual(4500);
|
|
47
|
+
expect(elapsed).toBeLessThan(7500);
|
|
48
|
+
} finally {
|
|
49
|
+
blocker.exec("ROLLBACK");
|
|
50
|
+
blocker.close();
|
|
51
|
+
writer.close();
|
|
52
|
+
rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}, 30000);
|
|
55
|
+
|
|
56
|
+
test("foreground retry scope crosses awaits, nests and excludes detached maintenance", async () => {
|
|
57
|
+
const db = new Database(":memory:");
|
|
58
|
+
const error = Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
|
|
59
|
+
const exec = spyOn(db, "exec").mockImplementation(() => {
|
|
60
|
+
throw error;
|
|
61
|
+
});
|
|
62
|
+
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
63
|
+
const acquire = () =>
|
|
64
|
+
withPrivilegedWriter(db, () => {
|
|
65
|
+
throw new Error("callback must not run");
|
|
66
|
+
});
|
|
67
|
+
let late: Promise<void> | undefined;
|
|
68
|
+
let release!: () => void;
|
|
69
|
+
const gate = new Promise<void>((resolve) => {
|
|
70
|
+
release = resolve;
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await withSqliteTransformPass(async () => {
|
|
74
|
+
await Promise.resolve();
|
|
75
|
+
expect(() => withSqliteTransformPass(acquire)).toThrow("after 3 attempts");
|
|
76
|
+
expect(exec).toHaveBeenCalledTimes(3);
|
|
77
|
+
exec.mockClear();
|
|
78
|
+
wait.mockClear();
|
|
79
|
+
withoutSqliteTransformPass(() => {
|
|
80
|
+
expect(acquire).toThrow("busy");
|
|
81
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(wait).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
exec.mockClear();
|
|
85
|
+
expect(acquire).toThrow("after 3 attempts");
|
|
86
|
+
expect(exec).toHaveBeenCalledTimes(3);
|
|
87
|
+
late = (async () => {
|
|
88
|
+
await gate;
|
|
89
|
+
exec.mockClear();
|
|
90
|
+
wait.mockClear();
|
|
91
|
+
expect(acquire).toThrow("busy");
|
|
92
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(wait).not.toHaveBeenCalled();
|
|
94
|
+
})();
|
|
95
|
+
});
|
|
96
|
+
release();
|
|
97
|
+
await late;
|
|
98
|
+
exec.mockClear();
|
|
99
|
+
expect(acquire).toThrow("busy");
|
|
100
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
101
|
+
} finally {
|
|
102
|
+
release();
|
|
103
|
+
exec.mockRestore();
|
|
104
|
+
wait.mockRestore();
|
|
105
|
+
db.close();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("throwing foreground passes cannot leak retry policy to later work", async () => {
|
|
110
|
+
const db = new Database(":memory:");
|
|
111
|
+
const exec = spyOn(db, "exec").mockImplementation(() => {
|
|
112
|
+
throw Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
|
|
113
|
+
});
|
|
114
|
+
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
115
|
+
try {
|
|
116
|
+
await expect(
|
|
117
|
+
withSqliteTransformPass(async () => {
|
|
118
|
+
throw new Error("pass failed");
|
|
119
|
+
}),
|
|
120
|
+
).rejects.toThrow("pass failed");
|
|
121
|
+
expect(() => withPrivilegedWriter(db, () => undefined)).toThrow("busy");
|
|
122
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(wait).not.toHaveBeenCalled();
|
|
124
|
+
} finally {
|
|
125
|
+
exec.mockRestore();
|
|
126
|
+
wait.mockRestore();
|
|
127
|
+
db.close();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("explicit deferred reads on a writable handle do not contend with the writer", () => {
|
|
132
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-deferred-read-"));
|
|
133
|
+
const path = join(dir, "context.db");
|
|
134
|
+
const writer = new Database(path);
|
|
135
|
+
const reader = new Database(path);
|
|
136
|
+
writer.exec(
|
|
137
|
+
"PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT); INSERT INTO result VALUES ('committed')",
|
|
138
|
+
);
|
|
139
|
+
reader.exec("PRAGMA busy_timeout=0");
|
|
140
|
+
writer.exec("BEGIN IMMEDIATE");
|
|
141
|
+
writer.exec("INSERT INTO result VALUES ('uncommitted')");
|
|
142
|
+
try {
|
|
143
|
+
const rows = withSqliteTransformPass(() =>
|
|
144
|
+
reader.transaction(() => reader.prepare("SELECT value FROM result").all()).deferred(),
|
|
145
|
+
);
|
|
146
|
+
expect(rows).toEqual([{ value: "committed" }]);
|
|
147
|
+
} finally {
|
|
148
|
+
writer.exec("ROLLBACK");
|
|
149
|
+
writer.close();
|
|
150
|
+
reader.close();
|
|
151
|
+
rmSync(dir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("an explicitly started foreground pass owns its lifetime independently", async () => {
|
|
156
|
+
const db = new Database(":memory:");
|
|
157
|
+
const exec = spyOn(db, "exec").mockImplementation(() => {
|
|
158
|
+
throw Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
|
|
159
|
+
});
|
|
160
|
+
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
161
|
+
let release!: () => void;
|
|
162
|
+
const gate = new Promise<void>((resolve) => {
|
|
163
|
+
release = resolve;
|
|
164
|
+
});
|
|
165
|
+
let child!: Promise<void>;
|
|
166
|
+
try {
|
|
167
|
+
await withSqliteTransformPass(async () => {
|
|
168
|
+
child = withSqliteTransformPass(async () => {
|
|
169
|
+
await gate;
|
|
170
|
+
expect(() => withPrivilegedWriter(db, () => undefined)).toThrow("after 3 attempts");
|
|
171
|
+
expect(exec).toHaveBeenCalledTimes(3);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
release();
|
|
175
|
+
await child;
|
|
176
|
+
} finally {
|
|
177
|
+
release();
|
|
178
|
+
exec.mockRestore();
|
|
179
|
+
wait.mockRestore();
|
|
180
|
+
db.close();
|
|
181
|
+
}
|
|
182
|
+
});
|