diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt index daf1a15782..926d7f3f45 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt @@ -1,5 +1,8 @@ package com.x8bit.bitwarden.data.platform.util +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + /** * Flat maps a successful [Result] with the given [transform] to another [Result], and leaves * failures untouched. @@ -20,3 +23,62 @@ fun T.asSuccess(): Result = */ fun Throwable.asFailure(): Result = Result.failure(this) + +/** + * Retrieves results from [firstResultProvider] and [secondResultProvider] by first running in + * parallel and then combining successful results using the given [zipper]. + */ +suspend fun zip( + firstResultProvider: suspend () -> Result, + secondResultProvider: suspend () -> Result, + zipper: suspend (first: T1, second: T2) -> R, +): Result = coroutineScope { + val firstResultDeferred = async { firstResultProvider() } + val secondResultDeferred = async { secondResultProvider() } + + val firstResult = firstResultDeferred.await() + val secondResult = secondResultDeferred.await() + + val errorOrNull = firstResult.exceptionOrNull() + ?: secondResult.exceptionOrNull() + + errorOrNull + ?.asFailure() + ?: zipper( + firstResult.getOrThrow(), + secondResult.getOrThrow(), + ) + .asSuccess() +} + +/** + * Retrieves results from [firstResultProvider], [secondResultProvider], and [thirdResultProvider] + * by first running in parallel and then combining successful results using the given [zipper]. + */ +suspend fun zip( + firstResultProvider: suspend () -> Result, + secondResultProvider: suspend () -> Result, + thirdResultProvider: suspend () -> Result, + zipper: suspend (first: T1, second: T2, third: T3) -> R, +): Result = coroutineScope { + val firstResultDeferred = async { firstResultProvider() } + val secondResultDeferred = async { secondResultProvider() } + val thirdResultDeferred = async { thirdResultProvider() } + + val firstResult = firstResultDeferred.await() + val secondResult = secondResultDeferred.await() + val thirdResult = thirdResultDeferred.await() + + val errorOrNull = firstResult.exceptionOrNull() + ?: secondResult.exceptionOrNull() + ?: thirdResult.exceptionOrNull() + + errorOrNull + ?.asFailure() + ?: zipper( + firstResult.getOrThrow(), + secondResult.getOrThrow(), + thirdResult.getOrThrow(), + ) + .asSuccess() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index b6154846d1..9a7e708d7d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.util.map import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.platform.util.flatMap +import com.x8bit.bitwarden.data.platform.util.zip import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService @@ -44,6 +45,7 @@ import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Default implementation of [VaultRepository]. @@ -54,7 +56,7 @@ class VaultRepositoryImpl constructor( private val ciphersService: CiphersService, private val vaultSdkSource: VaultSdkSource, private val authDiskSource: AuthDiskSource, - dispatcherManager: DispatcherManager, + private val dispatcherManager: DispatcherManager, ) : VaultRepository { private val scope = CoroutineScope(dispatcherManager.io) @@ -374,15 +376,21 @@ class VaultRepositoryImpl constructor( sendDataMutableStateFlow.update { newState } } - private suspend fun decryptSyncResponseAndUpdateVaultDataState(syncResponse: SyncResponseJson) { - val newState = vaultSdkSource - .decryptCipherList( - cipherList = syncResponse - .ciphers - .orEmpty() - .toEncryptedSdkCipherList(), - ) - .flatMap { decryptedCipherList -> + private suspend fun decryptSyncResponseAndUpdateVaultDataState( + syncResponse: SyncResponseJson, + ) = withContext(dispatcherManager.default) { + // Allow decryption of various types in parallel. + val newState = zip( + { + vaultSdkSource + .decryptCipherList( + cipherList = syncResponse + .ciphers + .orEmpty() + .toEncryptedSdkCipherList(), + ) + }, + { vaultSdkSource .decryptFolderList( folderList = syncResponse @@ -390,19 +398,15 @@ class VaultRepositoryImpl constructor( .orEmpty() .toEncryptedSdkFolderList(), ) - .map { decryptedFolderList -> - decryptedCipherList to decryptedFolderList - } - } + }, + ) { decryptedCipherList, decryptedFolderList -> + VaultData( + cipherViewList = decryptedCipherList, + folderViewList = decryptedFolderList, + ) + } .fold( - onSuccess = { (decryptedCipherList, decryptedFolderList) -> - DataState.Loaded( - data = VaultData( - cipherViewList = decryptedCipherList, - folderViewList = decryptedFolderList, - ), - ) - }, + onSuccess = { DataState.Loaded(data = it) }, onFailure = { DataState.Error(error = it) }, ) vaultDataMutableStateFlow.update { newState } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt index 2f911965a9..a8d3462bb2 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.platform.util +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -66,4 +67,120 @@ class ResultTest { throwable.asFailure(), ) } + + @Test + fun `zip with two arguments should return a success when both are successes`() = runTest { + assertEquals( + ("A" to 1).asSuccess(), + zip( + { "A".asSuccess() }, + { 1.asSuccess() }, + ) { first, second -> + first to second + }, + ) + } + + @Test + fun `zip with two arguments should return a failure when the first is a failure`() = runTest { + val throwable = Throwable() + assertEquals( + throwable.asFailure(), + zip( + { + @Suppress("USELESS_CAST") + throwable.asFailure() as Result + }, + { 1.asSuccess() }, + ) { first, second -> + first to second + }, + ) + } + + @Test + fun `zip with two arguments should return a failure when the second is a failure`() = runTest { + val throwable = Throwable() + assertEquals( + throwable.asFailure(), + zip( + { "A".asSuccess() }, + { + @Suppress("USELESS_CAST") + throwable.asFailure() as Result + }, + ) { first, second -> + first to second + }, + ) + } + + @Test + fun `zip with three arguments should return a success when all are successes`() = runTest { + assertEquals( + Triple("A", 1, true).asSuccess(), + zip( + { "A".asSuccess() }, + { 1.asSuccess() }, + { true.asSuccess() }, + ) { first, second, third -> + Triple(first, second, third) + }, + ) + } + + @Test + fun `zip with three arguments should return a failure when the first is a failure`() = runTest { + val throwable = Throwable() + assertEquals( + throwable.asFailure(), + zip( + { + @Suppress("USELESS_CAST") + throwable.asFailure() as Result + }, + { 1.asSuccess() }, + { true.asSuccess() }, + ) { first, second, third -> + Triple(first, second, third) + }, + ) + } + + @Test + fun `zip with three arguments should return a failure when the second is a failure`() = + runTest { + val throwable = Throwable() + assertEquals( + throwable.asFailure(), + zip( + { "A".asSuccess() }, + { + @Suppress("USELESS_CAST") + throwable.asFailure() as Result + }, + { true.asSuccess() }, + ) { first, second, third -> + Triple(first, second, third) + }, + ) + } + + @Test + fun `zip with three arguments should return a failure when the third is a failure`() = runTest { + val throwable = Throwable() + assertEquals( + throwable.asFailure(), + zip( + { "A".asSuccess() }, + { 1.asSuccess() }, + { + @Suppress("USELESS_CAST") + throwable.asFailure() as Result + }, + ) { first, second, third -> + Triple(first, second, third) + }, + ) + } }