/** * wallet-fake — a durable fake broker, and the page RELOAD that runs over it. * * Not a `*.test.ts`, so `bun test` does not pick it up: it is the montage two suites share * ({@link reloadOwnDocument} / the inbox drain), and both of them are about what survives a * reload — which is exactly the thing a per-file fake cannot express, because the wallet * has to outlive the library while the library keeps nothing. * * ── What makes it a wallet and not a stub ───────────────────────────────── * The quads live in the fake, never in the library. {@link reloadPage} drops every piece of * the library's module state and hands back a session that has to find its way home through * the store-root pointer, the doc-shim and the account record — the way a fresh page does. * Nothing is planted: a second session sees exactly what the first one WROTE. * * The SPARQL it answers is a tokenizer plus five shapes, rather than one regex per query * the author happened to think of: the reload path issues reads and writes from six * modules, and a fake that answers only the shapes someone enumerated is how a suite goes * green over a state the library never reaches. */ import { mock } from "bun:test"; import { configure } from "../src/index"; import { configureStoreRegistry, setCurrentUser, resetCaps, resetConfig, resetStoreRegistry, } from "../src/shared-wallet/bootstrap"; import { resetRegistryCache } from "../src/shared-wallet/account-registry"; import type { RegistrySession } from "../src/shared-wallet/account-registry"; import { resetOpenedRepos } from "../src/emulated-verifier/open-repo"; import { resetPublicStoreFetches } from "../src/emulated-verifier/public-store"; import { connectedUser } from "../src/emulated-verifier/connect"; import type { NgLike, UseShapeLike } from "../src/model/types"; export const SESSION: RegistrySession = { sessionId: "sid-wallet", privateStoreId: "PRIV-WALLET" }; const SHIM = "urn:ng-eventually:shim"; const INBOX = "urn:ng-eventually:inbox"; const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; export interface Quad { g: string; s: string; p: string; o: string; } /** * The repo ids this fake broker has ever handed out — MONOTONIC, and deliberately not a * counter inside {@link makeWallet}. * * A per-wallet counter restarted at each {@link bootPage}, so a reloaded page re-issued the * NURIs the previous one had minted: a document created after a reload came back as * `did:ng:o:doc6` when `did:ng:o:doc6` was already somebody else's inbox, and the two * aliased into one repo with no error anywhere. A broker never mints a repo id twice — an * id is a public key — so neither does this. */ let minted = 0; /** Reverse of the lib's `escapeLiteral`: one left-to-right pass over `\x`. */ function unescapeLiteral(s: string): string { let out = ""; for (let i = 0; i < s.length; i++) { if (s[i] === "\\" && i + 1 < s.length) { const next = s[++i]; out += next === "n" ? "\n" : next === "r" ? "\r" : next === "t" ? "\t" : next!; } else out += s[i]; } return out; } interface Token { kind: "term" | "sep"; value: string; } /** Tokenize a triple body into IRIs, literals, `a`, `;` and `.`. */ function tokenize(body: string): Token[] { const re = /<([^>]*)>|"((?:[^"\\]|\\.)*)"|(;)|(\.)|\ba\b/g; const out: Token[] = []; let m: RegExpExecArray | null; while ((m = re.exec(body)) !== null) { if (m[1] !== undefined) out.push({ kind: "term", value: m[1] }); else if (m[2] !== undefined) out.push({ kind: "term", value: unescapeLiteral(m[2]) }); else if (m[3] !== undefined) out.push({ kind: "sep", value: ";" }); else if (m[4] !== undefined) out.push({ kind: "sep", value: "." }); else out.push({ kind: "term", value: RDF_TYPE }); } return out; } /** ` p o ; p o . p o` → the triples it carries. */ function parseTriples(body: string): Array<{ s: string; p: string; o: string }> { const toks = tokenize(body); const out: Array<{ s: string; p: string; o: string }> = []; let subject: string | null = null; let i = 0; while (i < toks.length) { const t = toks[i]!; if (t.kind === "sep") { if (t.value === ".") subject = null; i += 1; continue; } if (subject === null) { subject = t.value; i += 1; continue; } const p = toks[i]; const o = toks[i + 1]; if (!p || !o || p.kind === "sep" || o.kind === "sep") break; out.push({ s: subject, p: p.value, o: o.value }); i += 2; } return out; } export interface FakeWallet { doc_create: ReturnType; sparql_update: ReturnType; sparql_query: ReturnType; /** Present only under {@link WalletOptions.unsyncedUntilSubscribed}. */ doc_subscribe?: ReturnType; _quads: Quad[]; } export interface WalletOptions { /** * Model the broker's cold start: a repo this PAGE has not subscribed to answers an * anchored read with **nothing**, and `doc_subscribe` is what brings its commits into * view (pushing the first `State` — the sync barrier `ensureRepoOpen` awaits). * * ── Why this is the real system's state, not a convenient one ───────────── * On a fresh session over the same persistent wallet, `Verifier::load` repopulates * `self.repos` from user storage, so the repo is PRESENT but unsynced and the anchored * query legitimately matches nothing — no error, no rows (the mechanism written out in * `emulated-verifier/open-repo.ts`, corrected there on 2026-08-03). Two consequences the * fake keeps faithfully: * * - a repo CREATED on this page is synced by construction (`doc_create` opens it, and * there is no remote history to fetch), which is why the defect is invisible to the * session that wrote the data; * - a WRITE does not sync anything. Appending a commit to a repo whose remote commits * have not arrived leaves them just as absent, so `sparql_update` never marks a repo * synced — only `doc_subscribe` does. * * OFF by default: the two reload suites that predate this run without a `doc_subscribe` * at all, where `ensureRepoOpen` is the documented no-op of the unit-fake path. */ unsyncedUntilSubscribed?: boolean; } /** * A quad-store fake `ng` over `quads` — the durable half. The library holds nothing across * a {@link reloadPage}; this does. * * By default no `doc_subscribe`: `ensureRepoOpen` is then the documented no-op of the * unit-fake path (`emulated-verifier/open-repo.ts`), so an anchored read resolves directly. * A limit of the fake broker, not a library state — and the one * {@link WalletOptions.unsyncedUntilSubscribed} lifts, for the suites that are about the * sync barrier itself. */ export function makeWallet(quads: Quad[], options: WalletOptions = {}): FakeWallet { /** The repos whose commits this PAGE can see — created here, or subscribed to. */ const synced = new Set(); const cold = options.unsyncedUntilSubscribed === true; const doc_create = mock(async () => { const nuri = `did:ng:o:doc${++minted}`; // Created here: nothing remote to wait for. This is why the session that wrote the // data never sees the cold-start defect, and the next one does. synced.add(nuri); return nuri; }); const doc_subscribe = mock(async (...a: unknown[]) => { const nuri = a[0] as string; const onChange = a[2] as (r: unknown) => void; synced.add(nuri); // `TabInfo` first, then the initial `State` — the platform's own order, so a waiter // that resolved on "the first push of any kind" would return BEFORE the barrier. setTimeout(() => onChange({ V0: { TabInfo: {} } }), 0); setTimeout(() => onChange({ V0: { State: {} } }), 0); return () => {}; }); const sparql_update = mock(async (...a: unknown[]) => { const query = a[1] as string; const anchor = a[2] as string | undefined; const del = query.match(/DELETE\s+WHERE\s*\{([\s\S]*)\}/i); if (del) { const pattern = del[1]!.match(/<([^>]+)>\s+<([^>]+)>\s+\?/); if (pattern && anchor !== undefined) { for (let i = quads.length - 1; i >= 0; i--) { const q = quads[i]!; if (q.g === anchor && q.s === pattern[1] && q.p === pattern[2]) quads.splice(i, 1); } } return undefined; } const wrapped = query.match(/GRAPH\s+<([^>]+)>\s*\{([\s\S]*)\}/); const g = wrapped ? wrapped[1]! : anchor; if (g === undefined) return undefined; const body = wrapped ? wrapped[2]! : query.replace(/^[\s\S]*?INSERT\s+DATA\s*\{/i, "").replace(/\}\s*$/, ""); for (const t of parseTriples(body)) quads.push({ g, ...t }); return undefined; }); const sparql_query = mock(async (...a: unknown[]) => { const query = a[1] as string; const anchor = a[3] as string | undefined; const wrapped = query.match(/GRAPH\s+<([^>]+)>/); const g = wrapped ? wrapped[1]! : anchor; // The repo the verifier resolves the read against — the anchor when there is one, // otherwise the graph named in the query. const target = anchor ?? g; // COLD: present but unsynced. No error, no rows — which is exactly why it is dangerous. if (cold && target !== undefined && !synced.has(target)) return { results: { bindings: [] } }; const inGraph = quads.filter((q) => q.g === g); // The whole-document read (`read-model.readDoc`). if (/SELECT\s+\?s\s+\?p\s+\?o/.test(query)) { return { results: { bindings: inGraph.map((q) => ({ s: { value: q.s }, p: { value: q.p }, o: { value: q.o }, })), }, }; } // The account record — several predicates on one subject. if (query.includes(`<${SHIM}:docPublic>`)) { const subjM = query.match(/<([^>]+)>\s+a\s+<[^>]*:Account>/); const only = subjM ? subjM[1]! : null; const bySubject = new Map>(); for (const q of inGraph) { if (only !== null && q.s !== only) continue; const rec = bySubject.get(q.s) ?? {}; if (q.p === `${SHIM}:id`) rec.id = q.o; if (q.p === `${SHIM}:docPublic`) rec.docPublic = q.o; if (q.p === `${SHIM}:docProtected`) rec.docProtected = q.o; if (q.p === `${SHIM}:docPrivate`) rec.docPrivate = q.o; bySubject.set(q.s, rec); } const bindings = [...bySubject.values()] .filter((r) => r.id !== undefined) .map((r) => ({ id: { value: r.id! }, docPublic: { value: r.docPublic ?? "" }, docProtected: { value: r.docProtected ?? "" }, docPrivate: { value: r.docPrivate ?? "" }, })); return { results: { bindings } }; } // An inbox's deposits — several predicates on one subject, `from` optional. if (query.includes(`<${INBOX}:payload>`)) { const bySubject = new Map>(); for (const q of inGraph) { const rec = bySubject.get(q.s) ?? {}; if (q.p === `${INBOX}:payload`) rec.payload = q.o; if (q.p === `${INBOX}:ts`) rec.ts = q.o; if (q.p === `${INBOX}:from`) rec.from = q.o; bySubject.set(q.s, rec); } const bindings = [...bySubject.values()] .filter((r) => r.payload !== undefined && r.ts !== undefined) .map((r) => { const row: Record = { payload: { value: r.payload! }, ts: { value: r.ts! }, }; if (r.from !== undefined) row.from = { value: r.from }; return row; }); return { results: { bindings } }; } // Everything else the library reads is one bound subject, one bound predicate, one // variable: the pointer, the store index, the Store/User/Header branches, the inbox // index and its owner. const one = query.match(/<([^>]+)>\s+<([^>]+)>\s+\?(\w+)/); if (one) { const bindings = inGraph .filter((q) => q.s === one[1] && q.p === one[2]) .map((q) => ({ [one[3]!]: { value: q.o } })); return { results: { bindings } }; } return { results: { bindings: [] } }; }); return cold ? { doc_create, doc_subscribe, sparql_update, sparql_query, _quads: quads } : { doc_create, sparql_update, sparql_query, _quads: quads }; } /** Wire the library onto `quads` — what a page load does. */ export function bootPage(quads: Quad[], options: WalletOptions = {}): FakeWallet { const ng = makeWallet(quads, options); configure({ ng: ng as unknown as NgLike, useShape: (() => {}) as unknown as UseShapeLike }); configureStoreRegistry({ getSession: async () => SESSION, normalizeId: (id) => id.trim() }); return ng; } /** * Drop everything the library holds in module scope — what a page reload does. * * Each call drops one module's state, and together they are all of it: the config and the * captured session, the registry caches (accounts, the resolved doc-shim, the inbox index), * the opened repos, the outer-overlay memo, the caps, and who is connected. */ export function forgetEverything(): void { setCurrentUser(null); resetConfig(); resetStoreRegistry(); resetRegistryCache(); resetOpenedRepos(); resetPublicStoreFetches(); resetCaps(); } /** A page RELOAD: the library forgets, the wallet does not. */ export function reloadPage(quads: Quad[], options: WalletOptions = {}): FakeWallet { forgetEverything(); return bootPage(quads, options); } /** * Sign in and let the connection work finish — what `ensureIdentity()` awaits, reached by * the harness's internal path rather than through the `barrier` (there is no DOM here). * * It REJECTS exactly where `ensureIdentity()` would, which is the point: a suite about a * sign-in that fails cannot use a sign-in that cannot fail. */ export async function signIn(id: string): Promise { setCurrentUser(id); await connectedUser(); }