feat(data): union read model — list via anchorless sparql_query, hang eliminated
Replace the reactive-ORM per-entity fan-out read (which HUNG 75s: orm_start_graph opened every scope graph and RepoNotFound on any fresh/unsynced doc aborted the subscription) with the read model: - readEntities.ts → lib readUnion: resolve the by-need doc set (my own scope docs via listMyEntityDocs + public events via the discovery index — NOT all-accounts fan-out), then ONE anchorless union sparql_query (GRAPH ?g, VALUES-pinned). Map to app types. Re-query on a change signal (no reactive union query). - countUserParticipations no longer fans out over all accounts (own docs only). - await loadTestData in the seed step; deleted orphaned useShapeWithDefaults; removed the old multistore-stopgap fan-out scenarios; added the read-model-probe. - Doctrine: rule_document-per-entity read half + _overview rewritten to the union model (write half unchanged). Result: the 75s ORM hang is ELIMINATED (0 hangs; build/tsc/lib-93-tests green; boundary clean). @data is NOT yet fully green: remaining failures are 90s step timeouts in the test-harness broker data ops (clearWallet / runUnionProbe / seed) this run — a harness/broker-op issue, not the read path. To finish separately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
---
|
||||
type: _overview
|
||||
summary: Comment Festipod persiste ses données via le SDK @ng-eventually/client — entités stockées comme documents par scope, stack ORM/SHEX, modes connected/demo, seed
|
||||
summary: Comment Festipod persiste ses données via le SDK @ng-eventually/client — entités stockées comme documents par scope, écriture SPARQL directe + lecture par modèle union, stack SHEX, modes connected/demo, seed
|
||||
triggers:
|
||||
keywords: [nextgraph, "@ng-eventually", useShape, ORM, SHEX, shape, scope, "@graph", NURI, sparql, seed, wallet, FestipodData, ngSession, ngGraph, bootstrap, document, entité]
|
||||
paths: ["src/shared/shapes/**", "src/shared/hooks/useShape*", "src/shared/context/NextGraphContext.tsx", "src/shared/context/FestipodDataContext.tsx", "src/shared/utils/ng*", "src/shared/data/seedData.ts"]
|
||||
keywords: [nextgraph, "@ng-eventually", union, readUnion, readEntities, SHEX, shape, scope, "@graph", NURI, sparql, seed, wallet, FestipodData, ngSession, ngGraph, bootstrap, document, entité]
|
||||
paths: ["src/shared/shapes/**", "src/shared/data/readEntities.ts", "src/shared/data/entityWrites.ts", "src/shared/context/NextGraphContext.tsx", "src/shared/context/FestipodDataContext.tsx", "src/shared/utils/ng*", "src/shared/data/seedData.ts"]
|
||||
---
|
||||
|
||||
# Data layer
|
||||
|
||||
Comment Festipod **persiste ses données** via NextGraph (P2P, local-first, chiffré de bout en bout). Le SDK de données est **`@ng-eventually/client`** : on le traite comme un SDK NextGraph fini — chaque entité est un **document** placé dans le store de son **scope** (public / protected / private), lu et écrit via l'ORM réactif. Le mapping *quelle entité → quel scope* est un fait **produit** (concept `functional-domain`, [[knowledge_data-scopes-and-discovery]]) ; ce concept décrit la **mécanique de persistance**.
|
||||
Comment Festipod **persiste ses données** via NextGraph (P2P, local-first, chiffré de bout en bout). Le SDK de données est **`@ng-eventually/client`** : on le traite comme un SDK NextGraph fini — chaque entité est un **document** placé dans le store de son **scope** (public / protected / private). L'**écriture** est un SPARQL direct dans le document de l'entité ; la **lecture** est le **modèle union** (résoudre les documents par besoin → ouvrir/sync → **une** requête `sparql_query` sans ancre sur l'union → re-query sur signal), et non un abonnement ORM réactif en fan-out (qui *hang*). Voir [[rule_document-per-entity]]. Le mapping *quelle entité → quel scope* est un fait **produit** (concept `functional-domain`, [[knowledge_data-scopes-and-discovery]]) ; ce concept décrit la **mécanique de persistance**.
|
||||
|
||||
> **Frontière SDK.** Le SDK de données de Festipod est `@ng-eventually/client` — initialisé/injecté **une seule fois** via `ngSession.configure(...)`. On l'écrit comme un SDK NextGraph **fini** : ne jamais documenter ici l'état courant de NextGraph (contraintes, contournements, internes broker) — cela vit dans le repo `@ng-eventually/client`. Voir [[knowledge_nextgraph-stack]].
|
||||
|
||||
|
||||
@@ -32,30 +32,53 @@ confiance.
|
||||
- À la création : demander au SDK **un document pour l'entité, dans son scope**
|
||||
(`createEntityDoc(scope)`) ; y écrire l'entité. Ne pas réutiliser un document d'un autre
|
||||
périmètre ni un document de niveau store.
|
||||
- En lecture : passer par le SDK, **par scope** — pas de résolution de document/NURI côté app.
|
||||
- En lecture : passer par le SDK via le **modèle de lecture union** (voir plus bas) — l'app
|
||||
résout un jeu de documents *par besoin* (index de découverte pour les événements publics ;
|
||||
ses propres documents de scope pour ses entités) et le SDK ouvre/synchronise puis lit
|
||||
l'union en **une seule** requête ; pas de résolution de NURI ni de choix union/ancré côté app.
|
||||
- Le mapping *entité → scope* (événement/PdR → public, profil réseau/participation → protected,
|
||||
settings → private) est un fait produit (concept `functional-domain`,
|
||||
[[knowledge_data-scopes-and-discovery]]).
|
||||
|
||||
## Écriture directe vs. set réactif (piège d'aller-retour)
|
||||
## Lecture : modèle union (open/sync + une requête ancrée-libre + re-query)
|
||||
|
||||
La **lecture** ne passe **PAS** par un abonnement ORM réactif en fan-out sur un jeu de documents
|
||||
par-entité (`useShape({ graphs: […] })`) : contre le vrai broker un document fraîchement créé /
|
||||
non-synchronisé dans ce fan-out fait avorter tout l'abonnement (`RepoNotFound`) → l'abonnement
|
||||
n'émet jamais son initial → **hang ~75 s**. À la place, la lecture est le **modèle union** du SDK
|
||||
([[knowledge_nextgraph-stack]], SDK `docs/read-model.md`) :
|
||||
|
||||
1. **résoudre par besoin** le jeu de NURIs à lire — événements publics via l'**index de découverte**
|
||||
(la seule énumération cross-comptes sanctionnée) ; « mes entités » (profil, participations) via
|
||||
**mes propres** documents de scope (`listMyEntityDocs(username, scope)`, borné à mon compte —
|
||||
jamais de fan-out sur tous les comptes) ;
|
||||
2. le SDK **ouvre/synchronise** ces documents puis exécute **UNE** requête `sparql_query`
|
||||
**sans ancre** sur l'union locale (`GRAPH ?g { … }`) et rend les triplets groupés par sujet
|
||||
(`src/shared/data/readEntities.ts` → `readModel.readUnion`) ;
|
||||
3. il n'y a **pas** de requête union réactive → la **réactivité = re-query** sur un signal de
|
||||
changement (un document créé/enregistré déclenche `bumpRead`).
|
||||
|
||||
Côté app, `FestipodDataContext` collecte les NURIs par besoin puis appelle `readEntities` ;
|
||||
un document fraîchement créé est aussi enregistré localement (`registerDoc`) pour apparaître
|
||||
immédiatement, avant que la re-liste ne le rattrape.
|
||||
|
||||
## Écriture directe (piège d'aller-retour)
|
||||
|
||||
L'**écriture** d'une entité se fait **directement dans son propre document** (via l'appel
|
||||
SPARQL du SDK — `src/shared/data/entityWrites.ts`, `writeEntity`), **pas** via l'ajout à
|
||||
l'ensemble réactif `ngSet.add`. Raison : l'ensemble réactif (`useShape(shape, { graphs })`)
|
||||
n'est *inscriptible* que si le document cible est **déjà** dans son scope d'abonnement ; or
|
||||
enregistrer le document fraîchement créé dans ce scope est un état React qui ne prend effet
|
||||
qu'au rendu **suivant** → on ne peut pas créer-puis-ajouter en une passe synchrone (boucle de
|
||||
seed, première création). Contre le vrai broker, `ngSet.add` sur un scope vide lève « Set is
|
||||
readonly because scope is empty » (les tests unitaires fake-ng ne l'attrapent pas).
|
||||
SPARQL du SDK — `src/shared/data/entityWrites.ts`, `writeEntity`), **pas** via l'ajout à un
|
||||
ensemble réactif. Raison : un ensemble réactif n'est *inscriptible* que si le document cible est
|
||||
**déjà** dans son scope d'abonnement ; or enregistrer le document fraîchement créé est un état
|
||||
React qui ne prend effet qu'au rendu **suivant** → on ne peut pas créer-puis-ajouter en une passe
|
||||
synchrone (boucle de seed, première création). Contre le vrai broker, un `add` sur un scope vide
|
||||
lève « Set is readonly because scope is empty » (les tests unitaires fake-ng ne l'attrapent pas).
|
||||
|
||||
Donc : **écriture = SPARQL direct dans le doc de l'entité** (immédiat, par-document) ;
|
||||
**lecture = réactive** (le NURI du doc est enregistré dans le `useShape({ graphs })`, l'ORM le
|
||||
relit). Idem pour la **mutation d'un champ** existant (p. ex. `participantCount`) : une mutation
|
||||
ORM en place est **locale** et se fait **écraser** par la re-synchro réactive du doc depuis le
|
||||
broker (retour à la valeur persistée) → persister via SPARQL (`updateEntityField` : DELETE puis
|
||||
**lecture = union + re-query** (ci-dessus). Idem pour la **mutation d'un champ** existant (p. ex. `participantCount`) : muter une valeur
|
||||
en mémoire ne tient pas — la re-query union relit la valeur **persistée** depuis le broker
|
||||
(retour à l'ancienne valeur) → persister via SPARQL (`updateEntityField` : DELETE puis
|
||||
INSERT du triplet) pour que le changement tienne et que la relecture concorde. Chaque champ est écrit avec le **bon terme RDF** selon la shape SHEX (xsd:integer /
|
||||
float / boolean, ou IRI pour les références `Participation.event`/`.user`) — un champ obligatoire
|
||||
manquant ou mal typé fait que l'ORM **jette l'entité** à la relecture (elle ne fait jamais
|
||||
manquant ou mal typé fait que la lecture **jette l'entité** (elle ne fait jamais
|
||||
l'aller-retour). Le **sujet** de l'entité = le **NURI de son document** (une entité = un document),
|
||||
ce qui donne un `@id` en `did:ng:…`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user