diff --git a/README.md b/README.md index 05e7c8c..6f35c4f 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,9 @@ Docs (this library's own engineering doctrine, under [`docs/`](./docs/)): constraint). - [`docs/read-model.md`](./docs/read-model.md) — the READ MODEL the polyfill implements: events via the global index, everything else by following a shared - graph; listing via a one-shot union `sparql_query` (never the ORM fan-out, which - hangs); reactivity via re-query on a change signal. + graph; listing via a bounded set of **per-doc anchored** `sparql_query`s (never + an anchorless union-scan of the physical wallet, never the ORM fan-out — both + hang/time out); reactivity via re-query on a change signal. - [`docs/decisions/`](./docs/decisions/) — historical current-SDK ADRs (private-store scope, SPARQL delete, shared-wallet login, discovery mechanism). - [`docs/fork-inbox-fallback.md`](./docs/fork-inbox-fallback.md) — the Rust-patch / @@ -40,6 +41,29 @@ Docs (this library's own engineering doctrine, under [`docs/`](./docs/)): - [`docs/migration-guide.md`](./docs/migration-guide.md) — the checklist for when real NextGraph matures. +## What is emulated (and how it goes away) + +**Nothing in this library is a real NextGraph feature.** Every behaviour below is +**emulated** — a stopgap fabricated on top of the *current, immature* NextGraph +(one shared wallet, everything physically readable). Each has a **real NextGraph +target**, and the switch to it is a **lib-only swap: the consumer's SDK-shaped code +does not change** (see [`docs/migration-guide.md`](./docs/migration-guide.md)). The +consumer always sees the mature SDK face; the emulation lives entirely here. + +| Behavior | What the consumer sees (SDK-shaped API) | How it's emulated today (on one shared wallet) | Real NextGraph target | Migration (what changes; consumer unchanged) | +|---|---|---|---|---| +| **Multi-user / per-user wallet** | Each username is its own identity with its own documents | **One shared wallet** everyone opens; "users" are **virtual wallets** — shim accounts keyed by a **virtual-wallet id**, each mapped to its documents in `store-registry.ts` ([`simulation.md`](./docs/simulation.md#physical-wallet-vs-virtual-wallet--never-enumerate-the-physical-one)) | One real per-user wallet each; native cross-wallet reads | Each virtual wallet → a real wallet; drop the shim; the physical/virtual split dissolves | +| **3 native stores per user** | `public` / `protected` / `private` scopes | **3 emulated scope-index documents** per account — each "store" is an index doc listing its entity-doc NURIs; all physically live in the ONE shared private store (`docCreate(..., undefined)`), scope is a **logical label** ([`simulation.md`](./docs/simulation.md#a-virtual-wallets-structure--the-three-emulated-stores)) | The user's 3 real native stores hold the entity documents | The 3 index docs become the 3 real stores; the logical scope label becomes real placement | +| **Per-document read isolation** | `getCaps().open(doc, scope, owner)` — the *acts* of granting | Emulated **`CapRegistry`** (`caps.ts`, per-document ReadCap) + **read filter** (`read-filter.ts`, defence-in-depth view) + read-set construction in `read-model.ts`; owner/connections injected as principals ([`simulation.md`](./docs/simulation.md#emulated-readcap--per-document-capsts--read-filterts)) | Broker/verifier only delivers documents the wallet holds a ReadCap for; `useShape` already returns an authorized subset | Translate the registry to real caps; **delete** the read filter (dead code) — access unit is already the document (`@graph`) | +| **Bilateral connections** | `declareConnections(peers)` — declare your own side | Emulated **connection registry** (`connections.ts`): directed assertions, a link is live only when **both** sides assert (two-sided) → drives protected read grants ([`simulation.md`](./docs/simulation.md#making-the-readcap-active--current-user--connection-driven-grants)) | Mutual capability exchange between two wallets | Real mutual caps replace the materialized link; drop the registry | +| **Inbox (registration notifications)** | `inbox.post` / `read` / `watch` | Emulated **deposits via SPARQL** into an inbox document + **in-client materialization** (curator played inline); no native `inbox_post` exists in the JS SDK ([`simulation.md`](./docs/simulation.md#emulated-inbox--curator-inboxts), [`nextgraph-current-state.md`](./docs/nextgraph-current-state.md#inbox)) | Native per-document inbox: `inbox_post_link` seals a deposit; a separate curator materializes it | `post` → native `inbox_post_link`; read side → a **separate curator package** (deferred) | +| **Discovery of all public events** | `submitToIndex(ref)` / `readIndex()` | Emulated **global index** = a document **owned by a reserved special account** (`@index`), fed via **its inbox** + inline **curator** (dedup); a **stable NURI** every client resolves. **NOT a physical-wallet scan** ([`simulation.md`](./docs/simulation.md#emulated-discovery-index--special-account-discoveryts), [`read-model.md`](./docs/read-model.md)) | A real owned global document (undecided owner — singleton-app path only glimpsed), fed via its native inbox, materialized by a curator | Special account disappears; ownership moves to the decided owner; `submitToIndex` → native inbox post; curator queries the real index | +| **Reads / listing** | `listMyMeetingPoints()`, `listEvents()`, … by need | **Per-doc ANCHORED `sparql_query`** over the **virtual** wallet's by-need doc set — **never an anchorless scan of the physical wallet** (O(wallet), ~90s timeout) and never the ORM fan-out (~75s hang) ([`read-model.md`](./docs/read-model.md)) | Native per-wallet reads over real per-user stores | The anchored read is already native; only *bringing a repo into the session* becomes a real broker sync (the `OpenRepo` TODO) | +| **Reactivity** | Lists update on change | **Re-query** the bounded per-doc anchored set on a lightweight change signal (`doc_subscribe` / ORM on an already-opened single store) — there is **no reactive union query** ([`read-model.md`](./docs/read-model.md#reactivity--re-query-on-a-change-signal-no-reactive-union)) | Native reactive reads | Re-query pattern collapses onto native reactive primitives | +| **Writes** | Write an entity to its scope | **Per-entity documents** via **direct SPARQL** (`docs.sparqlUpdate` on the real injected `ng`); `doc_create` can only target the **private** store today (`StoreRepo` not JS-constructible) ([`simulation.md`](./docs/simulation.md#reponotfound-and-the-orm_start_graph-scope-rule)) | Writes land in the entity's real store via native primitives | `docCreate` targets the real per-scope store once the SDK lets you construct one | +| **Login** | `login(username)` / `logout()` | **Faux `localStorage` identity** (`accounts.ts`): declarative username, no password, no NextGraph call — the shared-wallet broker gate stays open underneath ([`simulation.md`](./docs/simulation.md#faux-login-accountsts)) | Opening your **own** wallet at the broker gate IS the login | Remove faux login; the broker redirect becomes the real per-user login (flow shape unchanged) | +| **Write-guard** | Writes refused without the write cap | **Best-effort**: the guard (`ng-proxy.ts`) fires only on the public proxy, but real write paths call the injected `ng` directly (the `DataCloneError` constraint) → **not guarded today** ([`simulation.md`](./docs/simulation.md#write-guard-coverage-honest-scope)) | Broker/verifier enforces the write cap natively | Native enforcement replaces the guard; delete it (dead code) | + ## Packages | Package | Role | At migration | diff --git a/docs/read-model.md b/docs/read-model.md index 236f0a5..5f2326b 100644 --- a/docs/read-model.md +++ b/docs/read-model.md @@ -95,7 +95,8 @@ There is **no reactive union query**. So reactivity is assembled: - keep a lightweight reactive subscription — `doc_subscribe`, or the ORM on an **already-opened single store** (never a per-entity fan-out) — on the synced docs; -- on its change signal, **re-run** the one-shot union `sparql_query`. +- on its change signal, **re-run** the bounded set of per-doc anchored + `sparql_query`s (`readModel.readUnion`) — never an anchorless union-scan. Keep the reactive ORM strictly to already-opened single stores; it is a change *signal* source here, not the list source. diff --git a/docs/simulation.md b/docs/simulation.md index faed6e9..44d89a3 100644 --- a/docs/simulation.md +++ b/docs/simulation.md @@ -1,5 +1,16 @@ # How this library emulates mature NextGraph on ONE shared wallet +> **EVERYTHING in this file is EMULATION.** Not one behaviour described here is a +> real NextGraph feature: each is a **stopgap** the lib fabricates on top of the +> *current, immature* NextGraph (the exact gaps it compensates for are in +> [`nextgraph-current-state.md`](./nextgraph-current-state.md)). Every piece has a +> **real target** and goes away when NextGraph matures — the swap is **lib-only**, +> the consumer's code is unchanged. The per-behaviour recap table lives in the +> top-level [`README.md`](../README.md) (*What is emulated (and how it goes away)*); +> the removal checklist is [`migration-guide.md`](./migration-guide.md). Read this +> file for *how* each emulation works; read those two for *what is fake* and *what +> replaces it*. + The consumer writes against `@ng-eventually/client` **as if** NextGraph already shipped per-entity documents in public/protected/private stores, capabilities and inboxes. It hasn't (see [`nextgraph-current-state.md`](./nextgraph-current-state.md)). @@ -103,10 +114,12 @@ public/protected/private stores — on top of one shared wallet. handing the NURIs to `useShape({ graphs })` opens/syncs other accounts' possibly- unsynced docs and HANGS (the ORM fan-out, ~75s — see [`read-model.md`](./read-model.md)). The real READ path is - `readModel.readUnion(docs)` (open/sync by-need + ONE anchorless union - `sparql_query`); the app resolves the by-need doc set from the discovery index - (public events) and `listMyEntityDocs(username, scope)` (my own account, bounded — - no cross-account fan-out). + `readModel.readUnion(docs)`, which reads the by-need doc set with **one PER-DOC + ANCHORED `sparql_query`** — **never an anchorless union-scan** of the physical + wallet (that is O(wallet size) and timed out ~90s; see + [`read-model.md`](./read-model.md)). The app resolves the by-need doc set from the + discovery index (public events) and `listMyEntityDocs(username, scope)` (my own + account, bounded — no cross-account fan-out). - **GENERIC by construction.** The registry knows only the three native scopes, **zero** application entity kind. The consumer maps its entities to a scope and injects the session + username normalization via `configureStoreRegistry({