fix(data): restore per-entity write round-trip against the real broker
The per-document isolation refactor (one doc per entity) broke every @data
round-trip against the real broker (0 events readable) — fake-ng unit tests
missed it. Root causes + fixes:
- ngSet.add cannot write to an empty subscription scope ("Set is readonly
because scope is empty") → write each entity DIRECTLY into its own document via
SPARQL (new data/entityWrites.ts: writeEntity/updateEntityField), typing each
field with the correct RDF term per the SHEX shape (else the ORM drops the
entity on read). Reactive set stays read-only; the doc NURI is registered into
useShape({graphs}) for reactive reads.
- Current principal made STABLE and username-derived (urn:festipod:user:<name>),
available immediately at login and invariant — so a Participation's mandatory
fp:user is never empty and identity/cap-owner/connections all key on the same
value.
- Discovery deposits AS the current identity (harness sets current user first).
- Idempotence/deregistration checks made authoritative against the broker;
participantCount persisted via SPARQL. rule_document-per-entity enriched with
these write/read + stable-principal lessons.
Round-trip restored (seed readable, inscription+notif, persistent deregistration,
public discovery all pass in isolation). NOT yet stably green as a full suite:
@data oscillates 15–20/21 — residual failures are environmental (participation-
read fan-out lag on an accumulating persistent test wallet), same class as the
Chromium saturation; not a logic bug. Durable fix (follow-up): non-fan-out
materialized read + per-scenario test-wallet isolation. app build+tsc + lib 89
tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,9 +29,42 @@ confiance.
|
||||
|
||||
## Comment l'appliquer
|
||||
|
||||
- À la création : demander au SDK **un document pour l'entité, dans son scope** ; y écrire
|
||||
l'entité. Ne pas réutiliser un document d'un autre périmètre ni un document de niveau store.
|
||||
- À 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.
|
||||
- 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)
|
||||
|
||||
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).
|
||||
|
||||
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
|
||||
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
|
||||
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:…`.
|
||||
|
||||
Corollaire d'identité : une `Participation` porte un `fp:user` **obligatoire** — ne jamais
|
||||
l'écrire avec un principal vide (l'entité serait jetée en lecture). Le principal du user courant
|
||||
est **stable et dérivé du username** (`urn:festipod:user:<username-normalisé>`), disponible
|
||||
**immédiatement** après login (pas de dépendance à la lecture du profil protégé, qui peut
|
||||
retarder) et **invariant** (il ne bascule pas d'un fallback vers l'IRI de profil en cours de
|
||||
session, ce qui désynchroniserait une participation écrite sous une valeur d'une vérification
|
||||
sous l'autre). C'est le même principal que l'identité SDK (`setCurrentUser`) et le cap owner
|
||||
dérivent du username ; les connexions bilatérales (`declareConnections`) se déclarent avec ces
|
||||
mêmes clés username (pas des IRIs de profil) pour que « protégé = mes connexions » discrimine.
|
||||
|
||||
Reference in New Issue
Block a user