You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Performance/Refactoring: Migrate reconnect message synchronization from DDP loadMissedMessages to REST syncMessages with index optimizations
Context & Problem Statement
Currently, when a client loses connection and subsequently reconnects to a Rocket.Chat server, the reconnection sync flow loads missed messages using a legacy DDP method: 'loadMissedMessages'.
While this legacy flow has worked historically, it has several critical flaws that undermine data consistency and limit Rocket.Chat's architectural scalability:
Missing Offline Edits: The legacy DDP query retrieves messages based on their creation timestamp (ts). If a message is edited by another user while the client is offline, the client will completely miss this update because the creation ts is unchanged, leading to stale data on the client.
Missing Offline Deletions: The client is not informed of deletions that occur while offline. This leads to "ghost" messages remaining visible on the client UI until a full application reload is forced.
DDP Overhead: This mechanism locks Rocket.Chat into heavy, stateful, WebSocket-based DDP operations, obstructing efforts to migrate entirely to modern, stateless HTTP load balancers and REST-first APIs.
Proposed REST-Based Sync Architecture
To solve these flaws and advance the deprecation of legacy DDP, we propose migrating the reconnection message sync to the lightweight GET /v1/chat.syncMessages REST endpoint. This endpoint is designed to return both updated (edited/created) and deleted messages since a given timestamp in a single query.
Sequence Diagram
sequenceDiagram
participant Client as Client (Zustand Stores)
participant Hook as useLoadMissedMessages Hook
participant API as REST API (/v1/chat.syncMessages)
participant DB as MongoDB (Message & Trash)
Client->>Hook: Connection Restored (connected: true)
Hook->>Client: Retrieve last synced timestamp (lastUpdate)
Hook->>API: GET /v1/chat.syncMessages?roomId={rid}&lastUpdate={lastUpdate}
API->>DB: Query updated & deleted messages since lastUpdate
DB-->>API: Returns { updated: IMessage[], deleted: Array }
API-->>Hook: Return JSON Response
Hook->>Client: Upsert updated messages (upsertMessageBulk)
Hook->>Client: Purge deleted messages (Messages.state.delete)
importtype{IRoom}from'@rocket.chat/core-typings';import{useConnectionStatus,useEndpoint}from'@rocket.chat/ui-contexts';import{useEffect,useRef}from'react';import{LegacyRoomManager,upsertMessageBulk}from'../../../../app/ui-utils/client';import{Messages,Subscriptions}from'../../../stores';/** * Synchronizes missed, edited, and deleted messages for a room using REST API. */constsyncRoomMessages=async(rid: IRoom['_id'],syncMessages: (params: {roomId: string;lastUpdate: string})=>Promise<{result: {updated: any[];deleted: {_id: string;_deletedAt: string}[];};success: boolean;}>): Promise<void>=>{// Find the latest updated message to establish a highly reliable synchronization epochconstlastMessage=Messages.state.findFirst((record)=>record.rid===rid&&record._hidden!==true&&!record.temp,(a,b)=>b._updatedAt.getTime()-a._updatedAt.getTime(),);if(!lastMessage){return;}try{// Fetch updates and deletions since lastMessage._updatedAtconstresponse=awaitsyncMessages({roomId: rid,lastUpdate: lastMessage._updatedAt.toISOString(),});if(response.success&&response.result){const{ updated, deleted }=response.result;constsubscription=Subscriptions.state.find((record)=>record.rid===rid);// 1. Reconcile added and edited messagesif(updated&&updated.length>0){awaitupsertMessageBulk({msgs: updated, subscription });}// 2. Reconcile deleted messagesif(deleted&&deleted.length>0){deleted.forEach((delRecord)=>{Messages.state.delete(delRecord._id);});}}}catch(error){console.error('Error synchronizing room messages:',error);}};exportconstuseLoadMissedMessages=(): void=>{const{ connected }=useConnectionStatus();constconnectionWasOnlineRef=useRef(connected);constsyncMessages=useEndpoint('GET','/v1/chat.syncMessages');useEffect(()=>{if(connected===true&&connectionWasOnlineRef.current===false&&LegacyRoomManager.openedRooms){Object.keys(LegacyRoomManager.openedRooms).forEach((key)=>{constvalue=LegacyRoomManager.openedRooms[key];if(value.rid){syncRoomMessages(value.rid,syncMessages);}});}connectionWasOnlineRef.current=connected;},[connected,syncMessages]);};
Database Performance Analysis & Index Optimization
The codebase currently notes that /v1/chat.syncMessages is "currently too slow for this path" (see useLoadMissedMessages.ts:L24-26). Let's dissect the database query pattern to resolve this:
The Query Pattern of chat.syncMessages
The server-side implementation in apps/meteor/server/publications/messages.ts delegates to handleWithoutPagination(rid, lastUpdate):
Performance Bottleneck: Because ts is positioned between rid and _updatedAt, MongoDB must scan every index key matching rid to evaluate the range query on _updatedAt. This behaves as an $O(N)$ scan of the channel's message index keys, scaling extremely poorly for large channels with high volume.
Optimized Solution: Create a dedicated compound index on the message collection:
{rid: 1,_updatedAt: 1}
This allows MongoDB to perform an exact boundary scan on _updatedAt for the target roomId, ensuring $O(\log N)$ complexity.
Current Index: No specific compound index is defined for rid and _deletedAt in the trash collection, causing slow collection-wide scans on delete audits.
Optimized Solution: Create a compound index on the trash collection:
{__collection__: 1,rid: 1,_deletedAt: 1}
Automated Index Application
To apply the optimized index on startup, add it to modelIndexes() in packages/models/src/models/Messages.ts:
Optimize Database Indexes: Modify packages/models/src/models/Messages.ts to include { rid: 1, _updatedAt: 1 }.
Implement Client REST Sync: Refactor the useLoadMissedMessages hook to invoke /v1/chat.syncMessages and reconcile both updated and deleted message lists directly in Zustand state.
Handle Edge Cases:
Verify e2e rooms (handling encryption/decryption keys and pending statuses).
Ensure handling of potential large batches by checking if pagination is needed should the number of offline changes exceed a threshold.
Deprecate Legacy Code: Safely remove apps/meteor/server/methods/loadMissedMessages.ts and clean up its registration.
Performance/Refactoring: Migrate reconnect message synchronization from DDP loadMissedMessages to REST syncMessages with index optimizations
Context & Problem Statement
Currently, when a client loses connection and subsequently reconnects to a Rocket.Chat server, the reconnection sync flow loads missed messages using a legacy DDP method:
'loadMissedMessages'.While this legacy flow has worked historically, it has several critical flaws that undermine data consistency and limit Rocket.Chat's architectural scalability:
ts). If a message is edited by another user while the client is offline, the client will completely miss this update because the creationtsis unchanged, leading to stale data on the client.Proposed REST-Based Sync Architecture
To solve these flaws and advance the deprecation of legacy DDP, we propose migrating the reconnection message sync to the lightweight
GET /v1/chat.syncMessagesREST endpoint. This endpoint is designed to return both updated (edited/created) and deleted messages since a given timestamp in a single query.Sequence Diagram
sequenceDiagram participant Client as Client (Zustand Stores) participant Hook as useLoadMissedMessages Hook participant API as REST API (/v1/chat.syncMessages) participant DB as MongoDB (Message & Trash) Client->>Hook: Connection Restored (connected: true) Hook->>Client: Retrieve last synced timestamp (lastUpdate) Hook->>API: GET /v1/chat.syncMessages?roomId={rid}&lastUpdate={lastUpdate} API->>DB: Query updated & deleted messages since lastUpdate DB-->>API: Returns { updated: IMessage[], deleted: Array } API-->>Hook: Return JSON Response Hook->>Client: Upsert updated messages (upsertMessageBulk) Hook->>Client: Purge deleted messages (Messages.state.delete)Proposed Client-Side Hook (
useLoadMissedMessages.ts)Database Performance Analysis & Index Optimization
The codebase currently notes that
/v1/chat.syncMessagesis"currently too slow for this path"(seeuseLoadMissedMessages.ts:L24-26). Let's dissect the database query pattern to resolve this:The Query Pattern of
chat.syncMessagesThe server-side implementation in
apps/meteor/server/publications/messages.tsdelegates tohandleWithoutPagination(rid, lastUpdate):For Updated Messages:
{ rid: roomId, _hidden: { $ne: true }, _updatedAt: { $gt: lastUpdate } }{ rid: 1, ts: 1, _updatedAt: 1 }tsis positioned betweenridand_updatedAt, MongoDB must scan every index key matchingridto evaluate the range query on_updatedAt. This behaves as anmessagecollection:_updatedAtfor the targetroomId, ensuringFor Deleted Messages (Trash Collection):
{ __collection__: 'message', _deletedAt: { $gt: lastUpdate }, rid: roomId }ridand_deletedAtin the trash collection, causing slow collection-wide scans on delete audits.Automated Index Application
To apply the optimized index on startup, add it to
modelIndexes()inpackages/models/src/models/Messages.ts:Action Items & Migration Roadmap
packages/models/src/models/Messages.tsto include{ rid: 1, _updatedAt: 1 }.useLoadMissedMessageshook to invoke/v1/chat.syncMessagesand reconcile bothupdatedanddeletedmessage lists directly in Zustand state.apps/meteor/server/methods/loadMissedMessages.tsand clean up its registration.