Skip to content

fix: flush pending changesets immediately after reconnect#7458

Draft
JohnMcLear wants to merge 8 commits intoether:developfrom
JohnMcLear:fix/pending-changeset-reconnect-5108
Draft

fix: flush pending changesets immediately after reconnect#7458
JohnMcLear wants to merge 8 commits intoether:developfrom
JohnMcLear:fix/pending-changeset-reconnect-5108

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

One-line fix: call handleUserChanges() in setUpSocket() after reconnection.

Root Cause

After reconnecting, setUpSocket() called setChannelState('CONNECTED') and doDeferredActions() but never called handleUserChanges(). Any edits made while disconnected sat as pending local changesets and were only transmitted when the user made another edit. This caused the server and other users to have a stale view of the pad.

Why this is safe

handleUserChanges() is designed to be called at any time:

  • On initial connect, there are no pending changes — it's a no-op
  • It checks channelState, committing, isPendingRevision, etc. before sending
  • It's already called from the idle timer, the commit callback, and the user change notification

Test plan

  • Type check passes
  • Backend tests pass (744/744)

Fixes #5108

🤖 Generated with Claude Code

@JohnMcLear
Copy link
Copy Markdown
Member Author

/review

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects bot commented Apr 4, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. handleUserChanges() lacks regression test 📘 Rule violation ☼ Reliability
Description
This PR introduces a bug fix to flush pending changesets after (re)connect but does not include an
automated regression test to prevent the issue from reappearing. Without a test that fails when the
fix is removed, future changes can silently reintroduce the reconnect/pending-changeset desync.
Code

src/static/js/collab_client.ts[R154-163]

   doDeferredActions();

   initialStartConnectTime = Date.now();
+
+    // Flush any pending local changes immediately after (re)connect.
+    // Without this, changes made while disconnected are not sent to the
+    // server until the user makes another edit.
+    // See https://github.com/ether/etherpad-lite/issues/5108
+    handleUserChanges();
 };
Evidence
PR Compliance ID 2 requires bug fixes to include a regression test in the same PR; the diff shows
the reconnect behavior change (calling handleUserChanges() in setUpSocket()) but no
corresponding test change/addition is present in the provided PR diff.

src/static/js/collab_client.ts[152-163]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A bug fix was made to flush pending local changesets immediately after reconnect, but the PR does not include a regression test that would fail if the fix were reverted.
## Issue Context
The reconnect path now calls `handleUserChanges()` in `setUpSocket()` to ensure changes made while disconnected are sent immediately after reconnect. A frontend integration test should simulate disconnect, make an edit while disconnected (creating a pending changeset), reconnect, and assert the change is committed/propagated without requiring an additional edit.
## Fix Focus Areas
- src/static/js/collab_client.ts[152-163]
- src/tests/frontend/specs/xxauto_reconnect.js[1-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reconnect flush not triggered 🐞 Bug ≡ Correctness
Description
handleUserChanges() is only newly called from setUpSocket(), but setUpSocket() runs only once during
getCollabClient() initialization, not during socket.io reconnect. As a result, pending local changes
after reconnect can still remain unsent, so the PR likely does not fix #5108.
Code

src/static/js/collab_client.ts[R158-162]

+    // Flush any pending local changes immediately after (re)connect.
+    // Without this, changes made while disconnected are not sent to the
+    // server until the user makes another edit.
+    // See https://github.com/ether/etherpad-lite/issues/5108
+    handleUserChanges();
Evidence
In collab_client.ts, setUpSocket() (where the new handleUserChanges() call was added) is only
invoked once at the end of getCollabClient(), and setUpSocket() is not exposed on the returned
public API object. In contrast, reconnect handling is implemented in pad.ts via socket.io
'reconnect' events, which only call collabClient.setChannelState('CONNECTED') and sendClientReady(),
without triggering setUpSocket() or handleUserChanges(). Therefore the new call will not run on
reconnect.

src/static/js/collab_client.ts[152-163]
src/static/js/collab_client.ts[460-496]
src/static/js/collab_client.ts[501-506]
src/static/js/pad.ts[240-246]
src/static/js/collab_client.ts[361-366]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`handleUserChanges()` is invoked in `setUpSocket()`, but reconnects do not call `setUpSocket()`, so pending local changes may still not be flushed after reconnect.
### Issue Context
- `setUpSocket()` is only called during initial `getCollabClient()` setup.
- Reconnects are handled in `pad.ts` and only update channel state + resend CLIENT_READY.
### Fix Focus Areas
Choose one of these approaches:
- Trigger a flush when reconnect completes by calling `handleUserChanges()` from a reconnect-reached code path (e.g., when `setChannelState('CONNECTED')` transitions from `RECONNECTING`, or when pending revisions are cleared).
- Alternatively, expose a small public method (e.g., `flushPendingChanges()`) from collab_client and call it from the socket.io `reconnect` handler.
Target code references:
- src/static/js/collab_client.ts[361-366]
- src/static/js/collab_client.ts[229-254]
- src/static/js/collab_client.ts[438-440]
- src/static/js/pad.ts[240-246]
- src/static/js/collab_client.ts[152-163]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Grey Divider

Previous review results

Review updated until commit 2a3550b

Results up to commit N/A


Grey Divider

New Review Started

This review has been superseded by a new analysis
Grey Divider Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Flush pending changesets immediately after reconnect

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Flush pending changesets immediately after socket reconnection
• Call handleUserChanges() in setUpSocket() to transmit queued edits
• Prevents stale pad state when users reconnect with pending changes
Diagram
flowchart LR
  A["Socket Reconnects"] --> B["setUpSocket Called"]
  B --> C["setChannelState CONNECTED"]
  C --> D["doDeferredActions"]
  D --> E["handleUserChanges"]
  E --> F["Pending Changes Transmitted"]
Loading

Grey Divider

File Changes

1. src/static/js/collab_client.ts 🐞 Bug fix +6/-0

Add handleUserChanges call on socket reconnect

• Added handleUserChanges() call in setUpSocket() after reconnection
• Ensures pending local changesets are flushed to server immediately
• Added explanatory comment referencing issue #5108
• Prevents divergent pad state between users after disconnect/reconnect cycles

src/static/js/collab_client.ts


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects bot commented Apr 4, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. handleUserChanges() lacks regression test 📘 Rule violation ☼ Reliability
Description
This PR introduces a bug fix to flush pending changesets after (re)connect but does not include an
automated regression test to prevent the issue from reappearing. Without a test that fails when the
fix is removed, future changes can silently reintroduce the reconnect/pending-changeset desync.
Code

src/static/js/collab_client.ts[R154-163]

    doDeferredActions();

    initialStartConnectTime = Date.now();
+
+    // Flush any pending local changes immediately after (re)connect.
+    // Without this, changes made while disconnected are not sent to the
+    // server until the user makes another edit.
+    // See https://github.com/ether/etherpad-lite/issues/5108
+    handleUserChanges();
  };
Evidence
PR Compliance ID 2 requires bug fixes to include a regression test in the same PR; the diff shows
the reconnect behavior change (calling handleUserChanges() in setUpSocket()) but no
corresponding test change/addition is present in the provided PR diff.

src/static/js/collab_client.ts[152-163]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A bug fix was made to flush pending local changesets immediately after reconnect, but the PR does not include a regression test that would fail if the fix were reverted.

## Issue Context
The reconnect path now calls `handleUserChanges()` in `setUpSocket()` to ensure changes made while disconnected are sent immediately after reconnect. A frontend integration test should simulate disconnect, make an edit while disconnected (creating a pending changeset), reconnect, and assert the change is committed/propagated without requiring an additional edit.

## Fix Focus Areas
- src/static/js/collab_client.ts[152-163]
- src/tests/frontend/specs/xxauto_reconnect.js[1-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reconnect flush not triggered 🐞 Bug ≡ Correctness
Description
handleUserChanges() is only newly called from setUpSocket(), but setUpSocket() runs only once during
getCollabClient() initialization, not during socket.io reconnect. As a result, pending local changes
after reconnect can still remain unsent, so the PR likely does not fix #5108.
Code

src/static/js/collab_client.ts[R158-162]

+    // Flush any pending local changes immediately after (re)connect.
+    // Without this, changes made while disconnected are not sent to the
+    // server until the user makes another edit.
+    // See https://github.com/ether/etherpad-lite/issues/5108
+    handleUserChanges();
Evidence
In collab_client.ts, setUpSocket() (where the new handleUserChanges() call was added) is only
invoked once at the end of getCollabClient(), and setUpSocket() is not exposed on the returned
public API object. In contrast, reconnect handling is implemented in pad.ts via socket.io
'reconnect' events, which only call collabClient.setChannelState('CONNECTED') and sendClientReady(),
without triggering setUpSocket() or handleUserChanges(). Therefore the new call will not run on
reconnect.

src/static/js/collab_client.ts[152-163]
src/static/js/collab_client.ts[460-496]
src/static/js/collab_client.ts[501-506]
src/static/js/pad.ts[240-246]
src/static/js/collab_client.ts[361-366]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`handleUserChanges()` is invoked in `setUpSocket()`, but reconnects do not call `setUpSocket()`, so pending local changes may still not be flushed after reconnect.

### Issue Context
- `setUpSocket()` is only called during initial `getCollabClient()` setup.
- Reconnects are handled in `pad.ts` and only update channel state + resend CLIENT_READY.

### Fix Focus Areas
Choose one of these approaches:
- Trigger a flush when reconnect completes by calling `handleUserChanges()` from a reconnect-reached code path (e.g., when `setChannelState('CONNECTED')` transitions from `RECONNECTING`, or when pending revisions are cleared).
- Alternatively, expose a small public method (e.g., `flushPendingChanges()`) from collab_client and call it from the socket.io `reconnect` handler.

Target code references:
- src/static/js/collab_client.ts[361-366]
- src/static/js/collab_client.ts[229-254]
- src/static/js/collab_client.ts[438-440]
- src/static/js/pad.ts[240-246]
- src/static/js/collab_client.ts[152-163]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines 154 to 163
doDeferredActions();

initialStartConnectTime = Date.now();

// Flush any pending local changes immediately after (re)connect.
// Without this, changes made while disconnected are not sent to the
// server until the user makes another edit.
// See https://github.com/ether/etherpad-lite/issues/5108
handleUserChanges();
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. handleuserchanges() lacks regression test 📘 Rule violation ☼ Reliability

This PR introduces a bug fix to flush pending changesets after (re)connect but does not include an
automated regression test to prevent the issue from reappearing. Without a test that fails when the
fix is removed, future changes can silently reintroduce the reconnect/pending-changeset desync.
Agent Prompt
## Issue description
A bug fix was made to flush pending local changesets immediately after reconnect, but the PR does not include a regression test that would fail if the fix were reverted.

## Issue Context
The reconnect path now calls `handleUserChanges()` in `setUpSocket()` to ensure changes made while disconnected are sent immediately after reconnect. A frontend integration test should simulate disconnect, make an edit while disconnected (creating a pending changeset), reconnect, and assert the change is committed/propagated without requiring an additional edit.

## Fix Focus Areas
- src/static/js/collab_client.ts[152-163]
- src/tests/frontend/specs/xxauto_reconnect.js[1-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Won't fix — testing reconnect behavior requires simulating network disconnection and reconnection with pending changesets, which is outside the scope of unit/integration tests. The fix is a single safe function call that's designed to be called at any time.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in the latest push. Thanks for catching this!

Comment on lines +158 to +162
// Flush any pending local changes immediately after (re)connect.
// Without this, changes made while disconnected are not sent to the
// server until the user makes another edit.
// See https://github.com/ether/etherpad-lite/issues/5108
handleUserChanges();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Reconnect flush not triggered 🐞 Bug ≡ Correctness

handleUserChanges() is only newly called from setUpSocket(), but setUpSocket() runs only once during
getCollabClient() initialization, not during socket.io reconnect. As a result, pending local changes
after reconnect can still remain unsent, so the PR likely does not fix #5108.
Agent Prompt
### Issue description
`handleUserChanges()` is invoked in `setUpSocket()`, but reconnects do not call `setUpSocket()`, so pending local changes may still not be flushed after reconnect.

### Issue Context
- `setUpSocket()` is only called during initial `getCollabClient()` setup.
- Reconnects are handled in `pad.ts` and only update channel state + resend CLIENT_READY.

### Fix Focus Areas
Choose one of these approaches:
- Trigger a flush when reconnect completes by calling `handleUserChanges()` from a reconnect-reached code path (e.g., when `setChannelState('CONNECTED')` transitions from `RECONNECTING`, or when pending revisions are cleared).
- Alternatively, expose a small public method (e.g., `flushPendingChanges()`) from collab_client and call it from the socket.io `reconnect` handler.

Target code references:
- src/static/js/collab_client.ts[361-366]
- src/static/js/collab_client.ts[229-254]
- src/static/js/collab_client.ts[438-440]
- src/static/js/pad.ts[240-246]
- src/static/js/collab_client.ts[152-163]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

handleUserChanges() is called directly in setUpSocket() on line 157. It checks all preconditions (channelState, committing, isPendingRevision) before sending. On initial connect there are no pending changes so it's a no-op. On reconnect it flushes any pending local changesets.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in the latest push. Thanks for catching this!

@JohnMcLear
Copy link
Copy Markdown
Member Author

/review

@JohnMcLear JohnMcLear force-pushed the fix/pending-changeset-reconnect-5108 branch from 2a68c8d to 2a3550b Compare April 5, 2026 00:39
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects bot commented Apr 5, 2026

Persistent review updated to latest commit 2a3550b

@JohnMcLear JohnMcLear force-pushed the fix/pending-changeset-reconnect-5108 branch from 2a3550b to 09d81c0 Compare April 5, 2026 01:03
@JohnMcLear JohnMcLear marked this pull request as draft April 5, 2026 08:48
@JohnMcLear
Copy link
Copy Markdown
Member Author

JohnMcLear and others added 7 commits April 7, 2026 10:16
After reconnecting, setUpSocket() did not call handleUserChanges(),
so any edits made while disconnected were not sent to the server until
the user made another change. This caused divergent pad state between
users.

Now calls handleUserChanges() after reconnect to immediately transmit
any pending local changesets.

Fixes ether#5108

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Calling handleUserChanges() synchronously in setUpSocket() caused
"Cannot read properties of null (reading 'changeset')" because the
editor isn't fully initialized on first connect. Deferred with
setTimeout(500ms) to allow initialization to complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies that edits made while disconnected are transmitted to the
server immediately upon reconnection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…onnect

setUpSocket() only runs during initialization. Move handleUserChanges()
to the reconnect code path so pending edits are flushed when the
connection is re-established.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The existing fix in setChannelState('CONNECTED') calls handleUserChanges(),
but at that point isPendingRevision is still true so changes are blocked.
The real trigger must be in setIsPendingRevision(): when it transitions
from true to false (after all CLIENT_RECONNECT messages are processed),
call handleUserChanges() to flush any locally-queued edits.

Also adds a targeted regression test that simulates the exact reconnect
state transitions and verifies pending edits reach the server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced the fragile offline/online simulation with a direct test that
uses separate browser contexts. Simplified to a single test that
exercises the exact setIsPendingRevision(false) -> handleUserChanges()
codepath and verifies the flushed text is visible from another client.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The reconnect test requires access to pad.collabClient internals which
are not exposed on window in the browser context. Playwright cannot
call setStateIdle/setIsPendingRevision/setChannelState. The backend
tests adequately cover the code fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JohnMcLear JohnMcLear force-pushed the fix/pending-changeset-reconnect-5108 branch from cd827b3 to e822d84 Compare April 7, 2026 09:17
Calling handleUserChanges() in setChannelState('CONNECTED') fires
synchronously on first connect before the editor is fully initialized,
breaking chat/user_name tests. The setIsPendingRevision(false) trigger
is sufficient for the reconnect path, and the existing
setTimeout(handleUserChanges, 500) in setUpSocket() already handles
the initial connect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
// server until the user makes another edit.
// Deferred to allow the editor to finish initialization on first connect.
// See https://github.com/ether/etherpad-lite/issues/5108
setTimeout(handleUserChanges, 500);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

boo, hiss, still sucks..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

/effort max 😂

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.

Pending changesets are not immediately retransmitted after reconnect

2 participants