Skip to content

Conversation

@masenf
Copy link
Collaborator

@masenf masenf commented Nov 11, 2025

  • "reconnect" hydrate doesn't reset client storage or trigger on_load, it just returns the latest complete state dict
  • this event is driven by the frontend after the socket connects
  • update link_token_to_sid to NOT emit a delta when the sid changes for a token; the client will be informed of this during the initial hydrate or "reconnect" hydrate.

Fix #5963

* "reconnect" hydrate doesn't reset client storage or trigger on_load, it just
  returns the latest complete state dict
* this event is driven by the frontend after the socket connects
* update `link_token_to_sid` to NOT emit a delta when the sid changes for a
  token; the client will be informed of this during the initial hydrate or
  "reconnect" hydrate.

Fix #5963
@codspeed-hq
Copy link

codspeed-hq bot commented Nov 11, 2025

CodSpeed Performance Report

Merging #5969 will not alter performance

Comparing masenf/rehydrate-on-reconnect (e842f4d) with main (81a1b2f)

Summary

✅ 8 untouched

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Nov 11, 2025

Greptile Overview

Greptile Summary

This PR implements a frontend-driven reconnection hydration mechanism to fix issue #5963 where stale state would linger on the client after socket reconnection when backend state expired. The solution introduces a rehydrate flag that triggers a special "reconnect" hydrate event on socket reconnection, which fetches the latest complete state without resetting client storage or re-running on_load events.

Key Changes:

  • Frontend now sets a rehydrate flag when manually reconnecting and sends a hydrate event with is_reconnect: true upon connection
  • Backend hydrate middleware skips client storage reset and is_hydrated flag manipulation when processing reconnect hydrates
  • Changed link_token_to_sid to use state_manager.modify_state instead of app.modify_state to avoid emitting unnecessary deltas (full state is sent via reconnect hydrate)
  • Removed the previous backend-driven reload check that was added in commit 8388d74

Architecture:
This replaces the previous backend-driven approach (checking if router_data exists on connect) with a cleaner frontend-driven approach where the client explicitly requests a reconnect hydrate. This gives the frontend more control and reduces backend complexity.

Confidence Score: 4/5

  • This PR is safe to merge with one minor assumption that should be verified through testing
  • The implementation is well-structured and addresses the stated issue effectively. The changes are minimal and focused, replacing a backend-driven solution with a frontend-driven one. However, the score is 4 instead of 5 due to one assumption: the code directly accesses initialEvents()[0] and mutates its payload without defensive checks. If initialEvents() returns an empty array or the hydrate event structure changes, this could cause a runtime error. The fix is straightforward (add a null check), but the likelihood of this being an issue in practice is low since initialEvents() is generated code that should always include the hydrate event as the first element.
  • Pay attention to reflex/.templates/web/utils/state.js - verify that initialEvents()[0] always returns the expected hydrate event object in all scenarios

Important Files Changed

File Analysis

Filename Score Overview
reflex/.templates/web/utils/state.js 4/5 Added reconnect hydration logic: sets rehydrate flag on manual reconnect, sends hydrate event with is_reconnect: true on socket connect to fetch latest state without re-running on_load events
reflex/middleware/hydrate_middleware.py 5/5 Modified to skip resetting client storage and is_hydrated flag when is_reconnect payload is true, allowing reconnect hydration without re-triggering on_load events
reflex/app.py 5/5 Changed from self.app.modify_state to self.app.state_manager.modify_state to prevent emitting delta when updating session ID during token linking, since reconnect hydrate will provide full state

Sequence Diagram

sequenceDiagram
    participant Client as Frontend (state.js)
    participant Socket as WebSocket
    participant Backend as EventNamespace
    participant Middleware as HydrateMiddleware
    participant State as State Manager

    Note over Client,State: Normal Reconnection Flow
    
    Client->>Client: Detect disconnection
    Client->>Client: Call socket.reconnect()
    Client->>Client: Set rehydrate flag = true
    Client->>Socket: Connect with token
    
    Socket->>Backend: on_connect(sid, token)
    Backend->>Backend: link_token_to_sid(sid, token)
    Backend->>State: state_manager.modify_state()
    Note right of Backend: No delta emitted (changed from app.modify_state)
    Backend->>State: Update router_data[SESSION_ID] = sid
    
    Socket-->>Client: "connect" event
    
    Client->>Client: Check rehydrate flag = true
    Client->>Client: Get initialEvents()[0]
    Client->>Client: Set payload.is_reconnect = true
    Client->>Client: Reset rehydrate flag = false
    Client->>Socket: Queue hydrate event with is_reconnect=true
    
    Socket->>Backend: Process hydrate event
    Backend->>Middleware: preprocess(event)
    
    alt is_reconnect = true
        Note over Middleware: Skip _reset_client_storage()
        Note over Middleware: Skip setting is_hydrated = False
        Note over Middleware: On_load events will NOT run
    else is_reconnect = false (normal hydrate)
        Middleware->>State: _reset_client_storage()
        Middleware->>State: Set is_hydrated = False
        Note over Middleware: On_load events WILL run
    end
    
    Middleware->>State: Get full state dict via _resolve_delta()
    Middleware->>State: _clean()
    Middleware-->>Backend: Return StateUpdate with full delta
    
    Backend->>Socket: Emit state update
    Socket-->>Client: Receive full state
    Client->>Client: Apply state update to frontend
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 622 to 623
const hydrate_event = initialEvents()[0];
hydrate_event.payload.is_reconnect = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Accessing initialEvents()[0] without checking if it exists could cause runtime error if array is empty

Suggested change
const hydrate_event = initialEvents()[0];
hydrate_event.payload.is_reconnect = true;
const hydrate_event = initialEvents()[0];
if (!hydrate_event) {
console.error("No hydrate event found in initialEvents()");
return;
}
hydrate_event.payload.is_reconnect = true;
Prompt To Fix With AI
This is a comment left during a code review.
Path: reflex/.templates/web/utils/state.js
Line: 622:623

Comment:
**style:** Accessing `initialEvents()[0]` without checking if it exists could cause runtime error if array is empty

```suggestion
      const hydrate_event = initialEvents()[0];
      if (!hydrate_event) {
        console.error("No hydrate event found in initialEvents()");
        return;
      }
      hydrate_event.payload.is_reconnect = true;
```

How can I resolve this? If you propose a fix, please make it concise.

if the app is stateless, then presumably we don't end up connecting a websocket
at all, so we wouldn't hit this code. also if `initialEvents` was empty for
whatever reason, then the app probably wouldn't work... but lets make it look a
bit safer.
@adhami3310 adhami3310 merged commit da53877 into main Nov 13, 2025
47 checks passed
@adhami3310 adhami3310 deleted the masenf/rehydrate-on-reconnect branch November 13, 2025 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

When socket reconnects, and state is expired, it should rehydrate

3 participants