Ng eventually #1

Open
Sylvain wants to merge 110 commits from ng-eventually into main
6 changed files with 31 additions and 104 deletions
Showing only changes of commit 8ca79c6d16 - Show all commits
+8
View File
@@ -0,0 +1,8 @@
# Doc-debt — data-layer
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
## Raw markers (consolidate into blocks, then delete)
- TOUCHED src/shared/data/readEntities.ts @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/shared/context/FestipodDataContext.tsx @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
@@ -1,16 +0,0 @@
# language: fr
# THROWAWAY probe (T03.k) — pins the read-model union premise on the REAL broker.
# Remove after the read-model refactor lands.
@data @probe
Fonctionnalité: Probe du modèle de lecture (union locale sparql_query)
# VERIFIED on the real broker (T03.k): a GRAPH ?g { } body sans anchor voit
# l'UNION LOCALE de tous les graphes synchronisés c'est la prémisse du modèle
# de lecture (listing = open/sync + une seule requête union sans anchor). Un
# corps GRAPH ?g explicite itère sur TOUS les graphes nommés indépendamment du
# graphe par défaut : l'anchor ne restreint donc PAS un tel motif (il ne borne
# que le graphe par défaut). Le modèle n'a besoin que de l'union sans anchor.
Scénario: sparql_query sans anchor renvoie l'union locale des graphes synchronisés
Étant donné deux documents A et B contenant chacun un triplet distinct
Quand j'interroge l'union locale sans anchor
Alors la requête sans anchor voit A et B
@@ -1,25 +0,0 @@
import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from 'chai';
import type { FestipodWorld } from '../../../../shared/support/world';
// THROWAWAY probe steps (T03.k) — assert the read-model union premise against the
// REAL broker via window.__testData.runUnionProbe (harness-ng). Remove with the
// feature after the read-model refactor lands.
Given('deux documents A et B contenant chacun un triplet distinct', async function (this: FestipodWorld) {
const res = await this.appFrame!.evaluate(async () => await (window as any).__testData.runUnionProbe());
(this as any).unionProbe = res;
expect(res?.docA, 'doc A NURI').to.be.a('string');
expect(res?.docB, 'doc B NURI').to.be.a('string');
});
When("j'interroge l'union locale sans anchor", function (this: FestipodWorld) {
// The probe ran the query inside runUnionProbe; nothing more to do here.
expect((this as any).unionProbe, 'probe result').to.exist;
});
Then('la requête sans anchor voit A et B', function (this: FestipodWorld) {
const r = (this as any).unionProbe;
expect(r.unionHasA, `union must see A (objs=${JSON.stringify(r.unionObjs)})`).to.equal(true);
expect(r.unionHasB, `union must see B (objs=${JSON.stringify(r.unionObjs)})`).to.equal(true);
});
+6 -6
View File
@@ -215,9 +215,9 @@ function useNgData(): FestipodDataContextValue {
// (`createEntityDoc(scope)`, the SDK create). It READS by NEED: it asks the SDK
// for the document NURIs it may read (its own scope docs via `listEntityDocs`,
// the discovery index via `readDiscoveredEvents`) and hands them to the SDK's
// UNION READ (`readEntities` → `readModel.readUnion`) — the SDK opens/syncs the
// docs and runs ONE anchorless union `sparql_query`. There is NO reactive union
// query, so reactivity = RE-QUERY on a change signal (see `bumpRead`). This
// BY-NEED READ (`readEntities` → `readModel.readUnion`) — the SDK reads each of
// those docs by need (fast, per-document, independent of wallet size). There is NO
// reactive read, so reactivity = RE-QUERY on a change signal (see `bumpRead`). This
// replaces the OLD reactive-ORM fan-out (`useShape({ graphs })`), which HUNG
// ~75s on a per-entity fan-out (see readEntities.ts, SDK docs/read-model.md).
// `ready` gates the effects on the session.
@@ -285,9 +285,9 @@ function useNgData(): FestipodDataContextValue {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ready, username]);
// --- The UNION READ (replaces the reactive ORM fan-out) -------------------
// Open/sync the by-need docs and run ONE anchorless union query via the SDK,
// mapped to app types. Re-runs whenever the doc set or the re-query tick
// --- The BY-NEED READ (replaces the reactive ORM fan-out) -----------------
// Read the bounded by-need docs via the SDK (per-document, independent of wallet
// size), mapped to app types. Re-runs whenever the doc set or the re-query tick
// changes. `readReady` flips true after the first read so the empty state
// isn't mistaken for "wallet empty" by the auto-seed.
const [events, setEvents] = useState<FpEventData[]>([]);
+11 -11
View File
@@ -1,22 +1,22 @@
/**
* readEntities — the READ side of the one-document-per-entity model, mapping the
* SDK's union read (`readModel.readUnion`) to app types. This is the LISTING
* path: it asks the SDK to open/sync a set of documents and run ONE anchorless
* union `sparql_query`, then maps each returned subject's property bag to the
* corresponding Fp* type.
* SDK's read (`readModel.readUnion`) to app types. This is the LISTING path: it
* asks the SDK to read a BOUNDED, by-need set of documents, then maps each
* returned subject's property bag to the corresponding Fp* type.
*
* WHY this replaces the ORM `useShape({ graphs })` fan-out: subscribing a fan-out
* of per-entity documents through the reactive ORM HANGS (~75s) — a freshly
* created / not-yet-synced doc makes `RepoNotFound` abort the whole subscription
* (see the SDK's docs/read-model.md, verified on the real broker in T03.k). The
* union query is one-shot, so there is no reactive union: reactivity = RE-QUERY on
* a change signal (a doc was created / registered).
* (see the SDK's docs/read-model.md). The SDK read is one-shot, so there is no
* reactive read: reactivity = RE-QUERY on a change signal (a doc was created /
* registered).
*
* The app asks the SDK by NEED — it passes the document NURIs to read (from the
* discovery index for public events, or its own scope docs for my-entities) and
* never builds a store id or picks the union-vs-anchor mode. Placement + the
* union mechanism live in the SDK (read-model.ts); this file is only the Festipod
* domain mapping (fp: predicates → Fp* fields).
* trusts the returned set. HOW the SDK reads those docs (fast, per-document,
* independent of how much the wallet holds) is entirely internal to the SDK
* (read-model.ts); this file is only the Festipod domain mapping (fp: predicates
* → Fp* fields).
*/
import { readModel } from '@ng-eventually/client';
@@ -91,7 +91,7 @@ export interface ReadEntities {
}
/**
* Open/sync `docs` and run ONE union query (SDK `readModel.readUnion`), then map
* Read the by-need `docs` via the SDK (`readModel.readUnion`), then map
* each subject to its Fp* type by RDF `@type`. `docs` is the by-need set of
* document NURIs to read (the app resolves it: index-discovered event docs +
* my own scope docs). A subject whose participation carries no `fp:user` is
+6 -46
View File
@@ -128,12 +128,13 @@ function ConnectedHarness() {
// The app writes ONE DOCUMENT PER ENTITY (events → public per-entity docs,
// participations/users → protected per-entity docs) via `createEntityDoc`,
// and READS by the union model (T03.k): resolve the by-need doc NURIs (my own
// scope docs + the discovery index) then run ONE anchorless union
// `sparql_query` (`readEntities` → `readModel.readUnion`), re-querying on a
// change signal — never the reactive per-entity ORM fan-out (that HANGS). The
// and READS by need: resolve the bounded by-need doc NURIs (my own scope docs
// + the discovery index) then read EACH doc with its OWN anchored `sparql_query`
// (`readEntities` → `readModel.readUnion`), re-querying on a change signal —
// never the reactive per-entity ORM fan-out (that HANGS), and never an
// anchorless scan of all graphs (O(wallet), times out on a bloated wallet). The
// step-facing `events/users/participations` + mutations/queries delegate to the
// APP data context (`appData`), i.e. the exact union-read path the screens use.
// APP data context (`appData`), i.e. the exact read path the screens use.
// The step contract (`[...td.events]` with `@id`/`title`/`participantCount`,
// `.size`, `p.user`/`p.event`) is preserved by mapping the app types to that
// shape in a Set-like adapter.
@@ -510,47 +511,6 @@ function ConnectedHarness() {
return nuri;
},
/**
* T03.k PROBE — pins down the read-model union premise against the REAL
* broker (docs/read-model.md § Minimal broker probe). Creates two graph
* docs A and B, writes a DISTINCT triple into each (anchored per-doc),
* then queries GRAPH ?g { ?s ?p ?o } twice: once with NO anchor (expect
* BOTH A and B — the LOCAL UNION) and once anchored to A (expect ONLY A).
* Returns the graphs seen in each mode so the step can assert the model.
*/
async runUnionProbe() {
const sid = session.session_id;
const docA = await docs.docCreate(sid, 'Graph', 'data:graph', 'store', undefined);
const docB = await docs.docCreate(sid, 'Graph', 'data:graph', 'store', undefined);
const sA = `urn:probe:s:${Date.now().toString(36)}:a`;
const sB = `urn:probe:s:${Date.now().toString(36)}:b`;
await docs.sparqlUpdate(sid, `INSERT DATA { GRAPH <${docA}> { <${sA}> <urn:probe:p> "A" } }`, docA);
await docs.sparqlUpdate(sid, `INSERT DATA { GRAPH <${docB}> { <${sB}> <urn:probe:p> "B" } }`, docB);
// Query our OWN probe subjects (sA/sB) so the assertion is by triple,
// not by the repo_graph_name (which carries an overlay suffix and won't
// string-equal the doc NURI). ?g is still selected for observability.
const q = `SELECT ?g ?s ?o WHERE { GRAPH ?g { ?s <urn:probe:p> ?o . FILTER(?s IN (<${sA}>, <${sB}>)) } }`;
const readObjs = (res: any): string[] => {
const rows = Array.isArray(res) ? res : res?.results?.bindings ?? [];
return rows.map((r: any) => r?.o?.value).filter(Boolean);
};
// NO anchor → local union across all opened graphs.
const unionRes = await docs.sparqlQuery(sid, q, undefined, undefined);
const unionObjs = readObjs(unionRes);
// Anchor = A → one repo only.
const anchorRes = await docs.sparqlQuery(sid, q, undefined, docA);
const anchorObjs = readObjs(anchorRes);
return {
docA, docB,
unionObjs,
anchorObjs,
unionHasA: unionObjs.includes('A'),
unionHasB: unionObjs.includes('B'),
anchorHasA: anchorObjs.includes('A'),
anchorHasB: anchorObjs.includes('B'),
};
},
/**
* Round-trip the sharedWalletShim through the wallet: create an account
* (3 docs + SPARQL INSERT), drop the cache, reload from the wallet via