Skip to content

Commit a80916e

Browse files
authored
Merge pull request #446 from GetStream/fix-network-error-during-channel-init
fix: Can't recover from error during channel init
2 parents b96ad0e + 0ebbc5c commit a80916e

File tree

7 files changed

+170
-36
lines changed

7 files changed

+170
-36
lines changed

projects/stream-chat-angular/src/lib/channel-list/channel-list.component.spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,29 @@ describe('ChannelListComponent', () => {
8282
it('should display error indicator, if error happened', () => {
8383
expect(queryChatdownContainer()).toBeNull();
8484

85-
channelServiceMock.channels$.error(new Error('error'));
85+
channelServiceMock.channelQueryState$.next({
86+
state: 'error',
87+
error: new Error('error'),
88+
});
8689
fixture.detectChanges();
8790

8891
expect(queryChatdownContainer()).not.toBeNull();
8992
});
9093

9194
it('should display loading indicator, if loading', () => {
95+
channelServiceMock.channelQueryState$.next({
96+
state: 'in-progress',
97+
});
9298
fixture.detectChanges();
9399

94100
expect(queryChatdownContainer()).toBeNull();
95101
expect(queryLoadingIndicator()).not.toBeNull();
96102

97103
const channels = generateMockChannels();
98104
channelServiceMock.channels$.next(channels);
105+
channelServiceMock.channelQueryState$.next({
106+
state: 'success',
107+
});
99108
fixture.detectChanges();
100109

101110
expect(queryLoadingIndicator()).toBeNull();

projects/stream-chat-angular/src/lib/channel-list/channel-list.component.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
TemplateRef,
77
ViewChild,
88
} from '@angular/core';
9-
import { Observable, of, Subscription } from 'rxjs';
10-
import { catchError, map, startWith } from 'rxjs/operators';
9+
import { Observable, Subscription } from 'rxjs';
10+
import { map } from 'rxjs/operators';
1111
import { Channel } from 'stream-chat';
1212
import { ChannelService } from '../channel.service';
1313
import { CustomTemplatesService } from '../custom-templates.service';
@@ -45,14 +45,11 @@ export class ChannelListComponent implements AfterViewInit, OnDestroy {
4545
this.isOpen$ = this.channelListToggleService.isOpen$;
4646
this.channels$ = this.channelService.channels$;
4747
this.hasMoreChannels$ = this.channelService.hasMoreChannels$;
48-
this.isError$ = this.channels$.pipe(
49-
map(() => false),
50-
catchError(() => of(true)),
51-
startWith(false)
48+
this.isError$ = this.channelService.channelQueryState$.pipe(
49+
map((s) => !this.isLoadingMoreChannels && s?.state === 'error')
5250
);
53-
this.isInitializing$ = this.channels$.pipe(
54-
map((channels) => !channels),
55-
catchError(() => of(false))
51+
this.isInitializing$ = this.channelService.channelQueryState$.pipe(
52+
map((s) => !this.isLoadingMoreChannels && s?.state === 'in-progress')
5653
);
5754
this.subscriptions.push(
5855
this.customTemplatesService.channelPreviewTemplate$.subscribe(

projects/stream-chat-angular/src/lib/channel.service.spec.ts

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('ChannelService', () => {
4444
let init: (
4545
c?: Channel<DefaultStreamChatGenerics>[],
4646
sort?: ChannelSort<DefaultStreamChatGenerics>,
47-
options?: ChannelOptions,
47+
options?: ChannelOptions & { keepAliveChannels$OnError?: boolean },
4848
mockChannelQuery?: Function,
4949
shouldSetActiveChannel?: boolean
5050
) => Promise<Channel<DefaultStreamChatGenerics>[]>;
@@ -84,7 +84,7 @@ describe('ChannelService', () => {
8484
init = async (
8585
channels?: Channel<DefaultStreamChatGenerics>[],
8686
sort?: ChannelSort<DefaultStreamChatGenerics>,
87-
options?: ChannelOptions,
87+
options?: ChannelOptions & { keepAliveChannels$OnError?: boolean },
8888
mockChannelQuery?: Function,
8989
shouldSetActiveChannel?: boolean
9090
) => {
@@ -118,7 +118,7 @@ describe('ChannelService', () => {
118118
expect(mockChatClient.queryChannels).toHaveBeenCalledWith(
119119
jasmine.any(Object),
120120
jasmine.any(Object),
121-
options
121+
jasmine.objectContaining(options)
122122
);
123123
});
124124

@@ -179,6 +179,70 @@ describe('ChannelService', () => {
179179
);
180180
});
181181

182+
it('should handle errors during channel load', fakeAsync(() => {
183+
const spy = jasmine.createSpy();
184+
service.channels$.subscribe(spy);
185+
const activeChannelSpy = jasmine.createSpy();
186+
service.activeChannel$.subscribe(activeChannelSpy);
187+
188+
try {
189+
void init(undefined, undefined, { keepAliveChannels$OnError: true }, () =>
190+
mockChatClient.queryChannels.and.rejectWith('there was an error')
191+
);
192+
tick();
193+
// eslint-disable-next-line no-empty
194+
} catch (error) {}
195+
196+
const channels = generateMockChannels();
197+
mockChatClient.queryChannels.and.resolveTo(channels);
198+
spy.calls.reset();
199+
activeChannelSpy.calls.reset();
200+
const notificationService = TestBed.inject(NotificationService);
201+
const notificationSpy = jasmine.createSpy();
202+
notificationService.notifications$.subscribe(notificationSpy);
203+
notificationSpy.calls.reset();
204+
events$.next({ eventType: 'connection.recovered' } as ClientEvent);
205+
206+
tick();
207+
208+
expect(spy).toHaveBeenCalledWith(channels);
209+
expect(activeChannelSpy).toHaveBeenCalledWith(channels[0]);
210+
expect(notificationSpy).toHaveBeenCalledWith([]);
211+
}));
212+
213+
it('should emit channel query state correctly', async () => {
214+
const spy = jasmine.createSpy();
215+
service.channelQueryState$.subscribe(spy);
216+
217+
await init();
218+
219+
let calls = spy.calls.all();
220+
221+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
222+
expect(calls[1].args[0].state).toBe('in-progress');
223+
224+
expect(calls[2].args[0].state).toBe('success');
225+
226+
spy.calls.reset();
227+
228+
await expectAsync(
229+
init(undefined, undefined, undefined, () =>
230+
mockChatClient.queryChannels.and.rejectWith('there was an error')
231+
)
232+
).toBeRejected();
233+
234+
calls = spy.calls.all();
235+
236+
expect(calls[0].args[0]?.state).toBe('in-progress');
237+
238+
expect(calls[1].args[0]).toEqual({
239+
state: 'error',
240+
error: 'there was an error',
241+
});
242+
243+
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
244+
});
245+
182246
it('should not set active channel if #shouldSetActiveChannel is false', async () => {
183247
const activeChannelSpy = jasmine.createSpy();
184248
service.activeChannel$.subscribe(activeChannelSpy);
@@ -204,13 +268,16 @@ describe('ChannelService', () => {
204268
service.jumpToMessage$.subscribe(jumpToMessageSpy);
205269
const pinnedMessagesSpy = jasmine.createSpy();
206270
service.activeChannelPinnedMessages$.subscribe(pinnedMessagesSpy);
271+
const channelsQueryStateSpy = jasmine.createSpy();
272+
service.channelQueryState$.subscribe(channelsQueryStateSpy);
207273
messagesSpy.calls.reset();
208274
activeChannelSpy.calls.reset();
209275
channelsSpy.calls.reset();
210276
messageToQuoteSpy.calls.reset();
211277
latestMessagesSpy.calls.reset();
212278
jumpToMessageSpy.calls.reset();
213279
pinnedMessagesSpy.calls.reset();
280+
channelsQueryStateSpy.calls.reset();
214281
service.reset();
215282

216283
expect(messagesSpy).toHaveBeenCalledWith([]);
@@ -219,6 +286,7 @@ describe('ChannelService', () => {
219286
expect(messageToQuoteSpy).toHaveBeenCalledWith(undefined);
220287
expect(latestMessagesSpy).toHaveBeenCalledWith({});
221288
expect(pinnedMessagesSpy).toHaveBeenCalledWith([]);
289+
expect(channelsQueryStateSpy).toHaveBeenCalledWith(undefined);
222290

223291
channelsSpy.calls.reset();
224292
events$.next({

projects/stream-chat-angular/src/lib/channel.service.ts

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { NotificationService } from './notification.service';
2929
import { getReadBy } from './read-by';
3030
import {
3131
AttachmentUpload,
32+
ChannelQueryState,
3233
DefaultStreamChatGenerics,
3334
MessageReactionType,
3435
StreamMessage,
@@ -73,6 +74,10 @@ export class ChannelService<
7374
* Our platform documentation covers the topic of [channel events](https://getstream.io/chat/docs/javascript/event_object/?language=javascript#events) in depth.
7475
*/
7576
channels$: Observable<Channel<T>[] | undefined>;
77+
/**
78+
* The result of the latest channel query request.
79+
*/
80+
channelQueryState$: Observable<ChannelQueryState | undefined>;
7681
/**
7782
* Emits the currently active channel.
7883
*
@@ -264,7 +269,9 @@ export class ChannelService<
264269
}>({});
265270
private filters: ChannelFilters<T> | undefined;
266271
private sort: ChannelSort<T> | undefined;
267-
private options: ChannelOptions | undefined;
272+
private options:
273+
| (ChannelOptions & { keepAliveChannels$OnError?: boolean })
274+
| undefined;
268275
private readonly messagePageSize = 25;
269276
private messageToQuoteSubject = new BehaviorSubject<
270277
StreamMessage<T> | undefined
@@ -279,6 +286,9 @@ export class ChannelService<
279286
private shouldSetActiveChannel: boolean | undefined;
280287
private clientEventsSubscription: Subscription | undefined;
281288
private isStateRecoveryInProgress = false;
289+
private channelQueryStateSubject = new BehaviorSubject<
290+
ChannelQueryState | undefined
291+
>(undefined);
282292

283293
private channelListSetter = (
284294
channels: (Channel<T> | ChannelResponse<T>)[]
@@ -305,6 +315,7 @@ export class ChannelService<
305315
private parentMessageSetter = (message: StreamMessage<T> | undefined) => {
306316
this.activeParentMessageIdSubject.next(message?.id);
307317
};
318+
private dismissErrorNotification?: Function;
308319

309320
constructor(
310321
private chatClientService: ChatClientService<T>,
@@ -366,6 +377,7 @@ export class ChannelService<
366377
this.latestMessageDateByUserByChannelsSubject.asObservable();
367378
this.activeChannelPinnedMessages$ =
368379
this.activeChannelPinnedMessagesSubject.asObservable();
380+
this.channelQueryState$ = this.channelQueryStateSubject.asObservable();
369381
}
370382

371383
/**
@@ -554,31 +566,33 @@ export class ChannelService<
554566
async init(
555567
filters: ChannelFilters<T>,
556568
sort?: ChannelSort<T>,
557-
options?: ChannelOptions,
569+
options?: ChannelOptions & { keepAliveChannels$OnError?: boolean },
558570
shouldSetActiveChannel: boolean = true
559571
) {
560572
this.filters = filters;
561-
this.options = options || {
573+
this.options = {
562574
offset: 0,
563575
limit: 25,
564576
state: true,
565577
presence: true,
566578
watch: true,
567579
message_limit: this.messagePageSize,
580+
...options,
568581
};
569582
this.sort = sort || { last_message_at: -1, updated_at: -1 };
570583
this.shouldSetActiveChannel = shouldSetActiveChannel;
584+
this.clientEventsSubscription = this.chatClientService.events$.subscribe(
585+
(notification) => void this.handleNotification(notification)
586+
);
571587
try {
572588
const result = await this.queryChannels(this.shouldSetActiveChannel);
573-
this.clientEventsSubscription = this.chatClientService.events$.subscribe(
574-
(notification) => void this.handleNotification(notification)
575-
);
576589
return result;
577590
} catch (error) {
578-
this.notificationService.addPermanentNotification(
579-
'streamChat.Error loading channels',
580-
'error'
581-
);
591+
this.dismissErrorNotification =
592+
this.notificationService.addPermanentNotification(
593+
'streamChat.Error loading channels',
594+
'error'
595+
);
582596
throw error;
583597
}
584598
}
@@ -589,7 +603,10 @@ export class ChannelService<
589603
reset() {
590604
this.deselectActiveChannel();
591605
this.channelsSubject.next(undefined);
606+
this.channelQueryStateSubject.next(undefined);
592607
this.clientEventsSubscription?.unsubscribe();
608+
this.dismissErrorNotification?.();
609+
this.dismissErrorNotification = undefined;
593610
}
594611

595612
/**
@@ -973,7 +990,11 @@ export class ChannelService<
973990
if (this.options) {
974991
this.options.offset = 0;
975992
}
976-
await this.queryChannels(false, true);
993+
// If channel list is not inited, we set the active channel
994+
const shoulSetActiveChannel =
995+
this.shouldSetActiveChannel &&
996+
!this.activeChannelSubject.getValue();
997+
await this.queryChannels(shoulSetActiveChannel || false, true);
977998
// Thread messages are not refetched so active thread gets deselected to avoid displaying stale messages
978999
void this.setAsActiveParentMessage(undefined);
9791000
this.isStateRecoveryInProgress = false;
@@ -1264,6 +1285,7 @@ export class ChannelService<
12641285
recoverState = false
12651286
) {
12661287
try {
1288+
this.channelQueryStateSubject.next({ state: 'in-progress' });
12671289
const channels = await this.chatClientService.chatClient.queryChannels(
12681290
this.filters!,
12691291
this.sort || {},
@@ -1274,13 +1296,14 @@ export class ChannelService<
12741296
? []
12751297
: this.channelsSubject.getValue() || [];
12761298
this.channelsSubject.next([...prevChannels, ...channels]);
1277-
const currentActiveChannel = this.activeChannelSubject.getValue();
1299+
let currentActiveChannel = this.activeChannelSubject.getValue();
12781300
if (
12791301
channels.length > 0 &&
12801302
!currentActiveChannel &&
12811303
shouldSetActiveChannel
12821304
) {
12831305
this.setAsActiveChannel(channels[0]);
1306+
currentActiveChannel = this.activeChannelSubject.getValue();
12841307
}
12851308
if (
12861309
recoverState &&
@@ -1289,9 +1312,23 @@ export class ChannelService<
12891312
this.deselectActiveChannel();
12901313
}
12911314
this.hasMoreChannelsSubject.next(channels.length >= this.options!.limit!);
1315+
this.channelQueryStateSubject.next({ state: 'success' });
1316+
if (
1317+
this.options?.keepAliveChannels$OnError &&
1318+
this.dismissErrorNotification
1319+
) {
1320+
this.dismissErrorNotification();
1321+
}
12921322
return channels;
12931323
} catch (error) {
1294-
this.channelsSubject.error(error);
1324+
if (!this.options?.keepAliveChannels$OnError) {
1325+
this.channelsSubject.error(error);
1326+
}
1327+
this.channelQueryStateSubject.next({
1328+
state: 'error',
1329+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1330+
error,
1331+
});
12951332
throw error;
12961333
}
12971334
}

projects/stream-chat-angular/src/lib/channel/channel.component.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component } from '@angular/core';
2-
import { Observable, of, Subscription } from 'rxjs';
3-
import { catchError, map, startWith } from 'rxjs/operators';
2+
import { combineLatest, Observable, Subscription } from 'rxjs';
3+
import { map } from 'rxjs/operators';
44
import { ChannelService } from '../channel.service';
55
import { ThemeService } from '../theme.service';
66

@@ -24,14 +24,21 @@ export class ChannelComponent {
2424
private channelService: ChannelService,
2525
private themeService: ThemeService
2626
) {
27-
this.isError$ = this.channelService.channels$.pipe(
28-
map(() => false),
29-
catchError(() => of(true)),
30-
startWith(false)
27+
this.isError$ = combineLatest([
28+
this.channelService.channelQueryState$,
29+
this.channelService.activeChannel$,
30+
]).pipe(
31+
map(([state, activeChannel]) => {
32+
return !activeChannel && state?.state === 'error';
33+
})
3134
);
32-
this.isInitializing$ = this.channelService.channels$.pipe(
33-
map((channels) => !channels),
34-
catchError(() => of(false))
35+
this.isInitializing$ = combineLatest([
36+
this.channelService.channelQueryState$,
37+
this.channelService.activeChannel$,
38+
]).pipe(
39+
map(([state, activeChannel]) => {
40+
return !activeChannel && state?.state === 'in-progress';
41+
})
3542
);
3643
this.isActiveThread$ = this.channelService.activeParentMessageId$.pipe(
3744
map((id) => !!id)

0 commit comments

Comments
 (0)