/** * Notebook — a minimal application written against `@ng-eventually/sdk`. * * 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/sdk/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 scope, 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, readUnion, storeRegistry, subscribeDoc, connectedUser, type Nuri, type Scope, // Polyfill-era — these three go away, and they are the whole of what goes away. configure, configureStoreRegistry, setCurrentUser, } from "@ng-eventually/sdk"; import { ng as realNg, init as realInit } from "@ng-org/web"; // --- the domain, such as it is --------------------------------------------- const TITLE = "urn:notebook:title"; const BODY = "urn:notebook:body"; interface Note { doc: Nuri; title: string; body: string; } // --- bootstrap: the two polyfill-era calls --------------------------------- // // Everything else an application calls is SDK surface, preserved at migration. These // two are the scaffolding: `configure` becomes inert (the app will import the real SDK) // and the identity will come from the wallet instead of a barrier. let session: { session_id: string } | null = null; const sessionReady = new Promise<{ session_id: string }>((resolve) => { realInit((event: { status: string; session?: { session_id: string } }) => { if (event.status === "loggedin" && event.session) { session = event.session; resolve(event.session); } }, true, []); }); configure({ ng: realNg, useShape: (() => {}) as never, // this example reads through `readUnion`, not the ORM init: realInit, sharedWallet: { fileUrl: "/shared-wallet.ngw", password: (globalThis as { __NOTEBOOK_WALLET_PASSWORD__?: string }).__NOTEBOOK_WALLET_PASSWORD__ ?? "", }, }); configureStoreRegistry({ getSession: async () => { const s = session ?? (await sessionReady); return { sessionId: s.session_id, privateStoreId: (s as Record).private_store_id!, protectedStoreId: (s as Record).protected_store_id, publicStoreId: (s as Record).public_store_id, }; }, normalizeId: (id) => id.trim().replace(/^@/, "").toLowerCase(), }); // --- the acts --------------------------------------------------------------- /** Write a new note in `scope`. The document is created, then filled. */ async function writeNote(scope: Scope, title: string, body: string): Promise { const me = currentIdentity(); const doc = await storeRegistry.createEntityDoc(me, scope); const s = await sessionReady; await docs.sparqlUpdate( s.session_id, `INSERT DATA { <${doc}> <${TITLE}> "${escape(title)}" ; <${BODY}> "${escape(body)}" }`, doc, ); return doc; } /** My notes in `scope`, read the way the library intends: list, then read. */ async function myNotes(scope: Scope): Promise { const docsOfScope = await storeRegistry.listMyEntityDocs(currentIdentity(), scope); const subjects = await readUnion(docsOfScope); return subjects.map((s) => ({ doc: s.subject, title: s.props[TITLE]?.[0] ?? "(sans titre)", body: s.props[BODY]?.[0] ?? "", })); } /** * 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 [note] = await readUnion([reference]); if (!note) return null; return { doc: note.subject, title: note.props[TITLE]?.[0] ?? "(sans titre)", body: note.props[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 = ""; function currentIdentity(): string { if (!identity) throw new Error("not signed in yet"); return identity; } /** * Sign in. The library shows its access barrier when it needs one; the day the wallet * supplies the identity, this resolves silently and nothing here changes. * * The `connectedUser()` await is not optional decoration, and the applicative e2e is * what found that out: setting an identity FIRES the connection work — restoring what * others shared with you, draining your inboxes — but does not wait for it. Render * before it lands and a note someone just shared reads as unreadable, which looks like * a permission problem and is a timing one. At migration this becomes the session * opening, and the await stays exactly where it is. */ async function signIn(): Promise { await ensureIdentity(); await sessionReady; identity = readIdentityBack(); await connectedUser(); } /** The library owns the identity; the app asks for it rather than remembering it. */ function readIdentityBack(): string { return new URLSearchParams(location.search).get("ng-id") ?? localStorage.getItem("ng-eventually:identity") ?? ""; } 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 scope = (el("scope") as HTMLSelectElement).value as Scope; const notes = await myNotes(scope); el("notes").innerHTML = notes .map( (n) => `
  • ${n.title}${n.body}
    ${n.doc}
  • `, ) .join(""); el("who").textContent = identity; } function wire(): void { // Changing the scope changes which notes are listed — without this the list keeps // showing the previous scope's notes, which reads as "my note disappeared". el("scope").addEventListener("change", () => void refresh()); el("write").addEventListener("click", async () => { await writeNote((el("scope") as HTMLSelectElement).value as Scope, 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. (globalThis as { __notebook?: unknown }).__notebook = { watchNote, setCurrentUser };