refactor [collab/gitlab/github/azd]: rework paged list loaders

Force the client to pass "loadAll" explicitly to the loader
Make a test for the abstract implementation instead of one concrete impl

GitOrigin-RevId: 27323ebb9967d6c07b32938b71a343ea7f72f422
This commit is contained in:
Ivan Semenov
2026-02-13 14:11:18 +00:00
committed by intellij-monorepo-bot
parent 15c154b658
commit 212f40edc5
10 changed files with 316 additions and 229 deletions
@@ -18,7 +18,7 @@ object GraphQLListLoader {
requestRefreshFlow: Flow<Unit>? = null,
requestChangeFlow: Flow<Change<V>>? = null,
shouldTryToLoadAll: Boolean = false,
shouldTryToLoadAll: Boolean,
performRequest: suspend (cursor: String?) -> GraphQLConnectionDTO<V>?,
): ReloadablePotentiallyInfiniteListLoader<V> {
@@ -34,7 +34,7 @@ object GraphQLListLoader {
private class GraphQLListLoaderImpl<K, V>(
extractKey: (V) -> K,
shouldTryToLoadAll: Boolean = false,
shouldTryToLoadAll: Boolean,
private val performRequest: suspend (cursor: String?) -> GraphQLConnectionDTO<V>?,
) : PaginatedPotentiallyInfiniteListLoader<PageInfo, K, V>(PageInfo(), extractKey, shouldTryToLoadAll) {
@@ -48,11 +48,11 @@ private class GraphQLListLoaderImpl<K, V>(
override suspend fun performRequestAndProcess(
pageInfo: PageInfo,
f: (pageInfo: PageInfo?, results: List<V>?) -> Page<PageInfo, V>?,
createPage: (pageInfo: PageInfo?, results: List<V>?) -> Page<PageInfo, V>?,
): Page<PageInfo, V>? {
val results = performRequest(pageInfo.cursor)
val nextCursor = results?.pageInfo?.endCursor
return f(pageInfo.copy(nextCursor = nextCursor), results?.nodes)
return createPage(pageInfo.copy(nextCursor = nextCursor), results?.nodes)
}
}
@@ -164,6 +164,18 @@ interface PotentiallyInfiniteListLoader {
interface ReloadablePotentiallyInfiniteListLoader<V>
: ListLoader<V>, ReloadableListLoader, PotentiallyInfiniteListLoader
/**
* An abstract class that provides functionality for managing and loading a paginated
* potentially infinite list of data items. This loader supports reloading, refreshing,
* and incrementally loading more data pages, with support for handling failures and tracking busy states.
*
* @param PI The type representing page information, which tracks the pagination state.
* @param K The type representing keys extracted from data items.
* @param V The type of data items being loaded and managed by the loader.
* @param initialPageInfo The initial page information used to begin the pagination process.
* @param extractKey A lambda to extract a key of type [K] from a data item of type [V].
* @param shouldTryToLoadAll Indicates whether the loader should attempt to load all pages upfront.
*/
@ApiStatus.Internal
abstract class PaginatedPotentiallyInfiniteListLoader<PI : PageInfo<PI>, K, V>(
private val initialPageInfo: PI,
@@ -212,23 +224,29 @@ abstract class PaginatedPotentiallyInfiniteListLoader<PI : PageInfo<PI>, K, V>(
}
}
/**
* Refreshes the current state of the paginated list by reloading data for each page
* and loading the new pages if [shouldTryToLoadAll] is set
*/
private suspend fun doRefresh() {
coroutineScope {
val currentPages = pages.list ?: listOf()
doEmitPages(
runCatchingUser {
State(currentPages.mapNotNull { page ->
if (page.info == null) return@mapNotNull null
val newState = runCatchingUser {
val newPages = currentPages.mapNotNull { page ->
if (page.info == null) return@mapNotNull null
performRequestAndProcess(page.info) { pageInfo, results ->
page.copy(info = pageInfo, list = results ?: page.list)
}
}.toList(), null)
}.getOrElse {
State(currentPages, it)
})
performRequestAndProcess(page.info) { pageInfo, results ->
page.copy(info = pageInfo, list = results ?: page.list)
}
}
State(newPages, null)
}.getOrElse {
State(currentPages, it)
}
doEmitPages(newState)
}
// load the new pages we learned about during the refresh
if (shouldTryToLoadAll) {
loadAllImpl()
}
@@ -300,7 +318,7 @@ abstract class PaginatedPotentiallyInfiniteListLoader<PI : PageInfo<PI>, K, V>(
*/
protected abstract suspend fun performRequestAndProcess(
pageInfo: PI,
f: (pageInfo: PI?, results: List<V>?) -> Page<PI, V>?
createPage: (pageInfo: PI?, results: List<V>?) -> Page<PI, V>?
): Page<PI, V>?
/**
@@ -1,198 +0,0 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.collaboration.async
import app.cash.turbine.test
import com.intellij.collaboration.api.dto.GraphQLConnectionDTO
import com.intellij.collaboration.api.dto.GraphQLCursorPageInfoDTO
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifySequence
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import kotlin.time.Duration.Companion.seconds
class GraphQLListLoaderTest {
private data class DummyData(
val key: Int,
)
private class DummyDataConnection(
pageInfo: GraphQLCursorPageInfoDTO,
nodes: List<DummyData>,
) : GraphQLConnectionDTO<DummyData>(pageInfo, nodes)
companion object {
private val ALL_TEST_DATA: List<DummyData> = (0 until 100).map { DummyData(it) }
private fun blockingPageLookup(pageSize: Int, sizeLimiter: () -> Int = { ALL_TEST_DATA.size }): suspend (cursor: String?) -> DummyDataConnection? =
suspend@{ cursor ->
val startIndex = cursor?.toInt() ?: 0
val virtualSize = minOf(ALL_TEST_DATA.size, sizeLimiter())
val endIndex = minOf(virtualSize, startIndex + pageSize)
if (startIndex >= virtualSize) return@suspend null
val data = ALL_TEST_DATA.subList(startIndex, endIndex)
DummyDataConnection(GraphQLCursorPageInfoDTO(
startCursor = startIndex.toString(), endCursor = endIndex.toString(),
hasPreviousPage = startIndex > 0, hasNextPage = endIndex < virtualSize
), data)
}
}
// Stub for which method invocations are tracked by Mockito
private interface MockingPageLookup : suspend (String?) -> DummyDataConnection? {
companion object {
fun create(lookup: suspend (String?) -> DummyDataConnection?): MockingPageLookup = mockk<MockingPageLookup>().apply {
coEvery { this@apply.invoke(any()) }.coAnswers { lookup(firstArg()) }
}
}
}
@Test
fun `no pages are loaded initially`() = runTest {
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 10))
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, performRequest = pageLookupMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
assertThat(awaitItem().list).isNull()
expectNoEvents()
}
coVerify(exactly = 0) { pageLookupMock.invoke(any()) }
}
@Test
fun `one page is loaded initially with starting reload`() = runTest {
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 20))
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, performRequest = pageLookupMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
listLoader.reload()
assertThat(awaitItem().list).isNull()
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(20))
expectNoEvents()
}
coVerifySequence {
pageLookupMock.invoke(null)
}
}
@Test
fun `all pages are loaded initially with starting reload`() = runTest {
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 40))
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, shouldTryToLoadAll = true, performRequest = pageLookupMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
listLoader.reload()
assertThat(awaitItem().list).isNull()
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(40))
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(80))
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA)
expectNoEvents()
}
coVerifySequence {
pageLookupMock.invoke(null)
pageLookupMock.invoke(eq("40"))
pageLookupMock.invoke(eq("80"))
pageLookupMock.invoke(eq("100")) // impl intentionally ignores hasNext in favor of just trying to fetch
}
}
@Test
fun `only previously loaded pages are re-fetched upon refresh`() = runTest {
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 40))
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, performRequest = pageLookupMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
assertThat(awaitItem().list).isNull()
expectNoEvents()
listLoader.loadMore()
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(40))
expectNoEvents()
listLoader.refresh()
expectNoEvents()
}
coVerifySequence {
// loadMore
pageLookupMock.invoke(null)
// refresh
pageLookupMock.invoke(null)
}
}
@Test
fun `loadMore doesnt update state when no new data is available`() = runTest {
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 40))
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, shouldTryToLoadAll = true, performRequest = pageLookupMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
assertThat(awaitItem().list).isNull()
expectNoEvents()
listLoader.reload()
while (awaitItem().list != ALL_TEST_DATA) {
}
expectNoEvents()
listLoader.loadMore()
expectNoEvents()
}
coVerifySequence {
// reload
pageLookupMock.invoke(null)
pageLookupMock.invoke(eq("40"))
pageLookupMock.invoke(eq("80"))
pageLookupMock.invoke(eq("100"))
// loadMore
pageLookupMock.invoke(eq("100"))
}
}
@Test
fun `loadMore does update state when new data is available`() = runTest {
var dynamicSize = 20
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 40, sizeLimiter = { dynamicSize }))
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, shouldTryToLoadAll = true, performRequest = pageLookupMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
assertThat(awaitItem().list).isNull()
expectNoEvents()
listLoader.loadAll()
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(20))
expectNoEvents()
dynamicSize = 60
listLoader.loadMore()
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(60))
}
coVerifySequence {
// loadAll
pageLookupMock.invoke(null)
pageLookupMock.invoke(eq("20"))
// loadMore
pageLookupMock.invoke(eq("20"))
}
}
// TODO: Tests for (1) update function, (2) throwing an error inside fetch, (3) tracking reload/refresh flows maybe
// For more info, run and check coverage inside com.intellij.collaboration.async
}
@@ -0,0 +1,252 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.collaboration.async
import app.cash.turbine.test
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifySequence
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import kotlin.time.Duration.Companion.seconds
internal class PaginatedPotentiallyInfiniteListLoaderTest {
private data class DummyData(
val key: Int,
)
private data class DummyPageInfo(
val offset: Int,
val hasNext: Boolean,
) : PaginatedPotentiallyInfiniteListLoader.PageInfo<DummyPageInfo> {
override fun createNextPageInfo(): DummyPageInfo? =
if (hasNext) copy(offset = offset + PAGE_SIZE) else null
}
private data class DummyPage(
val pageInfo: DummyPageInfo?,
val data: List<DummyData>?,
)
private fun interface PageLoader {
fun computePage(offset: Int): DummyPage?
}
companion object {
private const val PAGE_SIZE = 20
private val ALL_TEST_DATA: List<DummyData> = (0 until 100).map { DummyData(it) }
private fun pageLoaderMock(sizeLimiter: () -> Int = { ALL_TEST_DATA.size }): PageLoader =
mockk<PageLoader> {
coEvery { computePage(any()) }.coAnswers {
val offset = firstArg() as Int
val virtualSize = minOf(ALL_TEST_DATA.size, sizeLimiter())
val endIndex = minOf(virtualSize, offset + PAGE_SIZE)
if (offset >= virtualSize) return@coAnswers null
val data = ALL_TEST_DATA.subList(offset, endIndex)
val hasNext = endIndex < virtualSize
DummyPage(DummyPageInfo(offset = offset, hasNext = hasNext), data)
}
}
}
private class TestLoader(
shouldTryToLoadAll: Boolean,
private val pageLoader: PageLoader,
) : PaginatedPotentiallyInfiniteListLoader<DummyPageInfo, Int, DummyData>(
initialPageInfo = DummyPageInfo(offset = 0, hasNext = true),
extractKey = { it.key },
shouldTryToLoadAll = shouldTryToLoadAll
) {
override suspend fun performRequestAndProcess(
pageInfo: DummyPageInfo,
createPage: (pageInfo: DummyPageInfo?, results: List<DummyData>?) -> Page<DummyPageInfo, DummyData>?,
): Page<DummyPageInfo, DummyData>? {
val response = pageLoader.computePage(pageInfo.offset) ?: return null
return createPage(response.pageInfo, response.data)
}
}
@Test
fun `no pages are loaded initially`() = runTest {
val pageLookupMock = pageLoaderMock(ALL_TEST_DATA::size)
val listLoader = TestLoader(shouldTryToLoadAll = false, pageLoader = pageLookupMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
assertThat(awaitItem().list).isNull()
expectNoEvents()
}
coVerify(exactly = 0) { pageLookupMock.computePage(any()) }
}
@Test
fun `one page is loaded initially with starting reload`() = runTest {
val pageLoaderMock = pageLoaderMock()
val listLoader = TestLoader(shouldTryToLoadAll = false, pageLoader = pageLoaderMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
listLoader.reload()
assertThat(awaitItem().list).isNull()
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(PAGE_SIZE))
expectNoEvents()
}
coVerifySequence {
pageLoaderMock.computePage(eq(0))
}
}
@Test
fun `all pages are loaded initially with starting reload`() = runTest {
val pageLoaderMock = pageLoaderMock()
val listLoader = TestLoader(shouldTryToLoadAll = true, pageLoader = pageLoaderMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
listLoader.reload()
assertThat(awaitItem().list).isNull()
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(PAGE_SIZE))
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(PAGE_SIZE * 2))
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(PAGE_SIZE * 3))
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(PAGE_SIZE * 4))
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA)
expectNoEvents()
}
coVerifySequence {
pageLoaderMock.computePage(eq(0))
pageLoaderMock.computePage(eq(PAGE_SIZE))
pageLoaderMock.computePage(eq(PAGE_SIZE * 2))
pageLoaderMock.computePage(eq(PAGE_SIZE * 3))
pageLoaderMock.computePage(eq(PAGE_SIZE * 4))
}
}
@Test
fun `only previously loaded pages are re-fetched upon refresh`() = runTest {
val pageLoaderMock = pageLoaderMock()
val listLoader = TestLoader(shouldTryToLoadAll = false, pageLoader = pageLoaderMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
assertThat(awaitItem().list).isNull()
expectNoEvents()
listLoader.loadMore()
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE))
expectNoEvents()
listLoader.refresh()
expectNoEvents()
}
coVerifySequence {
// loadMore
pageLoaderMock.computePage(eq(0))
// refresh
pageLoaderMock.computePage(eq(0))
}
}
@Test
fun `loadMore doesnt update state when no new data is available`() = runTest {
val pageLoaderMock = pageLoaderMock()
val listLoader = TestLoader(shouldTryToLoadAll = true, pageLoader = pageLoaderMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
assertThat(awaitItem().list).isNull()
expectNoEvents()
listLoader.reload()
while (awaitItem().list != ALL_TEST_DATA) {
}
expectNoEvents()
listLoader.loadMore()
expectNoEvents()
}
coVerifySequence {
// reload
pageLoaderMock.computePage(eq(0))
pageLoaderMock.computePage(eq(PAGE_SIZE))
pageLoaderMock.computePage(eq(PAGE_SIZE * 2))
pageLoaderMock.computePage(eq(PAGE_SIZE * 3))
pageLoaderMock.computePage(eq(PAGE_SIZE * 4))
// loadMore - no call because hasNext is false
}
}
@Test
fun `loadMore does update state when new data is available`() = runTest {
val pageLoaderMock = pageLoaderMock()
val listLoader = TestLoader(shouldTryToLoadAll = false, pageLoader = pageLoaderMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
assertThat(awaitItem().list).isNull()
expectNoEvents()
listLoader.loadMore()
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE))
expectNoEvents()
listLoader.loadMore()
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE * 2))
expectNoEvents()
}
coVerifySequence {
// loadAll
pageLoaderMock.computePage(eq(0))
// loadMore
pageLoaderMock.computePage(eq(PAGE_SIZE))
}
}
@Test
fun `all pages including new ones are loaded during refresh when full loading is requested`() = runTest {
var dynamicSize = PAGE_SIZE
val pageLoaderMock = pageLoaderMock(sizeLimiter = { dynamicSize })
val listLoader = TestLoader(shouldTryToLoadAll = true, pageLoader = pageLoaderMock)
listLoader.stateFlow.test(timeout = 1.seconds) {
assertThat(awaitItem().list).isNull()
expectNoEvents()
listLoader.loadAll()
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE))
expectNoEvents()
dynamicSize = PAGE_SIZE * 3
listLoader.refresh()
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE * 2))
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE * 3))
expectNoEvents()
}
coVerifySequence {
// loadAll
pageLoaderMock.computePage(eq(0))
// refresh
pageLoaderMock.computePage(eq(0))
// loadAll after refresh
pageLoaderMock.computePage(eq(PAGE_SIZE))
pageLoaderMock.computePage(eq(PAGE_SIZE * 2))
}
}
// TODO: Tests for (1) update function, (2) throwing an error inside fetch, (3) tracking reload/refresh flows maybe
// For more info, run and check coverage inside com.intellij.collaboration.async
}
@@ -51,7 +51,9 @@ internal class GHPRListLoader(
requestReloadFlow = reloadRequests,
requestRefreshFlow = refreshRequests,
requestChangeFlow = updateRequests
requestChangeFlow = updateRequests,
shouldTryToLoadAll = false
) { cursor ->
val page = GraphQLRequestPagination(afterCursor = cursor, pageSize = 50)
requestExecutor.executeSuspend(
@@ -177,7 +177,9 @@ internal class LoadedGitLabMergeRequest(
{ it.id },
requestReloadFlow = mergeRequestReloadRequest.withInitial(Unit),
requestRefreshFlow = mergeRequestRefreshRequest.combine(stateEventsRefreshRequest.withInitial(Unit)) { _, _ -> }
requestRefreshFlow = mergeRequestRefreshRequest.combine(stateEventsRefreshRequest.withInitial(Unit)) { _, _ -> },
shouldTryToLoadAll = false
) { uri, eTag ->
api.rest.loadUpdatableJsonList<GitLabResourceStateEventDTO>(
GitLabApiRequestName.REST_GET_MERGE_REQUEST_STATE_EVENTS, uri, eTag
@@ -193,7 +195,9 @@ internal class LoadedGitLabMergeRequest(
{ it.id },
requestReloadFlow = mergeRequestReloadRequest.withInitial(Unit),
requestRefreshFlow = mergeRequestRefreshRequest
requestRefreshFlow = mergeRequestRefreshRequest,
shouldTryToLoadAll = false
) { uri, eTag ->
api.rest.loadUpdatableJsonList<GitLabResourceLabelEventDTO>(
GitLabApiRequestName.REST_GET_MERGE_REQUEST_LABEL_EVENTS, uri, eTag
@@ -209,7 +213,9 @@ internal class LoadedGitLabMergeRequest(
{ it.id },
requestReloadFlow = mergeRequestReloadRequest.withInitial(Unit),
requestRefreshFlow = mergeRequestRefreshRequest
requestRefreshFlow = mergeRequestRefreshRequest,
shouldTryToLoadAll = false
) { uri, eTag ->
api.rest.loadUpdatableJsonList<GitLabResourceMilestoneEventDTO>(
GitLabApiRequestName.REST_GET_MERGE_REQUEST_MILESTONE_EVENTS, uri, eTag
@@ -101,7 +101,9 @@ class GitLabMergeRequestDiscussionsContainerImpl(
requestReloadFlow = reloadRequests,
requestRefreshFlow = updateRequests,
requestChangeFlow = discussionEvents
requestChangeFlow = discussionEvents,
shouldTryToLoadAll = false
) { uri, eTag ->
api.rest.loadUpdatableJsonList<GitLabDiscussionRestDTO>(
GitLabApiRequestName.REST_GET_MERGE_REQUEST_DISCUSSIONS, uri, eTag
@@ -158,7 +160,9 @@ class GitLabMergeRequestDiscussionsContainerImpl(
requestReloadFlow = reloadRequests,
requestRefreshFlow = updateRequests,
requestChangeFlow = draftNotesEvents
requestChangeFlow = draftNotesEvents,
shouldTryToLoadAll = false
) { uri, eTag ->
api.rest.loadUpdatableJsonList<GitLabMergeRequestDraftNoteRestDTO>(
GitLabApiRequestName.REST_GET_DRAFT_NOTES, uri, eTag
@@ -102,7 +102,8 @@ class CachingGitLabProjectMergeRequestsStore(private val project: Project,
getMergeRequestListURI(glProject, searchQuery),
{ it.id },
requestReloadFlow = tokenRefreshFlow.withInitial(Unit)
requestReloadFlow = tokenRefreshFlow.withInitial(Unit),
shouldTryToLoadAll = false
) { uri, etag ->
api.rest.loadUpdatableJsonList<GitLabMergeRequestShortRestDTO>(
GitLabApiRequestName.REST_GET_MERGE_REQUESTS, uri, etag
@@ -25,11 +25,11 @@ fun <K, V> startGitLabRestETagListLoaderIn(
requestRefreshFlow: Flow<Unit>? = null,
requestChangeFlow: Flow<Change<V>>? = null,
shouldTryToLoadAll: Boolean = false,
shouldTryToLoadAll: Boolean,
performRequest: suspend (uri: URI, eTag: String?) -> HttpResponse<out List<V>?>
): ReloadablePotentiallyInfiniteListLoader<V> {
val loader = GitLabRestETagListLoader(cs, initialURI, extractKey, shouldTryToLoadAll, performRequest)
val loader = GitLabRestETagListLoader(initialURI, extractKey, shouldTryToLoadAll, performRequest)
cs.launchNow { requestReloadFlow?.collect { loader.reload() } }
cs.launch { requestRefreshFlow?.collect { loader.refresh() } }
@@ -39,7 +39,6 @@ fun <K, V> startGitLabRestETagListLoaderIn(
}
private class GitLabRestETagListLoader<K, V>(
cs: CoroutineScope,
private val initialURI: URI,
extractKey: (V) -> K,
@@ -62,7 +61,7 @@ private class GitLabRestETagListLoader<K, V>(
override suspend fun performRequestAndProcess(
pageInfo: PageInfo,
f: (pageInfo: PageInfo?, results: List<V>?) -> Page<PageInfo, V>?
createPage: (pageInfo: PageInfo?, results: List<V>?) -> Page<PageInfo, V>?
): Page<PageInfo, V>? {
val response = try {
performRequest(pageInfo.link, pageInfo.etag)
@@ -87,6 +86,6 @@ private class GitLabRestETagListLoader<K, V>(
URIUtil.createUriWithCustomScheme(it, initialURI.scheme)
}
return f(pageInfo.copy(nextLink = nextLink, etag = newEtag), results)
return createPage(pageInfo.copy(nextLink = nextLink, etag = newEtag), results)
}
}
@@ -384,7 +384,8 @@ class GitLabApiTest : GitLabApiTestCase() {
val loader = startGitLabRestETagListLoaderIn(backgroundScope,
getMergeRequestStateEventsUri(glTest1Coordinates, "1"),
{ it.id },
reloadRequest) { uri, eTag ->
reloadRequest,
shouldTryToLoadAll = false) { uri, eTag ->
api.rest.loadUpdatableJsonList<GitLabResourceStateEventDTO>(
GitLabApiRequestName.REST_GET_MERGE_REQUEST_STATE_EVENTS, uri, eTag
)
@@ -405,7 +406,8 @@ class GitLabApiTest : GitLabApiTestCase() {
val loader = startGitLabRestETagListLoaderIn(backgroundScope,
getMergeRequestLabelEventsUri(glTest1Coordinates, "1"),
{ it.id },
reloadRequest) { uri, eTag ->
reloadRequest,
shouldTryToLoadAll = false) { uri, eTag ->
api.rest.loadUpdatableJsonList<GitLabResourceLabelEventDTO>(
GitLabApiRequestName.REST_GET_MERGE_REQUEST_STATE_EVENTS, uri, eTag
)
@@ -426,7 +428,8 @@ class GitLabApiTest : GitLabApiTestCase() {
val loader = startGitLabRestETagListLoaderIn(backgroundScope,
getMergeRequestMilestoneEventsUri(glTest1Coordinates, "1"),
{ it.id },
reloadRequest) { uri, eTag ->
reloadRequest,
shouldTryToLoadAll = false) { uri, eTag ->
api.rest.loadUpdatableJsonList<GitLabResourceMilestoneEventDTO>(
GitLabApiRequestName.REST_GET_MERGE_REQUEST_STATE_EVENTS, uri, eTag
)