5acc07a7e3
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>
115 lines
5.0 KiB
Markdown
115 lines
5.0 KiB
Markdown
# 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`.
|