From fcbc0e0d109123ada06639038f746b1707d48353 Mon Sep 17 00:00:00 2001 From: rajdubey Date: Fri, 30 Oct 2026 11:19:38 +0530 Subject: [PATCH 01/12] docs: Offline Support for the JavaScript SDK New Offline Support group (8 pages) under JavaScript (after Messaging): enable, cached-then-server reads, durable send outbox, reconnect sync, media caching, cache encryption, and config/API reference. Documented as GA, grounded in the JavaScript SDK's real public offline API and cross-checked against Android release-v5-v5.1.0 and the conformance spec. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XbguTw81eW7rnAGv6AksM8 --- docs.json | 13 + sdk/javascript/offline-cached-data.mdx | 303 +++++++++++++++++ sdk/javascript/offline-enable.mdx | 159 +++++++++ sdk/javascript/offline-encryption.mdx | 191 +++++++++++ sdk/javascript/offline-media-caching.mdx | 225 +++++++++++++ sdk/javascript/offline-overview.mdx | 90 +++++ sdk/javascript/offline-reference.mdx | 178 ++++++++++ sdk/javascript/offline-send.mdx | 401 +++++++++++++++++++++++ sdk/javascript/offline-sync.mdx | 295 +++++++++++++++++ 9 files changed, 1855 insertions(+) create mode 100644 sdk/javascript/offline-cached-data.mdx create mode 100644 sdk/javascript/offline-enable.mdx create mode 100644 sdk/javascript/offline-encryption.mdx create mode 100644 sdk/javascript/offline-media-caching.mdx create mode 100644 sdk/javascript/offline-overview.mdx create mode 100644 sdk/javascript/offline-reference.mdx create mode 100644 sdk/javascript/offline-send.mdx create mode 100644 sdk/javascript/offline-sync.mdx diff --git a/docs.json b/docs.json index 96952f214..87fb328cc 100644 --- a/docs.json +++ b/docs.json @@ -3163,6 +3163,19 @@ "sdk/javascript/reactions" ] }, + { + "group": "Offline Support", + "pages": [ + "sdk/javascript/offline-overview", + "sdk/javascript/offline-enable", + "sdk/javascript/offline-cached-data", + "sdk/javascript/offline-send", + "sdk/javascript/offline-sync", + "sdk/javascript/offline-media-caching", + "sdk/javascript/offline-encryption", + "sdk/javascript/offline-reference" + ] + }, "sdk/javascript/calling-overview", { "group": "Users", diff --git a/sdk/javascript/offline-cached-data.mdx b/sdk/javascript/offline-cached-data.mdx new file mode 100644 index 000000000..8f5524212 --- /dev/null +++ b/sdk/javascript/offline-cached-data.mdx @@ -0,0 +1,303 @@ +--- +title: "Show Cached Data" +sidebarTitle: "Show Cached Data" +description: "Render cached messages, conversations, and lists instantly and reconcile them against the server with the CometChat JavaScript SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Pass a fetch listener to get up to two phases: cached first (instant), then server. +const request = new CometChat.MessagesRequestBuilder().setUID("UID").setLimit(30).build(); + +request + .fetchPrevious( + new CometChat.MessageFetchListener({ + onCachedMessages: (messages) => { /* Phase 1: local cache. Render immediately. */ }, + onServerMessages: (messages) => { /* Phase 2: server. Reconcile, don't append. Never fires offline. */ }, + onError: (error) => { /* Offline ends here, after the cached page, with FAILED_TO_FETCH. */ }, + }) + ) + .catch(() => {}); // the returned Promise also rejects offline — handle it + +// Single items (additive two-phase overloads) +CometChat.getConversation("UID", "user", new CometChat.ConversationDetailsListener({ /* … */ })); +CometChat.getMessageDetails(messageId, new CometChat.SingleMessageFetchListener({ /* … */ })); +``` + + + +With offline support enabled, every list and lookup the SDK already offers can answer from the on-device cache first and then from the server. Your UI paints instantly from the cache, then reconciles when the server replies. This page describes that contract, the reads it applies to, and how to merge the two phases. + +## The two-phase contract + +Pass a fetch listener as the optional argument and each read fires **up to two** callbacks, always in this order: + + + +The SDK delivers a page from the local cache **first**. It fires **even when the page is empty** — an empty cached page is indistinguishable from a cache miss, by design. Render it right away. + + +After a successful server response, the SDK delivers the server's copy for the **same slice** and writes it through to the cache before the callback. Reconcile your displayed list against it. + + + +Three rules follow from this: + +- **Offline, the read ends with an error.** After the cached page, a read whose request gets no answer settles with `onError` carrying code `FAILED_TO_FETCH`. The call always settles — it never hangs. +- **The server phase never fires offline.** It fires only on a genuine server response. Never treat a cached page as "end of history"; an empty cached page only means the end of the *cached* window. +- **The Promise still settles too.** Passing a listener does not replace the returned Promise: it settles on the server outcome, so it also **rejects** offline. If you use only the listener, add a `.catch()` to avoid an unhandled rejection. Consume the result from one channel, not both. + + +With offline support **off**, these listeners behave like the classic Promise: the cached phase never fires, and the server result arrives in `onServer…`. Nothing changes until you enable offline (see [Enable Offline Support](/sdk/javascript/offline-enable)). Requires SDK **4.3.0** or later. + + +## Read messages + +`MessagesRequest` supports both directions. `fetchPrevious` pages toward older messages; `fetchNext` pages toward newer ones. Both take a `MessageFetchListener`. + + + +```typescript +const request: CometChat.MessagesRequest = new CometChat.MessagesRequestBuilder() + .setUID("UID") + .setLimit(30) + .build(); + +request + .fetchPrevious( + new CometChat.MessageFetchListener({ + onCachedMessages: (messages: CometChat.BaseMessage[]) => { + // Render the cached page immediately. + }, + onServerMessages: (messages: CometChat.BaseMessage[]) => { + // Reconcile this slice against the cached page, keyed by message id. + }, + onError: (error: CometChat.CometChatException) => { + console.log("Fetch failed:", error.code); + }, + }) + ) + .catch(() => {}); +``` + + +```javascript +const request = new CometChat.MessagesRequestBuilder() + .setUID("UID") + .setLimit(30) + .build(); + +request + .fetchPrevious( + new CometChat.MessageFetchListener({ + onCachedMessages: (messages) => { + // Render the cached page immediately. + }, + onServerMessages: (messages) => { + // Reconcile this slice against the cached page, keyed by message id. + }, + onError: (error) => { + console.log("Fetch failed:", error.code); + }, + }) + ) + .catch(() => {}); +``` + + + +## Read conversations + +`ConversationsRequest` pages with `fetchNext`, taking a `ConversationFetchListener`. + + + +```typescript +const request: CometChat.ConversationsRequest = new CometChat.ConversationsRequestBuilder() + .setLimit(30) + .build(); + +request + .fetchNext( + new CometChat.ConversationFetchListener({ + onCachedConversations: (conversations: CometChat.Conversation[]) => { + // Render the cached list immediately. + }, + onServerConversations: (conversations: CometChat.Conversation[]) => { + // Reconcile, keyed by conversation id. + }, + onError: (error: CometChat.CometChatException) => { + console.log("Fetch failed:", error.code); + }, + }) + ) + .catch(() => {}); +``` + + +```javascript +const request = new CometChat.ConversationsRequestBuilder() + .setLimit(30) + .build(); + +request + .fetchNext( + new CometChat.ConversationFetchListener({ + onCachedConversations: (conversations) => { + // Render the cached list immediately. + }, + onServerConversations: (conversations) => { + // Reconcile, keyed by conversation id. + }, + onError: (error) => { + console.log("Fetch failed:", error.code); + }, + }) + ) + .catch(() => {}); +``` + + + +## Read the other lists + +Every remaining list read accepts the same two-phase listener. Each listener is constructed from an object with cached/server callbacks of the matching type plus `onError`. + +| Request | Fetch method(s) | Listener | Cached / server callback | +|---|---|---|---| +| `MessagesRequest` | `fetchPrevious`, `fetchNext` | `MessageFetchListener` | `onCachedMessages` / `onServerMessages` | +| `ConversationsRequest` | `fetchNext` | `ConversationFetchListener` | `onCachedConversations` / `onServerConversations` | +| `ThreadsRequest` | `fetchNext` | `ThreadFetchListener` | `onCachedThreads` / `onServerThreads` | +| `UsersRequest` | `fetchNext` | `UserFetchListener` | `onCachedUsers` / `onServerUsers` | +| `BlockedUsersRequest` | `fetchNext` | `UserFetchListener` | `onCachedUsers` / `onServerUsers` | +| `GroupsRequest` | `fetchNext` | `GroupFetchListener` | `onCachedGroups` / `onServerGroups` | +| `GroupMembersRequest` | `fetchNext` | `GroupMemberFetchListener` | `onCachedGroupMembers` / `onServerGroupMembers` | +| `BannedMembersRequest` | `fetchNext` | `BannedMemberFetchListener` | `onCachedBannedGroupMembers` / `onServerBannedGroupMembers` | +| `ReactionsRequest` | `fetchPrevious`, `fetchNext` | `ReactionFetchListener` | `onCachedReactions` / `onServerReactions` | +| `NotificationFeedRequest` | `fetchNext` | `NotificationFeedFetchListener` | `onCachedNotificationFeedItems` / `onServerNotificationFeedItems` | + +The call shape is identical to the conversations example above — swap the request, the listener, and the callback names. + + + +```typescript +const request: CometChat.UsersRequest = new CometChat.UsersRequestBuilder() + .setLimit(30) + .build(); + +request + .fetchNext( + new CometChat.UserFetchListener({ + onCachedUsers: (users: CometChat.User[]) => { /* render */ }, + onServerUsers: (users: CometChat.User[]) => { /* reconcile, keyed by uid */ }, + onError: (error: CometChat.CometChatException) => console.log(error.code), + }) + ) + .catch(() => {}); +``` + + +```javascript +const request = new CometChat.UsersRequestBuilder() + .setLimit(30) + .build(); + +request + .fetchNext( + new CometChat.UserFetchListener({ + onCachedUsers: (users) => { /* render */ }, + onServerUsers: (users) => { /* reconcile, keyed by uid */ }, + onError: (error) => console.log(error.code), + }) + ) + .catch(() => {}); +``` + + + + +For `ReactionsRequest`, `fetchPrevious` and `fetchNext` are cached independently — each direction keeps its own cached pages. Users, groups, members, banned members, reactions, and the notification feed are served from a recorded copy of each request (up to `maxSnapshotQueries` distinct requests, each kept for `snapshotTtlDays`), so a list you never opened online is empty offline. See the [Configuration & API Reference](/sdk/javascript/offline-reference). + + +## Read a single item + +`getConversation` and `getMessageDetails` each accept a two-phase listener as a final argument. Unlike the list reads, the cached callback fires **only when the item is cached** — never with an empty or null object. A cache miss goes straight to the server phase online, or straight to `onError` offline. + + + +```typescript +CometChat.getConversation( + "UID", + CometChat.RECEIVER_TYPE.USER, + new CometChat.ConversationDetailsListener({ + onCachedConversation: (conversation: CometChat.Conversation) => { + // Fires only on a cache hit. + }, + onServerConversation: (conversation: CometChat.Conversation) => { + // Server copy, written through before delivery. + }, + onError: (error: CometChat.CometChatException) => console.log("Lookup failed:", error.code), + }) +).catch(() => {}); + +CometChat.getMessageDetails( + messageId, + new CometChat.SingleMessageFetchListener({ + onCachedMessage: (message: CometChat.BaseMessage) => { /* cache hit only */ }, + onServerMessage: (message: CometChat.BaseMessage) => { /* server copy */ }, + onError: (error: CometChat.CometChatException) => console.log(error.code), + }) +).catch(() => {}); +``` + + +```javascript +CometChat.getConversation( + "UID", + CometChat.RECEIVER_TYPE.USER, + new CometChat.ConversationDetailsListener({ + onCachedConversation: (conversation) => { + // Fires only on a cache hit. + }, + onServerConversation: (conversation) => { + // Server copy, written through before delivery. + }, + onError: (error) => console.log("Lookup failed:", error.code), + }) +).catch(() => {}); + +CometChat.getMessageDetails( + messageId, + new CometChat.SingleMessageFetchListener({ + onCachedMessage: (message) => { /* cache hit only */ }, + onServerMessage: (message) => { /* server copy */ }, + onError: (error) => console.log(error.code), + }) +).catch(() => {}); +``` + + + + +`CometChat.SingleConversationFetchListener` is a deprecated alias of `ConversationDetailsListener`. + + +## Merge the two phases + +The server phase **replaces the same slice** — it is a reconcile, not a second batch. Render the cached page straight away, then on the server phase diff it into your displayed list keyed by the item's stable id (message id, conversation id, uid, guid). Do not append the server page to the cached one. + + +Key your list by id so the two phases merge cleanly: update items that changed, add new ones, and drop cached items the server slice omits. To page further back, issue the next `fetchPrevious()` / `fetchNext()` rather than treating the server phase as more pages. + + +## When the cached page is empty + +The cache answers a read **only if every filter on it can be computed locally**. A single server-only filter makes the whole cached phase come back empty (or not fire at all), with no error and no warning, while the server phase still answers in full. The guiding rule is *better empty than wrong*. + +The most common server-only filter is `setSearchKeyword`, which blanks the cached phase on both messages and conversations. On `MessagesRequest`, `hasLinks`, `updatesOnly` and `interactionGoalCompletedOnly` are server-only too. + + +An offline-critical screen must use only cache-supported filters — one innocuous builder flag can silently disable the cached phase for a whole screen. The full per-builder matrix is in the [Configuration & API Reference](/sdk/javascript/offline-reference#cache-supported-filters). + diff --git a/sdk/javascript/offline-enable.mdx b/sdk/javascript/offline-enable.mdx new file mode 100644 index 000000000..32857cbf0 --- /dev/null +++ b/sdk/javascript/offline-enable.mdx @@ -0,0 +1,159 @@ +--- +title: "Enable Offline Support" +sidebarTitle: "Enable Offline Support" +description: "Turn on local caching and the durable send outbox, and verify offline support is active with the CometChat JavaScript SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// 1. Attach an offline config to AppSettings (off by default — enabled must be true). +const appSetting = new CometChat.AppSettingsBuilder() + .setRegion("REGION") + .setOfflineConfig({ enabled: true }) // explicit config; its `enabled` wins + // .offline(true) // shorthand: enable with all defaults + .build(); + +// 2. Pass the settings to init. +await CometChat.init("APP_ID", appSetting); + +// 3. Verify it is active for the logged-in user. +const active = CometChat.isOfflineEnabled(); // boolean +``` + + + +Offline support is **off by default**. You turn it on once, at initialization, by passing an offline config to the `AppSettingsBuilder`. Until you do, the SDK behaves exactly as before. This page covers the minimal setup; for every tunable option see the [Configuration & API Reference](/sdk/javascript/offline-reference). + + + + +Offline support requires CometChat JavaScript SDK **4.3.0** or later. + +```bash +npm install @cometchat/chat-sdk-javascript@4.3.0 +``` + +If you are new to the SDK, complete the [standard setup](/sdk/javascript/setup-sdk) first; the steps below only add offline configuration to your existing `init`. + + + +Call `setOfflineConfig(...)` on the `AppSettingsBuilder` with an object whose `enabled` is `true`. Every other key has a sensible default, so set only what you need. To enable with all defaults, use the `offline(true)` shorthand instead. + + + +```typescript +const appSetting: CometChat.AppSettings = new CometChat.AppSettingsBuilder() + .subscribePresenceForAllUsers() + .setRegion("REGION") + .autoEstablishSocketConnection(true) + .setOfflineConfig({ enabled: true }) // explicit config + // .offline(true) // shorthand: enable with defaults + .build(); +``` + + +```javascript +const appSetting = new CometChat.AppSettingsBuilder() + .subscribePresenceForAllUsers() + .setRegion("REGION") + .autoEstablishSocketConnection(true) + .setOfflineConfig({ enabled: true }) // explicit config + // .offline(true) // shorthand: enable with defaults + .build(); +``` + + + + +Offline turns on only for an explicit `enabled: true`. A config object without `enabled` leaves it off. If you call both `setOfflineConfig(...)` and `offline(...)`, the explicit config wins. + + + + +Pass the settings to `CometChat.init(...)` as you do today. No other call changes. + + + +```typescript +CometChat.init("APP_ID", appSetting).then( + (initialized: boolean) => { + console.log("Initialization completed successfully", initialized); + }, + (error: CometChat.CometChatException) => { + console.log("Initialization failed with error:", error); + } +); +``` + + +```javascript +CometChat.init("APP_ID", appSetting).then( + () => { + console.log("Initialization completed successfully"); + }, + (error) => { + console.log("Initialization failed with error:", error); + } +); +``` + + + + + +Call `CometChat.isOfflineEnabled()`. It returns `true` only while offline support is actually running for the logged-in user — so it is `false` before anyone logs in, after logout, while the server has switched offline off for your app, and while the local store is still opening, even when you enabled it in `AppSettings`. Read it at the moment of each decision and keep no copy of the result. + + + +```typescript +const active: boolean = CometChat.isOfflineEnabled(); +console.log("Offline support active:", active); +``` + + +```javascript +const active = CometChat.isOfflineEnabled(); +console.log("Offline support active:", active); +``` + + + + + + +## Enable from a settings object + +If you initialize with `CometChat.initFromSettings(settings)`, add an `offline` key under `chatSDK` in the settings object you pass. It accepts a boolean shorthand or a full configuration object, with the same rules as the code config. + +```json settings +{ + "appId": "APP_ID", + "region": "REGION", + "chatSDK": { + "offline": true + } +} +``` + +## Supported runtimes + +| Runtime | Data store | Media bytes | +|---|---|---| +| Browser | IndexedDB | OPFS. Where OPFS is missing, media is not cached. | +| Electron | SQLite in the main process | Files on disk | + +Where IndexedDB is missing or blocked (for example some private-browsing modes), offline stays off for the session and the SDK works online as before. + + +**Electron setup.** Install the optional peer dependencies `electron` (20 or later) and `better-sqlite3-multiple-ciphers` (9 or later), call `registerOfflineIpc()` from `@cometchat/chat-sdk-javascript/electron/main` in the main process, and `installCometChatBridge()` from `@cometchat/chat-sdk-javascript/electron/preload` in your preload script (with `contextIsolation: true`). The main process and the renderer must use the same package version. Browser apps need nothing extra. + + +## Server switch + +CometChat can switch offline support off for your app from the server. The server can turn it off, never on: offline runs only when your config enables it **and** the server switch is on. When the switch is off, `isOfflineEnabled()` is `false`. + +## Next steps + +Offline support is now active for the logged-in user. Continue with [Show Cached Data](/sdk/javascript/offline-cached-data) to adopt the fetch listeners, and [Send While Offline](/sdk/javascript/offline-send) for the durable outbox. Tune caps, TTLs, media, and encryption from the [Configuration & API Reference](/sdk/javascript/offline-reference). diff --git a/sdk/javascript/offline-encryption.mdx b/sdk/javascript/offline-encryption.mdx new file mode 100644 index 000000000..1d045b73b --- /dev/null +++ b/sdk/javascript/offline-encryption.mdx @@ -0,0 +1,191 @@ +--- +title: "Encrypt the Cache" +sidebarTitle: "Encrypt the Cache" +description: "Encrypt the offline message cache at rest, choose what happens when encryption cannot run, and inspect the live encryption status with the CometChat JavaScript SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// 1. Turn encryption on in the offline config (off by default). No extra dependency in a browser. +const appSetting = new CometChat.AppSettingsBuilder() + .setRegion("REGION") + .setOfflineConfig({ + enabled: true, + encryption: true, // encrypt the cache at rest + encryptionFailureBehavior: CometChat.EncryptionFailureBehavior.ONLINE_ONLY, // default; never writes plaintext + }) + .build(); + +// 2. Inspect what encryption is actually doing +const status = CometChat.getOfflineEncryptionStatus(); +status.enabled; // what was configured +status.atRest; // "FIELD_AES_GCM" (browser), "SQLCIPHER" (Electron) or "NONE" +status.keyStorage; // "SOFTWARE" (browser), "KEYSTORE" (Electron) or "NONE" +status.cipherAvailable; // can this runtime encrypt? +status.cipher; // cipher description once proven, else null +status.leakage; // what is never covered +``` + + + +Offline support keeps the logged-in user's messages in a local store on the device. Turn encryption on to store that content **encrypted at rest**. Encryption is **opt-in** and off by default; nothing changes until you set `encryption: true`. + + +This is not end-to-end encryption — the server still sees plaintext. It protects the on-device cache only. Cached **media files** are never encrypted, and in a browser some metadata stays readable so the cache can be queried. See [What is and isn't encrypted](#what-is-and-isn-t-encrypted). Requires SDK **4.3.0** or later. + + +## Requirements + +| Runtime | Cipher | What you need | +|---|---|---| +| Browser | AES-256-GCM over stored message and list content | A **secure context** (HTTPS or `localhost`) so `crypto.subtle` exists. Nothing to install. | +| Electron | SQLCipher for the whole database | `better-sqlite3-multiple-ciphers` (9 or later) installed in the main process, and an OS keyring where `safeStorage` needs one (for example Linux). | + + +Without a working cipher the cache **cannot run encrypted**. What happens then is decided by [`encryptionFailureBehavior`](#choose-what-happens-when-encryption-cannot-run) — by default the session runs server-only (no local cache). The SDK never creates a cache that is labelled encrypted but is not. + + +## Turn on encryption + +Set `encryption: true` in the offline config you pass to `setOfflineConfig` (see [Enable Offline Support](/sdk/javascript/offline-enable)). + + + +```typescript +const appSetting: CometChat.AppSettings = new CometChat.AppSettingsBuilder() + .setRegion("REGION") + .setOfflineConfig({ + enabled: true, + encryption: true, + encryptionFailureBehavior: CometChat.EncryptionFailureBehavior.ONLINE_ONLY, + }) + .build(); +``` + + +```javascript +const appSetting = new CometChat.AppSettingsBuilder() + .setRegion("REGION") + .setOfflineConfig({ + enabled: true, + encryption: true, + encryptionFailureBehavior: CometChat.EncryptionFailureBehavior.ONLINE_ONLY, + }) + .build(); +``` + + + +Switching `encryption` on or off between launches rebuilds the cache in the new format and reports `ENCRYPTION_CHANGED` to the [cache-reset listener](/sdk/javascript/offline-sync#when-the-cache-resets); unsent messages are lost in that rebuild. + +## Choose what happens when encryption cannot run + +`encryptionFailureBehavior` decides what the cache does when encryption **cannot be applied at all** on this runtime — for example an insecure origin in a browser (no `crypto.subtle`), or a plain `better-sqlite3` instead of the cipher build in Electron. It governs only that case: a transient failure still retries, and a lost or wrong key self-heals by wiping and rebuilding the cache **encrypted**. + +| Value | Behavior | Writes plaintext? | +|---|---|---| +| `ONLINE_ONLY` *(default)* | Do not open a local cache at all — the session runs server-only. Makes a setup mistake visible: offline simply does not work until it is fixed. | No | +| `PLAINTEXT_FALLBACK` | Open an **unencrypted** cache so offline keeps working, and log the downgrade at error level. `getOfflineEncryptionStatus()` reports `atRest: "NONE"`. | Yes | + + +`PLAINTEXT_FALLBACK` means cached message content may be stored **unencrypted** whenever encryption cannot run. This availability-over-guarantee trade is wrong for compliance use — keep the default `ONLINE_ONLY` if cached content must never be written in the clear. Any value other than `PLAINTEXT_FALLBACK` is treated as `ONLINE_ONLY`. + + +## Key storage + +You do not supply or manage the encryption key. In a browser, the SDK generates a per-user data key, wraps it with a **non-extractable** WebCrypto key, and keeps the wrapped key in the same IndexedDB as the data — so a browser clearing site data can never leave ciphertext without its key. In Electron the key is sealed by the OS through `safeStorage`. The key is never returned by any API. If it is lost or no longer opens the cache, the cache resets and rebuilds — see [Cache resets](#cache-resets-on-key-loss). + + +In a browser the key lives in software next to the data (`keyStorage: "SOFTWARE"`). That protects against casual disk inspection and partial data copies, but not against code running in your page's origin. + + +## Check encryption status + +`CometChat.getOfflineEncryptionStatus()` returns an object describing what encryption is **actually** doing — never what was merely requested. It is synchronous, safe to call at any time (even before login or with offline disabled), and never returns the key or any part of it. + + + +```typescript +const status = CometChat.getOfflineEncryptionStatus(); +console.log( + "enabled=" + status.enabled, + "atRest=" + status.atRest, + "keyStorage=" + status.keyStorage, + "cipherAvailable=" + status.cipherAvailable, + "cipher=" + status.cipher +); +``` + + +```javascript +const status = CometChat.getOfflineEncryptionStatus(); +console.log( + "enabled=" + status.enabled, + "atRest=" + status.atRest, + "keyStorage=" + status.keyStorage, + "cipherAvailable=" + status.cipherAvailable, + "cipher=" + status.cipher +); +``` + + + +| Field | Type | Meaning | +|---|---|---| +| `enabled` | `boolean` | What was **configured** (`encryption`), not what was achieved. | +| `atRest` | `string` | Coverage achieved: `"FIELD_AES_GCM"` (browser), `"SQLCIPHER"` (Electron), otherwise `"NONE"`. | +| `keyStorage` | `string` | `"SOFTWARE"` (browser), `"KEYSTORE"` (Electron, OS-sealed), or `"NONE"` when this session holds no key. Read `NONE` as "no key in use", not "no key exists". | +| `cipherAvailable` | `boolean` | Whether the cipher engine is present in this runtime. | +| `cipher` | `string \| null` | A description of the cipher once proven, else `null`. | +| `leakage` | `string` | What encryption at rest never covers. | + + +The most useful support signal is `enabled === true` with `cipherAvailable === false`: encryption was switched on but could not run. In a browser that almost always means an insecure origin; in Electron, a missing `better-sqlite3-multiple-ciphers` cipher build. Check `atRest` to see whether a cache was opened at all. + + +## What is and isn't encrypted + +| Data | Encrypted at rest? | +|---|---| +| Message and list content, conversations, the pending-send outbox | Yes | +| Browser only: message ids, timestamps, send state, message type and category, parent and quoted message ids, source, unread counts, conversation type, sync watermarks, row counts, pending-upload file sizes, MIME types and states | **No** — kept readable so the cache can be queried. `leakage` lists them exactly. | +| Cached media files (images, video, audio, documents) | **No** — stored as ordinary files outside the database | + +On Electron the whole database is encrypted with SQLCipher; only the media files stay outside it. + +## Cache resets on key loss + +If the key can no longer decrypt the stored cache, the SDK wipes the local cache and rebuilds it **encrypted** — it never silently falls back to plaintext for a cache that could be encrypted. Any unsent messages still queued in the outbox at that point are lost and each is failed with `DISCARDED`. + +Register an `OfflineCacheResetListener` to be told when this happens: + + + +```typescript +CometChat.addOfflineCacheResetListener( + "LISTENER_ID", + new CometChat.OfflineCacheResetListener({ + onOfflineCacheReset: (reason: string, pendingMessagesLost: number) => { + console.warn("Cache reset:", reason, "pending lost =", pendingMessagesLost); + }, + }) +); +``` + + +```javascript +CometChat.addOfflineCacheResetListener( + "LISTENER_ID", + new CometChat.OfflineCacheResetListener({ + onOfflineCacheReset: (reason, pendingMessagesLost) => { + console.warn("Cache reset:", reason, "pending lost =", pendingMessagesLost); + }, + }) +); +``` + + + +The encryption-related reasons are `KEY_LOST`, `WRONG_KEY`, `CORRUPTED`, and `ENCRYPTION_CHANGED` (you turned encryption on or off between launches). See [Stay in Sync](/sdk/javascript/offline-sync#when-the-cache-resets) for the full cache-reset flow, and the [API Reference](/sdk/javascript/offline-reference) for every field and enum. diff --git a/sdk/javascript/offline-media-caching.mdx b/sdk/javascript/offline-media-caching.mdx new file mode 100644 index 000000000..de367ac02 --- /dev/null +++ b/sdk/javascript/offline-media-caching.mdx @@ -0,0 +1,225 @@ +--- +title: "Media Caching" +sidebarTitle: "Media Caching" +description: "Cache image, video, audio and file attachments on the device so media stays viewable offline with the CometChat JavaScript SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Set the media caching mode at init (fixed for the session) +const appSetting = new CometChat.AppSettingsBuilder() + .setRegion("REGION") + .setOfflineConfig({ + enabled: true, + mediaCaching: CometChat.MediaCachingMode.AUTO, // AUTO (default) or OFF + maxMediaCacheSize: 500 * 1024 * 1024, // bytes; 0 = unlimited + }) + .build(); + +// Read the mode in effect +const mode = CometChat.getMediaCachingMode(); // "OFF" when offline is not active + +// Resolve cached bytes for an attachment +const attachment = mediaMessage.getAttachment(); +const url = await CometChat.getCachedMediaObjectURL(attachment); // blob: URL, or null if not cached +// ...use as / + +With offline support on, the SDK can cache attachment bytes durably on the device so media stays viewable offline — even after the server's signed URL has expired. This page covers the caching modes, how to read cached bytes, and how to download media on demand. + + +Media caching applies only while offline support is active for the logged-in user (`CometChat.isOfflineEnabled()` is `true`). See [Enable Offline Support](/sdk/javascript/offline-enable). In a browser, media bytes are kept in the Origin Private File System (OPFS); **where OPFS is not available, media is not cached** (messages and attachment metadata still are). + + +## Caching modes + +The caching mode is set once, in the offline config you pass at initialization, and is fixed for the SDK session. There is no runtime switch — to change it, re-initialize the SDK with a new config. + +| `MediaCachingMode` | Behavior | +|---|---| +| `AUTO` (default) | Incoming media bytes are downloaded and cached on receipt, then LRU-managed, so they are available offline. | +| `OFF` | Incoming media bytes are **not** downloaded or cached. The message and its attachment metadata (including the URL) are still cached, so the message appears offline and the UI loads media live from the URL when online. | + +Sending media always works in both modes. + + + +```typescript +const appSetting: CometChat.AppSettings = new CometChat.AppSettingsBuilder() + .setRegion("REGION") + .setOfflineConfig({ + enabled: true, + mediaCaching: CometChat.MediaCachingMode.AUTO, + maxMediaCacheSize: 500 * 1024 * 1024, + }) + .build(); +``` + + +```javascript +const appSetting = new CometChat.AppSettingsBuilder() + .setRegion("REGION") + .setOfflineConfig({ + enabled: true, + mediaCaching: CometChat.MediaCachingMode.AUTO, + maxMediaCacheSize: 500 * 1024 * 1024, + }) + .build(); +``` + + + +Read the mode in effect at runtime with `CometChat.getMediaCachingMode()`. It returns `"OFF"` whenever offline support is not active, whatever the app configured, so a UI layer can rely on it without reading the config itself. + +## Reading cached media + +When bytes are already cached, the SDK **sets the attachment's local path automatically** on the messages it delivers. Read it with `attachment.getLocalPath()`. In a browser that value is an OPFS storage handle (`opfs:…`), **not** something you can put in an `src` — to render a cached file, ask for an object URL instead. + +`getCachedMediaObjectURL` resolves a `blob:` URL for a cached attachment, or `null` when its bytes are not cached. **You must revoke the URL** with `URL.revokeObjectURL` when you are done with it. + + + +```typescript +const attachment: CometChat.Attachment = mediaMessage.getAttachment(); + +const objectUrl: string | null = await CometChat.getCachedMediaObjectURL(attachment); +if (objectUrl) { + imageElement.src = objectUrl; // render from disk — no network fetch + imageElement.onload = () => URL.revokeObjectURL(objectUrl); +} else { + imageElement.src = attachment.getUrl(); // not cached: load live when online +} +``` + + +```javascript +const attachment = mediaMessage.getAttachment(); + +const objectUrl = await CometChat.getCachedMediaObjectURL(attachment); +if (objectUrl) { + imageElement.src = objectUrl; // render from disk — no network fetch + imageElement.onload = () => URL.revokeObjectURL(objectUrl); +} else { + imageElement.src = attachment.getUrl(); // not cached: load live when online +} +``` + + + +To look up the cached handle without creating a URL, call `getCachedMediaPath`. It resolves `null` when the bytes are not cached (call `downloadMedia` to fetch them). + +```javascript +const handle = await CometChat.getCachedMediaPath(attachment); // null if not cached +``` + + +Each attachment of a multi-attachment message resolves independently. An attachment with no cached bytes has no local path, and the UI loads it from the URL as before. + + +## Downloading on demand + +Call `downloadMedia` to fetch and cache an attachment's bytes — for example when the user taps a placeholder. The SDK resolves cached bytes first, then the URL, and re-signs an expired link (HTTP 401, 403 or 410) through the message the attachment was delivered with before retrying once. `downloadMedia` returns nothing; results arrive on the `MediaCallbackListener`. + + + +```typescript +CometChat.downloadMedia( + attachment, + new CometChat.MediaCallbackListener({ + onSuccess: (localPath: string) => { + // Bytes cached; render via getCachedMediaObjectURL(attachment). + }, + onProgress: (percent: number) => { + // 0..100, best-effort. + }, + onError: (error: CometChat.CometChatException) => { + console.log(error.code); + }, + }) +); +``` + + +```javascript +CometChat.downloadMedia( + attachment, + new CometChat.MediaCallbackListener({ + onSuccess: (localPath) => { + // Bytes cached; render via getCachedMediaObjectURL(attachment). + }, + onProgress: (percent) => { + // 0..100, best-effort. + }, + onError: (error) => { + console.log(error.code); + }, + }) +); +``` + + + +`downloadMedia` calls `onError` with `ERROR_OFFLINE_DISABLED` when offline support is not running, and with `ERROR_OFFLINE_MEDIA_NO_URL` when no attachment, or an attachment without a URL, is given. + +## Observe background downloads + +In `AUTO` mode, incoming media downloads in the background with no per-call callback. Register a `MediaDownloadListener` to learn when the bytes land (or fail) so a UI tile can refresh. + + + +```typescript +CometChat.addMediaDownloadListener( + "MEDIA_LISTENER", + new CometChat.MediaDownloadListener({ + onMediaDownloaded: (messageId: string, attachmentId: string, localPath: string) => { + // attachmentId: the attachment URL without its query string + }, + onMediaDownloadFailed: (messageId: string, attachmentId: string, error: CometChat.CometChatException) => {}, + }) +); +``` + + +```javascript +CometChat.addMediaDownloadListener( + "MEDIA_LISTENER", + new CometChat.MediaDownloadListener({ + onMediaDownloaded: (messageId, attachmentId, localPath) => { + // attachmentId: the attachment URL without its query string + }, + onMediaDownloadFailed: (messageId, attachmentId, error) => {}, + }) +); +``` + + + +Remove the listener with `CometChat.removeMediaDownloadListener("MEDIA_LISTENER")`. The background listener carries no progress callback — progress is reported only on the explicit `downloadMedia` path. `MessageListener` also gains optional `onMediaDownloaded` and `onMediaDownloadFailed` callbacks. + +## Media cache size limit + +Cached media bytes are bounded by `maxMediaCacheSize` (default **500 MB**; `0` means unlimited). When the cache exceeds the limit, the least recently accessed media is deleted first until it is back under budget. Media belonging to un-sent messages and in-progress uploads is never evicted. + +Media is stored in OPFS (browser) or as files on disk (Electron); the location is not configurable. Eviction runs silently in the background — there is no cache-limit callback. To reason about size, use `CometChat.getCacheSize()` and the configured limits. See the [reference](/sdk/javascript/offline-reference) for the full set of options. + + +Cached media files are **not encrypted**, even when you turn on [cache encryption](/sdk/javascript/offline-encryption). + diff --git a/sdk/javascript/offline-overview.mdx b/sdk/javascript/offline-overview.mdx new file mode 100644 index 000000000..104a2f151 --- /dev/null +++ b/sdk/javascript/offline-overview.mdx @@ -0,0 +1,90 @@ +--- +title: "Offline Support" +sidebarTitle: "Offline Support" +description: "Understand how local caching, the send outbox, and reconnect catch-up keep chat working without a connection with the CometChat JavaScript SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Offline support is OFF by default. Turn it on at init (SDK 4.3.0 or later). +const appSetting = new CometChat.AppSettingsBuilder() + .setRegion("REGION") + .setOfflineConfig({ enabled: true }) // or .offline(true) for defaults + .build(); + +await CometChat.init("APP_ID", appSetting); + +// Is offline support active right now for the logged-in user? +const active = CometChat.isOfflineEnabled(); // boolean +``` + + + +Offline support keeps the logged-in user's conversations and messages on the device so your app can show them instantly, lets the user send messages while disconnected, and catches up automatically when the connection returns. It is **off by default** — nothing changes in your app until you enable it ([Enable Offline Support](/sdk/javascript/offline-enable)). This page explains the mental model; the other pages show the exact APIs. + + +Offline support ships in CometChat JavaScript SDK **4.3.0** or later. It runs in the browser (IndexedDB, with OPFS for media) and in Electron (SQLite). See [Enable Offline Support](/sdk/javascript/offline-enable#supported-runtimes). + + +## How it works + +Three ideas carry the whole feature. + + + +Reads that you pass a fetch listener go through the local cache first. The SDK delivers the cached page immediately — even when it is empty — then fetches from the server and delivers the fresh result. See [Show Cached Data](/sdk/javascript/offline-cached-data). + + +A message sent while offline is saved to a durable outbox and reported as pending instead of failing. It survives page reloads and app restarts and is sent when the connection returns. See [Send While Offline](/sdk/javascript/offline-send). + + +When the connection returns, the SDK syncs the conversations and messages that changed while it was away and flushes the outbox, so the local cache matches the server again. See [Stay in Sync](/sdk/javascript/offline-sync). + + + +```mermaid +flowchart LR + A[App read] --> B[(Local cache)] + B -- cached page first --> A + B --> C{Online?} + C -- yes --> D[Server] + D -- fresh result --> A + C -- no --> E[onError:\nFAILED_TO_FETCH] + F[Send while offline] --> G[(Durable outbox)] + G -- on reconnect --> D +``` + +## What changes in your app + +With offline support enabled, your fetch callbacks fire **twice** on the read path and sends gain a pending stage. To receive the cached and pending results you pass the new listener objects — `MessageFetchListener`, `ConversationFetchListener`, `MessageSentListener`, and the others listed on each page — as an optional argument. Every method still returns its usual Promise, so existing code keeps working. With offline support off, every API behaves exactly as it did before. + +## Queued, not failed + +A send made with no connection fires `onMessagePending`, then **one** `onError` with code `FAILED_TO_FETCH`, and the returned Promise rejects. That is a notice, not a failure: the message stays queued and `onMessageSent` fires later, on reconnect. See [Send While Offline](/sdk/javascript/offline-send#queued-not-failed). + +## One cache per user + +The SDK keeps a single local cache per app and user. + +- A page reload or app restart with a still-valid session reuses that cache, so the data is there instantly. +- Logging out, a session expiring, or logging in as a **different** user deletes the cache. You never clear it yourself — the SDK does it. + +See [Stay in Sync](/sdk/javascript/offline-sync) for cache resets and logout behaviour. + +## How much is kept + +By default the cache holds up to **100 conversations**, up to **500 messages per conversation**, around **250 MB** of cached content, and up to **500 MB** of cached media. There is no age limit unless you set `cacheDurationDays`. Older entries are evicted first as these limits are reached. Every limit is tunable — see the [Configuration & API Reference](/sdk/javascript/offline-reference). + +## Next steps + + + Turn it on and verify it is active. + The cached-first, server-next read contract. + The durable outbox and pending messages. + Reconnect catch-up, cache resets, logout. + Download and cache attachments for offline viewing. + Encrypt the local cache at rest. + Every option, method, listener, enum, and error. + diff --git a/sdk/javascript/offline-reference.mdx b/sdk/javascript/offline-reference.mdx new file mode 100644 index 000000000..fbe6fe46c --- /dev/null +++ b/sdk/javascript/offline-reference.mdx @@ -0,0 +1,178 @@ +--- +title: "Offline Configuration & API Reference" +sidebarTitle: "Configuration & API Reference" +description: "Every offline configuration option, method, listener, enum, model field, and error code in the CometChat JavaScript SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +| Field | Value | +| --- | --- | +| Package | `@cometchat/chat-sdk-javascript` (offline requires **4.3.0** or later) | +| Enable | `new CometChat.AppSettingsBuilder().setOfflineConfig({ enabled: true, … })` or `.offline(true)` | +| Verify | `CometChat.isOfflineEnabled()` | +| Reads | Pass a `…FetchListener` to `fetchNext()` / `fetchPrevious()` / `getConversation()` / `getMessageDetails()` | +| Sends | Pass a `MessageSentListener` to `sendMessage()` / `sendMediaMessage()` / `sendCustomMessage()` / `sendInteractiveMessage()` | +| Outbox | `getPendingMessages()` · `retryMessage()` · `retryAllFailedMessages()` · `discardLocalMessage()` | +| Sync | `addSyncListener()` · `forceSyncNow()` · `getSyncState()` · `getLastSyncTimestamp()` | + + + +This page is the complete lookup for offline support in the CometChat JavaScript SDK. For guided walkthroughs, start with [Offline Support](/sdk/javascript/offline-overview). + + +Offline support requires SDK **4.3.0** or later. + + +## OfflineConfig options + +Pass these keys in the object given to `AppSettingsBuilder.setOfflineConfig(...)`. Every key is optional except `enabled`, which must be an explicit `true`. Bad values are corrected, never rejected, and unknown keys are ignored. You can also pass `true`/`false` as a shorthand with `AppSettingsBuilder.offline(...)`. + +| Key | Type | Default | Range / clamp | Effect | +|---|---|---|---|---| +| `enabled` | `boolean` | `false` | only an explicit `true` enables | Master gate for the whole offline engine. With it off the SDK behaves exactly as before. | +| `cacheDurationDays` | `number` | `0` (unlimited) | negative treated as `0` | Message retention TTL in days. `0` = no age-based prune; eviction is by the other caps. | +| `maxStorageSize` | `number` (bytes) | `262144000` (250 MB) | `0` = unlimited | Byte ceiling for cached content, counted before encryption and excluding unsent messages. Over budget, the SDK removes recorded list reads first (LRU), then whole conversations (LRU; a conversation with unsent messages is kept), then the oldest saved and my-threads entries. | +| `maxMessagesPerConversation` | `number` | `500` | clamped `[1, 1000]`; `0`/over → `1000` | Per-conversation cached-message window. | +| `maxConversations` | `number` | `100` | clamped `[1, 1000]`; `0`/over → `1000` | Cached-conversation cap (whole-conversation LRU). Also the page size of the reconnect conversation refresh. | +| `maxPendingMessages` | `number` | `50` | clamped `[1, 100]`; `0`/over → `100` | Pending-send outbox capacity. | +| `autoSendPendingOnReconnect` | `boolean` | `true` | — | Auto-flush queued `PENDING` sends on reconnect. Gates only the outbox flush; the catch-up sync always runs. | +| `pendingQueueOverflowPolicy` | `PendingMessageOverflowPolicy` | `REJECT_NEWEST` | unknown → default | What the outbox does when it is full. | +| `maxMediaCacheSize` | `number` (bytes) | `524288000` (500 MB) | `0` = unlimited | Media-only byte budget (LRU). | +| `mediaCaching` | `MediaCachingMode` | `AUTO` | unknown → `AUTO` | Receive-side media mode. Fixed at init. | +| `encryption` | `boolean` | `false` | only an explicit `true` enables | Encrypt the cache at rest. See [Encrypt the Cache](/sdk/javascript/offline-encryption). | +| `encryptionFailureBehavior` | `EncryptionFailureBehavior` | `ONLINE_ONLY` | anything but `PLAINTEXT_FALLBACK` → `ONLINE_ONLY` | What happens when encryption cannot run at all. | +| `maxSnapshotQueries` | `number` | `100` | `0`/negative → `100` | Most distinct entity-list reads (users, groups, members, banned members, reactions, notification feed, threads) kept for offline replay; LRU. | +| `snapshotTtlDays` | `number` | `7` | `0`/negative → `7` | Days a recorded list read stays replayable. Past it, the read is empty offline until viewed online again. | +| `debugTracing` | `boolean` | `false` | — | Dev-only diagnostic trace. Leave off in production. | + +Count caps are always bounded and have no "unlimited" setting. Byte and day caps treat `0` as unlimited. + +## Public methods on CometChat + +| Method | Returns | Purpose | +|---|---|---| +| `isOfflineEnabled()` | `boolean` | Whether offline support is active for the current session. | +| `getMediaCachingMode()` | `string` | The effective media caching mode (`"OFF"` whenever offline is not active). | +| `getOfflineEncryptionStatus()` | `OfflineEncryptionStatus` | What encryption at rest is actually doing. | +| `getPendingMessages(conversationId?)` | `Promise` | All queued/failed messages in the outbox. | +| `retryMessage(message \| localMessageId, listener?)` | `Promise` | Retry one message. | +| `retryAllFailedMessages(listener?)` | `Promise` | Retry every transiently failed message. | +| `discardLocalMessage(message \| localMessageId, { onSuccess?, onError? }?)` | `Promise` | Remove one unsent message from the outbox. | +| `forceSyncNow(callback?)` | `void` | Trigger a catch-up sync immediately; the callback reports whether it started. | +| `getSyncState()` | `string` | `"IDLE"` or `"SYNCING"`. | +| `getLastSyncTimestamp()` | `Promise` | Server time of the last successful sync, in epoch seconds. | +| `clearOfflineCache()` | `Promise` | Wipe the local cache (and fail any unsent messages). | +| `getCacheSize()` | `Promise` | Cache size in bytes (cached content before encryption plus media). | +| `downloadMedia(attachment, listener?)` | `void` | Download and cache an attachment's bytes. | +| `getCachedMediaPath(attachment)` | `Promise` | Storage handle of already-cached bytes, or `null`. | +| `getCachedMediaObjectURL(attachment)` | `Promise` | A `blob:` URL for cached bytes, or `null`. Revoke it when done. | +| `getExistingUploadFileRequest(receiverId, receiverType, parentMessageId?)` | `Promise` | Re-attach to an in-progress offline upload. | + +### Listener registration + +| Method | Listener | +|---|---| +| `addMessageListener(name, listener)` / `removeMessageListener(name)` | `MessageListener` (adds `onMessagesSynced`, `onMediaDownloaded`, `onMediaDownloadFailed`) | +| `addOfflineMessageListener(name, listener)` / `removeOfflineMessageListener(name)` | `OfflineMessageListener` | +| `addSyncListener(name, listener)` / `removeSyncListener(name)` | `SyncListener` | +| `addOfflineCacheResetListener(name, listener)` / `removeOfflineCacheResetListener(name)` | `OfflineCacheResetListener` | +| `addMediaDownloadListener(name, listener)` / `removeMediaDownloadListener(name)` | `MediaDownloadListener` | + +Sync and cache-reset registrations are valid at any time, including before login, and survive logout, login and restarts. Registering the same `name` twice replaces the first listener. + +## Listeners and callbacks + +Every listener is constructed with an object of callbacks, for example `new CometChat.MessageFetchListener({ onCachedMessages, onServerMessages, onError })`. + +### Two-phase fetch listeners + +| Listener | Callbacks | +|---|---| +| `MessageFetchListener` | `onCachedMessages(messages)`, `onServerMessages(messages)`, `onError(error)` | +| `ThreadFetchListener` | `onCachedThreads(threads)`, `onServerThreads(threads)`, `onError(error)` | +| `SingleMessageFetchListener` | `onCachedMessage(message)`, `onServerMessage(message)`, `onError(error)` | +| `ConversationFetchListener` | `onCachedConversations(conversations)`, `onServerConversations(conversations)`, `onError(error)` | +| `ConversationDetailsListener` | `onCachedConversation(conversation)`, `onServerConversation(conversation)`, `onError(error)` | +| `UserFetchListener` | `onCachedUsers(users)`, `onServerUsers(users)`, `onError(error)` | +| `GroupFetchListener` | `onCachedGroups(groups)`, `onServerGroups(groups)`, `onError(error)` | +| `GroupMemberFetchListener` | `onCachedGroupMembers(members)`, `onServerGroupMembers(members)`, `onError(error)` | +| `BannedMemberFetchListener` | `onCachedBannedGroupMembers(members)`, `onServerBannedGroupMembers(members)`, `onError(error)` | +| `ReactionFetchListener` | `onCachedReactions(reactions)`, `onServerReactions(reactions)`, `onError(error)` | +| `NotificationFeedFetchListener` | `onCachedNotificationFeedItems(items)`, `onServerNotificationFeedItems(items)`, `onError(error)` | + +`SingleConversationFetchListener` is a deprecated alias of `ConversationDetailsListener`. `BannedMemberFetchListener` also still accepts the older `onCachedBannedMembers` / `onServerBannedMembers` names. + +### Send, sync, cache, and media listeners + +| Listener | Callbacks | +|---|---| +| `MessageSentListener` | `onMessagePending(message)`, `onMessageSent(message)`, `onError(error)` | +| `OfflineMessageListener` | `onOutboxMessagePending(message)`, `onOutboxMessageSent(message)`, `onOutboxMessageFailed(message, reason)` | +| `SyncListener` | `onConversationSyncStarted()`, `onConversationSyncCompleted(conversations)`, `onConversationSyncError(error)`, `onMessageSyncStarted()`, `onMessageSyncCompleted(messages, outcome)`, `onMessageSyncError(error)` | +| `OfflineCacheResetListener` | `onOfflineCacheReset(reason, pendingMessagesLost)` | +| `MediaCallbackListener` | `onSuccess(localPath)`, `onProgress(percent)`, `onError(error)` | +| `MediaDownloadListener` | `onMediaDownloaded(messageId, attachmentId, localPath)`, `onMediaDownloadFailed(messageId, attachmentId, error)` | +| `MessageListener` | `onMessagesSynced(messages)`, `onMediaDownloaded`, `onMediaDownloadFailed` (offline additions to the existing listener) | + +## Enums + +| Enum | Values | +|---|---| +| `MediaCachingMode` | `AUTO` (auto-download + retain received media), `OFF` (skip auto-download; metadata still cached) | +| `PendingMessageOverflowPolicy` | `DROP_OLDEST` (evict the oldest pending to admit a newer send), `REJECT_NEWEST` (reject the new send) | +| `EncryptionFailureBehavior` | `ONLINE_ONLY` (server-only; never plaintext), `PLAINTEXT_FALLBACK` (open an unencrypted cache) | +| `MessageStatus` | `PENDING`, `SENDING`, `SENT`, `DELIVERED`, `READ`, `FAILED` | +| `MessageFailureReason` | `TRANSIENT`, `PERMANENT`, `QUEUE_OVERFLOW`, `QUEUE_FULL`, `BYTE_OVERFLOW`, `DISCARDED` — see [Send While Offline](/sdk/javascript/offline-send#message-status-and-failure-reason) | +| `MessageSource` | `REALTIME`, `SYNC` (delivered by a catch-up sweep) | +| `SyncOutcome` | `CAUGHT_UP` (changelog fully replayed), `PARTIAL` (a page was ingested but more remains) | +| `SyncState` | `IDLE`, `SYNCING` | +| `OfflineCacheResetReason` | `KEY_LOST`, `WRONG_KEY`, `CORRUPTED`, `ESCALATED`, `ENCRYPTION_CHANGED` | + +All enums are string-valued and exposed on the `CometChat` namespace (for example `CometChat.MessageStatus.PENDING`). + +## Model fields + +| Field | Type | Meaning | +|---|---|---| +| `BaseMessage.getLocalMessageId()` | `string` | SDK-generated local handle for an optimistic/offline send; the stable key to reconcile the row when the server ack arrives. | +| `BaseMessage.getStatus()` | `MessageStatus` | Lifecycle status of the message. A status set by your app with `setStatus()` is never read by the offline layer. | +| `BaseMessage.getFailureReason()` | `MessageFailureReason` | Why the send failed; set only while `FAILED`. | +| `BaseMessage.getSource()` | `MessageSource` | `REALTIME` for a live message, `SYNC` for one delivered by catch-up. | +| `BaseMessage.getUpdatedAt()` | `number` | Last update time of the message. | +| `Attachment.getLocalPath()` | `string` | Storage handle of cached media bytes (`opfs:…` in a browser, a file path on Electron), or empty when not cached. Never sent to the server. | + +## Error codes + +The offline-related codes the SDK raises: + +| Code | Raised when | +|---|---| +| `FAILED_TO_FETCH` | A request got no answer (offline or dropped): a two-phase read's `onError` after the cached page, the not-connected notice on a queued send, and sync errors. | +| `ERROR_OFFLINE_SYNC_IN_PROGRESS` | `forceSyncNow(callback)` was called while a sync is already running. Constant: `CometChat.ERROR_OFFLINE_SYNC_IN_PROGRESS`. | +| `ERROR_OFFLINE_SYNC_COOLDOWN` | `forceSyncNow(callback)` was called within the 10-second cooldown; `details.retryAfterMs` has the remaining time. Constant: `CometChat.ERROR_OFFLINE_SYNC_COOLDOWN`. | +| `ERROR_OFFLINE_DISABLED` | An offline-only operation was called while offline support is not active. Constant: `CometChat.ERROR_OFFLINE_DISABLED`. | +| `ERROR_OFFLINE_MESSAGE_DISCARDED` | A queued message was removed before it could send. Constant: `CometChat.ERROR_OFFLINE_MESSAGE_DISCARDED`. | +| `ERROR_OFFLINE_MESSAGE_IN_FLIGHT` | `retryMessage` / `discardLocalMessage` targeted a message that is already being sent. Constant: `CometChat.ERROR_OFFLINE_MESSAGE_IN_FLIGHT`. | +| `ERROR_OFFLINE_MESSAGE_NOT_FOUND` | `retryMessage` / `discardLocalMessage` found no unsent message with the given local id. Constant: `CometChat.ERROR_OFFLINE_MESSAGE_NOT_FOUND`. | +| `ERROR_OFFLINE_MESSAGE_PERSIST_FAILED` | The local cache was briefly unavailable, so `getPendingMessages`, `retryMessage`, `retryAllFailedMessages` or `discardLocalMessage` could not read it. Try again. | +| `QUEUE_FULL` | A send was refused because the outbox is full; `details.maxPendingMessages` holds the cap. | +| `ERROR_OFFLINE_MEDIA_NO_URL` | `downloadMedia` was given no attachment, or an attachment without a URL. | +| `ERROR_JSON_EXCEPTION` | A sync request got a 2xx answer with an unreadable body; `details.httpStatus` holds the status. | +| `USER_NOT_LOGED_IN` | `forceSyncNow(callback)` was called with nobody logged in (spelling as the SDK returns it). | + +## Cache-supported filters + +A cached read answers only if **every** filter on the request can be computed locally. One server-only filter empties the cached phase while the server phase still answers in full. + +| Request | Answered from the cache | Server-only (empties the cached phase) | +|---|---|---| +| `MessagesRequest` | categories, types, parent message id, hide replies, hide deleted messages, tags, hide quoted messages, has mentions, mentioned UIDs, unread, has attachments, attachment types, has reactions, hide messages from blocked users | `searchKeyword`, `hasLinks`, `updatesOnly`, `interactionGoalCompletedOnly` | +| `ConversationsRequest` | conversation type, unread, tags, user tags, group tags, hide/only agentic, with-blocked-info, include blocked users, pinned-by | `searchKeyword` | + +Enrichment flags (`withTags`, `withParent`, mentions-with-tag/blocked info, `withUserAndGroupTags`) decorate a result without narrowing it and never gate the cache. Entity lists (users, groups, members, banned members, reactions, notification feed, threads) are replayed from recorded requests rather than filtered locally. + +## Deprecated overloads and replacements + +Nothing is deprecated for the main fetch and send methods: the listener is an **additive optional argument** and every method keeps returning its Promise. With offline off, a listener-bearing call behaves like the single-callback form. The only deprecated names are `SingleConversationFetchListener` (use `ConversationDetailsListener`) and the `onCachedBannedMembers` / `onServerBannedMembers` callback names (use the `…BannedGroupMembers` names). diff --git a/sdk/javascript/offline-send.mdx b/sdk/javascript/offline-send.mdx new file mode 100644 index 000000000..56fe1b0b0 --- /dev/null +++ b/sdk/javascript/offline-send.mdx @@ -0,0 +1,401 @@ +--- +title: "Send While Offline" +sidebarTitle: "Send While Offline" +description: "Queue messages sent without a connection, track their status, and retry failed sends with the CometChat JavaScript SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Send with the offline-aware listener (queued when offline, auto-sent on reconnect) +CometChat.sendMessage( + textMessage, + new CometChat.MessageSentListener({ + onMessagePending: (message) => { /* status === PENDING; queued on the device */ }, + onMessageSent: (message) => { /* server ack: real id + sentAt */ }, + onError: (error) => { /* FAILED_TO_FETCH = queued offline, not failed */ }, + }) +).catch(() => {}); // the returned Promise also rejects while offline + +// Read offline state off a message +message.getLocalMessageId(); // stable client handle while offline is running +message.getStatus(); // PENDING, SENDING, SENT, DELIVERED, READ, FAILED +message.getFailureReason(); // MessageFailureReason, set only while FAILED + +// Inspect and recover the outbox +await CometChat.getPendingMessages(conversationId?); // PENDING + SENDING + FAILED +await CometChat.retryMessage(localMessageId, sentListener); // one message +await CometChat.retryAllFailedMessages(sentListener); // transient failures only +await CometChat.discardLocalMessage(localMessageId); // delete an unsent message + +// Observe every outbox transition, app-wide +CometChat.addOfflineMessageListener("LISTENER_ID", new CometChat.OfflineMessageListener({ + onOutboxMessagePending: (message) => {}, + onOutboxMessageSent: (message) => {}, + onOutboxMessageFailed: (message, reason) => {}, +})); +``` + + + +With offline support enabled, a message you send while the device has no connection is **durably queued on the device** instead of failing. It keeps its place across a page reload or app restart and is sent automatically when the connection returns. This page shows the offline-aware send listener, how to read a message's status, how to inspect and recover the send queue, and the queue limits. + + +Offline support is **off by default** and requires SDK **4.3.0** or later. Queuing only happens once you [enable it](/sdk/javascript/offline-enable) **and** pass a `MessageSentListener`. A bare `await CometChat.sendMessage(message)` with no listener is never queued: offline, it rejects at once, as before. + + +## Send a Message + +Pass a `MessageSentListener` to `sendMessage`. Its lifecycle is `onMessagePending` first (the message is durably queued), then exactly one final outcome — `onMessageSent` (the server accepted it) or, for a real failure, `onError`. + + + +```typescript +const textMessage: CometChat.TextMessage = new CometChat.TextMessage( + "UID", + "Hello!", + CometChat.RECEIVER_TYPE.USER +); + +CometChat.sendMessage( + textMessage, + new CometChat.MessageSentListener({ + onMessagePending: (message: CometChat.BaseMessage) => { + // Queued and persisted; message.getStatus() === "PENDING" + console.log("Pending, local id:", message.getLocalMessageId()); + }, + onMessageSent: (message: CometChat.BaseMessage) => { + // Server ack: message now carries its real id and sentAt + console.log("Sent:", message.getId()); + }, + onError: (error: CometChat.CometChatException) => { + // FAILED_TO_FETCH while offline means "queued", not "failed" — see below + console.log("Send error:", error.code); + }, + }) +).catch(() => {}); +``` + + +```javascript +const textMessage = new CometChat.TextMessage( + "UID", + "Hello!", + CometChat.RECEIVER_TYPE.USER +); + +CometChat.sendMessage( + textMessage, + new CometChat.MessageSentListener({ + onMessagePending: (message) => { + // Queued and persisted; message.getStatus() === "PENDING" + console.log("Pending, local id:", message.getLocalMessageId()); + }, + onMessageSent: (message) => { + // Server ack: message now carries its real id and sentAt + console.log("Sent:", message.getId()); + }, + onError: (error) => { + // FAILED_TO_FETCH while offline means "queued", not "failed" — see below + console.log("Send error:", error.code); + }, + }) +).catch(() => {}); +``` + + + +The same `MessageSentListener` is accepted by all four send methods: + +```javascript +CometChat.sendMessage(textMessage, listener); // TextMessage +CometChat.sendMediaMessage(mediaMessage, listener); // MediaMessage +CometChat.sendCustomMessage(customMessage, listener); // CustomMessage +CometChat.sendInteractiveMessage(interactiveMessage, listener); // InteractiveMessage +``` + + +The returned Promise is **not** replaced by the listener. It means "was it sent now?": offline it **rejects**, and the eventual send is observed through `onMessageSent` or `getPendingMessages`, never the Promise. Always attach a `.catch()` when you rely on the listener alone. + + +## Queued, Not Failed + +When the device is offline, the send still fires `onMessagePending`, then fires `onError` **once** with the code `FAILED_TO_FETCH`. This is a **notification, not a failure**: the message stays durably `PENDING`, and `onMessageSent` fires **later**, on reconnect, when it actually goes. **Do not re-send on that error.** + +So for a single send, `onError` and `onMessageSent` can **both** fire (offline, then reconnect), and `onError` does not always mean the message failed. Read the message's status to tell the two apart — keep the `localMessageId` from `onMessagePending`, then look the message up in the outbox: + +| `status` after `onError` | Meaning | What to do | +|---|---|---| +| `PENDING` | Queued; not sent yet. Will auto-send on reconnect. | Show as sending/queued; nothing to do. | +| `FAILED` | A real failure; will not retry on its own. | Offer the user a manual retry (see below). | + + + +```typescript +let localId: string | undefined; + +CometChat.sendMessage( + textMessage, + new CometChat.MessageSentListener({ + onMessagePending: (message: CometChat.BaseMessage) => { + localId = message.getLocalMessageId(); + }, + onError: async () => { + const outbox = await CometChat.getPendingMessages(); + const queued = outbox.find((m) => m.getLocalMessageId() === localId); + if (queued?.getStatus() === CometChat.MessageStatus.PENDING) { + // Queued offline — it will send automatically on reconnect + } else if (queued?.getStatus() === CometChat.MessageStatus.FAILED) { + // Real failure — surface a retry action + } + }, + }) +).catch(() => {}); +``` + + +```javascript +let localId; + +CometChat.sendMessage( + textMessage, + new CometChat.MessageSentListener({ + onMessagePending: (message) => { + localId = message.getLocalMessageId(); + }, + onError: async () => { + const outbox = await CometChat.getPendingMessages(); + const queued = outbox.find((m) => m.getLocalMessageId() === localId); + if (queued?.getStatus() === CometChat.MessageStatus.PENDING) { + // Queued offline — it will send automatically on reconnect + } else if (queued?.getStatus() === CometChat.MessageStatus.FAILED) { + // Real failure — surface a retry action + } + }, + }) +).catch(() => {}); +``` + + + + +For a global view, `onOutboxMessageFailed` on the `OfflineMessageListener` (below) fires only for real failures and carries the reason — a simpler place to drive a "failed, tap to retry" state. + + +A send made while an earlier message in the **same conversation** is still unsent waits behind it and gets no notice: no `onError`, and its Promise does not reject. It resolves, with `onMessageSent`, when it is sent. + +### Message status and failure reason + +Every message exposes its offline state. `getLocalMessageId()` returns the SDK-generated handle that is stamped at compose (while offline is running) and stays stable across the whole lifecycle — use it to retry or discard the message. + +| Accessor | Type | Notes | +|---|---|---| +| `getLocalMessageId()` | `string` | Client handle; present while offline is running. | +| `getStatus()` | `MessageStatus` | `PENDING`, `SENDING`, `SENT`, `DELIVERED`, `READ`, `FAILED`. | +| `getFailureReason()` | `MessageFailureReason` | Set only while `FAILED`; otherwise empty. | + +`MessageFailureReason` values: + +| Reason | When | +|---|---| +| `TRANSIENT` | A temporary server error (for example 5xx or rate-limited), or no answer after five reconnect attempts. Retryable. | +| `PERMANENT` | The server rejected the message, the error was not recognized, or copying an attachment into the store timed out. A manual retry will not help. | +| `QUEUE_FULL` | A new send was refused because the queue is full (count cap). | +| `QUEUE_OVERFLOW` | An older queued message was evicted to admit a newer one (`DROP_OLDEST` only). | +| `BYTE_OVERFLOW` | A media send was refused because it exceeds the queue's byte budget. | +| `DISCARDED` | The message was removed before it could send (for example a cache clear or a conversation purge). | + +## Inspect the Queue + +`getPendingMessages` returns the **entire un-acked outbox** — every `PENDING`, `SENDING`, and `FAILED` message, ordered by local compose time. Sent messages are not included. Pass a conversation id to limit it to one conversation. Read each message's `status` to group them. + + + +```typescript +CometChat.getPendingMessages().then( + (messages: CometChat.BaseMessage[]) => { + messages.forEach((m) => console.log(m.getLocalMessageId(), "->", m.getStatus())); + }, + (error: CometChat.CometChatException) => { + console.log("Could not read the outbox:", error.code); + } +); +``` + + +```javascript +CometChat.getPendingMessages().then( + (messages) => { + messages.forEach((m) => console.log(m.getLocalMessageId(), "->", m.getStatus())); + }, + (error) => { + console.log("Could not read the outbox:", error.code); + } +); +``` + + + +A queued media message sent from local files comes back with its files (`message.files`). With offline off, or nobody signed in, it resolves with an empty list. If the local store is briefly unavailable (busy, locked, full, or still reopening), it **rejects** with `ERROR_OFFLINE_MESSAGE_PERSIST_FAILED` instead of returning an empty list — try again. + +### Observe the queue app-wide + +The per-call `MessageSentListener` lives only as long as the call. To watch every queue transition — a live send, a reconnect auto-flush, a manual retry, or a resumed send after a restart — register a global `OfflineMessageListener`. Its failed callback carries the `MessageFailureReason`. + + + +```typescript +CometChat.addOfflineMessageListener( + "LISTENER_ID", + new CometChat.OfflineMessageListener({ + onOutboxMessagePending: (message: CometChat.BaseMessage) => {}, + onOutboxMessageSent: (message: CometChat.BaseMessage) => {}, + onOutboxMessageFailed: (message: CometChat.BaseMessage, reason: string) => { + console.log("Failed:", message.getLocalMessageId(), reason); + }, + }) +); + +// Remove it when no longer needed +CometChat.removeOfflineMessageListener("LISTENER_ID"); +``` + + +```javascript +CometChat.addOfflineMessageListener( + "LISTENER_ID", + new CometChat.OfflineMessageListener({ + onOutboxMessagePending: (message) => {}, + onOutboxMessageSent: (message) => {}, + onOutboxMessageFailed: (message, reason) => { + console.log("Failed:", message.getLocalMessageId(), reason); + }, + }) +); + +// Remove it when no longer needed +CometChat.removeOfflineMessageListener("LISTENER_ID"); +``` + + + +## Retry and Discard + +There is no automatic retry of a `FAILED` message — recovery is explicit and user-invoked. Each method accepts a message or its `localMessageId`. + +- **`retryMessage(message, listener?)`** re-attempts one message. A `FAILED` message is reset to `PENDING` and sent; a `PENDING` message is attempted now rather than waiting for a reconnect. The optional `MessageSentListener` and the returned Promise receive that attempt's outcome. With no answer, it fires the not-connected notice once (`FAILED_TO_FETCH`) and the message stays `PENDING`. +- **`retryAllFailedMessages(listener?)`** retries **transient** failures only; permanent ones are skipped. +- **`discardLocalMessage(message, listener?)`** deletes an unsent (`PENDING` or `FAILED`) message from the device, including its copied media. The optional listener is `{ onSuccess, onError }`; `onSuccess` receives the `localMessageId`. + + + +```typescript +await CometChat.retryMessage(localMessageId, sentListener); + +await CometChat.retryAllFailedMessages(sentListener); + +CometChat.discardLocalMessage(localMessageId, { + onSuccess: (discardedLocalId: string) => {}, + onError: (error: CometChat.CometChatException) => { + console.log("Discard rejected:", error.code); + }, +}).catch(() => {}); +``` + + +```javascript +await CometChat.retryMessage(localMessageId, sentListener); + +await CometChat.retryAllFailedMessages(sentListener); + +CometChat.discardLocalMessage(localMessageId, { + onSuccess: (discardedLocalId) => {}, + onError: (error) => { + console.log("Discard rejected:", error.code); + }, +}).catch(() => {}); +``` + + + +A retry is never refused for the queue cap and never evicts other messages, so a retry may take the outbox over `maxPendingMessages`. + +`retryMessage` and `discardLocalMessage` reject, changing nothing, when: + +| Error code | Reason | +|---|---| +| `ERROR_OFFLINE_MESSAGE_IN_FLIGHT` | The message is already sending (`SENDING`, or a timed-out request still awaiting its answer). | +| `ERROR_OFFLINE_MESSAGE_NOT_FOUND` | No unsent message has that id (unknown, or already sent). | +| `ERROR_OFFLINE_DISABLED` | Offline support is not running. | +| `ERROR_OFFLINE_MESSAGE_PERSIST_FAILED` | The local cache was briefly unavailable. Try again. | + +## Auto-send on Reconnect + +By default, queued `PENDING` messages are sent automatically when the connection is re-established. This is controlled by `autoSendPendingOnReconnect` (default `true`); see [Enable Offline Support](/sdk/javascript/offline-enable) and the [reference](/sdk/javascript/offline-reference). With it off, a send that gets no answer fails at once with reason `TRANSIENT` (its listener gets one `onError` with `FAILED_TO_FETCH` and the message is `FAILED`), which you recover with `retryMessage`. + +A send that gets no answer for 15 seconds (media: 40 seconds plus upload time) counts as unanswered. After five reconnect attempts with no answer it fails with `TRANSIENT`. + +Reconnect also runs a catch-up sync for received messages — see [Stay in Sync](/sdk/javascript/offline-sync). + +## Queue Limits + +The outbox holds at most `maxPendingMessages` messages still trying to send (`PENDING` + `SENDING`); `FAILED` messages do not count. When the queue is full, `pendingQueueOverflowPolicy` decides what happens: + +| `PendingMessageOverflowPolicy` | Behaviour on overflow | +|---|---| +| `REJECT_NEWEST` (**default**) | The new send is refused and reported with reason `QUEUE_FULL`. Nothing already queued is lost. | +| `DROP_OLDEST` | The oldest `PENDING` message is evicted (reason `QUEUE_OVERFLOW`) to admit the new one. | + +A media send that exceeds the queue's byte budget is refused with reason `BYTE_OVERFLOW`. + +A **refused** send (`QUEUE_FULL` / `BYTE_OVERFLOW`) is never queued, so it fires **no** `onMessagePending`; it reports `onError`, and the global `OfflineMessageListener` receives `onOutboxMessageFailed(message, reason)` carrying the exact reason. For `QUEUE_FULL` the error's `code` is `QUEUE_FULL`, with `details.maxPendingMessages`. + + +A refused or evicted message (`QUEUE_FULL`, `QUEUE_OVERFLOW`, `BYTE_OVERFLOW`) has **no durable row** — it does not appear in `getPendingMessages`, and `retryMessage` on its id does nothing. Capture these from the failure callback and handle them at that moment. Prefer the default `REJECT_NEWEST`, which never silently drops an already-queued message from another conversation. + + +## Edits, Deletes, and Reactions Need a Connection + +Offline queuing applies to **sending new messages** only. Changes to existing messages — edit, delete, add or remove a reaction — and other own actions are **not queued offline**. They are sent to the server whenever you trigger them, and they fail with the not-connected error if the request gets no answer. + +## Durable Media Uploads + +A multi-attachment upload survives a restart: the SDK re-attaches to an in-progress batch instead of restarting it. Fetch the existing request for a conversation with `getExistingUploadFileRequest` (it resolves `null` when there is none, or offline is off), read each file's progress with `getItems()`, and retry an individual file with `retryAttachment(fileId)`. Uploads never auto-resume: restored files come back retryable and wait for the user. A `FAILED` file with `localCopyMissing: true` cannot be retried — ask the user to re-add it. + + + +```typescript +const request = await CometChat.getExistingUploadFileRequest("UID", CometChat.RECEIVER_TYPE.USER); + +if (request) { + for (const item of request.getItems()) { + console.log(item.fileId, "->", item.status); // "queued", "uploading", "uploaded", "failed", "rejected", … + if (item.status === "failed") { + request.retryAttachment(item.fileId); + } + } +} +``` + + +```javascript +const request = await CometChat.getExistingUploadFileRequest("UID", CometChat.RECEIVER_TYPE.USER); + +if (request) { + for (const item of request.getItems()) { + console.log(item.fileId, "->", item.status); // "queued", "uploading", "uploaded", "failed", "rejected", … + if (item.status === "failed") { + request.retryAttachment(item.fileId); + } + } +} +``` + + + +See [Media Caching](/sdk/javascript/offline-media-caching) for downloading and caching received attachments. + + +For the full list of offline methods, listeners, enums, model fields, and error codes, see the [Configuration & API Reference](/sdk/javascript/offline-reference). + diff --git a/sdk/javascript/offline-sync.mdx b/sdk/javascript/offline-sync.mdx new file mode 100644 index 000000000..8e7942f3d --- /dev/null +++ b/sdk/javascript/offline-sync.mdx @@ -0,0 +1,295 @@ +--- +title: "Stay in Sync" +sidebarTitle: "Stay in Sync" +description: "Catch up on everything that changed while offline, observe sync progress, trigger a manual sync, and manage the local cache with the CometChat JavaScript SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Observe the reconnect catch-up sync (conversation phase, then message phase) +CometChat.addSyncListener("SYNC_LISTENER", new CometChat.SyncListener({ + onConversationSyncCompleted: (conversations) => { /* blind-replace list */ }, + onMessageSyncCompleted: (messages, outcome) => { /* merge by id */ }, + onMessageSyncError: (error) => {}, +})); +CometChat.removeSyncListener("SYNC_LISTENER"); + +// Poll sync state +const state = CometChat.getSyncState(); // "IDLE" | "SYNCING" +const lastSync = await CometChat.getLastSyncTimestamp(); // server epoch seconds, 0 if never + +// Trigger a manual catch-up (callback is told whether it was accepted) +CometChat.forceSyncNow((error) => { /* IN_PROGRESS / COOLDOWN / DISABLED / USER_NOT_LOGED_IN */ }); + +// Observe cache rebuilds +CometChat.addOfflineCacheResetListener("RESET_LISTENER", new CometChat.OfflineCacheResetListener({ + onOfflineCacheReset: (reason, pendingMessagesLost) => {}, +})); + +// Inspect / clear the cache +const bytes = await CometChat.getCacheSize(); +await CometChat.clearOfflineCache(); // destructive; drops the un-sent outbox +``` + + + +When the connection comes back, 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. + + +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](/sdk/javascript/offline-enable). + + +## How catch-up works + +A sync runs automatically when the connection settles after a reconnect. It never runs on a brief connect-then-drop flap, and overlapping triggers coalesce into a single sweep. It proceeds in two phases, in order: + +1. **Conversation phase** — the conversation list is fetched as one fresh page (up to `maxConversations`). 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. 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 `CometChat.SyncListener` to observe the lifecycle of every automatic and manual sync. Each phase fires `started`, then exactly one of `completed` / `error`. + + + +```typescript +CometChat.addSyncListener( + "SYNC_LISTENER", + new CometChat.SyncListener({ + onConversationSyncStarted: () => {}, + onConversationSyncCompleted: (conversations: CometChat.Conversation[]) => { + // The full, fresh conversation page. Blind-replace your list. + }, + onConversationSyncError: (error: CometChat.CometChatException) => {}, + onMessageSyncStarted: () => {}, + onMessageSyncCompleted: (messages: CometChat.BaseMessage[], outcome: string) => { + // New + changed messages reconciled this sync. Merge by message id. + }, + onMessageSyncError: (error: CometChat.CometChatException) => {}, + }) +); +``` + + +```javascript +CometChat.addSyncListener( + "SYNC_LISTENER", + new CometChat.SyncListener({ + 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)` | A transport or parse failure; the cache is left untouched and the refresh retries on the next reconnect. | +| `onMessageSyncStarted()` | The message changelog sweep began. | +| `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 error always carries a `code`: `FAILED_TO_FETCH` when the request got no answer, `ERROR_JSON_EXCEPTION` when the server answered 2xx with an unreadable body (`details.httpStatus` holds the status), otherwise the server's own code. + +`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; it drains one page per reconnect. | + +Remove the listener with `CometChat.removeSyncListener("SYNC_LISTENER")`. Registrations are kept across login, logout and restarts, so register once. + +## 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 `message.getSource()` set to `SYNC`. +- **Updates** to already-cached messages arrive together on `onMessagesSynced(messages)`. + + + +```typescript +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. + }, + }) +); +``` + + +```javascript +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. + }, + }) +); +``` + + + +## Check sync state + +Poll the current sync state and the time of the last completed sync at any time. + + + +```typescript +const state: string = CometChat.getSyncState(); // "IDLE" or "SYNCING" +const lastSync: number = await CometChat.getLastSyncTimestamp(); // server epoch seconds; 0 if never synced +``` + + +```javascript +const state = CometChat.getSyncState(); // "IDLE" or "SYNCING" +const lastSync = await CometChat.getLastSyncTimestamp(); // server epoch seconds; 0 if never synced +``` + + + +`getSyncState()` returns `SYNCING` while a sweep is in flight, otherwise `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, and it enforces a 10-second cooldown between forced syncs. + +Pass a callback to learn whether the request was accepted; it receives no error (`null`) as soon as the catch-up starts. The sync's actual progress and outcome still arrive on your registered `SyncListener`. + + + +```typescript +CometChat.forceSyncNow((error?: CometChat.CometChatException) => { + if (error) { + // Rejected — see the code. + } else { + // Accepted and started. + } +}); +``` + + +```javascript +CometChat.forceSyncNow((error) => { + if (error) { + // Rejected — see the code. + } else { + // Accepted and started. + } +}); +``` + + + +If you do not need the acceptance result, call `CometChat.forceSyncNow()` with no argument; a rejected request is silently dropped. + +| 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; `details.retryAfterMs` carries the remaining cooldown. | +| `ERROR_OFFLINE_DISABLED` | A user is logged in but offline support is not active. | +| `USER_NOT_LOGED_IN` | Nobody is logged in (spelling as the SDK returns it). | + +## When the cache resets + +In rare cases the SDK cannot keep the existing cache — for example the encryption key became unrecoverable, or the database 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 `-1` (`CometChat.OFFLINE_PENDING_COUNT_UNKNOWN`) when the count is unknown. Each lost message that could still be read fails with `DISCARDED` on `onOutboxMessageFailed`. A logout never fires it. + + + +```typescript +CometChat.addOfflineCacheResetListener( + "RESET_LISTENER", + new CometChat.OfflineCacheResetListener({ + onOfflineCacheReset: (reason: string, pendingMessagesLost: number) => { + // The cache was rebuilt. Re-fetch anything you need; it will re-sync from the server. + }, + }) +); +``` + + +```javascript +CometChat.addOfflineCacheResetListener( + "RESET_LISTENER", + new CometChat.OfflineCacheResetListener({ + onOfflineCacheReset: (reason, pendingMessagesLost) => { + // The cache was rebuilt. Re-fetch anything you need; it will re-sync from the server. + }, + }) +); +``` + + + +| `OfflineCacheResetReason` | Cause | +|---|---| +| `KEY_LOST` | The encryption key could not be recovered (for example browser site data was partly cleared); a fresh key was minted. | +| `WRONG_KEY` | The stored key does not open the cache. | +| `CORRUPTED` | The cache is damaged. | +| `ESCALATED` | An unclassified open failure recurred on three consecutive launches and was escalated to a permanent rebuild. | +| `ENCRYPTION_CHANGED` | The `encryption` setting changed, so the cache was rebuilt in the configured format. | + +Remove the listener with `CometChat.removeOfflineCacheResetListener("RESET_LISTENER")`. See [Encrypt the Cache](/sdk/javascript/offline-encryption) for the encryption-related reasons. + + +A cache that another tab (or process) holds locked or busy is never rebuilt; offline stays off for that session instead. + + +## Manage the cache + +Read the current cache size, or clear the cache entirely. + + + +```typescript +const bytes: number = await CometChat.getCacheSize(); // cached content + media bytes +await CometChat.clearOfflineCache(); // wipe the entire per-user cache +``` + + +```javascript +const bytes = await CometChat.getCacheSize(); // cached content + media bytes +await CometChat.clearOfflineCache(); // wipe the entire per-user cache +``` + + + +`getCacheSize()` returns the size in bytes using the same measure as `maxStorageSize` — cached content before encryption, plus cached media bytes. It is not the size on disk and not everything your origin stores. It returns `0` when offline support is disabled. + + +`clearOfflineCache()` is destructive and irreversible. It empties the entire per-user cache — cached messages, conversations, lists, media bytes **and the un-sent outbox**. Each still-queued `PENDING` / `SENDING` message fails with reason `DISCARDED` (`ERROR_OFFLINE_MESSAGE_DISCARDED` on its per-send listener); one already `FAILED` is removed silently. It is a no-op unless offline support is active, and the encryption key is kept. + + + +You never need to clear the cache on logout — the SDK does it for you. The cache is stored **per app and user**: a reload with a still-valid session reuses it, and it is deleted on logout, token expiry, an app id change, or when a different user logs in. With offline off in your config, any offline data an earlier build left on the device is deleted when the session ends, and `logout()` resolves after the deletion (in a browser this can take up to about 5 seconds per database while another tab still has it open). See [Offline Support](/sdk/javascript/offline-overview). + From 6b5b7197e489b0046e8e3c5357070e4bf622ec4e Mon Sep 17 00:00:00 2001 From: rajdubey Date: Sat, 31 Oct 2026 23:23:20 +0530 Subject: [PATCH 02/12] docs(js-offline): drop Electron version minimums and an unverified claim The SDK declares no minimum Electron or better-sqlite3-multiple-ciphers version and does not check that the main and renderer package versions match. Also drop the setStatus note from the reference. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XbguTshd7eq8N2XxFabgLN --- sdk/javascript/offline-enable.mdx | 2 +- sdk/javascript/offline-encryption.mdx | 2 +- sdk/javascript/offline-reference.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/javascript/offline-enable.mdx b/sdk/javascript/offline-enable.mdx index 32857cbf0..095361822 100644 --- a/sdk/javascript/offline-enable.mdx +++ b/sdk/javascript/offline-enable.mdx @@ -147,7 +147,7 @@ If you initialize with `CometChat.initFromSettings(settings)`, add an `offline` Where IndexedDB is missing or blocked (for example some private-browsing modes), offline stays off for the session and the SDK works online as before. -**Electron setup.** Install the optional peer dependencies `electron` (20 or later) and `better-sqlite3-multiple-ciphers` (9 or later), call `registerOfflineIpc()` from `@cometchat/chat-sdk-javascript/electron/main` in the main process, and `installCometChatBridge()` from `@cometchat/chat-sdk-javascript/electron/preload` in your preload script (with `contextIsolation: true`). The main process and the renderer must use the same package version. Browser apps need nothing extra. +**Electron setup.** Install the optional peer dependencies `electron` and `better-sqlite3-multiple-ciphers`, call `registerOfflineIpc()` from `@cometchat/chat-sdk-javascript/electron/main` in the main process, and `installCometChatBridge()` from `@cometchat/chat-sdk-javascript/electron/preload` in your preload script (with `contextIsolation: true`). Browser apps need nothing extra. ## Server switch diff --git a/sdk/javascript/offline-encryption.mdx b/sdk/javascript/offline-encryption.mdx index 1d045b73b..431b7f0f1 100644 --- a/sdk/javascript/offline-encryption.mdx +++ b/sdk/javascript/offline-encryption.mdx @@ -41,7 +41,7 @@ This is not end-to-end encryption — the server still sees plaintext. It protec | Runtime | Cipher | What you need | |---|---|---| | Browser | AES-256-GCM over stored message and list content | A **secure context** (HTTPS or `localhost`) so `crypto.subtle` exists. Nothing to install. | -| Electron | SQLCipher for the whole database | `better-sqlite3-multiple-ciphers` (9 or later) installed in the main process, and an OS keyring where `safeStorage` needs one (for example Linux). | +| Electron | SQLCipher for the whole database | `better-sqlite3-multiple-ciphers` installed in the main process, and an OS keyring where `safeStorage` needs one (for example Linux). | Without a working cipher the cache **cannot run encrypted**. What happens then is decided by [`encryptionFailureBehavior`](#choose-what-happens-when-encryption-cannot-run) — by default the session runs server-only (no local cache). The SDK never creates a cache that is labelled encrypted but is not. diff --git a/sdk/javascript/offline-reference.mdx b/sdk/javascript/offline-reference.mdx index fbe6fe46c..458b19775 100644 --- a/sdk/javascript/offline-reference.mdx +++ b/sdk/javascript/offline-reference.mdx @@ -137,7 +137,7 @@ All enums are string-valued and exposed on the `CometChat` namespace (for exampl | Field | Type | Meaning | |---|---|---| | `BaseMessage.getLocalMessageId()` | `string` | SDK-generated local handle for an optimistic/offline send; the stable key to reconcile the row when the server ack arrives. | -| `BaseMessage.getStatus()` | `MessageStatus` | Lifecycle status of the message. A status set by your app with `setStatus()` is never read by the offline layer. | +| `BaseMessage.getStatus()` | `MessageStatus` | Lifecycle status of the message. | | `BaseMessage.getFailureReason()` | `MessageFailureReason` | Why the send failed; set only while `FAILED`. | | `BaseMessage.getSource()` | `MessageSource` | `REALTIME` for a live message, `SYNC` for one delivered by catch-up. | | `BaseMessage.getUpdatedAt()` | `number` | Last update time of the message. | From 2590ac58ca0231294500d264bef14493ec91cebe Mon Sep 17 00:00:00 2001 From: rajdubey Date: Sat, 31 Oct 2026 23:23:20 +0530 Subject: [PATCH 03/12] docs(js-offline): add automatic media download paths and fix retry on a refused send A refused or evicted send has no stored row, so retryMessage and discardLocalMessage reject with ERROR_OFFLINE_MESSAGE_NOT_FOUND. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XbguTshd7eq8N2XxFabgLN --- sdk/javascript/offline-media-caching.mdx | 11 +++++++++++ sdk/javascript/offline-send.mdx | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/sdk/javascript/offline-media-caching.mdx b/sdk/javascript/offline-media-caching.mdx index de367ac02..a7e57418f 100644 --- a/sdk/javascript/offline-media-caching.mdx +++ b/sdk/javascript/offline-media-caching.mdx @@ -89,6 +89,17 @@ const appSetting = new CometChat.AppSettingsBuilder() Read the mode in effect at runtime with `CometChat.getMediaCachingMode()`. It returns `"OFF"` whenever offline support is not active, whatever the app configured, so a UI layer can rely on it without reading the config itself. +## What downloads automatically + +In `AUTO` mode, media bytes are downloaded and cached automatically on the two live receive paths: + +- a **realtime** media message received while connected, and +- opening a chat **online** (the server read refreshes and caches its media). + +A media message that arrives through a reconnect catch-up [sync](/sdk/javascript/offline-sync) is cached as **metadata only**; its bytes download the next time you open that chat online. This holds in both `AUTO` and `OFF`. + +In `OFF` mode, nothing is auto-downloaded on any path; media loads live from its URL when online. `downloadMedia` still works. + ## Reading cached media When bytes are already cached, the SDK **sets the attachment's local path automatically** on the messages it delivers. Read it with `attachment.getLocalPath()`. In a browser that value is an OPFS storage handle (`opfs:…`), **not** something you can put in an `src` — to render a cached file, ask for an object URL instead. diff --git a/sdk/javascript/offline-send.mdx b/sdk/javascript/offline-send.mdx index 56fe1b0b0..3bc193427 100644 --- a/sdk/javascript/offline-send.mdx +++ b/sdk/javascript/offline-send.mdx @@ -352,7 +352,7 @@ A media send that exceeds the queue's byte budget is refused with reason `BYTE_O A **refused** send (`QUEUE_FULL` / `BYTE_OVERFLOW`) is never queued, so it fires **no** `onMessagePending`; it reports `onError`, and the global `OfflineMessageListener` receives `onOutboxMessageFailed(message, reason)` carrying the exact reason. For `QUEUE_FULL` the error's `code` is `QUEUE_FULL`, with `details.maxPendingMessages`. -A refused or evicted message (`QUEUE_FULL`, `QUEUE_OVERFLOW`, `BYTE_OVERFLOW`) has **no durable row** — it does not appear in `getPendingMessages`, and `retryMessage` on its id does nothing. Capture these from the failure callback and handle them at that moment. Prefer the default `REJECT_NEWEST`, which never silently drops an already-queued message from another conversation. +A refused or evicted message (`QUEUE_FULL`, `QUEUE_OVERFLOW`, `BYTE_OVERFLOW`) has **no durable row** — it does not appear in `getPendingMessages`, and `retryMessage` or `discardLocalMessage` on its id rejects with `ERROR_OFFLINE_MESSAGE_NOT_FOUND`. Capture these from the failure callback and handle them at that moment. Prefer the default `REJECT_NEWEST`, which never silently drops an already-queued message from another conversation. ## Edits, Deletes, and Reactions Need a Connection From 217cea3a764e3e95f5d492629d387ba00a7757c3 Mon Sep 17 00:00:00 2001 From: rajdubey Date: Sun, 1 Nov 2026 20:06:29 +0530 Subject: [PATCH 04/12] docs(js-offline): overview and enable follow the Android changes Single-item reads, listener-less calls, outbox loss on logout, isOfflineEnabled conditions. Remove the settings-object and server-switch sections as internal. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XbguTshd7eq8N2XxFabgLN --- sdk/javascript/offline-enable.mdx | 20 +------------------- sdk/javascript/offline-overview.mdx | 6 +++--- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/sdk/javascript/offline-enable.mdx b/sdk/javascript/offline-enable.mdx index 095361822..7a5852d09 100644 --- a/sdk/javascript/offline-enable.mdx +++ b/sdk/javascript/offline-enable.mdx @@ -103,7 +103,7 @@ CometChat.init("APP_ID", appSetting).then( -Call `CometChat.isOfflineEnabled()`. It returns `true` only while offline support is actually running for the logged-in user — so it is `false` before anyone logs in, after logout, while the server has switched offline off for your app, and while the local store is still opening, even when you enabled it in `AppSettings`. Read it at the moment of each decision and keep no copy of the result. +Call `CometChat.isOfflineEnabled()`. It returns `true` only while offline support is actually running for the logged-in user — so it is `false` before anyone logs in (even when you enabled it in `AppSettings`), after logout or session expiry, while the local store is still opening, and when the encrypted cache cannot open. Read it at the moment of each decision and keep no copy of the result. @@ -123,20 +123,6 @@ console.log("Offline support active:", active); -## Enable from a settings object - -If you initialize with `CometChat.initFromSettings(settings)`, add an `offline` key under `chatSDK` in the settings object you pass. It accepts a boolean shorthand or a full configuration object, with the same rules as the code config. - -```json settings -{ - "appId": "APP_ID", - "region": "REGION", - "chatSDK": { - "offline": true - } -} -``` - ## Supported runtimes | Runtime | Data store | Media bytes | @@ -150,10 +136,6 @@ Where IndexedDB is missing or blocked (for example some private-browsing modes), **Electron setup.** Install the optional peer dependencies `electron` and `better-sqlite3-multiple-ciphers`, call `registerOfflineIpc()` from `@cometchat/chat-sdk-javascript/electron/main` in the main process, and `installCometChatBridge()` from `@cometchat/chat-sdk-javascript/electron/preload` in your preload script (with `contextIsolation: true`). Browser apps need nothing extra. -## Server switch - -CometChat can switch offline support off for your app from the server. The server can turn it off, never on: offline runs only when your config enables it **and** the server switch is on. When the switch is off, `isOfflineEnabled()` is `false`. - ## Next steps Offline support is now active for the logged-in user. Continue with [Show Cached Data](/sdk/javascript/offline-cached-data) to adopt the fetch listeners, and [Send While Offline](/sdk/javascript/offline-send) for the durable outbox. Tune caps, TTLs, media, and encryption from the [Configuration & API Reference](/sdk/javascript/offline-reference). diff --git a/sdk/javascript/offline-overview.mdx b/sdk/javascript/offline-overview.mdx index 104a2f151..2e734a126 100644 --- a/sdk/javascript/offline-overview.mdx +++ b/sdk/javascript/offline-overview.mdx @@ -34,7 +34,7 @@ Three ideas carry the whole feature. -Reads that you pass a fetch listener go through the local cache first. The SDK delivers the cached page immediately — even when it is empty — then fetches from the server and delivers the fresh result. See [Show Cached Data](/sdk/javascript/offline-cached-data). +Reads that you pass a fetch listener go through the local cache first. For list reads the SDK delivers the cached page immediately, even when it is empty. Single-item lookups (`getMessageDetails`, `getConversation`) deliver a cached result only when that item is already cached. The SDK then fetches from the server and delivers the fresh result. See [Show Cached Data](/sdk/javascript/offline-cached-data). A message sent while offline is saved to a durable outbox and reported as pending instead of failing. It survives page reloads and app restarts and is sent when the connection returns. See [Send While Offline](/sdk/javascript/offline-send). @@ -58,7 +58,7 @@ flowchart LR ## What changes in your app -With offline support enabled, your fetch callbacks fire **twice** on the read path and sends gain a pending stage. To receive the cached and pending results you pass the new listener objects — `MessageFetchListener`, `ConversationFetchListener`, `MessageSentListener`, and the others listed on each page — as an optional argument. Every method still returns its usual Promise, so existing code keeps working. With offline support off, every API behaves exactly as it did before. +With offline support enabled, your fetch callbacks fire **twice** on the read path and sends gain a pending stage. To receive the cached and pending results you pass the new listener objects — `MessageFetchListener`, `ConversationFetchListener`, `MessageSentListener`, and the others listed on each page — as an optional argument. Every method still returns its usual Promise, so existing code keeps working. A call without a listener receives only the server result, and a send without a listener is never queued. With offline support off, every API behaves exactly as it did before. ## Queued, not failed @@ -69,7 +69,7 @@ A send made with no connection fires `onMessagePending`, then **one** `onError` The SDK keeps a single local cache per app and user. - A page reload or app restart with a still-valid session reuses that cache, so the data is there instantly. -- Logging out, a session expiring, or logging in as a **different** user deletes the cache. You never clear it yourself — the SDK does it. +- Logging out, a session expiring, or logging in as a **different** user deletes the cache, including any unsent messages still in the outbox, which are lost. You never clear it yourself — the SDK does it. See [Stay in Sync](/sdk/javascript/offline-sync) for cache resets and logout behaviour. From 284a7bd685b96a6266137e3a155c1c6b87611ade Mon Sep 17 00:00:00 2001 From: rajdubey Date: Sun, 1 Nov 2026 20:06:29 +0530 Subject: [PATCH 05/12] docs(js-offline): cached data and send follow the Android changes Overlapping fetches, blocked users, one direction per reactions request, server-only filters, queued-not-failed wording, auto-send conditions, failure reasons, queue limits. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XbguTshd7eq8N2XxFabgLN --- sdk/javascript/offline-cached-data.mdx | 8 ++++---- sdk/javascript/offline-send.mdx | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/sdk/javascript/offline-cached-data.mdx b/sdk/javascript/offline-cached-data.mdx index 8f5524212..a06d26c9a 100644 --- a/sdk/javascript/offline-cached-data.mdx +++ b/sdk/javascript/offline-cached-data.mdx @@ -39,13 +39,13 @@ Pass a fetch listener as the optional argument and each read fires **up to two** The SDK delivers a page from the local cache **first**. It fires **even when the page is empty** — an empty cached page is indistinguishable from a cache miss, by design. Render it right away. -After a successful server response, the SDK delivers the server's copy for the **same slice** and writes it through to the cache before the callback. Reconcile your displayed list against it. +After a successful server response, the SDK delivers the server's copy for the **same slice** and writes it through to the cache before the callback. Reconcile your displayed list against it. (Blocked users are the exception: their server page is delivered but not written to the cache. The cached blocked-users list is kept current by the SDK itself.) Three rules follow from this: -- **Offline, the read ends with an error.** After the cached page, a read whose request gets no answer settles with `onError` carrying code `FAILED_TO_FETCH`. The call always settles — it never hangs. +- **Offline, the read ends with an error.** After the cached page, a read whose request gets no answer settles with `onError` carrying code `FAILED_TO_FETCH`. A real server error reaches `onError` unchanged. The call always settles. Start the next fetch on a request object only after the previous one has returned: a second fetch started while one is running gets no cached page, and for messages, threads, reactions and the notification feed it ends with `onError` with code `REQUEST_IN_PROGRESS`. - **The server phase never fires offline.** It fires only on a genuine server response. Never treat a cached page as "end of history"; an empty cached page only means the end of the *cached* window. - **The Promise still settles too.** Passing a listener does not replace the returned Promise: it settles on the server outcome, so it also **rejects** offline. If you use only the listener, add a `.catch()` to avoid an unhandled rejection. Consume the result from one channel, not both. @@ -218,7 +218,7 @@ request -For `ReactionsRequest`, `fetchPrevious` and `fetchNext` are cached independently — each direction keeps its own cached pages. Users, groups, members, banned members, reactions, and the notification feed are served from a recorded copy of each request (up to `maxSnapshotQueries` distinct requests, each kept for `snapshotTtlDays`), so a list you never opened online is empty offline. See the [Configuration & API Reference](/sdk/javascript/offline-reference). +For `ReactionsRequest`, use one request object per direction: an object caches only the first direction it fetches, and its cached phase stays empty in the other direction. Users, groups, members, banned members, reactions, and the notification feed are served from a recorded copy of each request (up to `maxSnapshotQueries` distinct requests, each kept for `snapshotTtlDays`), so a list you never opened online is empty offline. See the [Configuration & API Reference](/sdk/javascript/offline-reference). ## Read a single item @@ -296,7 +296,7 @@ Key your list by id so the two phases merge cleanly: update items that changed, The cache answers a read **only if every filter on it can be computed locally**. A single server-only filter makes the whole cached phase come back empty (or not fire at all), with no error and no warning, while the server phase still answers in full. The guiding rule is *better empty than wrong*. -The most common server-only filter is `setSearchKeyword`, which blanks the cached phase on both messages and conversations. On `MessagesRequest`, `hasLinks`, `updatesOnly` and `interactionGoalCompletedOnly` are server-only too. +The most common server-only filter is `setSearchKeyword`, which blanks the cached phase on any request that carries a search keyword (threads, reactions and the notification feed never carry one). Other examples are `hasLinks`, `updatesOnly` and `setInteractionGoalCompletedOnly` on `MessagesRequest`, setting a `UID` and a `GUID` together (one sender's messages in a group), and the status filters (`UsersRequest.setStatus`, `GroupMembersRequest.setStatus`) on the entity lists. `setUpdatedAfter` is a paging input, not a filter. An offline-critical screen must use only cache-supported filters — one innocuous builder flag can silently disable the cached phase for a whole screen. The full per-builder matrix is in the [Configuration & API Reference](/sdk/javascript/offline-reference#cache-supported-filters). diff --git a/sdk/javascript/offline-send.mdx b/sdk/javascript/offline-send.mdx index 3bc193427..bcfc5c898 100644 --- a/sdk/javascript/offline-send.mdx +++ b/sdk/javascript/offline-send.mdx @@ -39,7 +39,7 @@ CometChat.addOfflineMessageListener("LISTENER_ID", new CometChat.OfflineMessageL -With offline support enabled, a message you send while the device has no connection is **durably queued on the device** instead of failing. It keeps its place across a page reload or app restart and is sent automatically when the connection returns. This page shows the offline-aware send listener, how to read a message's status, how to inspect and recover the send queue, and the queue limits. +With offline support enabled, a message you send while the device has no connection is **durably queued on the device** instead of failing. It keeps its place across a page reload or app restart and is sent automatically once the connection is back and stable (with `autoSendPendingOnReconnect` on, the default). This page shows the offline-aware send listener, how to read a message's status, how to inspect and recover the send queue, and the queue limits. Offline support is **off by default** and requires SDK **4.3.0** or later. Queuing only happens once you [enable it](/sdk/javascript/offline-enable) **and** pass a `MessageSentListener`. A bare `await CometChat.sendMessage(message)` with no listener is never queued: offline, it rejects at once, as before. @@ -47,7 +47,7 @@ Offline support is **off by default** and requires SDK **4.3.0** or later. Queui ## Send a Message -Pass a `MessageSentListener` to `sendMessage`. Its lifecycle is `onMessagePending` first (the message is durably queued), then exactly one final outcome — `onMessageSent` (the server accepted it) or, for a real failure, `onError`. +Pass a `MessageSentListener` to `sendMessage`. Its lifecycle is normally `onMessagePending` first (the message is durably queued), then its outcome: `onMessageSent` (the server accepted it) or `onError`. Offline, `onError` is a queued-not-failed notice and `onMessageSent` still fires later on reconnect, so both can fire for one send (see below). If the message cannot be queued at all, for example because its media file could not be copied (code `PERMANENT`), only `onError` fires. @@ -121,13 +121,13 @@ The returned Promise is **not** replaced by the listener. It means "was it sent ## Queued, Not Failed -When the device is offline, the send still fires `onMessagePending`, then fires `onError` **once** with the code `FAILED_TO_FETCH`. This is a **notification, not a failure**: the message stays durably `PENDING`, and `onMessageSent` fires **later**, on reconnect, when it actually goes. **Do not re-send on that error.** +When the device is offline, the send still fires `onMessagePending`, then fires `onError` **once** with the code `FAILED_TO_FETCH`. This is a **notification, not a failure**: the message stays durably `PENDING`, and `onMessageSent` fires **later**, on reconnect, when it actually goes. **Do not re-send on that error.** This is the default behaviour, with `autoSendPendingOnReconnect` on. With it off, an offline send becomes `FAILED` (`TRANSIENT`) right away, and you recover it with `retryMessage`. So for a single send, `onError` and `onMessageSent` can **both** fire (offline, then reconnect), and `onError` does not always mean the message failed. Read the message's status to tell the two apart — keep the `localMessageId` from `onMessagePending`, then look the message up in the outbox: | `status` after `onError` | Meaning | What to do | |---|---|---| -| `PENDING` | Queued; not sent yet. Will auto-send on reconnect. | Show as sending/queued; nothing to do. | +| `PENDING` | Queued; not sent yet. Auto-sends on reconnect (with `autoSendPendingOnReconnect` on, the default). | Show as sending/queued; nothing to do. | | `FAILED` | A real failure; will not retry on its own. | Offer the user a manual retry (see below). | @@ -199,12 +199,12 @@ Every message exposes its offline state. `getLocalMessageId()` returns the SDK-g | Reason | When | |---|---| -| `TRANSIENT` | A temporary server error (for example 5xx or rate-limited), or no answer after five reconnect attempts. Retryable. | +| `TRANSIENT` | A temporary server error (5xx, or 429 rate-limited), no answer after five automatic reconnect attempts, or, with `autoSendPendingOnReconnect` off, no answer at all. Retryable. | | `PERMANENT` | The server rejected the message, the error was not recognized, or copying an attachment into the store timed out. A manual retry will not help. | | `QUEUE_FULL` | A new send was refused because the queue is full (count cap). | | `QUEUE_OVERFLOW` | An older queued message was evicted to admit a newer one (`DROP_OLDEST` only). | -| `BYTE_OVERFLOW` | A media send was refused because it exceeds the queue's byte budget. | -| `DISCARDED` | The message was removed before it could send (for example a cache clear or a conversation purge). | +| `BYTE_OVERFLOW` | A media send was refused because one of its files is larger than `maxMediaCacheSize`, the budget for the media cache. | +| `DISCARDED` | A queued message was removed before it could send: by `clearOfflineCache`, by leaving or deleting its conversation, or because its saved media file is gone. Reported with code `ERROR_OFFLINE_MESSAGE_DISCARDED` on its send listener, and on `onOutboxMessageFailed`. | ## Inspect the Queue @@ -282,7 +282,7 @@ CometChat.removeOfflineMessageListener("LISTENER_ID"); ## Retry and Discard -There is no automatic retry of a `FAILED` message — recovery is explicit and user-invoked. Each method accepts a message or its `localMessageId`. +There is no automatic retry of a `FAILED` message — recovery is explicit and user-invoked. A `FAILED` message stays in the outbox until you retry or discard it. Each method accepts a message or its `localMessageId`. - **`retryMessage(message, listener?)`** re-attempts one message. A `FAILED` message is reset to `PENDING` and sent; a `PENDING` message is attempted now rather than waiting for a reconnect. The optional `MessageSentListener` and the returned Promise receive that attempt's outcome. With no answer, it fires the not-connected notice once (`FAILED_TO_FETCH`) and the message stays `PENDING`. - **`retryAllFailedMessages(listener?)`** retries **transient** failures only; permanent ones are skipped. @@ -345,9 +345,9 @@ The outbox holds at most `maxPendingMessages` messages still trying to send (`PE | `PendingMessageOverflowPolicy` | Behaviour on overflow | |---|---| | `REJECT_NEWEST` (**default**) | The new send is refused and reported with reason `QUEUE_FULL`. Nothing already queued is lost. | -| `DROP_OLDEST` | The oldest `PENDING` message is evicted (reason `QUEUE_OVERFLOW`) to admit the new one. | +| `DROP_OLDEST` | The oldest `PENDING` message is evicted (reason `QUEUE_OVERFLOW`) to admit the new one. If there is no `PENDING` message to evict, the new send is refused with `QUEUE_FULL`. | -A media send that exceeds the queue's byte budget is refused with reason `BYTE_OVERFLOW`. +A media send with a file larger than `maxMediaCacheSize` is refused with reason `BYTE_OVERFLOW`; nothing is evicted to make room. A **refused** send (`QUEUE_FULL` / `BYTE_OVERFLOW`) is never queued, so it fires **no** `onMessagePending`; it reports `onError`, and the global `OfflineMessageListener` receives `onOutboxMessageFailed(message, reason)` carrying the exact reason. For `QUEUE_FULL` the error's `code` is `QUEUE_FULL`, with `details.maxPendingMessages`. From 115b3988bd065bd8fe0c0ec6ab71a4a60d207727 Mon Sep 17 00:00:00 2001 From: rajdubey Date: Sun, 1 Nov 2026 20:06:29 +0530 Subject: [PATCH 06/12] docs(js-offline): sync, media, encryption and reference follow the Android changes Sync triggers and conversation removal, media budget, random key and reset counts, clamp rules, forceSyncNow, error codes. Drop debugTracing from the reference. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XbguTshd7eq8N2XxFabgLN --- sdk/javascript/offline-encryption.mdx | 6 +++--- sdk/javascript/offline-media-caching.mdx | 2 +- sdk/javascript/offline-reference.mdx | 27 +++++++++++++----------- sdk/javascript/offline-sync.mdx | 12 +++++++---- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/sdk/javascript/offline-encryption.mdx b/sdk/javascript/offline-encryption.mdx index 431b7f0f1..a140c0b8e 100644 --- a/sdk/javascript/offline-encryption.mdx +++ b/sdk/javascript/offline-encryption.mdx @@ -95,7 +95,7 @@ Switching `encryption` on or off between launches rebuilds the cache in the new ## Key storage -You do not supply or manage the encryption key. In a browser, the SDK generates a per-user data key, wraps it with a **non-extractable** WebCrypto key, and keeps the wrapped key in the same IndexedDB as the data — so a browser clearing site data can never leave ciphertext without its key. In Electron the key is sealed by the OS through `safeStorage`. The key is never returned by any API. If it is lost or no longer opens the cache, the cache resets and rebuilds — see [Cache resets](#cache-resets-on-key-loss). +You do not supply or manage the encryption key. In a browser, the SDK generates a random per-user data key, wraps it with a **non-extractable** WebCrypto key, and keeps the wrapped key in the same IndexedDB as the data — so a browser clearing site data can never leave ciphertext without its key. In Electron the key is sealed by the OS through `safeStorage`. The key is never returned by any API. If it is lost or no longer opens the cache, the cache resets and rebuilds — see [Cache resets](#cache-resets-on-key-loss). In a browser the key lives in software next to the data (`keyStorage: "SOFTWARE"`). That protects against casual disk inspection and partial data copies, but not against code running in your page's origin. @@ -157,7 +157,7 @@ On Electron the whole database is encrypted with SQLCipher; only the media files ## Cache resets on key loss -If the key can no longer decrypt the stored cache, the SDK wipes the local cache and rebuilds it **encrypted** — it never silently falls back to plaintext for a cache that could be encrypted. Any unsent messages still queued in the outbox at that point are lost and each is failed with `DISCARDED`. +If the key can no longer decrypt the stored cache, the SDK wipes the local cache and rebuilds it **encrypted** — it never silently falls back to plaintext for a cache that could be encrypted. Any unsent messages still queued in the outbox at that point are lost. They lived inside the unreadable store, so they usually cannot be counted: `pendingMessagesLost` is then `-1` (`CometChat.OFFLINE_PENDING_COUNT_UNKNOWN`), not zero. Each lost message that could still be read fails with `DISCARDED` on `onOutboxMessageFailed`. Register an `OfflineCacheResetListener` to be told when this happens: @@ -188,4 +188,4 @@ CometChat.addOfflineCacheResetListener( -The encryption-related reasons are `KEY_LOST`, `WRONG_KEY`, `CORRUPTED`, and `ENCRYPTION_CHANGED` (you turned encryption on or off between launches). See [Stay in Sync](/sdk/javascript/offline-sync#when-the-cache-resets) for the full cache-reset flow, and the [API Reference](/sdk/javascript/offline-reference) for every field and enum. +The encryption-related reasons are `KEY_LOST`, `WRONG_KEY`, `CORRUPTED`, `ESCALATED`, and `ENCRYPTION_CHANGED` (you turned encryption on or off between launches). See [Stay in Sync](/sdk/javascript/offline-sync#when-the-cache-resets) for the full cache-reset flow, and the [API Reference](/sdk/javascript/offline-reference) for every field and enum. diff --git a/sdk/javascript/offline-media-caching.mdx b/sdk/javascript/offline-media-caching.mdx index a7e57418f..f4d9123f7 100644 --- a/sdk/javascript/offline-media-caching.mdx +++ b/sdk/javascript/offline-media-caching.mdx @@ -227,7 +227,7 @@ Remove the listener with `CometChat.removeMediaDownloadListener("MEDIA_LISTENER" ## Media cache size limit -Cached media bytes are bounded by `maxMediaCacheSize` (default **500 MB**; `0` means unlimited). When the cache exceeds the limit, the least recently accessed media is deleted first until it is back under budget. Media belonging to un-sent messages and in-progress uploads is never evicted. +Cached media bytes are bounded by `maxMediaCacheSize` (default **500 MB**; `0` means unlimited). When the cache exceeds the limit, the least recently accessed media is deleted first until it is back under budget. Media belonging to un-sent messages and in-progress uploads is never evicted and does not count toward the budget. A single outgoing file larger than the budget is refused (a send fails with `BYTE_OVERFLOW`). Media is stored in OPFS (browser) or as files on disk (Electron); the location is not configurable. Eviction runs silently in the background — there is no cache-limit callback. To reason about size, use `CometChat.getCacheSize()` and the configured limits. See the [reference](/sdk/javascript/offline-reference) for the full set of options. diff --git a/sdk/javascript/offline-reference.mdx b/sdk/javascript/offline-reference.mdx index 458b19775..abdac44ed 100644 --- a/sdk/javascript/offline-reference.mdx +++ b/sdk/javascript/offline-reference.mdx @@ -32,22 +32,21 @@ Pass these keys in the object given to `AppSettingsBuilder.setOfflineConfig(...) | Key | Type | Default | Range / clamp | Effect | |---|---|---|---|---| | `enabled` | `boolean` | `false` | only an explicit `true` enables | Master gate for the whole offline engine. With it off the SDK behaves exactly as before. | -| `cacheDurationDays` | `number` | `0` (unlimited) | negative treated as `0` | Message retention TTL in days. `0` = no age-based prune; eviction is by the other caps. | -| `maxStorageSize` | `number` (bytes) | `262144000` (250 MB) | `0` = unlimited | Byte ceiling for cached content, counted before encryption and excluding unsent messages. Over budget, the SDK removes recorded list reads first (LRU), then whole conversations (LRU; a conversation with unsent messages is kept), then the oldest saved and my-threads entries. | -| `maxMessagesPerConversation` | `number` | `500` | clamped `[1, 1000]`; `0`/over → `1000` | Per-conversation cached-message window. | -| `maxConversations` | `number` | `100` | clamped `[1, 1000]`; `0`/over → `1000` | Cached-conversation cap (whole-conversation LRU). Also the page size of the reconnect conversation refresh. | -| `maxPendingMessages` | `number` | `50` | clamped `[1, 100]`; `0`/over → `100` | Pending-send outbox capacity. | +| `cacheDurationDays` | `number` | `0` (unlimited) | `<= 0` = unlimited | Message retention TTL in days. `0` = no age-based prune; eviction is by the other caps. | +| `maxStorageSize` | `number` (bytes) | `262144000` (250 MB) | `<= 0` = unlimited | Byte ceiling for cached content, counted before encryption and excluding unsent messages. Over budget, the SDK removes recorded list reads first (LRU), then whole conversations (LRU; a conversation with unsent messages is kept), then the oldest saved and my-threads entries. | +| `maxMessagesPerConversation` | `number` | `500` | clamped `[1, 1000]`; `<= 0` or over → `1000` | Per-conversation cached-message window. | +| `maxConversations` | `number` | `100` | clamped `[1, 1000]`; `<= 0` or over → `1000` | Cached-conversation cap (whole-conversation LRU). Also the page size of the reconnect conversation refresh. | +| `maxPendingMessages` | `number` | `50` | clamped `[1, 100]`; `<= 0` or over → `100` | Pending-send outbox capacity. | | `autoSendPendingOnReconnect` | `boolean` | `true` | — | Auto-flush queued `PENDING` sends on reconnect. Gates only the outbox flush; the catch-up sync always runs. | | `pendingQueueOverflowPolicy` | `PendingMessageOverflowPolicy` | `REJECT_NEWEST` | unknown → default | What the outbox does when it is full. | -| `maxMediaCacheSize` | `number` (bytes) | `524288000` (500 MB) | `0` = unlimited | Media-only byte budget (LRU). | +| `maxMediaCacheSize` | `number` (bytes) | `524288000` (500 MB) | `<= 0` = unlimited | Media-only byte budget (LRU). | | `mediaCaching` | `MediaCachingMode` | `AUTO` | unknown → `AUTO` | Receive-side media mode. Fixed at init. | | `encryption` | `boolean` | `false` | only an explicit `true` enables | Encrypt the cache at rest. See [Encrypt the Cache](/sdk/javascript/offline-encryption). | | `encryptionFailureBehavior` | `EncryptionFailureBehavior` | `ONLINE_ONLY` | anything but `PLAINTEXT_FALLBACK` → `ONLINE_ONLY` | What happens when encryption cannot run at all. | | `maxSnapshotQueries` | `number` | `100` | `0`/negative → `100` | Most distinct entity-list reads (users, groups, members, banned members, reactions, notification feed, threads) kept for offline replay; LRU. | | `snapshotTtlDays` | `number` | `7` | `0`/negative → `7` | Days a recorded list read stays replayable. Past it, the read is empty offline until viewed online again. | -| `debugTracing` | `boolean` | `false` | — | Dev-only diagnostic trace. Leave off in production. | -Count caps are always bounded and have no "unlimited" setting. Byte and day caps treat `0` as unlimited. +Count caps are always bounded and have no "unlimited" setting. Byte and day caps treat `<= 0` as unlimited. ## Public methods on CometChat @@ -60,7 +59,7 @@ Count caps are always bounded and have no "unlimited" setting. Byte and day caps | `retryMessage(message \| localMessageId, listener?)` | `Promise` | Retry one message. | | `retryAllFailedMessages(listener?)` | `Promise` | Retry every transiently failed message. | | `discardLocalMessage(message \| localMessageId, { onSuccess?, onError? }?)` | `Promise` | Remove one unsent message from the outbox. | -| `forceSyncNow(callback?)` | `void` | Trigger a catch-up sync immediately; the callback reports whether it started. | +| `forceSyncNow(callback?)` | `void` | Request a catch-up sync. Rejected if a sync is already running or one ran within the 10-second cooldown, and a no-op when offline is not active. Without a callback a rejection is dropped silently; with one, the callback reports whether it started and why not. | | `getSyncState()` | `string` | `"IDLE"` or `"SYNCING"`. | | `getLastSyncTimestamp()` | `Promise` | Server time of the last successful sync, in epoch seconds. | | `clearOfflineCache()` | `Promise` | Wipe the local cache (and fail any unsent messages). | @@ -70,6 +69,8 @@ Count caps are always bounded and have no "unlimited" setting. Byte and day caps | `getCachedMediaObjectURL(attachment)` | `Promise` | A `blob:` URL for cached bytes, or `null`. Revoke it when done. | | `getExistingUploadFileRequest(receiverId, receiverType, parentMessageId?)` | `Promise` | Re-attach to an in-progress offline upload. | +On the returned `UploadFileRequest`, `getItems()` lists each file's status and `retryAttachment(fileId)` (optionally with an `UploadFileListener`) retries one failed file. New batches are created with `CometChat.createUploadFileRequest`; see [Upload Files](/sdk/javascript/upload-files). + ### Listener registration | Method | Listener | @@ -111,7 +112,7 @@ Every listener is constructed with an object of callbacks, for example `new Come | `MessageSentListener` | `onMessagePending(message)`, `onMessageSent(message)`, `onError(error)` | | `OfflineMessageListener` | `onOutboxMessagePending(message)`, `onOutboxMessageSent(message)`, `onOutboxMessageFailed(message, reason)` | | `SyncListener` | `onConversationSyncStarted()`, `onConversationSyncCompleted(conversations)`, `onConversationSyncError(error)`, `onMessageSyncStarted()`, `onMessageSyncCompleted(messages, outcome)`, `onMessageSyncError(error)` | -| `OfflineCacheResetListener` | `onOfflineCacheReset(reason, pendingMessagesLost)` | +| `OfflineCacheResetListener` | `onOfflineCacheReset(reason, pendingMessagesLost)`; `pendingMessagesLost` is `-1` (`CometChat.OFFLINE_PENDING_COUNT_UNKNOWN`) when the number is unknown | | `MediaCallbackListener` | `onSuccess(localPath)`, `onProgress(percent)`, `onError(error)` | | `MediaDownloadListener` | `onMediaDownloaded(messageId, attachmentId, localPath)`, `onMediaDownloadFailed(messageId, attachmentId, error)` | | `MessageListener` | `onMessagesSynced(messages)`, `onMediaDownloaded`, `onMediaDownloadFailed` (offline additions to the existing listener) | @@ -158,6 +159,8 @@ The offline-related codes the SDK raises: | `ERROR_OFFLINE_MESSAGE_NOT_FOUND` | `retryMessage` / `discardLocalMessage` found no unsent message with the given local id. Constant: `CometChat.ERROR_OFFLINE_MESSAGE_NOT_FOUND`. | | `ERROR_OFFLINE_MESSAGE_PERSIST_FAILED` | The local cache was briefly unavailable, so `getPendingMessages`, `retryMessage`, `retryAllFailedMessages` or `discardLocalMessage` could not read it. Try again. | | `QUEUE_FULL` | A send was refused because the outbox is full; `details.maxPendingMessages` holds the cap. | +| `PERMANENT` / `BYTE_OVERFLOW` | A media send was refused before it was queued: its file could not be copied (`PERMANENT`), or one of its files is larger than `maxMediaCacheSize` (`BYTE_OVERFLOW`). | +| `REQUEST_IN_PROGRESS` | A fetch was started on a messages, threads, reactions or notification-feed request while an earlier fetch on the same object is still running. | | `ERROR_OFFLINE_MEDIA_NO_URL` | `downloadMedia` was given no attachment, or an attachment without a URL. | | `ERROR_JSON_EXCEPTION` | A sync request got a 2xx answer with an unreadable body; `details.httpStatus` holds the status. | | `USER_NOT_LOGED_IN` | `forceSyncNow(callback)` was called with nobody logged in (spelling as the SDK returns it). | @@ -168,10 +171,10 @@ A cached read answers only if **every** filter on the request can be computed lo | Request | Answered from the cache | Server-only (empties the cached phase) | |---|---|---| -| `MessagesRequest` | categories, types, parent message id, hide replies, hide deleted messages, tags, hide quoted messages, has mentions, mentioned UIDs, unread, has attachments, attachment types, has reactions, hide messages from blocked users | `searchKeyword`, `hasLinks`, `updatesOnly`, `interactionGoalCompletedOnly` | +| `MessagesRequest` | categories, types, parent message id, hide replies, hide deleted messages, tags, hide quoted messages, has mentions, mentioned UIDs, unread, has attachments, attachment types, has reactions, hide messages from blocked users | `searchKeyword`, `hasLinks`, `updatesOnly`, `interactionGoalCompletedOnly`, `UID` and `GUID` set together | | `ConversationsRequest` | conversation type, unread, tags, user tags, group tags, hide/only agentic, with-blocked-info, include blocked users, pinned-by | `searchKeyword` | -Enrichment flags (`withTags`, `withParent`, mentions-with-tag/blocked info, `withUserAndGroupTags`) decorate a result without narrowing it and never gate the cache. Entity lists (users, groups, members, banned members, reactions, notification feed, threads) are replayed from recorded requests rather than filtered locally. +Enrichment flags (`withTags`, `withParent`, mentions-with-tag/blocked info, `withUserAndGroupTags`) decorate a result without narrowing it and never gate the cache. Entity lists (users, groups, members, banned members, reactions, notification feed, threads) are replayed from recorded requests rather than filtered locally. A request with a search keyword is not recorded, and neither is a `UsersRequest` or `GroupMembersRequest` with a status filter, so their cached phase is empty. Pinned and saved message reads are answered from their own lists, not from the message cache. ## Deprecated overloads and replacements diff --git a/sdk/javascript/offline-sync.mdx b/sdk/javascript/offline-sync.mdx index 8e7942f3d..b238d2ef8 100644 --- a/sdk/javascript/offline-sync.mdx +++ b/sdk/javascript/offline-sync.mdx @@ -43,12 +43,12 @@ Everything on this page is a no-op unless offline support is active for the logg ## How catch-up works -A sync runs automatically when the connection settles after a reconnect. It never runs on a brief connect-then-drop flap, and overlapping triggers coalesce into a single sweep. It proceeds in two phases, in order: +A sync runs automatically whenever the connection settles, including the first connect after login or launch. 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. It proceeds in two phases, in order: 1. **Conversation phase** — the conversation list is fetched as one fresh page (up to `maxConversations`). 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. 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. +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 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 @@ -99,7 +99,7 @@ CometChat.addSyncListener( |---|---| | `onConversationSyncStarted()` | The conversation refresh began. | | `onConversationSyncCompleted(conversations)` | The refresh succeeded; `conversations` is the full conversation page. | -| `onConversationSyncError(error)` | A transport or parse failure; the cache is left untouched and the refresh retries on the next reconnect. | +| `onConversationSyncError(error)` | The refresh failed. A transport failure leaves the cache untouched; a malformed response removes nothing. Either way the refresh retries on the next reconnect. | | `onMessageSyncStarted()` | The message changelog sweep began. | | `onMessageSyncCompleted(messages, outcome)` | The sweep finished; `messages` carries the new and changed messages. | | `onMessageSyncError(error)` | The sweep failed; it resumes on the next reconnect. | @@ -111,7 +111,7 @@ The two phases are independent — a conversation-phase error does not stop the | `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; it drains one page per reconnect. | +| `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")`. Registrations are kept across login, logout and restarts, so register once. @@ -159,6 +159,10 @@ CometChat.addMessageListener( + +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. From 904be0639866e93b6d88282a99728bec30f7f0ef Mon Sep 17 00:00:00 2001 From: rajdubey Date: Sun, 1 Nov 2026 20:09:45 +0530 Subject: [PATCH 07/12] docs(js): offline notes on send, upload, receive, connection status and AI moderation pages Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XbguTshd7eq8N2XxFabgLN --- sdk/javascript/ai-moderation.mdx | 2 ++ sdk/javascript/connection-status.mdx | 2 ++ sdk/javascript/receive-message.mdx | 9 +++++ sdk/javascript/send-message.mdx | 53 ++++++++++++++++++++++++++++ sdk/javascript/upload-files.mdx | 1 + 5 files changed, 67 insertions(+) diff --git a/sdk/javascript/ai-moderation.mdx b/sdk/javascript/ai-moderation.mdx index 289252c26..fe31b2d9a 100644 --- a/sdk/javascript/ai-moderation.mdx +++ b/sdk/javascript/ai-moderation.mdx @@ -77,6 +77,8 @@ sequenceDiagram | 3. AI Processing | Moderation service analyzes the content | | 4. Result Event | `onMessageModerated` event fires with final status | +The `PENDING` here is the moderation status (`getModerationStatus()`). It is separate from the send status: with offline support enabled and a `MessageSentListener`, `onMessagePending` fires first, before the message reaches the server, and `onError` can mean the message is queued, not failed. See [Send While Offline](/sdk/javascript/offline-send). + ## Supported Message Types Moderation is triggered **only** for the following message types: diff --git a/sdk/javascript/connection-status.mdx b/sdk/javascript/connection-status.mdx index 093a09d80..a5623e77c 100644 --- a/sdk/javascript/connection-status.mdx +++ b/sdk/javascript/connection-status.mdx @@ -5,6 +5,8 @@ description: "Monitor CometChat WebSocket connection status in JavaScript apps w CometChat SDK provides you with a mechanism to get real-time status of the connection to CometChat web-socket servers. +With offline support enabled, your app keeps working without a connection: cached chats are shown instantly, and messages you send with a `MessageSentListener` are queued and delivered automatically on reconnect. See [Show Cached Data](/sdk/javascript/offline-cached-data) and [Send While Offline](/sdk/javascript/offline-send). + Connection Status provides you with the below 3 methods to get the status of the connection to CometChat web-socket servers: | Delegate Method | Information | diff --git a/sdk/javascript/receive-message.mdx b/sdk/javascript/receive-message.mdx index 7e10973ab..3826d8368 100644 --- a/sdk/javascript/receive-message.mdx +++ b/sdk/javascript/receive-message.mdx @@ -105,6 +105,8 @@ CometChat.removeMessageListener("UNIQUE_LISTENER_ID"); event on the other devices. +With offline support enabled, a registered `MessageListener` also receives `onMessagesSynced(messages)` after the SDK catches up on reconnect. It carries updates to already-cached messages that changed while offline: reactions, edits, deletes and receipts. Messages that arrived while offline still come one at a time through the usual `onTextMessageReceived`, `onMediaMessageReceived` and other typed callbacks, with `getSource()` set to `SYNC`. See [Stay in Sync](/sdk/javascript/offline-sync). + ## Unread Messages Fetch unread messages by adding `setUnread(true)` to the builder. Use `fetchPrevious()` to retrieve them. @@ -224,6 +226,8 @@ The `fetchPrevious()` method returns an array of [`BaseMessage`](/sdk/reference/ ## Search Messages +With offline support on, this filter is answered by the server only; the cached page is empty. + Add `setSearchKeyword()` to the builder to filter messages by keyword. @@ -464,6 +468,11 @@ CometChat.getMessageDetails(messageId).then( Returns a [`BaseMessage`](/sdk/reference/messages#basemessage) object (which may be a [`TextMessage`](/sdk/reference/messages#textmessage), [`MediaMessage`](/sdk/reference/messages#mediamessage), or [`CustomMessage`](/sdk/reference/messages#custommessage)). + +## Offline Behaviour + +With offline support enabled, `fetchPrevious()` and `fetchNext()` accept a `MessageFetchListener`: `onCachedMessages` delivers a page from the on-device cache first (even when empty), then `onServerMessages` delivers the reconciled server copy. The cache answers a page **only when every filter on the request is locally computable**; a single server-only filter makes the cached page empty with no error, while the server phase still answers in full. Offline, the fetch ends with `onError` with code `FAILED_TO_FETCH` after the cached page. `getMessageDetails()` accepts a `SingleMessageFetchListener` and delivers a cached copy only when the message is cached. See [Show Cached Data](/sdk/javascript/offline-cached-data) and the [filter table](/sdk/javascript/message-filtering#offline-behaviour). + --- ## Next Steps diff --git a/sdk/javascript/send-message.mdx b/sdk/javascript/send-message.mdx index 14cc60cd4..753ab57cd 100644 --- a/sdk/javascript/send-message.mdx +++ b/sdk/javascript/send-message.mdx @@ -159,6 +159,51 @@ The [`TextMessage`](/sdk/reference/messages#textmessage) class constructor takes On success, `sendMessage()` method returns a [`TextMessage`](/sdk/reference/messages#textmessage) | [`MediaMessage`](/sdk/reference/messages#mediamessage) | [`CustomMessage`](/sdk/reference/messages#custommessage) | [`BaseMessage`](/sdk/reference/messages#basemessage) object depending on the input message type. +### Send While Offline + +With [offline support](/sdk/javascript/offline-send) enabled, pass a `MessageSentListener` as the second argument to queue the message when the device has no connection. The same listener works with `sendMediaMessage()` and `sendCustomMessage()`. + + + +```typescript +CometChat.sendMessage( + textMessage, + new CometChat.MessageSentListener({ + onMessagePending: (message: CometChat.BaseMessage) => { + // Queued on the device; message.getStatus() === "PENDING" + }, + onMessageSent: (message: CometChat.BaseMessage) => { + // The server accepted the message + }, + onError: (error: CometChat.CometChatException) => { + // Offline, this also fires as a queued-not-failed notice (code FAILED_TO_FETCH) + }, + }) +).catch(() => {}); +``` + + +```javascript +CometChat.sendMessage( + textMessage, + new CometChat.MessageSentListener({ + onMessagePending: (message) => { + // Queued on the device; message.getStatus() === "PENDING" + }, + onMessageSent: (message) => { + // The server accepted the message + }, + onError: (error) => { + // Offline, this also fires as a queued-not-failed notice (code FAILED_TO_FETCH) + }, + }) +).catch(() => {}); +``` + + + +Offline, `onMessagePending` fires first, then `onError` with code `FAILED_TO_FETCH` as a not-connected notice, and `onMessageSent` follows once the message is sent on reconnect. In `onError`, look the message up with `getPendingMessages()` and check its `getStatus()`: `PENDING` means queued, `FAILED` means it failed. A send without a `MessageSentListener` is never queued; offline, its Promise rejects. See [Send While Offline](/sdk/javascript/offline-send). + ## Media Message Send images, videos, audio, or files using `CometChat.sendMediaMessage()`. @@ -437,6 +482,10 @@ The [`MediaMessage`](/sdk/reference/messages#mediamessage) class constructor tak On success, `sendMediaMessage()` returns a [MediaMessage](/sdk/reference/messages#mediamessage) object. + +With offline support enabled, pass a `MessageSentListener` to `sendMediaMessage()` to queue the message while offline. `onError` can then mean "queued", not "failed": check the message's status. See [Send While Offline](/sdk/javascript/offline-send). + + ### Add Caption Add text along with the media: @@ -917,6 +966,10 @@ The [`CustomMessage`](/sdk/reference/messages#custommessage) class constructor t On success, `sendCustomMessage()` returns a [CustomMessage](/sdk/reference/messages#custommessage) object. + +With offline support enabled, pass a `MessageSentListener` to `sendCustomMessage()` to queue the message while offline. `onError` can then mean "queued", not "failed": check the message's status. See [Send While Offline](/sdk/javascript/offline-send). + + ### Add Tags ```javascript diff --git a/sdk/javascript/upload-files.mdx b/sdk/javascript/upload-files.mdx index ce4153c54..4569b76d0 100644 --- a/sdk/javascript/upload-files.mdx +++ b/sdk/javascript/upload-files.mdx @@ -496,6 +496,7 @@ The SDK handles a few transport edge cases for you: - **Stalled uploads** — if a file makes no progress for 30 seconds, its upload is aborted and reported through `onFileFailure` with `ERR_UPLOAD_STALLED`. Retry it with `retryAttachment()`. - **Expired upload URLs** — the pre-signed upload URL for each file is valid for 15 minutes. `retryAttachment()` automatically requests a fresh URL if the original has expired, so retries keep working even after a long delay. If that fresh request is permanently denied (e.g. permissions changed), the file moves to **rejected** (`onFileError`) rather than looping as a failure. - **Storage errors** — if storage rejects the upload, the failure surfaces through `onFileFailure` with `ERR_S3_UPLOAD_FAILED` and, where available, the storage's own message. +- **Restarts** — with offline support enabled, an upload batch and its files are saved on the device. An in-progress batch survives a page reload or app restart, but it never resumes on its own: fetch it with `CometChat.getExistingUploadFileRequest` and retry each failed file. A media message you send with a `MessageSentListener` while offline is queued and sent on reconnect; `onError` there can mean "queued", not "failed". See [Send While Offline](/sdk/javascript/offline-send). ## Error handling From 9d360508717e98030a24caae5cdd6f01d2761c20 Mon Sep 17 00:00:00 2001 From: rajdubey Date: Sun, 1 Nov 2026 20:09:46 +0530 Subject: [PATCH 08/12] docs(js): offline listeners section and onMessagesSynced note on the real-time listeners page Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XbguTshd7eq8N2XxFabgLN --- sdk/javascript/all-real-time-listeners.mdx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/sdk/javascript/all-real-time-listeners.mdx b/sdk/javascript/all-real-time-listeners.mdx index d6749df14..3bf02c431 100644 --- a/sdk/javascript/all-real-time-listeners.mdx +++ b/sdk/javascript/all-real-time-listeners.mdx @@ -3,7 +3,7 @@ title: "All Real Time Listeners" description: "Use CometChat real-time listeners for user presence, groups, messages, calls, AI assistant events, and ongoing calls." --- -CometChat provides the following listeners. +CometChat provides the following listeners. With offline support enabled, four more listeners report what the SDK does on the device; see [Offline listeners](#offline-listeners). 1. [User Listener](/sdk/javascript/all-real-time-listeners#user-listener) 2. [Group Listener](/sdk/javascript/all-real-time-listeners#group-listener) @@ -291,6 +291,8 @@ CometChat.removeGroupListener(listenerID); Receive events for incoming messages, typing indicators, read/delivery receipts, message edits/deletes, reactions, and moderation results. +With offline support enabled, `MessageListener` also receives `onMessagesSynced` after the SDK catches up on reconnect. It carries updates to already-cached messages that changed while offline (reactions, edits, deletes and receipts); messages that arrived while offline still come through the typed callbacks such as `onTextMessageReceived`. See [Stay in Sync](/sdk/javascript/offline-sync). + | Method | Information | | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **onTextMessageReceived(message: CometChat.TextMessage)** | This event is triggered when a Text Message is received. | @@ -960,6 +962,18 @@ Remove the listener when no longer needed: CometChat.removeConnectionListener("UNIQUE_LISTENER_ID"); ``` + +## Offline Listeners + +With offline support enabled, these listeners report what the SDK does on the device rather than events from the server. Each is documented with examples on its own page. + +| Listener | What it reports | Details | +|---|---|---| +| `OfflineMessageListener` | Every outbox transition, app-wide: a message queued, sent or failed | [Send While Offline](/sdk/javascript/offline-send) | +| `SyncListener` | Progress and outcome of each catch-up sync | [Stay in Sync](/sdk/javascript/offline-sync) | +| `OfflineCacheResetListener` | The on-device cache was rebuilt, for example after the encryption key was lost | [Encrypt the Cache](/sdk/javascript/offline-encryption) | +| `MediaDownloadListener` | A background media download finished or failed | [Media Caching](/sdk/javascript/offline-media-caching) | + --- ## Next Steps From 376256276aea523c0807b64715e98e7d4856cd8b Mon Sep 17 00:00:00 2001 From: rajdubey Date: Sun, 1 Nov 2026 20:09:46 +0530 Subject: [PATCH 09/12] docs(js): offline behaviour on message filtering, threads, mentions and reactions pages Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XbguTshd7eq8N2XxFabgLN --- sdk/javascript/mentions.mdx | 10 +++++++ sdk/javascript/message-filtering.mdx | 37 ++++++++++++++++++++++++++ sdk/javascript/reactions.mdx | 19 ++++++++++--- sdk/javascript/thread-subscription.mdx | 10 +++++++ sdk/javascript/threaded-messages.mdx | 10 +++++++ 5 files changed, 83 insertions(+), 3 deletions(-) diff --git a/sdk/javascript/mentions.mdx b/sdk/javascript/mentions.mdx index 0ce740378..a2a779f66 100644 --- a/sdk/javascript/mentions.mdx +++ b/sdk/javascript/mentions.mdx @@ -406,6 +406,16 @@ message.getMentionedUsers(); The getMentionedUsers() method returns an array of [`User`](/sdk/reference/entities#user) objects. + +## Offline Behaviour + +With offline support enabled, these mention fetches run in two phases: `onCachedMessages` delivers the cached page first (even when empty), then `onServerMessages` delivers the reconciled server copy. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. To queue a mentioned message sent while offline, pass a `MessageSentListener` to `sendMessage()`; `onError` can then mean "queued", not "failed" (see [Send While Offline](/sdk/javascript/offline-send)). See [Show Cached Data](/sdk/javascript/offline-cached-data). + +| Builder method | Offline behaviour | +| --- | --- | +| `mentionsWithTagInfo()` | Cached, without the tag info of the mentioned users | +| `mentionsWithBlockedInfo()` | Cached, without the blocked info of the mentioned users | + --- ## Next Steps diff --git a/sdk/javascript/message-filtering.mdx b/sdk/javascript/message-filtering.mdx index 44845c5cf..401aea7f8 100644 --- a/sdk/javascript/message-filtering.mdx +++ b/sdk/javascript/message-filtering.mdx @@ -451,6 +451,8 @@ Useful for syncing messages with a local database — fetch only what's changed ## Updated messages only +With offline support on, this filter is answered by the server only; the cached page is empty. + Use `updatesOnly(true)` with `setUpdatedAfter()` to fetch only updated messages (not newly received ones). This method must be used together with `setUpdatedAfter()`. @@ -1136,6 +1138,8 @@ When `withTags(true)` is set, each message's `tags` field will be populated. Acc ## Messages with links +With offline support on, this filter is answered by the server only; the cached page is empty. + Use `hasLinks(true)` to fetch only messages containing links. Default is `false`. @@ -1545,6 +1549,39 @@ let messagesRequest = new CometChat.MessagesRequestBuilder() The response contains media message objects filtered to the specified attachment types. + +## Offline Behaviour + +With offline support enabled, `fetchPrevious()` / `fetchNext()` accept a `MessageFetchListener` and answer from the on-device cache first (`onCachedMessages`, even when empty) and then from the server (`onServerMessages`). The cache answers a page **only when every filter on the request is locally computable**: a single server-only filter makes the cached page empty with no error, while the server phase still answers in full. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. See [Show Cached Data](/sdk/javascript/offline-cached-data). + +| Builder method | Offline behaviour | +| --- | --- | +| `setUnread()` | Cached | +| `hideMessagesFromBlockedUsers()` | Cached | +| `updatesOnly()` | Server only — cached page is empty | +| `setCategory()` / `setCategories()` | Cached | +| `setType()` / `setTypes()` | Cached | +| `setParentMessageId()` | Cached | +| `hideReplies()` | Cached | +| `hideDeletedMessages()` | Cached | +| `hideQuotedMessages()` | Cached | +| `setTags()` | Cached | +| `withTags()` | Cached | +| `hasLinks()` | Server only — cached page is empty | +| `setSearchKeyword()` | Server only — cached page is empty | +| `setInteractionGoalCompletedOnly()` | Server only — cached page is empty | +| `hasAttachments()` | Cached | +| `hasReactions()` | Cached | +| `setAttachmentTypes()` | Cached | +| `hasMentions()` | Cached | +| `setMentionedUIDs()` | Cached | +| `mentionsWithTagInfo()` / `mentionsWithBlockedInfo()` | Cached, without the tag or blocked info of the mentioned users | +| `setPinned()` / `setSaved()` | Answered from the pinned or saved list | + +`setPinned` and `setSaved` are served from separate pinned and saved lists, not the message cache, so the other filters in this table do not apply to a pinned or saved read. + +`setMessageId`, `setUpdatedAfter`, `setLimit`, `setUID` and `setGUID` are scope and paging inputs, not filters, and do not change what the cache can answer. Setting both `setUID` and `setGUID` together (one sender's messages in a group) is answered by the server only. A read with no UID, GUID or parent message id (a cross-conversation read) is answered from the cache only when it also uses `setUnread`, `hasAttachments`, `hasMentions`, `setAttachmentTypes`, mentions of only the logged-in user, or only the `call` and `custom` categories. + ## Next Steps diff --git a/sdk/javascript/reactions.mdx b/sdk/javascript/reactions.mdx index 9e60b6b80..4cdaf8918 100644 --- a/sdk/javascript/reactions.mdx +++ b/sdk/javascript/reactions.mdx @@ -10,7 +10,7 @@ description: "Add, remove, fetch, and listen for message reactions with the Come | Package | `@cometchat/chat-sdk-javascript` | | Import | `import { CometChat } from "@cometchat/chat-sdk-javascript";` | | Key methods | `CometChat.addReaction(messageId, emoji)` · `CometChat.removeReaction(messageId, emoji)` · `message.getReactions()` · `reactionCount.getReactedByMe()` · `CometChat.CometChatHelper.updateMessageWithReactionInfo(message, messageReaction, action)` | -| Request builder | `new CometChat.ReactionRequestBuilder().setMessageId(messageId).setReaction(emoji).setLimit(limit).build()` → `.fetchNext()` / `.fetchPrevious()` (max 100 per request) | +| Request builder | `new CometChat.ReactionRequestBuilder().setMessageId(messageId).setReaction(emoji).setLimit(limit).build()` → `.fetchNext()` / `.fetchPrevious()` (default 10, max 1000 per request) | | Key classes | `CometChat.BaseMessage` · `CometChat.Reaction` · `CometChat.ReactionCount` · `CometChat.MessageReaction` · `CometChat.REACTION_ACTION` | | Primary output | `addReaction()` / `removeReaction()` resolve to a `CometChat.BaseMessage` with updated reactions · `fetchNext()` / `fetchPrevious()` resolve to `CometChat.Reaction[]` · all reject with `CometChat.CometChatException` | | Listeners registered | `CometChat.MessageListener` via `CometChat.addMessageListener(listenerID, ...)` / `CometChat.removeMessageListener(listenerID)`; key callbacks `onMessageReactionAdded` and `onMessageReactionRemoved` | @@ -144,13 +144,13 @@ reaction.getReactedByMe(); //Return true is logged-in user reacted on that messa ## Fetch Reactions for a Message -To get the full list of who reacted with what on a specific message, use `ReactionRequestBuilder`. You can paginate through results with `fetchNext()` and `fetchPrevious()` (max 100 per request). +To get the full list of who reacted with what on a specific message, use `ReactionRequestBuilder`. You can paginate through results with `fetchNext()` and `fetchPrevious()` (default 10, max 1000 per request). | Builder Method | Description | | --------------------- | ----------------------------------------------------------------------------------------------------- | | `setMessageId(value)` | The message ID to fetch reactions for. Required. | | `setReaction(value)` | Filter to a specific emoji (e.g., `"😊"`). When set, only reactions matching this emoji are returned. | -| `setLimit(value)` | Number of reactions to fetch per request. Max 100. | +| `setLimit(value)` | Number of reactions to fetch per request. Default 10, max 1000. | ### Fetch Next @@ -481,6 +481,19 @@ After calling this method, use modifiedBaseMessage.getReactions() to get the lat | `messageReaction` | [`MessageReaction`](/sdk/reference/auxiliary#messagereaction) | The reaction event received from the listener | | `action` | `REACTION_ACTION` | `CometChat.REACTION_ACTION.REACTION_ADDED` or `CometChat.REACTION_ACTION.REACTION_REMOVED` | + +## Offline Behaviour + +With offline support enabled, `ReactionsRequest.fetchNext()` and `fetchPrevious()` accept a `ReactionFetchListener` and first deliver `onCachedReactions` from the device (empty when nothing was recorded), then `onServerReactions` with the live page to reconcile by reaction id. A request object caches only the first direction it fetches, so use a separate request for each direction. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. See [Show Cached Data](/sdk/javascript/offline-cached-data). + +The cached phase replays the page this request last received from the server online, in the same direction; it does not re-run these settings as a query against the cache. + +| Builder method | Offline | +|---|---| +| `setLimit()` | Cached | +| `setMessageId()` | Cached | +| `setReaction()` | Cached | + --- ## Next Steps diff --git a/sdk/javascript/thread-subscription.mdx b/sdk/javascript/thread-subscription.mdx index 33226476f..f08f7f60b 100644 --- a/sdk/javascript/thread-subscription.mdx +++ b/sdk/javascript/thread-subscription.mdx @@ -408,6 +408,16 @@ Both `subscribeToThread()` and `unsubscribeFromThread()` reject with a `CometCha + +## Offline Behaviour + +With [offline support](/sdk/javascript/offline-cached-data) enabled, `fetchNext()` on a `ThreadsRequest` accepts a `ThreadFetchListener`: it delivers a cached page first (`onCachedThreads`, even when empty) and then the server list (`onServerThreads`). Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. With `setParticipatedByMe(true)` (the default), `onCachedThreads` holds the user's cached participated threads, in the same sort order; with `false`, it replays the last page fetched online for this request. `setUid()` / `setGuid()` scope that list to one conversation. A second `fetchNext()` started while one is running ends with `onError` with code `REQUEST_IN_PROGRESS`. + +| Builder method | Offline | +| --- | --- | +| `setParticipatedByMe()` | Cached (`true`: your participated threads; `false`: the last page fetched online) | +| `setUid()` / `setGuid()` | Cached (scope) | + --- ## Next Steps diff --git a/sdk/javascript/threaded-messages.mdx b/sdk/javascript/threaded-messages.mdx index b1c355367..165043e12 100644 --- a/sdk/javascript/threaded-messages.mdx +++ b/sdk/javascript/threaded-messages.mdx @@ -358,6 +358,16 @@ CometChat.removeMessageListener("UNIQUE_LISTENER_ID"); + +## Offline Behaviour + +With offline support enabled, thread fetches (`setParentMessageId()` on a `MessagesRequest`) run in two phases: `onCachedMessages` delivers the cached thread page first (even when empty), then `onServerMessages` delivers the reconciled server copy. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. Setting both `setUID` and `setGUID` on a thread read makes it server-only, as for any other message read. See [Show Cached Data](/sdk/javascript/offline-cached-data). + +| Builder method | Offline behaviour | +| --- | --- | +| `setParentMessageId()` | Cached | +| `hideReplies()` | Cached | + --- ## Next Steps From d3b1b948ecdc331102821daa11f525e2b59262f7 Mon Sep 17 00:00:00 2001 From: rajdubey Date: Sun, 1 Nov 2026 20:09:46 +0530 Subject: [PATCH 10/12] docs(js): offline notes on edit, delete, delete group, pin and save pages Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XbguTshd7eq8N2XxFabgLN --- sdk/javascript/delete-group.mdx | 2 ++ sdk/javascript/delete-message.mdx | 2 ++ sdk/javascript/edit-message.mdx | 2 ++ sdk/javascript/pin-conversation.mdx | 2 ++ sdk/javascript/pin-message.mdx | 2 ++ sdk/javascript/save-message.mdx | 2 ++ 6 files changed, 12 insertions(+) diff --git a/sdk/javascript/delete-group.mdx b/sdk/javascript/delete-group.mdx index 97f45ef5c..75078297e 100644 --- a/sdk/javascript/delete-group.mdx +++ b/sdk/javascript/delete-group.mdx @@ -7,6 +7,8 @@ description: "Delete CometChat groups from JavaScript apps by GUID when the logg Use `deleteGroup()` with the group's GUID. +Deleting a group needs a connection. With offline support enabled, `deleteGroup()` is **not** queued for later. After it succeeds, the group's conversation and cached messages are removed from the on-device cache at the next catch-up sync, and any messages still queued to it then fail with reason `DISCARDED`. See [Send While Offline](/sdk/javascript/offline-send). + ```typescript diff --git a/sdk/javascript/delete-message.mdx b/sdk/javascript/delete-message.mdx index 9179852a6..fd08e28fa 100644 --- a/sdk/javascript/delete-message.mdx +++ b/sdk/javascript/delete-message.mdx @@ -12,6 +12,8 @@ While [deleting a message](/sdk/javascript/delete-message#delete-a-message) is s Use `deleteMessage()` with the message ID. +Deleting a message needs a connection. With offline support enabled, `deleteMessage()` is **not** queued for later. After it succeeds, the deletion is applied to the message in the on-device cache if it is already cached; it never adds a new message. See [Send While Offline](/sdk/javascript/offline-send). + ```typescript diff --git a/sdk/javascript/edit-message.mdx b/sdk/javascript/edit-message.mdx index f4bab5085..95a22d828 100644 --- a/sdk/javascript/edit-message.mdx +++ b/sdk/javascript/edit-message.mdx @@ -12,6 +12,8 @@ While [editing a message](/sdk/javascript/edit-message#edit-a-message) is straig Use `editMessage()` with a [`TextMessage`](/sdk/reference/messages#textmessage) or [`CustomMessage`](/sdk/reference/messages#custommessage) object. Set the message ID using `setId()`. +Editing a message needs a connection. With offline support enabled, `editMessage()` is **not** queued for later. After it succeeds, the edit is applied to the message in the on-device cache if it is already cached; it never adds a new message. See [Send While Offline](/sdk/javascript/offline-send). + ### Add/Update Tags Use `setTags()` to update tags when editing. New tags replace existing ones. diff --git a/sdk/javascript/pin-conversation.mdx b/sdk/javascript/pin-conversation.mdx index 6becbf659..74dc2f986 100644 --- a/sdk/javascript/pin-conversation.mdx +++ b/sdk/javascript/pin-conversation.mdx @@ -137,6 +137,8 @@ To narrow the list to pins only, use `setPinnedBy()` with the tokens on `CometCh Passing an empty array is the same as not calling `setPinnedBy()` at all — you get the default, pin-ordered list. +With [offline support](/sdk/javascript/offline-cached-data) enabled, `fetchNext()` on a `setPinnedBy()` request accepts a `ConversationFetchListener`: it delivers a cached page first (`onCachedConversations`, even when empty), then the server list (`onServerConversations`). Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. + ## Check if a Conversation is Pinned diff --git a/sdk/javascript/pin-message.mdx b/sdk/javascript/pin-message.mdx index 27550926e..1fb86f366 100644 --- a/sdk/javascript/pin-message.mdx +++ b/sdk/javascript/pin-message.mdx @@ -131,6 +131,8 @@ The list is ordered by **pin time, most recently pinned first** — not by when count in a header. +With [offline support](/sdk/javascript/offline-cached-data) enabled, `fetchNext()` on a `setPinned(true)` request accepts a `MessageFetchListener`: it delivers a cached page first (`onCachedMessages`, even when empty) and then the server list (`onServerMessages`). Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. `onCachedMessages` holds the conversation's cached pinned messages. The pinned list is answered from its own list, not from the message cache, so the other filters on the request do not apply to it. + ## Check if a Message is Pinned Every `BaseMessage` carries its pin state. The presence of `pinnedAt` *is* the boolean — an unpinned message simply has no pin attributes. diff --git a/sdk/javascript/save-message.mdx b/sdk/javascript/save-message.mdx index 18045552b..d36e34ee4 100644 --- a/sdk/javascript/save-message.mdx +++ b/sdk/javascript/save-message.mdx @@ -149,6 +149,8 @@ Because the list spans conversations, every row carries its own context — use +With [offline support](/sdk/javascript/offline-cached-data) enabled, `fetchNext()` on a `setSaved(true)` request accepts a `MessageFetchListener`: it delivers a cached page first (`onCachedMessages`, even when empty) and then the server list (`onServerMessages`). Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. `onCachedMessages` holds the user's cached saved messages across all conversations. The saved list is answered from its own list, not from the message cache, so the other filters on the request do not apply to it. + ## Check if a Message is Saved The presence of `savedAt` *is* the boolean — an unsaved message simply has no save attribute. From be0f64eb519f48507f53e6bee0233150a0fbc0b9 Mon Sep 17 00:00:00 2001 From: rajdubey Date: Sun, 1 Nov 2026 20:09:46 +0530 Subject: [PATCH 11/12] docs(js): offline behaviour on conversations, users, groups, members, banned members, blocked users and campaigns pages Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XbguTshd7eq8N2XxFabgLN --- sdk/javascript/block-users.mdx | 7 ++++ sdk/javascript/campaigns.mdx | 14 ++++++++ sdk/javascript/group-kick-ban-members.mdx | 39 +++++++++++++++++++++++ sdk/javascript/retrieve-conversations.mdx | 21 ++++++++++++ sdk/javascript/retrieve-group-members.mdx | 16 ++++++++++ sdk/javascript/retrieve-groups.mdx | 15 +++++++++ sdk/javascript/retrieve-users.mdx | 26 +++++++++++++++ 7 files changed, 138 insertions(+) diff --git a/sdk/javascript/block-users.mdx b/sdk/javascript/block-users.mdx index b6a7d4713..68125faf6 100644 --- a/sdk/javascript/block-users.mdx +++ b/sdk/javascript/block-users.mdx @@ -157,6 +157,8 @@ let blockedUsersRequest = new CometChat.BlockedUsersRequestBuilder() ### Set Search Keyword +With offline support on, this filter is answered by the server only; the cached page is empty. + Filters blocked users by a search string. @@ -288,6 +290,11 @@ Relevant fields to access on returned users: | blockedByMe | `getBlockedByMe()` | `boolean` | Whether the logged-in user has blocked this user | | hasBlockedMe | `getHasBlockedMe()` | `boolean` | Whether this user has blocked the logged-in user | + +## Offline Behaviour + +With offline support enabled, `BlockedUsersRequest.fetchNext()` accepts a `UserFetchListener` and first delivers `onCachedUsers` from the SDK's own blocked-users list, which it refreshes on login and on reconnect and updates when you call `blockUsers()` or `unblockUsers()`. Then `onServerUsers` delivers the live page to reconcile by `uid`; the server page is not saved to the device. The cached page is empty when no users are blocked or when `setSearchKeyword()` is set. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. See [Show Cached Data](/sdk/javascript/offline-cached-data). + --- ## Next Steps diff --git a/sdk/javascript/campaigns.mdx b/sdk/javascript/campaigns.mdx index fc5ef04b4..1091e98d1 100644 --- a/sdk/javascript/campaigns.mdx +++ b/sdk/javascript/campaigns.mdx @@ -224,6 +224,20 @@ Call `fetchNext()` repeatedly for pagination. When the server has no more items, --- +## Offline Behaviour + +With offline support enabled, `NotificationFeedRequest.fetchNext()` accepts a `NotificationFeedFetchListener` and first delivers `onCachedNotificationFeedItems` from the device (empty when nothing was recorded), then `onServerNotificationFeedItems` with the live page to reconcile by item `id`. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. A `fetchNext()` started while another fetch of the same request is running ends with `onError` with code `REQUEST_IN_PROGRESS`. See [Show Cached Data](/sdk/javascript/offline-cached-data). + +| Builder method | Offline | +|---|---| +| `setLimit()` | Cached | +| `setReadState()` | Cached | +| `setCategory()` | Cached | +| `setChannelId()` | Cached | +| `setTags()` | Cached | +| `setDateFrom()` | Cached | +| `setDateTo()` | Cached | + ## Retrieve Notification Categories Use `NotificationCategoriesRequestBuilder` to fetch available categories for filter chips. diff --git a/sdk/javascript/group-kick-ban-members.mdx b/sdk/javascript/group-kick-ban-members.mdx index 46c090bb5..7871dce2a 100644 --- a/sdk/javascript/group-kick-ban-members.mdx +++ b/sdk/javascript/group-kick-ban-members.mdx @@ -218,6 +218,8 @@ let bannedGroupMembersRequest = new CometChat.BannedMembersRequestBuilder(GUID) ### Set Search Keyword +With offline support on, this filter is answered by the server only; the cached page is empty. + Filters banned members by a search string. @@ -294,6 +296,32 @@ bannedGroupMembersRequest.fetchNext().then( The `fetchNext()` method returns an array of [`GroupMember`](/sdk/reference/entities#groupmember) objects representing the banned members of the group. +### Set Scopes + +Filters banned members by one or more scopes (`admin`, `moderator`, `participant`). + + + +```typescript +let GUID: string = "GUID"; +let scopes: Array = ["admin", "moderator"]; +let bannedGroupMembersRequest: CometChat.BannedMembersRequest = new CometChat.BannedMembersRequestBuilder(GUID) + .setScopes(scopes) + .build(); +``` + + + +```javascript +let GUID = "GUID"; +let scopes = ["admin", "moderator"]; +let bannedGroupMembersRequest = new CometChat.BannedMembersRequestBuilder(GUID) + .setScopes(scopes) + .build(); +``` + + + ## Real-Time Kick/Ban/Unban Events Implement these `GroupListener` methods to receive real-time notifications: @@ -392,6 +420,17 @@ When fetching previous messages, kick/ban/unban actions appear as [Action](/sdk/ | `actionOn` | [User](/sdk/reference/entities#user) | The member who was unbanned | | `actionFor` | [Group](/sdk/reference/entities#group) | The group | + +## Offline Behaviour + +With offline support enabled, `BannedMembersRequest.fetchNext()` accepts a `BannedMemberFetchListener` and first delivers `onCachedBannedGroupMembers` from the device (empty when nothing was recorded), then `onServerBannedGroupMembers` with the live page to reconcile by `uid`. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. A request that uses a server-only filter records nothing, so its cached page is always empty. See [Show Cached Data](/sdk/javascript/offline-cached-data). + +| Builder method | Offline | +|---|---| +| `setLimit()` | Cached | +| `setScopes()` | Cached | +| `setSearchKeyword()` | Server only — cached page is empty | + --- ## Next Steps diff --git a/sdk/javascript/retrieve-conversations.mdx b/sdk/javascript/retrieve-conversations.mdx index 63374ed5f..3bf13ce63 100644 --- a/sdk/javascript/retrieve-conversations.mdx +++ b/sdk/javascript/retrieve-conversations.mdx @@ -318,6 +318,8 @@ Relevant fields to access on returned conversations: ### Search Conversations +With offline support on, this filter is answered by the server only; the cached page is empty. + Use `setSearchKeyword()` to search conversations by user or group name. @@ -613,6 +615,8 @@ When the conversation is fetched successfully, the response will return a single The `getConversation()` method returns a single [`Conversation`](/sdk/reference/entities#conversation) object. +With offline support enabled, pass a `ConversationDetailsListener` as the third argument: `onCachedConversation` first delivers the cached copy when there is one, then `onServerConversation` delivers the server copy. Offline, the read then ends with `onError` with code `FAILED_TO_FETCH`; when nothing is cached, `onError` is the only callback. + ## Convert Messages to Conversations Use `CometChatHelper.getConversationFromMessage()` to convert a received message into a [`Conversation`](/sdk/reference/entities#conversation) object. Useful for updating your Recent Chats list when receiving real-time messages. @@ -656,6 +660,23 @@ CometChat.CometChatHelper.getConversationFromMessage(message).then( The `getConversationFromMessage()` method returns a [`Conversation`](/sdk/reference/entities#conversation) object. + +## Offline Behaviour + +With [offline support](/sdk/javascript/offline-cached-data) enabled, `fetchNext()` accepts a `ConversationFetchListener` and delivers a cached page first (`onCachedConversations`, even when empty) and then the server list (`onServerConversations`). Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. The cached page is answered only when every builder filter on the request is locally computable: a single server-only filter blanks the cached page while the server phase still answers in full. + +| Builder method | Offline | +|---|---| +| `setConversationType()` | Cached | +| `setTags()` | Cached | +| `setUserTags()` / `setGroupTags()` | Cached | +| `setUnread()` | Cached | +| `includeBlockedUsers()` / `withBlockedInfo()` | Cached | +| `withUserAndGroupTags()` / `withTags()` | Cached (enrichment) | +| `setHideAgentic()` / `setOnlyAgentic()` | Cached | +| `setPinnedBy()` | Cached | +| `setSearchKeyword()` | Server only — cached page is empty | + --- ## Next Steps diff --git a/sdk/javascript/retrieve-group-members.mdx b/sdk/javascript/retrieve-group-members.mdx index 87ab6a857..abf8eed39 100644 --- a/sdk/javascript/retrieve-group-members.mdx +++ b/sdk/javascript/retrieve-group-members.mdx @@ -56,6 +56,8 @@ let groupMembersRequest = new CometChat.GroupMembersRequestBuilder(GUID) ### Set Search Keyword +With offline support on, this filter is answered by the server only; the cached page is empty. + Filters members by a search string. @@ -121,6 +123,8 @@ Relevant fields to access on returned members: ### Set Status +With offline support on, this filter is answered by the server only; the cached page is empty. + Filters members by online status: | Value | Description | @@ -203,6 +207,18 @@ groupMembers => { The `fetchNext()` method returns an array of [`GroupMember`](/sdk/reference/entities#groupmember) objects. `GroupMember` extends [`User`](/sdk/reference/entities#user) and adds group-specific fields. + +## Offline Behaviour + +With offline support enabled, `GroupMembersRequest.fetchNext()` accepts a `GroupMemberFetchListener` and first delivers `onCachedGroupMembers` from the device (empty when nothing was recorded), then `onServerGroupMembers` with the live page to reconcile by `uid`. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. A request that uses a server-only filter records nothing, so its cached page is always empty. See [Show Cached Data](/sdk/javascript/offline-cached-data). + +| Builder method | Offline | +|---|---| +| `setLimit()` | Cached | +| `setScopes()` | Cached | +| `setSearchKeyword()` | Server only — cached page is empty | +| `setStatus()` | Server only — cached page is empty | + --- ## Next Steps diff --git a/sdk/javascript/retrieve-groups.mdx b/sdk/javascript/retrieve-groups.mdx index e8f1017c9..0527af2f9 100644 --- a/sdk/javascript/retrieve-groups.mdx +++ b/sdk/javascript/retrieve-groups.mdx @@ -59,6 +59,8 @@ let groupsRequest = new CometChat.GroupsRequestBuilder() ### Set Search Keyword +With offline support on, this filter is answered by the server only; the cached page is empty. + Filters groups by a search string. @@ -331,6 +333,19 @@ Returns an object with GUIDs as keys and online member counts as values. `getOnlineGroupMemberCount()` resolves with a `{ guid: count }` object where each key is a group GUID and its value is the number of currently online members in that group. + +## Offline Behaviour + +With offline support enabled, `GroupsRequest.fetchNext()` accepts a `GroupFetchListener` and first delivers `onCachedGroups` from the device (empty when nothing was recorded), then `onServerGroups` with the live page to reconcile by `guid`. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. A request that uses a server-only filter records nothing, so its cached page is always empty. See [Show Cached Data](/sdk/javascript/offline-cached-data). + +| Builder method | Offline | +|---|---| +| `setLimit()` | Cached | +| `joinedOnly()` | Cached | +| `setTags()` | Cached | +| `withTags()` | Cached | +| `setSearchKeyword()` | Server only — cached page is empty | + --- ## Next Steps diff --git a/sdk/javascript/retrieve-users.mdx b/sdk/javascript/retrieve-users.mdx index 03c856596..64c841dec 100644 --- a/sdk/javascript/retrieve-users.mdx +++ b/sdk/javascript/retrieve-users.mdx @@ -132,6 +132,8 @@ let usersRequest = new CometChat.UsersRequestBuilder() ### Set Search Keyword +With offline support on, this filter is answered by the server only; the cached page is empty. + Filters users by a search string. @@ -164,6 +166,8 @@ let usersRequest = new CometChat.UsersRequestBuilder() ### Search In +With offline support on, this filter is answered by the server only; the cached page is empty. + Specifies which user properties to search. Works with `setSearchKeyword()`. By default, searches both UID and name. @@ -200,6 +204,8 @@ let usersRequest = new CometChat.UsersRequestBuilder() ### Set Status +With offline support on, this filter is answered by the server only; the cached page is empty. + Filters users by online status: - `CometChat.USER_STATUS.ONLINE` — Only online users @@ -606,6 +612,26 @@ userCount => { Returns the total online user count as a number. + +## Offline Behaviour + +With offline support enabled, `UsersRequest.fetchNext()` accepts a `UserFetchListener` and first delivers `onCachedUsers` from the device (the pages the server last returned for this exact request, replayed on-device, empty when nothing was recorded), then `onServerUsers` with the live page to reconcile by `uid`. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. A request that uses a server-only filter records nothing, so its cached page is always empty. See [Show Cached Data](/sdk/javascript/offline-cached-data). + +| Builder method | Offline | +|---|---| +| `setLimit()` | Cached | +| `setSearchKeyword()` | Server only — cached page is empty | +| `searchIn()` | Used only with `setSearchKeyword()`, so server only | +| `setStatus()` | Server only — cached page is empty | +| `hideBlockedUsers()` | Cached | +| `setRole()` / `setRoles()` | Cached | +| `friendsOnly()` | Cached | +| `setTags()` | Cached | +| `withTags()` | Cached | +| `setUIDs()` | Cached | +| `sortBy()` | Cached | +| `sortByOrder()` | Cached | + --- ## Next Steps From 6e435ac14e14593058443138f0e5c72e7088e512 Mon Sep 17 00:00:00 2001 From: rajdubey Date: Mon, 2 Nov 2026 16:17:39 +0530 Subject: [PATCH 12/12] docs(javascript): align offline docs with the release SDK - Replace the removed SingleMessageFetchListener with MessageDetailsListener and drop the SingleConversationFetchListener alias claim. - Document that a cold-start sync fires no onMessageSync* callbacks. - The 10-second forceSyncNow cooldown follows any sync, not only forced ones. - A blocked-users keyword search does not fire onCachedUsers. - The thread subscription flag is cached with the message when offline is on. - List every snapshot type under maxSnapshotQueries. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XbguTwdQxd6Vh7nDdURGwM --- sdk/javascript/block-users.mdx | 4 ++-- sdk/javascript/offline-cached-data.mdx | 12 ++++-------- sdk/javascript/offline-reference.mdx | 8 ++++---- sdk/javascript/offline-sync.mdx | 14 +++++++++----- sdk/javascript/receive-message.mdx | 2 +- sdk/javascript/thread-subscription.mdx | 4 ++-- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/sdk/javascript/block-users.mdx b/sdk/javascript/block-users.mdx index 68125faf6..ccd0d7205 100644 --- a/sdk/javascript/block-users.mdx +++ b/sdk/javascript/block-users.mdx @@ -157,7 +157,7 @@ let blockedUsersRequest = new CometChat.BlockedUsersRequestBuilder() ### Set Search Keyword -With offline support on, this filter is answered by the server only; the cached page is empty. +With offline support on, this filter is answered by the server only; `onCachedUsers` does not fire. Filters blocked users by a search string. @@ -293,7 +293,7 @@ Relevant fields to access on returned users: ## Offline Behaviour -With offline support enabled, `BlockedUsersRequest.fetchNext()` accepts a `UserFetchListener` and first delivers `onCachedUsers` from the SDK's own blocked-users list, which it refreshes on login and on reconnect and updates when you call `blockUsers()` or `unblockUsers()`. Then `onServerUsers` delivers the live page to reconcile by `uid`; the server page is not saved to the device. The cached page is empty when no users are blocked or when `setSearchKeyword()` is set. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page. See [Show Cached Data](/sdk/javascript/offline-cached-data). +With offline support enabled, `BlockedUsersRequest.fetchNext()` accepts a `UserFetchListener` and first delivers `onCachedUsers` from the SDK's own blocked-users list, which it refreshes on login and on reconnect and updates when you call `blockUsers()` or `unblockUsers()`. Then `onServerUsers` delivers the live page to reconcile by `uid`; the server page is not saved to the device. The cached page is empty when no users are blocked. When `setSearchKeyword()` is set, the cached phase is skipped, so `onCachedUsers` does not fire. Offline, the read ends with `onError` with code `FAILED_TO_FETCH` after the cached page, or with `onError` alone when the cached phase is skipped. See [Show Cached Data](/sdk/javascript/offline-cached-data). --- diff --git a/sdk/javascript/offline-cached-data.mdx b/sdk/javascript/offline-cached-data.mdx index a06d26c9a..2be4a813e 100644 --- a/sdk/javascript/offline-cached-data.mdx +++ b/sdk/javascript/offline-cached-data.mdx @@ -23,7 +23,7 @@ request // Single items (additive two-phase overloads) CometChat.getConversation("UID", "user", new CometChat.ConversationDetailsListener({ /* … */ })); -CometChat.getMessageDetails(messageId, new CometChat.SingleMessageFetchListener({ /* … */ })); +CometChat.getMessageDetails(messageId, new CometChat.MessageDetailsListener({ /* … */ })); ``` @@ -36,7 +36,7 @@ Pass a fetch listener as the optional argument and each read fires **up to two** -The SDK delivers a page from the local cache **first**. It fires **even when the page is empty** — an empty cached page is indistinguishable from a cache miss, by design. Render it right away. +The SDK delivers a page from the local cache **first**. It normally fires **even when the page is empty** — an empty cached page is indistinguishable from a cache miss, by design. Render it right away. Some reads skip this phase entirely, such as a blocked-users read with a search keyword; see [When the cached page is empty](#when-the-cached-page-is-empty). After a successful server response, the SDK delivers the server's copy for the **same slice** and writes it through to the cache before the callback. Reconcile your displayed list against it. (Blocked users are the exception: their server page is delivered but not written to the cache. The cached blocked-users list is kept current by the SDK itself.) @@ -244,7 +244,7 @@ CometChat.getConversation( CometChat.getMessageDetails( messageId, - new CometChat.SingleMessageFetchListener({ + new CometChat.MessageDetailsListener({ onCachedMessage: (message: CometChat.BaseMessage) => { /* cache hit only */ }, onServerMessage: (message: CometChat.BaseMessage) => { /* server copy */ }, onError: (error: CometChat.CometChatException) => console.log(error.code), @@ -270,7 +270,7 @@ CometChat.getConversation( CometChat.getMessageDetails( messageId, - new CometChat.SingleMessageFetchListener({ + new CometChat.MessageDetailsListener({ onCachedMessage: (message) => { /* cache hit only */ }, onServerMessage: (message) => { /* server copy */ }, onError: (error) => console.log(error.code), @@ -280,10 +280,6 @@ CometChat.getMessageDetails( - -`CometChat.SingleConversationFetchListener` is a deprecated alias of `ConversationDetailsListener`. - - ## Merge the two phases The server phase **replaces the same slice** — it is a reconcile, not a second batch. Render the cached page straight away, then on the server phase diff it into your displayed list keyed by the item's stable id (message id, conversation id, uid, guid). Do not append the server page to the cached one. diff --git a/sdk/javascript/offline-reference.mdx b/sdk/javascript/offline-reference.mdx index abdac44ed..87d706125 100644 --- a/sdk/javascript/offline-reference.mdx +++ b/sdk/javascript/offline-reference.mdx @@ -43,7 +43,7 @@ Pass these keys in the object given to `AppSettingsBuilder.setOfflineConfig(...) | `mediaCaching` | `MediaCachingMode` | `AUTO` | unknown → `AUTO` | Receive-side media mode. Fixed at init. | | `encryption` | `boolean` | `false` | only an explicit `true` enables | Encrypt the cache at rest. See [Encrypt the Cache](/sdk/javascript/offline-encryption). | | `encryptionFailureBehavior` | `EncryptionFailureBehavior` | `ONLINE_ONLY` | anything but `PLAINTEXT_FALLBACK` → `ONLINE_ONLY` | What happens when encryption cannot run at all. | -| `maxSnapshotQueries` | `number` | `100` | `0`/negative → `100` | Most distinct entity-list reads (users, groups, members, banned members, reactions, notification feed, threads) kept for offline replay; LRU. | +| `maxSnapshotQueries` | `number` | `100` | `0`/negative → `100` | Most distinct entity-list reads kept for offline replay, across users, groups, group members, banned members, reactions, the notification feed and threads with `setParticipatedByMe(false)`; LRU. | | `snapshotTtlDays` | `number` | `7` | `0`/negative → `7` | Days a recorded list read stays replayable. Past it, the read is empty offline until viewed online again. | Count caps are always bounded and have no "unlimited" setting. Byte and day caps treat `<= 0` as unlimited. @@ -93,7 +93,7 @@ Every listener is constructed with an object of callbacks, for example `new Come |---|---| | `MessageFetchListener` | `onCachedMessages(messages)`, `onServerMessages(messages)`, `onError(error)` | | `ThreadFetchListener` | `onCachedThreads(threads)`, `onServerThreads(threads)`, `onError(error)` | -| `SingleMessageFetchListener` | `onCachedMessage(message)`, `onServerMessage(message)`, `onError(error)` | +| `MessageDetailsListener` | `onCachedMessage(message)`, `onServerMessage(message)`, `onError(error)` | | `ConversationFetchListener` | `onCachedConversations(conversations)`, `onServerConversations(conversations)`, `onError(error)` | | `ConversationDetailsListener` | `onCachedConversation(conversation)`, `onServerConversation(conversation)`, `onError(error)` | | `UserFetchListener` | `onCachedUsers(users)`, `onServerUsers(users)`, `onError(error)` | @@ -103,7 +103,7 @@ Every listener is constructed with an object of callbacks, for example `new Come | `ReactionFetchListener` | `onCachedReactions(reactions)`, `onServerReactions(reactions)`, `onError(error)` | | `NotificationFeedFetchListener` | `onCachedNotificationFeedItems(items)`, `onServerNotificationFeedItems(items)`, `onError(error)` | -`SingleConversationFetchListener` is a deprecated alias of `ConversationDetailsListener`. `BannedMemberFetchListener` also still accepts the older `onCachedBannedMembers` / `onServerBannedMembers` names. +`BannedMemberFetchListener` also still accepts the older `onCachedBannedMembers` / `onServerBannedMembers` names. ### Send, sync, cache, and media listeners @@ -178,4 +178,4 @@ Enrichment flags (`withTags`, `withParent`, mentions-with-tag/blocked info, `wit ## Deprecated overloads and replacements -Nothing is deprecated for the main fetch and send methods: the listener is an **additive optional argument** and every method keeps returning its Promise. With offline off, a listener-bearing call behaves like the single-callback form. The only deprecated names are `SingleConversationFetchListener` (use `ConversationDetailsListener`) and the `onCachedBannedMembers` / `onServerBannedMembers` callback names (use the `…BannedGroupMembers` names). +Nothing is deprecated for the main fetch and send methods: the listener is an **additive optional argument** and every method keeps returning its Promise. With offline off, a listener-bearing call behaves like the single-callback form. The only deprecated names are the `onCachedBannedMembers` / `onServerBannedMembers` callback names (use the `…BannedGroupMembers` names). diff --git a/sdk/javascript/offline-sync.mdx b/sdk/javascript/offline-sync.mdx index b238d2ef8..c3616f6c2 100644 --- a/sdk/javascript/offline-sync.mdx +++ b/sdk/javascript/offline-sync.mdx @@ -46,13 +46,13 @@ Everything on this page is a no-op unless offline support is active for the logg A sync runs automatically whenever the connection settles, including the first connect after login or launch. 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. It proceeds in two phases, in order: 1. **Conversation phase** — the conversation list is fetched as one fresh page (up to `maxConversations`). 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. +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. A cold start skips this phase. -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 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. +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 first-ever sync (cold start) runs only the conversation phase: it 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 `CometChat.SyncListener` to observe the lifecycle of every automatic and manual sync. Each phase fires `started`, then exactly one of `completed` / `error`. +Register a `CometChat.SyncListener` to observe the lifecycle of every automatic and manual sync. Each phase that runs fires `started`, then exactly one of `completed` / `error`. A cold start runs only the conversation phase, so no `onMessageSync*` callback fires. @@ -100,10 +100,14 @@ CometChat.addSyncListener( | `onConversationSyncStarted()` | The conversation refresh began. | | `onConversationSyncCompleted(conversations)` | The refresh succeeded; `conversations` is the full conversation page. | | `onConversationSyncError(error)` | The refresh failed. A transport failure leaves the cache untouched; a malformed response removes nothing. Either way the refresh retries on the next reconnect. | -| `onMessageSyncStarted()` | The message changelog sweep began. | +| `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. | + +Do not wait on the `onMessageSync*` callbacks after first login, `clearOfflineCache()`, or a cache reset. A sync with no saved sync point runs only the conversation phase, so `onConversationSyncCompleted` or `onConversationSyncError` is its last callback. + + 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 error always carries a `code`: `FAILED_TO_FETCH` when the request got no answer, `ERROR_JSON_EXCEPTION` when the server answered 2xx with an unreadable body (`details.httpStatus` holds the status), otherwise the server's own code. `onMessageSyncCompleted` reports the outcome: @@ -186,7 +190,7 @@ const lastSync = await CometChat.getLastSyncTimestamp(); // server epoch seconds ## 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, and it enforces a 10-second cooldown between forced syncs. +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, and it is refused for 10 seconds after any sync ends, automatic or forced, whether or not that sync succeeded. Pass a callback to learn whether the request was accepted; it receives no error (`null`) as soon as the catch-up starts. The sync's actual progress and outcome still arrive on your registered `SyncListener`. diff --git a/sdk/javascript/receive-message.mdx b/sdk/javascript/receive-message.mdx index 3826d8368..0d5952f01 100644 --- a/sdk/javascript/receive-message.mdx +++ b/sdk/javascript/receive-message.mdx @@ -471,7 +471,7 @@ Returns a [`BaseMessage`](/sdk/reference/messages#basemessage) object (which may ## Offline Behaviour -With offline support enabled, `fetchPrevious()` and `fetchNext()` accept a `MessageFetchListener`: `onCachedMessages` delivers a page from the on-device cache first (even when empty), then `onServerMessages` delivers the reconciled server copy. The cache answers a page **only when every filter on the request is locally computable**; a single server-only filter makes the cached page empty with no error, while the server phase still answers in full. Offline, the fetch ends with `onError` with code `FAILED_TO_FETCH` after the cached page. `getMessageDetails()` accepts a `SingleMessageFetchListener` and delivers a cached copy only when the message is cached. See [Show Cached Data](/sdk/javascript/offline-cached-data) and the [filter table](/sdk/javascript/message-filtering#offline-behaviour). +With offline support enabled, `fetchPrevious()` and `fetchNext()` accept a `MessageFetchListener`: `onCachedMessages` delivers a page from the on-device cache first (even when empty), then `onServerMessages` delivers the reconciled server copy. The cache answers a page **only when every filter on the request is locally computable**; a single server-only filter makes the cached page empty with no error, while the server phase still answers in full. Offline, the fetch ends with `onError` with code `FAILED_TO_FETCH` after the cached page. `getMessageDetails()` accepts a `MessageDetailsListener` and delivers a cached copy only when the message is cached. See [Show Cached Data](/sdk/javascript/offline-cached-data) and the [filter table](/sdk/javascript/message-filtering#offline-behaviour). --- diff --git a/sdk/javascript/thread-subscription.mdx b/sdk/javascript/thread-subscription.mdx index f08f7f60b..7fa789630 100644 --- a/sdk/javascript/thread-subscription.mdx +++ b/sdk/javascript/thread-subscription.mdx @@ -15,9 +15,9 @@ The server subscribes a user to a thread automatically when they start it, reply ## How state works -The SDK keeps **no subscription state of its own**. There is no cache and no listener to reconcile: +The SDK keeps **no subscription state of its own** beyond the flag on each message, and there is no listener to reconcile: -- Every message fetch asks the server for the flag, and it arrives on the message — read it with `message.isThreadSubscribed()`. +- Every message fetch asks the server for the flag, and it arrives on the message — read it with `message.isThreadSubscribed()`. With [offline support](/sdk/javascript/offline-cached-data) on, the flag is also cached with the message, so a cached read returns the last known value and the server read then updates it. - `subscribeToThread()` and `unsubscribeFromThread()` resolve when the server has accepted the change. The resolved promise **is** the acknowledgement. Your app owns the resulting UI state. That means you decide when to flip a toggle optimistically, and you decide what a thread's state is before you have fetched it.