Skip to content

feat: offline sync queue - reconcile TEMP messages on restart, auto-r…#6964

Draft
deepak0x wants to merge 1 commit intoRocketChat:developfrom
deepak0x:feature/offline-sync-queue
Draft

feat: offline sync queue - reconcile TEMP messages on restart, auto-r…#6964
deepak0x wants to merge 1 commit intoRocketChat:developfrom
deepak0x:feature/offline-sync-queue

Conversation

@deepak0x
Copy link

@deepak0x deepak0x commented Feb 4, 2026

This PR adds a offline/sync flow to ensure messages don’t remain stuck after app restarts, crashes, or network changes.

  • Stuck TEMP messages: On login, messages that have been in TEMP state for more than 5 minutes are reconciled:

    • If the message exists on the server (via chat.getMessage), its status is updated to SENT.
    • Otherwise, the message is resent. If resend fails, it is moved to ERROR.
  • Auto-retry ERROR messages: When the app reconnects (METEOR.SUCCESS), all messages in ERROR state are automatically retried so users don’t need to manually tap Resend after going back online.

  • ** Details**:

    • Introduced messageSync.ts with reconcileTempMessages() and retryErrorMessages().
    • Exported changeMessageStatus from sendMessage.ts for reuse.
    • TEMP reconciliation runs once on login success.
    • Added a messageSync saga that retries ERROR messages on connection success when the user is logged in.

Issue(s)

Closes #6928

  • Messages stuck in TEMP remained in a perpetual Sending… state after app crash or force kill.
  • Messages in ERROR state were never retried automatically after network reconnection.

How to Test / Reproduce

Stuck TEMP Messages

  1. Open a room and send a message.
  2. Force close the app while the message is still in Sending… state.
  3. Reopen the app and navigate back to the room.

Expected result:

  • The message is reconciled:

    • Marked as SENT if it exists on the server, or
    • Resent automatically, or
    • Moved to ERROR if resend fails.

Auto-retry ERROR Messages

  1. Enable Airplane Mode.
  2. Send a message (it moves to ERROR).
  3. Disable Airplane Mode and wait for reconnection.

result:

  • The message is retried automatically without tapping Resend.

Screenshots


Types of Changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update

Checklist

  • I have read the CONTRIBUTING document
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Summary by CodeRabbit

  • New Features
    • Messages stuck in temporary status older than 5 minutes are automatically reconciled with server state
    • Messages with send errors are automatically retried
    • Message synchronization processes run in the background after login and on successful server connections

…etry ERROR on reconnect

- Export changeMessageStatus from sendMessage for reconciliation
- Add messageSync.ts: reconcileTempMessages (TEMP older than 5min) and retryErrorMessages
- Run reconcileTempMessages on login success (handleLoginSuccess)
- Add messageSync saga: retry ERROR messages on METEOR.SUCCESS when user logged in
- Register messageSync saga in root
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Walkthrough

The changes introduce message synchronization utilities to handle reconciliation of temporary messages and retry of failed message sends. A new messageSync module provides two public functions, while new sagas integrate these operations into the app's state management flow—one running after login and another on successful meteor events.

Changes

Cohort / File(s) Summary
Message Sync Library
app/lib/methods/messageSync.ts, app/lib/methods/sendMessage.ts
Adds new messageSync module with reconcileTempMessages() and retryErrorMessages() utilities. Exports changeMessageStatus from sendMessage to enable status updates in reconciliation logic.
Redux-Saga Integration
app/sagas/messageSync.js, app/sagas/login.js, app/sagas/index.js
Introduces messageSync saga triggered on METEOR.SUCCESS to retry error messages. Adds reconcileTempMessagesSaga to login flow for post-login message reconciliation. Registers messageSync in root saga concurrency.

Sequence Diagram

sequenceDiagram
    actor User
    participant Redux as Redux/Sagas
    participant LocalDB as Local Database
    participant Server as Server API
    
    User->>Redux: Login Success
    activate Redux
    Redux->>LocalDB: reconcileTempMessagesSaga triggered
    activate LocalDB
    LocalDB->>LocalDB: Query TEMP messages (>5min old)
    loop For each TEMP message
        LocalDB->>Server: getSingleMessage(id)
        alt Server message exists
            Server-->>LocalDB: Message found
            LocalDB->>LocalDB: changeMessageStatus → SENT
        else Server message not found
            Server-->>LocalDB: Not found
            LocalDB->>Server: resendMessage()
        end
    end
    deactivate LocalDB
    deactivate Redux
    
    User->>Redux: METEOR.SUCCESS event
    activate Redux
    Redux->>LocalDB: retryErrorMessagesSaga triggered
    activate LocalDB
    LocalDB->>LocalDB: Query ERROR status messages
    loop For each ERROR message
        LocalDB->>Server: resendMessage()
        Server-->>LocalDB: Send result
    end
    deactivate LocalDB
    deactivate Redux
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Through gardens of messages, old and new they hop,
Reconciling status, so errors will stop,
When login succeeds, the sync magic starts,
TEMP becomes SENT, healing broken parts,
No message left behind in this rabbity delight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title is truncated and incompletely describes the main feature—it's cut off at 'auto-r…' missing the complete action descriptor. Expand the title to fully describe both key features: reconciling TEMP messages on restart and auto-retrying ERROR messages, e.g., 'feat: offline sync queue - reconcile TEMP messages on restart and auto-retry ERROR messages'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Reviews will stop working after February 8, 2026 if the new IP is not added to your allowlist.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/lib/methods/messageSync.ts`:
- Around line 33-49: The outer catch around getSingleMessage treats any error as
"message missing" and triggers resendMessage, which can create duplicates on
transient failures; update the error handling in the try/catch that surrounds
getSingleMessage (and the subsequent changeMessageStatus call) to inspect the
thrown error (e.g. check err.response?.status, err.status, or err.code) and only
call resendMessage(record, record.tmid ?? undefined) when the error indicates
the message is truly not found (e.g. 404 or specific "not found" code); for
other errors (network timeouts, 5xx, auth failures) either rethrow or log and
abort without resending so resendMessage is not invoked erroneously. Ensure you
reference getSingleMessage, changeMessageStatus, resendMessage, messagesStatus
and preserve the inner catch(e) { log(e) } behavior for the actual resend path.
🧹 Nitpick comments (2)
app/sagas/messageSync.js (1)

21-23: Consider using takeLatest instead of takeEvery to prevent concurrent retries.

METEOR.SUCCESS can fire multiple times during reconnection scenarios. Using takeEvery could spawn multiple concurrent retryErrorMessagesSaga instances, potentially causing the same ERROR messages to be retried simultaneously, leading to duplicate sends or race conditions.

♻️ Proposed fix
-const root = function* root() {
-	yield takeEvery(METEOR.SUCCESS, retryErrorMessagesSaga);
-};
+import { call, select, takeLatest } from 'redux-saga/effects';
+
+const root = function* root() {
+	yield takeLatest(METEOR.SUCCESS, retryErrorMessagesSaga);
+};
app/sagas/login.js (1)

251-251: Note: Reconciliation may be cancelled after 2 seconds.

The handleLoginSuccess task is cancelled after a 2-second timeout (see Lines 426-430). If reconcileTempMessages is processing many stuck messages, it may be interrupted mid-execution. This follows the existing pattern for other forked tasks, but consider whether the reconciliation should complete independently.

If uninterrupted execution is important, consider spawning instead of forking:

💡 Alternative using spawn for detached execution
-		yield fork(reconcileTempMessagesSaga);
+		yield spawn(reconcileTempMessagesSaga);

Note: spawn creates a detached task that won't be cancelled when the parent is cancelled. You'd need to import spawn from redux-saga/effects.

Comment on lines +33 to +49
try {
const serverMessage = await getSingleMessage(record.id);
if (serverMessage) {
await changeMessageStatus(
record.id,
messagesStatus.SENT,
record.tmid ?? undefined,
serverMessage
);
}
} catch {
try {
await resendMessage(record, record.tmid ?? undefined);
} catch (e) {
log(e);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Catch-all may cause unintended resends on transient failures.

The outer catch block (Lines 43-48) treats any getSingleMessage failure as "message doesn't exist on server" and triggers a resend. However, transient network errors, authentication failures, or server errors would also fall into this path, potentially causing duplicate messages.

Consider checking the error type or response status before deciding to resend:

🛡️ Proposed approach
 		for (const record of tempMessages as TMessageModel[]) {
 			try {
 				const serverMessage = await getSingleMessage(record.id);
 				if (serverMessage) {
 					await changeMessageStatus(
 						record.id,
 						messagesStatus.SENT,
 						record.tmid ?? undefined,
 						serverMessage
 					);
+				} else {
+					// Message confirmed not on server, safe to resend
+					await resendMessage(record, record.tmid ?? undefined);
 				}
-			} catch {
+			} catch (e: any) {
+				// Only resend if we got a 404/not-found response
+				// Otherwise log and skip to avoid duplicates on transient errors
+				if (e?.data?.error === 'error-invalid-message' || e?.response?.status === 404) {
 				try {
 					await resendMessage(record, record.tmid ?? undefined);
 				} catch (e) {
 					log(e);
 				}
+				} else {
+					log(e);
+				}
 			}
 		}
🤖 Prompt for AI Agents
In `@app/lib/methods/messageSync.ts` around lines 33 - 49, The outer catch around
getSingleMessage treats any error as "message missing" and triggers
resendMessage, which can create duplicates on transient failures; update the
error handling in the try/catch that surrounds getSingleMessage (and the
subsequent changeMessageStatus call) to inspect the thrown error (e.g. check
err.response?.status, err.status, or err.code) and only call
resendMessage(record, record.tmid ?? undefined) when the error indicates the
message is truly not found (e.g. 404 or specific "not found" code); for other
errors (network timeouts, 5xx, auth failures) either rethrow or log and abort
without resending so resendMessage is not invoked erroneously. Ensure you
reference getSingleMessage, changeMessageStatus, resendMessage, messagesStatus
and preserve the inner catch(e) { log(e) } behavior for the actual resend path.

@deepak0x deepak0x marked this pull request as draft February 4, 2026 14:04
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.

feature: Messages stuck in TEMP/ERROR status indefinitely without auto-retry after app restart/network recovery

1 participant