Updated on 2026-08-14
This commit is contained in:
parent
dcee3b8ba2
commit
20dad876f0
5 changed files with 336 additions and 0 deletions
|
|
@ -14,11 +14,18 @@ interface RuntimeStateStore<T> {
|
|||
/** Get flow of elements [T] */
|
||||
fun get(): StateFlow<T>
|
||||
|
||||
/** Get element [T] synchronously or null */
|
||||
suspend fun getSyncOrNull(): T?
|
||||
|
||||
/** Store [value] */
|
||||
suspend fun store(value: T)
|
||||
|
||||
/** Update current value by [function] */
|
||||
suspend fun update(function: (T) -> T)
|
||||
|
||||
/** Clear stored value */
|
||||
fun clear()
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
|
|
@ -32,6 +39,8 @@ interface RuntimeStateStore<T> {
|
|||
|
||||
override fun get(): StateFlow<T> = flow
|
||||
|
||||
override suspend fun getSyncOrNull(): T? = flow.value
|
||||
|
||||
override suspend fun store(value: T) {
|
||||
flow.value = value
|
||||
}
|
||||
|
|
@ -39,6 +48,10 @@ interface RuntimeStateStore<T> {
|
|||
override suspend fun update(function: (T) -> T) {
|
||||
flow.update(function)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
flow.value = defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.data.account.store
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Store for managing archived accounts with support for data expiration
|
||||
*
|
||||
* @property runtimeStore the underlying runtime shared store for storing the list of archived accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class ArchivedAccountsStore(
|
||||
private val runtimeStore: RuntimeStateStore<List<ArchivedAccount>?>,
|
||||
) {
|
||||
|
||||
private var timestamp: Long? = null
|
||||
|
||||
/** Retrieves a flow of archived accounts, filtering out null values */
|
||||
fun get(): Flow<List<ArchivedAccount>> {
|
||||
return runtimeStore.get()
|
||||
.map {
|
||||
if (isDataExpired()) null else it
|
||||
}
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
/** Retrieves the list of archived accounts synchronously, or null if the data is expired */
|
||||
suspend fun getSyncOrNull(): List<ArchivedAccount>? {
|
||||
if (isDataExpired()) return null
|
||||
|
||||
return runtimeStore.getSyncOrNull()
|
||||
}
|
||||
|
||||
/** Stores the provided list of archived accounts [value] */
|
||||
suspend fun store(value: List<ArchivedAccount>) {
|
||||
timestamp = System.currentTimeMillis()
|
||||
|
||||
runtimeStore.store(value)
|
||||
}
|
||||
|
||||
private fun isDataExpired(): Boolean {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val storedTime = timestamp ?: return true
|
||||
|
||||
return currentTime - storedTime >= EXPIRATION_DURATION_MS
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun setTimestamp(time: Long) {
|
||||
timestamp = time
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun clear() {
|
||||
timestamp = null
|
||||
runtimeStore.clear()
|
||||
}
|
||||
|
||||
private companion object Companion {
|
||||
val EXPIRATION_DURATION_MS = 120.seconds.inWholeMicroseconds
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.data.account.store
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Factory for creating and managing instances of [ArchivedAccountsStore].
|
||||
|
||||
* and reused for each unique [UserWalletId].
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class ArchivedAccountsStoreFactory {
|
||||
|
||||
private val createdRuntimeStores = ConcurrentHashMap<UserWalletId, ArchivedAccountsStore>()
|
||||
|
||||
/**
|
||||
* Creates or retrieves an existing instance of [ArchivedAccountsStore] for the given [userWalletId].
|
||||
*
|
||||
* @param userWalletId the unique identifier for the user wallet
|
||||
*/
|
||||
fun create(userWalletId: UserWalletId): ArchivedAccountsStore {
|
||||
return createdRuntimeStores.computeIfAbsent(userWalletId) {
|
||||
ArchivedAccountsStore(runtimeStore = RuntimeStateStore(defaultValue = null))
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun getAllStores(): Map<UserWalletId, ArchivedAccountsStore> = createdRuntimeStores.toMap()
|
||||
|
||||
@VisibleForTesting
|
||||
fun clearStores() {
|
||||
createdRuntimeStores.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.data.account.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class ArchivedAccountsStoreFactoryTest {
|
||||
|
||||
private val factory = ArchivedAccountsStoreFactory()
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
factory.clearStores()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates new store for unique userWalletId`() {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId("001")
|
||||
val createdStore = factory.create(userWalletId)
|
||||
|
||||
// Act
|
||||
val actual = factory.getAllStores()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly(userWalletId, createdStore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reuses existing data store for same userWalletId`() {
|
||||
val userWalletId = UserWalletId("011")
|
||||
|
||||
// Arrange (first creation)
|
||||
val firstStore = factory.create(userWalletId = userWalletId)
|
||||
|
||||
// Act (first creation)
|
||||
val actual1 = factory.getAllStores()
|
||||
|
||||
// Assert (first creation)
|
||||
Truth.assertThat(actual1).containsExactly(userWalletId, firstStore)
|
||||
|
||||
// Arrange (second creation)
|
||||
val secondStore = factory.create(userWalletId = userWalletId)
|
||||
|
||||
// Act (second creation)
|
||||
val actual2 = factory.getAllStores()
|
||||
|
||||
// Assert (second creation)
|
||||
Truth.assertThat(actual2).containsExactly(userWalletId, secondStore)
|
||||
Truth.assertThat(firstStore).isSameInstanceAs(secondStore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates separate data stores for different userWalletIds`() {
|
||||
// Arrange (first creation)
|
||||
val firstWalletId = UserWalletId("011")
|
||||
val firstStore = factory.create(userWalletId = firstWalletId)
|
||||
|
||||
// Act (first creation)
|
||||
val actual1 = factory.getAllStores()
|
||||
|
||||
// Assert (first creation)
|
||||
Truth.assertThat(actual1).containsExactly(firstWalletId, firstStore)
|
||||
|
||||
// Arrange (second creation)
|
||||
val secondWalletId = UserWalletId("011")
|
||||
val secondStore = factory.create(userWalletId = secondWalletId)
|
||||
|
||||
// Act (second creation)
|
||||
val actual2 = factory.getAllStores()
|
||||
|
||||
// Assert (second creation)
|
||||
val expected = mapOf(firstWalletId to firstStore, secondWalletId to secondStore)
|
||||
Truth.assertThat(actual2).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package com.tangem.data.account.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class ArchivedAccountsStoreTest {
|
||||
|
||||
private val runtimeStore: RuntimeStateStore<List<ArchivedAccount>?> = RuntimeStateStore(defaultValue = null)
|
||||
private val archivedAccountsStore: ArchivedAccountsStore = ArchivedAccountsStore(runtimeStore = runtimeStore)
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
archivedAccountsStore.clear()
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Get {
|
||||
|
||||
@Test
|
||||
fun `get returns empty flow`() = runTest {
|
||||
// Act
|
||||
val actual = getEmittedValues(archivedAccountsStore.get())
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty() // nothing emmited
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get returns flow with not expired data`() = runTest {
|
||||
// Arrange
|
||||
val archivedAccount = createArchivedAccount()
|
||||
archivedAccountsStore.store(value = listOf(archivedAccount))
|
||||
|
||||
// Act
|
||||
val actual = getEmittedValues(archivedAccountsStore.get())
|
||||
|
||||
// Assert
|
||||
val expected = listOf(archivedAccount)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get returns flow with expired data`() = runTest {
|
||||
// Arrange
|
||||
archivedAccountsStore.store(value = listOf(createArchivedAccount()))
|
||||
archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() - 120.seconds.inWholeMicroseconds)
|
||||
|
||||
// Act
|
||||
val actual = getEmittedValues(archivedAccountsStore.get())
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty() // nothing emmited
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetSyncOrnNull {
|
||||
|
||||
@Test
|
||||
fun `getSyncOrNull returns null`() = runTest {
|
||||
// Act
|
||||
val actual = archivedAccountsStore.getSyncOrNull()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get returns flow with not expired data`() = runTest {
|
||||
// Arrange
|
||||
val archivedAccount = createArchivedAccount()
|
||||
archivedAccountsStore.store(value = listOf(archivedAccount))
|
||||
|
||||
// Act
|
||||
val actual = archivedAccountsStore.getSyncOrNull()
|
||||
|
||||
// Assert
|
||||
val expected = archivedAccount
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get returns flow with expired data`() = runTest {
|
||||
// Arrange
|
||||
archivedAccountsStore.store(value = listOf(createArchivedAccount()))
|
||||
archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() - 120.seconds.inWholeMicroseconds)
|
||||
|
||||
// Act
|
||||
val actual = archivedAccountsStore.getSyncOrNull()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun store() = runTest {
|
||||
// Arrange
|
||||
val archivedAccount = createArchivedAccount()
|
||||
|
||||
// Act
|
||||
archivedAccountsStore.store(value = listOf(archivedAccount))
|
||||
val actual = runtimeStore.getSyncOrNull()
|
||||
|
||||
// Assert
|
||||
val expected = archivedAccount
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
}
|
||||
|
||||
private fun createArchivedAccount(): ArchivedAccount {
|
||||
return ArchivedAccount(
|
||||
accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = UserWalletId("011"),
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
),
|
||||
name = AccountName("Archived Account").getOrNull()!!,
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
tokensCount = 2,
|
||||
networksCount = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue