Skip to content

Commit 4fb855d

Browse files
committed
fix(v2): de-duplicate a set filter before fingerprinting it
The filters compile to inArray, which is set membership, so workflowIds=A,A,B selects exactly what A,B does. Sorting alone still bound them to different pages, so an equivalent filter with a repeated member 400d mid-walk.
1 parent 75357db commit 4fb855d

2 files changed

Lines changed: 22 additions & 7 deletions

File tree

apps/sim/lib/api/cursor-binding.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@ describe('unordered filter scope parts', () => {
199199
const b = cursorScopeKey({ workflowIds: unorderedScopePart('B,A') })
200200
expect(a).toBe(b)
201201
})
202+
it('fingerprints a duplicate-bearing set identically', () => {
203+
// The filters compile to `inArray`, which is set membership, so A,A,B
204+
// selects exactly what A,B does and must resume the same page.
205+
expect(cursorScopeKey({ workflowIds: unorderedScopePart('A,A,B') })).toBe(
206+
cursorScopeKey({ workflowIds: unorderedScopePart('A,B') })
207+
)
208+
expect(unorderedScopePart('B,A,B')).toBe('A,B')
209+
})
210+
202211
it('still separates genuinely different sets', () => {
203212
expect(cursorScopeKey({ workflowIds: unorderedScopePart('A,B') })).not.toBe(
204213
cursorScopeKey({ workflowIds: unorderedScopePart('A,C') })

apps/sim/lib/api/cursor-binding.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,22 @@ export type CursorScopePart =
6868
* spelling, so a caller who reorders an equivalent filter mid-walk gets a 400
6969
* for a page that is genuinely the next one.
7070
*
71-
* {@link canonicalJson} already sorts object keys, so this only has to sort the
72-
* list members. Empty members are dropped because the parsers drop them too.
71+
* {@link canonicalJson} already sorts object keys, so this only has to normalize
72+
* the list. Members are de-duplicated as well as sorted: the filters compile to
73+
* `inArray`, which is set membership, so `A,A,B` selects exactly what `A,B` does
74+
* and must not bind to a different page. Empty members are dropped because the
75+
* parsers drop them too.
7376
*/
7477
export function unorderedScopePart(raw: string | undefined): string | undefined {
7578
if (raw === undefined) return undefined
76-
const members = raw
77-
.split(',')
78-
.map((member) => member.trim())
79-
.filter((member) => member.length > 0)
80-
.sort()
79+
const members = [
80+
...new Set(
81+
raw
82+
.split(',')
83+
.map((member) => member.trim())
84+
.filter((member) => member.length > 0)
85+
),
86+
].sort()
8187
return members.length > 0 ? members.join(',') : undefined
8288
}
8389

0 commit comments

Comments
 (0)