diff --git a/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/GradleEnterpriseApiExtensions.kt b/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/GradleEnterpriseApiExtensions.kt index ed4345b6..1a5d12e6 100644 --- a/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/GradleEnterpriseApiExtensions.kt +++ b/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/GradleEnterpriseApiExtensions.kt @@ -1,14 +1,15 @@ package com.gabrielfeo.gradle.enterprise.api +import com.gabrielfeo.gradle.enterprise.api.internal.API_MAX_BUILDS +import com.gabrielfeo.gradle.enterprise.api.internal.operator.pagedUntilLastBuild +import com.gabrielfeo.gradle.enterprise.api.internal.operator.withGradleAttributes import com.gabrielfeo.gradle.enterprise.api.model.* import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async import kotlinx.coroutines.flow.* import retrofit2.await -private const val API_MAX_BUILDS = 1000 - /** * Gets builds on demand from the API, in as many requests as necessary. It allows * for queries of any size, as opposed to [GradleEnterpriseApi.getBuilds] which is limited by the @@ -24,43 +25,17 @@ fun GradleEnterpriseApi.getBuildsFlow( fromInstant: Long? = null, fromBuild: String? = null, ): Flow = flow { - var lastBuildId: String? = null - while (true) { - val call = when (lastBuildId) { - null -> getBuilds( - since = since, sinceBuild = sinceBuild, - fromInstant = fromInstant, fromBuild = fromBuild, - maxBuilds = API_MAX_BUILDS, - ) - else -> getBuilds(fromBuild = lastBuildId, maxBuilds = API_MAX_BUILDS) - } - val builds = call.await() - emitAll(builds.asFlow()) - when { - builds.isEmpty() || builds.size < API_MAX_BUILDS -> break - else -> lastBuildId = builds.last().id - } - } + val firstBuilds = getBuilds( + since = since, + sinceBuild = sinceBuild, + fromInstant = fromInstant, + fromBuild = fromBuild, + maxBuilds = API_MAX_BUILDS, + ).await() + val pagedBuilds = firstBuilds.asFlow().pagedUntilLastBuild(maxPerRequest = API_MAX_BUILDS) + emitAll(pagedBuilds) } -/** - * Joins builds with their [GradleAttributes], which comes from a different endpoint - * ([GradleEnterpriseApi.getGradleAttributes]). - * - * Don't expect client-side filtering to be efficient. Does as many concurrent calls - * as it can, requesting attributes in an eager coroutine, in [scope]. - */ -fun Flow.withGradleAttributes( - scope: CoroutineScope = GlobalScope, -): Flow> = - map { build -> - build to scope.async { - api.getGradleAttributes(build.id).await() - } - }.buffer(Int.MAX_VALUE).map { (build, attrs) -> - build to attrs.await() - } - /** * Gets [GradleAttributes] of all builds from a given date. Queries [GradleEnterpriseApi.getBuilds] * first, since it's the only endpoint providing a timeline of builds, then maps each to @@ -69,7 +44,10 @@ fun Flow.withGradleAttributes( * Don't expect client-side filtering to be efficient. Does as many concurrent calls * as it can, requesting attributes in an eager coroutine, in [scope]. For other params, * see [getBuildsFlow] and [GradleEnterpriseApi.getBuilds]. + * + * @param scope CoroutineScope in which to create coroutines. Defaults to [GlobalScope]. */ +@OptIn(DelicateCoroutinesApi::class) fun GradleEnterpriseApi.getGradleAttributesFlow( since: Long = 0, sinceBuild: String? = null, diff --git a/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/internal/ApiConstants.kt b/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/internal/ApiConstants.kt new file mode 100644 index 00000000..5a6a17e4 --- /dev/null +++ b/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/internal/ApiConstants.kt @@ -0,0 +1,8 @@ +package com.gabrielfeo.gradle.enterprise.api.internal + +import com.gabrielfeo.gradle.enterprise.api.GradleEnterpriseApi + +/** + * Undocumented max value of `/api/builds?maxBuilds`. Last checked in 2022.4. + */ +internal const val API_MAX_BUILDS = 1000 diff --git a/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/internal/operator/pagedUntilLastBuild.kt b/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/internal/operator/pagedUntilLastBuild.kt new file mode 100644 index 00000000..da677c7b --- /dev/null +++ b/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/internal/operator/pagedUntilLastBuild.kt @@ -0,0 +1,34 @@ +package com.gabrielfeo.gradle.enterprise.api.internal.operator + +import com.gabrielfeo.gradle.enterprise.api.* +import com.gabrielfeo.gradle.enterprise.api.internal.API_MAX_BUILDS +import com.gabrielfeo.gradle.enterprise.api.model.* +import kotlinx.coroutines.flow.* +import retrofit2.await + +/** + * Emits all available builds starting from the upstream Flow builds until the last build available. + * Makes paged requests to the API using `fromBuild`, [maxPerRequest] at a time. + */ +internal fun Flow.pagedUntilLastBuild( + maxPerRequest: Int, +): Flow { + val firstBuilds = this + return flow { + var lastBuildId = "" + firstBuilds.collect { + lastBuildId = it.id + emit(it) + } + if (lastBuildId.isEmpty()) { + return@flow + } else while (true) { + val builds = api.getBuilds(fromBuild = lastBuildId, maxBuilds = maxPerRequest).await() + emitAll(builds.asFlow()) + when { + builds.isEmpty() || builds.size < API_MAX_BUILDS -> break + else -> lastBuildId = builds.last().id + } + } + } +} diff --git a/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/internal/operator/withGradleAttributes.kt b/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/internal/operator/withGradleAttributes.kt new file mode 100644 index 00000000..ea3f1818 --- /dev/null +++ b/src/main/kotlin/com/gabrielfeo/gradle/enterprise/api/internal/operator/withGradleAttributes.kt @@ -0,0 +1,25 @@ +package com.gabrielfeo.gradle.enterprise.api.internal.operator + +import com.gabrielfeo.gradle.enterprise.api.* +import com.gabrielfeo.gradle.enterprise.api.model.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import retrofit2.await + +/** + * Joins builds with their [GradleAttributes], which comes from a different endpoint + * ([GradleEnterpriseApi.getGradleAttributes]). + * + * Don't expect client-side filtering to be efficient. Does as many concurrent calls + * as it can, requesting attributes in an eager coroutine, in [scope]. + */ +internal fun Flow.withGradleAttributes( + scope: CoroutineScope, +): Flow> = + map { build -> + build to scope.async { + api.getGradleAttributes(build.id).await() + } + }.buffer(Int.MAX_VALUE).map { (build, attrs) -> + build to attrs.await() + }