feat(data): participantCount via Option B (deposit + owner materialization)
Remove the write-isolation violation: joinEvent/leaveEvent no longer write participantCount on the event doc (a non-owner writing the owner's public doc — illegitimate in NextGraph). The joiner/leaver only write their own protected participation doc and DEPOSIT a marker into the event inbox (depositRegistration / depositLeave). The event OWNER's session materializes: it subscribes (inbox.watch, doc_subscribe — no polling) to the inboxes of its OWNED events (ownedEventIds), and on each deposit recomputes participantCount on its OWN event doc. The count is DERIVED, not incremented: materializeAttendance derives the SET of distinct active registrations (new-participant deduped by uid, MINUS leave-participant by regUid/fallback eventId+userId), count = 1 (host self) + |active set|. A pure function of the inbox → broker re-syncs converge, never double-count nor resurrect (idempotent); the write is guarded (only on change → no loop). Authoritative deleteParticipation preserved (caveat_participation-deletion). Because the owner writes its own PUBLIC event doc and every session subscribes to it (P3), the count round-trips reactively to all — no reload. Owner-offline = eventual (V1; a future @ng-eventually/service materializes on the owner's behalf). Real 2-browser e2e (e2e-multibrowser.feature): B registers → A materializes → count 1→2 reactively (no reload) + unknown participant; B leaves → count →1. 14/14 green. Gates: @data auth 4/4, @data isolation 4/4, build + tsc clean. Doctrine: knowledge_context-internals (Option B section). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Pièges internes de FestipodDataContext — currentUserId = principal stable dérivé de l'identifiant, auto-seed dev-only supprimé par loadTestData (seed possédé par l'identité courante), participantCount muté en place (cache), mutations no-op en mode local malgré le toast
|
||||
last_checked: 2026-07-06
|
||||
last_checked: 2026-07-07
|
||||
---
|
||||
|
||||
# Internals & pièges de `FestipodDataContext`
|
||||
@@ -21,9 +21,14 @@ Un auto-seed se déclenche **uniquement hors production** (`process.env.NODE_ENV
|
||||
- Le seed est **possédé par l'identité courante** (`bootstrapWallet(…, owner)`), pas par un propriétaire fixe : les entités protégées seedées (profils) passent ainsi le cap de lecture par-document du propriétaire (sinon elles seraient masquées et jamais relues).
|
||||
- **Pas de retry** au-delà : si le seed échoue, écran vide + `console.error`. Le délai de 3s reste heuristique.
|
||||
|
||||
## `participantCount` muté en place
|
||||
## `participantCount` — dérivé et possédé par le propriétaire (Option B)
|
||||
|
||||
`joinEvent`/`leaveEvent`/`updateEvent` **mutent directement** `ngEvent.participantCount` (`+1`/`-1`) — c'est un **cache** du nombre de `Participation`, pas une valeur recalculée. Il peut **désynchroniser** des objets `Participation` réels (ex. après un crash, un rejeu, ou la suppression partielle décrite dans [[caveat_participation-deletion]]). Ne pas s'y fier comme source de vérité du nombre de participants.
|
||||
**Depuis Option B (2026-07-07)** : `participantCount` n'est plus muté en place par l'inscrit. Le flux est dépôt-inbox → matérialisation-propriétaire :
|
||||
- `joinEvent`/`leaveEvent` n'écrivent **plus** `participantCount` sur le doc de l'événement (ce serait une violation d'isolation — l'inscrit écrirait le doc d'un autre ; le write NextGraph est membership-bound, pas d'append). L'inscrit écrit seulement son **propre** doc de participation (protected) puis **dépose** un marqueur dans l'inbox de l'événement (`depositRegistration` sur join, `depositLeave` sur leave, `src/shared/data/registration.ts`).
|
||||
- La session du **propriétaire** de l'événement matérialise : elle est abonnée (`inbox.watch`, `doc_subscribe`, sans polling) à l'inbox de ses events possédés (`ownedEventIds` = `listMyEntityDocs(owner,'public')` + les events fraîchement créés), et sur chaque dépôt **recalcule** `participantCount` sur **son propre** doc d'événement (`updateEntityField` sur son doc). C'est le seul écrivain du compteur.
|
||||
- **Le compteur est DÉRIVÉ, pas incrémenté** : `materializeAttendance` (registration.ts) lit l'inbox et calcule l'**ensemble** des inscriptions actives distinctes (dépôts `new-participant` dédupés par `uid`, MOINS ceux annulés par un `leave-participant` — par `regUid` exact ou fallback `(eventId, userId)`). `participantCount = 1 (hôte lui-même, base de création) + |ensemble actif|`. Comme c'est une **fonction pure de l'inbox**, un rejeu de sync broker converge — jamais de double-comptage ni de décrément fantôme (idempotence). L'écriture est gardée (n'écrit que si la valeur change), anti-boucle.
|
||||
- **Propriétaire hors-ligne = éventuel** : seule la session du propriétaire matérialise ; déconnecté, le compteur n'avance pas pour les autres (les participations/dépôts restent persistés — rien n'est perdu ; un futur service matérialisera à sa place).
|
||||
- Le compteur reste néanmoins un **agrégat**, pas la liste des participants nommés : `getEventParticipants` (identité nommée) reste gouverné par le cap de lecture protected ([[caveat_participation-deletion]] pour la suppression autoritative, inchangée). Cf. le brief `brief_2026-07-06_reactive-reads-and-attendance` §B.
|
||||
|
||||
## Changement d'identité = session fraîche (isolation)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user