import type { Context } from "@earendil-works/chord"; import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context"; import { type CommitPublication, type ConversationId, type DocumentAddress, type DocumentCommitChange, type DocumentId, type DocumentPoint, type DocumentRecord, type Id, MemoryStorage, type Seq, type StorageWrite, } from "@earendil-works/pi-durable"; import { SessionImpl } from "../src/session/session.ts"; export const context: Context = BACKGROUND_CONTEXT; type Deferred = { readonly promise: Promise; readonly resolve: () => void }; function deferred(): Deferred { let resolve!: () => void; const promise = new Promise((done) => { resolve = done; }); return { promise, resolve }; } /** A gate that holds calls until released and reports when the first held call arrives. */ export type Gate = { readonly entered: Promise; release(): void; }; /** Memory storage with observable commits, held calls, and injected commit failures. */ export class ControlledStorage extends MemoryStorage { /** Exact borrowed batches admitted by Session. */ readonly admittedCommits: (readonly StorageWrite[])[] = []; /** Detached batches for value assertions. */ readonly commits: (readonly StorageWrite[])[] = []; mintCount = 0; documentReadCount = 0; #commitGate: { gate: Deferred; entered: Deferred } | undefined; #findGate: { gate: Deferred; entered: Deferred } | undefined; #commitFailure: Error | undefined; holdCommits(): Gate { const held = { gate: deferred(), entered: deferred() }; this.#commitGate = held; return { entered: held.entered.promise, release: () => this.#release("commit", held) }; } holdFindDocument(): Gate { const held = { gate: deferred(), entered: deferred() }; this.#findGate = held; return { entered: held.entered.promise, release: () => this.#release("find", held) }; } /** Simulate a crash during the held commit: it never reaches storage, and later commits proceed. */ crash(): void { this.#commitGate = undefined; } failNextCommit(error: Error): void { this.#commitFailure = error; } /** Reopen after `close()`, as a fresh process would reopen the same database, keeping every committed record. */ reopen(): this { (this as unknown as { closed: boolean }).closed = false; return this; } #release(kind: "commit" | "find", held: { gate: Deferred }): void { if (kind === "commit" && this.#commitGate === held) this.#commitGate = undefined; if (kind === "find" && this.#findGate === held) this.#findGate = undefined; held.gate.resolve(); } override async commit(writes: readonly StorageWrite[], commitContext: Context): Promise { this.admittedCommits.push(writes); this.commits.push(structuredClone(writes)); const held = this.#commitGate; if (held !== undefined) { held.entered.resolve(); await held.gate.promise; } const failure = this.#commitFailure; if (failure !== undefined) { this.#commitFailure = undefined; throw failure; } return super.commit(writes, commitContext); } override mintId>(): Promise { this.mintCount++; return super.mintId(); } override document(id: DocumentId, at: DocumentPoint, callContext: Context) { this.documentReadCount++; return super.document(id, at, callContext); } override async findDocument( address: DocumentAddress, at: DocumentPoint, callContext: Context, ): Promise { const held = this.#findGate; if (held !== undefined) { held.entered.resolve(); await held.gate.promise; } return super.findDocument(address, at, callContext); } } /** Session kernel plus its controlled storage and every committed publication. */ export function openTestSession(): { readonly storage: ControlledStorage; readonly session: SessionImpl; readonly publications: CommitPublication[]; } { const storage = new ControlledStorage(); // These tests pin the cache's identities and loads, so documents stay as long as the default keeps them. const session = new SessionImpl(storage, Date.now, () => 600_000); const publications: CommitPublication[] = []; session.subscribeCommits((publication) => { publications.push(publication); }); return { storage, session, publications }; } export function documentChanges( publication: CommitPublication, ): readonly Extract[] { return publication.changes.filter( (change): change is Extract => change.type === "document", ); } export function documentCopyChanges( publication: CommitPublication, ): readonly Extract[] { return publication.changes.filter( (change): change is Extract => change.type === "document.copy", ); } /** Create one conversation and return its ID. */ export async function createConversation(session: SessionImpl): Promise { return session.commit(async (tx) => (await tx.createConversation({ ownership: { kind: "ownerless" } })).id, context); } /** Resolve after pending microtasks and one macrotask turn. */ export function flush(): Promise { return new Promise((resolve) => setTimeout(resolve, 0)); }