pi-mono/packages/chord/test/next/store/engine.test.ts
Mario Zechner 14b4c56e57 feat(chord,durable): Chord v2 slice 6b, Durable and jobs on the store
StoreStorage (Durable's storage on Chord v2's store, @earendil-works/pi-durable/storage/store) passes
Durable's storage conformance suite and runs Durable's harness tests (npm run test:store). The store
gets owner names and claims, one id allocator, Durable's Storage semantics for refused steps, history
none/latest/rewindable with reads at a point, archive revisions and applyBatch. self.follow follows
Durable's synchronous observers as Chord sources. Persisted jobs: records and state in the owner's
store, restarts as Durable's tasks, abort stored, deferred state. Chord's ./next export returns for
the adapter surface.
2026-10-28 19:18:30 +00:00

265 lines
12 KiB
TypeScript

// The engine (slice 6a §10, §11, slice 6b §1.5): documents as bases and deltas, their keys in code point order, the
// history rules at a new base and at retirement (R1), reads at a point, records inserted as they are.
import { describe, expect, it } from "vitest";
import { BACKGROUND_CONTEXT as context } from "../../../src/context/index.ts";
import type { Op } from "../../../src/delta/index.ts";
import type { ArchivePart } from "../../../src/next/store/archive.ts";
import { sqliteBackend } from "../../../src/next/store/backend.ts";
import {
changeDocument,
createDocument,
type DocumentAddress,
type DocumentHistory,
type DocumentRecord,
findDocument,
insertDocument,
listDocumentKeys,
listDocuments,
loadDocument,
readDocument,
retireDocument,
retireRecord,
writeContent,
} from "../../../src/next/store/engine.ts";
import { importStore } from "../../../src/next/store/import.ts";
import { TABLES } from "../../../src/next/store/schema.ts";
import type { StagedWrite } from "../../../src/next/store/step.ts";
import { type ChordStore, openStore } from "../../../src/next/store/store.ts";
import { openNodeSqliteDatabase } from "../../../src/sqlite/node.ts";
import { exported, rejection } from "../support.ts";
const OWNER = { owner: "app.store", namespace: "chord", kind: "notes" };
/** A store on `:memory:` (or `on`) and a commit helper resolving with the created ids. */
async function engine(on?: ChordStore) {
const store =
on ?? ((await openStore(sqliteBackend(await openNodeSqliteDatabase(":memory:")), context)) as ChordStore);
const commit = async (...writes: StagedWrite[]): Promise<readonly number[]> => {
let created: readonly number[] = [];
await store.commit(
writes,
{
admit: () => true,
adopt: (_seq, ids) => {
created = ids;
},
},
context,
);
return created;
};
const at = (key: string): DocumentAddress => ({ ...OWNER, key });
const base = (value: object, version = 1) => ({
version,
fingerprint: `f${version}`.padEnd(16, "0"),
value: value as never,
});
const create = (key: string, value: object) => createDocument(at(key), base(value), JSON.stringify(value));
const delta = (id: number, revision: number, ops: Op[]) =>
changeDocument(id, revision, { kind: "delta", version: 1, ops }, JSON.stringify(ops));
const read = <T>(fn: Parameters<ChordStore["read"]>[0]) => store.read(fn, context) as Promise<T>;
return { store, commit, at, base, create, delta, read };
}
/** `store`'s archive with `edit` applied to its parts, imported into a fresh `:memory:` store. */
async function reimported(store: ChordStore, edit: (parts: ArchivePart[]) => unknown[]): Promise<ChordStore> {
const parts = edit(await exported(store));
return (await importStore(sqliteBackend(await openNodeSqliteDatabase(":memory:")), parts, context)) as ChordStore;
}
/** A staged write applying `apply` in the step (an adapter's composite). */
function staged(apply: StagedWrite["apply"]): StagedWrite {
return { order: "write", apply };
}
/** An adapter's record of `history` at `key`, with an explicit id and the step's seq. */
function record(id: number, key: string, history: DocumentHistory, createdAt: number): DocumentRecord {
return { id, ...OWNER, key, history, createdAt, revision: 1, version: 1, fingerprint: "" };
}
const LIMIT = Number.MAX_SAFE_INTEGER;
describe("documents", () => {
it("load as their base with the deltas after it applied; a new base prunes the older revisions", async () => {
const { commit, at, base, create, delta, read } = await engine();
const [id] = await commit(create("a", { n: 0 }));
await commit(delta(id!, 1, [["s", ["n"], 1]]));
await commit(delta(id!, 2, [["s", ["n"], 2]]));
const found = (await read<DocumentRecord | undefined>((e) => findDocument(e, at("a"))))!;
expect(found).toMatchObject({ id, revision: 3, version: 1, history: "none", createdAt: 1 });
expect(await read((e) => loadDocument(e, found))).toMatchObject({ value: { n: 2 }, deltasSinceBase: 2 });
const next = base({ m: 9 }, 2);
await commit(changeDocument(id!, 3, { kind: "base", ...next }, JSON.stringify(next.value)));
const rebased = (await read<DocumentRecord | undefined>((e) => findDocument(e, at("a"))))!;
expect(rebased).toMatchObject({ revision: 4, version: 2, fingerprint: next.fingerprint });
expect(await read((e) => loadDocument(e, rebased))).toMatchObject({ value: { m: 9 }, deltasSinceBase: 0 });
expect(await read((e) => e.all(`SELECT seq FROM ${TABLES.revisions}`))).toEqual([{ seq: 4 }]);
});
it("list live keys in code point order, paging after a key", async () => {
const { commit, create, read } = await engine();
await commit(create("\u{1F600}", {}), create("\uFF5E", {}), create("b", {}), create("a", {}));
expect(await read((e) => listDocumentKeys(e, OWNER, undefined, 10))).toEqual(["a", "b", "\uFF5E", "\u{1F600}"]);
expect(await read((e) => listDocumentKeys(e, OWNER, "b", 1))).toEqual(["\uFF5E"]);
});
it("retire a none record with its row and revisions (R1); the address takes a new incarnation", async () => {
const { commit, at, create, read } = await engine();
const [id] = await commit(create("a", { n: 0 }));
const [next] = await commit(retireDocument(id!, 1), create("a", { n: 1 }));
expect(next).toBe(3);
expect(await read((e) => e.all(`SELECT id FROM ${TABLES.documents}`))).toEqual([{ id: 3 }]);
expect(await read((e) => e.all(`SELECT document_id FROM ${TABLES.revisions}`))).toEqual([{ document_id: 3 }]);
expect(await read((e) => findDocument(e, at("a")))).toMatchObject({ id: 3 });
});
it("refuse two live records at one address and a delta on another version: the step is refused", async () => {
const first = await engine();
await first.commit(first.create("a", {}));
expect(String(await rejection(first.commit(first.create("a", {}))))).toContain("UNIQUE");
const second = await engine();
const [id] = await second.commit(second.create("a", {}));
const crossing = changeDocument(id!, 1, { kind: "delta", version: 2, ops: [] }, "[]");
expect(String(await rejection(second.commit(crossing)))).toContain("not live at revision 1");
expect(await second.read(async () => 1)).toBe(1);
});
});
describe("history rules (R1) and reads at a point", () => {
/** Creates adapter record `id` at "a" (its base `{ n: 0 }`), then changes and retires it in later commits. */
async function lifetime(history: DocumentHistory) {
const { store, commit, at, read } = await engine();
await commit(
staged(async (step) => {
const made = record(10, "a", history, step.seq);
await insertDocument(step, made, '{"n":0}');
return [];
}),
);
await commit(
staged(async (step) => {
const content = { kind: "base", version: 1, fingerprint: "", value: { n: 1 } } as const;
await writeContent(step, 10, history, 1, content, '{"n":1}');
return [];
}),
);
await commit(
staged(async (step) => {
await writeContent(
step,
10,
history,
2,
{ kind: "delta", version: 1, ops: [["s", ["n"], 2]] },
'[["s",["n"],2]]',
);
return [];
}),
);
// An adapter's retirement (Durable's): the record read in the step, retired by its history's rule.
await commit(
staged(async (step) => {
await retireRecord(step, (await readDocument(step, 10))!);
return [];
}),
);
return { store, read, at };
}
it("keep a retired latest record's row without revisions, and every revision of a rewindable one", async () => {
const latest = await lifetime("latest");
expect(await latest.read((e) => e.all(`SELECT id, retired_at FROM ${TABLES.documents}`))).toEqual([
{ id: 10, retired_at: 4 },
]);
expect(await latest.read((e) => e.all(`SELECT seq FROM ${TABLES.revisions}`))).toEqual([]);
const rewindable = await lifetime("rewindable");
expect(await rewindable.read((e) => e.all(`SELECT seq, kind FROM ${TABLES.revisions} ORDER BY seq`))).toEqual([
{ seq: 1, kind: "base" },
{ seq: 2, kind: "base" },
{ seq: 3, kind: "delta" },
]);
});
it("find a record at a point by its half-open lifetime, and load a rewindable one at any point inside it", async () => {
const { read, at } = await lifetime("rewindable");
const found = await Promise.all(
[0, 1, 3, 4, "current" as const].map((point) =>
read<DocumentRecord | undefined>((e) => findDocument(e, at("a"), point)),
),
);
expect(found.map((each) => each?.id)).toEqual([undefined, 10, 10, undefined, undefined]);
const record = found[1]!;
expect(record.retiredAt).toBe(4);
for (const [point, n] of [
[1, 0],
[2, 1],
[3, 2],
] as const) {
expect(await read((e) => loadDocument(e, record, point))).toMatchObject({ value: { n }, version: 1 });
}
expect(await read((e) => listDocuments(e, OWNER, 3, 0, 10))).toMatchObject([{ id: 10 }]);
expect(await read((e) => listDocuments(e, OWNER, "current", 0, 10))).toEqual([]);
});
it("find a retired latest record at a point, whose content is gone", async () => {
const { read, at } = await lifetime("latest");
const found = (await read<DocumentRecord | undefined>((e) => findDocument(e, at("a"), 2)))!;
expect(found).toMatchObject({ id: 10, retiredAt: 4, history: "latest" });
// A read returns data (the store stops on a read's throw, slice 6b §1.4): the outcome as a value.
const outcome = read((e) =>
loadDocument(e, found, 2).then(
() => "loaded",
(error: unknown) => String(error),
),
);
expect(await outcome).toContain("no base");
});
it("insert a create-plus-retire as an empty lifetime that never holds the live address", async () => {
const { commit, read, at, create } = await engine();
const [live] = await commit(create("a", {}));
await commit(
staged(async (step) => {
await insertDocument(step, { ...record(20, "a", "latest", step.seq), retiredAt: step.seq }, "{}");
return [];
}),
);
expect(await read((e) => findDocument(e, at("a")))).toMatchObject({ id: live });
expect(await read((e) => e.all(`SELECT document_id FROM ${TABLES.revisions} WHERE document_id = 20`))).toEqual(
[],
);
});
});
describe("counters at the safe-integer limit", () => {
it("allocate document ids up to the safe range after an import near it; past it the commit is refused", async () => {
const { store: source } = await engine();
const near = (next: number) =>
reimported(source, ([manifest, ...rest]) => [{ ...manifest, nextId: next }, ...rest]);
const { store, commit, create } = await engine(await near(LIMIT - 1));
expect(await commit(create("a", { n: 0 }))).toEqual([LIMIT - 1]);
expect(store.nextId).toBe(LIMIT);
expect(String(await rejection(commit(create("b", { n: 0 }))))).toContain("ID space is exhausted");
// Two creations in one commit where only one id is left: nothing of it is written.
const both = await engine(await near(LIMIT - 1));
expect(String(await rejection(both.commit(both.create("a", { n: 0 }), both.create("b", { n: 0 }))))).toContain(
"ID space is exhausted",
);
expect([both.store.lastSeq, both.store.nextId]).toEqual([0, LIMIT - 1]);
});
it("count revisions up to the safe range after an import near it; a change past it is refused", async () => {
const { store: source, commit: seed, create } = await engine();
await seed(create("a", { n: 0 }));
const store = await reimported(source, (parts) =>
parts.map((part) =>
part.kind === "document.create" ? { ...part, record: { ...part.record, revision: LIMIT - 1 } } : part,
),
);
const { commit, delta, read, at } = await engine(store);
const found = (await read<DocumentRecord | undefined>((e) => findDocument(e, at("a"))))!;
await commit(delta(found.id, LIMIT - 1, [["s", ["n"], 1]]));
expect(String(await rejection(commit(delta(found.id, LIMIT, [["s", ["n"], 2]]))))).toContain("safe range");
});
});