@retaw106/mmc-oc 2.2.1 → 2.4.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 +14 -3
- package/dist/config/deprecated-config.d.ts +4 -0
- package/dist/config/deprecated-config.d.ts.map +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +2 -2
- package/dist/config/schema/magic-context.d.ts +36 -3
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config-init.js +14 -3
- package/dist/{context-mrvm92n8.js → context-c72brxe5.js} +2410 -1801
- package/dist/{derive-budgets-rjjz5f7y.js → derive-budgets-s1p37t2g.js} +2 -2
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/historian-failure-reason.d.ts +7 -0
- package/dist/features/magic-context/historian-failure-reason.d.ts.map +1 -0
- 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 +21 -3
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +7 -1
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/pi-content-decisions.d.ts +7 -1
- package/dist/features/magic-context/pi-content-decisions.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +29 -0
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/storage-clone.d.ts +2 -0
- package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
- package/dist/features/magic-context/storage-compartment-history-version.d.ts +4 -0
- package/dist/features/magic-context/storage-compartment-history-version.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +1 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +23 -5
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts +7 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts +5 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tag-identity-revision.d.ts +50 -0
- package/dist/features/magic-context/storage-tag-identity-revision.d.ts.map +1 -0
- package/dist/features/magic-context/storage-tags.d.ts +159 -5
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/tagger.d.ts +2 -0
- package/dist/features/magic-context/tagger.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +60 -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/active-anthropic-turn.d.ts +2 -0
- package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -1
- package/dist/hooks/magic-context/apply-operations.d.ts +10 -1
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts +2 -2
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- 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.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/edit-marker.d.ts +2 -0
- package/dist/hooks/magic-context/edit-marker.d.ts.map +1 -1
- 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/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/history-boundary-repair.d.ts +11 -2
- package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-measured-request.d.ts +3 -1
- package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-persist.d.ts +10 -13
- package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts +62 -0
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +38 -0
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-replay.d.ts +16 -9
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +141 -2
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/opaque-thinking.d.ts +12 -0
- package/dist/hooks/magic-context/opaque-thinking.d.ts.map +1 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts +1 -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 +10 -0
- package/dist/hooks/magic-context/pending-materialization.d.ts.map +1 -0
- package/dist/hooks/magic-context/postprocess-read-cache.d.ts.map +1 -1
- package/dist/hooks/magic-context/provider-visible-parts.d.ts +51 -0
- package/dist/hooks/magic-context/provider-visible-parts.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts +22 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +3 -0
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-formatting.d.ts +5 -0
- package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +123 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/reasoning-budget.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/sentinel.d.ts +41 -1
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/strict-thinking-prefix.d.ts +8 -0
- package/dist/hooks/magic-context/strict-thinking-prefix.d.ts.map +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts +38 -24
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- 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 +29 -0
- package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-dates.d.ts +6 -0
- package/dist/hooks/magic-context/system-prompt-dates.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-delivery.d.ts +47 -0
- package/dist/hooks/magic-context/system-prompt-delivery.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +18 -5
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +19 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-owner-warmup.d.ts +14 -0
- package/dist/hooks/magic-context/tag-owner-warmup.d.ts.map +1 -0
- 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/token-count-exact.d.ts +35 -0
- package/dist/hooks/magic-context/token-count-exact.d.ts.map +1 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts +2 -0
- 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.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +9 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +21 -11
- 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-g2z2k5m4.js → index-27ddyx39.js} +2 -2
- package/dist/{index-btp5tdc8.js → index-48e9znrc.js} +1248 -570
- package/dist/{index-5cep2r9n.js → index-5ty9rnbx.js} +60 -258
- package/dist/{index-z2g20msd.js → index-azbmnekv.js} +4 -4
- package/dist/{index-1j34a56j.js → index-dwh99z66.js} +293 -36
- package/dist/{index-qbkzdkk0.js → index-k59af64t.js} +6 -2
- package/dist/{index-5b7x80ec.js → index-kahkxcqw.js} +12532 -10348
- package/dist/{index-53g38ggt.js → index-kmbme1v1.js} +1 -1
- package/dist/{index-amycb7b1.js → index-r7s7d350.js} +3 -3
- package/dist/{index-33gprt9g.js → index-yjv8dpht.js} +1 -1
- package/dist/index-ykq24x1n.js +1477 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +114 -2
- package/dist/{message-time-backfill-phrwp2fz.js → message-time-backfill-j96x1sab.js} +3 -3
- package/dist/model-resolution-cvyrrcjw.js +8 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-2xm4sc4y.js → read-session-chunk-t8bg6wkz.js} +6 -4
- package/dist/recomp-orchestrator-k760qy1n.js +21 -0
- package/dist/{safe-notification-target-ph3xd1ve.js → safe-notification-target-1jkf0m82.js} +2 -2
- package/dist/{send-session-notification-rnc4nrsr.js → send-session-notification-njjf3b4s.js} +5 -5
- package/dist/shared/auto-search-lifecycle.d.ts +14 -0
- package/dist/shared/auto-search-lifecycle.d.ts.map +1 -1
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/model-suggestion-retry.d.ts +21 -0
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/models-dev-temperature.d.ts +18 -0
- package/dist/shared/models-dev-temperature.d.ts.map +1 -0
- package/dist/shared/processed-image-eligibility.d.ts +22 -0
- package/dist/shared/processed-image-eligibility.d.ts.map +1 -0
- package/dist/shared/project-directory-key.d.ts +16 -0
- package/dist/shared/project-directory-key.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/recovery-reasoning-prefix.d.ts +9 -0
- package/dist/shared/recovery-reasoning-prefix.d.ts.map +1 -0
- package/dist/shared/sqlite.d.ts +35 -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 +9 -0
- package/dist/shared/storage-file-mutex.d.ts.map +1 -0
- package/dist/shared/storage-permissions.d.ts +6 -0
- package/dist/shared/storage-permissions.d.ts.map +1 -1
- package/dist/shared/subagent-temperature.d.ts +46 -0
- package/dist/shared/subagent-temperature.d.ts.map +1 -0
- package/dist/shared/tag-owner-id.d.ts +1 -0
- package/dist/shared/tag-owner-id.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts +9 -0
- package/dist/shared/tag-transcript.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/shared/window-geometry.d.ts +9 -66
- package/dist/shared/window-geometry.d.ts.map +1 -1
- package/dist/standalone-plugin-marker.d.ts +8 -0
- package/dist/standalone-plugin-marker.d.ts.map +1 -0
- package/dist/tools/ctx-expand/search-archive.d.ts +4 -0
- package/dist/tools/ctx-expand/search-archive.d.ts.map +1 -0
- package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/constants.d.ts +1 -2
- package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts +1 -1
- package/dist/tools/ctx-reduce/tools.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/copied-tag-notation.d.ts +19 -0
- package/dist/v2/hooks/copied-tag-notation.d.ts.map +1 -0
- package/dist/v2/hooks/generate.d.ts +9 -2
- 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/payload.d.ts.map +1 -1
- package/dist/v2/hooks/system-prompt-response.d.ts +5 -0
- package/dist/v2/hooks/system-prompt-response.d.ts.map +1 -0
- package/dist/v2/server.d.ts.map +1 -1
- package/dist/v2/server.js +26593 -21940
- package/dist/v2/store-reader.d.ts +27 -1
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/web/main.js +401 -23
- package/dist/{wrapup-orchestrator-yknks3ad.js → wrapup-orchestrator-b2gswatj.js} +10 -10
- package/package.json +1 -1
- package/src/config/deprecated-config.ts +18 -0
- package/src/config/index.test.ts +13 -37
- package/src/config/index.ts +2 -2
- package/src/config/latch-permanence-guard.test.ts +4 -0
- package/src/config/project-security.test.ts +17 -0
- package/src/config/project-security.ts +3 -3
- package/src/config/schema/magic-context.test.ts +50 -0
- package/src/config/schema/magic-context.ts +44 -7
- package/src/shared/auto-search-lifecycle.ts +43 -0
- package/src/shared/logger.ts +10 -2
- package/src/shared/model-suggestion-retry.ts +5 -1
- package/src/shared/models-dev-cache.ts +1 -7
- package/src/shared/models-dev-temperature.ts +107 -0
- package/src/shared/processed-image-eligibility.test.ts +68 -0
- package/src/shared/processed-image-eligibility.ts +41 -0
- package/src/shared/processed-image-position-review.json +20 -0
- package/src/shared/processed-image-position-review.test.ts +19 -0
- package/src/shared/project-directory-key.ts +20 -0
- 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/recovery-reasoning-prefix.test.ts +34 -0
- package/src/shared/recovery-reasoning-prefix.ts +35 -0
- package/src/shared/root-async-context.probe.mjs +36 -0
- package/src/shared/root-async-context.test.ts +49 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +4 -3
- package/src/shared/sqlite-busy-budget-review.test.ts +291 -0
- package/src/shared/sqlite-foreground-wait.test.ts +304 -0
- package/src/shared/sqlite-pass-scope.test.ts +4 -4
- package/src/shared/sqlite-writer-diagnostics.test.ts +52 -12
- package/src/shared/sqlite.ts +428 -46
- package/src/shared/stable-json.ts +79 -7
- package/src/shared/storage-file-mutex.test.ts +50 -0
- package/src/shared/storage-file-mutex.ts +130 -0
- package/src/shared/storage-permissions.ts +46 -0
- package/src/shared/subagent-temperature.ts +110 -0
- package/src/shared/tag-owner-id.ts +13 -0
- package/src/shared/tag-transcript.ts +118 -0
- 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/src/shared/window-geometry.test.ts +102 -236
- package/src/shared/window-geometry.ts +23 -413
- package/dist/index-ramz5kkm.js +0 -514
- package/dist/model-resolution-wazv26n0.js +0 -6
- package/dist/recomp-orchestrator-frmwj9hz.js +0 -21
- package/src/shared/__fixtures__/window-overlay.v1.json +0 -337
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
Database,
|
|
7
|
+
guardSqliteTransformPass,
|
|
8
|
+
SqliteAcquisitionBusyError,
|
|
9
|
+
withSqliteBackgroundWriter,
|
|
10
|
+
withSqliteTransformPass,
|
|
11
|
+
} from "./sqlite";
|
|
12
|
+
import { createTestTempDirFromPath } from "./test-temp-dir";
|
|
13
|
+
|
|
14
|
+
function fixture() {
|
|
15
|
+
const dir = createTestTempDirFromPath(join(tmpdir(), "mc-busy-review-"));
|
|
16
|
+
const path = join(dir, "context.db");
|
|
17
|
+
const db = new Database(path);
|
|
18
|
+
db.exec("PRAGMA busy_timeout=5000; CREATE TABLE result(id INTEGER PRIMARY KEY, n INTEGER)");
|
|
19
|
+
db.exec("INSERT INTO result VALUES (1, 0)");
|
|
20
|
+
return {
|
|
21
|
+
db,
|
|
22
|
+
path,
|
|
23
|
+
close() {
|
|
24
|
+
db.close();
|
|
25
|
+
rmSync(dir, { recursive: true, force: true });
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function lock(path: string, mode: "read" | "write" | "exclusive", holdMs: number) {
|
|
31
|
+
const child = Bun.spawn(
|
|
32
|
+
[
|
|
33
|
+
"bun",
|
|
34
|
+
"--eval",
|
|
35
|
+
`import { Database } from "bun:sqlite";
|
|
36
|
+
const db = new Database(process.argv[1]);
|
|
37
|
+
db.exec(process.argv[2] === "read" ? "BEGIN; SELECT * FROM result"
|
|
38
|
+
: process.argv[2] === "write" ? "BEGIN IMMEDIATE" : "BEGIN EXCLUSIVE");
|
|
39
|
+
console.log("LOCKED");
|
|
40
|
+
Bun.sleepSync(Number(process.argv[3]));
|
|
41
|
+
db.exec("COMMIT");
|
|
42
|
+
db.close();`,
|
|
43
|
+
path,
|
|
44
|
+
mode,
|
|
45
|
+
String(holdMs),
|
|
46
|
+
],
|
|
47
|
+
{ stdout: "pipe", stderr: "pipe", windowsHide: true },
|
|
48
|
+
);
|
|
49
|
+
const reader = child.stdout.getReader();
|
|
50
|
+
let output = "";
|
|
51
|
+
while (!output.includes("LOCKED\n")) {
|
|
52
|
+
const chunk = await reader.read();
|
|
53
|
+
if (chunk.done) {
|
|
54
|
+
throw new Error(`locker failed: ${await new Response(child.stderr).text()}`);
|
|
55
|
+
}
|
|
56
|
+
output += new TextDecoder().decode(chunk.value);
|
|
57
|
+
}
|
|
58
|
+
reader.releaseLock();
|
|
59
|
+
return { exited: child.exited };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const method of ["run", "get", "all", "exec"] as const) {
|
|
63
|
+
test(`a rollback-journal commit BUSY retries ${method} exactly once after the reader leaves`, async () => {
|
|
64
|
+
const { db, path, close } = fixture();
|
|
65
|
+
const sql = "UPDATE result SET n = n + 1 WHERE id = 1";
|
|
66
|
+
const statement = db.prepare(`${sql} RETURNING n`);
|
|
67
|
+
const { exited } = await lock(path, "read", 150);
|
|
68
|
+
try {
|
|
69
|
+
await withSqliteTransformPass(() => {
|
|
70
|
+
if (method === "exec") db.exec(sql);
|
|
71
|
+
else statement[method]();
|
|
72
|
+
});
|
|
73
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([{ id: 1, n: 1 }]);
|
|
74
|
+
expect(db.inTransaction).toBe(false);
|
|
75
|
+
} finally {
|
|
76
|
+
expect(await exited).toBe(0);
|
|
77
|
+
close();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const scope of ["foreground", "background", "unscoped"] as const) {
|
|
83
|
+
test(`exec in ${scope} does not wait on an unrelated attached database schema`, async () => {
|
|
84
|
+
const main = fixture();
|
|
85
|
+
const aux = fixture();
|
|
86
|
+
main.db.prepare("ATTACH DATABASE ? AS aux").run(aux.path);
|
|
87
|
+
const { exited } = await lock(aux.path, "exclusive", 2500);
|
|
88
|
+
try {
|
|
89
|
+
const started = performance.now();
|
|
90
|
+
const operation = () => main.db.exec("UPDATE main.result SET n = n + 1");
|
|
91
|
+
if (scope === "foreground") {
|
|
92
|
+
await withSqliteTransformPass(() => {
|
|
93
|
+
guardSqliteTransformPass({ assert() {}, remainingMs: () => 75 });
|
|
94
|
+
operation();
|
|
95
|
+
});
|
|
96
|
+
} else if (scope === "background") withSqliteBackgroundWriter(operation);
|
|
97
|
+
else operation();
|
|
98
|
+
expect(performance.now() - started).toBeLessThan(1500);
|
|
99
|
+
expect(main.db.prepare("SELECT n FROM main.result").get()).toEqual({ n: 1 });
|
|
100
|
+
} finally {
|
|
101
|
+
expect(await exited).toBe(0);
|
|
102
|
+
main.close();
|
|
103
|
+
aux.close();
|
|
104
|
+
}
|
|
105
|
+
}, 10_000);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const location of ["main", "aux"] as const) {
|
|
109
|
+
test(`BUSY retries leave single-statement effects atomic in ${location}, including AFTER triggers`, async () => {
|
|
110
|
+
const main = fixture();
|
|
111
|
+
const aux = fixture();
|
|
112
|
+
main.db.prepare("ATTACH DATABASE ? AS aux").run(aux.path);
|
|
113
|
+
main.db.exec("CREATE TABLE audit(n INTEGER)");
|
|
114
|
+
main.db.exec(`CREATE TEMP TRIGGER audit_insert AFTER INSERT ON ${location}.result
|
|
115
|
+
BEGIN INSERT INTO audit VALUES (new.n); END`);
|
|
116
|
+
main.db.exec(`CREATE TEMP TRIGGER audit_update AFTER UPDATE ON ${location}.result
|
|
117
|
+
BEGIN INSERT INTO audit VALUES (new.n); END`);
|
|
118
|
+
main.db.exec(`CREATE TEMP TRIGGER audit_delete AFTER DELETE ON ${location}.result
|
|
119
|
+
BEGIN INSERT INTO audit VALUES (old.n); END`);
|
|
120
|
+
const forms = [
|
|
121
|
+
`INSERT INTO ${location}.result VALUES (2, 10)`,
|
|
122
|
+
`INSERT OR IGNORE INTO ${location}.result VALUES (2, 20)`,
|
|
123
|
+
`INSERT OR REPLACE INTO ${location}.result VALUES (2, 30)`,
|
|
124
|
+
`INSERT INTO ${location}.result VALUES (2, 40) ON CONFLICT(id) DO UPDATE SET n=n+1`,
|
|
125
|
+
`UPDATE ${location}.result SET n=n+1 WHERE id=2`,
|
|
126
|
+
`DELETE FROM ${location}.result WHERE id=2`,
|
|
127
|
+
];
|
|
128
|
+
try {
|
|
129
|
+
for (const sql of forms) {
|
|
130
|
+
const statement = main.db.prepare(`${sql} RETURNING *`);
|
|
131
|
+
const blocker = new Database(location === "main" ? main.path : aux.path);
|
|
132
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
133
|
+
main.db.exec("PRAGMA busy_timeout=0");
|
|
134
|
+
const before = main.db
|
|
135
|
+
.prepare(`SELECT * FROM ${location}.result ORDER BY id`)
|
|
136
|
+
.all();
|
|
137
|
+
const beforeAudit = main.db.prepare("SELECT * FROM audit").all();
|
|
138
|
+
try {
|
|
139
|
+
expect(() => statement.all()).toThrow("locked");
|
|
140
|
+
expect(
|
|
141
|
+
main.db.prepare(`SELECT * FROM ${location}.result ORDER BY id`).all(),
|
|
142
|
+
).toEqual(before);
|
|
143
|
+
expect(main.db.prepare("SELECT * FROM audit").all()).toEqual(beforeAudit);
|
|
144
|
+
expect(main.db.inTransaction).toBe(false);
|
|
145
|
+
} finally {
|
|
146
|
+
blocker.exec("ROLLBACK");
|
|
147
|
+
blocker.close();
|
|
148
|
+
}
|
|
149
|
+
const { exited } = await lock(
|
|
150
|
+
location === "main" ? main.path : aux.path,
|
|
151
|
+
"write",
|
|
152
|
+
60,
|
|
153
|
+
);
|
|
154
|
+
try {
|
|
155
|
+
await withSqliteTransformPass(() => statement.all());
|
|
156
|
+
} finally {
|
|
157
|
+
expect(await exited).toBe(0);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
expect(main.db.prepare(`SELECT * FROM ${location}.result ORDER BY id`).all()).toEqual([
|
|
161
|
+
{ id: 1, n: 0 },
|
|
162
|
+
]);
|
|
163
|
+
expect(main.db.prepare("SELECT * FROM audit").all()).toEqual([
|
|
164
|
+
{ n: 10 },
|
|
165
|
+
{ n: 30 },
|
|
166
|
+
{ n: 31 },
|
|
167
|
+
{ n: 32 },
|
|
168
|
+
{ n: 32 },
|
|
169
|
+
]);
|
|
170
|
+
} finally {
|
|
171
|
+
main.close();
|
|
172
|
+
aux.close();
|
|
173
|
+
}
|
|
174
|
+
}, 10_000);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
test("an attached DML-only committed prefix is not replayed after a later BUSY", () => {
|
|
178
|
+
const main = fixture();
|
|
179
|
+
const free = fixture();
|
|
180
|
+
const blocked = fixture();
|
|
181
|
+
try {
|
|
182
|
+
main.db.prepare("ATTACH DATABASE ? AS free").run(free.path);
|
|
183
|
+
main.db.prepare("ATTACH DATABASE ? AS blocked").run(blocked.path);
|
|
184
|
+
blocked.db.exec("BEGIN IMMEDIATE");
|
|
185
|
+
withSqliteTransformPass(() => {
|
|
186
|
+
expect(() =>
|
|
187
|
+
main.db.exec("UPDATE free.result SET n=n+1; UPDATE blocked.result SET n=n+1"),
|
|
188
|
+
).toThrow(SqliteAcquisitionBusyError);
|
|
189
|
+
});
|
|
190
|
+
expect(main.db.prepare("SELECT n FROM free.result").get()).toEqual({ n: 1 });
|
|
191
|
+
expect(main.db.prepare("SELECT n FROM blocked.result").get()).toEqual({ n: 0 });
|
|
192
|
+
} finally {
|
|
193
|
+
blocked.db.exec("ROLLBACK");
|
|
194
|
+
main.close();
|
|
195
|
+
free.close();
|
|
196
|
+
blocked.close();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
for (const location of ["main", "free"] as const) {
|
|
201
|
+
test(`an ignored AUTOINCREMENT prefix in ${location} is committed only once before a later BUSY`, () => {
|
|
202
|
+
const main = fixture();
|
|
203
|
+
const free = fixture();
|
|
204
|
+
const blocked = fixture();
|
|
205
|
+
try {
|
|
206
|
+
main.db.prepare("ATTACH DATABASE ? AS free").run(free.path);
|
|
207
|
+
main.db.exec(`CREATE TABLE ${location}.numbered(id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT UNIQUE);
|
|
208
|
+
INSERT INTO ${location}.numbered(value) VALUES ('existing')`);
|
|
209
|
+
main.db.prepare("ATTACH DATABASE ? AS blocked").run(blocked.path);
|
|
210
|
+
blocked.db.exec("BEGIN IMMEDIATE");
|
|
211
|
+
const before = main.db.prepare("SELECT total_changes() AS n").get();
|
|
212
|
+
withSqliteTransformPass(() => {
|
|
213
|
+
guardSqliteTransformPass({ assert() {}, remainingMs: () => 50 });
|
|
214
|
+
expect(() =>
|
|
215
|
+
main.db.exec(
|
|
216
|
+
`INSERT OR IGNORE INTO ${location}.numbered(value) VALUES ('existing'); UPDATE blocked.result SET n=n+1`,
|
|
217
|
+
),
|
|
218
|
+
).toThrow(SqliteAcquisitionBusyError);
|
|
219
|
+
});
|
|
220
|
+
expect(main.db.prepare("SELECT total_changes() AS n").get()).toEqual(before);
|
|
221
|
+
expect(
|
|
222
|
+
main.db
|
|
223
|
+
.prepare(`SELECT seq FROM ${location}.sqlite_sequence WHERE name='numbered'`)
|
|
224
|
+
.get(),
|
|
225
|
+
).toEqual({ seq: 2 });
|
|
226
|
+
expect(main.db.prepare(`SELECT * FROM ${location}.numbered`).all()).toEqual([
|
|
227
|
+
{ id: 1, value: "existing" },
|
|
228
|
+
]);
|
|
229
|
+
} finally {
|
|
230
|
+
blocked.db.exec("ROLLBACK");
|
|
231
|
+
main.close();
|
|
232
|
+
free.close();
|
|
233
|
+
blocked.close();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
test("a WAL BUSY_SNAPSHOT in an explicit read transaction is not retried", () => {
|
|
239
|
+
const { db, path, close } = fixture();
|
|
240
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
241
|
+
const other = new Database(path);
|
|
242
|
+
try {
|
|
243
|
+
db.exec("BEGIN; SELECT * FROM result");
|
|
244
|
+
other.exec("UPDATE result SET n=n+1");
|
|
245
|
+
withSqliteTransformPass(() => {
|
|
246
|
+
expect(() => db.prepare("UPDATE result SET n=n+10").run()).toThrow("locked");
|
|
247
|
+
});
|
|
248
|
+
expect(db.inTransaction).toBe(true);
|
|
249
|
+
db.exec("ROLLBACK");
|
|
250
|
+
expect(db.prepare("SELECT n FROM result").get()).toEqual({ n: 1 });
|
|
251
|
+
} finally {
|
|
252
|
+
if (db.inTransaction) db.exec("ROLLBACK");
|
|
253
|
+
other.close();
|
|
254
|
+
close();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("a BUSY from an AFTER trigger writing an attached database rolls back the outer statement", async () => {
|
|
259
|
+
const main = fixture();
|
|
260
|
+
const aux = fixture();
|
|
261
|
+
main.db.prepare("ATTACH DATABASE ? AS aux").run(aux.path);
|
|
262
|
+
// The TEMP trigger resolves an unqualified table in main first; give the
|
|
263
|
+
// attached target its own name so the trigger must write the other file.
|
|
264
|
+
main.db.exec("ALTER TABLE aux.result RENAME TO attached_result");
|
|
265
|
+
main.db.exec(`CREATE TEMP TRIGGER update_aux AFTER UPDATE ON main.result
|
|
266
|
+
BEGIN UPDATE attached_result SET n=n+1 WHERE id=1; END`);
|
|
267
|
+
const statement = main.db.prepare("UPDATE main.result SET n=n+1 RETURNING n");
|
|
268
|
+
const blocker = new Database(aux.path);
|
|
269
|
+
try {
|
|
270
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
271
|
+
main.db.exec("PRAGMA busy_timeout=0");
|
|
272
|
+
expect(() => statement.all()).toThrow("locked");
|
|
273
|
+
expect(main.db.prepare("SELECT n FROM main.result").get()).toEqual({ n: 0 });
|
|
274
|
+
expect(main.db.prepare("SELECT n FROM aux.attached_result").get()).toEqual({ n: 0 });
|
|
275
|
+
expect(main.db.inTransaction).toBe(false);
|
|
276
|
+
blocker.exec("ROLLBACK");
|
|
277
|
+
const { exited } = await lock(aux.path, "write", 100);
|
|
278
|
+
try {
|
|
279
|
+
await withSqliteTransformPass(() => statement.all());
|
|
280
|
+
} finally {
|
|
281
|
+
expect(await exited).toBe(0);
|
|
282
|
+
}
|
|
283
|
+
expect(main.db.prepare("SELECT n FROM main.result").get()).toEqual({ n: 1 });
|
|
284
|
+
expect(main.db.prepare("SELECT n FROM aux.attached_result").get()).toEqual({ n: 1 });
|
|
285
|
+
} finally {
|
|
286
|
+
if (blocker.inTransaction) blocker.exec("ROLLBACK");
|
|
287
|
+
blocker.close();
|
|
288
|
+
main.close();
|
|
289
|
+
aux.close();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { log } from "./logger";
|
|
6
|
+
import {
|
|
7
|
+
Database,
|
|
8
|
+
guardSqliteTransformPass,
|
|
9
|
+
registerSqliteDiagnosticSink,
|
|
10
|
+
SqliteAcquisitionBusyError,
|
|
11
|
+
withPrivilegedWriter,
|
|
12
|
+
withSqliteTransformPass,
|
|
13
|
+
} from "./sqlite";
|
|
14
|
+
import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
|
|
15
|
+
import { createTestTempDirFromPath } from "./test-temp-dir";
|
|
16
|
+
|
|
17
|
+
function fixture() {
|
|
18
|
+
const dir = createTestTempDirFromPath(join(tmpdir(), "mc-foreground-wait-"));
|
|
19
|
+
const path = join(dir, "context.db");
|
|
20
|
+
const db = new Database(path);
|
|
21
|
+
db.exec(`
|
|
22
|
+
PRAGMA journal_mode=WAL;
|
|
23
|
+
PRAGMA busy_timeout=5000;
|
|
24
|
+
CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER);
|
|
25
|
+
CREATE TABLE result(value TEXT);
|
|
26
|
+
`);
|
|
27
|
+
return {
|
|
28
|
+
db,
|
|
29
|
+
path,
|
|
30
|
+
close() {
|
|
31
|
+
db.close();
|
|
32
|
+
rmSync(dir, { recursive: true, force: true });
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Upstream also drives two cases through the OC1 messages-transform handler
|
|
38
|
+
// (1 s hold served, 2.5 s hold refused with StorageBusyRefusalError). That
|
|
39
|
+
// adapter is deleted in this fork; the in-pass acquisition-wait behavior those
|
|
40
|
+
// cases cover is exercised here against the core primitives instead, and the
|
|
41
|
+
// 1 s/2.5 s boundaries are additionally covered by scripts/smoke-node-sqlite.ts.
|
|
42
|
+
for (const mode of ["BEGIN", "autocommit"]) {
|
|
43
|
+
test(`foreground ${mode} wait is capped by the pass deadline`, () => {
|
|
44
|
+
const { db, path, close } = fixture();
|
|
45
|
+
const blocker = new Database(path);
|
|
46
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
47
|
+
const diagnostics: string[] = [];
|
|
48
|
+
registerSqliteDiagnosticSink((line) => diagnostics.push(line));
|
|
49
|
+
try {
|
|
50
|
+
const started = performance.now();
|
|
51
|
+
withSqliteTransformPass(() => {
|
|
52
|
+
guardSqliteTransformPass({ assert() {}, remainingMs: () => 100 });
|
|
53
|
+
expect(() => {
|
|
54
|
+
if (mode === "BEGIN") withPrivilegedWriter(db, () => undefined);
|
|
55
|
+
else db.prepare("INSERT INTO result VALUES ('late')").run();
|
|
56
|
+
}).toThrow(SqliteAcquisitionBusyError);
|
|
57
|
+
});
|
|
58
|
+
expect(performance.now() - started).toBeGreaterThanOrEqual(75);
|
|
59
|
+
expect(performance.now() - started).toBeLessThan(700);
|
|
60
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
|
|
61
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
62
|
+
} finally {
|
|
63
|
+
registerSqliteDiagnosticSink(log);
|
|
64
|
+
blocker.exec("ROLLBACK");
|
|
65
|
+
blocker.close();
|
|
66
|
+
close();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fork shape: upstream drives the "slow uncontended SQL preserves admission"
|
|
72
|
+
// pair through the OC1 messages-transform handler (deleted in this fork — see
|
|
73
|
+
// the note above); the lease invariant it protects is asserted here against
|
|
74
|
+
// the core primitives instead.
|
|
75
|
+
test("uncontended autocommit work does not consume the foreground wait budget", async () => {
|
|
76
|
+
const { db, path, close } = fixture();
|
|
77
|
+
const blocker = new Database(path);
|
|
78
|
+
const timeouts: number[] = [];
|
|
79
|
+
const nativeExec = db.exec.bind(db);
|
|
80
|
+
const exec = spyOn(db, "exec").mockImplementation((sql) => {
|
|
81
|
+
const timeout = /^PRAGMA busy_timeout=(\d+)$/.exec(sql);
|
|
82
|
+
if (timeout) timeouts.push(Number(timeout[1]));
|
|
83
|
+
return nativeExec(sql);
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
await withSqliteTransformPass(async () => {
|
|
87
|
+
guardSqliteTransformPass({ assert() {}, remainingMs: () => 30_000 });
|
|
88
|
+
// Spend ~1.5s inside successful, uncontended SQL. Recursive CTEs
|
|
89
|
+
// keep this independent of CPU speed and UDF APIs.
|
|
90
|
+
const cte =
|
|
91
|
+
"WITH RECURSIVE n(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM n WHERE x<1000000)";
|
|
92
|
+
const slowWrite = db.prepare(
|
|
93
|
+
`${cte} INSERT INTO result SELECT sum(x) FROM n RETURNING value`,
|
|
94
|
+
);
|
|
95
|
+
const slowRead = db.prepare(`${cte} SELECT sum(x) FROM n`);
|
|
96
|
+
const started = performance.now();
|
|
97
|
+
do {
|
|
98
|
+
slowWrite.all();
|
|
99
|
+
slowRead.all();
|
|
100
|
+
} while (performance.now() - started < 1500);
|
|
101
|
+
// The contended retry must still see (nearly) the full budget.
|
|
102
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
103
|
+
timeouts.length = 0;
|
|
104
|
+
expect(() => db.prepare("INSERT INTO result VALUES ('blocked')").run()).toThrow(
|
|
105
|
+
SqliteAcquisitionBusyError,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
expect(timeouts[0]).toBe(0);
|
|
109
|
+
expect(timeouts[1]).toBeGreaterThanOrEqual(1900);
|
|
110
|
+
} finally {
|
|
111
|
+
exec.mockRestore();
|
|
112
|
+
if (blocker.inTransaction) blocker.exec("ROLLBACK");
|
|
113
|
+
blocker.close();
|
|
114
|
+
close();
|
|
115
|
+
}
|
|
116
|
+
}, 30_000);
|
|
117
|
+
|
|
118
|
+
test("BUSY autocommit probes do not double-apply INSERT OR IGNORE, UPSERT or RETURNING", async () => {
|
|
119
|
+
const { db, path, close } = fixture();
|
|
120
|
+
db.exec(
|
|
121
|
+
"CREATE TABLE counter(id INTEGER PRIMARY KEY, n INTEGER); INSERT INTO counter VALUES (1, 0)",
|
|
122
|
+
);
|
|
123
|
+
const forms = [
|
|
124
|
+
["INSERT OR IGNORE INTO counter VALUES (2, 1)", "run"],
|
|
125
|
+
["INSERT INTO counter VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET n = n + 1", "run"],
|
|
126
|
+
["UPDATE counter SET n = n + 1 WHERE id = 1 RETURNING n", "get"],
|
|
127
|
+
[
|
|
128
|
+
"INSERT INTO counter VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET n = n + 1 RETURNING n",
|
|
129
|
+
"all",
|
|
130
|
+
],
|
|
131
|
+
] as const;
|
|
132
|
+
try {
|
|
133
|
+
for (const [sql, method] of forms) {
|
|
134
|
+
const blocker = new Database(path);
|
|
135
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
136
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
137
|
+
const before = db.prepare("SELECT * FROM counter ORDER BY id").all();
|
|
138
|
+
try {
|
|
139
|
+
expect(() => db.prepare(sql)[method]()).toThrow("locked");
|
|
140
|
+
expect(db.prepare("SELECT * FROM counter ORDER BY id").all()).toEqual(before);
|
|
141
|
+
expect(db.inTransaction).toBe(false);
|
|
142
|
+
} finally {
|
|
143
|
+
blocker.exec("ROLLBACK");
|
|
144
|
+
blocker.close();
|
|
145
|
+
}
|
|
146
|
+
const locker = await startSqliteWriteLocker(path, 100);
|
|
147
|
+
try {
|
|
148
|
+
await withSqliteTransformPass(() => db.prepare(sql)[method]());
|
|
149
|
+
} finally {
|
|
150
|
+
expect(await locker.exited).toBe(0);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
expect(db.prepare("SELECT * FROM counter ORDER BY id").all()).toEqual([
|
|
154
|
+
{ id: 1, n: 3 },
|
|
155
|
+
{ id: 2, n: 1 },
|
|
156
|
+
]);
|
|
157
|
+
} finally {
|
|
158
|
+
close();
|
|
159
|
+
}
|
|
160
|
+
}, 30_000);
|
|
161
|
+
|
|
162
|
+
test("many contended autocommit writes share at most two seconds of lock waiting", async () => {
|
|
163
|
+
const { db, path, close } = fixture();
|
|
164
|
+
const blocker = new Database(path);
|
|
165
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
166
|
+
const timeouts: number[] = [];
|
|
167
|
+
const nativeExec = db.exec.bind(db);
|
|
168
|
+
const exec = spyOn(db, "exec").mockImplementation((sql) => {
|
|
169
|
+
const timeout = /^PRAGMA busy_timeout=(\d+)$/.exec(sql);
|
|
170
|
+
if (timeout) timeouts.push(Number(timeout[1]));
|
|
171
|
+
return nativeExec(sql);
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
await withSqliteTransformPass(() => {
|
|
175
|
+
// Use real BUSY failures but a controlled accounting clock: each
|
|
176
|
+
// retry costs 400ms regardless of runner scheduling. The deadline
|
|
177
|
+
// keeps the real waits short without hiding lease exhaustion.
|
|
178
|
+
guardSqliteTransformPass({ assert() {}, remainingMs: () => 10 });
|
|
179
|
+
let now = 0;
|
|
180
|
+
const clock = spyOn(performance, "now").mockImplementation(() => (now += 400));
|
|
181
|
+
try {
|
|
182
|
+
for (let i = 0; i < 8; i++) {
|
|
183
|
+
expect(() => db.prepare("INSERT INTO result VALUES ('one')").run()).toThrow(
|
|
184
|
+
SqliteAcquisitionBusyError,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
clock.mockRestore();
|
|
189
|
+
}
|
|
190
|
+
// Five charged retries exhaust 2000ms. All later retries must have
|
|
191
|
+
// zero timeout even though the deadline still offers another 10ms.
|
|
192
|
+
expect(timeouts).toEqual([
|
|
193
|
+
...Array.from({ length: 5 }, () => [0, 10, 5000]).flat(),
|
|
194
|
+
...Array.from({ length: 3 }, () => [0, 0, 5000]).flat(),
|
|
195
|
+
]);
|
|
196
|
+
blocker.exec("ROLLBACK");
|
|
197
|
+
db.prepare("INSERT INTO result VALUES ('uncontended')").run();
|
|
198
|
+
});
|
|
199
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([{ value: "uncontended" }]);
|
|
200
|
+
} finally {
|
|
201
|
+
exec.mockRestore();
|
|
202
|
+
if (blocker.inTransaction) blocker.exec("ROLLBACK");
|
|
203
|
+
blocker.close();
|
|
204
|
+
close();
|
|
205
|
+
}
|
|
206
|
+
}, 30_000);
|
|
207
|
+
|
|
208
|
+
test("an exec script charges its single attempt without a zero-wait probe", () => {
|
|
209
|
+
const { db, path, close } = fixture();
|
|
210
|
+
const blocker = new Database(path);
|
|
211
|
+
const timeouts: number[] = [];
|
|
212
|
+
const nativeExec = db.exec.bind(db);
|
|
213
|
+
const exec = spyOn(db, "exec").mockImplementation((sql) => {
|
|
214
|
+
const timeout = /^PRAGMA busy_timeout=(\d+)$/.exec(sql);
|
|
215
|
+
if (timeout) timeouts.push(Number(timeout[1]));
|
|
216
|
+
return nativeExec(sql);
|
|
217
|
+
});
|
|
218
|
+
try {
|
|
219
|
+
withSqliteTransformPass(() => {
|
|
220
|
+
let now = 0;
|
|
221
|
+
const clock = spyOn(performance, "now").mockImplementation(() => (now += 2500));
|
|
222
|
+
try {
|
|
223
|
+
db.exec(
|
|
224
|
+
"INSERT INTO result VALUES ('prefix'); INSERT INTO result VALUES ('suffix')",
|
|
225
|
+
);
|
|
226
|
+
} finally {
|
|
227
|
+
clock.mockRestore();
|
|
228
|
+
}
|
|
229
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
230
|
+
expect(() => db.prepare("INSERT INTO result VALUES ('blocked')").run()).toThrow(
|
|
231
|
+
SqliteAcquisitionBusyError,
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
expect(timeouts).toEqual([2000, 5000, 0, 0, 5000]);
|
|
235
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([
|
|
236
|
+
{ value: "prefix" },
|
|
237
|
+
{ value: "suffix" },
|
|
238
|
+
]);
|
|
239
|
+
} finally {
|
|
240
|
+
exec.mockRestore();
|
|
241
|
+
if (blocker.inTransaction) blocker.exec("ROLLBACK");
|
|
242
|
+
blocker.close();
|
|
243
|
+
close();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("an explicit transaction keeps its timeout and never probes or retries", () => {
|
|
248
|
+
const { db, path, close } = fixture();
|
|
249
|
+
const other = new Database(path);
|
|
250
|
+
db.exec("PRAGMA busy_timeout=0; BEGIN; SELECT * FROM result");
|
|
251
|
+
other.exec("INSERT INTO result VALUES ('other')");
|
|
252
|
+
const exec = spyOn(db, "exec");
|
|
253
|
+
try {
|
|
254
|
+
withSqliteTransformPass(() => {
|
|
255
|
+
expect(() => db.prepare("INSERT INTO result VALUES ('stale')").run()).toThrow("locked");
|
|
256
|
+
});
|
|
257
|
+
expect(exec).not.toHaveBeenCalled();
|
|
258
|
+
expect(db.inTransaction).toBe(true);
|
|
259
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 0 });
|
|
260
|
+
} finally {
|
|
261
|
+
exec.mockRestore();
|
|
262
|
+
if (db.inTransaction) db.exec("ROLLBACK");
|
|
263
|
+
other.close();
|
|
264
|
+
close();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("an autocommit exec script never replays a committed prefix after BUSY", () => {
|
|
269
|
+
const main = fixture();
|
|
270
|
+
const auxiliary = fixture();
|
|
271
|
+
const free = fixture();
|
|
272
|
+
try {
|
|
273
|
+
main.db.prepare("ATTACH DATABASE ? AS aux").run(auxiliary.path);
|
|
274
|
+
main.db.prepare("ATTACH DATABASE ? AS free").run(free.path);
|
|
275
|
+
auxiliary.db.exec("BEGIN IMMEDIATE");
|
|
276
|
+
withSqliteTransformPass(() => {
|
|
277
|
+
guardSqliteTransformPass({ assert() {}, remainingMs: () => 100 });
|
|
278
|
+
expect(() =>
|
|
279
|
+
main.db.exec(
|
|
280
|
+
"INSERT INTO main.result VALUES ('prefix'); INSERT INTO aux.result VALUES ('blocked')",
|
|
281
|
+
),
|
|
282
|
+
).toThrow(SqliteAcquisitionBusyError);
|
|
283
|
+
expect(() =>
|
|
284
|
+
main.db.exec(
|
|
285
|
+
"CREATE TABLE applied_once(id); INSERT INTO aux.result VALUES ('blocked')",
|
|
286
|
+
),
|
|
287
|
+
).toThrow(SqliteAcquisitionBusyError);
|
|
288
|
+
expect(() =>
|
|
289
|
+
main.db.exec(
|
|
290
|
+
"CREATE TABLE free.applied_once(id); INSERT INTO aux.result VALUES ('blocked')",
|
|
291
|
+
),
|
|
292
|
+
).toThrow(SqliteAcquisitionBusyError);
|
|
293
|
+
});
|
|
294
|
+
expect(main.db.prepare("SELECT * FROM main.result").all()).toEqual([{ value: "prefix" }]);
|
|
295
|
+
expect(
|
|
296
|
+
main.db.prepare("SELECT name FROM sqlite_schema WHERE name='applied_once'").all(),
|
|
297
|
+
).toHaveLength(1);
|
|
298
|
+
} finally {
|
|
299
|
+
auxiliary.db.exec("ROLLBACK");
|
|
300
|
+
auxiliary.close();
|
|
301
|
+
main.close();
|
|
302
|
+
free.close();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
@@ -100,7 +100,7 @@ test("foreground admission keeps the loop ticking while a separate writer holds
|
|
|
100
100
|
}
|
|
101
101
|
}, 30000);
|
|
102
102
|
|
|
103
|
-
test("a foreground in-pass writer has bounded
|
|
103
|
+
test("a foreground in-pass writer has bounded 2000ms tolerance and restores its timeout", () => {
|
|
104
104
|
const { db, path, close } = fixture();
|
|
105
105
|
const blocker = new Database(path);
|
|
106
106
|
blocker.exec("BEGIN IMMEDIATE");
|
|
@@ -109,15 +109,15 @@ test("a foreground in-pass writer has bounded 250ms tolerance and restores its t
|
|
|
109
109
|
expect(() =>
|
|
110
110
|
withSqliteTransformPass(() => withPrivilegedWriter(db, () => undefined)),
|
|
111
111
|
).toThrow(SqliteAcquisitionBusyError);
|
|
112
|
-
expect(performance.now() - start).toBeGreaterThanOrEqual(
|
|
113
|
-
expect(performance.now() - start).toBeLessThan(
|
|
112
|
+
expect(performance.now() - start).toBeGreaterThanOrEqual(1900);
|
|
113
|
+
expect(performance.now() - start).toBeLessThan(2500);
|
|
114
114
|
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
115
115
|
} finally {
|
|
116
116
|
blocker.exec("ROLLBACK");
|
|
117
117
|
blocker.close();
|
|
118
118
|
close();
|
|
119
119
|
}
|
|
120
|
-
});
|
|
120
|
+
}, 30000);
|
|
121
121
|
|
|
122
122
|
test("ready background publication retries on a timer and retains its write", async () => {
|
|
123
123
|
const { db, path, close } = fixture();
|