@cortexkit/opencode-magic-context 0.49.7 → 0.49.8
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/auto-search-worker.js +6 -6
- package/dist/compaction-marker-candidates-worker.js +2 -2
- package/dist/{compaction-marker-candidates-a0edbqrr.js → compaction-marker-candidates-xxz35vwp.js} +3 -3
- package/dist/{conflict-warning-hook-myk5hbt9.js → conflict-warning-hook-w3crzrar.js} +8 -8
- package/dist/{context-ffr7e0g7.js → context-x0ccfv0f.js} +65 -42
- package/dist/{derive-budgets-j4ry8j8n.js → derive-budgets-3nbxsyss.js} +2 -2
- package/dist/embedding-backfill-3h2m5n0q.js +21 -0
- package/dist/embedding-worker-process.js +6 -2
- package/dist/embedding-worker.js +6 -5
- package/dist/features/magic-context/compaction-marker-candidates.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/cron.d.ts +2 -0
- package/dist/features/magic-context/dreamer/cron.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/provider-output-failure.d.ts +1 -0
- package/dist/features/magic-context/dreamer/provider-output-failure.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-worker-client.d.ts.map +1 -1
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/migration-worker-client.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/wake-plane.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/history-owner-ty29fz0f.js +244 -0
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-worker-client.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts.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/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts +1 -0
- package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
- package/dist/hooks/magic-context/raw-ordinal-warmup.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +8 -4
- package/dist/hooks/magic-context/read-session-chunk.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/rust-refusal-recovery.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- 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/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
- package/dist/{index-g8qbdb03.js → index-0jz47061.js} +6 -3
- package/dist/{index-4mjhqadz.js → index-150zz6fh.js} +1 -1
- package/dist/{index-j58gqrn2.js → index-2rw244rh.js} +31 -24
- package/dist/{index-0gyq3djm.js → index-4hb8nt93.js} +3 -3
- package/dist/{index-ax2jhztn.js → index-7bjgyaj1.js} +4 -4
- package/dist/{index-h2wc01qk.js → index-8kk7yh6m.js} +2 -2
- package/dist/{index-n5a170gs.js → index-9g1c4bt9.js} +81 -58
- package/dist/{index-w6bd03dc.js → index-a43xq1pg.js} +5 -5
- package/dist/{index-tqf7yg27.js → index-a4b0gbyg.js} +1 -1
- package/dist/{index-37q80veb.js → index-angx1s0k.js} +4 -3
- package/dist/{index-wq16ahsn.js → index-b4gcb3rk.js} +8 -8
- package/dist/{index-z1tjp257.js → index-b64n4d0h.js} +1 -1
- package/dist/{index-1bt88epp.js → index-bd9cjab3.js} +1 -1
- package/dist/{index-gbmqa9x9.js → index-bmwfzz28.js} +9 -6
- package/dist/{index-qx53vezw.js → index-gjvjh960.js} +1 -1
- package/dist/{index-ebnngykq.js → index-gzg9ekc1.js} +19 -18
- package/dist/{index-5tbvdff4.js → index-hxrvx6cy.js} +79 -40
- package/dist/{index-w23dh319.js → index-jw4gbfkv.js} +1 -1
- package/dist/index-knvkbfvh.js +25 -0
- package/dist/{index-vkbqdnkj.js → index-kvw6xew3.js} +6 -3
- package/dist/{index-rzmjkdd6.js → index-mp2kghkv.js} +3 -27
- package/dist/{index-nsjyysj0.js → index-mvvephtj.js} +5 -1
- package/dist/{index-9yyqq3mm.js → index-n78avt2p.js} +1 -1
- package/dist/{index-vt4sf4ep.js → index-pgy3h49n.js} +4 -4
- package/dist/{index-w6vr754t.js → index-pjgzdezw.js} +13 -9
- package/dist/{v2/server-1rfgbjfn.js → index-q28hsbpg.js} +15 -3
- package/dist/{index-a8acx4gc.js → index-q76br5zn.js} +3 -3
- package/dist/{index-j5hce1ab.js → index-qgp5vnt4.js} +5 -5
- package/dist/{index-sf94at64.js → index-rg1cezmj.js} +22 -22
- package/dist/{index-sh91t19a.js → index-s178h9z8.js} +4 -3
- package/dist/{index-tagss15p.js → index-w1ejb9qr.js} +105 -58
- package/dist/{index-k6xvbj2e.js → index-w28y7d0z.js} +2 -0
- package/dist/{index-j8epsc0g.js → index-wafzvyhg.js} +33 -10
- package/dist/{index-rwmpkss5.js → index-xf6k8szp.js} +4 -4
- package/dist/{index-tmtty2x5.js → index-xpn05jxq.js} +4 -4
- package/dist/{index-n7kwfw2r.js → index-ydjgz36f.js} +7 -7
- package/dist/{index-4btknbqh.js → index-zf9nydbd.js} +48 -38
- package/dist/index.js +51 -50
- package/dist/{logger-ewak5q1s.js → logger-wn328tfg.js} +1 -1
- package/dist/{message-index-bounded-04rycpx4.js → message-index-bounded-hxjp56qk.js} +16 -16
- package/dist/{message-index-rqtse28y.js → message-index-zs882h8e.js} +16 -16
- package/dist/{message-time-backfill-a5ytnp53.js → message-time-backfill-nb5hhnrt.js} +3 -3
- package/dist/migration-worker.js +11 -11
- package/dist/plugin/boot-quiet.d.ts.map +1 -1
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +2 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{project-embedding-registry-k0sd9k7v.js → project-embedding-registry-vy7sv9jt.js} +11 -11
- package/dist/raw-ordinal-warmup-worker.js +6 -6
- package/dist/{read-session-chunk-h1btx938.js → read-session-chunk-t9r7r0sg.js} +11 -11
- package/dist/{read-session-raw-vdf4wh78.js → read-session-raw-gm6xj30v.js} +3 -3
- package/dist/recomp-orchestrator-gr36zvd2.js +60 -0
- package/dist/{safe-notification-target-q0cyrx3x.js → safe-notification-target-jdynm677.js} +2 -2
- package/dist/{search-3rbdx6hw.js → search-qjen1pmz.js} +24 -24
- package/dist/{send-session-notification-2tp44tfa.js → send-session-notification-q8ngbap3.js} +6 -6
- package/dist/shared/history-owner.d.ts +12 -0
- package/dist/shared/history-owner.d.ts.map +1 -0
- package/dist/shared/off-thread-migration-clock.d.ts +8 -1
- package/dist/shared/off-thread-migration-clock.d.ts.map +1 -1
- package/dist/shared/redaction.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts +2 -0
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/user-facing-codes.d.ts +2 -2
- package/dist/{sqlite-j3tvg8r0.js → sqlite-4pqyynrs.js} +3 -1
- package/dist/{storage-memory-8168bgef.js → storage-memory-j3v3hc83.js} +2 -2
- package/dist/{storage-tags-61z5j2f9.js → storage-tags-a513rkj3.js} +5 -5
- package/dist/{transform-postprocess-phase-7qts7h54.js → transform-postprocess-phase-yv463b7y.js} +30 -30
- package/dist/v2/auto-search-worker.js +6 -6
- package/dist/v2/{compaction-marker-candidates-6nypjkpf.js → compaction-marker-candidates-a15w3mqh.js} +3 -3
- package/dist/v2/compaction-marker-candidates-worker.js +2 -2
- package/dist/v2/{context-xq4rtywr.js → context-wd82v9z6.js} +137 -68
- package/dist/v2/{derive-budgets-enckmg7y.js → derive-budgets-0rmpmxjw.js} +3 -3
- package/dist/v2/embedding-backfill-vr7wdtrf.js +20 -0
- package/dist/v2/embedding-worker-process.js +6 -2
- package/dist/v2/embedding-worker.js +6 -5
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/history-owner-2cr9py37.js +243 -0
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/store.d.ts +1 -0
- package/dist/v2/hooks/store.d.ts.map +1 -1
- package/dist/v2/{logger-pj02s7sr.js → logger-te6e1j72.js} +1 -1
- package/dist/v2/{message-index-bounded-5vh1h2g7.js → message-index-bounded-madg2kfr.js} +16 -16
- package/dist/v2/{message-index-8e5z2xx8.js → message-index-yd94vt9n.js} +16 -16
- package/dist/v2/{message-time-backfill-2z365239.js → message-time-backfill-rz1pnpaj.js} +3 -3
- package/dist/v2/migration-worker.js +11 -11
- package/dist/v2/{project-embedding-registry-b77awytr.js → project-embedding-registry-ja23aygk.js} +11 -11
- package/dist/v2/raw-ordinal-warmup-worker.js +6 -6
- package/dist/v2/{read-session-chunk-4sat83cx.js → read-session-chunk-7qs791f3.js} +11 -11
- package/dist/v2/{read-session-raw-a3hw9jsp.js → read-session-raw-h4azh85b.js} +3 -3
- package/dist/v2/recomp-orchestrator-ee91r9s6.js +59 -0
- package/dist/v2/{safe-notification-target-y3p1hce9.js → safe-notification-target-936374xx.js} +2 -2
- package/dist/v2/search-pn15rr52.js +46 -0
- package/dist/v2/{send-session-notification-ppgc8qdz.js → send-session-notification-6n7chhyp.js} +6 -6
- package/dist/v2/{server-v4fwha43.js → server-08k4zkfk.js} +48 -38
- package/dist/v2/{server-ras2sxnc.js → server-44ryn9rb.js} +9 -6
- package/dist/v2/{server-fw31krgq.js → server-4easnf78.js} +19 -19
- package/dist/v2/{server-hcvcxzx7.js → server-4gz0xkrc.js} +5 -5
- package/dist/v2/{server-d6g4whc2.js → server-4qr18rfg.js} +3 -3
- package/dist/v2/{server-sy4b1sra.js → server-4rs8sm06.js} +5 -5
- package/dist/v2/{server-brhj8qtd.js → server-63ha7f0f.js} +3 -27
- package/dist/v2/{server-3t0q2cqx.js → server-7dv9wt9r.js} +1 -1
- package/dist/v2/{server-cw6521wn.js → server-7e087rvk.js} +2 -0
- package/dist/v2/{server-hb6xhh9q.js → server-988cqjme.js} +6 -3
- package/dist/v2/{server-fpqe39as.js → server-9a6bwc12.js} +4 -4
- package/dist/v2/{server-t4k4qexw.js → server-9e5r8y60.js} +5 -1
- package/dist/v2/{server-7ekna9zs.js → server-adfte03x.js} +2 -2
- package/dist/v2/{server-qcw43339.js → server-ak7w74j9.js} +4 -4
- package/dist/v2/{server-dd0nja67.js → server-bx59ezg6.js} +4 -3
- package/dist/v2/{server-676xw7at.js → server-ckrb5rm8.js} +1 -1
- package/dist/{index-w4mw92w3.js → v2/server-cxsr0ng6.js} +15 -3
- package/dist/v2/{server-t528t8qm.js → server-hn9fb3v0.js} +2 -2
- package/dist/v2/{server-7srtwsa4.js → server-j3dm74ey.js} +9 -9
- package/dist/v2/{server-w3zeywp1.js → server-jjsf29aq.js} +4 -4
- package/dist/v2/server-knvkbfvh.js +25 -0
- package/dist/v2/{server-9yyqq3mm.js → server-n78avt2p.js} +1 -1
- package/dist/v2/{server-kdrezdy0.js → server-njnx39q2.js} +6 -3
- package/dist/v2/{server-x08d1xf3.js → server-npm52h2q.js} +4 -3
- package/dist/v2/{server-vb7pbd9p.js → server-nveratm5.js} +1 -1
- package/dist/v2/{server-mv1w7t6z.js → server-q360kdb8.js} +77 -38
- package/dist/v2/{server-kzns2hbz.js → server-s6jqh8bj.js} +1 -1
- package/dist/v2/{server-6byqfbqf.js → server-shme41gj.js} +1 -1
- package/dist/v2/{server-j8epsc0g.js → server-wafzvyhg.js} +33 -10
- package/dist/v2/{server-gtv9wbhq.js → server-xj4mewze.js} +81 -58
- package/dist/v2/{server-nfan6zb6.js → server-y5g32dhq.js} +31 -24
- package/dist/v2/{server-jdwd03ep.js → server-yak3sxya.js} +7 -7
- package/dist/v2/{server-wkdgjjch.js → server-zrwaj29a.js} +2 -2
- package/dist/v2/server.js +7 -6
- package/dist/v2/{sqlite-j3tvg8r0.js → sqlite-4pqyynrs.js} +3 -1
- package/dist/v2/{storage-memory-8168bgef.js → storage-memory-j3v3hc83.js} +2 -2
- package/dist/v2/{storage-tags-j2fzq3sw.js → storage-tags-spa92sgj.js} +5 -5
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/v2/{wrapup-orchestrator-s92ebfzv.js → wrapup-orchestrator-5wxy1ggv.js} +38 -35
- package/dist/wrapup-orchestrator-gv08c6f9.js +47 -0
- package/package.json +1 -1
- package/src/config/compaction-accessor-guard.test.ts +3 -0
- package/src/features/magic-context/dreamer/cron.ts +8 -0
- package/src/shared/history-owner.test.ts +60 -0
- package/src/shared/history-owner.ts +43 -0
- package/src/shared/host-stall-profiler-node.ts +3 -1
- package/src/shared/host-stall-profiler.ts +3 -1
- package/src/shared/off-thread-migration-clock.test.ts +80 -10
- package/src/shared/off-thread-migration-clock.ts +37 -13
- package/src/shared/redaction.test.ts +16 -0
- package/src/shared/redaction.ts +8 -1
- package/src/shared/root-async-context.probe.mjs +36 -0
- package/src/shared/root-async-context.test.ts +49 -0
- package/src/shared/rpc-server.ts +27 -23
- package/src/shared/sqlite-busy-budget-review.test.ts +291 -0
- package/src/shared/sqlite-foreground-wait.test.ts +253 -1
- package/src/shared/sqlite.ts +55 -14
- package/src/shared/user-facing-codes.ts +2 -2
- package/dist/embedding-backfill-re16xncd.js +0 -21
- package/dist/recomp-orchestrator-trcgmm23.js +0 -60
- package/dist/v2/embedding-backfill-xd9ygzgd.js +0 -20
- package/dist/v2/recomp-orchestrator-n11zv1f1.js +0 -59
- package/dist/v2/search-mvgqtkkz.js +0 -46
- package/dist/wrapup-orchestrator-df61jmz9.js +0 -47
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { setTimeout as delay, setImmediate as tick } from "node:timers/promises";
|
|
4
|
+
import { Worker } from "node:worker_threads";
|
|
5
|
+
import { inRootAsyncContext } from "./sqlite";
|
|
6
|
+
|
|
7
|
+
const scope = new AsyncLocalStorage();
|
|
8
|
+
const gc = globalThis.Bun ? () => Bun.gc(true) : globalThis.gc;
|
|
9
|
+
const references = {};
|
|
10
|
+
const resources = [];
|
|
11
|
+
const timerStores = {};
|
|
12
|
+
const ambientTick = () => { timerStores.ambient = scope.getStore() !== undefined; };
|
|
13
|
+
const rootTick = () => { timerStores.root = scope.getStore() !== undefined; };
|
|
14
|
+
function create(name, root, worker) {
|
|
15
|
+
const payload = { synthetic: name };
|
|
16
|
+
references[name] = new WeakRef(payload);
|
|
17
|
+
scope.run(payload, () => {
|
|
18
|
+
const make = worker
|
|
19
|
+
? () => new Worker(new URL("worker.mjs", import.meta.url))
|
|
20
|
+
: () => setInterval(root ? rootTick : ambientTick, 5);
|
|
21
|
+
resources.push(root ? inRootAsyncContext(make) : make());
|
|
22
|
+
assert.equal(scope.getStore(), payload, "Root execution must restore the caller's store");
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
create("ambientWorker", false, true);
|
|
26
|
+
create("rootWorker", true, true);
|
|
27
|
+
create("ambientTimer", false, false);
|
|
28
|
+
create("rootTimer", true, false);
|
|
29
|
+
await delay(30);
|
|
30
|
+
for (let i = 0; i < 12; i++) { await tick(); gc(); await tick(); }
|
|
31
|
+
const retained = Object.fromEntries(Object.entries(references).map(([key, ref]) => [key, Number(ref.deref() !== undefined)]));
|
|
32
|
+
for (const resource of resources) {
|
|
33
|
+
if (resource instanceof Worker) await resource.terminate();
|
|
34
|
+
else clearInterval(resource);
|
|
35
|
+
}
|
|
36
|
+
console.log(JSON.stringify({ runtime: globalThis.Bun ? `Bun ${Bun.version}` : process.version, timerStores, retained }));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createTestTempDirFromPath } from "./test-temp-dir";
|
|
6
|
+
|
|
7
|
+
for (const runtime of ["Node", "Bun"] as const) {
|
|
8
|
+
test(`root resources do not inherit request stores (${runtime})`, async () => {
|
|
9
|
+
const directory = createTestTempDirFromPath(join(tmpdir(), "root-context-"));
|
|
10
|
+
try {
|
|
11
|
+
const entry = join(directory, "probe.mjs");
|
|
12
|
+
const build = await Bun.build({
|
|
13
|
+
entrypoints: [new URL("root-async-context.probe.mjs", import.meta.url).pathname],
|
|
14
|
+
target: "node",
|
|
15
|
+
format: "esm",
|
|
16
|
+
external: ["bun:sqlite", "node:sqlite"],
|
|
17
|
+
});
|
|
18
|
+
expect(build.success).toBe(true);
|
|
19
|
+
await writeFile(entry, await build.outputs[0].text());
|
|
20
|
+
await writeFile(
|
|
21
|
+
join(directory, "worker.mjs"),
|
|
22
|
+
`import { parentPort } from 'node:worker_threads'; parentPort.on('message', () => {});`,
|
|
23
|
+
);
|
|
24
|
+
const child = Bun.spawn(
|
|
25
|
+
[...(runtime === "Node" ? ["node", "--expose-gc"] : [process.execPath]), entry],
|
|
26
|
+
{
|
|
27
|
+
stdout: "pipe",
|
|
28
|
+
stderr: "pipe",
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
const [stdout, stderr, exit] = await Promise.all([
|
|
33
|
+
new Response(child.stdout).text(),
|
|
34
|
+
new Response(child.stderr).text(),
|
|
35
|
+
child.exited,
|
|
36
|
+
]);
|
|
37
|
+
expect(exit, stderr).toBe(0);
|
|
38
|
+
const result = JSON.parse(stdout);
|
|
39
|
+
console.log(`${runtime} ALS propagation: ${stdout.trim()}`);
|
|
40
|
+
expect(result.timerStores).toEqual({ ambient: true, root: false });
|
|
41
|
+
expect(result.retained.rootWorker).toBe(0);
|
|
42
|
+
expect(result.retained.rootTimer).toBe(0);
|
|
43
|
+
expect(result.retained.ambientTimer).toBe(1);
|
|
44
|
+
if (runtime === "Node") expect(result.retained.ambientWorker).toBe(1);
|
|
45
|
+
} finally {
|
|
46
|
+
await rm(directory, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
package/src/shared/rpc-server.ts
CHANGED
|
@@ -133,31 +133,33 @@ export class MagicContextRpcServer {
|
|
|
133
133
|
}
|
|
134
134
|
this.startedAt = Date.now();
|
|
135
135
|
const self = this;
|
|
136
|
-
const server =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
websocket: {
|
|
143
|
-
open(ws) {
|
|
144
|
-
// Close the socket if it doesn't authenticate promptly. A
|
|
145
|
-
// never-authenticated socket holds no sink and is harmless,
|
|
146
|
-
// but we don't want to keep raw connections open forever.
|
|
147
|
-
ws.data.authTimer = setTimeout(() => {
|
|
148
|
-
if (!ws.data.authed) ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
|
|
149
|
-
}, WS_AUTH_TIMEOUT_MS);
|
|
136
|
+
const server = inRootAsyncContext(() =>
|
|
137
|
+
Bun.serve<WsData>({
|
|
138
|
+
port: 0,
|
|
139
|
+
hostname: "127.0.0.1",
|
|
140
|
+
fetch(req, srv) {
|
|
141
|
+
return self.handleFetch(req, srv);
|
|
150
142
|
},
|
|
151
|
-
|
|
152
|
-
|
|
143
|
+
websocket: {
|
|
144
|
+
open(ws) {
|
|
145
|
+
// Close the socket if it doesn't authenticate promptly. A
|
|
146
|
+
// never-authenticated socket holds no sink and is harmless,
|
|
147
|
+
// but we don't want to keep raw connections open forever.
|
|
148
|
+
ws.data.authTimer = setTimeout(() => {
|
|
149
|
+
if (!ws.data.authed) ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
|
|
150
|
+
}, WS_AUTH_TIMEOUT_MS);
|
|
151
|
+
},
|
|
152
|
+
message(ws, raw) {
|
|
153
|
+
self.handleWsMessage(ws, raw);
|
|
154
|
+
},
|
|
155
|
+
close(ws) {
|
|
156
|
+
if (ws.data.authTimer) clearTimeout(ws.data.authTimer);
|
|
157
|
+
ws.data.unregister?.();
|
|
158
|
+
self.sockets.delete(ws);
|
|
159
|
+
},
|
|
153
160
|
},
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
ws.data.unregister?.();
|
|
157
|
-
self.sockets.delete(ws);
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
161
163
|
|
|
162
164
|
this.server = server;
|
|
163
165
|
this.port = server.port ?? 0;
|
|
@@ -429,3 +431,5 @@ function json(body: unknown, status = 200): Response {
|
|
|
429
431
|
headers: { "Content-Type": "application/json" },
|
|
430
432
|
});
|
|
431
433
|
}
|
|
434
|
+
|
|
435
|
+
import { inRootAsyncContext } from "./sqlite";
|
|
@@ -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
|
+
});
|