import type { NextGraphPort, Nuri, NuriLike } from "../src/port"; import { asNuri } from "../src/nuri"; /** * An in-memory NextGraph, standing in for `@ng-eventually/polyfill` behind * `NextGraphPort`. * * Every rule below is one the polyfill actually enforces, annotated with where it * comes from — the published contract, or the code implementing it. The point is * that no test here can reach a state the real system does not produce: * * - only a document's owner writes to it, and a read key never grants a write * (`contract_polyfill-surface`, "Guarantees"); * - anyone may DEPOSIT into a document's inbox (`inbox.postToDocument`); * - depositing into a document whose owner never opened an inbox THROWS, rather * than silently going nowhere; * - opening an inbox on a document is refused to anyone but its owner * (`openDocumentInbox`: doing so publishes the document's address); * - a document nobody created cannot be named: reaching for one REJECTS. * * ## Reading is done from OUTSIDE the port, on purpose * * This package neither reads a document nor reads an inbox — what an index receives * is made into entries by the layer below. So `contentsOf` and `depositsIn` are * inspections of this double, not operations of the port: a test asks what the two * acts LEFT BEHIND, and cannot accidentally hand the package back a way to read. */ /** One deposit this double is holding, as its inbox holds it. */ export interface StoredDeposit { /** The depositor, as the polyfill defaults it to the current user. */ readonly from: string; readonly payload: unknown; readonly ts: number; } type Properties = Map; interface StoredDocument { readonly nuri: Nuri; readonly owner: string; readonly subjects: Map; /** `undefined` until the owner opens one — the state `postToDocument` refuses. */ deposits: StoredDeposit[] | undefined; } export class FakeNextGraph { readonly #documents = new Map(); #documentCount = 0; #clock = 0; /** A handle bound to one identity, exactly as a polyfill session is one user's. */ portFor(user: string): NextGraphPort { const network = this; return { async createPublicDocument(): Promise { return network.#createDocument(user); }, async addLiteralProperty( doc: NuriLike, subject: string, predicate: string, value: string, ): Promise { network.#add(user, asNuri(doc), subject, predicate, value); }, async openInbox(doc: NuriLike): Promise { network.#openInbox(user, asNuri(doc)); }, async depositTo(doc: NuriLike, payload: unknown): Promise { network.#deposit(user, asNuri(doc), payload); }, }; } /** Every triple a document holds, read from outside the port. */ contentsOf(doc: NuriLike): { subject: string; predicate: string; values: string[] }[] { const stored = this.#require(asNuri(doc)); const out: { subject: string; predicate: string; values: string[] }[] = []; for (const [subject, properties] of stored.subjects) { for (const [predicate, values] of properties) out.push({ subject, predicate, values }); } return out; } /** * What is waiting in a document's inbox, oldest first — or `null` when its owner * never opened one, which is a state and not a failure. */ depositsIn(doc: NuriLike): readonly StoredDeposit[] | null { const stored = this.#require(asNuri(doc)); if (stored.deposits === undefined) return null; return [...stored.deposits].sort((a, b) => a.ts - b.ts); } #createDocument(owner: string): Nuri { this.#documentCount += 1; const nuri = `did:ng:o:doc-${this.#documentCount}` as Nuri; this.#documents.set(nuri, { nuri, owner, subjects: new Map(), deposits: undefined }); return nuri; } #require(doc: Nuri): StoredDocument { const stored = this.#documents.get(doc); if (stored === undefined) throw new Error(`cannot open ${doc}`); return stored; } #add(user: string, doc: Nuri, subject: string, predicate: string, value: string): void { const stored = this.#require(doc); if (stored.owner !== user) { throw new Error( `${user} may not write ${doc}: only a document's owner writes to it, and ` + "holding its read key never grants a write", ); } let properties = stored.subjects.get(subject); if (properties === undefined) { properties = new Map(); stored.subjects.set(subject, properties); } const values = properties.get(predicate); if (values === undefined) { properties.set(predicate, [value]); return; } // An INSERT of a triple already present is a no-op in RDF: a graph is a set. if (!values.includes(value)) values.push(value); } #openInbox(user: string, doc: Nuri): void { const stored = this.#require(doc); if (stored.owner !== user) { throw new Error( `${user} may not open an inbox on ${doc}: opening one publishes the ` + "document's address, so a non-owner would route the owner's deposits to itself", ); } stored.deposits ??= []; } #deposit(user: string, doc: Nuri, payload: unknown): void { const stored = this.#require(doc); if (stored.deposits === undefined) { throw new Error(`${doc} has no inbox — its owner never opened one`); } this.#clock += 1; stored.deposits.push({ from: user, payload, ts: this.#clock }); } } /** * A public document holding one business entity with a value for `field` — what * an application creates and then hands to an index. * * Built through the very same port an application has: nothing here reaches * behind the surface to plant a state a real caller could not produce. */ export async function publishObject( port: NextGraphPort, field: string, value: string, ): Promise { const object = await port.createPublicDocument(); await port.addLiteralProperty(object, object, field, value); return object; }