> ## Documentation Index
> Fetch the complete documentation index at: https://www.cometchat.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Stay in Sync

> Catch up on everything that changed while offline, observe sync progress, trigger a manual sync, and manage the local cache with the CometChat React Native Chat SDK.

> [accordion] AI Integration Quick Reference
>
> | Field | Value |
> | - | - |
> | Observe sync | `CometChat.addSyncListener(id, listener)` / `removeSyncListener(id)` |
> | Poll state | `CometChat.getSyncState()` → `IDLE` \| `SYNCING`; `await CometChat.getLastSyncTimestamp()` → server epoch seconds, `0` if never |
> | Manual sync | `await CometChat.forceSyncNow()` — resolves when the sweep is accepted |
> | Cache resets | `CometChat.addOfflineCacheResetListener(id, listener)` |
> | Cache size / clear | `await CometChat.getCacheSize()` (bytes) · `await CometChat.clearOfflineCache()` (destructive) |
> | Prerequisites | [Offline support enabled](https://www.cometchat.com/docs/sdk/react-native/offline-enable.md) |
>
> ```typescript theme={null}
> CometChat.addSyncListener("SYNC_LISTENER", {
>   onConversationSyncCompleted: (conversations) => { /* blind-replace list */ },
>   onMessageSyncCompleted: (messages, outcome) => { /* merge by id */ },
>   onMessageSyncError: (error) => {},
> });
> CometChat.removeSyncListener("SYNC_LISTENER");
>
> const state = CometChat.getSyncState();                 // IDLE | SYNCING
> const lastSync: number = await CometChat.getLastSyncTimestamp();
>
> await CometChat.forceSyncNow();                          // rejects: IN_PROGRESS / COOLDOWN / DISABLED / NOT_LOGGED_IN
>
> CometChat.addOfflineCacheResetListener("RESET_LISTENER", {
>   onOfflineCacheReset: (reason, pendingMessagesLost) => {},
> });
>
> const bytes: number = await CometChat.getCacheSize();
> await CometChat.clearOfflineCache();                     // destructive; drops the un-sent outbox
> ```

When the device comes back online, the SDK runs a **catch-up sync**: it refreshes the conversation list, then replays everything that changed in your messages since the last sync, and merges the results into the local cache. This page covers how to observe that sync, trigger one on demand, and manage the cache it maintains.

> [!NOTE]
>
> Everything on this page is a no-op unless offline support is active for the logged-in user (`CometChat.isOfflineEnabled()` is `true`). See [Enable Offline Support](https://www.cometchat.com/docs/sdk/react-native/offline-enable.md). The message catch-up depends on the server's `updatedAt` message changelog; against a server without it, each sweep returns nothing and missed edits, deletes, and reactions are not synced.

## How catch-up works

A sync runs automatically when the connection settles after a reconnect (the connection must hold for about 2.5 seconds). It never runs on a brief connect-then-drop flap, and overlapping triggers coalesce: at most one follow-up sweep is queued behind the running one. The outbox is flushed first, and the sweep starts after the flush finishes or after 20 seconds, whichever comes first. The sweep proceeds in two phases, in order:

1. **Conversation phase** — the conversation list is fetched as one fresh page. A conversation-list UI can blind-replace its list with the result.
2. **Message phase** — the SDK replays the message changelog from the last sync point. New messages and updates to already-cached messages are merged into the cache.

The sync is incremental and safe to repeat: it only moves forward from the last successful point, and a failed or interrupted phase simply resumes on the next reconnect. The conversation refresh does remove conversations you are no longer part of, along with their cached messages, media, and any still-queued messages for them (which fail with reason `DISCARDED`). A refresh that fails, or whose response cannot be read, removes nothing. A single sweep request that gets no answer is aborted after 60 seconds and counts as a failed phase. A first-ever sync (cold start) seeds its starting point from the newest conversation and fetches no message history — older history is reached by scrolling online.

## Listen for sync progress

Register a `SyncListener` to observe the lifecycle of every automatic and manual sync. Each phase fires `started`, then exactly one of `completed` / `error`. Every callback is optional.

> [tab] TypeScript
>
> ```typescript theme={null}
> CometChat.addSyncListener("SYNC_LISTENER", {
>   onConversationSyncStarted: () => {},
>
>   onConversationSyncCompleted: (conversations: CometChat.Conversation[]) => {
>     // The full, fresh conversation page. Blind-replace your list.
>   },
>
>   onConversationSyncError: (error: CometChat.CometChatException) => {},
>
>   onMessageSyncStarted: () => {},
>
>   onMessageSyncCompleted: (messages: CometChat.BaseMessage[], outcome: CometChat.SyncOutcome) => {
>     // New + changed messages reconciled this sync. Merge by message id.
>   },
>
>   onMessageSyncError: (error: CometChat.CometChatException) => {},
> });
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> CometChat.addSyncListener("SYNC_LISTENER", {
>   onConversationSyncStarted: () => {},
>
>   onConversationSyncCompleted: (conversations) => {
>     // The full, fresh conversation page. Blind-replace your list.
>   },
>
>   onConversationSyncError: (error) => {},
>
>   onMessageSyncStarted: () => {},
>
>   onMessageSyncCompleted: (messages, outcome) => {
>     // New + changed messages reconciled this sync. Merge by message id.
>   },
>
>   onMessageSyncError: (error) => {},
> });
> ```

| Callback | When it fires |
| - | - |
| `onConversationSyncStarted()` | The conversation refresh began. |
| `onConversationSyncCompleted(conversations)` | The refresh succeeded; `conversations` is the full conversation page. |
| `onConversationSyncError(error)` | The refresh failed, or its response could not be read. Nothing is removed from the cache, and the refresh retries on the next reconnect. |
| `onMessageSyncStarted()` | The message changelog sweep began. Never fires on a cold start. |
| `onMessageSyncCompleted(messages, outcome)` | The sweep finished; `messages` carries the new and changed messages. |
| `onMessageSyncError(error)` | The sweep failed; it resumes on the next reconnect. |

The two phases are independent — a conversation-phase error does not stop the message phase. A phase error is informational: the cache is never left in a wrong state and the sweep resumes automatically.

The `SyncListener` is self-sufficient: `onMessageSyncCompleted` carries the swept messages, including `action` and `call` messages that the `MessageListener` does not deliver, so you can use it without a `MessageListener`.

`onMessageSyncCompleted` reports the outcome:

| `SyncOutcome` | Meaning |
| - | - |
| `CAUGHT_UP` | The changelog was fully replayed; the cache reflects every server change up to the sync point. |
| `PARTIAL` | A page was ingested and the sync point advanced, but a backlog remains. Each sync, automatic or from `forceSyncNow`, drains one page of up to 1000 changes. |

Remove the listener with `CometChat.removeSyncListener("SYNC_LISTENER")`.

## Receiving synced messages

If you already use a `CometChat.MessageListener`, synced messages still arrive there, so you usually do not need a `SyncListener` to keep your chat up to date:

* **Genuinely new** messages arrive one-by-one on `onTextMessageReceived` (and the other typed variants), with `getSource()` equal to `MessageSource.SYNC`.
* **Updates** to already-cached messages arrive together on `onMessagesSynced(messages)`. Upsert the batch by id; never append it.

> [tab] TypeScript
>
> ```typescript theme={null}
> CometChat.addMessageListener(
>   "MSG_LISTENER",
>   new CometChat.MessageListener({
>     onTextMessageReceived: (message: CometChat.TextMessage) => {
>       if (message.getSource() === CometChat.MessageSource.SYNC) {
>         // Delivered by a catch-up sync. Suppress notification spam if you wish.
>       }
>     },
>     onMessagesSynced: (messages: CometChat.BaseMessage[]) => {
>       // Updates to already-cached messages. Merge by id.
>     },
>   })
> );
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> CometChat.addMessageListener(
>   "MSG_LISTENER",
>   new CometChat.MessageListener({
>     onTextMessageReceived: (message) => {
>       if (message.getSource() === CometChat.MessageSource.SYNC) {
>         // Delivered by a catch-up sync. Suppress notification spam if you wish.
>       }
>     },
>     onMessagesSynced: (messages) => {
>       // Updates to already-cached messages. Merge by id.
>     },
>   })
> );
> ```

> [!NOTE]
>
> On reconnect, queued messages are sent first, then the sync runs. `forceSyncNow` runs only the sync; it does not send queued messages (use `retryMessage` or `retryAllFailedMessages` for those).

## Check sync state

Poll the current sync state and the time of the last completed sync at any time.

> [tab] TypeScript
>
> ```typescript theme={null}
> const state: CometChat.SyncState = CometChat.getSyncState();             // IDLE or SYNCING
> const lastSync: number = await CometChat.getLastSyncTimestamp();         // server epoch seconds; 0 if never synced
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> const state = CometChat.getSyncState();                // IDLE or SYNCING
> const lastSync = await CometChat.getLastSyncTimestamp(); // server epoch seconds; 0 if never synced
> ```

`getSyncState()` returns `SyncState.SYNCING` while a sweep is in flight, otherwise `SyncState.IDLE` (also when offline support is disabled). `getLastSyncTimestamp()` returns a **server-provided** epoch value in seconds, never the device clock, and `0` if the cache has never synced.

## Trigger a manual sync

Call `forceSyncNow` to run a catch-up immediately — for example behind a pull-to-refresh. It routes through the same throttle as the automatic sync, so it will not start a second overlapping sweep or hammer the backend; manual syncs must be at least 10 seconds apart.

The returned Promise resolves when the sweep is **accepted**, not when it completes. The sync's actual progress and outcome arrive on your registered `SyncListener`. You can instead pass an optional `{ onSuccess, onError }` callback object.

> [tab] TypeScript
>
> ```typescript theme={null}
> CometChat.forceSyncNow().then(
>   () => {
>     // Accepted and started.
>   },
>   (error: CometChat.CometChatException) => {
>     // Rejected — see the code.
>     console.log(error.code, error.details);
>   }
> );
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> CometChat.forceSyncNow().then(
>   () => {
>     // Accepted and started.
>   },
>   (error) => {
>     // Rejected — see the code.
>     console.log(error.code, error.details);
>   }
> );
> ```

| Rejection code | Meaning |
| - | - |
| `ERROR_OFFLINE_SYNC_IN_PROGRESS` | A sync (automatic or manual) is already running. |
| `ERROR_OFFLINE_SYNC_COOLDOWN` | A sync ran too recently; `error.details.retryAfterMs` carries the remaining cooldown in milliseconds. |
| `ERROR_OFFLINE_DISABLED` | A user is logged in but offline support is not active. |
| `USER_NOT_LOGED_IN` | Nobody is logged in. The spelling is the SDK's existing code. |

## When the cache resets

In rare cases the SDK cannot keep the existing cache — for example the encryption key became unrecoverable, or the database file is corrupt. When this happens it rebuilds the cache from scratch and notifies an `OfflineCacheResetListener`. Any un-sent messages that were queued are lost in the rebuild; `pendingMessagesLost` reports how many, or `CometChat.OFFLINE_PENDING_COUNT_UNKNOWN` (`-1`) when they could not be counted — that value means "unknown", not "none".

> [tab] TypeScript
>
> ```typescript theme={null}
> CometChat.addOfflineCacheResetListener("RESET_LISTENER", {
>   onOfflineCacheReset: (reason: CometChat.OfflineCacheResetReason, pendingMessagesLost: number) => {
>     // The cache was rebuilt. Re-fetch anything you need; it will re-sync from the server.
>   },
> });
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> CometChat.addOfflineCacheResetListener("RESET_LISTENER", {
>   onOfflineCacheReset: (reason, pendingMessagesLost) => {
>     // The cache was rebuilt. Re-fetch anything you need; it will re-sync from the server.
>   },
> });
> ```

| `OfflineCacheResetReason` | Cause |
| - | - |
| `KEY_LOST` | The device key could not be recovered (for example cleared app data or a restored backup); a fresh key was minted. |
| `WRONG_KEY` | The database exists but does not decrypt under the current key. |
| `CORRUPTED` | The database file is corrupt on disk. |
| `ESCALATED` | An unclassified failure recurred across launches and was escalated to a permanent rebuild. |
| `ENCRYPTION_CHANGED` | The `encryption` setting changed, so the on-disk cache was rebuilt in the configured format. |

On a relaunch with a saved login, the cache can be reset during startup. The SDK delivers that reset just after `await CometChat.init(...)` resolves. Add the cache-reset listener, and the [outbox listener](https://www.cometchat.com/docs/sdk/react-native/offline-send.md#observe-the-queue-app-wide), straight after `init` resolves, before you await anything else, or you will miss it.

> [tab] TypeScript
>
> ```typescript theme={null}
> await CometChat.init("APP_ID", appSettings);
>
> CometChat.addOfflineCacheResetListener("RESET_LISTENER", {
>   onOfflineCacheReset: (reason, pendingMessagesLost) => {},
> });
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> await CometChat.init("APP_ID", appSettings);
>
> CometChat.addOfflineCacheResetListener("RESET_LISTENER", {
>   onOfflineCacheReset: (reason, pendingMessagesLost) => {},
> });
> ```

Remove the listener with `CometChat.removeOfflineCacheResetListener("RESET_LISTENER")`. See [Encrypt the Cache](https://www.cometchat.com/docs/sdk/react-native/offline-encryption.md) for the encryption-related reasons.

## Manage the cache

Read the current cache size, or clear the cache entirely.

> [tab] TypeScript
>
> ```typescript theme={null}
> const bytes: number = await CometChat.getCacheSize(); // DB + media byte store, in bytes
>
> await CometChat.clearOfflineCache();                  // empty the per-user cache in place
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> const bytes = await CometChat.getCacheSize(); // DB + media byte store, in bytes
>
> await CometChat.clearOfflineCache();          // empty the per-user cache in place
> ```

`getCacheSize()` returns the total cache size in bytes (local database plus the media byte store) as a single figure, or `0` when offline support is disabled.

> [!WARNING]
>
> `clearOfflineCache()` is destructive and irreversible. It empties the entire per-user cache — cached messages, conversations, media bytes **and the un-sent outbox** — while the store stays open. Each still-queued `PENDING` / `SENDING` message fails with reason `DISCARDED`; a message already on the wire waits for its answer and, if accepted, is reported sent. Messages already `FAILED` are removed silently. It is a no-op unless offline support is active.

> [!NOTE]
>
> You never need to clear the cache on logout — the SDK does it for you. The cache is stored **per user**: it survives app restarts and a token refresh for the same logged-in user, and is wiped automatically on logout, when the server rejects the auth token, or when a different user or app ID is used. See [Offline Support](https://www.cometchat.com/docs/sdk/react-native/offline-overview.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.