Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/ExpoMessaging/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"react-native-teleport": "^1.1.12",
"react-native-web": "^0.21.2",
"react-native-worklets": "0.11.1",
"stream-chat": "^9.50.3",
"stream-chat": "^9.51.0",
"stream-chat-expo": "workspace:^",
"stream-chat-react-native-core": "workspace:^"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/SampleApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"react-native-teleport": "^1.1.12",
"react-native-video": "^6.19.2",
"react-native-worklets": "^0.11.1",
"stream-chat": "^9.50.3",
"stream-chat": "^9.51.0",
"stream-chat-react-native": "workspace:^",
"stream-chat-react-native-core": "workspace:^"
},
Expand Down
2 changes: 1 addition & 1 deletion package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"path": "0.12.7",
"react-native-markdown-package": "1.8.2",
"react-native-url-polyfill": "^2.0.0",
"stream-chat": "^9.50.3",
"stream-chat": "^9.51.0",
"use-sync-external-store": "^1.5.0"
},
"peerDependencies": {
Expand Down
28 changes: 26 additions & 2 deletions package/src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useStreami18n } from '../../hooks/useStreami18n';
import init from '../../init';

import { NativeHandlers } from '../../native';
import { DEFAULT_MAX_SYNC_EVENTS_LIMIT } from '../../store/constants';
import { OfflineDB } from '../../store/OfflineDB';

import type { Streami18n } from '../../utils/i18n/Streami18n';
Expand All @@ -44,6 +45,28 @@ export type ChatProps = Pick<ChatContextValue, 'client'> &
* Enables offline storage and loading for chat data.
*/
enableOfflineSupport?: boolean;
/**
* Optional positive cap on the number of events a single `/sync` response may
* contain before the offline sync manager skips replaying those events into
* local storage.
*
* On reconnect the SDK downloads the events missed while offline and writes
* them to the offline DB. For a very large payload this replay is both costly
* on-device and unnecessary for what the user is looking at — the active
* channel list and any open channel are refreshed independently on reconnect
* (via `queryChannels` + `channel.watch()`). When the payload exceeds this
* limit the replay is skipped and that reconnect refresh covers the visible
* channels; inactive channels are hydrated on their next explicit query. The
* last-sync timestamp is still advanced so the same payload is not retried.
*
* Defaults to {@link DEFAULT_MAX_SYNC_EVENTS_LIMIT} (250). Pass `false` to
* disable the limit entirely (replay every event — the historical behavior).
*
* Only relevant when `enableOfflineSupport` is enabled.
*
* @default 250
*/
maxSyncEventsLimit?: number | false;
/**
* When true, multipart uploads use the SDK's native upload adapter when available.
* When false, uploads stay on the default axios adapter.
Expand Down Expand Up @@ -151,6 +174,7 @@ const ChatWithContext = (props: PropsWithChildren<ChatProps>) => {
enableOfflineSupport = false,
i18nInstance,
isMessageAIGenerated,
maxSyncEventsLimit = DEFAULT_MAX_SYNC_EVENTS_LIMIT,
style,
useNativeMultipartUpload = false,
} = props;
Expand Down Expand Up @@ -224,7 +248,7 @@ const ChatWithContext = (props: PropsWithChildren<ChatProps>) => {

const initializeDatabase = async () => {
if (!client.offlineDb) {
client.setOfflineDBApi(new OfflineDB({ client }));
client.setOfflineDBApi(new OfflineDB({ client, maxSyncEventsLimit }));
}

if (client.offlineDb) {
Expand All @@ -233,7 +257,7 @@ const ChatWithContext = (props: PropsWithChildren<ChatProps>) => {
};

initializeDatabase();
}, [userID, enableOfflineSupport, client]);
}, [userID, enableOfflineSupport, client, maxSyncEventsLimit]);

useEffect(() => {
if (!client) {
Expand Down
39 changes: 38 additions & 1 deletion package/src/components/Chat/__tests__/Chat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import { View } from 'react-native';

import NetInfo from '@react-native-community/netinfo';

import { act, cleanup, render, waitFor } from '@testing-library/react-native';

import type { ChatContextValue } from '../../../contexts/chatContext/ChatContext';
Expand All @@ -13,6 +12,7 @@ import { useTranslationContext } from '../../../contexts/translationContext/Tran
import dispatchConnectionChangedEvent from '../../../mock-builders/event/connectionChanged';
import dispatchConnectionRecoveredEvent from '../../../mock-builders/event/connectionRecovered';
import { getTestClient, getTestClientWithUser, setUser } from '../../../mock-builders/mock';
import { DEFAULT_MAX_SYNC_EVENTS_LIMIT } from '../../../store/constants';
import { Streami18n } from '../../../utils/i18n/Streami18n';
import { Chat } from '../Chat';

Expand Down Expand Up @@ -330,4 +330,41 @@ describe('TranslationContext', () => {
);
});
});

it('forwards maxSyncEventsLimit to the offline DB sync manager', async () => {
const chatClientWithUser = await getTestClientWithUser({ id: 'testID' });

render(<Chat client={chatClientWithUser} enableOfflineSupport maxSyncEventsLimit={42} />);

await waitFor(() => {
expect(chatClientWithUser.offlineDb).toBeDefined();
});
expect(chatClientWithUser.offlineDb!.syncManager.syncMaxEventCount).toBe(42);
});

it('defaults maxSyncEventsLimit to 250 when not provided', async () => {
const chatClientWithUser = await getTestClientWithUser({ id: 'testID' });

render(<Chat client={chatClientWithUser} enableOfflineSupport />);

await waitFor(() => {
expect(chatClientWithUser.offlineDb).toBeDefined();
});
expect(chatClientWithUser.offlineDb!.syncManager.syncMaxEventCount).toBe(
DEFAULT_MAX_SYNC_EVENTS_LIMIT,
);
expect(DEFAULT_MAX_SYNC_EVENTS_LIMIT).toBe(250);
});

it('disables the sync event limit when maxSyncEventsLimit is false', async () => {
const chatClientWithUser = await getTestClientWithUser({ id: 'testID' });

render(<Chat client={chatClientWithUser} enableOfflineSupport maxSyncEventsLimit={false} />);

await waitFor(() => {
expect(chatClientWithUser.offlineDb).toBeDefined();
});
// `false` opts out: the client stores no limit (undefined), so replay always runs.
expect(chatClientWithUser.offlineDb!.syncManager.syncMaxEventCount).toBeUndefined();
});
});
13 changes: 11 additions & 2 deletions package/src/store/OfflineDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@ import * as api from './apis';
import { SqliteClient } from './SqliteClient';

export class OfflineDB extends AbstractOfflineDB {
constructor({ client }: { client: StreamChat }) {
super({ client });
constructor({
client,
maxSyncEventsLimit,
}: {
client: StreamChat;
maxSyncEventsLimit?: number | false;
}) {
super({
client,
syncMaxEventCount: maxSyncEventsLimit === false ? undefined : maxSyncEventsLimit,
});
}

upsertCidsForQuery = api.upsertCidsForQuery;
Expand Down
8 changes: 8 additions & 0 deletions package/src/store/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export const DB_NAME = 'stream-chat-react-native';
export const DB_LOCATION = 'databases';
export const DB_STATUS_ERROR = 1;

/**
* Default value for the `maxSyncEventsLimit` prop on `Chat`. Chosen conservatively
* and below the backend hard cap; tune with performance data. The underlying LLC
* (`stream-chat`) has no default of its own, this default is implied purely by the
* RN SDK.
*/
export const DEFAULT_MAX_SYNC_EVENTS_LIMIT = 250;
51 changes: 30 additions & 21 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7029,7 +7029,7 @@ __metadata:
react-native-teleport: "npm:^1.1.12"
react-native-web: "npm:^0.21.2"
react-native-worklets: "npm:0.11.1"
stream-chat: "npm:^9.50.3"
stream-chat: "npm:^9.51.0"
stream-chat-expo: "workspace:^"
stream-chat-react-native-core: "workspace:^"
typescript: "npm:6.0.3"
Expand Down Expand Up @@ -7524,15 +7524,15 @@ __metadata:
languageName: node
linkType: hard

"axios@npm:^1.16.1":
version: 1.17.0
resolution: "axios@npm:1.17.0"
"axios@npm:^1.19.0":
version: 1.19.0
resolution: "axios@npm:1.19.0"
dependencies:
follow-redirects: "npm:^1.16.0"
form-data: "npm:^4.0.5"
form-data: "npm:^4.0.6"
https-proxy-agent: "npm:^5.0.1"
proxy-from-env: "npm:^2.1.0"
checksum: 10c0/c4fa19ff3a3a63bde48beec03ad816b133b9a6385cccffffe172577ab18c6a70e299280d57f12c80c867fe25df41f92cb91d3a8258708a6d2be3e9e085f92650
checksum: 10c0/559fe7d51291787def61566a3db78b87510c8faf9c8a8c006d9d8b933808628ff0d8eca7756b40ae25e07da96d946c68c1ffba3da9075ed0b5d3661801d76869
languageName: node
linkType: hard

Expand Down Expand Up @@ -11080,16 +11080,16 @@ __metadata:
languageName: node
linkType: hard

"form-data@npm:^4.0.5":
version: 4.0.5
resolution: "form-data@npm:4.0.5"
"form-data@npm:^4.0.6":
version: 4.0.6
resolution: "form-data@npm:4.0.6"
dependencies:
asynckit: "npm:^0.4.0"
combined-stream: "npm:^1.0.8"
es-set-tostringtag: "npm:^2.1.0"
hasown: "npm:^2.0.2"
mime-types: "npm:^2.1.12"
checksum: 10c0/dd6b767ee0bbd6d84039db12a0fa5a2028160ffbfaba1800695713b46ae974a5f6e08b3356c3195137f8530dcd9dfcb5d5ae1eeff53d0db1e5aad863b619ce3b
hasown: "npm:^2.0.4"
mime-types: "npm:^2.1.35"
checksum: 10c0/43947a77bf0ff45c6ceed789778982d47a3f3e720a74b71721174ebf3310a5f1a8be1d6b38a3ee3688e8a18a2c4273073ec0844cd37efda3eaf46d41c9c318ff
languageName: node
linkType: hard

Expand Down Expand Up @@ -11618,6 +11618,15 @@ __metadata:
languageName: node
linkType: hard

"hasown@npm:^2.0.4":
version: 2.0.4
resolution: "hasown@npm:2.0.4"
dependencies:
function-bind: "npm:^1.1.2"
checksum: 10c0/2d8de939e270b70618f8cebb69746620db10617dbb495bc66ddad326955ea24d3ca4af133aff3eb7c1853e0218f867bc2b050ec26fe02e3aea58f880ffc5e506
languageName: node
linkType: hard

"hermes-compiler@npm:250829098.0.14":
version: 250829098.0.14
resolution: "hermes-compiler@npm:250829098.0.14"
Expand Down Expand Up @@ -15006,7 +15015,7 @@ __metadata:
languageName: node
linkType: hard

"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.35, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
"mime-types@npm:^2.1.27, mime-types@npm:^2.1.35, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
Expand Down Expand Up @@ -18101,7 +18110,7 @@ __metadata:
react-native-teleport: "npm:^1.1.12"
react-native-video: "npm:^6.19.2"
react-native-worklets: "npm:^0.11.1"
stream-chat: "npm:^9.50.3"
stream-chat: "npm:^9.51.0"
stream-chat-react-native: "workspace:^"
stream-chat-react-native-core: "workspace:^"
typescript: "npm:6.0.3"
Expand Down Expand Up @@ -18840,7 +18849,7 @@ __metadata:
react-native-worklets: "npm:^0.11.1"
react-test-renderer: "npm:19.2.3"
rimraf: "npm:^6.0.1"
stream-chat: "npm:^9.50.3"
stream-chat: "npm:^9.51.0"
typescript: "npm:6.0.3"
use-sync-external-store: "npm:^1.5.0"
uuid: "npm:^11.1.0"
Expand Down Expand Up @@ -18914,15 +18923,15 @@ __metadata:
languageName: unknown
linkType: soft

"stream-chat@npm:^9.50.3":
version: 9.50.3
resolution: "stream-chat@npm:9.50.3"
"stream-chat@npm:^9.51.0":
version: 9.51.0
resolution: "stream-chat@npm:9.51.0"
dependencies:
"@types/jsonwebtoken": "npm:^9.0.8"
"@types/ws": "npm:^8.18.1"
axios: "npm:^1.16.1"
axios: "npm:^1.19.0"
base64-js: "npm:^1.5.1"
form-data: "npm:^4.0.5"
form-data: "npm:^4.0.6"
isomorphic-ws: "npm:^5.0.0"
jsonwebtoken: "npm:^9.0.3"
linkifyjs: "npm:^4.3.3"
Expand All @@ -18932,7 +18941,7 @@ __metadata:
built: true
husky:
built: true
checksum: 10c0/75be67c6533f6bf02313565eb164115a2b61b399ab268f931689bb709ee75f6a5c68c01baa835082252856b2c307c13c4aa8a6986de20ab0f19cda1f062c7d9b
checksum: 10c0/a2888b1dad9496f8ba35e5afb44f88a6ce6b64785780b0244eba349ee10c52594f43c08a16a23924b14640a50849ce835773f016435725378bf0bbcfe0d636ed
languageName: node
linkType: hard

Expand Down
Loading