Skip to content

Commit f5376d6

Browse files
committed
feat: add AsyncPaginator page-level view yielding unified Page
1 parent b70e1fe commit f5376d6

2 files changed

Lines changed: 63 additions & 21 deletions

File tree

sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/AsyncPaginator.kt

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ import java.util.function.Consumer
3838
*
3939
* The walk completes when any of:
4040
*
41-
* - the current page reports `hasNext == false`, or
42-
* - the current page's `nextPageRequest()` returns `null`, or
41+
* - the strategy returns a `null` next request (end of stream), or
4342
* - [maxPages] pages have been fetched (the safety cap), or
4443
* - the consumer throws, or a transport/parse failure occurs (the result future completes
4544
* exceptionally).
@@ -146,7 +145,8 @@ public class AsyncPaginator<T>
146145
* @param consumer Invoked once per item. See the class-level "Consumer threading" KDoc.
147146
* @return A future that completes when the walk finishes.
148147
*/
149-
public fun forEachAsync(consumer: Consumer<in T>): CompletableFuture<Void> = startWalk(consumer, null)
148+
public fun forEachAsync(consumer: Consumer<in T>): CompletableFuture<Void> =
149+
startWalk({ page -> page.items.forEach(consumer::accept) }, null)
150150

151151
/**
152152
* Like [forEachAsync], but runs the page-draining driver — and therefore every
@@ -165,7 +165,7 @@ public class AsyncPaginator<T>
165165
public fun forEachAsync(
166166
consumer: Consumer<in T>,
167167
executor: Executor,
168-
): CompletableFuture<Void> = startWalk(consumer, executor)
168+
): CompletableFuture<Void> = startWalk({ page -> page.items.forEach(consumer::accept) }, executor)
169169

170170
/**
171171
* Collects every item across every page into a single list, in server-defined order.
@@ -188,26 +188,52 @@ public class AsyncPaginator<T>
188188
*/
189189
public fun collectAllAsync(executor: Executor): CompletableFuture<List<T>> = collectInto(executor)
190190

191+
/**
192+
* Walks every page, invoking [consumer] for each fully-materialized [Page] in
193+
* server-defined order. Each [Page] is a pure value carrying the page's items and
194+
* snapshotted response metadata (status code, headers, request) — the response has
195+
* already been closed before the consumer is called.
196+
*
197+
* Cancellation, executor, and ordering semantics are identical to [forEachAsync].
198+
*
199+
* @param consumer Invoked once per page.
200+
* @return A future that completes when the walk finishes.
201+
*/
202+
public fun forEachPageAsync(consumer: Consumer<in Page<T>>): CompletableFuture<Void> =
203+
startWalk({ page -> consumer.accept(page) }, null)
204+
205+
/**
206+
* Like [forEachPageAsync], but runs the page-draining driver on [executor].
207+
*
208+
* @param consumer Invoked once per page, on [executor].
209+
* @param executor Executor on which the driver and consumer run.
210+
* @return A future that completes when the walk finishes.
211+
*/
212+
public fun forEachPageAsync(
213+
consumer: Consumer<in Page<T>>,
214+
executor: Executor,
215+
): CompletableFuture<Void> = startWalk({ page -> consumer.accept(page) }, executor)
216+
191217
private fun startWalk(
192-
consumer: Consumer<in T>,
218+
pageSink: (Page<T>) -> Unit,
193219
executor: Executor?,
194220
): CompletableFuture<Void> {
195221
val result = CompletableFuture<Void>()
196-
Walk(consumer, executor, result).start()
222+
Walk(pageSink, executor, result).start()
197223
return result
198224
}
199225

200226
private fun collectInto(executor: Executor?): CompletableFuture<List<T>> {
201227
val items = ArrayList<T>()
202-
return startWalk({ items.add(it) }, executor).thenApply { items }
228+
return startWalk({ page -> items.addAll(page.items) }, executor).thenApply { items }
203229
}
204230

205231
/**
206232
* Drives one independent walk. Single-call state; never reused across [forEachAsync]
207233
* invocations.
208234
*/
209235
private inner class Walk(
210-
private val consumer: Consumer<in T>,
236+
private val pageSink: (Page<T>) -> Unit,
211237
private val executor: Executor?,
212238
private val result: CompletableFuture<Void>,
213239
) {
@@ -218,7 +244,7 @@ public class AsyncPaginator<T>
218244
// would otherwise recurse into the loop that scheduled it. We trampoline instead —
219245
// whichever stack frame owns the loop picks up the staged work.
220246
private val driving = AtomicBoolean(false)
221-
private var pendingPage: PageInfo<T>? = null
247+
private var pendingPage: ParsedPage<T>? = null
222248

223249
// The transport future for the page currently being fetched: null until the first
224250
// fetch, thereafter the most recently dispatched exchange (it is never reset to null,
@@ -332,8 +358,8 @@ public class AsyncPaginator<T>
332358
* Stages a completed page future for draining. Returns `true` to continue driving,
333359
* `false` if the future failed (the walk is then terminated exceptionally).
334360
*/
335-
private fun stagePage(future: CompletableFuture<PageInfo<T>>): Boolean {
336-
val page: PageInfo<T> =
361+
private fun stagePage(future: CompletableFuture<ParsedPage<T>>): Boolean {
362+
val page: ParsedPage<T> =
337363
try {
338364
future.join()
339365
} catch (t: Throwable) {
@@ -348,15 +374,9 @@ public class AsyncPaginator<T>
348374
* Emits a page's items to the consumer, then schedules the next request. Returns
349375
* `true` to continue driving, `false` if the consumer threw (walk aborted).
350376
*/
351-
private fun drainPage(page: PageInfo<T>): Boolean {
377+
private fun drainPage(page: ParsedPage<T>): Boolean {
352378
try {
353-
val items = page.items
354-
var i = 0
355-
val size = items.size
356-
while (i < size) {
357-
consumer.accept(items[i])
358-
i++
359-
}
379+
pageSink(page.page)
360380
nextRequest = page.nextRequest
361381
} catch (t: Throwable) {
362382
result.completeExceptionally(t)
@@ -370,7 +390,7 @@ public class AsyncPaginator<T>
370390
* mirroring [Paginator]'s per-page lifecycle. The returned future completes with the
371391
* parsed page or exceptionally if the transport or strategy fails.
372392
*/
373-
private fun fetchPage(request: Request): CompletableFuture<PageInfo<T>> {
393+
private fun fetchPage(request: Request): CompletableFuture<ParsedPage<T>> {
374394
pagesFetched++
375395
val transportFuture: CompletableFuture<Response> =
376396
try {
@@ -398,7 +418,8 @@ public class AsyncPaginator<T>
398418
error("AsyncHttpClient.executeAsync completed with a null Response")
399419
}
400420
try {
401-
strategy.parse(response, initialRequest)
421+
val info = strategy.parse(response, initialRequest)
422+
ParsedPage(Page.from(response, info.items), info.nextRequest)
402423
} finally {
403424
response.close()
404425
}
@@ -409,3 +430,9 @@ public class AsyncPaginator<T>
409430

410431
/** Outcome of a single step of [AsyncPaginator]'s trampoline loop. */
411432
private enum class Step { CONTINUE, DONE, SUSPEND }
433+
434+
/** A parsed page plus the request that fetches the next one (null at end of stream). */
435+
private class ParsedPage<T>(
436+
val page: Page<T>,
437+
val nextRequest: Request?,
438+
)

sdk-core/src/test/kotlin/org/dexpace/sdk/core/pagination/AsyncPaginatorTest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,21 @@ class AsyncPaginatorTest {
565565
}
566566
assertTrue(ex.cause is RejectedExecutionException, "cause was ${ex.cause}")
567567
}
568+
569+
@Test
570+
fun `forEachPageAsync delivers rich pages in order`() {
571+
val client = threePageClient()
572+
val paginator = AsyncPaginator(client, initialRequest(), strategy())
573+
574+
val collectedPages = java.util.concurrent.CopyOnWriteArrayList<Page<String>>()
575+
paginator.forEachPageAsync { page -> collectedPages.add(page) }.join()
576+
577+
assertEquals(3, collectedPages.size)
578+
assertEquals(listOf("a", "b"), collectedPages[0].items)
579+
assertEquals(listOf("c", "d"), collectedPages[1].items)
580+
assertEquals(listOf("e"), collectedPages[2].items)
581+
assertTrue(collectedPages.all { it.statusCode == 200 }, "all pages must carry HTTP 200")
582+
}
568583
}
569584

570585
/**

0 commit comments

Comments
 (0)