Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265/** * Test-only in-memory fakes for the vault stack. Imported ONLY by `*.test.ts`. * * These are fakes (real in-memory state), not mocks: assertions are made on * their state — the doc store, on-disk files, the snapshot — rather than on * call order. The repo and the doc session share a single `DocStore`, so edits * made through a session are visible to the engine via the repo and vice-versa, * matching the production relationship where `acquireDocSession` and * `HabitatRepoImpl` wrap the same underlying record. */import * as nodeFs from 'fs';import * as Automerge from '@automerge/automerge';
import type { AutomergeDocSession } from '../../habitat/automergeDocSession';import type { AutomergeDocSchema } from '../../editors/automerge/automergeEditorBinding';import type { FilesystemApi, FsDirent } from '../../filesystem/types';import type { DirectoryDocument, FileDocument, HabitatDocHandle, HabitatRepo, HabitatUri,} from '../types';
// ---------------------------------------------------------------------------// Shared in-memory document store// ---------------------------------------------------------------------------
export type DocStore = Map<HabitatUri, Automerge.Doc<unknown>>;
// ---------------------------------------------------------------------------// Node-native FilesystemApi — a fake of the IPC-backed VaultFilesystemApi that// writes real files to disk so tests can assert on their contents.// ---------------------------------------------------------------------------
export class NodeFilesystemApi implements FilesystemApi { async readdir(dirPath: string): Promise<FsDirent[]> { const entries = nodeFs.readdirSync(dirPath, { withFileTypes: true }); return entries.map((e) => ({ name: e.name, isDirectory: e.isDirectory(), isFile: e.isFile(), })); }
async readFile(filePath: string): Promise<string> { return nodeFs.readFileSync(filePath, 'utf-8'); }
async writeFile(filePath: string, content: string): Promise<void> { nodeFs.writeFileSync(filePath, content, 'utf-8'); }
async fileExists(filePath: string): Promise<boolean> { return nodeFs.existsSync(filePath); }
async mkdir(dirPath: string): Promise<void> { nodeFs.mkdirSync(dirPath, { recursive: true }); }
async rename(oldPath: string, newPath: string): Promise<void> { nodeFs.renameSync(oldPath, newPath); }
async deletePath(filePath: string): Promise<void> { nodeFs.rmSync(filePath, { recursive: true, force: true }); }
async copyFile(src: string, dest: string): Promise<void> { nodeFs.copyFileSync(src, dest); }}
// ---------------------------------------------------------------------------// In-memory HabitatRepo backed by real Automerge over a shared DocStore// ---------------------------------------------------------------------------
class InMemoryDocHandle<T> implements HabitatDocHandle<T> { constructor( private uri: HabitatUri, private store: DocStore, ) {}
private get current(): Automerge.Doc<T> { return this.store.get(this.uri) as Automerge.Doc<T>; }
doc(): Automerge.Doc<T> { return this.current; }
heads(): string[] { return Automerge.getHeads(this.current); }
change(fn: (d: T) => void): void { const next = Automerge.change( this.current, fn as Automerge.ChangeFn<unknown>, ); this.store.set(this.uri, next); }
changeAt(heads: string[], fn: (d: T) => void): void { const result = Automerge.changeAt( this.current, heads, fn as Automerge.ChangeFn<unknown>, ); if (result.newHeads !== null) { this.store.set(this.uri, result.newDoc); } }
view(heads: string[]): Automerge.Doc<T> { return Automerge.view(this.current, heads) as Automerge.Doc<T>; }
on(_event: 'change', _cb: () => void): () => void { return () => {}; }}
export class InMemoryHabitatRepo implements HabitatRepo { private counter = 0;
constructor(public readonly store: DocStore = new Map()) {}
private makeUri(collection: string): HabitatUri { return `at://did:plc:test/${collection}/${++this.counter}` as HabitatUri; }
private handle<T>(uri: HabitatUri): InMemoryDocHandle<T> { return new InMemoryDocHandle<T>(uri, this.store); }
async find<T>(uri: HabitatUri): Promise<HabitatDocHandle<T>> { if (!this.store.has(uri)) { throw new Error(`[InMemoryHabitatRepo] doc not found: ${uri}`); } return this.handle<T>(uri); }
async createDirectory( initial: DirectoryDocument, ): Promise<{ uri: HabitatUri; handle: HabitatDocHandle<DirectoryDocument> }> { const uri = this.makeUri('network.habitat.vault.directory'); this.store.set( uri, Automerge.from(initial as unknown as Record<string, unknown>), ); return { uri, handle: this.handle<DirectoryDocument>(uri) }; }
async createFile( initial: FileDocument, ): Promise<{ uri: HabitatUri; handle: HabitatDocHandle<FileDocument> }> { const uri = this.makeUri('network.habitat.vault.file'); this.store.set( uri, Automerge.from(initial as unknown as Record<string, unknown>), ); return { uri, handle: this.handle<FileDocument>(uri) }; }
async delete(uri: HabitatUri): Promise<void> { this.store.delete(uri); }
/** Test helper: directly read a doc from the store. */ getDoc<T>(uri: HabitatUri): Automerge.Doc<T> | undefined { return this.store.get(uri) as Automerge.Doc<T> | undefined; }
/** Test helper: count docs in the store. */ docCount(): number { return this.store.size; }}
// ---------------------------------------------------------------------------// Fake live document session backed by the same DocStore// ---------------------------------------------------------------------------
export class FakeDocSession implements AutomergeDocSession { released = false; readonly provider = null; readonly node = null; readonly ownerRecord = {} as AutomergeDocSession['ownerRecord']; readonly loadPromise = Promise.resolve();
private listeners = new Set<() => void>();
constructor( public readonly uri: string, private store: DocStore, ) {}
get doc(): Automerge.Doc<AutomergeDocSchema> { return this.store.get( this.uri as HabitatUri, ) as Automerge.Doc<AutomergeDocSchema>; }
getTitle = (): string => { const doc = this.doc as { name?: unknown } | undefined; return typeof doc?.name === 'string' ? doc.name : ''; };
applyChange = (fn: (doc: AutomergeDocSchema) => void): void => { this.store.set( this.uri as HabitatUri, Automerge.change(this.doc, fn as Automerge.ChangeFn<unknown>), ); this.notify(); };
changeAt = (heads: string[], fn: (doc: AutomergeDocSchema) => void): void => { const result = Automerge.changeAt( this.doc, heads, fn as Automerge.ChangeFn<unknown>, ); if (result.newHeads !== null) { this.store.set(this.uri as HabitatUri, result.newDoc); this.notify(); } };
subscribe = (cb: () => void): (() => void) => { this.listeners.add(cb); return () => this.listeners.delete(cb); };
resync = async (): Promise<void> => {};
release = (): void => { this.released = true; };
private notify(): void { for (const listener of this.listeners) listener(); }}
/** * Build a fake session factory over a shared `DocStore`. One `FakeDocSession` * is reused per uri so tests can read its state after opening a document. */export function makeFakeSessions(store: DocStore) { const sessions = new Map<string, FakeDocSession>(); return { acquire: (uri: string): FakeDocSession => { let session = sessions.get(uri); if (!session) { session = new FakeDocSession(uri, store); sessions.set(uri, session); } return session; }, get: (uri: string): FakeDocSession | undefined => sessions.get(uri), };}