# The READ MODEL the polyfill implements How the polyfill turns "give me my lists" into concrete NextGraph reads on the shared wallet. This is a **design decision**, grounded entirely in the query capability documented in [`nextgraph-current-state.md`](./nextgraph-current-state.md) § *The query capability*. The consumer (Festipod) never sees any of this: it asks `@ng-eventually/client` for its lists **by need** and trusts the answer — the whole read mechanism lives here, in the polyfill. The governing constraints (all verified in `nextgraph-rs`, cited there): - One local oxigraph store per session; every opened repo is a **named graph**. - `sparql_query` with **no anchor** → the **LOCAL UNION** of all opened graphs; with an anchor → **one** repo. Union is **read-only**. - A repo is queryable **only after it is opened/synced** (needs its NURI + ReadCap; no store-level read inheritance). - **No reactive union query**, and the reactive ORM **hangs** if handed a per-entity / unsynced graph fan-out (`RepoNotFound` aborts `orm_start_graph`). ## Two read regimes — enumerate vs follow There is **no cross-wallet read** in current NextGraph, so nothing is globally enumerable "for free". The polyfill splits every list into one of two regimes: ### Events (all public) = the GLOBAL INDEX — the ONE enumeration hack Public events are the only thing enumerated across accounts, via the emulated discovery index (`discovery.readIndex`, see [`simulation.md`](./simulation.md) § *Emulated discovery index*). This is the ONE "hack", and it is justified precisely because P2P has no cross-wallet read: without a shared index a client could never learn that another account's public event-doc **exists**. `readIndex` yields the event-doc **NURIs** to open/sync; those repos then enter the local union and become union-queryable. ### Everything else = FOLLOW a graph, never enumerate across accounts My participations / my profile, a connection's shared protected data, my notifications — **none** of these is enumerated across accounts. Each is reached by **what is already reachable to me**: - **my own docs** (always in `self.repos`); - docs reachable via a **connection's shared cap** (a bilateral connection surfaces the peer's protected NURIs — see the bilateral connection registry in [`simulation.md`](./simulation.md)); - my **inbox** (deposits addressed to me). The rule of thumb: **Access ≠ discovery.** You only union-query over graphs you were already entitled to open. ## Listing = open/sync + ONE union query (never the ORM fan-out) To produce a list: 1. **Open/sync** the relevant repos (the index-yielded event NURIs, my own docs, a connection's shared NURIs). This is what puts them in the local union. 2. Run a **single** `sparql_query` with **NO anchor** over the LOCAL UNION, using a `GRAPH ?g { ... }` body so each result row is attributed to its source graph. Do **NOT** drive listing through the reactive ORM's per-document fan-out (`orm_start_graph` over many graphs): a freshly-created or not-yet-synced graph in the fan-out makes `RepoNotFound` abort the whole subscription → the readyPromise never resolves → the ~75s hang (root cause verified in [`nextgraph-current-state.md`](./nextgraph-current-state.md) § *The ORM fan-out hang*). ## Reactivity = re-query on a change signal (no reactive union) 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`. Keep the reactive ORM strictly to already-opened single stores; it is a change *signal* source here, not the list source. ## The boundary with the consumer Festipod asks the SDK for its lists by need (`listMyMeetingPoints()`, `listEvents()`, …) and trusts the returned set. It never constructs a NURI, never picks the union-vs-anchor mode, never touches the ORM. Open/sync + union-query + re-query-on-signal all live in the polyfill. ## Minimal broker probe (confirms the union behaviour) The one experiment that pins down union vs anchor, to run against a real broker: 1. `doc_create` two docs **A** and **B** (own docs → both opened into the session store). 2. `sparql_update` a **distinct** triple into each (target A's `@graph`, then B's). 3. **No anchor** — expect BOTH graphs: ``` sparql_query( sid, "SELECT ?g ?s ?p ?o WHERE { GRAPH ?g { ?s ?p ?o } }", undefined /* base */, undefined /* anchor → UserSite → LOCAL UNION */ ) // → rows from BOTH A's and B's graphs ``` 4. **Anchor = A** — expect only A: ``` sparql_query(sid, "SELECT ?g ?s ?p ?o WHERE { GRAPH ?g { ?s ?p ?o } }", undefined, A /* string NURI → one repo */) // → rows from A's graph only ``` If (3) returns both and (4) returns only A, the union read model above holds as implemented in `resolve_target_for_sparql` / `set_default_graph_as_union`.