Files
ng-eventually/docs/read-model.md
T
Sylvain Duchesne 5acc07a7e3 docs: query capability (local-union sparql_query) + the read model
Source-verified against nextgraph-rs:
- nextgraph-current-state.md: NextGraph keeps ONE local oxigraph store per
  session; each synced repo is a named graph. sparql_query with NO anchor
  (UserSite/None) queries the UNION of all synced graphs (set_default_graph_as
  _union); with an anchor it is restricted to one repo. Union is read-only
  (updates need a doc anchor). No reactive SPARQL (one-shot). Root cause of the
  ORM fan-out hang: orm_start_graph opens every graph in scope; a fresh/unsynced
  per-entity doc → RepoNotFound aborts the subscription → the 75s never-fires.
- read-model.md (new): the read model — events via the global index (the one
  enumeration hack); everything else by following a shared graph, opened/synced,
  then listed via a single anchorless union sparql_query (never the ORM per-doc
  fan-out); reactivity via re-query on a doc_subscribe/ORM change signal. Plus
  the minimal broker probe to confirm the union behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 17:32:22 +02:00

5.0 KiB

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 § 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 § 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);
  • 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 § 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.