Skip to content

Commit 358aa56

Browse files
committed
Merge branch 'main' into claude/js-sdk-audit-6x115l-c3-paginator-cursor
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8xeJm2Hd923k8uo6eoFd2
2 parents 6c0a8c5 + 3cf7f3e commit 358aa56

2 files changed

Lines changed: 80 additions & 14 deletions

File tree

‎src/lib/seam-paginator.ts‎

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,7 @@ export class SeamPaginator<
145145
EnsureReadonlyArray<TResponse[TResponseKey]>
146146
> {
147147
const items = [] as EnsureMutableArray<TResponse[TResponseKey]>
148-
let [current, pagination] = await this.firstPage()
149-
items.push(...current)
150-
while (pagination.hasNextPage) {
151-
;[current, pagination] = await this.nextPage(pagination.nextPageCursor)
148+
for await (const [current] of this.#walk()) {
152149
items.push(...current)
153150
}
154151
return items as EnsureReadonlyArray<TResponse[TResponseKey]>
@@ -158,12 +155,7 @@ export class SeamPaginator<
158155
* Yields each item across all pages, fetching the next page as needed.
159156
*/
160157
async *flatten(): AsyncGenerator<ElementOfArray<TResponse[TResponseKey]>> {
161-
let [current, pagination] = await this.firstPage()
162-
for (const item of current) {
163-
yield item
164-
}
165-
while (pagination.hasNextPage) {
166-
;[current, pagination] = await this.nextPage(pagination.nextPageCursor)
158+
for await (const [current] of this.#walk()) {
167159
for (const item of current) {
168160
yield item
169161
}
@@ -176,13 +168,25 @@ export class SeamPaginator<
176168
async *[Symbol.asyncIterator](): AsyncGenerator<
177169
EnsureReadonlyArray<TResponse[TResponseKey]>
178170
> {
179-
let [current, pagination] = await this.firstPage()
180-
yield current
181-
while (pagination.hasNextPage) {
182-
;[current, pagination] = await this.nextPage(pagination.nextPageCursor)
171+
for await (const [current] of this.#walk()) {
183172
yield current
184173
}
185174
}
175+
176+
async *#walk(): AsyncGenerator<
177+
[EnsureReadonlyArray<TResponse[TResponseKey]>, Pagination]
178+
> {
179+
const seenCursors = new Set<SeamPageCursor>()
180+
let page = await this.firstPage()
181+
yield page
182+
while (page[1].hasNextPage) {
183+
const cursor = page[1].nextPageCursor
184+
if (cursor == null || seenCursors.has(cursor)) return
185+
seenCursors.add(cursor)
186+
page = await this.nextPage(cursor)
187+
yield page
188+
}
189+
}
186190
}
187191

188192
const usesParams = (method: string): boolean =>

‎test/seam/connect/seam-paginator.test.ts‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,68 @@ test('SeamPaginator: sends the page cursor when the request has no parameters',
130130
)
131131
})
132132

133+
test('SeamPaginator: stops iterating when the page cursor repeats', async (t) => {
134+
const { seed, endpoint } = await getTestServer(t)
135+
const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint })
136+
137+
nock(endpoint)
138+
.get('/devices/list')
139+
.query({ limit: '1', _strict: 'true' })
140+
.reply(200, {
141+
devices: [{ device_id: 'device-1' }],
142+
pagination: {
143+
has_next_page: true,
144+
next_page_cursor: 'repeated-cursor',
145+
next_page_url: null,
146+
},
147+
})
148+
.get('/devices/list')
149+
.query({ limit: '1', page_cursor: 'repeated-cursor', _strict: 'true' })
150+
.reply(200, {
151+
devices: [{ device_id: 'device-2' }],
152+
pagination: {
153+
has_next_page: true,
154+
next_page_cursor: 'repeated-cursor',
155+
next_page_url: null,
156+
},
157+
})
158+
159+
const pages = seam.createPaginator(seam.devices.list({ limit: 1 }))
160+
const devices = await pages.flattenToArray()
161+
162+
t.deepEqual(
163+
devices.map(({ device_id: deviceId }) => deviceId),
164+
['device-1', 'device-2'],
165+
)
166+
})
167+
168+
test('SeamPaginator: stops iterating when there is a next page without a cursor', async (t) => {
169+
const { seed, endpoint } = await getTestServer(t)
170+
const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint })
171+
172+
nock(endpoint)
173+
.get('/devices/list')
174+
.query({ limit: '1', _strict: 'true' })
175+
.reply(200, {
176+
devices: [{ device_id: 'device-1' }],
177+
pagination: {
178+
has_next_page: true,
179+
next_page_cursor: null,
180+
next_page_url: null,
181+
},
182+
})
183+
184+
const pages = seam.createPaginator(seam.devices.list({ limit: 1 }))
185+
186+
const seenPages = []
187+
for await (const page of pages) {
188+
seenPages.push(page)
189+
}
190+
191+
t.is(seenPages.length, 1)
192+
t.is(seenPages[0]?.[0]?.device_id, 'device-1')
193+
})
194+
133195
test('SeamPaginator: instance allows iteration over all pages', async (t) => {
134196
const { seed, endpoint } = await getTestServer(t)
135197
const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint })

0 commit comments

Comments
 (0)