Le nom @ng-eventually/sdk entrait en collision avec le SDK de NextGraph, dont ce paquet est justement un polyfill. Impossible d'écrire « le SDK » sans lever l'ambiguïté à chaque phrase — et le contrat publié, lu par une application, était le pire endroit pour laisser traîner ça. packages/sdk → packages/polyfill, @ng-eventually/sdk → @ng-eventually/polyfill, contract_sdk-surface → contract_polyfill-surface, e2e/sdk-entry.ts → e2e/polyfill-entry.ts, docs/sdk-reference.md → docs/polyfill-reference.md. Les occurrences de « SDK » qui désignent celui de NextGraph restent intactes, y compris les chemins dans nextgraph-rs (sdk/js/orm, sdk/js/web). Le tri s'est fait occurrence par occurrence, pas par substitution. Le contrat énonce désormais son identité en une phrase : « This package is a polyfill of NextGraph's SDK. »
5.8 KiB
Write loss on socket death (SerializationError)
Post-mortem — 2026-07-14 · Status: OPEN (not addressed).
An entity written just before a period of inactivity can be silently lost: it is absent on reconnection. (Whether the write never durably reached the broker, or reached it and is not read back on a cold reconnection, is not settled — see Epistemic caveat below. The wording here deliberately states only the observed symptom.) The account / identity survives (no fork). Observed in real conditions (Festipod, Firefox) during a pause after login/creation.
Symptom
- The user logs in, the app creates an entity (a Festipod event).
- A period of inactivity follows (idle, tab in the background…).
- The broker socket dies spontaneously with
SOCKET IS CLOSED Some(Left(SerializationError)). - On reconnection, the created entity has disappeared; the app reads back its own scope empty.
Evidence (VERIFIED — live Firefox logs, 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 ← the account SURVIVES (no fork)
[user1][polyfill] readScopeIndex(…) → 0 entities ← but the scope is EMPTY
… set reçu: 0 objets Event (public)
… SOCKET IS CLOSED Some(Left(SerializationError)) [51, 3, 223, …]
Interpretation (plausible mechanism, not settled): the write was pushed into the local outbox, but the socket died before it was durably flushed into the broker topic; on reconnection, the outbox replay fails (Err(TopicNotFound)) because the topic was never created on the broker side → the event is abandoned. The account, for its part, had already been durably resolved (resolveAccount → 1 record): it is neither lost nor forked.
Epistemic caveat. The evidence establishes the symptom (loss +
Err(TopicNotFound)+ the scope read returning 0 — logged above asreadScopeIndex, since renamedreadUserStore). The exact mechanism is not settled between (i) loss at write time (the write never durably reaches the broker) and (ii) cold-rehydration failure (the write is on the broker but a fresh session does not reopen its own scope). TheErr(TopicNotFound)on the outbox replay leans toward (i) in this Firefox case. See the @data repro below, which exhibits a neighboring symptom but does not settle (i) vs (ii).
Causal chain (TRACED — reading of the NextGraph core, to be re-verified)
- The
SerializationErrorcloses the socket. The core emits the disconnection in two places, and they are not the same file:engine/net/src/broker.rs:1074sendsLocalBrokerMessage::Disconnected, which the SDK turns intodisconnections_sender.send(...)atsdk/rust/src/local_broker.rs:648. Navigate by symbol — the line numbers are volatile, and the earlier note in this file put thesendinbroker.rs, which it never was. - This disconnection is pushed to subscribers via
disconnections_subscribe(cb)(PUSH stream). - NextGraph reconnection is an unimplemented
// TODO(≈broker.rs:1051-1076): nothing re-establishes the socket nor re-flushes the outbox. user_connectreturns a snapshot{ server_id, server_ip, error, since }at call time — not a stream, unusable for detecting a later drop.- No write-durability confirmation API: a caller cannot
awaitthe guarantee that a write has reached the broker.
What the SDK exposes but does not consume
disconnections_subscribe does fire on this failure — but neither the polyfill (@ng-eventually/polyfill) nor the consumer app subscribes to it. The signal exists, nobody listens to it; on the app side, no mechanism retries or warns the user.
Scope & not reproduced
- Observed on Firefox only to date. A manual test on another browser did not trigger the
SerializationErrornor its consequences. - @data reproduction (Chromium, real broker) — 2026-07-14, decisive. The existing @data reconnection test (
reconnexion-meme-identite) was a false green: it read A's repos back from the persistent profile's local IndexedDB, never from the broker. A genuinely cold reader (non-persistentfreshBrowsercontext, the same wallet/account A, no local state — seeded from the wallet captured before the event) reads 0 events from A (BARRIER timed-out (8000ms),CONNECTION ESTABLISHED). A different signature from the Firefox case (no socket death; theOUTBOX emptyis the reader's, trivially empty) and it does not settle (i) vs (ii) — an empty barrier is compatible with both. Established on the other hand: @data has never verified the broker durability of A's own reads, and cold rehydration from the broker fails. Repro:src/modules/event/features/reconnexion-froide-sans-local.feature(Festipod). - To settle (i) vs (ii): independently verify that A's write reaches the broker — e.g. a warm reader / a second identity reads the event's public doc (the two-identity isolation scenario). If it sees it → the write is durable → the cold reader's 0 is a (ii) (rehydration). Otherwise → (i).
Fix leads (not arbitrated)
- Core — fix the
SerializationErrorand implement the reconnection TODO (re-establish the socket + re-flush the outbox). - SDK / polyfill — consume
disconnections_subscribe→ reconnection + outbox re-flush as a mitigation, independently of the core. - Durability API — expose a confirmation that a write has reached the broker, so that the caller can
awaitit.
Links
docs/nextgraph-current-state.md— current state of the core (disconnection / reconnection to be cross-referenced here).- Product impact + consumer-side caveat: Festipod concept
data-layer→caveat_write-durability-across-disconnect.