Room State Synchronization#
Overview#
Room state synchronization covers how Element clients maintain consistent views of room membership, and how that state is persisted and restored across sessions. Bugs in this area typically manifest as:
- Rooms remaining visible in the room list after a user has left
- Call indicators persisting after a leave event (stale UI)
- Session/crypto data loss due to IndexedDB eviction under storage pressure
- "Missing session data" dialogs at startup
Membership State: Data Flow (matrix-js-sdk)#
Sync responses arrive in processSyncResponse() in src/sync.ts, which routes rooms into one of four paths — join, invite, leave, or knock . Each path calls injectRoomEvents() , which applies state events via liveTimeline.initialiseState() → RoomState.setStateEvents().
setStateEvents() is the central hub for membership changes: it calls getOrCreateMember(), then RoomMember.setMembershipEvent() to update the membership property and emit RoomMemberEvent.Membership when it changes . EventTimeline.addEvent() conditionally applies state events to the room's state object based on the addToState flag .
Key models:
| File | Purpose |
|---|---|
src/sync.ts | Sync loop; routes join/leave/invite events |
src/models/room-state.ts | RoomState.setStateEvents() — processes membership events |
src/models/room-member.ts | RoomMember.setMembershipEvent() — owns membership property |
src/models/room.ts | addLiveEvents() — entry point for live event processing |
src/models/event-timeline.ts | addEvent() — applies state events to RoomState |
Room List Updates on Leave/Rejoin (matrix-react-sdk)#
RoomListStore listens for MatrixActions.Room.myMembership events and calls onDispatchMyMembership() . When membership changes, it triggers a RoomUpdateCause.PossibleTagChange, causing Algorithm.handleRoomUpdate() to recompute the room's effective membership via getEffectiveMembershipTag():
EffectiveMembership.Leave→ room moves toDefaultTagID.Archivedtag (removed from active lists)EffectiveMembership.Invite→ room moves toDefaultTagID.Invite- Join → room uses its normal tag from
getTagsOfJoinedRoom()
Rooms that need tag changes are explicitly removed with RoomUpdateCause.RoomRemoved and re-added with RoomUpdateCause.NewRoom .
Known gap: SlidingRoomListStore has incomplete support for DefaultTagID.Archived, meaning archived room filtering may not work correctly when feature_sliding_sync is enabled .
Stale Room Visibility Bug (issue #33697)#
A confirmed reproducible issue: after leaving a room (especially during an active call), Element Web can continue showing the room and its call indicator in the room list even though the user is no longer a member . Pressing "reconnect" on the call UI always fails because the client is no longer in the room. The issue resolves on client restart and appears tied to the myMembership event not propagating cleanly in all leave-during-call paths .
Storage Persistence and Session Loss#
Room membership and sync state are persisted via IndexedDBStore (sync/session store). Profile data (display names, avatars) survives restarts only through saved presence events, persisted with a 5-minute write delay (WRITE_DELAY_MS) . On failure, the sync store degrades to in-memory operation .
Separate from the sync store, the crypto store (IndexedDBCryptoStore) holds all E2EE session data. Loss of this store means the user must re-verify their identity and loses access to prior encrypted messages .
IndexedDB Eviction Under Storage Pressure#
Browsers can silently evict non-persistent IndexedDB data under disk or memory pressure. Element detects this at startup via checkConsistency(): if mx_crypto_initialised is set in localStorage but the crypto store is empty, it surfaces the StorageEvictedDialog .
Real-world triggers include running out of disk space and browser-side eviction without user action . When eviction occurs, localStorage metadata remains intact, causing a consistency mismatch that Element detects as a missing session .
Defense mechanisms:
- Persistent storage request —
tryPersistStorage()callsnavigator.storage.persist()at login to opt the origin out of automatic eviction . - Sync store worker fallback — if the dedicated Web Worker for IndexedDB fails, the store falls back to the main-thread
LocalIndexedDBStoreBackendrather than failing entirely (PR #5361) . - Consistency check —
checkConsistency()validates both sync and crypto stores on every session restore and login .
Rust crypto: Unlike the legacy stack, the Rust crypto backend requires IndexedDB and has no fallback. If IndexedDB is inaccessible, the client cannot start .
Pickle Key / OS Keychain Issues#
On Desktop, the pickle key (used to encrypt access tokens and key the crypto store) lives in the OS keychain via Electron's safeStorage. A known bug: if the keychain is transiently unavailable, loadOrCreatePickleKey() calls createPickleKey and overwrites the still-valid ciphertext — turning a transient failure into permanent session loss. PR #33986 proposes a fix using typed SafeStorageDecryptionError . This is the root cause behind "Unable to restore session: Error decrypting secret access_token: no pickle key found" errors seen on Linux .
Key Source Files Reference#
| File | Repo | Role |
|---|---|---|
src/sync.ts | matrix-js-sdk | Sync loop and room event routing |
src/models/room-state.ts | matrix-js-sdk | RoomState.setStateEvents() — membership state hub |
src/models/room-member.ts | matrix-js-sdk | RoomMember.setMembershipEvent() |
src/store/indexeddb.ts | matrix-js-sdk | Sync/session store with worker fallback |
src/stores/room-list/RoomListStore.ts | matrix-react-sdk | Dispatches room list updates on membership changes |
src/stores/room-list/algorithms/Algorithm.ts | matrix-react-sdk | Tag-based room list sorting/filtering |
apps/web/src/utils/StorageManager.ts | element-web | checkConsistency(), tryPersistStorage() |
apps/web/src/Lifecycle.ts | element-web | Session restore, pickle key loading |