From b3d3602dc5af6768e5b03613a7ca22ad3b47ec8d Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Mon, 9 Feb 2026 01:03:29 +0000 Subject: fix: Support nested smart lists with cycle detection (#2470) * fix: Support nested smart lists and prevent infinite loops --------- Co-authored-by: Claude --- packages/trpc/models/lists.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) (limited to 'packages/trpc/models') diff --git a/packages/trpc/models/lists.ts b/packages/trpc/models/lists.ts index 29945b92..10d7d9bf 100644 --- a/packages/trpc/models/lists.ts +++ b/packages/trpc/models/lists.ts @@ -809,8 +809,8 @@ export abstract class List { } abstract get type(): "manual" | "smart"; - abstract getBookmarkIds(ctx: AuthedContext): Promise; - abstract getSize(ctx: AuthedContext): Promise; + abstract getBookmarkIds(visitedListIds?: Set): Promise; + abstract getSize(): Promise; abstract addBookmark(bookmarkId: string): Promise; abstract removeBookmark(bookmarkId: string): Promise; abstract mergeInto( @@ -820,6 +820,8 @@ export abstract class List { } export class SmartList extends List { + private static readonly MAX_VISITED_LISTS = 30; + parsedQuery: ReturnType | null = null; constructor(ctx: AuthedContext, list: ZBookmarkList & { userId: string }) { @@ -847,12 +849,27 @@ export class SmartList extends List { return this.parsedQuery; } - async getBookmarkIds(): Promise { + async getBookmarkIds(visitedListIds = new Set()): Promise { + if (visitedListIds.size >= SmartList.MAX_VISITED_LISTS) { + return []; + } + + if (visitedListIds.has(this.list.id)) { + return []; + } + + const newVisitedListIds = new Set(visitedListIds); + newVisitedListIds.add(this.list.id); + const parsedQuery = this.getParsedQuery(); if (!parsedQuery.matcher) { return []; } - return await getBookmarkIdsFromMatcher(this.ctx, parsedQuery.matcher); + return await getBookmarkIdsFromMatcher( + this.ctx, + parsedQuery.matcher, + newVisitedListIds, + ); } async getSize(): Promise { @@ -898,7 +915,7 @@ export class ManualList extends List { return this.list.type; } - async getBookmarkIds(): Promise { + async getBookmarkIds(_visitedListIds?: Set): Promise { const results = await this.ctx.db .select({ id: bookmarksInLists.bookmarkId }) .from(bookmarksInLists) -- cgit v1.2.3-70-g09d2