diff --git a/src/channel_manager.ts b/src/channel_manager.ts index 1c9208400..f09ea07c2 100644 --- a/src/channel_manager.ts +++ b/src/channel_manager.ts @@ -317,7 +317,19 @@ export class ChannelManager extends WithSubscriptions { `Maximum number of retries reached in queryChannels. Last error message is: ${err}`, ); - this.state.partialNext({ error: wrappedError }); + const state = this.state.getLatestValue(); + // If the offline support is enabled, and there are channels in the DB, we should not error out. + const isOfflineSupportEnabledWithChannels = + this.client.offlineDb && state.channels.length > 0; + + this.state.partialNext({ + error: isOfflineSupportEnabledWithChannels ? undefined : wrappedError, + pagination: { + ...state.pagination, + isLoading: false, + isLoadingNext: false, + }, + }); return; } @@ -444,7 +456,11 @@ export class ChannelManager extends WithSubscriptions { this.client.logger('error', (error as Error).message); this.state.next((currentState) => ({ ...currentState, - pagination: { ...currentState.pagination, isLoadingNext: false }, + pagination: { + ...currentState.pagination, + isLoadingNext: false, + isLoading: false, + }, })); throw error; } diff --git a/src/offline-support/offline_sync_manager.ts b/src/offline-support/offline_sync_manager.ts index 4499e493f..28e706ef9 100644 --- a/src/offline-support/offline_sync_manager.ts +++ b/src/offline-support/offline_sync_manager.ts @@ -1,6 +1,9 @@ import type { ExecuteBatchDBQueriesType } from './types'; import type { StreamChat } from '../client'; import type { AbstractOfflineDB } from './offline_support_api'; +import type { AxiosError } from 'axios'; +import { isAxiosError } from 'axios'; +import type { APIErrorResponse } from '../types'; /** * Manages synchronization between the local offline database and the Stream backend. @@ -174,8 +177,21 @@ export class OfflineDBSyncManager { }); } catch (e) { console.log('An error has occurred while syncing the DB.', e); + + if (isAxiosError(e) && e.code === 'ECONNABORTED') { + // If the sync was aborted due to timeout, we can simply return + return; + } + + const error = e as AxiosError; + + if (error.response?.data?.code === 23) { + return; + } + // Error will be raised by the sync API if there are too many events. // In that case reset the entire DB and start fresh. + // We avoid resetting the DB if the error is due to timeout. await this.offlineDb.resetDB(); } }; diff --git a/test/unit/channel_manager.test.ts b/test/unit/channel_manager.test.ts index c96747688..41bc57ae5 100644 --- a/test/unit/channel_manager.test.ts +++ b/test/unit/channel_manager.test.ts @@ -73,7 +73,9 @@ describe('ChannelManager', () => { }); it('should properly set eventHandlerOverrides, options and queryChannelsRequest if they are passed', async () => { - const eventHandlerOverrides = { newMessageHandler: () => {} }; + const eventHandlerOverrides = { + newMessageHandler: () => {}, + }; const options = { allowNotLoadedChannelPromotionForEvent: { 'channel.visible': false, @@ -805,6 +807,45 @@ describe('ChannelManager', () => { expect(initialized).to.be.false; }); + it('should not set error when offline support is enabled and there are channels in the DB', async () => { + clientQueryChannelsStub.callsFake(() => mockChannelPages[2]); + client.setOfflineDBApi(new MockOfflineDB({ client })); + await client.offlineDb!.init(client.userID as string); + + channelManager.state.partialNext({ + channels: mockChannelPages[2], + }); + + clientQueryChannelsStub.rejects(new Error('fail')); + const sleepSpy = vi.spyOn(utils, 'sleep'); + const stateChangeSpy = sinon.spy(); + channelManager.state.subscribeWithSelector( + (nextValue) => ({ + error: nextValue.error, + }), + stateChangeSpy, + ); + stateChangeSpy.resetHistory(); + + await channelManager['executeChannelsQuery']({ + filters: { filterA: true }, + sort: { asc: 1 }, + options: { limit: 10, offset: 0 }, + }); + + const { channels, initialized, error, pagination } = + channelManager.state.getLatestValue(); + + expect(clientQueryChannelsStub.callCount).to.equal( + DEFAULT_QUERY_CHANNELS_RETRY_COUNT + 1, + ); // initial + however many retried are configured + expect(sleepSpy).toHaveBeenCalledTimes(DEFAULT_QUERY_CHANNELS_RETRY_COUNT); + expect(error).toEqual(undefined); + expect(channels.length).to.equal(5); + expect(initialized).to.be.false; + expect(pagination.isLoading).to.be.false; + }); + it('does not retry more than 3 times', async () => { clientQueryChannelsStub.rejects(new Error('fail')); const sleepSpy = vi.spyOn(utils, 'sleep'); diff --git a/test/unit/offline-support/offline_support_api.test.ts b/test/unit/offline-support/offline_support_api.test.ts index 38bacfe93..84a3322f4 100644 --- a/test/unit/offline-support/offline_support_api.test.ts +++ b/test/unit/offline-support/offline_support_api.test.ts @@ -2325,6 +2325,42 @@ describe('OfflineDBSyncManager', () => { expect(upsertUserSyncStatusSpy).toHaveBeenCalled(); }); + it('do not reset the DB if sync API throws an AxiosError with request timeout', async () => { + const recentDate = new Date(); + recentDate.setDate(recentDate.getDate() - 10); + getLastSyncedAtSpy.mockResolvedValueOnce(recentDate.toString()); + + const axiosError = { + isAxiosError: true, + code: 'ECONNABORTED', + response: { data: { code: 4 } }, + } as AxiosError; + + syncApiSpy.mockRejectedValueOnce(axiosError); + + await (syncManager as any).sync(); + + expect(resetDBSpy).not.toHaveBeenCalled(); + expect(upsertUserSyncStatusSpy).not.toHaveBeenCalled(); + }); + + it('do not reset the DB if sync API throws a BE error with response timeout', async () => { + const recentDate = new Date(); + recentDate.setDate(recentDate.getDate() - 10); + getLastSyncedAtSpy.mockResolvedValueOnce(recentDate.toString()); + + const axiosError = { + response: { data: { code: 23 } }, + } as AxiosError; + + syncApiSpy.mockRejectedValueOnce(axiosError); + + await (syncManager as any).sync(); + + expect(resetDBSpy).not.toHaveBeenCalled(); + expect(upsertUserSyncStatusSpy).not.toHaveBeenCalled(); + }); + it('resets DB if sync API throws an error', async () => { const recentDate = new Date(); recentDate.setDate(recentDate.getDate() - 10);