diff --git a/.project/concepts/data-layer/knowledge_context-internals.md b/.project/concepts/data-layer/knowledge_context-internals.md index f8a49da..a7c01ed 100644 --- a/.project/concepts/data-layer/knowledge_context-internals.md +++ b/.project/concepts/data-layer/knowledge_context-internals.md @@ -25,6 +25,12 @@ Un auto-seed se déclenche **uniquement hors production** (`process.env.NODE_ENV `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. +## Changement d'identité = session fraîche (isolation) + +Le jeu de lecture par besoin (`publicDocs`/`protectedDocs`) **accumule** les docs de scope de l'identité courante (pour ne pas perdre un doc juste créé avant la re-liste). Or le stopgap wallet-partagé garde **un seul arbre React** au travers d'un faux-logout + re-login sous un **autre identifiant** (pas de rechargement — `AccountContext.login` ne fait que réécrire l'identifiant en localStorage, `AuthGate` ne remonte rien). Sans réinitialisation, **les docs PROTECTED de l'identité précédente (ses participations) survivent dans le jeu de lecture de la nouvelle identité et fuient** via la lecture union : le cap gate ne peut pas les filtrer quand le registre de caps (en mémoire) ne gouverne pas ce doc *cette* session (doc persisté d'un run antérieur, ou chargement frais où les caps sont vides). Symptôme observé : un utilisateur B voyait la participation de A (et l'événement de A apparaissait sur l'**accueil** de B, car l'accueil = `getUserEvents(currentUserId)`, cf. concept `app-architecture`). + +**Règle** : traiter **tout changement d'identifiant** comme une session fraîche — un `useEffect([username])` (ref-gardé pour ne pas tirer au premier mount) vide `publicDocs`/`protectedDocs`, appelle `resetCaps()` + `resetRegistryCache()`, puis bump le read tick ; l'effet de listing reconstruit le jeu **borné à la nouvelle identité**. L'isolation reste par-document/émulée (concept `app-security`, [[knowledge_trust-model]]) ; ce reset ne fait que supprimer le report d'état inter-identités. + ## Mutations no-op en mode local En mode local/demo (`useLocalData`), `createEvent`/`joinEvent`/`leaveEvent`/`updateEvent` sont des **no-ops** (`console.log`, l'état ne change pas) — mais les écrans affichent quand même un **toast de succès** (« Tu participes »). UX potentiellement trompeuse : l'utilisateur croit s'être inscrit alors que rien n'a changé. Voir [[knowledge_data-modes]] pour le choix du provider selon le statut. diff --git a/src/shared/context/FestipodDataContext.tsx b/src/shared/context/FestipodDataContext.tsx index 76e09f2..c25ad10 100644 --- a/src/shared/context/FestipodDataContext.tsx +++ b/src/shared/context/FestipodDataContext.tsx @@ -29,7 +29,8 @@ import { useAccount, normalizeUsername } from './AccountContext'; // Relationship is a Festipod concept: the app keeps its own bilateral registry // and hands the SDK only directed read grants (see shared/utils/connections). import { declareConnections } from '../utils/connections'; -import { listMyEntityDocs, createEntityDoc } from '../utils/storeRegistry'; +import { listMyEntityDocs, createEntityDoc, resetRegistryCache } from '../utils/storeRegistry'; +import { resetCaps } from '@ng-eventually/client/polyfill'; import { submitEventToIndex, readDiscoveredEvents } from '../data/discovery'; import { readEntities } from '../data/readEntities'; import { writeEntity, updateEntityField, ENTITY_TYPE, str, int, flt, bool, iri } from '../data/entityWrites'; @@ -253,6 +254,37 @@ function useNgData(): FestipodDataContextValue { setReadTick(t => t + 1); }, []); + // IDENTITY SWITCH = FRESH SESSION (isolation). The by-need read set + // (publicDocs/protectedDocs) ACCUMULATES the current identity's own scope docs + // (`listMyEntityDocs(username, …)`) so a just-created doc isn't dropped before + // the re-list. But the shared-wallet stopgap keeps ONE React tree across a faux + // logout + re-login under a DIFFERENT identifier (no page reload — see + // AuthGate/AccountContext), so without a reset the PREVIOUS identity's PROTECTED + // docs (its participations) survive in the new identity's read set and leak + // through the union read: the cap gate cannot filter them when the cap registry + // does not govern that doc THIS session (a doc persisted in a prior run, or a + // fresh load where caps are empty). Treat every identity change as a fresh + // session: drop the accumulated read set (the listing effect rebuilds it bounded + // to the NEW identity), and reset the emulated caps + registry cache so nothing + // from the old identity lingers. Ref-guarded so it fires only on a real change, + // not on the first mount (empty sets already). + const prevOwnerRef = useRef(undefined); + useEffect(() => { + if (prevOwnerRef.current === undefined) { + prevOwnerRef.current = username; + return; + } + if (prevOwnerRef.current === username) return; + prevOwnerRef.current = username; + // Fresh session for the new identity: clear the previous identity's read set + // and the emulated isolation state, then let the listing effect rebuild. + setPublicDocs([]); + setProtectedDocs([]); + resetCaps(); + resetRegistryCache(); + setReadTick(t => t + 1); + }, [username]); + // Resolve the by-need doc NURIs — READ BY NEED, never an all-accounts fan-out // (the OLD `listEntityDocs('public'|'protected')` enumerated EVERY account and // tried to open/sync other accounts' unsynced docs → HANG ~75s; see