@@ -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. */
411432private 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+ )
0 commit comments