From 456baa7823903363a96058dba72b1211f472cfb8 Mon Sep 17 00:00:00 2001 From: Vadym Yaroshchuk Date: Mon, 26 Jun 2023 16:19:57 +0200 Subject: [PATCH] feat: pagination (#18) --- gradle/wrapper/gradle-wrapper.properties | 2 +- .../api/grpc/GrpcTimeMatesRequestsEngine.kt | 13 +- .../sessions/AuthorizedSessionsApi.kt | 54 ++--- .../GetAuthorizationSessionsRequest.kt | 10 +- .../sdk/common/annotations/ExperimentalApi.kt | 14 +- .../exceptions/handler/SafeExceptionResult.kt | 8 +- .../timemates/sdk/common/pagination/Page.kt | 8 + .../{types/value => pagination}/PageToken.kt | 9 +- .../sdk/common/pagination/PagesIterator.kt | 187 ++++++++++++++++++ .../sdk/common/types/TimeMatesEntity.kt | 4 +- .../timemates/sdk/common/types/value/Count.kt | 2 +- .../io/timemates/sdk/timers/TimersApi.kt | 22 ++- .../sdk/timers/members/TimerMembersApi.kt | 27 ++- .../timers/members/invites/TimerInvitesApi.kt | 24 ++- .../invites/requests/GetInvitesRequest.kt | 10 +- .../members/requests/GetMembersRequest.kt | 10 +- .../members/requests/KickMemberRequest.kt | 1 - .../timers/requests/GetUserTimersRequest.kt | 11 +- .../timemates/sdk/users/profile/types/User.kt | 3 +- 19 files changed, 329 insertions(+), 90 deletions(-) create mode 100644 sdk/src/commonMain/kotlin/io/timemates/sdk/common/pagination/Page.kt rename sdk/src/commonMain/kotlin/io/timemates/sdk/common/{types/value => pagination}/PageToken.kt (50%) create mode 100644 sdk/src/commonMain/kotlin/io/timemates/sdk/common/pagination/PagesIterator.kt diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fae0804..bbec4bc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-rc-2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/grpc-engine/src/main/kotlin/io/timemates/api/grpc/GrpcTimeMatesRequestsEngine.kt b/grpc-engine/src/main/kotlin/io/timemates/api/grpc/GrpcTimeMatesRequestsEngine.kt index f1240d2..84ada3d 100644 --- a/grpc-engine/src/main/kotlin/io/timemates/api/grpc/GrpcTimeMatesRequestsEngine.kt +++ b/grpc-engine/src/main/kotlin/io/timemates/api/grpc/GrpcTimeMatesRequestsEngine.kt @@ -36,10 +36,11 @@ import io.timemates.sdk.common.exceptions.PermissionDeniedException import io.timemates.sdk.common.exceptions.UnauthorizedException import io.timemates.sdk.common.exceptions.UnavailableException import io.timemates.sdk.common.exceptions.UnsupportedException +import io.timemates.sdk.common.pagination.Page +import io.timemates.sdk.common.pagination.PageToken import io.timemates.sdk.common.types.TimeMatesEntity import io.timemates.sdk.common.types.TimeMatesRequest import io.timemates.sdk.common.types.value.Count -import io.timemates.sdk.common.types.value.PageToken import io.timemates.sdk.files.requests.GetFileBytesRequest import io.timemates.sdk.files.requests.UploadFileRequest import io.timemates.sdk.files.types.value.FileId @@ -158,7 +159,7 @@ public class GrpcTimeMatesRequestsEngine( .build(), headers = authorizedMetadata(request.accessHash), ).let { response -> - GetAuthorizationSessionsRequest.Result( + Page( response.authorizationsList.map(authMapper::grpcAuthorizationToSdkAuthorization), nextPageToken = response.nextPageToken.takeIf { it.isNotEmpty() } ?.let(PageToken::createOrThrow), @@ -246,7 +247,7 @@ public class GrpcTimeMatesRequestsEngine( }, headers = authorizedMetadata(request.accessHash) ).let { response -> - GetUserTimersRequest.Result( + Page( response.timersList.map { timersMapper.grpcTimerToSdkTimer(it) }, nextPageToken = response.nextPageToken.takeIf { response.hasNextPageToken() @@ -270,7 +271,7 @@ public class GrpcTimeMatesRequestsEngine( }, headers = authorizedMetadata(request.accessHash) ).let { response -> - GetMembersRequest.Response( + Page( response.usersList.map { usersMapper.grpcUserToSdkUser(it) }, nextPageToken = response.nextPageToken?.takeIf { it.isNotEmpty() } ?.let { PageToken.createOrThrow(it) } @@ -299,8 +300,8 @@ public class GrpcTimeMatesRequestsEngine( }, headers = authorizedMetadata(request.accessHash), ).let { response -> - GetInvitesRequest.Result( - invites = response.invitesList.map { timersMapper.grpcInviteToSdkInvite(it) }, + Page( + results = response.invitesList.map { timersMapper.grpcInviteToSdkInvite(it) }, nextPageToken = response.nextPageToken.takeIf { it.isNotEmpty() } ?.let { PageToken.createOrThrow(it) } ) diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/authorization/sessions/AuthorizedSessionsApi.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/authorization/sessions/AuthorizedSessionsApi.kt index ccc1fe9..6150783 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/authorization/sessions/AuthorizedSessionsApi.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/authorization/sessions/AuthorizedSessionsApi.kt @@ -5,15 +5,22 @@ import io.timemates.sdk.authorization.sessions.requests.TerminateCurrentAuthoriz import io.timemates.sdk.authorization.sessions.types.Authorization import io.timemates.sdk.authorization.sessions.types.value.AuthorizationId import io.timemates.sdk.common.annotations.UnimplementedApi +import io.timemates.sdk.common.constructor.createOrThrow import io.timemates.sdk.common.engine.TimeMatesRequestsEngine import io.timemates.sdk.common.internal.flatMap +import io.timemates.sdk.common.pagination.Page +import io.timemates.sdk.common.pagination.PageToken +import io.timemates.sdk.common.pagination.PagesIterator +import io.timemates.sdk.common.pagination.PagesIteratorImpl import io.timemates.sdk.common.providers.AccessHashProvider import io.timemates.sdk.common.providers.getAsResult import io.timemates.sdk.common.types.Empty -import io.timemates.sdk.common.types.value.PageToken +import io.timemates.sdk.common.types.value.Count import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds public class AuthorizedSessionsApi( private val engine: TimeMatesRequestsEngine, @@ -25,7 +32,7 @@ public class AuthorizedSessionsApi( * @param pageToken The token representing the page to retrieve. Pass `null` to start from the beginning. * @return The result of the API call, containing the list of authorizations and the next page token. */ - public suspend fun getSessions(pageToken: PageToken?): Result { + public suspend fun getSessions(pageToken: PageToken?): Result> { return tokenProvider.getAsResult() .flatMap { token -> engine.execute(GetAuthorizationSessionsRequest(pageToken, token)) } } @@ -48,38 +55,13 @@ public class AuthorizedSessionsApi( } } -/** - * Retrieves a flow of authorization sessions across multiple pages. - * - * @param pageToken The token representing the page to retrieve. Pass `null` to start from the beginning. - * @return A flow emitting lists of authorizations retrieved from each page. - */ -public fun AuthorizedSessionsApi.getSessionPages( +public fun AuthorizedSessionsApi.getSessionsPages( pageToken: PageToken?, - maxRetryCount: Int, -): Flow> = flow { - var retryCount = 0 - var currentToken = pageToken - - while (true) { - val response = getSessions(currentToken) - - if (response.isSuccess) { - val result = response.getOrThrow() - emit(result.authorizations) - - if (currentToken == result.nextPageToken) - break - - currentToken = result.nextPageToken - retryCount = 0 - } else { - retryCount++ - if (retryCount > maxRetryCount) { - break - } - - delay(1000) // Add delay before retrying - } - } -} \ No newline at end of file + maxRetries: Count = Count.createOrThrow(5), + initialDelayOnRetries: Duration = 1.seconds, +): PagesIterator = PagesIteratorImpl( + initialPageToken = pageToken, + delayOnServerErrors = initialDelayOnRetries, + maxRetries = maxRetries, + provider = { _, pageToken -> getSessions(pageToken) } +) \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/authorization/sessions/requests/GetAuthorizationSessionsRequest.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/authorization/sessions/requests/GetAuthorizationSessionsRequest.kt index a6c1058..d727df4 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/authorization/sessions/requests/GetAuthorizationSessionsRequest.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/authorization/sessions/requests/GetAuthorizationSessionsRequest.kt @@ -2,16 +2,12 @@ package io.timemates.sdk.authorization.sessions.requests import io.timemates.sdk.authorization.sessions.types.Authorization import io.timemates.sdk.authorization.types.value.AccessHash +import io.timemates.sdk.common.pagination.Page +import io.timemates.sdk.common.pagination.PageToken import io.timemates.sdk.common.types.TimeMatesEntity import io.timemates.sdk.common.types.TimeMatesRequest -import io.timemates.sdk.common.types.value.PageToken public data class GetAuthorizationSessionsRequest( val nextPageToken: PageToken?, val accessHash: AccessHash, -) : TimeMatesRequest() { - public data class Result( - val authorizations: List, - val nextPageToken: PageToken? - ) : TimeMatesEntity() -} \ No newline at end of file +) : TimeMatesRequest>() \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/annotations/ExperimentalApi.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/annotations/ExperimentalApi.kt index 324e944..a920340 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/annotations/ExperimentalApi.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/annotations/ExperimentalApi.kt @@ -1,4 +1,14 @@ package io.timemates.sdk.common.annotations -@RequiresOptIn(message = "Given Sdk API is experimental", level = RequiresOptIn.Level.WARNING) -public annotation class ExperimentalApi \ No newline at end of file +/** + * Annotation used to indicate that a given SDK API is experimental. + */ +@RequiresOptIn(message = "Given SDK API is experimental", level = RequiresOptIn.Level.ERROR) +public annotation class ExperimentalApi(val status: ApiStatus) + +public enum class ApiStatus { + PROTOTYPE, + IN_PROGRESS, + NEEDS_REVISION, + STABLE_CANDIDATE, +} \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/exceptions/handler/SafeExceptionResult.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/exceptions/handler/SafeExceptionResult.kt index 2060581..352fdc8 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/exceptions/handler/SafeExceptionResult.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/exceptions/handler/SafeExceptionResult.kt @@ -2,6 +2,7 @@ package io.timemates.sdk.common.exceptions.handler +import io.timemates.sdk.common.annotations.ApiStatus import io.timemates.sdk.common.annotations.ExperimentalApi import io.timemates.sdk.common.annotations.InternalApi import io.timemates.sdk.common.types.TimeMatesEntity @@ -17,6 +18,7 @@ import io.timemates.sdk.common.exceptions.* * @param E The type of the exception. * @property result The original result of the operation. */ +@ExperimentalApi(status = ApiStatus.NEEDS_REVISION) @JvmInline public value class SafeExceptionResult @InternalApi constructor( @property:InternalApi public val result: Result @@ -31,7 +33,7 @@ public value class SafeExceptionResult @InternalApi constructo * @receiver The original result of the operation. * @return The [SafeExceptionResult] containing the original result. */ -@ExperimentalApi +@ExperimentalApi(status = ApiStatus.NEEDS_REVISION) public fun Result.safeResult(): SafeExceptionResult { return SafeExceptionResult(this) } @@ -136,7 +138,7 @@ public fun SafeExceptionResult.ignoreNotFound(): SafeE * @param block The block of code to be executed if any exception is encountered. * @return The [SafeExceptionResult] after executing the block. */ -@ExperimentalApi +@ExperimentalApi(status = ApiStatus.NEEDS_REVISION) public inline fun SafeExceptionResult.whenOtherError( block: (E) -> Unit ): SafeExceptionResult { @@ -155,7 +157,7 @@ public inline fun SafeExceptionResult.whenOtherError( * * But, if it's possible, it's better to use just [Result] API instead of [SafeExceptionResult]. */ -@ExperimentalApi +@ExperimentalApi(status = ApiStatus.NEEDS_REVISION) public inline fun SafeExceptionResult.whenAnyError( block: (Throwable) -> Unit ): SafeExceptionResult { diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/pagination/Page.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/pagination/Page.kt new file mode 100644 index 0000000..37fb362 --- /dev/null +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/pagination/Page.kt @@ -0,0 +1,8 @@ +package io.timemates.sdk.common.pagination + +import io.timemates.sdk.common.types.TimeMatesEntity + +public data class Page( + val results: List, + val nextPageToken: PageToken?, +) : TimeMatesEntity() \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/types/value/PageToken.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/pagination/PageToken.kt similarity index 50% rename from sdk/src/commonMain/kotlin/io/timemates/sdk/common/types/value/PageToken.kt rename to sdk/src/commonMain/kotlin/io/timemates/sdk/common/pagination/PageToken.kt index c69b9a6..209b3a6 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/types/value/PageToken.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/pagination/PageToken.kt @@ -1,7 +1,14 @@ -package io.timemates.sdk.common.types.value +package io.timemates.sdk.common.pagination import io.timemates.sdk.common.constructor.Factory +/** + * A value class representing a page token. + * A page token is used to propagate pagination in APIs. It is a string token that + * identifies a specific page of results, and is used to retrieve the next page + * of results. + * @param string The string representation of the page token. + */ @JvmInline public value class PageToken private constructor(public val string: String) { public companion object : Factory() { diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/pagination/PagesIterator.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/pagination/PagesIterator.kt new file mode 100644 index 0000000..134dbd3 --- /dev/null +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/pagination/PagesIterator.kt @@ -0,0 +1,187 @@ +package io.timemates.sdk.common.pagination + +import io.timemates.sdk.common.annotations.ApiStatus +import io.timemates.sdk.common.annotations.ExperimentalApi +import io.timemates.sdk.common.constructor.createOrThrow +import io.timemates.sdk.common.exceptions.TimeMatesException +import io.timemates.sdk.common.pagination.PagesIteratorImpl.State +import io.timemates.sdk.common.types.TimeMatesEntity +import io.timemates.sdk.common.types.value.Count +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.retry +import kotlin.time.Duration + +/** + * Interface representing a page iterator for paginated data retrieval. + * + * @param T The type of elements in the page. + */ +public interface PagesIterator { + /** + * Returns `true` if there is another page available, `false` otherwise. + * + * @return `true` if there is another page, `false` otherwise. + */ + public suspend operator fun hasNext(): Boolean + + /** + * Returns the next page of elements. + * + * @return The list of elements in the next page. + * @throws NoSuchElementException if there are no more pages available. + */ + @Throws(NoSuchElementException::class) + public suspend operator fun next(): List + + /** + * Returns an iterator over the pages. + * + * @return The iterator itself. + */ + public operator fun iterator(): PagesIterator = this +} + +/** + * Converts the [PagesIterator] into a [Flow] of lists of elements. + * + * @return A [Flow] that emits lists of elements from the page iterator. + */ +public fun PagesIterator.asFlow(): Flow> = flow { + for (elements in this@asFlow) + emit(elements) +} + +/** + * Collects all elements from the [PagesIterator] into a [List]. + * + * @return A [List] containing all elements from the page iterator. + */ +public suspend fun PagesIterator.toList(): List { + return buildList { + for (elements in this@toList) + addAll(elements) + } +} + +/** + * Converts the elements from the [PagesIterator] into a [Sequence]. + * + * **Note that it's actually iterated at first time.** This is the reason, why + * it's actually experimental. + * + * @return A [Sequence] containing all elements from the page iterator. + */ +@ExperimentalApi(status = ApiStatus.NEEDS_REVISION) +public suspend fun PagesIterator.asSequence(): Sequence { + return toList().asSequence() +} + +/** + * Performs the given [block] on each page of elements from the [PagesIterator]. + * + * @param block The block to be executed on each page of elements. + */ +public suspend inline fun PagesIterator.forEachPage(block: (List) -> Unit) { + while (hasNext()) { + block(next()) + } +} + +/** + * Performs the given [block] on each element from the [PagesIterator]. + * + * @param block The block to be executed on each element. + */ +public suspend inline fun PagesIterator.forEach(block: (T) -> Unit) { + forEachPage { page -> + page.forEach(block) + } +} + + +/** + * Internal implementation class for the [PagesIterator] interface. + * + * @param T The type of elements in the page. + * @property chunkSize The number of elements per page. + * @property initialPageToken The initial page token to start from. + * @property provider The function responsible for fetching pages of data. + * @property delayOnServerErrors Delay between retries + * @property maxRetries Max count of retries until iterator reaches [State.DONE] state. + * @property increaseDelayOnRetries Determines whether we should increase initial [delayOnServerErrors] + * on every retry. + */ +internal class PagesIteratorImpl( + private val chunkSize: Count = Count.createOrThrow(20), + private val initialPageToken: PageToken?, + prevPage: Page? = null, + private val provider: suspend (size: Count, pageToken: PageToken?) -> Result>, + private val delayOnServerErrors: Duration, + private val maxRetries: Count = Count.createOrThrow(5), + private val increaseDelayOnRetries: Boolean = true, +) : PagesIterator { + /** + * Enum representing the state of the page iterator. + * + * - **DONE** – no more elements present. + * - **UNKNOWN** – initial state of the iterator, needs to be queried first. + * - **READY** – has more elements to consume (it's sdk-side decision, so there's possibility that + * next page has no elements at all). + */ + private enum class State { + UNKNOWN, READY, DONE, + } + + private var state: State = State.UNKNOWN + private var lastPage: Page? = prevPage + + /** + * Returns `true` if there is another page available, `false` otherwise. + */ + override suspend fun hasNext(): Boolean { + return state != State.DONE + } + + /** + * Returns the next page of elements. + * + * @throws NoSuchElementException if there are no more pages available. + */ + override suspend fun next(): List { + if (state == State.DONE) + throw NoSuchElementException("No more pages.") + + var delayOnServerErrors = delayOnServerErrors.inWholeMilliseconds + + return createPageFlow() + .catch { + delay(delayOnServerErrors) + if (increaseDelayOnRetries) + delayOnServerErrors *= 2 + } + .retry(maxRetries.int.toLong()) { cause -> cause is TimeMatesException } + .firstOrNull() + ?: emptyList().also { state = State.DONE } + } + + private fun createPageFlow(): Flow> = flow { + val nextPageToken = lastPage?.nextPageToken ?: initialPageToken + + val result = provider(chunkSize, nextPageToken) + val page = result.getOrThrow() + + state = if (page.results.size < chunkSize.int || page.nextPageToken == null) { + State.DONE + } else { + State.READY + } + + lastPage = page + + emit(page.results) + } +} diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/types/TimeMatesEntity.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/types/TimeMatesEntity.kt index 6edfda4..b9e553d 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/types/TimeMatesEntity.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/types/TimeMatesEntity.kt @@ -7,7 +7,7 @@ package io.timemates.sdk.common.types * It does not declare any methods or properties and is used solely to categorize classes as TimeMates entities. * Classes implementing this interface can be considered part of the TimeMates SDK. * - * It is not implemented by value-objects, such as [io.timemates.sdk.authorization.entities.value.EmailAddress], for example, - * because they considered only as a part of request / response entities. + * It is not implemented by value-objects, such as [io.timemates.sdk.users.profile.types.value.EmailAddress], + * for example, because they considered only as a part of request / response entities. */ public abstract class TimeMatesEntity internal constructor() \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/types/value/Count.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/types/value/Count.kt index 90fb157..1275167 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/common/types/value/Count.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/common/types/value/Count.kt @@ -4,7 +4,7 @@ import io.timemates.sdk.common.constructor.CreationFailure import io.timemates.sdk.common.constructor.Factory @JvmInline -public value class Count internal constructor(public val int: Int) { +public value class Count private constructor(public val int: Int) { public companion object : Factory() { public const val MIN_VALUE: Int = 0 diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/TimersApi.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/TimersApi.kt index 60f4aa8..802d4b1 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/TimersApi.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/TimersApi.kt @@ -1,11 +1,16 @@ package io.timemates.sdk.timers +import io.timemates.sdk.common.constructor.createOrThrow import io.timemates.sdk.common.engine.TimeMatesRequestsEngine import io.timemates.sdk.common.internal.flatMap +import io.timemates.sdk.common.pagination.Page +import io.timemates.sdk.common.pagination.PagesIterator +import io.timemates.sdk.common.pagination.PagesIteratorImpl +import io.timemates.sdk.common.pagination.PageToken import io.timemates.sdk.common.providers.AccessHashProvider import io.timemates.sdk.common.providers.getAsResult import io.timemates.sdk.common.types.Empty -import io.timemates.sdk.common.types.value.PageToken +import io.timemates.sdk.common.types.value.Count import io.timemates.sdk.timers.members.TimerMembersApi import io.timemates.sdk.timers.requests.* import io.timemates.sdk.timers.types.Timer @@ -13,6 +18,8 @@ import io.timemates.sdk.timers.types.TimerSettings import io.timemates.sdk.timers.types.value.TimerDescription import io.timemates.sdk.timers.types.value.TimerId import io.timemates.sdk.timers.types.value.TimerName +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds /** * Provides methods for managing timers. @@ -84,7 +91,7 @@ public class TimersApi( * @param pageToken The token representing the next page of results, or null for the first page. * @return A [Result] containing the result of the get user timers request if successful, or an error if unsuccessful. */ - public suspend fun getUserTimers(pageToken: PageToken?): Result { + public suspend fun getUserTimers(pageToken: PageToken? = null): Result> { return tokenProvider.getAsResult() .flatMap { token -> engine.execute(GetUserTimersRequest(token, pageToken)) } } @@ -100,3 +107,14 @@ public class TimersApi( .flatMap { token -> engine.execute(RemoveTimerRequest(token, timerId)) } } } + +public fun TimersApi.getUserTimersPages( + pageToken: PageToken? = null, + maxRetries: Count = Count.createOrThrow(5), + initialDelayOnRetries: Duration = 1.seconds, +): PagesIterator = PagesIteratorImpl( + initialPageToken = pageToken, + provider = { _, pageToken -> getUserTimers(pageToken) }, + maxRetries = maxRetries, + delayOnServerErrors = initialDelayOnRetries, +) \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/TimerMembersApi.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/TimerMembersApi.kt index 7cedcea..b066287 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/TimerMembersApi.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/TimerMembersApi.kt @@ -1,16 +1,24 @@ package io.timemates.sdk.timers.members +import io.timemates.sdk.common.constructor.createOrThrow import io.timemates.sdk.common.engine.TimeMatesRequestsEngine import io.timemates.sdk.common.internal.flatMap +import io.timemates.sdk.common.pagination.Page +import io.timemates.sdk.common.pagination.PagesIterator +import io.timemates.sdk.common.pagination.PagesIteratorImpl +import io.timemates.sdk.common.pagination.PageToken import io.timemates.sdk.common.providers.AccessHashProvider import io.timemates.sdk.common.providers.getAsResult import io.timemates.sdk.common.types.Empty -import io.timemates.sdk.common.types.value.PageToken +import io.timemates.sdk.common.types.value.Count import io.timemates.sdk.timers.members.invites.TimerInvitesApi import io.timemates.sdk.timers.members.requests.GetMembersRequest import io.timemates.sdk.timers.members.requests.KickMemberRequest import io.timemates.sdk.timers.types.value.TimerId +import io.timemates.sdk.users.profile.types.User import io.timemates.sdk.users.profile.types.value.UserId +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds /** * Provides methods for managing timer members. @@ -32,7 +40,10 @@ public class TimerMembersApi( * @param pageToken The token representing the next page of results, or null for the first page. * @return A [Result] containing the result of the get members request if successful, or an error if unsuccessful. */ - public suspend fun getMembers(timerId: TimerId, pageToken: PageToken?): Result { + public suspend fun getMembers( + timerId: TimerId, + pageToken: PageToken? = null, + ): Result> { return tokenProvider.getAsResult() .flatMap { token -> engine.execute(GetMembersRequest(token, timerId, pageToken)) } } @@ -49,3 +60,15 @@ public class TimerMembersApi( .flatMap { token -> engine.execute(KickMemberRequest(token, timerId, userId)) } } } + +public fun TimerMembersApi.getMembersPages( + timerId: TimerId, + pageToken: PageToken? = null, + maxRetries: Count = Count.createOrThrow(5), + initialDelayOnRetries: Duration = 1.seconds, +): PagesIterator = PagesIteratorImpl( + initialPageToken = pageToken, + maxRetries = maxRetries, + delayOnServerErrors = initialDelayOnRetries, + provider = { _, pageToken -> getMembers(timerId, pageToken) }, +) diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/invites/TimerInvitesApi.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/invites/TimerInvitesApi.kt index d44574a..65a689c 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/invites/TimerInvitesApi.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/invites/TimerInvitesApi.kt @@ -1,17 +1,24 @@ package io.timemates.sdk.timers.members.invites +import io.timemates.sdk.common.constructor.createOrThrow import io.timemates.sdk.common.engine.TimeMatesRequestsEngine import io.timemates.sdk.common.internal.flatMap +import io.timemates.sdk.common.pagination.Page +import io.timemates.sdk.common.pagination.PagesIterator +import io.timemates.sdk.common.pagination.PagesIteratorImpl +import io.timemates.sdk.common.pagination.PageToken import io.timemates.sdk.common.providers.AccessHashProvider import io.timemates.sdk.common.providers.getAsResult import io.timemates.sdk.common.types.Empty import io.timemates.sdk.common.types.value.Count -import io.timemates.sdk.common.types.value.PageToken import io.timemates.sdk.timers.members.invites.requests.CreateInviteRequest import io.timemates.sdk.timers.members.invites.requests.GetInvitesRequest import io.timemates.sdk.timers.members.invites.requests.RemoveInviteRequest +import io.timemates.sdk.timers.members.invites.types.Invite import io.timemates.sdk.timers.members.invites.types.value.InviteCode import io.timemates.sdk.timers.types.value.TimerId +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds /** * Provides methods for managing timer invites. @@ -44,7 +51,7 @@ public class TimerInvitesApi( * @param pageToken The token representing the next page of results, or null for the first page. * @return A [Result] containing the result of the get invites request if successful, or an error if unsuccessful. */ - public suspend fun getInvites(timerId: TimerId, pageToken: PageToken?): Result { + public suspend fun getInvites(timerId: TimerId, pageToken: PageToken?): Result> { return tokenProvider.getAsResult() .flatMap { token -> engine.execute(GetInvitesRequest(token, timerId, pageToken)) } } @@ -59,6 +66,17 @@ public class TimerInvitesApi( public suspend fun removeInvite(timerId: TimerId, inviteCode: InviteCode): Result { return tokenProvider.getAsResult() .flatMap { token -> engine.execute(RemoveInviteRequest(token, timerId, inviteCode)) } - .map { Empty } } } + +public fun TimerInvitesApi.getInvitesPages( + timerId: TimerId, + pageToken: PageToken? = null, + maxRetries: Count = Count.createOrThrow(5), + initialDelayOnRetries: Duration = 1.seconds, +): PagesIterator = PagesIteratorImpl( + initialPageToken = pageToken, + maxRetries = maxRetries, + delayOnServerErrors = initialDelayOnRetries, + provider = { _, pageToken -> getInvites(timerId, pageToken) }, +) diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/invites/requests/GetInvitesRequest.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/invites/requests/GetInvitesRequest.kt index 2ed4479..3a22851 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/invites/requests/GetInvitesRequest.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/invites/requests/GetInvitesRequest.kt @@ -1,9 +1,10 @@ package io.timemates.sdk.timers.members.invites.requests import io.timemates.sdk.authorization.types.value.AccessHash +import io.timemates.sdk.common.pagination.Page +import io.timemates.sdk.common.pagination.PageToken import io.timemates.sdk.common.types.TimeMatesEntity import io.timemates.sdk.common.types.TimeMatesRequest -import io.timemates.sdk.common.types.value.PageToken import io.timemates.sdk.timers.members.invites.types.Invite import io.timemates.sdk.timers.types.value.TimerId @@ -11,9 +12,4 @@ public data class GetInvitesRequest( val accessHash: AccessHash, val timerId: TimerId, val pageToken: PageToken?, -) : TimeMatesRequest() { - public data class Result( - val invites: List, - val nextPageToken: PageToken?, - ) : TimeMatesEntity() -} \ No newline at end of file +) : TimeMatesRequest>() \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/requests/GetMembersRequest.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/requests/GetMembersRequest.kt index 59a86ef..c8e4d1b 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/requests/GetMembersRequest.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/requests/GetMembersRequest.kt @@ -1,9 +1,10 @@ package io.timemates.sdk.timers.members.requests import io.timemates.sdk.authorization.types.value.AccessHash +import io.timemates.sdk.common.pagination.Page +import io.timemates.sdk.common.pagination.PageToken import io.timemates.sdk.common.types.TimeMatesEntity import io.timemates.sdk.common.types.TimeMatesRequest -import io.timemates.sdk.common.types.value.PageToken import io.timemates.sdk.timers.types.value.TimerId import io.timemates.sdk.users.profile.types.User @@ -11,9 +12,4 @@ public data class GetMembersRequest( val accessHash: AccessHash, val timerId: TimerId, val pageToken: PageToken?, -) : TimeMatesRequest() { - public data class Response( - val members: List, - val nextPageToken: PageToken?, - ) : TimeMatesEntity() -} \ No newline at end of file +) : TimeMatesRequest>() \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/requests/KickMemberRequest.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/requests/KickMemberRequest.kt index 22741a3..8b8909a 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/requests/KickMemberRequest.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/members/requests/KickMemberRequest.kt @@ -3,7 +3,6 @@ package io.timemates.sdk.timers.members.requests import io.timemates.sdk.authorization.types.value.AccessHash import io.timemates.sdk.common.types.Empty import io.timemates.sdk.common.types.TimeMatesRequest -import io.timemates.sdk.common.types.value.PageToken import io.timemates.sdk.timers.types.value.TimerId import io.timemates.sdk.users.profile.types.value.UserId diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/requests/GetUserTimersRequest.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/requests/GetUserTimersRequest.kt index d924af8..f5f86e2 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/requests/GetUserTimersRequest.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/timers/requests/GetUserTimersRequest.kt @@ -1,17 +1,12 @@ package io.timemates.sdk.timers.requests import io.timemates.sdk.authorization.types.value.AccessHash -import io.timemates.sdk.common.types.TimeMatesEntity +import io.timemates.sdk.common.pagination.Page +import io.timemates.sdk.common.pagination.PageToken import io.timemates.sdk.common.types.TimeMatesRequest -import io.timemates.sdk.common.types.value.PageToken import io.timemates.sdk.timers.types.Timer public data class GetUserTimersRequest( val accessHash: AccessHash, val pageToken: PageToken?, -) : TimeMatesRequest() { - public data class Result( - val timers: List, - val nextPageToken: PageToken?, - ) : TimeMatesEntity() -} \ No newline at end of file +) : TimeMatesRequest>() \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/io/timemates/sdk/users/profile/types/User.kt b/sdk/src/commonMain/kotlin/io/timemates/sdk/users/profile/types/User.kt index a5fed45..ebbd6d3 100644 --- a/sdk/src/commonMain/kotlin/io/timemates/sdk/users/profile/types/User.kt +++ b/sdk/src/commonMain/kotlin/io/timemates/sdk/users/profile/types/User.kt @@ -1,5 +1,6 @@ package io.timemates.sdk.users.profile.types +import io.timemates.sdk.common.types.TimeMatesEntity import io.timemates.sdk.files.types.value.FileId import io.timemates.sdk.users.profile.types.value.EmailAddress import io.timemates.sdk.users.profile.types.value.UserDescription @@ -21,4 +22,4 @@ public data class User( val description: UserDescription, val avatarFileId: FileId, val emailAddress: EmailAddress?, -) \ No newline at end of file +) : TimeMatesEntity() \ No newline at end of file