Skip to content

Commit f63c7ca

Browse files
committed
feat: support missing ChannelManager features in BasePaginator
Support setting paginator items directly, optional request retries, offline DB in ChannelPaginator, identification of pagination restart based on query shape change.
1 parent 1bc55d9 commit f63c7ca

13 files changed

+1919
-552
lines changed

src/ChannelPaginatorsOrchestrator.ts

Lines changed: 151 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Unsubscribe } from './store';
77
import { StateStore } from './store';
88
import type {
99
EventHandlerPipelineHandler,
10+
FindEventHandlerParams,
1011
InsertEventHandlerPayload,
1112
LabeledEventHandler,
1213
} from './EventHandlerPipeline';
@@ -17,11 +18,32 @@ export type ChannelPaginatorsOrchestratorEventHandlerContext = {
1718
orchestrator: ChannelPaginatorsOrchestrator;
1819
};
1920

21+
type EventHandlerContext = ChannelPaginatorsOrchestratorEventHandlerContext;
22+
2023
type SupportedEventType = EventTypes | (string & {});
2124

22-
const reEmit: EventHandlerPipelineHandler<
23-
ChannelPaginatorsOrchestratorEventHandlerContext
24-
> = ({ event, ctx: { orchestrator } }) => {
25+
const getCachedChannelFromEvent = (
26+
event: Event,
27+
cache: Record<string, Channel>,
28+
): Channel | undefined => {
29+
let channel: Channel | undefined = undefined;
30+
if (event.cid) {
31+
channel = cache[event.cid];
32+
} else if (event.channel_id && event.channel_type) {
33+
// todo: is there a central method to construct the cid from type and channel id?
34+
channel = cache[`${event.channel_type}:${event.channel_id}`];
35+
} else if (event.channel) {
36+
channel = cache[event.channel.cid];
37+
} else {
38+
return;
39+
}
40+
return channel;
41+
};
42+
43+
const reEmit: EventHandlerPipelineHandler<EventHandlerContext> = ({
44+
event,
45+
ctx: { orchestrator },
46+
}) => {
2547
if (!event.cid) return;
2648
const channel = orchestrator.client.activeChannels[event.cid];
2749
if (!channel) return;
@@ -33,31 +55,37 @@ const reEmit: EventHandlerPipelineHandler<
3355
});
3456
};
3557

36-
const removeItem: EventHandlerPipelineHandler<
37-
ChannelPaginatorsOrchestratorEventHandlerContext
38-
> = ({ event, ctx: { orchestrator } }) => {
58+
const removeItem: EventHandlerPipelineHandler<EventHandlerContext> = ({
59+
event,
60+
ctx: { orchestrator },
61+
}) => {
3962
if (!event.cid) return;
4063
const channel = orchestrator.client.activeChannels[event.cid];
4164
orchestrator.paginators.forEach((paginator) => {
4265
paginator.removeItem({ id: event.cid, item: channel });
4366
});
4467
};
4568

46-
const updateLists: EventHandlerPipelineHandler<
47-
ChannelPaginatorsOrchestratorEventHandlerContext
48-
> = async ({ event, ctx: { orchestrator } }) => {
49-
let channel: Channel | undefined = undefined;
50-
if (event.cid) {
51-
channel = orchestrator.client.activeChannels[event.cid];
52-
} else if (event.channel_id && event.channel_type) {
53-
// todo: is there a central method to construct the cid from type and channel id?
54-
channel =
55-
orchestrator.client.activeChannels[`${event.channel_type}:${event.channel_id}`];
56-
} else if (event.channel) {
57-
channel = orchestrator.client.activeChannels[event.channel.cid];
58-
} else {
59-
return;
60-
}
69+
// todo: documentation: show how to implement allowNewMessagesFromUnfilteredChannels just by inserting event handler
70+
// at the start of the handler pipeline and filter out events for unknown channels
71+
export const ignoreEventsForUnknownChannels: EventHandlerPipelineHandler<
72+
EventHandlerContext
73+
> = ({ event, ctx: { orchestrator } }) => {
74+
const channel: Channel | undefined = getCachedChannelFromEvent(
75+
event,
76+
orchestrator.client.activeChannels,
77+
);
78+
if (!channel) return { action: 'stop' };
79+
};
80+
81+
const updateLists: EventHandlerPipelineHandler<EventHandlerContext> = async ({
82+
event,
83+
ctx: { orchestrator },
84+
}) => {
85+
let channel: Channel | undefined = getCachedChannelFromEvent(
86+
event,
87+
orchestrator.client.activeChannels,
88+
);
6189

6290
if (!channel) {
6391
const [type, id] = event.cid
@@ -96,104 +124,91 @@ const updateLists: EventHandlerPipelineHandler<
96124
};
97125

98126
// we have to make sure that client.activeChannels is always up-to-date
99-
const channelDeletedHandler: LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext> =
100-
{
101-
handle: removeItem,
102-
id: 'ChannelPaginatorsOrchestrator:default-handler:channel.deleted',
103-
};
127+
const channelDeletedHandler: LabeledEventHandler<EventHandlerContext> = {
128+
handle: removeItem,
129+
id: 'ChannelPaginatorsOrchestrator:default-handler:channel.deleted',
130+
};
104131

105132
// fixme: this handler should not be handled by the orchestrator but as Channel does not have reactive state,
106133
// we need to re-emit the whole list to reflect the changes
107-
const channelUpdatedHandler: LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext> =
108-
{
109-
handle: reEmit,
110-
id: 'ChannelPaginatorsOrchestrator:default-handler:channel.updated',
111-
};
134+
const channelUpdatedHandler: LabeledEventHandler<EventHandlerContext> = {
135+
handle: reEmit,
136+
id: 'ChannelPaginatorsOrchestrator:default-handler:channel.updated',
137+
};
112138

113139
// fixme: this handler should not be handled by the orchestrator but as Channel does not have reactive state,
114140
// we need to re-emit the whole list to reflect the changes
115-
const channelTruncatedHandler: LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext> =
116-
{
117-
handle: reEmit,
118-
id: 'ChannelPaginatorsOrchestrator:default-handler:channel.truncated',
119-
};
120-
121-
const channelVisibleHandler: LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext> =
122-
{
123-
handle: updateLists,
124-
id: 'ChannelPaginatorsOrchestrator:default-handler:channel.visible',
125-
};
141+
const channelTruncatedHandler: LabeledEventHandler<EventHandlerContext> = {
142+
handle: reEmit,
143+
id: 'ChannelPaginatorsOrchestrator:default-handler:channel.truncated',
144+
};
145+
146+
const channelVisibleHandler: LabeledEventHandler<EventHandlerContext> = {
147+
handle: updateLists,
148+
id: 'ChannelPaginatorsOrchestrator:default-handler:channel.visible',
149+
};
126150

127151
// members filter - should not be impacted as id is stable - cannot be updated
128152
// member.user.name - can be impacted
129-
const memberUpdatedHandler: LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext> =
130-
{
131-
handle: updateLists,
132-
id: 'ChannelPaginatorsOrchestrator:default-handler:member.updated',
133-
};
134-
135-
const messageNewHandler: LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext> =
136-
{
137-
handle: updateLists,
138-
id: 'ChannelPaginatorsOrchestrator:default-handler:message.new',
139-
};
140-
141-
const notificationAddedToChannelHandler: LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext> =
142-
{
143-
handle: updateLists,
144-
id: 'ChannelPaginatorsOrchestrator:default-handler:notification.added_to_channel',
145-
};
146-
147-
const notificationMessageNewHandler: LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext> =
148-
{
149-
handle: updateLists,
150-
id: 'ChannelPaginatorsOrchestrator:default-handler:notification.message_new',
151-
};
152-
153-
const notificationRemovedFromChannelHandler: LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext> =
154-
{
155-
handle: removeItem,
156-
id: 'ChannelPaginatorsOrchestrator:default-handler:notification.removed_from_channel',
157-
};
153+
const memberUpdatedHandler: LabeledEventHandler<EventHandlerContext> = {
154+
handle: updateLists,
155+
id: 'ChannelPaginatorsOrchestrator:default-handler:member.updated',
156+
};
157+
158+
const messageNewHandler: LabeledEventHandler<EventHandlerContext> = {
159+
handle: updateLists,
160+
id: 'ChannelPaginatorsOrchestrator:default-handler:message.new',
161+
};
162+
163+
const notificationAddedToChannelHandler: LabeledEventHandler<EventHandlerContext> = {
164+
handle: updateLists,
165+
id: 'ChannelPaginatorsOrchestrator:default-handler:notification.added_to_channel',
166+
};
167+
168+
const notificationMessageNewHandler: LabeledEventHandler<EventHandlerContext> = {
169+
handle: updateLists,
170+
id: 'ChannelPaginatorsOrchestrator:default-handler:notification.message_new',
171+
};
172+
173+
const notificationRemovedFromChannelHandler: LabeledEventHandler<EventHandlerContext> = {
174+
handle: removeItem,
175+
id: 'ChannelPaginatorsOrchestrator:default-handler:notification.removed_from_channel',
176+
};
158177

159178
// fixme: updates users for member object in all the channels which are loaded with that member - normalization would be beneficial
160-
const userPresenceChangedHandler: LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext> =
161-
{
162-
handle: ({ event, ctx: { orchestrator } }) => {
163-
const eventUser = event.user;
164-
if (!eventUser?.id) return;
165-
orchestrator.paginators.forEach((paginator) => {
166-
const paginatorItems = paginator.items;
167-
if (!paginatorItems) return;
168-
let updated = false;
169-
paginatorItems.forEach((channel) => {
170-
if (channel.state.members[eventUser.id]) {
171-
channel.state.members[eventUser.id].user = event.user;
172-
updated = true;
173-
}
174-
if (channel.state.membership.user?.id === eventUser.id) {
175-
channel.state.membership.user = eventUser;
176-
updated = true;
177-
}
178-
});
179-
if (updated) {
180-
// fixme: user is not reactive and so the whole list has to be re-rendered
181-
paginator.state.partialNext({ items: [...paginatorItems] });
179+
const userPresenceChangedHandler: LabeledEventHandler<EventHandlerContext> = {
180+
handle: ({ event, ctx: { orchestrator } }) => {
181+
const eventUser = event.user;
182+
if (!eventUser?.id) return;
183+
orchestrator.paginators.forEach((paginator) => {
184+
const paginatorItems = paginator.items;
185+
if (!paginatorItems) return;
186+
let updated = false;
187+
paginatorItems.forEach((channel) => {
188+
if (channel.state.members[eventUser.id]) {
189+
channel.state.members[eventUser.id].user = event.user;
190+
updated = true;
191+
}
192+
if (channel.state.membership.user?.id === eventUser.id) {
193+
channel.state.membership.user = eventUser;
194+
updated = true;
182195
}
183196
});
184-
},
185-
id: 'ChannelPaginatorsOrchestrator:default-handler:user.presence.changed',
186-
};
197+
if (updated) {
198+
// fixme: user is not reactive and so the whole list has to be re-rendered
199+
paginator.state.partialNext({ items: [...paginatorItems] });
200+
}
201+
});
202+
},
203+
id: 'ChannelPaginatorsOrchestrator:default-handler:user.presence.changed',
204+
};
187205

188206
export type ChannelPaginatorsOrchestratorState = {
189207
paginators: ChannelPaginator[];
190208
};
191209

192210
export type ChannelPaginatorsOrchestratorEventHandlers = Partial<
193-
Record<
194-
SupportedEventType,
195-
LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext>[]
196-
>
211+
Record<SupportedEventType, LabeledEventHandler<EventHandlerContext>[]>
197212
>;
198213

199214
export type ChannelPaginatorsOrchestratorOptions = {
@@ -205,14 +220,15 @@ export type ChannelPaginatorsOrchestratorOptions = {
205220
export class ChannelPaginatorsOrchestrator extends WithSubscriptions {
206221
client: StreamChat;
207222
state: StateStore<ChannelPaginatorsOrchestratorState>;
208-
protected pipelines = new Map<
223+
protected _pipelines = new Map<
209224
SupportedEventType,
210-
EventHandlerPipeline<ChannelPaginatorsOrchestratorEventHandlerContext>
225+
EventHandlerPipeline<EventHandlerContext>
211226
>();
212227

213228
protected static readonly defaultEventHandlers: ChannelPaginatorsOrchestratorEventHandlers =
214229
{
215230
'channel.deleted': [channelDeletedHandler],
231+
'channel.hidden': [channelDeletedHandler],
216232
'channel.updated': [channelUpdatedHandler],
217233
'channel.truncated': [channelTruncatedHandler],
218234
'channel.visible': [channelVisibleHandler],
@@ -243,7 +259,11 @@ export class ChannelPaginatorsOrchestrator extends WithSubscriptions {
243259
return this.state.getLatestValue().paginators;
244260
}
245261

246-
private get ctx(): ChannelPaginatorsOrchestratorEventHandlerContext {
262+
get pipelines(): Map<SupportedEventType, EventHandlerPipeline<EventHandlerContext>> {
263+
return this._pipelines;
264+
}
265+
266+
private get ctx(): EventHandlerContext {
247267
return { orchestrator: this };
248268
}
249269

@@ -291,17 +311,39 @@ export class ChannelPaginatorsOrchestrator extends WithSubscriptions {
291311
...payload
292312
}: {
293313
eventType: SupportedEventType;
294-
} & InsertEventHandlerPayload<ChannelPaginatorsOrchestratorEventHandlerContext>): Unsubscribe {
314+
} & InsertEventHandlerPayload<EventHandlerContext>): Unsubscribe {
295315
return this.ensurePipeline(eventType).insert(payload);
296316
}
297317

318+
setEventHandlers({
319+
eventType,
320+
handlers,
321+
}: {
322+
eventType: SupportedEventType;
323+
handlers: LabeledEventHandler<EventHandlerContext>[];
324+
}) {
325+
return this.ensurePipeline(eventType).replaceAll(handlers);
326+
}
327+
328+
removeEventHandlers({
329+
eventType,
330+
handlers,
331+
}: {
332+
eventType: SupportedEventType;
333+
handlers: FindEventHandlerParams<EventHandlerContext>[];
334+
}) {
335+
const pipeline = this._pipelines.get(eventType);
336+
if (!pipeline) return;
337+
handlers.forEach((params) => pipeline.remove(params));
338+
}
339+
298340
/** Subscribe to WS (and more buses via attachBus) */
299341
registerSubscriptions(): Unsubscribe {
300342
if (!this.hasSubscriptions) {
301343
this.addUnsubscribeFunction(
302344
// todo: maybe we should have a wrapper here to decide, whether the event is a LocalEventBus event or else supported by client
303345
this.client.on((event: Event) => {
304-
const pipe = this.pipelines.get(event.type);
346+
const pipe = this._pipelines.get(event.type);
305347
if (pipe) {
306348
pipe.run(event, this.ctx);
307349
}
@@ -315,13 +357,13 @@ export class ChannelPaginatorsOrchestrator extends WithSubscriptions {
315357

316358
ensurePipeline(
317359
eventType: SupportedEventType,
318-
): EventHandlerPipeline<ChannelPaginatorsOrchestratorEventHandlerContext> {
319-
let pipe = this.pipelines.get(eventType);
360+
): EventHandlerPipeline<EventHandlerContext> {
361+
let pipe = this._pipelines.get(eventType);
320362
if (!pipe) {
321-
pipe = new EventHandlerPipeline<ChannelPaginatorsOrchestratorEventHandlerContext>({
363+
pipe = new EventHandlerPipeline<EventHandlerContext>({
322364
id: `ChannelPaginatorsOrchestrator:${eventType}`,
323365
});
324-
this.pipelines.set(eventType, pipe);
366+
this._pipelines.set(eventType, pipe);
325367
}
326368
return pipe;
327369
}

0 commit comments

Comments
 (0)