pi-mono/packages/durable/test/examples/04-watches.ts
Mario Zechner 8b0caefc22 feat(durable,chord): publish commits at adoption, address-following watches, synchronous observers
Durable publishes a stored commit inside Storage.commit's adopt: the
telemetry events, its own observers (document watches, conversation
views, task graph, scheduler, waits), and a dispatch of host
subscribeCommits listeners one microtask later, before every wait and
abort the commit triggers. A commit the storage rejects after adoption is
published before the Session fails; a throwing telemetry adapter is
reported as host_error "telemetry" instead of rejecting a durable commit.

Chord's watch listeners take one change object, { value, ops, retired }
({ value } for native watches); retirement is a flag, not an end.
Durable's document watches follow their address, and a task document's
watch ends after its retirement call. New synchronous observers for
adapters: observeDoc, observeConversation, observeTaskGraph; the watches
are built on them. The env file watcher's change type is renamed
FileChange.
2026-10-26 20:44:34 +00:00

54 lines
2.1 KiB
TypeScript

// Serialize asynchronous document work with a watch.
// Run from packages/durable:
// node --conditions=source --experimental-strip-types test/examples/04-watches.ts
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { createSession, defineDoc, MemoryStorage } from "../../src/index.ts";
const context = BACKGROUND_CONTEXT;
const session = createSession(new MemoryStorage());
const Notes = defineDoc<{ text: string }>({
checkpointWhen: () => false,
kind: "example.notes",
version: 1,
scope: "conversation",
history: "rewindable",
fork: "asOf",
initial: () => ({ text: "" }),
});
const chat = await session.commit(async (tx) => {
const conversation = await tx.createConversation({ ownership: { kind: "ownerless" } });
(await tx.doc(Notes, conversation.id)).text = "first";
return conversation;
}, context);
// A watch starts from one stable acquisition revision. Callbacks never overlap:
// commits made while one runs arrive folded into the next call, which gets the
// newest value and ops that turn the previously delivered value into it (one
// full-value replacement `[["r", value]]` once more than 10,000 ops pile up).
const notesWatch = await session.watchDoc(Notes, chat.id, context);
if (notesWatch === undefined) throw new Error("notes are absent");
console.log("watch baseline:", notesWatch.value);
const busy = Promise.withResolvers<void>();
const done = Promise.withResolvers<void>();
let calls = 0;
notesWatch.start(async ({ value, ops }, _deliveryContext) => {
console.log("watch update:", value, JSON.stringify(ops));
// The first call stays busy, as a slow consumer would, while three more commits land.
if (++calls === 1) await busy.promise;
else done.resolve();
});
await session.commit(async (tx) => {
(await tx.doc(Notes, chat.id)).text = "observed";
}, context);
for (const more of [" asynchronously", ", folded", "!"]) {
await session.commit(async (tx) => {
(await tx.doc(Notes, chat.id)).text += more;
}, context);
}
busy.resolve();
await done.promise;
console.log("listener calls:", calls);
await notesWatch.stop();
await session.close(context);