@cortexkit/opencode-magic-context 0.44.2 → 0.44.3
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/{conflict-warning-hook-1msyvb51.js → conflict-warning-hook-hmpzxep6.js} +3 -3
- package/dist/{derive-budgets-gd2270s0.js → derive-budgets-vjmvh6pz.js} +3 -1
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts +3 -0
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/message-index-async.d.ts +1 -1
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/session-project-storage.d.ts +5 -5
- package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
- package/dist/features/magic-context/storage-embedding-measurements.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/derive-budgets.d.ts +4 -0
- package/dist/hooks/magic-context/derive-budgets.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-host-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-persist.d.ts +1 -4
- package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
- package/dist/hooks/magic-context/producer-window-guard.d.ts +4 -1
- package/dist/hooks/magic-context/producer-window-guard.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +1 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts +6 -0
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -1
- package/dist/{index-r9vkp4n9.js → index-33q7s9y2.js} +1 -1
- package/dist/{index-egkfvhms.js → index-4an2p8qa.js} +80 -14
- package/dist/{index-18tjha8g.js → index-638hf49n.js} +1 -1
- package/dist/{index-stb5fg12.js → index-9y5p0d50.js} +31 -1
- package/dist/{index-esxfcz5z.js → index-dgg5y7er.js} +1 -1
- package/dist/{index-3x9caerx.js → index-nf9sk8sh.js} +3 -3
- package/dist/{index-gtzcmv90.js → index-q5p1aqvv.js} +5 -5
- package/dist/{index-40s1xz22.js → index-rdsaxnf2.js} +1 -1
- package/dist/{index-ttdn47aq.js → index-yhm60r53.js} +134 -49
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +453 -226
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-wmvgnd24.js → read-session-chunk-n072mcq4.js} +2 -2
- package/dist/{recomp-orchestrator-rk40ff15.js → recomp-orchestrator-19tr6zya.js} +6 -6
- package/dist/{send-session-notification-6xsrcxnp.js → send-session-notification-04hvd54c.js} +2 -2
- package/dist/shared/run-token-log.d.ts +13 -0
- package/dist/shared/run-token-log.d.ts.map +1 -0
- package/dist/shared/sqlite.d.ts +7 -0
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/{transform-postprocess-phase-t7xgdhdf.js → transform-postprocess-phase-z2mfc5w1.js} +6 -6
- package/dist/v2/hidden-completion.d.ts +7 -1
- 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-terminal-error.d.ts +8 -0
- package/dist/v2/hooks/hidden-terminal-error.d.ts.map +1 -0
- package/dist/v2/hooks/tools.d.ts +1 -1
- package/dist/v2/hooks/tools.d.ts.map +1 -1
- package/dist/v2/server.js +584 -153
- package/dist/{wrapup-orchestrator-h8tr10c6.js → wrapup-orchestrator-sy4m56a7.js} +7 -7
- package/package.json +1 -1
- package/src/config/latch-permanence-guard.test.ts +4 -0
- package/src/shared/data-path.test.ts +1 -1
- package/src/shared/models-dev-cache.test.ts +30 -1
- package/src/shared/run-token-log.test.ts +65 -0
- package/src/shared/run-token-log.ts +39 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +19 -11
- package/src/shared/sqlite-pass-scope.test.ts +115 -144
- package/src/shared/sqlite.ts +123 -20
- package/src/shared/storage-busy.test.ts +34 -57
- package/src/shared/write-transaction-attribution.test.ts +13 -11
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runManagedWrapup2
|
|
3
|
-
} from "./index-
|
|
4
|
-
import"./index-
|
|
5
|
-
import"./index-
|
|
3
|
+
} from "./index-nf9sk8sh.js";
|
|
4
|
+
import"./index-rdsaxnf2.js";
|
|
5
|
+
import"./index-yhm60r53.js";
|
|
6
6
|
import"./index-sd3egckz.js";
|
|
7
|
-
import"./index-
|
|
8
|
-
import"./index-
|
|
9
|
-
import"./index-
|
|
7
|
+
import"./index-9y5p0d50.js";
|
|
8
|
+
import"./index-33q7s9y2.js";
|
|
9
|
+
import"./index-dgg5y7er.js";
|
|
10
10
|
import"./index-y5145x8k.js";
|
|
11
|
-
import"./index-
|
|
11
|
+
import"./index-4an2p8qa.js";
|
|
12
12
|
import"./index-kr5qehbd.js";
|
|
13
13
|
import"./index-jdxxq8cz.js";
|
|
14
14
|
import"./index-kwe03yde.js";
|
package/package.json
CHANGED
|
@@ -53,6 +53,10 @@ const KNOWN_SLOTS: Record<string, KnownSlot> = {
|
|
|
53
53
|
classification: "VERDICT",
|
|
54
54
|
reason: "Correct: circuit state expires and a half-open probe re-evaluates the endpoint.",
|
|
55
55
|
},
|
|
56
|
+
"packages/plugin/src/features/magic-context/memory/project-identity.ts:homeProjectPermission": {
|
|
57
|
+
classification: "VERDICT",
|
|
58
|
+
reason: "Boot config publishes the home-project permission; the setter replaces it on another host initialization, while explicit resolution arguments override it.",
|
|
59
|
+
},
|
|
56
60
|
"packages/plugin/src/features/magic-context/memory/project-identity.ts:directoryFallbackCache":
|
|
57
61
|
{
|
|
58
62
|
classification: "VERDICT",
|
|
@@ -29,7 +29,7 @@ const savedEnv = {
|
|
|
29
29
|
describe("data-path", () => {
|
|
30
30
|
beforeEach(() => {
|
|
31
31
|
process.env.XDG_CACHE_HOME = undefined;
|
|
32
|
-
process.env.XDG_DATA_HOME =
|
|
32
|
+
process.env.XDG_DATA_HOME = process.env.MAGIC_CONTEXT_TEST_DATA_DIR;
|
|
33
33
|
delete process.env.MAGIC_CONTEXT_TEST_DATA_DIR;
|
|
34
34
|
delete process.env.NODE_ENV;
|
|
35
35
|
process.env.LOCALAPPDATA = undefined;
|
|
@@ -2,7 +2,14 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
resolveHistorianProducerLimits,
|
|
7
|
+
resolveKnownHistorianContextLimit,
|
|
8
|
+
} from "../hooks/magic-context/derive-budgets";
|
|
9
|
+
import {
|
|
10
|
+
historianProducerReserve,
|
|
11
|
+
producerInputTokenLimit,
|
|
12
|
+
} from "../hooks/magic-context/producer-window-guard";
|
|
6
13
|
import {
|
|
7
14
|
clearModelsDevCache,
|
|
8
15
|
getModelsDevCacheState,
|
|
@@ -231,6 +238,28 @@ describe("models-dev-cache (SDK-only)", () => {
|
|
|
231
238
|
expect(getSdkContextLimit("openai", "gpt-5.4-mini")).toBe(922000);
|
|
232
239
|
});
|
|
233
240
|
|
|
241
|
+
test("issue 567: production resolver keeps input cap separate from context reserve", async () => {
|
|
242
|
+
await refreshModelLimitsFromApi(
|
|
243
|
+
makeClient([
|
|
244
|
+
{
|
|
245
|
+
id: "auth-provider",
|
|
246
|
+
models: {
|
|
247
|
+
model: { limit: { context: 400_000, input: 272_000, output: 128_000 } },
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
]),
|
|
251
|
+
);
|
|
252
|
+
const resolved = getSdkContextLimit("auth-provider", "model", undefined, {
|
|
253
|
+
reservation: "none",
|
|
254
|
+
});
|
|
255
|
+
const limits = resolveHistorianProducerLimits("auth-provider/model");
|
|
256
|
+
const reserve = historianProducerReserve(limits.context, undefined, 128_000);
|
|
257
|
+
expect(resolved).toBe(272_000);
|
|
258
|
+
expect(limits).toEqual({ context: 400_000, input: 272_000 });
|
|
259
|
+
expect(reserve).toBe(100_000);
|
|
260
|
+
expect(producerInputTokenLimit(limits.context, reserve, limits.input)).toBe(263_840);
|
|
261
|
+
});
|
|
262
|
+
|
|
234
263
|
test("explicit SDK-resolved config limit wins over a larger catalog or detected value", async () => {
|
|
235
264
|
// OpenCode's config.providers() has already applied opencode.json over its
|
|
236
265
|
// catalog. The cache must preserve that 120K effective value, and a later
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { runTokenLog } from "./run-token-log";
|
|
3
|
+
|
|
4
|
+
describe("run token log host usage", () => {
|
|
5
|
+
test("OpenCode assistant message tokens retain reasoning and cache", () => {
|
|
6
|
+
const message = {
|
|
7
|
+
info: {
|
|
8
|
+
finish: "length",
|
|
9
|
+
tokens: { input: 10, output: 50, reasoning: 48, cache: { read: 7, write: 0 } },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
expect(runTokenLog(message.info.tokens, 64, message.info.finish)).toEqual({
|
|
13
|
+
input: 10,
|
|
14
|
+
output: 50,
|
|
15
|
+
reasoning: 48,
|
|
16
|
+
cache_read: 7,
|
|
17
|
+
cache_write: 0,
|
|
18
|
+
max_tokens: 64,
|
|
19
|
+
finish_reason: "length",
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("OpenCode hidden-completion usage preserves omitted reasoning as null", () => {
|
|
24
|
+
const row = {
|
|
25
|
+
data: {
|
|
26
|
+
finish: "stop",
|
|
27
|
+
tokens: { input: 20, output: 4, cache: { read: 0, write: 3 } },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
expect(runTokenLog(row.data.tokens, undefined, row.data.finish)).toEqual({
|
|
31
|
+
input: 20,
|
|
32
|
+
output: 4,
|
|
33
|
+
reasoning: null,
|
|
34
|
+
cache_read: 0,
|
|
35
|
+
cache_write: 3,
|
|
36
|
+
max_tokens: null,
|
|
37
|
+
finish_reason: "stop",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("Pi stdout usage and absent usage remain distinguishable", () => {
|
|
42
|
+
const message = {
|
|
43
|
+
stopReason: "length",
|
|
44
|
+
usage: { input: 3, output: 12, cacheRead: 2, cacheWrite: 0 },
|
|
45
|
+
};
|
|
46
|
+
expect(runTokenLog(message.usage, 12, message.stopReason)).toEqual({
|
|
47
|
+
input: 3,
|
|
48
|
+
output: 12,
|
|
49
|
+
reasoning: null,
|
|
50
|
+
cache_read: 2,
|
|
51
|
+
cache_write: 0,
|
|
52
|
+
max_tokens: 12,
|
|
53
|
+
finish_reason: "length",
|
|
54
|
+
});
|
|
55
|
+
expect(runTokenLog(undefined)).toEqual({
|
|
56
|
+
input: null,
|
|
57
|
+
output: null,
|
|
58
|
+
reasoning: null,
|
|
59
|
+
cache_read: null,
|
|
60
|
+
cache_write: null,
|
|
61
|
+
max_tokens: null,
|
|
62
|
+
finish_reason: null,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface RunTokenLog {
|
|
2
|
+
input: number | null;
|
|
3
|
+
output: number | null;
|
|
4
|
+
reasoning: number | null;
|
|
5
|
+
cache_read: number | null;
|
|
6
|
+
cache_write: number | null;
|
|
7
|
+
max_tokens: number | null;
|
|
8
|
+
finish_reason: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function count(value: unknown): number | null {
|
|
12
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Keep provider omissions distinct from measured zero in diagnostic logs. */
|
|
16
|
+
export function runTokenLog(tokens: unknown, maxTokens?: number, finish?: unknown): RunTokenLog {
|
|
17
|
+
const usage = tokens && typeof tokens === "object" ? (tokens as Record<string, unknown>) : {};
|
|
18
|
+
const cache =
|
|
19
|
+
usage.cache && typeof usage.cache === "object"
|
|
20
|
+
? (usage.cache as Record<string, unknown>)
|
|
21
|
+
: {};
|
|
22
|
+
return {
|
|
23
|
+
input: count(usage.input ?? usage.input_tokens),
|
|
24
|
+
output: count(usage.output ?? usage.output_tokens),
|
|
25
|
+
reasoning: count(usage.reasoning ?? usage.reasoning_tokens),
|
|
26
|
+
cache_read: count(
|
|
27
|
+
usage.cacheRead ?? usage.cache_read ?? usage.cached_input_tokens ?? cache.read,
|
|
28
|
+
),
|
|
29
|
+
cache_write: count(
|
|
30
|
+
usage.cacheWrite ?? usage.cache_write ?? usage.cache_write_tokens ?? cache.write,
|
|
31
|
+
),
|
|
32
|
+
max_tokens: count(maxTokens),
|
|
33
|
+
finish_reason: typeof finish === "string" ? finish : null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatRunTokenLog(tokens: RunTokenLog): string {
|
|
38
|
+
return `tokens=${JSON.stringify(tokens)}`;
|
|
39
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { expect,
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Database,
|
|
7
|
+
withAsyncPrivilegedWriter,
|
|
8
|
+
withPrivilegedWriter,
|
|
9
|
+
withSqliteTransformPass,
|
|
10
|
+
} from "./sqlite";
|
|
6
11
|
import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
|
|
7
12
|
|
|
8
13
|
for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
|
|
@@ -10,7 +15,9 @@ for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
|
|
|
10
15
|
const dir = mkdtempSync(join(tmpdir(), "mc-routed-acquisition-"));
|
|
11
16
|
const path = join(dir, "context.db");
|
|
12
17
|
const db = new Database(path);
|
|
13
|
-
db.exec(
|
|
18
|
+
db.exec(
|
|
19
|
+
"PRAGMA journal_mode=WAL; PRAGMA busy_timeout=50; CREATE TABLE result(value TEXT); CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
|
|
20
|
+
);
|
|
14
21
|
const locker = await startSqliteWriteLocker(path, 650);
|
|
15
22
|
let calls = 0;
|
|
16
23
|
const write = () => {
|
|
@@ -19,7 +26,8 @@ for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
|
|
|
19
26
|
return "managed";
|
|
20
27
|
};
|
|
21
28
|
try {
|
|
22
|
-
withSqliteTransformPass(() => {
|
|
29
|
+
await withSqliteTransformPass(async () => {
|
|
30
|
+
await withAsyncPrivilegedWriter(db, () => undefined);
|
|
23
31
|
if (mode === "literal") {
|
|
24
32
|
db.exec("BEGIN IMMEDIATE");
|
|
25
33
|
write();
|
|
@@ -43,7 +51,9 @@ for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
|
|
|
43
51
|
|
|
44
52
|
test("routed transactions preserve nesting, receiver, arguments and rollback without callback retries", () => {
|
|
45
53
|
const db = new Database(":memory:");
|
|
46
|
-
db.exec(
|
|
54
|
+
db.exec(
|
|
55
|
+
"CREATE TABLE result(value TEXT); CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
|
|
56
|
+
);
|
|
47
57
|
let calls = 0;
|
|
48
58
|
const transaction = db.transaction(function (this: { prefix: string }, value: string) {
|
|
49
59
|
calls++;
|
|
@@ -69,23 +79,21 @@ test("exhausted routed acquisition never enters the callback or multiplies privi
|
|
|
69
79
|
const path = join(dir, "context.db");
|
|
70
80
|
const blocker = new Database(path);
|
|
71
81
|
const db = new Database(path);
|
|
72
|
-
blocker.exec(
|
|
82
|
+
blocker.exec(
|
|
83
|
+
"PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT); CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
|
|
84
|
+
);
|
|
73
85
|
db.exec("PRAGMA busy_timeout=0");
|
|
74
86
|
blocker.exec("BEGIN IMMEDIATE");
|
|
75
|
-
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
76
87
|
let callbacks = 0;
|
|
77
88
|
try {
|
|
78
89
|
for (const run of [
|
|
79
90
|
() => db.transaction(() => callbacks++)(),
|
|
80
91
|
() => withPrivilegedWriter(db, () => callbacks++),
|
|
81
92
|
]) {
|
|
82
|
-
|
|
83
|
-
expect(() => withSqliteTransformPass(run)).toThrow("after 3 attempts");
|
|
84
|
-
expect(wait).toHaveBeenCalledTimes(2);
|
|
93
|
+
expect(() => withSqliteTransformPass(run)).toThrow("acquisition remained busy");
|
|
85
94
|
expect(callbacks).toBe(0);
|
|
86
95
|
}
|
|
87
96
|
} finally {
|
|
88
|
-
wait.mockRestore();
|
|
89
97
|
blocker.exec("ROLLBACK");
|
|
90
98
|
blocker.close();
|
|
91
99
|
db.close();
|
|
@@ -1,182 +1,153 @@
|
|
|
1
|
-
import { expect,
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import {
|
|
6
|
+
beginSqliteWriterAsync,
|
|
6
7
|
Database,
|
|
7
|
-
|
|
8
|
+
isTransientSqliteError,
|
|
9
|
+
SqliteAcquisitionBusyError,
|
|
10
|
+
withAsyncPrivilegedWriter,
|
|
8
11
|
withPrivilegedWriter,
|
|
12
|
+
withSqliteBackgroundWriter,
|
|
9
13
|
withSqliteTransformPass,
|
|
10
14
|
} from "./sqlite";
|
|
15
|
+
import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
function fixture() {
|
|
18
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-busy-yield-"));
|
|
19
|
+
const path = join(dir, "context.db");
|
|
20
|
+
const db = new Database(path);
|
|
21
|
+
db.exec(
|
|
22
|
+
"PRAGMA journal_mode=WAL; CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
|
|
23
|
+
);
|
|
24
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
25
|
+
return {
|
|
26
|
+
db,
|
|
27
|
+
path,
|
|
28
|
+
close: () => {
|
|
29
|
+
db.close();
|
|
30
|
+
rmSync(dir, { recursive: true, force: true });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("tool-style transaction outside a pass waits for a brief lock", async () => {
|
|
36
|
+
const { db, path, close } = fixture();
|
|
37
|
+
const locker = await startSqliteWriteLocker(path, 300);
|
|
20
38
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
const start = performance.now();
|
|
40
|
+
db.transaction(() =>
|
|
41
|
+
db.prepare("INSERT INTO context_privilege_state(id, enabled) VALUES (1, 0)").run(),
|
|
42
|
+
).immediate();
|
|
43
|
+
expect(performance.now() - start).toBeGreaterThanOrEqual(250);
|
|
44
|
+
expect(db.prepare("SELECT COUNT(*) AS count FROM context_privilege_state").get()).toEqual({
|
|
45
|
+
count: 1,
|
|
46
|
+
});
|
|
47
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
25
48
|
} finally {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
db.close();
|
|
49
|
+
await locker.exited;
|
|
50
|
+
close();
|
|
29
51
|
}
|
|
30
|
-
});
|
|
52
|
+
}, 30000);
|
|
31
53
|
|
|
32
|
-
test("background
|
|
33
|
-
const
|
|
34
|
-
const path = join(dir, "context.db");
|
|
54
|
+
test("named background acquisition gives up quickly and leaves work for retry", () => {
|
|
55
|
+
const { db, path, close } = fixture();
|
|
35
56
|
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
57
|
blocker.exec("BEGIN IMMEDIATE");
|
|
40
|
-
let
|
|
58
|
+
let calls = 0;
|
|
59
|
+
let retryQueued = false;
|
|
41
60
|
try {
|
|
42
61
|
const start = performance.now();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
try {
|
|
63
|
+
withSqliteBackgroundWriter(() => db.transaction(() => calls++)());
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (!isTransientSqliteError(error)) throw error;
|
|
66
|
+
retryQueued = true;
|
|
67
|
+
}
|
|
68
|
+
expect(performance.now() - start).toBeLessThan(250);
|
|
69
|
+
expect(retryQueued).toBe(true);
|
|
70
|
+
expect(calls).toBe(0);
|
|
71
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
48
72
|
} finally {
|
|
49
73
|
blocker.exec("ROLLBACK");
|
|
50
74
|
blocker.close();
|
|
51
|
-
|
|
52
|
-
rmSync(dir, { recursive: true, force: true });
|
|
75
|
+
close();
|
|
53
76
|
}
|
|
54
|
-
}
|
|
77
|
+
});
|
|
55
78
|
|
|
56
|
-
test("foreground
|
|
57
|
-
const db =
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
});
|
|
79
|
+
test("foreground admission keeps the loop ticking while a separate writer holds the lock", async () => {
|
|
80
|
+
const { db, path, close } = fixture();
|
|
81
|
+
const locker = await startSqliteWriteLocker(path, 650);
|
|
82
|
+
let timerFired = false;
|
|
83
|
+
let calls = 0;
|
|
72
84
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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);
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
timerFired = true;
|
|
87
|
+
}, 100);
|
|
88
|
+
await withSqliteTransformPass(() =>
|
|
89
|
+
withAsyncPrivilegedWriter(db, () => {
|
|
90
|
+
calls++;
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
expect(timerFired).toBe(true);
|
|
94
|
+
expect(calls).toBe(1);
|
|
95
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
101
96
|
} finally {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
wait.mockRestore();
|
|
105
|
-
db.close();
|
|
97
|
+
await locker.exited;
|
|
98
|
+
close();
|
|
106
99
|
}
|
|
107
|
-
});
|
|
100
|
+
}, 30000);
|
|
108
101
|
|
|
109
|
-
test("
|
|
110
|
-
const db =
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
102
|
+
test("a foreground in-pass writer has bounded 250ms tolerance and restores its timeout", () => {
|
|
103
|
+
const { db, path, close } = fixture();
|
|
104
|
+
const blocker = new Database(path);
|
|
105
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
115
106
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
expect(()
|
|
122
|
-
expect(
|
|
123
|
-
expect(wait).not.toHaveBeenCalled();
|
|
107
|
+
const start = performance.now();
|
|
108
|
+
expect(() =>
|
|
109
|
+
withSqliteTransformPass(() => withPrivilegedWriter(db, () => undefined)),
|
|
110
|
+
).toThrow(SqliteAcquisitionBusyError);
|
|
111
|
+
expect(performance.now() - start).toBeGreaterThanOrEqual(200);
|
|
112
|
+
expect(performance.now() - start).toBeLessThan(400);
|
|
113
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
124
114
|
} finally {
|
|
125
|
-
exec
|
|
126
|
-
|
|
127
|
-
|
|
115
|
+
blocker.exec("ROLLBACK");
|
|
116
|
+
blocker.close();
|
|
117
|
+
close();
|
|
128
118
|
}
|
|
129
119
|
});
|
|
130
120
|
|
|
131
|
-
test("
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
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')");
|
|
121
|
+
test("ready background publication retries on a timer and retains its write", async () => {
|
|
122
|
+
const { db, path, close } = fixture();
|
|
123
|
+
const locker = await startSqliteWriteLocker(path, 650);
|
|
124
|
+
let ticked = false;
|
|
142
125
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
);
|
|
146
|
-
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
ticked = true;
|
|
128
|
+
}, 100);
|
|
129
|
+
await beginSqliteWriterAsync(db, "historian-publish");
|
|
130
|
+
db.prepare("INSERT INTO context_privilege_state(id, enabled) VALUES (1, 0)").run();
|
|
131
|
+
db.exec("COMMIT");
|
|
132
|
+
expect(ticked).toBe(true);
|
|
133
|
+
expect(db.prepare("SELECT COUNT(*) AS count FROM context_privilege_state").get()).toEqual({
|
|
134
|
+
count: 1,
|
|
135
|
+
});
|
|
136
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
147
137
|
} finally {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
reader.close();
|
|
151
|
-
rmSync(dir, { recursive: true, force: true });
|
|
138
|
+
await locker.exited;
|
|
139
|
+
close();
|
|
152
140
|
}
|
|
153
|
-
});
|
|
141
|
+
}, 30000);
|
|
154
142
|
|
|
155
|
-
test("an
|
|
156
|
-
const db =
|
|
157
|
-
const
|
|
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>;
|
|
143
|
+
test("an in-pass BEGIN tolerates a sibling writer that releases within 250ms", async () => {
|
|
144
|
+
const { db, path, close } = fixture();
|
|
145
|
+
const locker = await startSqliteWriteLocker(path, 150);
|
|
166
146
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
await gate;
|
|
170
|
-
expect(() => withPrivilegedWriter(db, () => undefined)).toThrow("after 3 attempts");
|
|
171
|
-
expect(exec).toHaveBeenCalledTimes(3);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
release();
|
|
175
|
-
await child;
|
|
147
|
+
withSqliteTransformPass(() => withPrivilegedWriter(db, () => undefined));
|
|
148
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
176
149
|
} finally {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
wait.mockRestore();
|
|
180
|
-
db.close();
|
|
150
|
+
await locker.exited;
|
|
151
|
+
close();
|
|
181
152
|
}
|
|
182
|
-
});
|
|
153
|
+
}, 30000);
|