diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/async/GraphQLListLoader.kt b/platform/collaboration-tools/src/com/intellij/collaboration/async/GraphQLListLoader.kt index 7eb47ba245f2..b5231d967488 100644 --- a/platform/collaboration-tools/src/com/intellij/collaboration/async/GraphQLListLoader.kt +++ b/platform/collaboration-tools/src/com/intellij/collaboration/async/GraphQLListLoader.kt @@ -18,7 +18,7 @@ object GraphQLListLoader { requestRefreshFlow: Flow? = null, requestChangeFlow: Flow>? = null, - shouldTryToLoadAll: Boolean = false, + shouldTryToLoadAll: Boolean, performRequest: suspend (cursor: String?) -> GraphQLConnectionDTO?, ): ReloadablePotentiallyInfiniteListLoader { @@ -34,7 +34,7 @@ object GraphQLListLoader { private class GraphQLListLoaderImpl( extractKey: (V) -> K, - shouldTryToLoadAll: Boolean = false, + shouldTryToLoadAll: Boolean, private val performRequest: suspend (cursor: String?) -> GraphQLConnectionDTO?, ) : PaginatedPotentiallyInfiniteListLoader(PageInfo(), extractKey, shouldTryToLoadAll) { @@ -48,11 +48,11 @@ private class GraphQLListLoaderImpl( override suspend fun performRequestAndProcess( pageInfo: PageInfo, - f: (pageInfo: PageInfo?, results: List?) -> Page?, + createPage: (pageInfo: PageInfo?, results: List?) -> Page?, ): Page? { 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) } } diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/async/ListLoader.kt b/platform/collaboration-tools/src/com/intellij/collaboration/async/ListLoader.kt index 054fd1f173fd..8d329e8f27d4 100644 --- a/platform/collaboration-tools/src/com/intellij/collaboration/async/ListLoader.kt +++ b/platform/collaboration-tools/src/com/intellij/collaboration/async/ListLoader.kt @@ -164,6 +164,18 @@ interface PotentiallyInfiniteListLoader { interface ReloadablePotentiallyInfiniteListLoader : ListLoader, 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, K, V>( private val initialPageInfo: PI, @@ -212,23 +224,29 @@ abstract class PaginatedPotentiallyInfiniteListLoader, 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, K, V>( */ protected abstract suspend fun performRequestAndProcess( pageInfo: PI, - f: (pageInfo: PI?, results: List?) -> Page? + createPage: (pageInfo: PI?, results: List?) -> Page? ): Page? /** diff --git a/platform/collaboration-tools/test/com/intellij/collaboration/async/GraphQLListLoaderTest.kt b/platform/collaboration-tools/test/com/intellij/collaboration/async/GraphQLListLoaderTest.kt deleted file mode 100644 index 0653fbe54367..000000000000 --- a/platform/collaboration-tools/test/com/intellij/collaboration/async/GraphQLListLoaderTest.kt +++ /dev/null @@ -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, - ) : GraphQLConnectionDTO(pageInfo, nodes) - - companion object { - private val ALL_TEST_DATA: List = (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().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(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(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(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(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(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(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 -} \ No newline at end of file diff --git a/platform/collaboration-tools/test/com/intellij/collaboration/async/PaginatedPotentiallyInfiniteListLoaderTest.kt b/platform/collaboration-tools/test/com/intellij/collaboration/async/PaginatedPotentiallyInfiniteListLoaderTest.kt new file mode 100644 index 000000000000..2b9a37dace50 --- /dev/null +++ b/platform/collaboration-tools/test/com/intellij/collaboration/async/PaginatedPotentiallyInfiniteListLoaderTest.kt @@ -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 { + override fun createNextPageInfo(): DummyPageInfo? = + if (hasNext) copy(offset = offset + PAGE_SIZE) else null + } + + private data class DummyPage( + val pageInfo: DummyPageInfo?, + val data: List?, + ) + + private fun interface PageLoader { + fun computePage(offset: Int): DummyPage? + } + + companion object { + private const val PAGE_SIZE = 20 + private val ALL_TEST_DATA: List = (0 until 100).map { DummyData(it) } + + private fun pageLoaderMock(sizeLimiter: () -> Int = { ALL_TEST_DATA.size }): PageLoader = + mockk { + 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( + initialPageInfo = DummyPageInfo(offset = 0, hasNext = true), + extractKey = { it.key }, + shouldTryToLoadAll = shouldTryToLoadAll + ) { + override suspend fun performRequestAndProcess( + pageInfo: DummyPageInfo, + createPage: (pageInfo: DummyPageInfo?, results: List?) -> Page?, + ): Page? { + 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 +} diff --git a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/data/GHPRListLoader.kt b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/data/GHPRListLoader.kt index fb2dabe374ed..04afe762d1f5 100644 --- a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/data/GHPRListLoader.kt +++ b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/data/GHPRListLoader.kt @@ -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( diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequest.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequest.kt index bd0ec7d885e6..ad0129294f3a 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequest.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequest.kt @@ -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( 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( 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( GitLabApiRequestName.REST_GET_MERGE_REQUEST_MILESTONE_EVENTS, uri, eTag diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestDiscussionsContainer.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestDiscussionsContainer.kt index cb8b3dd136fe..4af9ea3f2849 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestDiscussionsContainer.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestDiscussionsContainer.kt @@ -101,7 +101,9 @@ class GitLabMergeRequestDiscussionsContainerImpl( requestReloadFlow = reloadRequests, requestRefreshFlow = updateRequests, - requestChangeFlow = discussionEvents + requestChangeFlow = discussionEvents, + + shouldTryToLoadAll = false ) { uri, eTag -> api.rest.loadUpdatableJsonList( 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( GitLabApiRequestName.REST_GET_DRAFT_NOTES, uri, eTag diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProjectMergeRequestsStore.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProjectMergeRequestsStore.kt index 8070b7c409ab..652650e8ab21 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProjectMergeRequestsStore.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProjectMergeRequestsStore.kt @@ -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( GitLabApiRequestName.REST_GET_MERGE_REQUESTS, uri, etag diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/loaders/GitLabRestETagListLoader.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/loaders/GitLabRestETagListLoader.kt index 005e64da0932..542d1d198371 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/loaders/GitLabRestETagListLoader.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/loaders/GitLabRestETagListLoader.kt @@ -25,11 +25,11 @@ fun startGitLabRestETagListLoaderIn( requestRefreshFlow: Flow? = null, requestChangeFlow: Flow>? = null, - shouldTryToLoadAll: Boolean = false, + shouldTryToLoadAll: Boolean, performRequest: suspend (uri: URI, eTag: String?) -> HttpResponse?> ): ReloadablePotentiallyInfiniteListLoader { - 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 startGitLabRestETagListLoaderIn( } private class GitLabRestETagListLoader( - cs: CoroutineScope, private val initialURI: URI, extractKey: (V) -> K, @@ -62,7 +61,7 @@ private class GitLabRestETagListLoader( override suspend fun performRequestAndProcess( pageInfo: PageInfo, - f: (pageInfo: PageInfo?, results: List?) -> Page? + createPage: (pageInfo: PageInfo?, results: List?) -> Page? ): Page? { val response = try { performRequest(pageInfo.link, pageInfo.etag) @@ -87,6 +86,6 @@ private class GitLabRestETagListLoader( URIUtil.createUriWithCustomScheme(it, initialURI.scheme) } - return f(pageInfo.copy(nextLink = nextLink, etag = newEtag), results) + return createPage(pageInfo.copy(nextLink = nextLink, etag = newEtag), results) } } diff --git a/plugins/gitlab/gitlab-core/testApi/org/jetbrains/plugins/gitlab/apitests/GitLabApiTest.kt b/plugins/gitlab/gitlab-core/testApi/org/jetbrains/plugins/gitlab/apitests/GitLabApiTest.kt index 513886e278e8..2d2e58a9b656 100644 --- a/plugins/gitlab/gitlab-core/testApi/org/jetbrains/plugins/gitlab/apitests/GitLabApiTest.kt +++ b/plugins/gitlab/gitlab-core/testApi/org/jetbrains/plugins/gitlab/apitests/GitLabApiTest.kt @@ -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( 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( 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( GitLabApiRequestName.REST_GET_MERGE_REQUEST_STATE_EVENTS, uri, eTag )