/** * Notebook — a minimal application written against `@ng-eventually/polyfill`. * * It exists for two reasons, and the second is the one that matters: * * 1. **It shows how to use the library.** Every call here is what a real consumer * writes; there is no test scaffolding, no privileged import, no reaching into the * library's internals. If something is awkward here, it is awkward for everyone. * * 2. **It is what the applicative e2e suite drives** (`packages/polyfill/e2e/notebook.ts`). * The other suite talks to a bag of methods on `window.__sdk`, which proves the * functions run but never that an application could be written with them — and that * gap shipped a real defect: a document's inbox was green in tests and unusable in * practice, because the harness handed the address across an identity boundary * through a variable. No application can do that. This app can only do what an * application can do, so a test that passes here means the surface is usable, not * merely callable. * * ── The domain is deliberately thin ─────────────────────────────────────── * Notes. Each user writes their own, may publish one, may hand a reader the key to a * private one, and may leave a message on someone else's note. That is enough to * exercise placement by store, capability possession, directed sharing, per-document * inboxes and reactive reads — without inventing a product. * * ── Plain DOM, on purpose ───────────────────────────────────────────────── * The library imposes no framework, so its example must not adopt one: a consumer * reading this should see the SDK calls, not a component tree. The UI here is the * shortest thing that makes each act reachable. */ import { // SDK-shaped — these survive migration, the real SDK replaces them in place. docs, ensureIdentity, inbox, init, storeRegistry, subscribeDoc, initNg as initNgSignals, useShapeQuery, type Nuri, type ShapeObservable, type ShapeType, type BaseType, // Polyfill-era — ONE call, and it is the whole of what goes away. configure, } from "@ng-eventually/polyfill"; import { ng as realNg, init as realInit } from "@ng-org/web"; // The SDK's shape query, which `useShapeQuery` reads through since `1.0.0-dev.6`, and // the call that hands the ORM its session. Both are handed to `configure` below, the // same way the `ng` is: this package never imports the SDK itself. import { initNg as ormInitNg, RdfOrmSubscription } from "@ng-org/orm"; // --- the domain, such as it is --------------------------------------------- const TITLE = "urn:notebook:title"; const BODY = "urn:notebook:body"; const NOTE = "urn:notebook:Note"; const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; interface Note { doc: Nuri; title: string; body: string; } /** * Which store a note goes in, as the interface offers it - and the only thing that word * still decides. * * A store is chosen when a note is CREATED, and it is named by its reference: the library * hands out one per store and `createEntityDoc` takes it, exactly where `doc_create` takes * its store. There is no matching question on the way back - **a read cannot be scoped by * store**, upstream or here, so the list below is every note this identity holds, whatever * shelf it was written to. That is not a shortcoming of this example; it is what the * target can express. */ function shelf(name: string): Promise { return name === "public" ? storeRegistry.publicStore() : name === "protected" ? storeRegistry.protectedStore() : storeRegistry.privateStore(); } /** The shape of a note: a subject carrying `rdf:type urn:notebook:Note`. */ const NoteShape = { shape: "urn:notebook:NoteShape", schema: { "urn:notebook:NoteShape": { iri: "urn:notebook:NoteShape", predicates: [ { iri: RDF_TYPE, readablePredicate: "type", minCardinality: 1, maxCardinality: 1, dataTypes: [{ literals: [NOTE], valType: "iri" }], }, // The two fields a note actually carries. A shape declares what comes BACK as // well as what matches: a predicate the shape does not name is not projected onto // the object, so leaving these out would return notes with no title and no body. { iri: TITLE, readablePredicate: "title", minCardinality: 1, maxCardinality: 1, dataTypes: [{ valType: "string" }], }, { iri: BODY, readablePredicate: "body", minCardinality: 1, maxCardinality: 1, dataTypes: [{ valType: "string" }], }, ], }, }, } satisfies ShapeType; // --- bootstrap: ONE polyfill-era call -------------------------------------- // // Everything else an application calls is SDK surface, preserved at migration. This one // is the scaffolding, and at migration it goes: the app imports the real SDK, and the // identity comes from the wallet instead of a barrier. configure({ ng: realNg, useShape: (() => {}) as never, // this app reads through `useShapeQuery`, never the hook ormSubscription: RdfOrmSubscription.getOrCreate, init: realInit, initNg: ormInitNg, sharedWallet: { fileUrl: "/shared-wallet.ngw", password: (globalThis as { __NOTEBOOK_WALLET_PASSWORD__?: string }).__NOTEBOOK_WALLET_PASSWORD__ ?? "", }, }); // The library's `init`, not the injected one — and this line is SDK-shaped, kept at // migration. It settles the identity before handing the page to the broker, so the // round-trip leaves with the identifier in the URL it carries. `realInit` called here // would navigate away first, and the barrier would never show. // // The callback is this application's own business, and only its own: it keeps the session // because the SPARQL primitives below take a `session_id`, exactly as the real SDK's do. // It used to ALSO hand the library a thunk unwrapping this event into a session shape — // plumbing every consumer wrote identically, and this one wrote wrong twice. The library // catches the same event on its way through `init` now (2026-08-12), so what is left here // is only what this app itself reads. const sessionReady = new Promise<{ session_id: string }>((resolve) => { init((event: { status: string; session?: { session_id: string } }) => { if (event.status === "loggedin" && event.session) { // The ORM reaches the SDK through a module-level promise of its own; nothing it // opens starts before this. Through the library's `initNg`, which is the SDK-shaped // call and survives migration. initNgSignals(realNg, event.session); resolve(event.session); } }, true, []); }); // --- the acts --------------------------------------------------------------- /** Write a new note on `shelfName`'s shelf. The document is created, then filled. */ async function writeNote(shelfName: string, title: string, body: string): Promise { const doc = await storeRegistry.createEntityDoc(await shelf(shelfName)); const s = await sessionReady; // The type triple is what makes the note findable afterwards: the reactive read asks // for a SHAPE, and a shape is pinned on `rdf:type`. Nothing indexes it for us. await docs.sparqlUpdate( s.session_id, `INSERT DATA { <${doc}> <${RDF_TYPE}> <${NOTE}> ; <${TITLE}> "${escape(title)}" ; <${BODY}> "${escape(body)}" }`, doc, ); return doc; } /** * My notes, read the way the library offers them: ONE reactive query over everything this * identity holds, shaped as a note. * * `did:ng:i` is the scope that means the whole local dataset - the caller's own documents * are what it can reach. The application does not list its documents and then read them: * there is no call to list them with, and there is no question "what is in this store" to * ask. It names a SHAPE, and what it holds of that shape comes back. */ let notes: ShapeObservable | null = null; function noteQuery(): ShapeObservable { if (!notes) { notes = useShapeQuery(NoteShape, { graphs: "did:ng:i" }); // One subscription for the life of the page: a note that arrives - written here, or // shared by somebody else - re-renders the list with no call from this code. notes.subscribe(() => renderNotes()); } return notes; } /** Wait until the query has ANSWERED - pending is not an answer, and neither is a failure * read as an empty list. */ function answered(q: ShapeObservable): Promise { return new Promise((resolve) => { let off: (() => void) | null = null; const look = (): void => { if (q.getSnapshot().isPending) return; off?.(); resolve(); }; off = q.subscribe(look); look(); }); } function currentNotes(): Note[] { return noteQuery() .getSnapshot() .data.map((s) => ({ // The read hands back the SHAPE's own objects now, not property bags: `@graph` is // the document a note lives in, and each field is the predicate's readable name. doc: s["@graph"] as Nuri, title: typeof s.title === "string" ? s.title : "(sans titre)", body: typeof s.body === "string" ? s.body : "", })); } /** * Read someone else's note from its REFERENCE. * * A reference is what circulates — you do not discover a note, someone gives you its * reference (a message, a URL, a QR code). It carries no key, and that is the point: * if the note is in a PUBLIC store the store hands its key to whoever asks, so the * reference is enough; if it is protected, the reference names the note and opens * nothing, until its owner shares it (see `shareNote`). * * It arrives as a plain string, from a field or a URL, and goes straight in: the * library validates it. Nothing to narrow, nothing to cast, and nothing that will have * to change when the real SDK takes that same string. */ async function readSharedNote(reference: string): Promise { const s = await sessionReady; // ONE anchored query, naming the note. The anchor scopes the read to that document's // own graph, so the body carries no `GRAPH <...>` wrapper; the library asks the public // store for the key on the way in, which is what makes a bare reference enough. // // This was `readUnion([reference])` until `1.0.0-dev.8`. What an application composes // itself now is the projection below - the SELECT and the grouping - because the // primitive that did it was a batch nothing upstream answers. const result = await docs.sparqlQuery( s.session_id, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, reference, ); const rows = (result as { results?: { bindings?: Array> } }) .results?.bindings ?? []; const values = (predicate: string): string[] => rows.filter((r) => r["p"]?.value === predicate).map((r) => r["o"]!.value); if (rows.length === 0) return null; return { doc: reference as Nuri, title: values(TITLE)[0] ?? "(sans titre)", body: values(BODY)[0] ?? "", }; } /** * Hand a reader access to one of my notes. * * Names the NOTE and the PERSON — the two things this app has. Neither the key nor the * recipient's inbox appears: an application will handle neither once this is native * (upstream the verifier fills `ContactDetails.read_cap` itself), so it handles neither * now. Refuses if the note is not mine to share. */ async function shareNote(doc: Nuri, withUser: string): Promise { await inbox.share(doc, withUser); } /** Open a note for messages — only its owner can, and only they will read them. */ async function openNoteForMessages(doc: Nuri): Promise { await storeRegistry.openDocumentInbox(doc); } /** Leave a message on someone else's note. One call, naming the NOTE. */ async function leaveMessage(doc: Nuri, text: string): Promise { await inbox.postToDocument(doc, { payload: { text } }); } /** The messages left on one of my notes — named by the note, like leaving one. */ async function messagesOn(doc: Nuri): Promise { const deposits = await inbox.readForDocument(doc); return deposits.map((d) => String((d.payload as { text?: string })?.text ?? "")); } /** Re-render whenever a note changes — locally or from a peer. */ function watchNote(doc: Nuri, onChange: () => void): () => void { return subscribeDoc(doc, onChange); } // --- identity --------------------------------------------------------------- let identity = ""; /** * Sign in, and learn who you are. * * One await, and it covers everything: the library settles the identity, waits for the * connection work it fires (restoring what others shared with you, draining your * inboxes), and **returns the identity**. The application keeps it only to display it — * no call takes it, because a session belongs to one user and the target's own * `doc_create` carries no user at all. * * This used to read the library's private storage key to find out who it was, which is a * boundary no consumer should be able to see. Writing this application is what made that * visible. */ async function signIn(): Promise { identity = await ensureIdentity(); await sessionReady; } function escape(s: string): string { return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); } // --- the thinnest UI that makes each act reachable -------------------------- const el = (id: string): HTMLElement => document.getElementById(id)!; const val = (id: string): string => (el(id) as HTMLInputElement).value.trim(); async function refresh(): Promise { const q = noteQuery(); q.refetch(); await answered(q); renderNotes(); // What `ensureIdentity()` returns is an OPAQUE identifier, not a display name: the SDK // publishes none. So it is shown verbatim and marked as an identifier — never parsed, // never split, never dressed up as a person's name. const idTag = document.createElement("code"); idTag.textContent = identity; el("who").replaceChildren("id ", idTag); } function renderNotes(): void { el("notes").innerHTML = currentNotes() .map( (n) => `
  • ${n.title} — ${n.body}
    ${n.doc}
  • `, ) .join(""); } function wire(): void { // The shelf chooses where the NEXT note is written, and nothing else: a read cannot be // scoped by store, so the list below always shows every note this identity holds. // Re-rendering on a change is still worth it — a note shared in the meantime shows up. el("scope").addEventListener("change", () => void refresh()); el("write").addEventListener("click", async () => { await writeNote((el("scope") as HTMLSelectElement).value, val("title"), val("body")); await refresh(); }); el("openRef").addEventListener("click", async () => { el("shared").textContent = ""; // never show a previous answer beside a new question const note = await readSharedNote(val("reference")); el("shared").textContent = note ? `${note.title} — ${note.body}` : "(illisible)"; }); el("leave").addEventListener("click", async () => { el("left").textContent = ""; await leaveMessage(val("onNote") as Nuri, val("message")); el("left").textContent = "déposé"; }); el("notes").addEventListener("click", async (e) => { const target = e.target as HTMLElement; const doc = target.dataset.doc as Nuri | undefined; if (!doc) return; el("shareResult").textContent = ""; el("messages").textContent = ""; if (target.classList.contains("share")) { await shareNote(doc, val("shareWith")); el("shareResult").textContent = "partagé"; } else if (target.classList.contains("open")) { await openNoteForMessages(doc); el("shareResult").textContent = "ouverte aux messages"; } else if (target.classList.contains("msgs")) { el("messages").textContent = (await messagesOn(doc)).join(" | ") || "(aucun)"; } }); } async function main(): Promise { wire(); await signIn(); await refresh(); } void main(); // The e2e suite drives this app through the DOM. It exposes nothing else: a test that // needed a back door would be testing something an application cannot do. `watchNote` // is here because reactivity has no visible surface in this UI yet — not as an escape // hatch, and it takes no identity: switching user means reloading with another `?ng-id=`, // exactly as switching upstream means opening another wallet. (globalThis as { __notebook?: unknown }).__notebook = { watchNote };