pi-mono/packages/chord/test/examples/21-persisted-documents.ts
Mario Zechner e9c0ff4d84 feat(chord): Chord v2 is the package's root
src/next is src (the main export), src/next-node is src/node with short subpaths (/http, /workers, /plugins,
/plugins/runtime, /app, /surfaces, /web, /react, /tui, /tui/launcher); the foundations get /json and /watch;
Chord v1 moves to src/old (/old, /old/node) until p2 runs on v2; tests and docs move likewise (test/, docs/,
test/old, docs/old). Durable and env import the foundations from their subpaths.
2026-11-02 22:36:55 +00:00

91 lines
3.6 KiB
TypeScript

// Persisted documents: `persist: true` keeps an object's documents in the host's store, so a new host on the same
// store finds them again; `checkpointWhen` decides when a change is stored as a new base instead of a delta.
// Run from packages/chord:
// node --conditions=source --experimental-strip-types test/examples/21-persisted-documents.ts
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { BACKGROUND_CONTEXT } from "../../src/context/index.ts";
import { arr, local, method, obj, str } from "../../src/contracts/index.ts";
import { defineState } from "../../src/documents/index.ts";
import { createHost, defineFacet, type Reference } from "../../src/host/index.ts";
import { openNodeSqliteDatabase } from "../../src/sqlite/node.ts";
import { openStore, sqliteBackend } from "../../src/store/index.ts";
const context = BACKGROUND_CONTEXT;
// A persisted singleton: loaded once when its object activates; every 20th stored change is a new base.
const Tags = defineState("tags", {
schema: obj({ tags: arr(str()) }),
version: 1,
initial: () => ({ tags: [] }),
persist: true,
checkpointWhen: (_value, _ops, { deltasSinceBase }) => deltasSinceBase >= 20,
});
// A persisted keyed document: each key loaded on its first read, then cached until idle for ten minutes.
const Notes = defineState("notes", {
schema: obj({ body: str() }),
version: 1,
initial: () => ({ body: "" }),
keyed: true,
persist: true,
checkpointWhen: () => false,
});
const Notebook = local("example.notebook", {
version: 1,
params: {},
members: {
tags: obj({ tags: arr(str()) }),
write: method(obj({ id: str(), body: str(), tag: str() })),
read: method(str(), str()),
},
});
const facet = defineFacet((host) => {
// Persisted documents need a stable identity, as tables do: a local type with empty params.
host.serve(Notebook, (_, self) => {
const tags = self.state(Tags);
const notes = self.state(Notes);
return {
tags,
write: async ({ id, body, tag }, ctx) => {
// Both documents commit in one store step, or neither does.
await self.tx(async (tx) => {
(await tx.draft(notes, id)).body = body;
const draft = await tx.draft(tags);
if (!draft.tags.includes(tag)) draft.tags.push(tag);
}, ctx);
return null;
},
read: async (id, ctx) => (await notes.get(id, ctx))?.body ?? "(none)",
};
});
});
/** A host on the store at `path`, run until `work` is done with the notebook. */
async function session(path: string, work: (notebook: Reference<typeof Notebook>) => Promise<void>) {
const store = await openStore(sqliteBackend(await openNodeSqliteDatabase(path)), context);
const host = createHost({ store, onError: (error) => console.error("background error:", error) });
await host.load([{ name: "@example/notebook", facet }], context);
await work(await host.get(Notebook, {}, context));
await host.close(context);
await store.close(context);
}
const directory = await mkdtemp(join(tmpdir(), "chord-example-"));
const path = join(directory, "notebook.db");
try {
await session(path, async (notebook) => {
await notebook.write({ id: "groceries", body: "oat milk", tag: "home" }, context);
await notebook.write({ id: "standup", body: "demo the store", tag: "work" }, context);
console.log("first host, tags:", notebook.tags.value.tags);
});
// A new host on the same file: the documents come back from the store.
await session(path, async (notebook) => {
console.log("second host, tags:", notebook.tags.value.tags);
console.log("second host, standup:", await notebook.read("standup", context));
});
} finally {
await rm(directory, { recursive: true, force: true });
}