pi-mono/packages/durable/test/sqlite-cloudflare.test.ts
Mario Zechner e5c6182bab feat(durable): group commit and batch Storage.commit
A Session no longer holds its line while a commit is stored. Commits that
arrive while one storage call is in flight go out together in the next call,
each with its own seq and outcome; a job runs until it touches a key an
unpublished commit wrote, then waits for that commit's publication. Outcomes
are delivered in line order; publication is in line order, one commit per
macrotask. The scheduler's decisions run as regions that record the keys they
read and rerun after a wait, with side effects only after a clean check.

Storage.commit(batches, receive, context) replaces commit(writes, adopt,
context): prefix semantics (Durable's own checks refuse a batch and keep the
ones before it; any other failure rejects the call), receipts per stored
batch, optional publishing() scope. Memory, SQLite, JSONL, IndexedDB and
StoreStorage implement it natively; StoreStorage through the new
ChordStore.commitSeries (one store transaction per call), replacing
commitInBatch.

A normal run makes no storage reads: bounded record cache, conversation
tails, task-document index, kept context ranges. A tool slot's running status
and a generation's attempt are stored with the next commit (12 instead of 14
commits per run).

One Harness with 128 conversations over storage with a 4 ms round trip: about
5 -> 440 agent runs per second. Design: packages/durable/docs/group-commit.md.
2026-10-30 10:41:11 +00:00

83 lines
3.8 KiB
TypeScript

import { DatabaseSync } from "node:sqlite";
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { registerStorageConformance } from "@earendil-works/pi-durable/testing";
import { describe, expect, it } from "vitest";
import { durableObjectStorage } from "../../chord/test/sqlite/helpers.ts";
import { idFromNumber } from "../src/ids.ts";
import { DurableObjectSqliteDatabase, openDurableObjectSqliteStorage } from "../src/storage/sqlite/cloudflare.ts";
import { commitWrites } from "../src/testing/commit-writes.ts";
import { type EntryId, ROOT_CONVERSATION_ID } from "../src/types.ts";
registerStorageConformance({ describe, expect, it }, "SqliteStorage on a prefixed Durable Object", async (use) =>
use(await openDurableObjectSqliteStorage(durableObjectStorage(), { prefix: "pi_" })),
);
describe("Durable Object SQLite adapter", () => {
it("keeps schema names unprefixed by default", async () => {
const sqlite = new DatabaseSync(":memory:");
await openDurableObjectSqliteStorage(durableObjectStorage(sqlite));
expect(sqlite.prepare("SELECT version FROM durable_schema").get()).toEqual({ version: 2 });
expect(sqlite.prepare("SELECT name FROM sqlite_master WHERE name = 'pi_durable_schema'").get()).toBeUndefined();
});
it("prefixes pi's tables and indexes without changing host tables", async () => {
const sqlite = new DatabaseSync(":memory:");
const storage = durableObjectStorage(sqlite);
const database = new DurableObjectSqliteDatabase(storage);
await database.exec("CREATE TABLE host_notes (text TEXT)");
await database.exec("CREATE TABLE tasks (text TEXT)");
await openDurableObjectSqliteStorage(storage, { prefix: "pi_" });
const names = sqlite
.prepare("SELECT name FROM sqlite_master WHERE type IN ('table', 'index') AND name NOT LIKE 'sqlite_%'")
.all()
.map((row) => String(row.name));
expect(names).toContain("host_notes");
expect(names).toContain("tasks");
expect(names).not.toContain("pi_host_notes");
const piNames = names.filter((name) => !["host_notes", "tasks"].includes(name));
expect(piNames.length).toBeGreaterThan(10);
expect(piNames.every((name) => name.startsWith("pi_"))).toBe(true);
expect(piNames).toContain("pi_tasks");
expect(piNames).toContain("pi_durable_schema");
});
it("isolates concurrently opened prefixes on one Durable Object", async () => {
const storage = durableObjectStorage();
const [first, second] = await Promise.all([
openDurableObjectSqliteStorage(storage, { prefix: "first_" }),
openDurableObjectSqliteStorage(storage, { prefix: "second_" }),
]);
const entryId = idFromNumber<EntryId>(2);
await Promise.all([
commitWrites(
first,
[
{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } },
{ type: "entry", value: { id: entryId, conversationId: ROOT_CONVERSATION_ID, kind: "first" } },
],
BACKGROUND_CONTEXT,
),
commitWrites(
second,
[
{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } },
{ type: "entry", value: { id: entryId, conversationId: ROOT_CONVERSATION_ID, kind: "second" } },
],
BACKGROUND_CONTEXT,
),
]);
expect((await first.entry(entryId, BACKGROUND_CONTEXT))?.entry.kind).toBe("first");
expect((await second.entry(entryId, BACKGROUND_CONTEXT))?.entry.kind).toBe("second");
await Promise.all([first.close(BACKGROUND_CONTEXT), second.close(BACKGROUND_CONTEXT)]);
});
it("rejects invalid and reserved prefixes", async () => {
await expect(openDurableObjectSqliteStorage(durableObjectStorage(), { prefix: "" })).rejects.toThrow("Invalid");
await expect(openDurableObjectSqliteStorage(durableObjectStorage(), { prefix: "bad-prefix" })).rejects.toThrow(
"Invalid",
);
await expect(openDurableObjectSqliteStorage(durableObjectStorage(), { prefix: "sqlite_pi_" })).rejects.toThrow(
"reserved",
);
});
});