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>
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_querywith 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 (
RepoNotFoundabortsorm_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:
- 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.
- Run a single
sparql_querywith NO anchor over the LOCAL UNION, using aGRAPH ?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:
-
doc_createtwo docs A and B (own docs → both opened into the session store). -
sparql_updatea distinct triple into each (target A's@graph, then B's). -
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 -
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.