import type { Draft } from "@earendil-works/chord"; import { type ConversationId, createSession, type DocumentState, type DocumentWatch, defineDoc, defineDocFamily, defineEntry, type EntryId, type EntryRecord, MemoryStorage, ROOT_CONVERSATION_ID, type Session, type TaskId, type Tx, type TypedEntry, } from "@earendil-works/pi-durable"; import { describe, expect, expectTypeOf, it } from "vitest"; import { idFromNumber } from "../src/ids.ts"; import { context } from "./session-support.ts"; type State = { value: number }; const initial = (): State => ({ value: 0 }); const SessionDoc = defineDoc({ kind: "t.session", version: 1, scope: "session", initial }); const LatestDoc = defineDoc({ kind: "t.latest", version: 1, scope: "conversation", history: "latest", fork: "current", initial, }); const RewindableDoc = defineDoc({ kind: "t.rewindable", version: 1, scope: "conversation", history: "rewindable", fork: "asOf", initial, }); const TaskDoc = defineDoc({ kind: "t.task", version: 1, scope: "task", initial }); const SessionFamily = defineDocFamily({ kind: "t.session-family", version: 1, family: true, scope: "session", initial: (seed) => ({ value: seed }), }); const ConversationFamily = defineDocFamily({ kind: "t.conversation-family", version: 1, family: true, scope: "conversation", history: "rewindable", fork: "initial", initial: (seed) => ({ value: seed }), }); const TaskFamily = defineDocFamily({ kind: "t.task-family", version: 1, family: true, scope: "task", initial: (seed) => ({ value: seed }), }); describe("document definitions", () => { it("validates persisted version semantics", () => { expect(() => defineDoc({ kind: "k", version: 0, scope: "session", initial })).toThrow("positive integer"); expect(() => defineDoc({ kind: "k", version: 1.5, scope: "session", initial })).toThrow( "positive integer", ); expect(defineDoc({ kind: "", version: 1, scope: "session", initial }).definition.kind).toBe(""); expect(SessionDoc.definition.kind).toBe("t.session"); }); it("types every owner, key, and seed overload", async () => { const session: Session = createSession(new MemoryStorage()); const check = async (tx: Tx, conversationId: ConversationId, taskId: TaskId): Promise => { expectTypeOf(await tx.doc(SessionDoc)).toEqualTypeOf>(); expectTypeOf(await tx.doc(LatestDoc, conversationId)).toEqualTypeOf>(); expectTypeOf(await tx.doc(RewindableDoc, conversationId)).toEqualTypeOf>(); expectTypeOf(await tx.doc(TaskDoc, taskId)).toEqualTypeOf>(); expectTypeOf(await tx.doc(SessionFamily, "k", 1)).toEqualTypeOf>(); expectTypeOf(await tx.doc(ConversationFamily, conversationId, "k", 1)).toEqualTypeOf>(); expectTypeOf(await tx.doc(TaskFamily, taskId, "k", 1)).toEqualTypeOf>(); await tx.retireDoc(SessionDoc); await tx.retireDoc(LatestDoc, conversationId); await tx.retireDoc(TaskDoc, taskId); await tx.retireDoc(SessionFamily, "k"); await tx.retireDoc(ConversationFamily, conversationId, "k"); await tx.retireDoc(TaskFamily, taskId, "k"); // @ts-expect-error Session documents take no owner await tx.doc(SessionDoc, conversationId); // @ts-expect-error conversation documents require a conversation ID await tx.doc(LatestDoc); // @ts-expect-error family access requires a creation seed await tx.doc(SessionFamily, "k"); // @ts-expect-error family seeds are typed await tx.doc(SessionFamily, "k", "seed"); // @ts-expect-error family retirement takes no seed await tx.retireDoc(TaskFamily, taskId, "k", 1); }; expect(check).toBeTypeOf("function"); expectTypeOf(await session.snapshot(SessionDoc, context)).toEqualTypeOf | undefined>(); const taskId = idFromNumber(1); expectTypeOf(await session.snapshot(LatestDoc, ROOT_CONVERSATION_ID, context)).toEqualTypeOf< Readonly | undefined >(); expectTypeOf(await session.snapshot(TaskDoc, taskId, context)).toEqualTypeOf | undefined>(); expectTypeOf(await session.snapshot(SessionFamily, "k", context)).toEqualTypeOf | undefined>(); expectTypeOf(await session.snapshot(ConversationFamily, ROOT_CONVERSATION_ID, "k", context)).toEqualTypeOf< Readonly | undefined >(); expectTypeOf(await session.snapshot(TaskFamily, taskId, "k", context)).toEqualTypeOf< Readonly | undefined >(); // @ts-expect-error snapshots never take a creation seed await session.snapshot(SessionFamily, "k", 1, context).catch(() => undefined); await session.close(context); }); it("types historical reads, states, watches, and typed entries", () => { const Note = defineEntry<{ text: string }>("t.note"); const Marker = defineEntry("t.marker"); type Rewound = Promise | undefined>; type StateOf = Promise | undefined>; type WatchOf = Promise | undefined>; const check = async (session: Session, tx: Tx, conversationId: ConversationId, taskId: TaskId, at: EntryId) => { expectTypeOf(session.snapshotAsOf(RewindableDoc, conversationId, at, context)).toEqualTypeOf(); expectTypeOf( session.snapshotAsOf(ConversationFamily, conversationId, "k", at, context), ).toEqualTypeOf(); // @ts-expect-error latest conversation documents keep no history void session.snapshotAsOf(LatestDoc, conversationId, at, context); // @ts-expect-error Session documents keep no history void session.snapshotAsOf(SessionDoc, conversationId, at, context); // @ts-expect-error task documents keep no history void session.snapshotAsOf(TaskDoc, taskId, at, context); expectTypeOf(session.documentState(SessionDoc, context)).toEqualTypeOf(); expectTypeOf(session.documentState(LatestDoc, conversationId, context)).toEqualTypeOf(); expectTypeOf(session.documentState(TaskDoc, taskId, context)).toEqualTypeOf(); expectTypeOf(session.documentState(SessionFamily, "k", context)).toEqualTypeOf(); expectTypeOf(session.documentState(ConversationFamily, conversationId, "k", context)).toEqualTypeOf(); expectTypeOf(session.documentState(TaskFamily, taskId, "k", context)).toEqualTypeOf(); expectTypeOf(session.watchDoc(SessionDoc, context)).toEqualTypeOf(); expectTypeOf(session.watchDoc(LatestDoc, conversationId, context)).toEqualTypeOf(); expectTypeOf(session.watchDoc(TaskDoc, taskId, context)).toEqualTypeOf(); expectTypeOf(session.watchDoc(SessionFamily, "k", context)).toEqualTypeOf(); expectTypeOf(session.watchDoc(ConversationFamily, conversationId, "k", context)).toEqualTypeOf(); expectTypeOf(session.watchDoc(TaskFamily, taskId, "k", context)).toEqualTypeOf(); // @ts-expect-error a watch of a Session document takes no owner void session.watchDoc(SessionDoc, conversationId, context); expectTypeOf(await tx.entry(at)).toEqualTypeOf(); expectTypeOf(await tx.entry(Note, at)).toEqualTypeOf | undefined>(); expectTypeOf(await tx.appendEntry(conversationId, { kind: "t.raw" })).toEqualTypeOf(); const note = await tx.appendEntry(Note, conversationId, { data: { text: "x" } }); expectTypeOf(note).toEqualTypeOf>(); expectTypeOf(note.data.text).toEqualTypeOf(); expectTypeOf(await tx.appendEntry(Marker, conversationId, {})).toEqualTypeOf>(); // @ts-expect-error a data entry requires its data await tx.appendEntry(Note, conversationId, {}); // @ts-expect-error data is typed by the token await tx.appendEntry(Note, conversationId, { data: { text: 1 } }); // @ts-expect-error the token supplies the kind await tx.appendEntry(Note, conversationId, { kind: "t.note", data: { text: "x" } }); // @ts-expect-error an entry kind without data takes none await tx.appendEntry(Marker, conversationId, { data: 1 }); }; expect(check).toBeTypeOf("function"); }); });