diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt index 39c2ac83..6e74462c 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt @@ -24,6 +24,7 @@ import org.mobilenativefoundation.store.store5.impl.extensions.now import org.mobilenativefoundation.store.store5.internal.concurrent.ThreadSafety import org.mobilenativefoundation.store.store5.internal.definition.WriteRequestQueue import org.mobilenativefoundation.store.store5.internal.result.EagerConflictResolutionResult +import org.mobilenativefoundation.store.store5.internal.result.StoreDelegateWriteResult @OptIn(ExperimentalStoreApi::class) internal class RealMutableStore( @@ -85,25 +86,30 @@ internal class RealMutableStore StoreWriteResponse. - when (updaterResult) { - is UpdaterResult.Error.Exception -> StoreWriteResponse.Error.Exception(updaterResult.error) - is UpdaterResult.Error.Message -> StoreWriteResponse.Error.Message(updaterResult.message) - is UpdaterResult.Success.Typed<*> -> { - val typedValue = updaterResult.value as? Response - if (typedValue == null) { - StoreWriteResponse.Success.Untyped(updaterResult.value) - } else { - StoreWriteResponse.Success.Typed(updaterResult.value) + // Only proceed to network if local write succeeded. + when (val delegateWriteResult = delegate.write(writeRequest.key, writeRequest.value)) { + is StoreDelegateWriteResult.Error.Exception -> { + StoreWriteResponse.Error.Exception(delegateWriteResult.error) + } + is StoreDelegateWriteResult.Error.Message -> { + StoreWriteResponse.Error.Message(delegateWriteResult.error) + } + is StoreDelegateWriteResult.Success -> { + // Try to sync to network. + when (val updaterResult = tryUpdateServer(writeRequest)) { + is UpdaterResult.Error.Exception -> StoreWriteResponse.Error.Exception(updaterResult.error) + is UpdaterResult.Error.Message -> StoreWriteResponse.Error.Message(updaterResult.message) + is UpdaterResult.Success.Typed<*> -> { + val typedValue = updaterResult.value as? Response + if (typedValue == null) { + StoreWriteResponse.Success.Untyped(updaterResult.value) + } else { + StoreWriteResponse.Success.Typed(updaterResult.value) + } + } + is UpdaterResult.Success.Untyped -> StoreWriteResponse.Success.Untyped(updaterResult.value) } } - - is UpdaterResult.Success.Untyped -> StoreWriteResponse.Success.Untyped(updaterResult.value) } } catch (throwable: Throwable) { StoreWriteResponse.Error.Exception(throwable) diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt index d0198297..20f7fe8b 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt @@ -332,9 +332,13 @@ internal class RealStore( value: Output, ): StoreDelegateWriteResult = try { - memCache?.put(key, value) - sourceOfTruth?.write(key, converter.fromOutputToLocal(value)) - StoreDelegateWriteResult.Success + val writeException = sourceOfTruth?.write(key, converter.fromOutputToLocal(value)) + if (writeException != null) { + StoreDelegateWriteResult.Error.Exception(writeException) + } else { + memCache?.put(key, value) + StoreDelegateWriteResult.Success + } } catch (error: Throwable) { StoreDelegateWriteResult.Error.Exception(error) } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/SourceOfTruthWithBarrier.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/SourceOfTruthWithBarrier.kt index a0a0953b..d791757a 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/SourceOfTruthWithBarrier.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/SourceOfTruthWithBarrier.kt @@ -144,11 +144,18 @@ internal class SourceOfTruthWithBarrier( + key = key, + value = Note(key, "content"), + created = 3333L, + onCompletions = null, + ) + + // When + val response = mutableStore.write(request) + + // Then + val errorResponse = assertIs(response) + val writeException = assertIs(errorResponse.error) + val cause = assertIs(writeException.cause) + assertEquals(errorMessage, cause.message) + } + + @Test + fun write_givenSourceOfTruthFailure_whenCalled_thenNetworkSyncNotAttempted() = + runTest { + // Given + val key = "key" + testUpdater.postCallCount = 0 + testSourceOfTruth.throwOnWrite(key) { IllegalStateException("SOT failure") } + + val request = + StoreWriteRequest.of( + key = key, + value = Note(key, "content"), + created = 4444L, + onCompletions = null, + ) + + // When + val response = mutableStore.write(request) + + // Then + assertIs(response) + assertEquals(0, testUpdater.postCallCount, "Network updater should not be called when SOT write fails") + } + + @Test + fun write_givenSourceOfTruthFailure_whenCalled_thenMemCacheNotUpdated() = + runTest { + // Given + val key = "key" + testSourceOfTruth.throwOnWrite(key) { IllegalStateException("SOT failure") } + + val request = + StoreWriteRequest.of( + key = key, + value = Note(key, "content"), + created = 6666L, + onCompletions = null, + ) + + // When + val response = mutableStore.write(request) + + // Then + assertIs(response) + assertNull(delegateStore.latestOrNull(key), "Value should not be in cache after SOT write failure") + } + + @Test + fun write_givenNoSourceOfTruth_whenCalled_thenSucceeds() = + runTest { + // Given + val storeWithoutSot = + testStore( + fetcher = testFetcher, + sourceOfTruth = null, + converter = testConverter, + validator = testValidator, + memoryCache = testCache, + ) + val mutableStoreWithoutSot = + RealMutableStore( + delegate = storeWithoutSot, + updater = testUpdater, + bookkeeper = testBookkeeper, + logger = testLogger, + ) + + val request = + StoreWriteRequest.of( + key = "noSotKey", + value = Note("id", "content"), + created = 5555L, + onCompletions = null, + ) + + // When + val response = mutableStoreWithoutSot.write(request) + + // Then + assertIs(response) + } + @Test fun clearAll_givenSomeKeys_whenCalled_thenDelegateIsCleared() = runTest { diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/mutablestore/util/TestStore.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/mutablestore/util/TestStore.kt index 1a2fb1f5..62504297 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/mutablestore/util/TestStore.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/mutablestore/util/TestStore.kt @@ -14,7 +14,7 @@ internal fun testStore( dispatcher: CoroutineDispatcher = Dispatchers.Default, scope: CoroutineScope = CoroutineScope(dispatcher), fetcher: Fetcher = TestFetcher(), - sourceOfTruth: SourceOfTruth = TestSourceOfTruth(), + sourceOfTruth: SourceOfTruth? = TestSourceOfTruth(), converter: Converter = TestConverter(), validator: Validator = TestValidator(), memoryCache: Cache = TestCache(), diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/mutablestore/util/TestUpdater.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/mutablestore/util/TestUpdater.kt index 802faa34..f83dc893 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/mutablestore/util/TestUpdater.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/mutablestore/util/TestUpdater.kt @@ -8,11 +8,13 @@ class TestUpdater : Updater