Post-mortem 2026-07-14 (ouvert) : symptôme + preuves Firefox verbatim, chaîne causale tracée (socket→Disconnected→reconnexion en TODO), réserve (i) perte-écriture vs (ii) réhydratation à froid, repro @data décisive (test de reconnexion existant faux-vert = lecture IndexedDB locale). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
5.7 KiB
Perte d'écriture lors d'une mort de socket (SerializationError)
Post-mortem — 2026-07-14 · Statut : OUVERT (non traité).
Une entité écrite juste avant une période d'inactivité peut être perdue silencieusement : l'écriture n'atteint jamais durablement le broker, et l'entité est absente à la reconnexion. Le compte / l'identité survit (pas de fork). Observé en conditions réelles (Festipod, Firefox) lors d'une pause après login/création.
Symptôme
- L'utilisateur se connecte, l'app crée une entité (un événement Festipod).
- Une période d'inactivité suit (idle, onglet en arrière-plan…).
- Le socket broker meurt spontanément avec
SOCKET IS CLOSED Some(Left(SerializationError)). - À la reconnexion, l'entité créée a disparu ; l'app relit son propre scope vide.
Preuves (VÉRIFIÉ — logs Firefox en direct, verbatim)
… REPLAY TOPIC NOT FOUND <topic> IN OVERLAY <overlay>
… NEED REPLAY true
… SENDING EVENTS FROM OUTBOX RETURNED: Err(TopicNotFound)
[user1][polyfill] resolveAccount(user1) → 1 record ← le compte SURVIT (pas de fork)
[user1][polyfill] readScopeIndex(…) → 0 entities ← mais le scope est VIDE
… set reçu: 0 objets Event (public)
… SOCKET IS CLOSED Some(Left(SerializationError)) [51, 3, 223, …]
Lecture (mécanisme plausible, non tranché) : l'écriture a été poussée dans l'outbox local, mais le socket est mort avant qu'elle ne soit flushée durablement dans le topic broker ; à la reconnexion, le replay de l'outbox échoue (Err(TopicNotFound)) parce que le topic n'a jamais été créé côté broker → l'événement est abandonné. Le compte, lui, avait déjà été résolu durablement (resolveAccount → 1 record) : il n'est ni perdu ni forké.
Réserve épistémique. Les preuves établissent le symptôme (perte +
Err(TopicNotFound)+readScopeIndex → 0). Le mécanisme exact n'est pas tranché entre (i) perte à l'écriture (l'écriture n'atteint jamais durablement le broker) et (ii) échec de réhydratation à froid (l'écriture est sur le broker mais une session fraîche ne rouvre pas le scope propre). LeErr(TopicNotFound)sur le replay outbox penche pour (i) dans ce cas Firefox. Voir la repro @data ci-dessous, qui expose un symptôme voisin mais ne tranche pas (i) vs (ii).
Chaîne causale (TRACÉ — lecture du core NextGraph, à re-vérifier)
- La
SerializationErrorferme le socket. Le core émet la déconnexion :broker.rs→LocalBrokerMessage::Disconnected→disconnections_sender.send(...)(≈broker.rs:1051, à re-vérifier — numéro volatil, se repérer par le symbole). - Cette déconnexion est poussée aux abonnés via
disconnections_subscribe(cb)(flux PUSH). - La reconnexion NextGraph est un
// TODOnon implémenté (≈broker.rs:1051-1076) : rien ne rétablit le socket ni ne re-flushe l'outbox. user_connectrenvoie un instantané{ server_id, server_ip, error, since }au moment de l'appel — pas un flux, inutilisable pour détecter une chute ultérieure.- Aucune API de confirmation de durabilité d'écriture : un appelant ne peut pas
awaitla garantie qu'une écriture a atteint le broker.
Ce que le SDK expose mais ne consomme pas
disconnections_subscribe se déclenche sur cette panne — mais ni le polyfill (@ng-eventually/client) ni l'app consommateur ne s'y abonnent. Le signal existe, personne ne l'écoute ; côté app, aucun mécanisme ne re-tente ni n'avertit l'utilisateur.
Portée & non-reproduit
- Observé Firefox uniquement à ce jour. Un test manuel sur un autre navigateur n'a pas déclenché la
SerializationErrorni ses conséquences. - Reproduction @data (Chromium, broker réel) — 2026-07-14, décisive. Le test de reconnexion @data existant (
reconnexion-meme-identite) était faux-vert : il relisait les repos de A depuis l'IndexedDB local du profil persistant, jamais depuis le broker. Un lecteur réellement à froid (contexte non-persistantfreshBrowser, même wallet/compte A, aucun état local — seedé du wallet capturé avant l'événement) lit 0 événement de A (BARRIER timed-out (8000ms),CONNECTION ESTABLISHED). Signature différente du cas Firefox (pas de mort de socket ; l'OUTBOX emptyest celui du lecteur, trivialement vide) et ne tranche pas (i) vs (ii) — un barrier vide est compatible avec les deux. Établi en revanche : @data n'a jamais vérifié la durabilité broker des lectures propres de A, et la réhydratation à froid depuis le broker échoue. Repro :src/modules/event/features/reconnexion-froide-sans-local.feature(Festipod). - Pour trancher (i) vs (ii) : vérifier indépendamment que l'écriture de A atteint le broker — p.ex. un lecteur chaud / une seconde identité lit le doc public de l'événement (le scénario d'isolation deux-identités). S'il le voit → l'écriture est durable → le 0 du lecteur à froid est un (ii) (réhydratation). Sinon → (i).
Pistes de correction (non arbitré)
- Core — corriger la
SerializationErroret implémenter le TODO de reconnexion (rétablir le socket + re-flusher l'outbox). - SDK / polyfill — consommer
disconnections_subscribe→ reconnexion + re-flush outbox comme mitigation, indépendamment du core. - API de durabilité — exposer une confirmation qu'une écriture a atteint le broker, pour que l'appelant puisse l'
await.
Liens
docs/nextgraph-current-state.md— état courant du core (déconnexion / reconnexion à cross-référencer ici).- Impact produit + caveat côté consommateur : concept Festipod
data-layer→caveat_write-durability-across-disconnect.