Skip to content

Commit 553cbe8

Browse files
committed
feat: allow to keep channels in certain matching paginators and not in other matching paginators
1 parent 6947159 commit 553cbe8

File tree

3 files changed

+96
-1
lines changed

3 files changed

+96
-1
lines changed

src/ChannelPaginatorsOrchestrator.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ export class ChannelPaginatorsOrchestrator extends WithSubscriptions {
294294
EventHandlerPipeline<EventHandlerContext>
295295
>();
296296
protected ownershipResolver?: PaginatorOwnershipResolver;
297+
/** Track paginators already wrapped with ownership-aware filtering */
298+
protected _wrappedForOwnership = new WeakSet<ChannelPaginator>();
297299

298300
protected static readonly defaultEventHandlers: ChannelPaginatorsOrchestratorEventHandlers =
299301
{
@@ -330,6 +332,8 @@ export class ChannelPaginatorsOrchestrator extends WithSubscriptions {
330332
for (const [type, handlers] of Object.entries(finalEventHandlers)) {
331333
if (handlers) this.ensurePipeline(type).replaceAll(handlers);
332334
}
335+
// Ensure ownership rules are applied to initial paginators' query results
336+
this.paginators.forEach((p) => this.wrapPaginatorFiltering(p));
333337
}
334338

335339
get paginators(): ChannelPaginator[] {
@@ -369,6 +373,46 @@ export class ChannelPaginatorsOrchestrator extends WithSubscriptions {
369373
return new Set(this.ownershipResolver?.({ channel, matchingPaginators }) ?? []);
370374
}
371375

376+
/**
377+
* Filter a page of query results for a specific paginator according to ownership rules.
378+
* If no owners are specified by the resolver, all matching paginators keep the item.
379+
*/
380+
protected filterItemsByOwnership({
381+
paginator,
382+
items,
383+
}: {
384+
paginator: ChannelPaginator;
385+
items: Channel[];
386+
}): Channel[] {
387+
if (!items.length) return items;
388+
const result: Channel[] = [];
389+
for (const ch of items) {
390+
const matchingPaginators = this.paginators.filter((p) => p.matchesFilter(ch));
391+
const ownerIds = this.resolveOwnership(ch, matchingPaginators);
392+
const noOwnersOrPaginatorIsOwner =
393+
ownerIds.size === 0 || ownerIds.has(paginator.id);
394+
395+
if (noOwnersOrPaginatorIsOwner) {
396+
result.push(ch);
397+
}
398+
}
399+
return result;
400+
}
401+
402+
/**
403+
* Wrap paginator.filterQueryResults so that ownership rules are applied whenever
404+
* the paginator ingests results from a server query (first page and subsequent pages).
405+
*/
406+
protected wrapPaginatorFiltering(paginator: ChannelPaginator) {
407+
if (this._wrappedForOwnership.has(paginator)) return;
408+
const original = paginator.filterQueryResults.bind(paginator);
409+
paginator.filterQueryResults = (items: Channel[]) => {
410+
const filtered = original(items) as Channel[];
411+
return this.filterItemsByOwnership({ paginator, items: filtered });
412+
};
413+
this._wrappedForOwnership.add(paginator);
414+
}
415+
372416
getPaginatorById(id: string) {
373417
return this.paginators.find((p) => p.id === id);
374418
}
@@ -392,6 +436,8 @@ export class ChannelPaginatorsOrchestrator extends WithSubscriptions {
392436
);
393437
paginators.splice(validIndex, 0, paginator);
394438
this.state.partialNext({ paginators });
439+
// Wrap newly inserted paginator to enforce ownership on query results
440+
this.wrapPaginatorFiltering(paginator);
395441
}
396442

397443
addEventHandler({

src/pagination/ChannelPaginator.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ const hasUnreadFilterResolver: FieldToDataResolver<Channel> = {
9999
matchesField: (field) => field === 'has_unread',
100100
resolve: (channel) => {
101101
const ownUserId = channel.getClient().user?.id;
102-
return ownUserId && channel.state.read[ownUserId].unread_messages > 0;
102+
return (
103+
ownUserId &&
104+
channel.state.read[ownUserId] &&
105+
channel.state.read[ownUserId].unread_messages > 0
106+
);
103107
},
104108
};
105109

test/unit/ChannelPaginatorsOrchestrator.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,51 @@ describe('ChannelPaginatorsOrchestrator', () => {
165165
expect(p3.items).toBeUndefined();
166166
});
167167
});
168+
169+
it('applies ownership rules to paginators when they paginate', async () => {
170+
const ch1 = makeChannel('messaging:101');
171+
const ch2 = makeChannel('messaging:102');
172+
const queryChannelSpy = vi.spyOn(client, 'queryChannels').mockResolvedValue([ch1]);
173+
const p1 = new ChannelPaginator({
174+
client,
175+
filters: { type: 'messaging' },
176+
id: 'p1',
177+
paginatorOptions: { pageSize: 1 },
178+
});
179+
const p2 = new ChannelPaginator({
180+
client,
181+
filters: { type: 'messaging' },
182+
id: 'p2',
183+
paginatorOptions: { pageSize: 1 },
184+
});
185+
new ChannelPaginatorsOrchestrator({
186+
client,
187+
paginators: [p1, p2],
188+
ownershipResolver: [p2.id],
189+
});
190+
191+
await Promise.all([p1, p2].map((p) => p.next()));
192+
193+
await vi.waitFor(() => {
194+
expect(p1.items).toHaveLength(0);
195+
// even though ownership claimed by p2, it is still possible to request next page.
196+
expect(p1.hasNext).toBe(true);
197+
expect(p2.items).toHaveLength(1);
198+
expect(p2.items).toStrictEqual([ch1]);
199+
expect(p2.hasNext).toBe(true);
200+
});
201+
202+
queryChannelSpy.mockResolvedValue([ch2]);
203+
await Promise.all([p1, p2].map((p) => p.next()));
204+
205+
await vi.waitFor(() => {
206+
expect(p1.items).toHaveLength(0);
207+
expect(p1.hasNext).toBe(true);
208+
expect(p2.items).toHaveLength(2);
209+
expect(p2.items).toStrictEqual([ch1, ch2]);
210+
expect(p2.hasNext).toBe(true);
211+
});
212+
});
168213
});
169214

170215
describe('constructor', () => {

0 commit comments

Comments
 (0)