Ensure VaultDiskSource emits when replace operation does not actually change any data (#412)

This commit is contained in:
David Perez 2023-12-19 14:27:03 -06:00 committed by Álison Fernandes
parent 27140bf02c
commit bf9845d7a0
6 changed files with 100 additions and 52 deletions

View file

@ -0,0 +1,9 @@
package com.x8bit.bitwarden.data.platform.repository.util
import kotlinx.coroutines.flow.MutableSharedFlow
/**
* Creates a [MutableSharedFlow] with a buffer of [Int.MAX_VALUE].
*/
fun <T> bufferedMutableSharedFlow(): MutableSharedFlow<T> =
MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE)

View file

@ -25,6 +25,9 @@ interface VaultDiskSource {
/**
* Replaces all [vault] data for a given [userId] with the new `vault`.
*
* This will always cause the [getCiphers], [getCollections], and [getFolders] functions to
* re-emit even if the underlying data has not changed.
*/
suspend fun replaceVaultData(userId: String, vault: SyncResponseJson)

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.datasource.disk
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FoldersDao
@ -12,6 +13,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ -25,49 +27,63 @@ class VaultDiskSourceImpl(
private val json: Json,
) : VaultDiskSource {
private val forceCiphersFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Cipher>>()
private val forceCollectionsFlow =
bufferedMutableSharedFlow<List<SyncResponseJson.Collection>>()
private val forceFolderFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Folder>>()
override fun getCiphers(
userId: String,
): Flow<List<SyncResponseJson.Cipher>> =
ciphersDao
.getAllCiphers(userId = userId)
.map { entities ->
entities.map { entity ->
json.decodeFromString<SyncResponseJson.Cipher>(entity.cipherJson)
}
}
merge(
forceCiphersFlow,
ciphersDao
.getAllCiphers(userId = userId)
.map { entities ->
entities.map { entity ->
json.decodeFromString<SyncResponseJson.Cipher>(entity.cipherJson)
}
},
)
override fun getCollections(
userId: String,
): Flow<List<SyncResponseJson.Collection>> =
collectionsDao
.getAllCollections(userId = userId)
.map { entities ->
entities.map { entity ->
SyncResponseJson.Collection(
id = entity.id,
name = entity.name,
organizationId = entity.organizationId,
shouldHidePasswords = entity.shouldHidePasswords,
externalId = entity.externalId,
isReadOnly = entity.isReadOnly,
)
}
}
merge(
forceCollectionsFlow,
collectionsDao
.getAllCollections(userId = userId)
.map { entities ->
entities.map { entity ->
SyncResponseJson.Collection(
id = entity.id,
name = entity.name,
organizationId = entity.organizationId,
shouldHidePasswords = entity.shouldHidePasswords,
externalId = entity.externalId,
isReadOnly = entity.isReadOnly,
)
}
},
)
override fun getFolders(
userId: String,
): Flow<List<SyncResponseJson.Folder>> =
foldersDao
.getAllFolders(userId = userId)
.map { entities ->
entities.map { entity ->
SyncResponseJson.Folder(
id = entity.id,
name = entity.name,
revisionDate = entity.revisionDate,
)
}
}
merge(
forceFolderFlow,
foldersDao
.getAllFolders(userId = userId)
.map { entities ->
entities.map { entity ->
SyncResponseJson.Folder(
id = entity.id,
name = entity.name,
revisionDate = entity.revisionDate,
)
}
},
)
override suspend fun replaceVaultData(
userId: String,
@ -116,11 +132,17 @@ class VaultDiskSourceImpl(
},
)
}
awaitAll(
deferredCiphers,
deferredCollections,
deferredFolders,
)
// When going from 0 items to 0 items, the respective dao flow will not re-emit
// So we use this to give it a little push.
if (!deferredCiphers.await()) {
forceCiphersFlow.tryEmit(emptyList())
}
if (!deferredCollections.await()) {
forceCollectionsFlow.tryEmit(emptyList())
}
if (!deferredFolders.await()) {
forceFolderFlow.tryEmit(emptyList())
}
}
}

View file

@ -30,18 +30,21 @@ interface CiphersDao {
): Flow<List<CipherEntity>>
/**
* Deletes all the stored ciphers associated with the given [userId].
* Deletes all the stored ciphers associated with the given [userId]. This will return the
* number of rows deleted by this query.
*/
@Query("DELETE FROM ciphers WHERE user_id = :userId")
suspend fun deleteAllCiphers(userId: String)
suspend fun deleteAllCiphers(userId: String): Int
/**
* Deletes all the stored ciphers associated with the given [userId] and then add all new
* [ciphers] to the database.
* [ciphers] to the database. This will return `true` if any changes were made to the database
* and `false` otherwise.
*/
@Transaction
suspend fun replaceAllCiphers(userId: String, ciphers: List<CipherEntity>) {
deleteAllCiphers(userId)
suspend fun replaceAllCiphers(userId: String, ciphers: List<CipherEntity>): Boolean {
val deletedCiphersCount = deleteAllCiphers(userId)
insertCiphers(ciphers)
return deletedCiphersCount > 0 || ciphers.isNotEmpty()
}
}

View file

@ -34,10 +34,11 @@ interface CollectionsDao {
fun getAllCollections(userId: String): Flow<List<CollectionEntity>>
/**
* Deletes all the stored collections associated with the given [userId].
* Deletes all the stored collections associated with the given [userId]. This will return the
* number of rows deleted by this query.
*/
@Query("DELETE FROM collections WHERE user_id = :userId")
suspend fun deleteAllCollections(userId: String)
suspend fun deleteAllCollections(userId: String): Int
/**
* Deletes the stored collection associated with the given [userId] that matches the
@ -48,11 +49,18 @@ interface CollectionsDao {
/**
* Deletes all the stored [collections] associated with the given [userId] and then add all new
* `collections` to the database.
* `collections` to the database. This will return `true` if any changes were made to the
* database and `false` otherwise.
*
* @return `true` if any changes were made to the database.
*/
@Transaction
suspend fun replaceAllCollections(userId: String, collections: List<CollectionEntity>) {
deleteAllCollections(userId)
suspend fun replaceAllCollections(
userId: String,
collections: List<CollectionEntity>,
): Boolean {
val deletedCollectionsCount = deleteAllCollections(userId)
insertCollections(collections)
return deletedCollectionsCount > 0 || collections.isNotEmpty()
}
}

View file

@ -36,10 +36,11 @@ interface FoldersDao {
): Flow<List<FolderEntity>>
/**
* Deletes all the stored folders associated with the given [userId].
* Deletes all the stored folders associated with the given [userId]. This will return the
* number of rows deleted by this query.
*/
@Query("DELETE FROM folders WHERE user_id = :userId")
suspend fun deleteAllFolders(userId: String)
suspend fun deleteAllFolders(userId: String): Int
/**
* Deletes the stored folder associated with the given [userId] that matches the [folderId].
@ -49,11 +50,13 @@ interface FoldersDao {
/**
* Deletes all the stored [folders] associated with the given [userId] and then add all new
* `folders` to the database.
* `folders` to the database. This will return `true` if any changes were made to the database
* and `false` otherwise.
*/
@Transaction
suspend fun replaceAllFolders(userId: String, folders: List<FolderEntity>) {
deleteAllFolders(userId)
suspend fun replaceAllFolders(userId: String, folders: List<FolderEntity>): Boolean {
val deletedFoldersCount = deleteAllFolders(userId)
insertFolders(folders)
return deletedFoldersCount > 0 || folders.isNotEmpty()
}
}