@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
1
|
+
import { expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
withPrivilegedWriter,
|
|
12
12
|
withSqliteTransformPass,
|
|
13
13
|
} from "./sqlite";
|
|
14
|
+
import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
|
|
14
15
|
import { createTestTempDirFromPath } from "./test-temp-dir";
|
|
15
16
|
|
|
16
17
|
function fixture() {
|
|
@@ -66,3 +67,238 @@ for (const mode of ["BEGIN", "autocommit"]) {
|
|
|
66
67
|
}
|
|
67
68
|
});
|
|
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
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Database as NativeDatabase } from "bun:sqlite";
|
|
2
|
+
import { expect, spyOn, test } from "bun:test";
|
|
3
|
+
import { rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
createSqliteTurnDatabase,
|
|
8
|
+
Database,
|
|
9
|
+
hasSqliteTurnDeadlineRouting,
|
|
10
|
+
withoutSqliteTransformPass,
|
|
11
|
+
withSqliteBackgroundWriter,
|
|
12
|
+
withSqliteTransformPass,
|
|
13
|
+
withSqliteTurnDeadline,
|
|
14
|
+
} from "./sqlite";
|
|
15
|
+
import { createTestTempDirFromPath } from "./test-temp-dir";
|
|
16
|
+
|
|
17
|
+
async function fixture(run: (db: Database, path: string) => Promise<void>) {
|
|
18
|
+
const root = createTestTempDirFromPath(join(tmpdir(), "sqlite-turn-deadline-"));
|
|
19
|
+
const path = join(root, "context.db");
|
|
20
|
+
const db = createSqliteTurnDatabase(path);
|
|
21
|
+
db.exec(
|
|
22
|
+
"PRAGMA journal_mode=DELETE; PRAGMA busy_timeout=900; CREATE TABLE t(value); INSERT INTO t VALUES(1)",
|
|
23
|
+
);
|
|
24
|
+
try {
|
|
25
|
+
await run(db, path);
|
|
26
|
+
} finally {
|
|
27
|
+
db.close();
|
|
28
|
+
rmSync(root, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function hold(path: string, reader = false) {
|
|
33
|
+
const child = Bun.spawn(
|
|
34
|
+
[
|
|
35
|
+
process.execPath,
|
|
36
|
+
"-e",
|
|
37
|
+
`
|
|
38
|
+
const { Database } = require('bun:sqlite');
|
|
39
|
+
const db = new Database(process.argv[1]);
|
|
40
|
+
db.exec(${JSON.stringify(reader ? "BEGIN; SELECT * FROM t" : "BEGIN EXCLUSIVE")});
|
|
41
|
+
console.log('HELD');
|
|
42
|
+
setInterval(() => {}, 1000);
|
|
43
|
+
`,
|
|
44
|
+
path,
|
|
45
|
+
],
|
|
46
|
+
{ stdout: "pipe", stderr: "pipe", windowsHide: true },
|
|
47
|
+
);
|
|
48
|
+
const output = child.stdout.getReader();
|
|
49
|
+
const first = await output.read();
|
|
50
|
+
output.releaseLock();
|
|
51
|
+
if (!new TextDecoder().decode(first.value).includes("HELD")) {
|
|
52
|
+
child.kill();
|
|
53
|
+
throw new Error(`holder failed: ${await new Response(child.stderr).text()}`);
|
|
54
|
+
}
|
|
55
|
+
return async () => {
|
|
56
|
+
child.kill();
|
|
57
|
+
await child.exited;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function timeout(db: Database): number {
|
|
62
|
+
return (db.prepare("PRAGMA busy_timeout").get() as { timeout: number }).timeout;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function bounded(operation: () => unknown) {
|
|
66
|
+
const start = performance.now();
|
|
67
|
+
expect(() =>
|
|
68
|
+
withSqliteTurnDeadline(() => Math.max(0, 80 - (performance.now() - start)), operation),
|
|
69
|
+
).toThrow(/locked|busy/i);
|
|
70
|
+
expect(performance.now() - start).toBeLessThan(400);
|
|
71
|
+
expect(performance.now() - start).toBeGreaterThan(40);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test("ordinary connections keep native cached SELECT methods and do not opt in", () => {
|
|
75
|
+
const db = new Database(":memory:");
|
|
76
|
+
const native = new NativeDatabase(":memory:");
|
|
77
|
+
try {
|
|
78
|
+
expect(hasSqliteTurnDeadlineRouting(db)).toBe(false);
|
|
79
|
+
expect(db.prepare("SELECT 1").get).toBe(native.prepare("SELECT 1").get);
|
|
80
|
+
} finally {
|
|
81
|
+
db.close();
|
|
82
|
+
native.close();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("native deadline timeout is read once, not on every cached SELECT", () => {
|
|
87
|
+
let reads = 0;
|
|
88
|
+
const prepare = Database.prototype.prepare;
|
|
89
|
+
const spy = spyOn(Database.prototype, "prepare").mockImplementation(function (
|
|
90
|
+
this: Database,
|
|
91
|
+
sql: string,
|
|
92
|
+
) {
|
|
93
|
+
const statement = prepare.call(this, sql);
|
|
94
|
+
if (sql === "PRAGMA busy_timeout") {
|
|
95
|
+
const get = statement.get.bind(statement);
|
|
96
|
+
statement.get = (...args: unknown[]) => {
|
|
97
|
+
reads++;
|
|
98
|
+
return get(...args);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return statement;
|
|
102
|
+
});
|
|
103
|
+
let db: Database | undefined;
|
|
104
|
+
try {
|
|
105
|
+
db = createSqliteTurnDatabase(":memory:");
|
|
106
|
+
db.exec("PRAGMA busy_timeout=900");
|
|
107
|
+
const read = db.prepare("SELECT 1 AS value");
|
|
108
|
+
withSqliteTurnDeadline(
|
|
109
|
+
() => 80,
|
|
110
|
+
() => {
|
|
111
|
+
for (let i = 0; i < 100; i++) expect(read.get()).toEqual({ value: 1 });
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
expect(reads).toBe(1);
|
|
115
|
+
} finally {
|
|
116
|
+
db?.close();
|
|
117
|
+
spy.mockRestore();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
for (const method of ["get", "all", "run", "values", "iterate"] as const) {
|
|
122
|
+
test(`turn deadline bounds cached SELECT ${method} and restores timeout`, async () => {
|
|
123
|
+
await fixture(async (db, path) => {
|
|
124
|
+
expect(hasSqliteTurnDeadlineRouting(db)).toBe(true);
|
|
125
|
+
const statement: any = db.prepare("SELECT * FROM t");
|
|
126
|
+
const release = await hold(path);
|
|
127
|
+
try {
|
|
128
|
+
bounded(() =>
|
|
129
|
+
method === "iterate" ? statement.iterate().next() : statement[method](),
|
|
130
|
+
);
|
|
131
|
+
expect(timeout(db)).toBe(900);
|
|
132
|
+
} finally {
|
|
133
|
+
await release();
|
|
134
|
+
}
|
|
135
|
+
expect(statement.get()).toEqual({ value: 1 });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
test("turn deadline bounds exec, prepare, deferred reads and COMMIT", async () => {
|
|
141
|
+
await fixture(async (db, path) => {
|
|
142
|
+
const read = db.prepare("SELECT * FROM t");
|
|
143
|
+
const fresh = createSqliteTurnDatabase(path);
|
|
144
|
+
fresh.exec("PRAGMA busy_timeout=900");
|
|
145
|
+
const release = await hold(path);
|
|
146
|
+
try {
|
|
147
|
+
bounded(() => db.exec("SELECT * FROM t"));
|
|
148
|
+
bounded(() => fresh.prepare("SELECT * FROM t"));
|
|
149
|
+
db.exec("BEGIN DEFERRED");
|
|
150
|
+
bounded(() => read.get());
|
|
151
|
+
db.exec("ROLLBACK");
|
|
152
|
+
} finally {
|
|
153
|
+
fresh.close();
|
|
154
|
+
await release();
|
|
155
|
+
}
|
|
156
|
+
db.exec("BEGIN IMMEDIATE; UPDATE t SET value=2");
|
|
157
|
+
const releaseReader = await hold(path, true);
|
|
158
|
+
try {
|
|
159
|
+
bounded(() => db.exec("COMMIT"));
|
|
160
|
+
expect(db.inTransaction).toBe(true);
|
|
161
|
+
expect(timeout(db)).toBe(900);
|
|
162
|
+
db.exec("ROLLBACK");
|
|
163
|
+
} finally {
|
|
164
|
+
await releaseReader();
|
|
165
|
+
}
|
|
166
|
+
expect(read.get()).toEqual({ value: 1 });
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("timeout mirror follows setters, prepare-time effects and failed script prefixes", async () => {
|
|
171
|
+
await fixture(async (db, path) => {
|
|
172
|
+
db.exec(
|
|
173
|
+
"PRAGMA main.busy_timeout(850); SELECT '; PRAGMA busy_timeout=0;'; -- PRAGMA busy_timeout=1",
|
|
174
|
+
);
|
|
175
|
+
const setter = db.prepare("PRAGMA busy_timeout=800");
|
|
176
|
+
expect(timeout(db)).toBe(800);
|
|
177
|
+
const success = () =>
|
|
178
|
+
withSqliteTurnDeadline(
|
|
179
|
+
() => 80,
|
|
180
|
+
() => db.prepare("SELECT 1").get(),
|
|
181
|
+
);
|
|
182
|
+
success();
|
|
183
|
+
expect(timeout(db)).toBe(800);
|
|
184
|
+
db.exec("PRAGMA busy_timeout=750");
|
|
185
|
+
setter.get();
|
|
186
|
+
const before = timeout(db);
|
|
187
|
+
success();
|
|
188
|
+
expect(timeout(db)).toBe(before);
|
|
189
|
+
const iterator = db.prepare("PRAGMA busy_timeout=600").iterate();
|
|
190
|
+
db.exec("PRAGMA busy_timeout=500");
|
|
191
|
+
iterator.next();
|
|
192
|
+
const after = timeout(db);
|
|
193
|
+
success();
|
|
194
|
+
expect(timeout(db)).toBe(after);
|
|
195
|
+
iterator.return?.();
|
|
196
|
+
expect(() => db.exec("PRAGMA busy_timeout=700; SELECT * FROM missing_table")).toThrow();
|
|
197
|
+
const read = db.prepare("SELECT * FROM t");
|
|
198
|
+
const release = await hold(path);
|
|
199
|
+
try {
|
|
200
|
+
bounded(() => read.get());
|
|
201
|
+
expect(timeout(db)).toBe(700);
|
|
202
|
+
} finally {
|
|
203
|
+
await release();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("deadline leaves zero-wait probes and existing writer acquisition semantics intact", async () => {
|
|
209
|
+
await fixture(async (db, path) => {
|
|
210
|
+
const release = await hold(path);
|
|
211
|
+
try {
|
|
212
|
+
bounded(() => withSqliteTransformPass(() => db.exec("BEGIN IMMEDIATE")));
|
|
213
|
+
expect(timeout(db)).toBe(900);
|
|
214
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
215
|
+
const start = performance.now();
|
|
216
|
+
expect(() =>
|
|
217
|
+
withSqliteTurnDeadline(
|
|
218
|
+
() => 80,
|
|
219
|
+
() => db.exec("SELECT * FROM t"),
|
|
220
|
+
),
|
|
221
|
+
).toThrow();
|
|
222
|
+
expect(performance.now() - start).toBeLessThan(100);
|
|
223
|
+
expect(timeout(db)).toBe(0);
|
|
224
|
+
} finally {
|
|
225
|
+
await release();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
for (const detach of [withoutSqliteTransformPass, withSqliteBackgroundWriter]) {
|
|
231
|
+
test(`detached native reads retain configured timeout (${detach.name})`, async () => {
|
|
232
|
+
await fixture(async (db, path) => {
|
|
233
|
+
const read = db.prepare("SELECT * FROM t");
|
|
234
|
+
const release = await hold(path);
|
|
235
|
+
try {
|
|
236
|
+
const start = performance.now();
|
|
237
|
+
expect(() =>
|
|
238
|
+
withSqliteTurnDeadline(
|
|
239
|
+
() => 0,
|
|
240
|
+
() => detach(() => read.get()),
|
|
241
|
+
),
|
|
242
|
+
).toThrow();
|
|
243
|
+
expect(performance.now() - start).toBeGreaterThan(700);
|
|
244
|
+
expect(timeout(db)).toBe(900);
|
|
245
|
+
} finally {
|
|
246
|
+
await release();
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const outcome of ["success", "throw", "reject"] as const) {
|
|
253
|
+
test(`completed native lease stops consulting request callback (${outcome})`, async () => {
|
|
254
|
+
const db = createSqliteTurnDatabase(":memory:");
|
|
255
|
+
let reads = 0;
|
|
256
|
+
let descendant!: () => unknown;
|
|
257
|
+
let resume!: () => void;
|
|
258
|
+
const gate = new Promise<void>((resolve) => {
|
|
259
|
+
resume = resolve;
|
|
260
|
+
});
|
|
261
|
+
let pending!: Promise<unknown>;
|
|
262
|
+
try {
|
|
263
|
+
try {
|
|
264
|
+
await withSqliteTurnDeadline(
|
|
265
|
+
() => {
|
|
266
|
+
reads++;
|
|
267
|
+
return 80;
|
|
268
|
+
},
|
|
269
|
+
() => {
|
|
270
|
+
pending = gate.then(() => descendant());
|
|
271
|
+
descendant = () => db.prepare("SELECT 1").get();
|
|
272
|
+
if (outcome === "throw") throw new Error("sync failure");
|
|
273
|
+
if (outcome === "reject") return Promise.reject(new Error("async failure"));
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
expect(String(error)).toContain("failure");
|
|
278
|
+
}
|
|
279
|
+
resume();
|
|
280
|
+
await pending;
|
|
281
|
+
expect(reads).toBe(0);
|
|
282
|
+
} finally {
|
|
283
|
+
db.close();
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|