diff --git a/.codecov.yml b/.codecov.yml index bd4987b68..817c42208 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,5 +1,5 @@ coverage: - range: 50..80 + range: 70..80 round: down precision: 2 diff --git a/.github/workflows/.ci_test_and_publish.yml b/.github/workflows/.ci_test_and_publish.yml index 633d0c9ef..04cff63de 100644 --- a/.github/workflows/.ci_test_and_publish.yml +++ b/.github/workflows/.ci_test_and_publish.yml @@ -20,7 +20,7 @@ jobs: uses: actions/setup-java@v2 with: distribution: zulu - java-version: 11 + java-version: 17 - name: Upload Artifacts run: ./gradlew publishAllPublicationsToMavenCentralRepository --no-daemon --no-parallel @@ -56,7 +56,7 @@ jobs: uses: actions/setup-java@v2 with: distribution: zulu - java-version: 11 + java-version: 17 - name: Run tests run: ./gradlew check --rerun-tasks --stacktrace - name: Upload code coverage diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 842d2792e..1d21c541d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -6,17 +6,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + - name: Set up our JDK environment + uses: actions/setup-java@v2 + with: + distribution: zulu + java-version: 17 - name: Setup Gradle uses: gradle/gradle-build-action@v2 - name: Run check with Gradle Wrapper - run: ./gradlew check - - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - uses: codecov/codecov-action@v3 - with: - files: ./kover/coverage.xml - name: codecov-umbrella - verbose: true \ No newline at end of file + run: ./gradlew check \ No newline at end of file diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 000000000..f9c6c3f62 --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,12 @@ +name: Codecov +on: [ push, pull_request ] +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: codecov/codecov-action@v3 + with: + files: ./kover/coverage.xml + name: codecov-umbrella + verbose: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index b54ac9caf..837bfac3f 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,6 @@ captures/ # Keystore files *.jks -store/kover +*/kover *.podspec yarn.lock \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 13a6ceacc..5ddb19c08 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,9 @@ +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin +import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask + plugins { - id("org.jlleitschuh.gradle.ktlint") version "11.0.0" + id("org.jlleitschuh.gradle.ktlint") version libs.versions.ktlintGradle.get() id("com.diffplug.spotless") version "6.4.1" } @@ -49,15 +53,26 @@ subprojects { tasks { withType { kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" } } withType().configureEach { - sourceCompatibility = JavaVersion.VERSION_11.name - targetCompatibility = JavaVersion.VERSION_11.name + sourceCompatibility = JavaVersion.VERSION_17.name + targetCompatibility = JavaVersion.VERSION_17.name } } // Workaround for https://youtrack.jetbrains.com/issue/KT-62040 tasks.getByName("wrapper") + +// Workaround for https://youtrack.jetbrains.com/issue/KT-63014 +plugins.withType { + extensions.configure(NodeJsRootExtension::class) { + nodeVersion = "21.0.0-v8-canary20231019bd785be450" + nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary" + } + tasks.withType { + args.add("--ignore-engines") + } +} diff --git a/cache/build.gradle.kts b/cache/build.gradle.kts index 7ebfb5b9f..3f96708a6 100644 --- a/cache/build.gradle.kts +++ b/cache/build.gradle.kts @@ -3,8 +3,6 @@ import com.vanniktech.maven.publish.SonatypeHost.S01 import org.jetbrains.dokka.gradle.DokkaTask import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension -import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask plugins { kotlin("multiplatform") @@ -70,7 +68,7 @@ kotlin { } } - jvmToolchain(11) + jvmToolchain(17) } android { @@ -92,8 +90,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } @@ -101,7 +99,7 @@ tasks.withType().configureEach { dokkaSourceSets.configureEach { reportUndocumented.set(false) skipDeprecated.set(true) - jdkVersion.set(8) + jdkVersion.set(11) } } @@ -127,12 +125,3 @@ koverMerged { onCheck.set(true) } } - -// See https://youtrack.jetbrains.com/issue/KT-63014 -rootProject.the().apply { - nodeVersion = "21.0.0-v8-canary20231024d0ddc81258" - nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary" -} -tasks.withType().configureEach { - args.add("--ignore-engines") -} diff --git a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Cache.kt b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Cache.kt index b6ef9c86e..a7d9156ac 100644 --- a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Cache.kt +++ b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Cache.kt @@ -14,7 +14,10 @@ interface Cache { * @throws UncheckedExecutionException If an unchecked exception was thrown while loading the value. * @throws ExecutionError If an error was thrown while loading the value. */ - fun getOrPut(key: Key, valueProducer: () -> Value): Value + fun getOrPut( + key: Key, + valueProducer: () -> Value, + ): Value /** * @return Map of the [Value] associated with each [Key] in [keys]. Returned map only contains entries already present in the cache. @@ -26,7 +29,10 @@ interface Cache { * If the cache previously contained a value associated with [key], the old value is replaced by [value]. * Prefer [getOrPut] when using the conventional "If cached, then return. Otherwise create, cache, and then return" pattern. */ - fun put(key: Key, value: Value) + fun put( + key: Key, + value: Value, + ) /** * Copies all of the mappings from the specified map to the cache. The effect of this call is diff --git a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/CacheBuilder.kt b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/CacheBuilder.kt index a40afe31a..9a0782da5 100644 --- a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/CacheBuilder.kt +++ b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/CacheBuilder.kt @@ -19,43 +19,52 @@ class CacheBuilder { internal var ticker: Ticker? = null private set - fun concurrencyLevel(producer: () -> Int): CacheBuilder = apply { - concurrencyLevel = producer.invoke() - } - - fun maximumSize(maximumSize: Long): CacheBuilder = apply { - if (maximumSize < 0) { - throw IllegalArgumentException("Maximum size must be non-negative.") + fun concurrencyLevel(producer: () -> Int): CacheBuilder = + apply { + concurrencyLevel = producer.invoke() } - this.maximumSize = maximumSize - } - fun expireAfterAccess(duration: Duration): CacheBuilder = apply { - if (duration.isNegative()) { - throw IllegalArgumentException("Duration must be non-negative.") + fun maximumSize(maximumSize: Long): CacheBuilder = + apply { + if (maximumSize < 0) { + throw IllegalArgumentException("Maximum size must be non-negative.") + } + this.maximumSize = maximumSize } - expireAfterAccess = duration - } - fun expireAfterWrite(duration: Duration): CacheBuilder = apply { - if (duration.isNegative()) { - throw IllegalArgumentException("Duration must be non-negative.") + fun expireAfterAccess(duration: Duration): CacheBuilder = + apply { + if (duration.isNegative()) { + throw IllegalArgumentException("Duration must be non-negative.") + } + expireAfterAccess = duration } - expireAfterWrite = duration - } - fun ticker(ticker: Ticker): CacheBuilder = apply { - this.ticker = ticker - } + fun expireAfterWrite(duration: Duration): CacheBuilder = + apply { + if (duration.isNegative()) { + throw IllegalArgumentException("Duration must be non-negative.") + } + expireAfterWrite = duration + } - fun weigher(maximumWeight: Long, weigher: Weigher): CacheBuilder = apply { - if (maximumWeight < 0) { - throw IllegalArgumentException("Maximum weight must be non-negative.") + fun ticker(ticker: Ticker): CacheBuilder = + apply { + this.ticker = ticker } - this.maximumWeight = maximumWeight - this.weigher = weigher - } + fun weigher( + maximumWeight: Long, + weigher: Weigher, + ): CacheBuilder = + apply { + if (maximumWeight < 0) { + throw IllegalArgumentException("Maximum weight must be non-negative.") + } + + this.maximumWeight = maximumWeight + this.weigher = weigher + } fun build(): Cache { if (maximumSize != -1L && weigher != null) { diff --git a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/LocalCache.kt b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/LocalCache.kt index f0ec6ff0d..295fd7462 100644 --- a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/LocalCache.kt +++ b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/LocalCache.kt @@ -28,7 +28,6 @@ import kotlin.math.min import kotlin.time.Duration internal class LocalCache(builder: CacheBuilder) { - /** * Mask value for indexing into segments. The upper bits of a key's hash code are used to choose * the segment. @@ -114,12 +113,16 @@ internal class LocalCache(builder: CacheBuilder) { segment: Segment?, entry: ReferenceEntry?, value: V, - weight: Int + weight: Int, ): ValueReference { - return if (weight == 1) StrongValueReference(value) else WeightedStrongValueReference( - value, - weight - ) + return if (weight == 1) { + StrongValueReference(value) + } else { + WeightedStrongValueReference( + value, + weight, + ) + } } } @@ -130,7 +133,7 @@ internal class LocalCache(builder: CacheBuilder) { segment: Segment?, entry: ReferenceEntry?, value: V, - weight: Int + weight: Int, ): ValueReference } @@ -143,19 +146,18 @@ internal class LocalCache(builder: CacheBuilder) { segment: Segment?, key: K, hash: Int, - next: ReferenceEntry? + next: ReferenceEntry?, ): ReferenceEntry { return StrongEntry(key, hash, next) } } object StrongAccess : EntryFactory() { - override fun newEntry( segment: Segment?, key: K, hash: Int, - next: ReferenceEntry? + next: ReferenceEntry?, ): ReferenceEntry { return StrongAccessEntry(key, hash, next) } @@ -163,7 +165,7 @@ internal class LocalCache(builder: CacheBuilder) { override fun copyEntry( segment: Segment?, original: ReferenceEntry, - newNext: ReferenceEntry? + newNext: ReferenceEntry?, ): ReferenceEntry { val newEntry = super.copyEntry(segment, original, newNext) copyAccessEntry(original, newEntry) @@ -172,12 +174,11 @@ internal class LocalCache(builder: CacheBuilder) { } object StrongWrite : EntryFactory() { - override fun newEntry( segment: Segment?, key: K, hash: Int, - next: ReferenceEntry? + next: ReferenceEntry?, ): ReferenceEntry { return StrongWriteEntry(key, hash, next) } @@ -185,7 +186,7 @@ internal class LocalCache(builder: CacheBuilder) { override fun copyEntry( segment: Segment?, original: ReferenceEntry, - newNext: ReferenceEntry? + newNext: ReferenceEntry?, ): ReferenceEntry { val newEntry = super.copyEntry(segment, original, newNext) copyWriteEntry(original, newEntry) @@ -194,12 +195,11 @@ internal class LocalCache(builder: CacheBuilder) { } object StrongAccessWrite : EntryFactory() { - override fun newEntry( segment: Segment?, key: K, hash: Int, - next: ReferenceEntry? + next: ReferenceEntry?, ): ReferenceEntry { return StrongAccessWriteEntry(key, hash, next) } @@ -207,7 +207,7 @@ internal class LocalCache(builder: CacheBuilder) { override fun copyEntry( segment: Segment?, original: ReferenceEntry, - newNext: ReferenceEntry? + newNext: ReferenceEntry?, ): ReferenceEntry { val newEntry = super.copyEntry(segment, original, newNext) copyAccessEntry(original, newEntry) @@ -228,28 +228,31 @@ internal class LocalCache(builder: CacheBuilder) { segment: Segment?, key: K, hash: Int, - next: ReferenceEntry? + next: ReferenceEntry?, ): ReferenceEntry /** * Copies an entry, assigning it a new `next` entry. * + * Guarded by [Segment] + * * @param original the entry to copy * @param newNext entry in the same bucket */ - // Guarded By Segment.this open fun copyEntry( segment: Segment?, original: ReferenceEntry, - newNext: ReferenceEntry? + newNext: ReferenceEntry?, ): ReferenceEntry { return newEntry(segment, original.key, original.hash, newNext) } - // Guarded By Segment.this + /** + * Guarded by [Segment] + */ fun copyAccessEntry( original: ReferenceEntry, - newEntry: ReferenceEntry + newEntry: ReferenceEntry, ) { // TODO(fry): when we link values instead of entries this method can go // away, as can connectAccessOrder, nullifyAccessOrder. @@ -259,10 +262,12 @@ internal class LocalCache(builder: CacheBuilder) { nullifyAccessOrder(original) } - // Guarded By Segment.this + /** + * Guarded by [Segment] + */ fun copyWriteEntry( original: ReferenceEntry, - newEntry: ReferenceEntry + newEntry: ReferenceEntry, ) { // TODO(fry): when we link values instead of entries this method can go // away, as can connectWriteOrder, nullifyWriteOrder. @@ -283,7 +288,11 @@ internal class LocalCache(builder: CacheBuilder) { * Look-up table for factories. */ private val factories = arrayOf(Strong, StrongAccess, StrongWrite, StrongAccessWrite) - fun getFactory(usesAccessQueue: Boolean, usesWriteQueue: Boolean): EntryFactory { + + fun getFactory( + usesAccessQueue: Boolean, + usesWriteQueue: Boolean, + ): EntryFactory { val flags = ((if (usesAccessQueue) ACCESS_MASK else 0) or if (usesWriteQueue) WRITE_MASK else 0) return factories[flags] } @@ -313,15 +322,15 @@ internal class LocalCache(builder: CacheBuilder) { /** * Creates a copy of this reference for the given entry. * - * - * * `value` may be null only for a loading reference. */ - - fun copyFor(value: V?, entry: ReferenceEntry?): ValueReference + fun copyFor( + value: V?, + entry: ReferenceEntry?, + ): ValueReference /** - * Notifify pending loads that a new value was set. This is only relevant to loading + * Notify pending loads that a new value was set. This is only relevant to loading * value references. */ fun notifyNewValue(newValue: V) @@ -339,29 +348,27 @@ internal class LocalCache(builder: CacheBuilder) { /** * An entry in a reference map. * - * * Entries in the map can be in the following states: * - * * Valid: * - Live: valid key/value are set * - Loading: loading is pending * - * * Invalid: * - Expired: time expired (key/value may still be set) * - Collected: key/value was partially collected, but not yet cleaned up * - Unset: marked as unset, awaiting cleanup or reuse */ private interface ReferenceEntry { - /** - * Returns the value reference from this entry. - */ - /** - * Sets the value reference for this entry. - */ var valueReference: ValueReference? + /** + * Returns the value reference from this entry. + */ get() = throw UnsupportedOperationException() + + /** + * Sets the value reference for this entry. + */ set(_) = throw UnsupportedOperationException() /** @@ -381,69 +388,81 @@ internal class LocalCache(builder: CacheBuilder) { */ val key: K get() = throw UnsupportedOperationException() + /* * Used by entries that use access order. Access entries are maintained in a doubly-linked list. * New entries are added at the tail of the list at write time; stale entries are expired from * the head of the list. */ - /** - * Returns the time that this entry was last accessed, in ns. - */ - /** - * Sets the entry access time in ns. - */ var accessTime: Long + /** + * Returns the time that this entry was last accessed, in ns. + */ get() = throw UnsupportedOperationException() + + /** + * Sets the entry access time in ns. + */ set(_) = throw UnsupportedOperationException() - /** - * Returns the next entry in the access queue. - */ - /** - * Sets the next entry in the access queue. - */ + var nextInAccessQueue: ReferenceEntry + /** + * Returns the next entry in the access queue. + */ get() = throw UnsupportedOperationException() + + /** + * Sets the next entry in the access queue. + */ set(_) = throw UnsupportedOperationException() - /** - * Returns the previous entry in the access queue. - */ - /** - * Sets the previous entry in the access queue. - */ + var previousInAccessQueue: ReferenceEntry + /** + * Returns the previous entry in the access queue. + */ get() = throw UnsupportedOperationException() + + /** + * Sets the previous entry in the access queue. + */ set(_) = throw UnsupportedOperationException() + /* * Implemented by entries that use write order. Write entries are maintained in a * doubly-linked list. New entries are added at the tail of the list at write time and stale * entries are expired from the head of the list. */ - /** - * Returns the time that this entry was last written, in ns. - */ - /** - * Sets the entry write time in ns. - */ var writeTime: Long + /** + * Returns the time that this entry was last written, in ns. + */ get() = throw UnsupportedOperationException() + + /** + * Sets the entry write time in ns. + */ set(_) = throw UnsupportedOperationException() - /** - * Returns the next entry in the write queue. - */ - /** - * Sets the next entry in the write queue. - */ + var nextInWriteQueue: ReferenceEntry + /** + * Returns the next entry in the write queue. + */ get() = throw UnsupportedOperationException() + + /** + * Sets the next entry in the write queue. + */ set(_) = throw UnsupportedOperationException() - /** - * Returns the previous entry in the write queue. - */ - /** - * Sets the previous entry in the write queue. - */ + var previousInWriteQueue: ReferenceEntry + /** + * Returns the previous entry in the write queue. + */ get() = throw UnsupportedOperationException() + + /** + * Sets the previous entry in the write queue. + */ set(_) = throw UnsupportedOperationException() } @@ -493,15 +512,17 @@ internal class LocalCache(builder: CacheBuilder) { * strong entries store the key reference directly while soft and weak entries delegate to their * respective superclasses. */ + /** * Used for strongly-referenced keys. + * + * The code below is exactly the same for each entry type. */ private open class StrongEntry( - override val key: K, // The code below is exactly the same for each entry type. + override val key: K, override val hash: Int, - override val next: ReferenceEntry? + override val next: ReferenceEntry?, ) : ReferenceEntry { - private val _valueReference = atomic?>(unset()) override var valueReference: ValueReference? = _valueReference.value } @@ -509,7 +530,7 @@ internal class LocalCache(builder: CacheBuilder) { private class StrongAccessEntry( key: K, hash: Int, - next: ReferenceEntry? + next: ReferenceEntry?, ) : StrongEntry(key, hash, next) { // The code below is exactly the same for each access entry type. @@ -527,7 +548,7 @@ internal class LocalCache(builder: CacheBuilder) { private class StrongWriteEntry( key: K, hash: Int, - next: ReferenceEntry? + next: ReferenceEntry?, ) : StrongEntry(key, hash, next) { // The code below is exactly the same for each write entry type. @@ -544,7 +565,7 @@ internal class LocalCache(builder: CacheBuilder) { private class StrongAccessWriteEntry( key: K, hash: Int, - next: ReferenceEntry? + next: ReferenceEntry?, ) : StrongEntry(key, hash, next) { // The code below is exactly the same for each access entry type. @@ -574,10 +595,17 @@ internal class LocalCache(builder: CacheBuilder) { private open class StrongValueReference(private val referent: V) : ValueReference { override fun get(): V = referent + override val weight: Int = 1 override val entry: ReferenceEntry? = null - override fun copyFor(value: V?, entry: ReferenceEntry?): ValueReference = this + + override fun copyFor( + value: V?, + entry: ReferenceEntry?, + ): ValueReference = this + override val isActive: Boolean = true + override fun notifyNewValue(newValue: V) {} } @@ -586,7 +614,7 @@ internal class LocalCache(builder: CacheBuilder) { */ private class WeightedStrongValueReference( referent: V, - override val weight: Int + override val weight: Int, ) : StrongValueReference(referent) @@ -594,7 +622,11 @@ internal class LocalCache(builder: CacheBuilder) { * This method is a convenience for testing. Code should call [Segment.newEntry] directly. */ - private fun newEntry(key: K, hash: Int, next: ReferenceEntry?): ReferenceEntry { + private fun newEntry( + key: K, + hash: Int, + next: ReferenceEntry?, + ): ReferenceEntry { val segment = segmentFor(hash) segment.reentrantLock.lock() return try { @@ -606,11 +638,12 @@ internal class LocalCache(builder: CacheBuilder) { /** * This method is a convenience for testing. Code should call [Segment.copyEntry] directly. + * + * Guarded by [Segment] */ - // Guarded By Segment.this private fun copyEntry( original: ReferenceEntry, - newNext: ReferenceEntry? + newNext: ReferenceEntry?, ): ReferenceEntry? { val hash = original.hash return segmentFor(hash).copyEntry(original, newNext) @@ -618,12 +651,13 @@ internal class LocalCache(builder: CacheBuilder) { /** * This method is a convenience for testing. Code should call [Segment.setValue] instead. + * + * Guarded by [Segment] */ - // Guarded By Segment.this private fun newValueReference( entry: ReferenceEntry, value: V, - weight: Int + weight: Int, ): ValueReference { val hash = entry.hash return valueStrength.referenceValue(segmentFor(hash), entry, value, weight) @@ -637,16 +671,23 @@ internal class LocalCache(builder: CacheBuilder) { * @param hash the hash code for the key * @return the segment */ - private fun segmentFor(hash: Int): Segment = // TODO(fry): Lazily create segments? + private fun segmentFor(hash: Int): Segment = + // TODO(fry): Lazily create segments? segments[hash ushr segmentShift and segmentMask] as Segment - private fun createSegment(initialCapacity: Int, maxSegmentWeight: Long): Segment = - Segment(this, initialCapacity, maxSegmentWeight) + private fun createSegment( + initialCapacity: Int, + maxSegmentWeight: Long, + ): Segment = Segment(this, initialCapacity, maxSegmentWeight) // expiration + /** * Returns true if the entry has expired. */ - private fun isExpired(entry: ReferenceEntry, now: Long): Boolean = + private fun isExpired( + entry: ReferenceEntry, + now: Long, + ): Boolean = if (expiresAfterAccess && now - entry.accessTime >= expireAfterAccessNanos) { true } else { @@ -657,8 +698,13 @@ internal class LocalCache(builder: CacheBuilder) { private class SegmentTable(val size: Int) { private val table: AtomicArray?> = atomicArrayOfNulls(size) + operator fun get(idx: Int) = table[idx].value - operator fun set(idx: Int, value: ReferenceEntry?) { + + operator fun set( + idx: Int, + value: ReferenceEntry?, + ) { table[idx].value = value } } @@ -669,13 +715,11 @@ internal class LocalCache(builder: CacheBuilder) { private class Segment( private val map: LocalCache, initialCapacity: Int, - private val maxSegmentWeight: Long + private val maxSegmentWeight: Long, ) { /* * TODO(fry): Consider copying variables (like evictsBySize) from outer class into this class. * It will require more memory but will reduce indirection. - */ - /* * Segments maintain a table of entry lists that are ALWAYS kept in a consistent state, so can * be read without locking. Next fields of nodes are immutable (final). All list additions are * performed at the front of each bin. This makes it easy to check changes, and also fast to @@ -759,8 +803,11 @@ internal class LocalCache(builder: CacheBuilder) { */ private val accessQueue: MutableQueue> - fun newEntry(key: K, hash: Int, next: ReferenceEntry?): ReferenceEntry = - map.entryFactory.newEntry(this, key, hash, next) + fun newEntry( + key: K, + hash: Int, + next: ReferenceEntry?, + ): ReferenceEntry = map.entryFactory.newEntry(this, key, hash, next) /** * Copies `original` into a new entry chained to `newNext`. Returns the new entry, @@ -768,7 +815,7 @@ internal class LocalCache(builder: CacheBuilder) { */ fun copyEntry( original: ReferenceEntry, - newNext: ReferenceEntry? + newNext: ReferenceEntry?, ): ReferenceEntry? { val valueReference = original.valueReference val value = valueReference!!.get() @@ -784,7 +831,12 @@ internal class LocalCache(builder: CacheBuilder) { /** * Sets a new value of an entry. Adds newly created entries at the end of the access queue. */ - fun setValue(entry: ReferenceEntry, key: K, value: V, now: Long) { + fun setValue( + entry: ReferenceEntry, + key: K, + value: V, + now: Long, + ) { val previous = entry.valueReference val weight = map.weigher(key, value) if (weight < 0) throw IllegalStateException("Weights must be non-negative") @@ -794,16 +846,18 @@ internal class LocalCache(builder: CacheBuilder) { } // recency queue, shared by expiration and eviction + /** * Records the relative order in which this read was performed by adding `entry` to the * recency queue. At write-time, or when the queue is full past the threshold, the queue will * be drained and the entries therein processed. * - * - * * Note: locked reads should use [.recordLockedRead]. */ - private fun recordRead(entry: ReferenceEntry, now: Long) { + private fun recordRead( + entry: ReferenceEntry, + now: Long, + ) { if (map.recordsAccess) { entry.accessTime = now } @@ -814,12 +868,13 @@ internal class LocalCache(builder: CacheBuilder) { * Updates the eviction metadata that `entry` was just read. This currently amounts to * adding `entry` to relevant eviction lists. * - * - * * Note: this method should only be called under lock, as it directly manipulates the * eviction queues. Unlocked reads should use [.recordRead]. */ - private fun recordLockedRead(entry: ReferenceEntry, now: Long) { + private fun recordLockedRead( + entry: ReferenceEntry, + now: Long, + ) { if (map.recordsAccess) { entry.accessTime = now } @@ -830,7 +885,11 @@ internal class LocalCache(builder: CacheBuilder) { * Updates eviction metadata that `entry` was just written. This currently amounts to * adding `entry` to relevant eviction lists. */ - private fun recordWrite(entry: ReferenceEntry, weight: Int, now: Long) { + private fun recordWrite( + entry: ReferenceEntry, + weight: Int, + now: Long, + ) { // we are already under lock, so drain the recency queue immediately drainRecencyQueue() totalWeight += weight.toLong() @@ -863,6 +922,7 @@ internal class LocalCache(builder: CacheBuilder) { } } // expiration + /** * Cleanup expired entries when the lock is available. */ @@ -895,7 +955,10 @@ internal class LocalCache(builder: CacheBuilder) { } // eviction - private fun enqueueNotification(entry: ReferenceEntry, cause: RemovalCause?) { + private fun enqueueNotification( + entry: ReferenceEntry, + cause: RemovalCause?, + ) { enqueueNotification(entry.key, entry.hash, entry.valueReference, cause) } @@ -903,7 +966,7 @@ internal class LocalCache(builder: CacheBuilder) { key: K?, hash: Int, valueReference: ValueReference?, - cause: RemovalCause? + cause: RemovalCause?, ) { valueReference?.weight?.toLong()?.apply { totalWeight -= this @@ -960,7 +1023,10 @@ internal class LocalCache(builder: CacheBuilder) { } // Specialized implementations of map methods - private fun getEntry(key: K, hash: Int): ReferenceEntry? { + private fun getEntry( + key: K, + hash: Int, + ): ReferenceEntry? { var e = getFirst(hash) while (e != null) { if (e.hash != hash) { @@ -976,7 +1042,11 @@ internal class LocalCache(builder: CacheBuilder) { return null } - private fun getLiveEntry(key: K, hash: Int, now: Long): ReferenceEntry? { + private fun getLiveEntry( + key: K, + hash: Int, + now: Long, + ): ReferenceEntry? { val e = getEntry(key, hash) if (e == null) { return null @@ -992,7 +1062,10 @@ internal class LocalCache(builder: CacheBuilder) { * loading, or expired. */ - fun get(key: K, hash: Int): V? { + fun get( + key: K, + hash: Int, + ): V? { return try { if (count.value != 0) { // read-volatile val now = map.ticker() @@ -1009,7 +1082,11 @@ internal class LocalCache(builder: CacheBuilder) { } } - fun getOrPut(key: K, hash: Int, defaultValue: () -> V): V { + fun getOrPut( + key: K, + hash: Int, + defaultValue: () -> V, + ): V { reentrantLock.lock() return try { if (count.value != 0) { // read-volatile @@ -1030,7 +1107,12 @@ internal class LocalCache(builder: CacheBuilder) { } } - fun put(key: K, hash: Int, value: V, onlyIfAbsent: Boolean): V? { + fun put( + key: K, + hash: Int, + value: V, + onlyIfAbsent: Boolean, + ): V? { reentrantLock.lock() return try { val now = map.ticker() @@ -1053,19 +1135,20 @@ internal class LocalCache(builder: CacheBuilder) { return when { entryValue == null -> { ++modCount - val newCount = if (valueReference.isActive) { - enqueueNotification( - key, - hash, - valueReference, - RemovalCause.COLLECTED - ) - setValue(e, key, value, now) - count.value // count remains unchanged - } else { - setValue(e, key, value, now) - count.value + 1 - } + val newCount = + if (valueReference.isActive) { + enqueueNotification( + key, + hash, + valueReference, + RemovalCause.COLLECTED, + ) + setValue(e, key, value, now) + count.value // count remains unchanged + } else { + setValue(e, key, value, now) + count.value + 1 + } count.value = newCount // write-volatile evictEntries(e) null @@ -1086,7 +1169,7 @@ internal class LocalCache(builder: CacheBuilder) { key, hash, valueReference, - RemovalCause.REPLACED + RemovalCause.REPLACED, ) setValue(e, key, value, now) evictEntries(e) @@ -1111,7 +1194,10 @@ internal class LocalCache(builder: CacheBuilder) { } } - fun remove(key: K, hash: Int): V? { + fun remove( + key: K, + hash: Int, + ): V? { reentrantLock.lock() return try { val now = map.ticker() @@ -1125,24 +1211,31 @@ internal class LocalCache(builder: CacheBuilder) { if (e.hash == hash && key == entryKey) { val valueReference = e.valueReference val entryValue = valueReference!!.get() - val cause: RemovalCause = when { - entryValue != null -> { - RemovalCause.EXPLICIT - } + val cause: RemovalCause = + when { + entryValue != null -> { + RemovalCause.EXPLICIT + } - valueReference.isActive -> { - RemovalCause.COLLECTED - } + valueReference.isActive -> { + RemovalCause.COLLECTED + } - else -> { - // currently loading - return null + else -> { + // currently loading + return null + } } - } ++modCount - val newFirst = removeValueFromChain( - first!!, e, entryKey, hash, valueReference, cause - ) + val newFirst = + removeValueFromChain( + first!!, + e, + entryKey, + hash, + valueReference, + cause, + ) val newCount = count.value - 1 table[index] = newFirst count.value = newCount // write-volatile @@ -1265,7 +1358,7 @@ internal class LocalCache(builder: CacheBuilder) { key: K, hash: Int, valueReference: ValueReference, - cause: RemovalCause? + cause: RemovalCause?, ): ReferenceEntry? { enqueueNotification(key, hash, valueReference, cause) writeQueue.remove(entry) @@ -1275,7 +1368,7 @@ internal class LocalCache(builder: CacheBuilder) { private fun removeEntryFromChain( first: ReferenceEntry, - entry: ReferenceEntry + entry: ReferenceEntry, ): ReferenceEntry? { var newCount = count.value var newFirst = entry.next @@ -1303,7 +1396,7 @@ internal class LocalCache(builder: CacheBuilder) { private fun removeEntry( entry: ReferenceEntry, hash: Int, - cause: RemovalCause? + cause: RemovalCause?, ): Boolean { val table = table.value val index = hash and table.size - 1 @@ -1313,9 +1406,15 @@ internal class LocalCache(builder: CacheBuilder) { while (e != null) { if (e === entry) { ++modCount - val newFirst = removeValueFromChain( - first!!, e, e.key, hash, e.valueReference!!, cause - ) + val newFirst = + removeValueFromChain( + first!!, + e, + e.key, + hash, + e.valueReference!!, + cause, + ) val newCount = count.value - 1 table[index] = newFirst count.value = newCount // write-volatile @@ -1374,9 +1473,9 @@ internal class LocalCache(builder: CacheBuilder) { private fun runUnlockedCleanup() { // locked cleanup may generate notifications we can send unlocked - /*if (!isHeldByCurrentThread) { - map.processPendingNotifications() - }*/ + // if (!isHeldByCurrentThread) { + // map.processPendingNotifications() + // } } init { @@ -1395,15 +1494,21 @@ internal class LocalCache(builder: CacheBuilder) { private interface Queue { fun poll(): T? + fun add(value: T) } private interface MutableQueue : Queue, Iterable { fun peek(): E? + fun isEmpty(): Boolean + val size: Int + fun clear() + fun remove(element: E): Boolean + fun contains(element: E): Boolean } @@ -1454,13 +1559,14 @@ internal class LocalCache(builder: CacheBuilder) { */ private class WriteQueue : MutableQueue> { - private val head: ReferenceEntry = object : ReferenceEntry { - override var writeTime: Long - get() = Long.MAX_VALUE - set(_) {} - override var nextInWriteQueue: ReferenceEntry = this - override var previousInWriteQueue: ReferenceEntry = this - } + private val head: ReferenceEntry = + object : ReferenceEntry { + override var writeTime: Long + get() = Long.MAX_VALUE + set(_) {} + override var nextInWriteQueue: ReferenceEntry = this + override var previousInWriteQueue: ReferenceEntry = this + } // implements Queue override fun add(value: ReferenceEntry) { @@ -1494,11 +1600,9 @@ internal class LocalCache(builder: CacheBuilder) { return next !== NullEntry } - override fun contains(element: ReferenceEntry): Boolean = - element.nextInWriteQueue !== NullEntry + override fun contains(element: ReferenceEntry): Boolean = element.nextInWriteQueue !== NullEntry - override fun isEmpty(): Boolean = - head.nextInWriteQueue === head + override fun isEmpty(): Boolean = head.nextInWriteQueue === head override val size: Int get() { @@ -1522,14 +1626,15 @@ internal class LocalCache(builder: CacheBuilder) { head.previousInWriteQueue = head } - override fun iterator(): Iterator> = iterator { - var value = peek() - while (value != null) { - yield(value) - val next = value.nextInWriteQueue - value = if (next === head) null else next + override fun iterator(): Iterator> = + iterator { + var value = peek() + while (value != null) { + yield(value) + val next = value.nextInWriteQueue + value = if (next === head) null else next + } } - } } /** @@ -1548,13 +1653,14 @@ internal class LocalCache(builder: CacheBuilder) { * for the current model. */ private class AccessQueue : MutableQueue> { - private val head: ReferenceEntry = object : ReferenceEntry { - override var accessTime: Long - get() = Long.MAX_VALUE - set(_) {} - override var nextInAccessQueue: ReferenceEntry = this - override var previousInAccessQueue: ReferenceEntry = this - } + private val head: ReferenceEntry = + object : ReferenceEntry { + override var accessTime: Long + get() = Long.MAX_VALUE + set(_) {} + override var nextInAccessQueue: ReferenceEntry = this + override var previousInAccessQueue: ReferenceEntry = this + } // implements Queue override fun add(value: ReferenceEntry) { @@ -1588,11 +1694,9 @@ internal class LocalCache(builder: CacheBuilder) { return next !== NullEntry } - override fun contains(element: ReferenceEntry): Boolean = - element.nextInAccessQueue !== NullEntry + override fun contains(element: ReferenceEntry): Boolean = element.nextInAccessQueue !== NullEntry - override fun isEmpty(): Boolean = - head.nextInAccessQueue === head + override fun isEmpty(): Boolean = head.nextInAccessQueue === head override val size: Int get() { @@ -1616,14 +1720,15 @@ internal class LocalCache(builder: CacheBuilder) { head.previousInAccessQueue = head } - override fun iterator(): Iterator> = iterator { - var value = peek() - while (value != null) { - yield(value) - val next = value.nextInAccessQueue - value = if (next === head) null else next + override fun iterator(): Iterator> = + iterator { + var value = peek() + while (value != null) { + yield(value) + val next = value.nextInAccessQueue + value = if (next === head) null else next + } } - } } // Cache support @@ -1639,12 +1744,18 @@ internal class LocalCache(builder: CacheBuilder) { return segmentFor(hash).get(key, hash) } - fun put(key: K, value: V): V? { + fun put( + key: K, + value: V, + ): V? { val hash = hash(key) return segmentFor(hash).put(key, hash, value, false) } - fun getOrPut(key: K, defaultValue: () -> V): V { + fun getOrPut( + key: K, + defaultValue: () -> V, + ): V { val hash = hash(key) return segmentFor(hash).getOrPut(key, hash, defaultValue) } @@ -1663,45 +1774,51 @@ internal class LocalCache(builder: CacheBuilder) { // Serialization Support internal class LocalManualCache private constructor(private val localCache: LocalCache) : Cache { - constructor(builder: CacheBuilder) : this(LocalCache(builder)) + constructor(builder: CacheBuilder) : this(LocalCache(builder)) - // Cache methods - override fun getIfPresent(key: K): V? { - return localCache.getIfPresent(key) - } + // Cache methods + override fun getIfPresent(key: K): V? { + return localCache.getIfPresent(key) + } - override fun put(key: K, value: V) { - localCache.put(key, value) - } + override fun put( + key: K, + value: V, + ) { + localCache.put(key, value) + } - override fun invalidate(key: K) { - localCache.remove(key) - } + override fun invalidate(key: K) { + localCache.remove(key) + } - override fun getOrPut(key: K, valueProducer: () -> V): V { - return localCache.getOrPut(key, valueProducer) - } + override fun getOrPut( + key: K, + valueProducer: () -> V, + ): V { + return localCache.getOrPut(key, valueProducer) + } - override fun getAllPresent(keys: List<*>): Map { - TODO("Not yet implemented") - } + override fun getAllPresent(keys: List<*>): Map { + TODO("Not yet implemented") + } - override fun invalidateAll(keys: List) { - TODO("Not yet implemented") - } + override fun invalidateAll(keys: List) { + TODO("Not yet implemented") + } - override fun putAll(map: Map) { - TODO("Not yet implemented") - } + override fun putAll(map: Map) { + TODO("Not yet implemented") + } - override fun invalidateAll() { - localCache.clear() - } + override fun invalidateAll() { + localCache.clear() + } - override fun size(): Long { - TODO("Not yet implemented") + override fun size(): Long { + TODO("Not yet implemented") + } } - } companion object { /* @@ -1729,7 +1846,6 @@ internal class LocalCache(builder: CacheBuilder) { * operates per-segment rather than globally for increased implementation simplicity. We expect * the cache hit rate to be similar to that of a global LRU algorithm. */ - // Constants private val OneWeigher: Weigher = { _, _ -> 1 } /** @@ -1749,8 +1865,6 @@ internal class LocalCache(builder: CacheBuilder) { * ordering information is updated. This is used to avoid lock contention by recording a memento * of reads and delaying a lock acquisition until the threshold is crossed or a mutation occurs. * - * - * * This must be a (2^n)-1 as it is used as a mask. */ const val DRAIN_THRESHOLD = 0x3F @@ -1758,28 +1872,29 @@ internal class LocalCache(builder: CacheBuilder) { /** * Placeholder. Indicates that the value hasn't been set yet. */ - private val UNSET: ValueReference = object : ValueReference { - override fun get(): Any? { - return null - } + private val UNSET: ValueReference = + object : ValueReference { + override fun get(): Any? { + return null + } - override val weight: Int - get() = 0 - override val entry: ReferenceEntry? - get() = null + override val weight: Int + get() = 0 + override val entry: ReferenceEntry? + get() = null - override fun copyFor( - value: Any?, - entry: ReferenceEntry? - ): ValueReference { - return this - } + override fun copyFor( + value: Any?, + entry: ReferenceEntry?, + ): ValueReference { + return this + } - override val isActive: Boolean - get() = false + override val isActive: Boolean + get() = false - override fun notifyNewValue(newValue: Any) {} - } + override fun notifyNewValue(newValue: Any) {} + } /** * Singleton placeholder that indicates a value is being loaded. @@ -1790,32 +1905,32 @@ internal class LocalCache(builder: CacheBuilder) { @Suppress("UNCHECKED_CAST") private fun nullEntry() = NullEntry as ReferenceEntry - private val DISCARDING_QUEUE: MutableQueue = object : MutableQueue { - override fun add(value: Any) {} + private val DISCARDING_QUEUE: MutableQueue = + object : MutableQueue { + override fun add(value: Any) {} - override fun peek(): Any? = null + override fun peek(): Any? = null - override fun poll(): Any? = null + override fun poll(): Any? = null - override fun iterator(): MutableIterator = HashSet().iterator() + override fun iterator(): MutableIterator = HashSet().iterator() - override val size: Int = 0 + override val size: Int = 0 - override fun isEmpty(): Boolean = true + override fun isEmpty(): Boolean = true - override fun clear() {} + override fun clear() {} - override fun remove(element: Any): Boolean = false + override fun remove(element: Any): Boolean = false - override fun contains(element: Any): Boolean = false - } + override fun contains(element: Any): Boolean = false + } /** * Queue that discards all elements. */ @Suppress("UNCHECKED_CAST") - private fun discardingQueue(): MutableQueue = - DISCARDING_QUEUE as MutableQueue + private fun discardingQueue(): MutableQueue = DISCARDING_QUEUE as MutableQueue /** * Applies a supplemental hash function to a given hash code, which defends against poor quality @@ -1842,7 +1957,7 @@ internal class LocalCache(builder: CacheBuilder) { // Guarded By Segment.this private fun connectAccessOrder( previous: ReferenceEntry, - next: ReferenceEntry + next: ReferenceEntry, ) { previous.nextInAccessQueue = next next.previousInAccessQueue = previous @@ -1858,7 +1973,7 @@ internal class LocalCache(builder: CacheBuilder) { // Guarded By Segment.this private fun connectWriteOrder( previous: ReferenceEntry, - next: ReferenceEntry + next: ReferenceEntry, ) { previous.nextInWriteQueue = next next.previousInWriteQueue = previous @@ -1876,11 +1991,12 @@ internal class LocalCache(builder: CacheBuilder) { * Creates a new, empty map with the specified strategy, initial capacity and concurrency level. */ init { - this.maxWeight = when { - builder.expireAfterAccess == Duration.ZERO || builder.expireAfterWrite == Duration.ZERO -> 0L - builder.weigher != null -> builder.maximumWeight - else -> builder.maximumSize - } + this.maxWeight = + when { + builder.expireAfterAccess == Duration.ZERO || builder.expireAfterWrite == Duration.ZERO -> 0L + builder.weigher != null -> builder.maximumWeight + else -> builder.maximumSize + } this.weigher = builder.weigher ?: OneWeigher as Weigher this.expireAfterAccessNanos = diff --git a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/RemovalCause.kt b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/RemovalCause.kt index c6689c924..2f9f209fa 100644 --- a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/RemovalCause.kt +++ b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/RemovalCause.kt @@ -11,5 +11,5 @@ internal enum class RemovalCause(val wasEvicted: Boolean) { REPLACED(false), COLLECTED(true), EXPIRED(true), - SIZE(true); + SIZE(true), } diff --git a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/StoreMultiCache.kt b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/StoreMultiCache.kt index 7003bc582..4ef39459d 100644 --- a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/StoreMultiCache.kt +++ b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/StoreMultiCache.kt @@ -2,6 +2,7 @@ package org.mobilenativefoundation.store.cache5 +import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.core5.KeyProvider import org.mobilenativefoundation.store.core5.StoreData import org.mobilenativefoundation.store.core5.StoreKey @@ -12,37 +13,43 @@ import org.mobilenativefoundation.store.core5.StoreKey * Depends on [StoreMultiCacheAccessor] for internal data management. * @see [Cache]. */ -class StoreMultiCache, Single : StoreData.Single, Collection : StoreData.Collection, Output : StoreData>( - private val keyProvider: KeyProvider, - singlesCache: Cache, Single> = CacheBuilder, Single>().build(), - collectionsCache: Cache, Collection> = CacheBuilder, Collection>().build(), -) : Cache { +@ExperimentalStoreApi +class StoreMultiCache, S : StoreData.Single, C : StoreData.Collection, O : StoreData>( + private val keyProvider: KeyProvider, + singlesCache: Cache, S> = CacheBuilder, S>().build(), + collectionsCache: Cache, C> = CacheBuilder, C>().build(), +) : Cache { + private val accessor = + StoreMultiCacheAccessor( + singlesCache = singlesCache, + collectionsCache = collectionsCache, + ) - private val accessor = StoreMultiCacheAccessor( - singlesCache = singlesCache, - collectionsCache = collectionsCache, - ) + private fun K.castSingle() = this as StoreKey.Single - private fun Key.castSingle() = this as StoreKey.Single - private fun Key.castCollection() = this as StoreKey.Collection + private fun K.castCollection() = this as StoreKey.Collection - private fun StoreKey.Collection.cast() = this as Key - private fun StoreKey.Single.cast() = this as Key + private fun StoreKey.Collection.cast() = this as K - override fun getIfPresent(key: Key): Output? { + private fun StoreKey.Single.cast() = this as K + + override fun getIfPresent(key: K): O? { return when (key) { - is StoreKey.Single<*> -> accessor.getSingle(key.castSingle()) as? Output - is StoreKey.Collection<*> -> accessor.getCollection(key.castCollection()) as? Output + is StoreKey.Single<*> -> accessor.getSingle(key.castSingle()) as? O + is StoreKey.Collection<*> -> accessor.getCollection(key.castCollection()) as? O else -> { throw UnsupportedOperationException(invalidKeyErrorMessage(key)) } } } - override fun getOrPut(key: Key, valueProducer: () -> Output): Output { + override fun getOrPut( + key: K, + valueProducer: () -> O, + ): O { return when (key) { is StoreKey.Single<*> -> { - val single = accessor.getSingle(key.castSingle()) as? Output + val single = accessor.getSingle(key.castSingle()) as? O if (single != null) { single } else { @@ -53,7 +60,7 @@ class StoreMultiCache, Single : StoreData.Single -> { - val collection = accessor.getCollection(key.castCollection()) as? Output + val collection = accessor.getCollection(key.castCollection()) as? O if (collection != null) { collection } else { @@ -69,18 +76,18 @@ class StoreMultiCache, Single : StoreData.Single): Map { - val map = mutableMapOf() + override fun getAllPresent(keys: List<*>): Map { + val map = mutableMapOf() keys.filterIsInstance>().forEach { key -> when (key) { is StoreKey.Collection -> { val collection = accessor.getCollection(key) - collection?.let { map[key.cast()] = it as Output } + collection?.let { map[key.cast()] = it as O } } is StoreKey.Single -> { val single = accessor.getSingle(key) - single?.let { map[key.cast()] = it as Output } + single?.let { map[key.cast()] = it as O } } } } @@ -88,48 +95,52 @@ class StoreMultiCache, Single : StoreData.Single) { + override fun invalidateAll(keys: List) { keys.forEach { key -> invalidate(key) } } - override fun invalidate(key: Key) { + override fun invalidate(key: K) { when (key) { is StoreKey.Single<*> -> accessor.invalidateSingle(key.castSingle()) is StoreKey.Collection<*> -> accessor.invalidateCollection(key.castCollection()) } } - override fun putAll(map: Map) { + override fun putAll(map: Map) { map.entries.forEach { (key, value) -> put(key, value) } } - override fun put(key: Key, value: Output) { + override fun put( + key: K, + value: O, + ) { when (key) { is StoreKey.Single<*> -> { - val single = value as Single + val single = value as S accessor.putSingle(key.castSingle(), single) val collectionKey = keyProvider.fromSingle(key.castSingle(), single) val existingCollection = accessor.getCollection(collectionKey) if (existingCollection != null) { - val updatedItems = existingCollection.items.toMutableList().map { - if (it.id == single.id) { - single - } else { - it + val updatedItems = + existingCollection.items.toMutableList().map { + if (it.id == single.id) { + single + } else { + it + } } - } - val updatedCollection = existingCollection.copyWith(items = updatedItems) as Collection + val updatedCollection = existingCollection.copyWith(items = updatedItems) as C accessor.putCollection(collectionKey, updatedCollection) } } is StoreKey.Collection<*> -> { - val collection = value as Collection + val collection = value as C accessor.putCollection(key.castCollection(), collection) collection.items.forEach { - val single = it as? Single + val single = it as? S if (single != null) { accessor.putSingle(keyProvider.fromCollection(key.castCollection(), single), single) } @@ -147,7 +158,6 @@ class StoreMultiCache, Single : StoreData.Single): Collection? = synchronized(this) { - collectionsCache.getIfPresent(key) - } + fun getCollection(key: StoreKey.Collection): Collection? = + synchronized(this) { + collectionsCache.getIfPresent(key) + } /** * Retrieves an individual item from the cache using the provided key. @@ -46,9 +47,10 @@ class StoreMultiCacheAccessor): Single? = synchronized(this) { - singlesCache.getIfPresent(key) - } + fun getSingle(key: StoreKey.Single): Single? = + synchronized(this) { + singlesCache.getIfPresent(key) + } /** * Stores a collection of items in the cache and updates the key set. @@ -58,7 +60,10 @@ class StoreMultiCacheAccessor, collection: Collection) = synchronized(this) { + fun putCollection( + key: StoreKey.Collection, + collection: Collection, + ) = synchronized(this) { collectionsCache.put(key, collection) keys.add(key) } @@ -71,7 +76,10 @@ class StoreMultiCacheAccessor, single: Single) = synchronized(this) { + fun putSingle( + key: StoreKey.Single, + single: Single, + ) = synchronized(this) { singlesCache.put(key, single) keys.add(key) } @@ -81,11 +89,12 @@ class StoreMultiCacheAccessor) = synchronized(this) { - singlesCache.invalidate(key) - keys.remove(key) - } + fun invalidateSingle(key: StoreKey.Single) = + synchronized(this) { + singlesCache.invalidate(key) + keys.remove(key) + } /** * Removes a collection of items from the cache and updates the key set. @@ -106,10 +116,11 @@ class StoreMultiCacheAccessor) = synchronized(this) { - collectionsCache.invalidate(key) - keys.remove(key) - } + fun invalidateCollection(key: StoreKey.Collection) = + synchronized(this) { + collectionsCache.invalidate(key) + keys.remove(key) + } /** * Calculates the total count of items in the cache, including both single items and items in collections. @@ -118,25 +129,26 @@ class StoreMultiCacheAccessor -> { - val single = singlesCache.getIfPresent(key) - if (single != null) { - count++ + fun size(): Long = + synchronized(this) { + var count = 0L + for (key in keys) { + when (key) { + is StoreKey.Single -> { + val single = singlesCache.getIfPresent(key) + if (single != null) { + count++ + } } - } - is StoreKey.Collection -> { - val collection = collectionsCache.getIfPresent(key) - if (collection != null) { - count += collection.items.size + is StoreKey.Collection -> { + val collection = collectionsCache.getIfPresent(key) + if (collection != null) { + count += collection.items.size + } } } } + count } - count - } } diff --git a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Weigher.kt b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Weigher.kt index bbb076e2a..dbb73292e 100644 --- a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Weigher.kt +++ b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Weigher.kt @@ -3,4 +3,4 @@ package org.mobilenativefoundation.store.cache5 /** * @return Weight of a cache entry. Must be non-negative. There is no unit for entry weights. Rather, they are simply relative to each other. */ -typealias Weigher = (key: Key, value: Value) -> Int +typealias Weigher = (key: Key, value: Value) -> Int diff --git a/cache/src/commonTest/kotlin/org/mobilenativefoundation/store/cache5/CacheTests.kt b/cache/src/commonTest/kotlin/org/mobilenativefoundation/store/cache5/CacheTests.kt index 2488c4eda..7b587b54e 100644 --- a/cache/src/commonTest/kotlin/org/mobilenativefoundation/store/cache5/CacheTests.kt +++ b/cache/src/commonTest/kotlin/org/mobilenativefoundation/store/cache5/CacheTests.kt @@ -7,100 +7,102 @@ import kotlin.test.assertEquals import kotlin.time.Duration.Companion.milliseconds class CacheTests { - private val cache: Cache = CacheBuilder().build() - - @Test - fun getIfPresent() { - cache.put("key", "value") - assertEquals("value", cache.getIfPresent("key")) - } - - @Test - fun getOrPut() { - assertEquals("value", cache.getOrPut("key") { "value" }) - } - - @Ignore // Not implemented yet - @Test - fun getAllPresent() { - cache.put("key1", "value1") - cache.put("key2", "value2") - assertEquals(mapOf("key1" to "value1", "key2" to "value2"), cache.getAllPresent(listOf("key1", "key2"))) - } - - @Ignore // Not implemented yet - @Test - fun putAll() { - cache.putAll(mapOf("key1" to "value1", "key2" to "value2")) - assertEquals(mapOf("key1" to "value1", "key2" to "value2"), cache.getAllPresent(listOf("key1", "key2"))) - } - - @Test - fun invalidate() { - cache.put("key", "value") - cache.invalidate("key") - assertEquals(null, cache.getIfPresent("key")) - } - - @Ignore // Not implemented yet - @Test - fun invalidateAll() { - cache.put("key1", "value1") - cache.put("key2", "value2") - cache.invalidateAll(listOf("key1", "key2")) - assertEquals(null, cache.getIfPresent("key1")) - assertEquals(null, cache.getIfPresent("key2")) - } - - @Ignore // Not implemented yet - @Test - fun size() { - cache.put("key1", "value1") - cache.put("key2", "value2") - assertEquals(2, cache.size()) - } - - @Test - fun maximumSize() { - val cache = CacheBuilder().maximumSize(1).build() - cache.put("key1", "value1") - cache.put("key2", "value2") - assertEquals(null, cache.getIfPresent("key1")) - assertEquals("value2", cache.getIfPresent("key2")) - } - - @Test - fun maximumWeight() { - val cache = CacheBuilder().weigher(399) { _, _ -> 100 }.build() - cache.put("key1", "value1") - cache.put("key2", "value2") - assertEquals(null, cache.getIfPresent("key1")) - assertEquals("value2", cache.getIfPresent("key2")) - } - - @Test - fun expireAfterAccess() = runTest { - var timeNs = 0L - val cache = CacheBuilder().expireAfterAccess(100.milliseconds).ticker { timeNs }.build() - cache.put("key", "value") - - timeNs += 50.milliseconds.inWholeNanoseconds - assertEquals("value", cache.getIfPresent("key")) - - timeNs += 100.milliseconds.inWholeNanoseconds - assertEquals(null, cache.getIfPresent("key")) - } - - @Test - fun expireAfterWrite() = runTest { - var timeNs = 0L - val cache = CacheBuilder().expireAfterWrite(100.milliseconds).ticker { timeNs }.build() - cache.put("key", "value") - - timeNs += 50.milliseconds.inWholeNanoseconds - assertEquals("value", cache.getIfPresent("key")) - - timeNs += 50.milliseconds.inWholeNanoseconds - assertEquals(null, cache.getIfPresent("key")) - } + private val cache: Cache = CacheBuilder().build() + + @Test + fun getIfPresent() { + cache.put("key", "value") + assertEquals("value", cache.getIfPresent("key")) + } + + @Test + fun getOrPut() { + assertEquals("value", cache.getOrPut("key") { "value" }) + } + + @Ignore // Not implemented yet + @Test + fun getAllPresent() { + cache.put("key1", "value1") + cache.put("key2", "value2") + assertEquals(mapOf("key1" to "value1", "key2" to "value2"), cache.getAllPresent(listOf("key1", "key2"))) + } + + @Ignore // Not implemented yet + @Test + fun putAll() { + cache.putAll(mapOf("key1" to "value1", "key2" to "value2")) + assertEquals(mapOf("key1" to "value1", "key2" to "value2"), cache.getAllPresent(listOf("key1", "key2"))) + } + + @Test + fun invalidate() { + cache.put("key", "value") + cache.invalidate("key") + assertEquals(null, cache.getIfPresent("key")) + } + + @Ignore // Not implemented yet + @Test + fun invalidateAll() { + cache.put("key1", "value1") + cache.put("key2", "value2") + cache.invalidateAll(listOf("key1", "key2")) + assertEquals(null, cache.getIfPresent("key1")) + assertEquals(null, cache.getIfPresent("key2")) + } + + @Ignore // Not implemented yet + @Test + fun size() { + cache.put("key1", "value1") + cache.put("key2", "value2") + assertEquals(2, cache.size()) + } + + @Test + fun maximumSize() { + val cache = CacheBuilder().maximumSize(1).build() + cache.put("key1", "value1") + cache.put("key2", "value2") + assertEquals(null, cache.getIfPresent("key1")) + assertEquals("value2", cache.getIfPresent("key2")) + } + + @Test + fun maximumWeight() { + val cache = CacheBuilder().weigher(399) { _, _ -> 100 }.build() + cache.put("key1", "value1") + cache.put("key2", "value2") + assertEquals(null, cache.getIfPresent("key1")) + assertEquals("value2", cache.getIfPresent("key2")) + } + + @Test + fun expireAfterAccess() = + runTest { + var timeNs = 0L + val cache = CacheBuilder().expireAfterAccess(100.milliseconds).ticker { timeNs }.build() + cache.put("key", "value") + + timeNs += 50.milliseconds.inWholeNanoseconds + assertEquals("value", cache.getIfPresent("key")) + + timeNs += 100.milliseconds.inWholeNanoseconds + assertEquals(null, cache.getIfPresent("key")) + } + + @Test + fun expireAfterWrite() = + runTest { + var timeNs = 0L + val cache = CacheBuilder().expireAfterWrite(100.milliseconds).ticker { timeNs }.build() + cache.put("key", "value") + + timeNs += 50.milliseconds.inWholeNanoseconds + assertEquals("value", cache.getIfPresent("key")) + + timeNs += 50.milliseconds.inWholeNanoseconds + assertEquals(null, cache.getIfPresent("key")) + } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index a44ebacf1..6200ddb53 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -39,7 +39,7 @@ kotlin { } } - jvmToolchain(11) + jvmToolchain(17) } android { @@ -61,8 +61,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } @@ -70,7 +70,7 @@ tasks.withType().configureEach { dokkaSourceSets.configureEach { reportUndocumented.set(false) skipDeprecated.set(true) - jdkVersion.set(11) + jdkVersion.set(17) } } diff --git a/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/InsertionStrategy.kt b/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/InsertionStrategy.kt index fe87030f7..23206ad77 100644 --- a/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/InsertionStrategy.kt +++ b/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/InsertionStrategy.kt @@ -4,5 +4,5 @@ package org.mobilenativefoundation.store.core5 enum class InsertionStrategy { APPEND, PREPEND, - REPLACE + REPLACE, } diff --git a/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/KeyProvider.kt b/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/KeyProvider.kt index 1fa84d815..3e320a9fe 100644 --- a/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/KeyProvider.kt +++ b/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/KeyProvider.kt @@ -2,6 +2,13 @@ package org.mobilenativefoundation.store.core5 @ExperimentalStoreApi interface KeyProvider> { - fun fromCollection(key: StoreKey.Collection, value: Single): StoreKey.Single - fun fromSingle(key: StoreKey.Single, value: Single): StoreKey.Collection + fun fromCollection( + key: StoreKey.Collection, + value: Single, + ): StoreKey.Single + + fun fromSingle( + key: StoreKey.Single, + value: Single, + ): StoreKey.Collection } diff --git a/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/StoreData.kt b/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/StoreData.kt index 30895bba1..2011c4f83 100644 --- a/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/StoreData.kt +++ b/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/StoreData.kt @@ -7,7 +7,6 @@ package org.mobilenativefoundation.store.core5 */ @ExperimentalStoreApi interface StoreData { - /** * Represents a single identifiable item. */ @@ -29,6 +28,9 @@ interface StoreData { /** * Inserts items to the existing collection and returns the updated collection. */ - fun insertItems(strategy: InsertionStrategy, items: List): Collection + fun insertItems( + strategy: InsertionStrategy, + items: List, + ): Collection } } diff --git a/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/StoreKey.kt b/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/StoreKey.kt index 529f762a8..ab367d564 100644 --- a/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/StoreKey.kt +++ b/core/src/commonMain/kotlin/org/mobilenativefoundation/store/core5/StoreKey.kt @@ -8,7 +8,6 @@ package org.mobilenativefoundation.store.core5 */ @ExperimentalStoreApi interface StoreKey { - /** * Represents a key for fetching an individual item. */ @@ -50,7 +49,7 @@ interface StoreKey { NEWEST, OLDEST, ALPHABETICAL, - REVERSE_ALPHABETICAL + REVERSE_ALPHABETICAL, } /** diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7516546c6..c7b0581f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ androidTargetSdk = "33" atomicFu = "0.23.2" baseKotlin = "1.9.22" dokkaGradlePlugin = "1.9.10" -ktlintGradle = "10.2.1" +ktlintGradle = "12.1.0" jacocoGradlePlugin = "0.8.7" mavenPublishPlugin = "0.22.0" moleculeGradlePlugin = "1.2.1" diff --git a/multicast/build.gradle.kts b/multicast/build.gradle.kts index 2b602120f..8532c34aa 100644 --- a/multicast/build.gradle.kts +++ b/multicast/build.gradle.kts @@ -54,7 +54,7 @@ kotlin { } } - jvmToolchain(11) + jvmToolchain(17) } android { @@ -76,8 +76,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } @@ -85,7 +85,7 @@ tasks.withType().configureEach { dokkaSourceSets.configureEach { reportUndocumented.set(false) skipDeprecated.set(true) - jdkVersion.set(8) + jdkVersion.set(17) } } diff --git a/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/Actor.kt b/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/Actor.kt index dac13ce0a..9a483141a 100644 --- a/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/Actor.kt +++ b/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/Actor.kt @@ -18,16 +18,17 @@ internal fun CoroutineScope.actor( context: CoroutineContext = EmptyCoroutineContext, capacity: Int = 0, onCompletion: CompletionHandler? = null, - block: suspend CoroutineScope.(ReceiveChannel) -> Unit + block: suspend CoroutineScope.(ReceiveChannel) -> Unit, ): SendChannel { val channel = Channel(capacity) - val job = launch(context) { - try { - block(channel) - } finally { - if (isActive) channel.cancel() + val job = + launch(context) { + try { + block(channel) + } finally { + if (isActive) channel.cancel() + } } - } if (onCompletion != null) job.invokeOnCompletion(handler = onCompletion) return channel } diff --git a/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/ChannelManager.kt b/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/ChannelManager.kt index 6c35fa005..d596cf717 100644 --- a/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/ChannelManager.kt +++ b/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/ChannelManager.kt @@ -24,10 +24,9 @@ import kotlinx.coroutines.flow.Flow import org.mobilenativefoundation.store.multicast5.ChannelManager.Message internal interface ChannelManager { - suspend fun addDownstream( channel: SendChannel>, - piggybackOnly: Boolean = false + piggybackOnly: Boolean = false, ) suspend fun removeDownstream(channel: SendChannel>) @@ -46,7 +45,7 @@ internal interface ChannelManager { * Tracking whether this channel is a piggyback only channel that can be closed without ever * receiving a value or error. */ - val piggybackOnly: Boolean = false + val piggybackOnly: Boolean = false, ) { private var _awaitsDispatch: Boolean = !piggybackOnly @@ -81,7 +80,7 @@ internal interface ChannelManager { */ class AddChannel( val channel: SendChannel>, - val piggybackOnly: Boolean = false + val piggybackOnly: Boolean = false, ) : Message() /** @@ -102,7 +101,7 @@ internal interface ChannelManager { * Ack that is completed by all receiver. Upstream producer will await this before asking * for a new value from upstream */ - val delivered: CompletableDeferred + val delivered: CompletableDeferred, ) : Dispatch() /** @@ -112,14 +111,14 @@ internal interface ChannelManager { /** * The error sent by the upstream */ - val error: Throwable + val error: Throwable, ) : Dispatch() class UpstreamFinished( /** * SharedFlowProducer finished emitting */ - val producer: SharedFlowProducer + val producer: SharedFlowProducer, ) : Dispatch() } } @@ -149,7 +148,6 @@ internal class StoreChannelManager( * it will receive values as well. */ private val piggybackingDownstream: Boolean = false, - /** * If true, an active upstream will stay alive even if all downstreams are closed. A downstream * coming in later will receive a value from the live upstream. @@ -161,8 +159,7 @@ internal class StoreChannelManager( * Called when a value is dispatched */ private val onEach: suspend (T) -> Unit, - - private val upstream: Flow + private val upstream: Flow, ) : ChannelManager { init { require(!keepUpstreamAlive || bufferSize > 0) { @@ -170,11 +167,12 @@ internal class StoreChannelManager( } } - override suspend fun addDownstream(channel: SendChannel>, piggybackOnly: Boolean) = - actor.send(Message.AddChannel(channel, piggybackOnly)) + override suspend fun addDownstream( + channel: SendChannel>, + piggybackOnly: Boolean, + ) = actor.send(Message.AddChannel(channel, piggybackOnly)) - override suspend fun removeDownstream(channel: SendChannel>) = - actor.send(Message.RemoveChannel(channel)) + override suspend fun removeDownstream(channel: SendChannel>) = actor.send(Message.RemoveChannel(channel)) override suspend fun close() = actor.close() @@ -184,7 +182,6 @@ internal class StoreChannelManager( * Actor that does all the work. Any state and functionality should go here. */ private inner class Actor : StoreRealActor>(scope) { - private val buffer = Buffer(bufferSize) /** @@ -311,9 +308,10 @@ internal class StoreChannelManager( * Remove a downstream collector. */ private suspend fun doRemove(channel: SendChannel>) { - val index = channels.indexOfFirst { - it.hasChannel(channel) - } + val index = + channels.indexOfFirst { + it.hasChannel(channel) + } if (index >= 0) { channels.removeAt(index) if (!keepUpstreamAlive && channels.isEmpty()) { @@ -330,10 +328,11 @@ internal class StoreChannelManager( "cannot add a piggyback only downstream when piggybackDownstream is disabled" } addEntry( - entry = ChannelManager.ChannelEntry( - channel = msg.channel, - piggybackOnly = msg.piggybackOnly - ) + entry = + ChannelManager.ChannelEntry( + channel = msg.channel, + piggybackOnly = msg.piggybackOnly, + ), ) if (!msg.piggybackOnly) { activateIfNecessary() @@ -352,9 +351,10 @@ internal class StoreChannelManager( * Internally add the new downstream collector to our list, send it anything buffered. */ private suspend fun addEntry(entry: ChannelManager.ChannelEntry) { - val new = channels.none { - it.hasChannel(entry) - } + val new = + channels.none { + it.hasChannel(entry) + } check(new) { "$entry is already in the list." } @@ -376,7 +376,9 @@ internal class StoreChannelManager( */ private interface Buffer { fun add(item: Message.Dispatch.Value) + fun isEmpty() = items.isEmpty() + val items: Collection> } @@ -395,11 +397,12 @@ private class NoBuffer : Buffer { * Create a new buffer insteance based on the provided limit. */ @Suppress("FunctionName") -private fun Buffer(limit: Int): Buffer = if (limit > 0) { - BufferImpl(limit) -} else { - NoBuffer() -} +private fun Buffer(limit: Int): Buffer = + if (limit > 0) { + BufferImpl(limit) + } else { + NoBuffer() + } /** * A real buffer implementation that has a FIFO queue. @@ -407,6 +410,7 @@ private fun Buffer(limit: Int): Buffer = if (limit > 0) { private class BufferImpl(private val limit: Int) : Buffer { override val items = ArrayDeque>(limit.coerceAtMost(10)) + override fun add(item: Message.Dispatch.Value) { while (items.size >= limit) { items.removeFirst() @@ -415,5 +419,4 @@ private class BufferImpl(private val limit: Int) : } } -internal fun Message.Dispatch.Value.markDelivered() = - delivered.complete(Unit) +internal fun Message.Dispatch.Value.markDelivered() = delivered.complete(Unit) diff --git a/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/Multicaster.kt b/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/Multicaster.kt index 74f6b1ac0..f006e853b 100644 --- a/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/Multicaster.kt +++ b/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/Multicaster.kt @@ -50,7 +50,6 @@ class Multicaster( * Source function to create a new flow when necessary. */ private val source: Flow, - /** * If true, downstream is never closed by the multicaster unless upstream throws an error. * Instead, it is kept open and if a new downstream shows up that causes us to restart the flow, @@ -67,9 +66,8 @@ class Multicaster( /** * Called when upstream dispatches a value. */ - private val onEach: suspend (T) -> Unit + private val onEach: suspend (T) -> Unit, ) { - internal var channelManagerFactory: () -> ChannelManager = { StoreChannelManager( scope = scope, @@ -77,7 +75,7 @@ class Multicaster( upstream = source, piggybackingDownstream = piggybackingDownstream, keepUpstreamAlive = keepUpstreamAlive, - onEach = onEach + onEach = onEach, ) } @@ -98,28 +96,29 @@ class Multicaster( } return flow { val channel = Channel>(Channel.UNLIMITED) - val subFlow = channel.consumeAsFlow() - .onStart { - try { - channelManager.addDownstream(channel, piggybackOnly) - } catch (closed: ClosedSendChannelException) { - // before we could start, channel manager was closed. - // close our downstream manually as it won't be closed by the ChannelManager - channel.close() - } - } - .transform, T> { - emit(it.value) - it.delivered.complete(Unit) - }.onCompletion { - withContext(NonCancellable) { + val subFlow = + channel.consumeAsFlow() + .onStart { try { - channelManager.removeDownstream(channel) + channelManager.addDownstream(channel, piggybackOnly) } catch (closed: ClosedSendChannelException) { - // ignore, we might be closed because ChannelManager is closed + // before we could start, channel manager was closed. + // close our downstream manually as it won't be closed by the ChannelManager + channel.close() + } + } + .transform, T> { + emit(it.value) + it.delivered.complete(Unit) + }.onCompletion { + withContext(NonCancellable) { + try { + channelManager.removeDownstream(channel) + } catch (closed: ClosedSendChannelException) { + // ignore, we might be closed because ChannelManager is closed + } } } - } emitAll(subFlow) } } diff --git a/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/SharedFlowProducer.kt b/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/SharedFlowProducer.kt index 5b3dbf946..a4ae02aa3 100644 --- a/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/SharedFlowProducer.kt +++ b/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/SharedFlowProducer.kt @@ -38,27 +38,28 @@ import kotlinx.coroutines.launch internal class SharedFlowProducer( private val scope: CoroutineScope, private val src: Flow, - private val sendUpsteamMessage: suspend (ChannelManager.Message.Dispatch) -> Unit + private val sendUpsteamMessage: suspend (ChannelManager.Message.Dispatch) -> Unit, ) { - private val collectionJob: Job = scope.launch(start = CoroutineStart.LAZY) { - try { - src.catch { - sendUpsteamMessage(ChannelManager.Message.Dispatch.Error(it)) - }.collect { - val ack = CompletableDeferred() - sendUpsteamMessage( - ChannelManager.Message.Dispatch.Value( - it, - ack + private val collectionJob: Job = + scope.launch(start = CoroutineStart.LAZY) { + try { + src.catch { + sendUpsteamMessage(ChannelManager.Message.Dispatch.Error(it)) + }.collect { + val ack = CompletableDeferred() + sendUpsteamMessage( + ChannelManager.Message.Dispatch.Value( + it, + ack, + ), ) - ) - // suspend until at least 1 receives the new value - ack.await() + // suspend until at least 1 receives the new value + ack.await() + } + } catch (closed: ClosedSendChannelException) { + // ignore. if consumers are gone, it might close itself. } - } catch (closed: ClosedSendChannelException) { - // ignore. if consumers are gone, it might close itself. } - } /** * Starts the collection of the upstream flow. diff --git a/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/StoreRealActor.kt b/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/StoreRealActor.kt index d5b8e0cd7..502ef6c5b 100644 --- a/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/StoreRealActor.kt +++ b/multicast/src/commonMain/kotlin/org/mobilenativefoundation/store/multicast5/StoreRealActor.kt @@ -27,30 +27,31 @@ import kotlinx.coroutines.channels.SendChannel */ @Suppress("EXPERIMENTAL_API_USAGE") internal abstract class StoreRealActor( - scope: CoroutineScope + scope: CoroutineScope, ) { private val inboundChannel: SendChannel private val closeCompleted = CompletableDeferred() private val didClose = atomic(false) init { - inboundChannel = scope.actor( - capacity = 0 - ) { - try { - for (msg in it) { - if (msg === CLOSE_TOKEN) { - doClose() - break - } else { - @Suppress("UNCHECKED_CAST") - handle(msg as T) + inboundChannel = + scope.actor( + capacity = 0, + ) { + try { + for (msg in it) { + if (msg === CLOSE_TOKEN) { + doClose() + break + } else { + @Suppress("UNCHECKED_CAST") + handle(msg as T) + } } + } finally { + doClose() } - } finally { - doClose() } - } } private fun doClose() { diff --git a/paging/build.gradle.kts b/paging/build.gradle.kts index f93e1b172..96ca43b27 100644 --- a/paging/build.gradle.kts +++ b/paging/build.gradle.kts @@ -53,7 +53,7 @@ kotlin { } } - jvmToolchain(11) + jvmToolchain(17) } android { @@ -75,8 +75,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } @@ -84,7 +84,7 @@ tasks.withType().configureEach { dokkaSourceSets.configureEach { reportUndocumented.set(false) skipDeprecated.set(true) - jdkVersion.set(11) + jdkVersion.set(17) } } diff --git a/paging/src/commonMain/kotlin/org/mobilenativefoundation/store/paging5/LaunchPagingStore.kt b/paging/src/commonMain/kotlin/org/mobilenativefoundation/store/paging5/LaunchPagingStore.kt index 58138e5e5..3fee41c26 100644 --- a/paging/src/commonMain/kotlin/org/mobilenativefoundation/store/paging5/LaunchPagingStore.kt +++ b/paging/src/commonMain/kotlin/org/mobilenativefoundation/store/paging5/LaunchPagingStore.kt @@ -36,7 +36,6 @@ private fun , Output : StoreData> launchPagingS val stateFlow = MutableStateFlow>(StoreReadResponse.Initial) scope.launch { - try { val firstKey = keys.first() if (firstKey !is StoreKey.Collection<*>) throw IllegalArgumentException("Invalid key type") @@ -102,12 +101,13 @@ fun , Output : StoreData> MutableStore, Output : StoreData> joinData( key: Key, prevResponse: StoreReadResponse, - currentResponse: StoreReadResponse.Data + currentResponse: StoreReadResponse.Data, ): StoreReadResponse.Data { - val lastOutput = when (prevResponse) { - is StoreReadResponse.Data -> prevResponse.value as? StoreData.Collection> - else -> null - } + val lastOutput = + when (prevResponse) { + is StoreReadResponse.Data -> prevResponse.value as? StoreData.Collection> + else -> null + } val currentData = currentResponse.value as StoreData.Collection> diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/LaunchPagingStoreTests.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/LaunchPagingStoreTests.kt index af2d0e036..3ab4f97c7 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/LaunchPagingStoreTests.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/LaunchPagingStoreTests.kt @@ -42,126 +42,133 @@ class LaunchPagingStoreTests { } @Test - fun transitionFromInitialToData() = testScope.runTest { - val key = PostKey.Cursor("1", 10) - val keys = flowOf(key) - val stateFlow = store.launchPagingStore(this, keys) - - stateFlow.test { - val state1 = awaitItem() - assertIs(state1) - val state2 = awaitItem() - assertIs(state2) - val state3 = awaitItem() - assertIs>(state3) - expectNoEvents() + fun transitionFromInitialToData() = + testScope.runTest { + val key = PostKey.Cursor("1", 10) + val keys = flowOf(key) + val stateFlow = store.launchPagingStore(this, keys) + + stateFlow.test { + val state1 = awaitItem() + assertIs(state1) + val state2 = awaitItem() + assertIs(state2) + val state3 = awaitItem() + assertIs>(state3) + expectNoEvents() + } } - } @Test - fun multipleValidKeysEmittedInSuccession() = testScope.runTest { - val key1 = PostKey.Cursor("1", 10) - val key2 = PostKey.Cursor("11", 10) - val keys = flowOf(key1, key2) - val stateFlow = store.launchPagingStore(this, keys) - - stateFlow.test { - val state1 = awaitItem() - assertIs(state1) - val state2 = awaitItem() - assertIs(state2) - val state3 = awaitItem() - assertIs>(state3) - assertEquals("1", state3.value.posts[0].postId) - - val state4 = awaitItem() - assertIs>(state4) - assertEquals("11", state4.value.posts[0].postId) - assertEquals("1", state4.value.posts[10].postId) - val data4 = state4.value - assertIs(data4) - assertEquals(20, data4.items.size) - expectNoEvents() + fun multipleValidKeysEmittedInSuccession() = + testScope.runTest { + val key1 = PostKey.Cursor("1", 10) + val key2 = PostKey.Cursor("11", 10) + val keys = flowOf(key1, key2) + val stateFlow = store.launchPagingStore(this, keys) + + stateFlow.test { + val state1 = awaitItem() + assertIs(state1) + val state2 = awaitItem() + assertIs(state2) + val state3 = awaitItem() + assertIs>(state3) + assertEquals("1", state3.value.posts[0].postId) + + val state4 = awaitItem() + assertIs>(state4) + assertEquals("11", state4.value.posts[0].postId) + assertEquals("1", state4.value.posts[10].postId) + val data4 = state4.value + assertIs(data4) + assertEquals(20, data4.items.size) + expectNoEvents() + } } - } @Test - fun sameKeyEmittedMultipleTimes() = testScope.runTest { - val key = PostKey.Cursor("1", 10) - val keys = flowOf(key, key) - val stateFlow = store.launchPagingStore(this, keys) - - stateFlow.test { - val state1 = awaitItem() - assertIs(state1) - val state2 = awaitItem() - assertIs(state2) - val state3 = awaitItem() - assertIs>(state3) - expectNoEvents() + fun sameKeyEmittedMultipleTimes() = + testScope.runTest { + val key = PostKey.Cursor("1", 10) + val keys = flowOf(key, key) + val stateFlow = store.launchPagingStore(this, keys) + + stateFlow.test { + val state1 = awaitItem() + assertIs(state1) + val state2 = awaitItem() + assertIs(state2) + val state3 = awaitItem() + assertIs>(state3) + expectNoEvents() + } } - } @Test - fun multipleKeysWithReadsAndWrites() = testScope.runTest { - val api = FakePostApi() - val db = FakePostDatabase(userId) - val factory = PostStoreFactory(api = api, db = db) - val store = factory.create() - - val key1 = PostKey.Cursor("1", 10) - val key2 = PostKey.Cursor("11", 10) - val keys = flowOf(key1, key2) - - val stateFlow = store.launchPagingStore(this, keys) - stateFlow.test { - val initialState = awaitItem() - assertIs(initialState) - val loadingState = awaitItem() - assertIs(loadingState) - val loadedState1 = awaitItem() - assertIs>(loadedState1) - val data1 = loadedState1.value - assertEquals(10, data1.posts.size) - val loadedState2 = awaitItem() - assertIs>(loadedState2) - val data2 = loadedState2.value - assertEquals(20, data2.posts.size) + fun multipleKeysWithReadsAndWrites() = + testScope.runTest { + val api = FakePostApi() + val db = FakePostDatabase(userId) + val factory = PostStoreFactory(api = api, db = db) + val store = factory.create() + + val key1 = PostKey.Cursor("1", 10) + val key2 = PostKey.Cursor("11", 10) + val keys = flowOf(key1, key2) + + val stateFlow = store.launchPagingStore(this, keys) + stateFlow.test { + val initialState = awaitItem() + assertIs(initialState) + val loadingState = awaitItem() + assertIs(loadingState) + val loadedState1 = awaitItem() + assertIs>(loadedState1) + val data1 = loadedState1.value + assertEquals(10, data1.posts.size) + val loadedState2 = awaitItem() + assertIs>(loadedState2) + val data2 = loadedState2.value + assertEquals(20, data2.posts.size) + } + + val cached = + store.stream(StoreReadRequest.cached(key1, refresh = false)) + .first { it.dataOrNull() != null } + assertIs>(cached) + assertEquals(StoreReadResponseOrigin.Cache, cached.origin) + val data = cached.requireData() + assertIs(data) + assertEquals(10, data.posts.size) + + val cached2 = + store.stream(StoreReadRequest.cached(PostKey.Single("2"), refresh = false)) + .first { it.dataOrNull() != null } + assertIs>(cached2) + assertEquals(StoreReadResponseOrigin.Cache, cached2.origin) + val data2 = cached2.requireData() + assertIs(data2) + assertEquals("2", data2.title) + + store.write(StoreWriteRequest.of(PostKey.Single("2"), PostData.Post("2", "2-modified"))) + + val cached3 = + store.stream(StoreReadRequest.cached(PostKey.Single("2"), refresh = false)) + .first { it.dataOrNull() != null } + assertIs>(cached3) + assertEquals(StoreReadResponseOrigin.Cache, cached3.origin) + val data3 = cached3.requireData() + assertIs(data3) + assertEquals("2-modified", data3.title) + + val cached4 = + store.stream(StoreReadRequest.cached(PostKey.Cursor("1", 10), refresh = false)) + .first { it.dataOrNull() != null } + assertIs>(cached4) + assertEquals(StoreReadResponseOrigin.Cache, cached4.origin) + val data4 = cached4.requireData() + assertIs(data4) + assertEquals("2-modified", data4.posts[1].title) } - - val cached = store.stream(StoreReadRequest.cached(key1, refresh = false)) - .first { it.dataOrNull() != null } - assertIs>(cached) - assertEquals(StoreReadResponseOrigin.Cache, cached.origin) - val data = cached.requireData() - assertIs(data) - assertEquals(10, data.posts.size) - - val cached2 = store.stream(StoreReadRequest.cached(PostKey.Single("2"), refresh = false)) - .first { it.dataOrNull() != null } - assertIs>(cached2) - assertEquals(StoreReadResponseOrigin.Cache, cached2.origin) - val data2 = cached2.requireData() - assertIs(data2) - assertEquals("2", data2.title) - - store.write(StoreWriteRequest.of(PostKey.Single("2"), PostData.Post("2", "2-modified"))) - - val cached3 = store.stream(StoreReadRequest.cached(PostKey.Single("2"), refresh = false)) - .first { it.dataOrNull() != null } - assertIs>(cached3) - assertEquals(StoreReadResponseOrigin.Cache, cached3.origin) - val data3 = cached3.requireData() - assertIs(data3) - assertEquals("2-modified", data3.title) - - val cached4 = - store.stream(StoreReadRequest.cached(PostKey.Cursor("1", 10), refresh = false)) - .first { it.dataOrNull() != null } - assertIs>(cached4) - assertEquals(StoreReadResponseOrigin.Cache, cached4.origin) - val data4 = cached4.requireData() - assertIs(data4) - assertEquals("2-modified", data4.posts[1].title) - } } diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FakePostApi.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FakePostApi.kt index 7764cc65e..9e4343951 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FakePostApi.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FakePostApi.kt @@ -1,7 +1,6 @@ package org.mobilenativefoundation.store.paging5.util class FakePostApi : PostApi { - private val posts = mutableMapOf() private val postsList = mutableListOf() @@ -22,7 +21,10 @@ class FakePostApi : PostApi { } } - override suspend fun get(cursor: String?, size: Int): FeedGetRequestResult { + override suspend fun get( + cursor: String?, + size: Int, + ): FeedGetRequestResult { val firstIndexInclusive = postsList.indexOfFirst { it.postId == cursor } val lastIndexExclusive = firstIndexInclusive + size val posts = postsList.subList(firstIndexInclusive, lastIndexExclusive) diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FakePostDatabase.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FakePostDatabase.kt index a126c9b3f..ac8fa76a8 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FakePostDatabase.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FakePostDatabase.kt @@ -3,16 +3,18 @@ package org.mobilenativefoundation.store.paging5.util class FakePostDatabase(private val userId: String) : PostDatabase { private val posts = mutableMapOf() private val feeds = mutableMapOf() + override fun add(post: PostData.Post) { posts[post.id] = post - val nextFeed = feeds[userId]?.posts?.map { - if (it.postId == post.postId) { - post - } else { - it + val nextFeed = + feeds[userId]?.posts?.map { + if (it.postId == post.postId) { + post + } else { + it + } } - } nextFeed?.let { feeds[userId] = PostData.Feed(nextFeed) @@ -27,7 +29,10 @@ class FakePostDatabase(private val userId: String) : PostDatabase { return posts[postId] } - override fun findFeedByUserId(cursor: String?, size: Int): PostData.Feed? { + override fun findFeedByUserId( + cursor: String?, + size: Int, + ): PostData.Feed? { val feed = feeds[userId] return feed } diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FeedGetRequestResult.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FeedGetRequestResult.kt index e1d13e50e..073efc92b 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FeedGetRequestResult.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/FeedGetRequestResult.kt @@ -1,10 +1,11 @@ package org.mobilenativefoundation.store.paging5.util sealed class FeedGetRequestResult { - data class Data(val data: PostData.Feed) : FeedGetRequestResult() + sealed class Error : FeedGetRequestResult() { data class Message(val error: String) : Error() + data class Exception(val error: kotlin.Exception) : Error() } } diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostApi.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostApi.kt index 90d87e601..182bbe047 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostApi.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostApi.kt @@ -2,6 +2,11 @@ package org.mobilenativefoundation.store.paging5.util interface PostApi { suspend fun get(postId: String): PostGetRequestResult - suspend fun get(cursor: String?, size: Int): FeedGetRequestResult + + suspend fun get( + cursor: String?, + size: Int, + ): FeedGetRequestResult + suspend fun put(post: PostData.Post): PostPutRequestResult } diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostData.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostData.kt index ad6b05d28..686e12270 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostData.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostData.kt @@ -10,9 +10,13 @@ sealed class PostData : StoreData { data class Feed(val posts: List) : StoreData.Collection, PostData() { override val items: List get() = posts + override fun copyWith(items: List): StoreData.Collection = copy(posts = items) - override fun insertItems(strategy: InsertionStrategy, items: List): StoreData.Collection { + override fun insertItems( + strategy: InsertionStrategy, + items: List, + ): StoreData.Collection { return when (strategy) { InsertionStrategy.APPEND -> { val updatedItems = items.toMutableList() diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostDatabase.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostDatabase.kt index d8ae595c9..a9f229c97 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostDatabase.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostDatabase.kt @@ -2,7 +2,13 @@ package org.mobilenativefoundation.store.paging5.util interface PostDatabase { fun add(post: PostData.Post) + fun add(feed: PostData.Feed) + fun findPostByPostId(postId: String): PostData.Post? - fun findFeedByUserId(cursor: String?, size: Int): PostData.Feed? + + fun findFeedByUserId( + cursor: String?, + size: Int, + ): PostData.Feed? } diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostGetRequestResult.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostGetRequestResult.kt index d481f661f..a5564afc0 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostGetRequestResult.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostGetRequestResult.kt @@ -1,10 +1,11 @@ package org.mobilenativefoundation.store.paging5.util sealed class PostGetRequestResult { - data class Data(val data: PostData.Post) : PostGetRequestResult() + sealed class Error : PostGetRequestResult() { data class Message(val error: String) : Error() + data class Exception(val error: kotlin.Exception) : Error() } } diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostKey.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostKey.kt index 451c5e0b9..7d7a54f08 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostKey.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostKey.kt @@ -9,10 +9,10 @@ sealed class PostKey : StoreKey { override val size: Int, override val sort: StoreKey.Sort? = null, override val filters: List>? = null, - override val insertionStrategy: InsertionStrategy = InsertionStrategy.APPEND + override val insertionStrategy: InsertionStrategy = InsertionStrategy.APPEND, ) : StoreKey.Collection.Cursor, PostKey() data class Single( - override val id: String + override val id: String, ) : StoreKey.Single, PostKey() } diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostPutRequestResult.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostPutRequestResult.kt index fdf855dbb..36cea329a 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostPutRequestResult.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostPutRequestResult.kt @@ -1,10 +1,11 @@ package org.mobilenativefoundation.store.paging5.util sealed class PostPutRequestResult { - data class Data(val data: PostData.Post) : PostPutRequestResult() + sealed class Error : PostPutRequestResult() { data class Message(val error: String) : Error() + data class Exception(val error: kotlin.Exception) : Error() } } diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostStoreFactory.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostStoreFactory.kt index 8ed9b2011..b3e014a83 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostStoreFactory.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/util/PostStoreFactory.kt @@ -5,10 +5,10 @@ package org.mobilenativefoundation.store.paging5.util import kotlinx.coroutines.flow.flow import org.mobilenativefoundation.store.cache5.Cache import org.mobilenativefoundation.store.cache5.StoreMultiCache +import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.core5.KeyProvider import org.mobilenativefoundation.store.core5.StoreKey import org.mobilenativefoundation.store.store5.Converter -import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.store5.Fetcher import org.mobilenativefoundation.store.store5.MutableStore import org.mobilenativefoundation.store.store5.SourceOfTruth @@ -18,71 +18,72 @@ import org.mobilenativefoundation.store.store5.UpdaterResult import kotlin.math.floor class PostStoreFactory(private val api: PostApi, private val db: PostDatabase) { - - private fun createFetcher(): Fetcher = Fetcher.of { key -> - when (key) { - is PostKey.Single -> { - when (val result = api.get(key.id)) { - is PostGetRequestResult.Data -> { - result.data - } - - is PostGetRequestResult.Error.Exception -> { - throw Throwable(result.error) - } - - is PostGetRequestResult.Error.Message -> { - throw Throwable(result.error) + private fun createFetcher(): Fetcher = + Fetcher.of { key -> + when (key) { + is PostKey.Single -> { + when (val result = api.get(key.id)) { + is PostGetRequestResult.Data -> { + result.data + } + + is PostGetRequestResult.Error.Exception -> { + throw Throwable(result.error) + } + + is PostGetRequestResult.Error.Message -> { + throw Throwable(result.error) + } } } - } - is PostKey.Cursor -> { - when (val result = api.get(key.cursor, key.size)) { - is FeedGetRequestResult.Data -> { - result.data - } + is PostKey.Cursor -> { + when (val result = api.get(key.cursor, key.size)) { + is FeedGetRequestResult.Data -> { + result.data + } - is FeedGetRequestResult.Error.Exception -> { - throw Throwable(result.error) - } + is FeedGetRequestResult.Error.Exception -> { + throw Throwable(result.error) + } - is FeedGetRequestResult.Error.Message -> { - throw Throwable(result.error) + is FeedGetRequestResult.Error.Message -> { + throw Throwable(result.error) + } } } } } - } - - private fun createSourceOfTruth(): SourceOfTruth = SourceOfTruth.of( - reader = { key -> - flow { - when (key) { - is PostKey.Single -> { - val post = db.findPostByPostId(key.id) - emit(post) - } - is PostKey.Cursor -> { - val feed = db.findFeedByUserId(key.cursor, key.size) - emit(feed) + private fun createSourceOfTruth(): SourceOfTruth = + SourceOfTruth.of( + reader = { key -> + flow { + when (key) { + is PostKey.Single -> { + val post = db.findPostByPostId(key.id) + emit(post) + } + + is PostKey.Cursor -> { + val feed = db.findFeedByUserId(key.cursor, key.size) + emit(feed) + } } } - } - }, - writer = { key, data -> - when { - key is PostKey.Single && data is PostData.Post -> { - db.add(data) - } + }, + writer = { key, data -> + when { + key is PostKey.Single && data is PostData.Post -> { + db.add(data) + } - key is PostKey.Cursor && data is PostData.Feed -> { - db.add(data) + key is PostKey.Cursor && data is PostData.Feed -> { + db.add(data) + } } - } - } - ) + }, + ) private fun createConverter(): Converter = Converter.Builder() @@ -90,49 +91,53 @@ class PostStoreFactory(private val api: PostApi, private val db: PostDatabase) { .fromOutputToLocal { it } .build() - private fun createUpdater(): Updater = Updater.by( - post = { key, data -> - when { - key is PostKey.Single && data is PostData.Post -> { - when (val result = api.put(data)) { - is PostPutRequestResult.Data -> UpdaterResult.Success.Typed(result) - is PostPutRequestResult.Error.Exception -> UpdaterResult.Error.Exception(result.error) - is PostPutRequestResult.Error.Message -> UpdaterResult.Error.Message(result.error) + private fun createUpdater(): Updater = + Updater.by( + post = { key, data -> + when { + key is PostKey.Single && data is PostData.Post -> { + when (val result = api.put(data)) { + is PostPutRequestResult.Data -> UpdaterResult.Success.Typed(result) + is PostPutRequestResult.Error.Exception -> UpdaterResult.Error.Exception(result.error) + is PostPutRequestResult.Error.Message -> UpdaterResult.Error.Message(result.error) + } } - } - else -> UpdaterResult.Error.Message("Unsupported: key: ${key::class}, data: ${data::class}") - } - } - ) + else -> UpdaterResult.Error.Message("Unsupported: key: ${key::class}, data: ${data::class}") + } + }, + ) private fun createPagingCacheKeyProvider(): KeyProvider = object : KeyProvider { override fun fromCollection( key: StoreKey.Collection, - value: PostData.Post + value: PostData.Post, ): StoreKey.Single { return PostKey.Single(value.postId) } - override fun fromSingle(key: StoreKey.Single, value: PostData.Post): StoreKey.Collection { + override fun fromSingle( + key: StoreKey.Single, + value: PostData.Post, + ): StoreKey.Collection { val id = value.postId.toInt() val cursor = (floor(id.toDouble() / 10) * 10) + 1 return PostKey.Cursor(cursor.toInt().toString(), 10) } } - private fun createMemoryCache(): Cache = - StoreMultiCache(createPagingCacheKeyProvider()) - - fun create(): MutableStore = StoreBuilder.from( - fetcher = createFetcher(), - sourceOfTruth = createSourceOfTruth(), - memoryCache = createMemoryCache() - ).toMutableStoreBuilder( - converter = createConverter() - ).build( - updater = createUpdater(), - bookkeeper = null - ) + private fun createMemoryCache(): Cache = StoreMultiCache(createPagingCacheKeyProvider()) + + fun create(): MutableStore = + StoreBuilder.from( + fetcher = createFetcher(), + sourceOfTruth = createSourceOfTruth(), + memoryCache = createMemoryCache(), + ).toMutableStoreBuilder( + converter = createConverter(), + ).build( + updater = createUpdater(), + bookkeeper = null, + ) } diff --git a/rx2/build.gradle.kts b/rx2/build.gradle.kts index d7ca6cb77..2915fd0d6 100644 --- a/rx2/build.gradle.kts +++ b/rx2/build.gradle.kts @@ -45,20 +45,20 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } kotlin { - jvmToolchain(11) + jvmToolchain(17) } tasks.withType().configureEach { dokkaSourceSets.configureEach { reportUndocumented.set(false) skipDeprecated.set(true) - jdkVersion.set(8) + jdkVersion.set(11) } } diff --git a/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxFetcher.kt b/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxFetcher.kt index b2e4a9f8d..57332ee8e 100644 --- a/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxFetcher.kt +++ b/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxFetcher.kt @@ -19,7 +19,7 @@ import org.mobilenativefoundation.store.store5.Store * @param flowableFactory a factory for a [Flowable] source of network records. */ fun Fetcher.Companion.ofResultFlowable( - flowableFactory: (key: Key) -> Flowable> + flowableFactory: (key: Key) -> Flowable>, ): Fetcher = ofResultFlow { key: Key -> flowableFactory(key).asFlow() } /** @@ -34,7 +34,7 @@ fun Fetcher.Companion.ofResultFlowable( * @param singleFactory a factory for a [Single] source of network records. */ fun Fetcher.Companion.ofResultSingle( - singleFactory: (key: Key) -> Single> + singleFactory: (key: Key) -> Single>, ): Fetcher = ofResultFlowable { key: Key -> singleFactory(key).toFlowable() } /** @@ -49,9 +49,8 @@ fun Fetcher.Companion.ofResultSingle( * * @param flowFactory a factory for a [Flowable] source of network records. */ -fun Fetcher.Companion.ofFlowable( - flowableFactory: (key: Key) -> Flowable -): Fetcher = ofFlow { key: Key -> flowableFactory(key).asFlow() } +fun Fetcher.Companion.ofFlowable(flowableFactory: (key: Key) -> Flowable): Fetcher = + ofFlow { key: Key -> flowableFactory(key).asFlow() } /** * Creates a new [Fetcher] from a [singleFactory] and translate the results to a [FetcherResult]. @@ -65,6 +64,5 @@ fun Fetcher.Companion.ofFlowable( * * @param singleFactory a factory for a [Single] source of network records. */ -fun Fetcher.Companion.ofSingle( - singleFactory: (key: Key) -> Single -): Fetcher = ofFlowable { key: Key -> singleFactory(key).toFlowable() } \ No newline at end of file +fun Fetcher.Companion.ofSingle(singleFactory: (key: Key) -> Single): Fetcher = + ofFlowable { key: Key -> singleFactory(key).toFlowable() } diff --git a/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxSourceOfTruth.kt b/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxSourceOfTruth.kt index 27700f284..68e18adc5 100644 --- a/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxSourceOfTruth.kt +++ b/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxSourceOfTruth.kt @@ -1,6 +1,5 @@ package org.mobilenativefoundation.store.rx2 - import io.reactivex.Completable import io.reactivex.Flowable import io.reactivex.Maybe @@ -19,20 +18,20 @@ import org.mobilenativefoundation.store.store5.SourceOfTruth * @param deleteAll function for deleting all records in the source of truth * */ -fun SourceOfTruth.Companion.ofMaybe( +fun SourceOfTruth.Companion.ofMaybe( reader: (Key) -> Maybe, writer: (Key, Local) -> Completable, delete: ((Key) -> Completable)? = null, - deleteAll: (() -> Completable)? = null + deleteAll: (() -> Completable)? = null, ): SourceOfTruth { val deleteFun: (suspend (Key) -> Unit)? = - if (delete != null) { key -> delete(key).await() } else null + if (delete != null) { key -> delete(key).await() } else null val deleteAllFun: (suspend () -> Unit)? = deleteAll?.let { { deleteAll().await() } } return of( - nonFlowReader = { key -> reader.invoke(key).awaitSingleOrNull() }, - writer = { key, output -> writer.invoke(key, output).await() }, - delete = deleteFun, - deleteAll = deleteAllFun + nonFlowReader = { key -> reader.invoke(key).awaitSingleOrNull() }, + writer = { key, output -> writer.invoke(key, output).await() }, + delete = deleteFun, + deleteAll = deleteAllFun, ) } @@ -46,19 +45,19 @@ fun SourceOfTruth.Companion.ofMaybe( * @param deleteAll function for deleting all records in the source of truth * */ -fun SourceOfTruth.Companion.ofFlowable( - reader: (Key) -> Flowable, - writer: (Key, Local) -> Completable, - delete: ((Key) -> Completable)? = null, - deleteAll: (() -> Completable)? = null +fun SourceOfTruth.Companion.ofFlowable( + reader: (Key) -> Flowable, + writer: (Key, Local) -> Completable, + delete: ((Key) -> Completable)? = null, + deleteAll: (() -> Completable)? = null, ): SourceOfTruth { val deleteFun: (suspend (Key) -> Unit)? = - if (delete != null) { key -> delete(key).await() } else null + if (delete != null) { key -> delete(key).await() } else null val deleteAllFun: (suspend () -> Unit)? = deleteAll?.let { { deleteAll().await() } } return of( - reader = { key -> reader.invoke(key).asFlow() }, - writer = { key, output -> writer.invoke(key, output).await() }, - delete = deleteFun, - deleteAll = deleteAllFun + reader = { key -> reader.invoke(key).asFlow() }, + writer = { key, output -> writer.invoke(key, output).await() }, + delete = deleteFun, + deleteAll = deleteAllFun, ) -} \ No newline at end of file +} diff --git a/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxStore.kt b/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxStore.kt index e84ef78b4..b411961bb 100644 --- a/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxStore.kt +++ b/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxStore.kt @@ -1,6 +1,5 @@ package org.mobilenativefoundation.store.rx2 - import io.reactivex.Completable import io.reactivex.Flowable import kotlinx.coroutines.rx2.asFlowable @@ -19,15 +18,14 @@ import org.mobilenativefoundation.store.store5.impl.extensions.get * @param request - see [StoreReadRequest] for configurations */ fun Store.observe(request: StoreReadRequest): Flowable> = - stream(request).asFlowable() + stream(request).asFlowable() /** * Purge a particular entry from memory and disk cache. * Persistent storage will only be cleared if a delete function was passed to * [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store]. */ -fun Store.observeClear(key: Key): Completable = - rxCompletable { clear(key) } +fun Store.observeClear(key: Key): Completable = rxCompletable { clear(key) } /** * Purge all entries from memory and disk cache. @@ -35,8 +33,7 @@ fun Store.observeClear(key: Key): Complet * [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store]. */ @ExperimentalStoreApi -fun Store.observeClearAll(): Completable = - rxCompletable { clear() } +fun Store.observeClearAll(): Completable = rxCompletable { clear() } /** * Helper factory that will return data as a [Single] for [key] if it is cached otherwise will return fresh/network data (updating your caches) diff --git a/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxStoreBuilder.kt b/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxStoreBuilder.kt index f22914729..5d378c3d4 100644 --- a/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxStoreBuilder.kt +++ b/rx2/src/main/kotlin/org/mobilenativefoundation/store/rx2/RxStoreBuilder.kt @@ -19,8 +19,6 @@ import org.mobilenativefoundation.store.store5.StoreBuilder * @param scheduler - scheduler to use for sharing * if a scheduler is not set Store will use [GlobalScope] */ -fun StoreBuilder.withScheduler( - scheduler: Scheduler -): StoreBuilder { +fun StoreBuilder.withScheduler(scheduler: Scheduler): StoreBuilder { return scope(CoroutineScope(scheduler.asCoroutineDispatcher())) -} \ No newline at end of file +} diff --git a/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/FlowTestExt.kt b/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/FlowTestExt.kt index e4bd601a3..422460532 100644 --- a/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/FlowTestExt.kt +++ b/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/FlowTestExt.kt @@ -16,8 +16,6 @@ package org.mobilenativefoundation.store.rx2.test * limitations under the License. */ - - import com.google.common.truth.FailureMetadata import com.google.common.truth.Subject import com.google.common.truth.Truth @@ -36,9 +34,9 @@ internal fun TestCoroutineScope.assertThat(flow: Flow): FlowSubject { @OptIn(ExperimentalCoroutinesApi::class) internal class FlowSubject constructor( - failureMetadata: FailureMetadata, - private val testCoroutineScope: TestCoroutineScope, - private val actual: Flow + failureMetadata: FailureMetadata, + private val testCoroutineScope: TestCoroutineScope, + private val actual: Flow, ) : Subject(failureMetadata, actual) { /** * Takes all items in the flow that are available by collecting on it as long as there are @@ -49,16 +47,17 @@ internal class FlowSubject constructor( */ suspend fun emitsExactly(vararg expected: T) { val collectedSoFar = mutableListOf() - val collectionCoroutine = testCoroutineScope.async { - actual.collect { - collectedSoFar.add(it) - if (collectedSoFar.size > expected.size) { - assertWithMessage("Too many emissions in the flow (only first additional item is shown)") + val collectionCoroutine = + testCoroutineScope.async { + actual.collect { + collectedSoFar.add(it) + if (collectedSoFar.size > expected.size) { + assertWithMessage("Too many emissions in the flow (only first additional item is shown)") .that(collectedSoFar) .isEqualTo(expected) + } } } - } testCoroutineScope.advanceUntilIdle() if (!collectionCoroutine.isActive) { collectionCoroutine.getCompletionExceptionOrNull()?.let { @@ -67,19 +66,22 @@ internal class FlowSubject constructor( } collectionCoroutine.cancelAndJoin() assertWithMessage("Flow didn't exactly emit expected items") - .that(collectedSoFar) - .isEqualTo(expected.toList()) + .that(collectedSoFar) + .isEqualTo(expected.toList()) } class Factory( - private val testCoroutineScope: TestCoroutineScope + private val testCoroutineScope: TestCoroutineScope, ) : Subject.Factory, Flow> { - override fun createSubject(metadata: FailureMetadata, actual: Flow): FlowSubject { + override fun createSubject( + metadata: FailureMetadata, + actual: Flow, + ): FlowSubject { return FlowSubject( - failureMetadata = metadata, - actual = actual, - testCoroutineScope = testCoroutineScope + failureMetadata = metadata, + actual = actual, + testCoroutineScope = testCoroutineScope, ) } } -} \ No newline at end of file +} diff --git a/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/HotRxSingleStoreTest.kt b/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/HotRxSingleStoreTest.kt index c63e5e801..3cb617ddf 100644 --- a/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/HotRxSingleStoreTest.kt +++ b/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/HotRxSingleStoreTest.kt @@ -25,49 +25,51 @@ class HotRxSingleStoreTest { @Test fun `GIVEN a hot fetcher WHEN two cached and one fresh call THEN fetcher is only called twice`() = - testScope.runBlockingTest { - val fetcher: FakeRxFetcher> = FakeRxFetcher( - 3 to FetcherResult.Data("three-1"), - 3 to FetcherResult.Data("three-2") + testScope.runBlockingTest { + val fetcher: FakeRxFetcher> = + FakeRxFetcher( + 3 to FetcherResult.Data("three-1"), + 3 to FetcherResult.Data("three-2"), ) - val pipeline = StoreBuilder.from(Fetcher.ofResultSingle { fetcher.fetch(it) }) - .scope(testScope) - .build() + val pipeline = + StoreBuilder.from(Fetcher.ofResultSingle { fetcher.fetch(it) }) + .scope(testScope) + .build() - assertThat(pipeline.stream(StoreReadRequest.cached(3, refresh = false))) - .emitsExactly( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - StoreReadResponse.Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() - ) - ) - assertThat( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)) - ).emitsExactly( - StoreReadResponse.Data( - value = "three-1", - origin = StoreReadResponseOrigin.Cache - ) + assertThat(pipeline.stream(StoreReadRequest.cached(3, refresh = false))) + .emitsExactly( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), ) + assertThat( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)), + ).emitsExactly( + StoreReadResponse.Data( + value = "three-1", + origin = StoreReadResponseOrigin.Cache, + ), + ) - assertThat(pipeline.stream(StoreReadRequest.fresh(3))) - .emitsExactly( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - StoreReadResponse.Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) - ) - } + assertThat(pipeline.stream(StoreReadRequest.fresh(3))) + .emitsExactly( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), + ) + } } class FakeRxFetcher( - vararg val responses: Pair + vararg val responses: Pair, ) { private var index = 0 @@ -81,4 +83,4 @@ class FakeRxFetcher( assertThat(pair.first).isEqualTo(key) return Single.just(pair.second) } -} \ No newline at end of file +} diff --git a/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxFlowableStoreTest.kt b/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxFlowableStoreTest.kt index 392980e30..d7ec5d998 100644 --- a/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxFlowableStoreTest.kt +++ b/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxFlowableStoreTest.kt @@ -29,82 +29,90 @@ class RxFlowableStoreTest { private val testScheduler = TestScheduler() private val atomicInteger = AtomicInteger(0) private val fakeDisk = mutableMapOf() - private val store = StoreBuilder.from( - fetcher = Fetcher.ofResultFlowable { - Flowable.create( - { emitter -> - emitter.onNext( - FetcherResult.Data("$it ${atomicInteger.incrementAndGet()} occurrence") + private val store = + StoreBuilder.from( + fetcher = + Fetcher.ofResultFlowable { + Flowable.create( + { emitter -> + emitter.onNext( + FetcherResult.Data("$it ${atomicInteger.incrementAndGet()} occurrence"), + ) + emitter.onNext( + FetcherResult.Data("$it ${atomicInteger.incrementAndGet()} occurrence"), + ) + emitter.onComplete() + }, + BackpressureStrategy.BUFFER, ) - emitter.onNext( - FetcherResult.Data("$it ${atomicInteger.incrementAndGet()} occurrence") - ) - emitter.onComplete() }, - BackpressureStrategy.BUFFER - ) - }, - sourceOfTruth = SourceOfTruth.ofFlowable( - reader = { - if (fakeDisk[it] != null) - Flowable.fromCallable { fakeDisk[it]!! } - else - Flowable.empty() - }, - writer = { key, value -> - Completable.fromAction { fakeDisk[key] = value } - } + sourceOfTruth = + SourceOfTruth.ofFlowable( + reader = { + if (fakeDisk[it] != null) { + Flowable.fromCallable { fakeDisk[it]!! } + } else { + Flowable.empty() + } + }, + writer = { key, value -> + Completable.fromAction { fakeDisk[key] = value } + }, + ), ) - ) - .withScheduler(testScheduler) - .build() + .withScheduler(testScheduler) + .build() @Test fun simpleTest() { - val testSubscriber1 = store.observe(StoreReadRequest.fresh(3)) - .subscribeOn(testScheduler) - .test() + val testSubscriber1 = + store.observe(StoreReadRequest.fresh(3)) + .subscribeOn(testScheduler) + .test() testScheduler.triggerActions() testSubscriber1 .awaitCount(3) .assertValues( StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), StoreReadResponse.Data("3 1 occurrence", StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Data("3 2 occurrence", StoreReadResponseOrigin.Fetcher()) + StoreReadResponse.Data("3 2 occurrence", StoreReadResponseOrigin.Fetcher()), ) - val testSubscriber2 = store.observe(StoreReadRequest.cached(3, false)) - .subscribeOn(testScheduler) - .test() + val testSubscriber2 = + store.observe(StoreReadRequest.cached(3, false)) + .subscribeOn(testScheduler) + .test() testScheduler.triggerActions() testSubscriber2 .awaitCount(2) .assertValues( StoreReadResponse.Data("3 2 occurrence", StoreReadResponseOrigin.Cache), - StoreReadResponse.Data("3 2 occurrence", StoreReadResponseOrigin.SourceOfTruth) + StoreReadResponse.Data("3 2 occurrence", StoreReadResponseOrigin.SourceOfTruth), ) - val testSubscriber3 = store.observe(StoreReadRequest.fresh(3)) - .subscribeOn(testScheduler) - .test() + val testSubscriber3 = + store.observe(StoreReadRequest.fresh(3)) + .subscribeOn(testScheduler) + .test() testScheduler.triggerActions() testSubscriber3 .awaitCount(3) .assertValues( StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), StoreReadResponse.Data("3 3 occurrence", StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Data("3 4 occurrence", StoreReadResponseOrigin.Fetcher()) + StoreReadResponse.Data("3 4 occurrence", StoreReadResponseOrigin.Fetcher()), ) - val testSubscriber4 = store.observe(StoreReadRequest.cached(3, false)) - .subscribeOn(testScheduler) - .test() + val testSubscriber4 = + store.observe(StoreReadRequest.cached(3, false)) + .subscribeOn(testScheduler) + .test() testScheduler.triggerActions() testSubscriber4 .awaitCount(2) .assertValues( StoreReadResponse.Data("3 4 occurrence", StoreReadResponseOrigin.Cache), - StoreReadResponse.Data("3 4 occurrence", StoreReadResponseOrigin.SourceOfTruth) + StoreReadResponse.Data("3 4 occurrence", StoreReadResponseOrigin.SourceOfTruth), ) } -} \ No newline at end of file +} diff --git a/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxSingleStoreExtensionsTest.kt b/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxSingleStoreExtensionsTest.kt index e70b4d9b2..6ddee0df1 100644 --- a/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxSingleStoreExtensionsTest.kt +++ b/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxSingleStoreExtensionsTest.kt @@ -9,12 +9,12 @@ import kotlinx.coroutines.FlowPreview import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.rx2.freshSingle import org.mobilenativefoundation.store.rx2.getSingle import org.mobilenativefoundation.store.rx2.ofMaybe import org.mobilenativefoundation.store.rx2.ofResultSingle import org.mobilenativefoundation.store.rx2.withScheduler -import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.store5.Fetcher import org.mobilenativefoundation.store.store5.FetcherResult import org.mobilenativefoundation.store.store5.SourceOfTruth @@ -30,21 +30,23 @@ class RxSingleStoreExtensionsTest { private var fakeDisk = mutableMapOf() private val store = StoreBuilder.from( - fetcher = Fetcher.ofResultSingle { - Single.fromCallable { FetcherResult.Data("$it ${atomicInteger.incrementAndGet()}") } - }, - sourceOfTruth = SourceOfTruth.ofMaybe( - reader = { Maybe.fromCallable { fakeDisk[it] } }, - writer = { key, value -> - Completable.fromAction { fakeDisk[key] = value } - }, - delete = { key -> - Completable.fromAction { fakeDisk.remove(key) } + fetcher = + Fetcher.ofResultSingle { + Single.fromCallable { FetcherResult.Data("$it ${atomicInteger.incrementAndGet()}") } }, - deleteAll = { - Completable.fromAction { fakeDisk.clear() } - } - ) + sourceOfTruth = + SourceOfTruth.ofMaybe( + reader = { Maybe.fromCallable { fakeDisk[it] } }, + writer = { key, value -> + Completable.fromAction { fakeDisk[key] = value } + }, + delete = { key -> + Completable.fromAction { fakeDisk.remove(key) } + }, + deleteAll = { + Completable.fromAction { fakeDisk.clear() } + }, + ), ) .withScheduler(Schedulers.trampoline()) .build() @@ -75,4 +77,4 @@ class RxSingleStoreExtensionsTest { .await() .assertValue("3 2") } -} \ No newline at end of file +} diff --git a/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxSingleStoreTest.kt b/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxSingleStoreTest.kt index 894c4503a..6276514de 100644 --- a/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxSingleStoreTest.kt +++ b/rx2/src/test/kotlin/org/mobilenativefoundation/store/rx2/test/RxSingleStoreTest.kt @@ -9,13 +9,13 @@ import kotlinx.coroutines.FlowPreview import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.rx2.observe import org.mobilenativefoundation.store.rx2.observeClear import org.mobilenativefoundation.store.rx2.observeClearAll import org.mobilenativefoundation.store.rx2.ofMaybe import org.mobilenativefoundation.store.rx2.ofResultSingle import org.mobilenativefoundation.store.rx2.withScheduler -import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.store5.Fetcher import org.mobilenativefoundation.store.store5.FetcherResult import org.mobilenativefoundation.store.store5.SourceOfTruth @@ -34,21 +34,23 @@ class RxSingleStoreTest { private var fakeDisk = mutableMapOf() private val store = StoreBuilder.from( - fetcher = Fetcher.ofResultSingle { - Single.fromCallable { FetcherResult.Data("$it ${atomicInteger.incrementAndGet()}") } - }, - sourceOfTruth = SourceOfTruth.ofMaybe( - reader = { Maybe.fromCallable { fakeDisk[it] } }, - writer = { key, value -> - Completable.fromAction { fakeDisk[key] = value } + fetcher = + Fetcher.ofResultSingle { + Single.fromCallable { FetcherResult.Data("$it ${atomicInteger.incrementAndGet()}") } }, - delete = { key -> - Completable.fromAction { fakeDisk.remove(key) } - }, - deleteAll = { - Completable.fromAction { fakeDisk.clear() } - } - ) + sourceOfTruth = + SourceOfTruth.ofMaybe( + reader = { Maybe.fromCallable { fakeDisk[it] } }, + writer = { key, value -> + Completable.fromAction { fakeDisk[key] = value } + }, + delete = { key -> + Completable.fromAction { fakeDisk.remove(key) } + }, + deleteAll = { + Completable.fromAction { fakeDisk.clear() } + }, + ), ) .withScheduler(Schedulers.trampoline()) .build() @@ -60,7 +62,7 @@ class RxSingleStoreTest { .awaitCount(2) .assertValues( StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Data("3 1", StoreReadResponseOrigin.Fetcher()) + StoreReadResponse.Data("3 1", StoreReadResponseOrigin.Fetcher()), ) store.observe(StoreReadRequest.cached(3, false)) @@ -68,7 +70,7 @@ class RxSingleStoreTest { .awaitCount(2) .assertValues( StoreReadResponse.Data("3 1", StoreReadResponseOrigin.Cache), - StoreReadResponse.Data("3 1", StoreReadResponseOrigin.SourceOfTruth) + StoreReadResponse.Data("3 1", StoreReadResponseOrigin.SourceOfTruth), ) store.observe(StoreReadRequest.fresh(3)) @@ -76,7 +78,7 @@ class RxSingleStoreTest { .awaitCount(2) .assertValues( StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Data("3 2", StoreReadResponseOrigin.Fetcher()) + StoreReadResponse.Data("3 2", StoreReadResponseOrigin.Fetcher()), ) store.observe(StoreReadRequest.cached(3, false)) @@ -84,7 +86,7 @@ class RxSingleStoreTest { .awaitCount(2) .assertValues( StoreReadResponse.Data("3 2", StoreReadResponseOrigin.Cache), - StoreReadResponse.Data("3 2", StoreReadResponseOrigin.SourceOfTruth) + StoreReadResponse.Data("3 2", StoreReadResponseOrigin.SourceOfTruth), ) } @@ -99,7 +101,7 @@ class RxSingleStoreTest { .awaitCount(2) .assertValues( StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Data("3 1", StoreReadResponseOrigin.Fetcher()) + StoreReadResponse.Data("3 1", StoreReadResponseOrigin.Fetcher()), ) } @@ -115,7 +117,7 @@ class RxSingleStoreTest { .awaitCount(2) .assertValues( StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Data("3 1", StoreReadResponseOrigin.Fetcher()) + StoreReadResponse.Data("3 1", StoreReadResponseOrigin.Fetcher()), ) store.observe(StoreReadRequest.cached(4, false)) @@ -123,7 +125,7 @@ class RxSingleStoreTest { .awaitCount(2) .assertValues( StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Data("4 2", StoreReadResponseOrigin.Fetcher()) + StoreReadResponse.Data("4 2", StoreReadResponseOrigin.Fetcher()), ) } -} \ No newline at end of file +} diff --git a/store/build.gradle.kts b/store/build.gradle.kts index 11a3bd4ae..6724973d6 100644 --- a/store/build.gradle.kts +++ b/store/build.gradle.kts @@ -16,10 +16,6 @@ plugins { id("kotlinx-atomicfu") } -rootProject.plugins.withType { - rootProject.the().nodeVersion = "16.13.1" -} - kotlin { android() jvm() @@ -75,7 +71,7 @@ kotlin { } } - jvmToolchain(11) + jvmToolchain(17) } android { @@ -97,8 +93,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } @@ -106,7 +102,7 @@ tasks.withType().configureEach { dokkaSourceSets.configureEach { reportUndocumented.set(false) skipDeprecated.set(true) - jdkVersion.set(11) + jdkVersion.set(17) } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Bookkeeper.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Bookkeeper.kt index d147315b8..35b3e42db 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Bookkeeper.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Bookkeeper.kt @@ -11,8 +11,14 @@ import org.mobilenativefoundation.store.store5.impl.extensions.now interface Bookkeeper { suspend fun getLastFailedSync(key: Key): Long? - suspend fun setLastFailedSync(key: Key, timestamp: Long = now()): Boolean + + suspend fun setLastFailedSync( + key: Key, + timestamp: Long = now(), + ): Boolean + suspend fun clear(key: Key): Boolean + suspend fun clearAll(): Boolean companion object { @@ -20,7 +26,7 @@ interface Bookkeeper { getLastFailedSync: suspend (key: Key) -> Long?, setLastFailedSync: suspend (key: Key, timestamp: Long) -> Boolean, clear: suspend (key: Key) -> Boolean, - clearAll: suspend () -> Boolean + clearAll: suspend () -> Boolean, ): Bookkeeper = RealBookkeeper(getLastFailedSync, setLastFailedSync, clear, clearAll) } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Converter.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Converter.kt index 2215319ad..b6bbe74a3 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Converter.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Converter.kt @@ -11,15 +11,14 @@ package org.mobilenativefoundation.store.store5 */ interface Converter { fun fromNetworkToLocal(network: Network): Local + fun fromOutputToLocal(output: Output): Local class Builder { - lateinit var fromOutputToLocal: ((output: Output) -> Local) lateinit var fromNetworkToLocal: ((network: Network) -> Local) - fun build(): Converter = - RealConverter(fromOutputToLocal, fromNetworkToLocal) + fun build(): Converter = RealConverter(fromOutputToLocal, fromNetworkToLocal) fun fromOutputToLocal(converter: (output: Output) -> Local): Builder { fromOutputToLocal = converter @@ -37,9 +36,7 @@ private class RealConverter( private val fromOutputToLocal: ((output: Output) -> Local), private val fromNetworkToLocal: ((network: Network) -> Local), ) : Converter { - override fun fromNetworkToLocal(network: Network): Local = - fromNetworkToLocal.invoke(network) + override fun fromNetworkToLocal(network: Network): Local = fromNetworkToLocal.invoke(network) - override fun fromOutputToLocal(output: Output): Local = - fromOutputToLocal.invoke(output) + override fun fromOutputToLocal(output: Output): Local = fromOutputToLocal.invoke(output) } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Fetcher.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Fetcher.kt index 2ba75f5fe..940e1ee20 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Fetcher.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Fetcher.kt @@ -44,9 +44,8 @@ interface Fetcher { * * @param flowFactory a factory for a [Flow]ing source of network records. */ - fun ofResultFlow( - flowFactory: (Key) -> Flow> - ): Fetcher = FactoryFetcher(factory = flowFactory) + fun ofResultFlow(flowFactory: (Key) -> Flow>): Fetcher = + FactoryFetcher(factory = flowFactory) /** * Creates a [Fetcher] with a [fallback] from a [flowFactory]. @@ -56,7 +55,7 @@ interface Fetcher { fun ofResultFlowWithFallback( name: String, flowFactory: (Key) -> Flow>, - fallback: Fetcher + fallback: Fetcher, ): Fetcher = FactoryFetcherWithFallback(name = name, factory = flowFactory, fallback = fallback) /** @@ -70,9 +69,8 @@ interface Fetcher { * * @param fetch a source of network records. */ - fun ofResult( - fetch: suspend (Key) -> FetcherResult - ): Fetcher = ofResultFlow(fetch.asFlow()) + fun ofResult(fetch: suspend (Key) -> FetcherResult): Fetcher = + ofResultFlow(fetch.asFlow()) /** * Creates a [Fetcher] with a [fallback] from a non-Flow source. @@ -82,7 +80,7 @@ interface Fetcher { fun ofResultWithFallback( name: String, fetch: suspend (Key) -> FetcherResult, - fallback: Fetcher + fallback: Fetcher, ): Fetcher = ofResultFlowWithFallback(name, fetch.asFlow(), fallback) /** @@ -99,12 +97,13 @@ interface Fetcher { */ fun ofFlow( name: String? = null, - flowFactory: (Key) -> Flow - ): Fetcher = FactoryFetcher { key: Key -> - flowFactory(key) - .map> { FetcherResult.Data(it, name) } - .catch { throwable: Throwable -> emit(FetcherResult.Error.Exception(throwable)) } - } + flowFactory: (Key) -> Flow, + ): Fetcher = + FactoryFetcher { key: Key -> + flowFactory(key) + .map> { FetcherResult.Data(it, name) } + .catch { throwable: Throwable -> emit(FetcherResult.Error.Exception(throwable)) } + } /** * Creates a [Fetcher] with a [fallback] from a [flowFactory]. @@ -115,13 +114,14 @@ interface Fetcher { name: String, fallback: Fetcher, flowFactory: (Key) -> Flow, - ): Fetcher = FactoryFetcherWithFallback(name = name, factory = { key: Key -> - flowFactory(key) - .map> { - FetcherResult.Data(it, name) - } - .catch { throwable: Throwable -> emit(FetcherResult.Error.Exception(throwable)) } - }, fallback = fallback) + ): Fetcher = + FactoryFetcherWithFallback(name = name, factory = { key: Key -> + flowFactory(key) + .map> { + FetcherResult.Data(it, name) + } + .catch { throwable: Throwable -> emit(FetcherResult.Error.Exception(throwable)) } + }, fallback = fallback) /** * "Creates" a [Fetcher] from a non-[Flow] source and translate the results to a [FetcherResult]. @@ -136,9 +136,8 @@ interface Fetcher { */ fun of( name: String? = null, - fetch: suspend (key: Key) -> Network - ): Fetcher = - ofFlow(name, fetch.asFlow()) + fetch: suspend (key: Key) -> Network, + ): Fetcher = ofFlow(name, fetch.asFlow()) /** * Creates a [Fetcher] with a [fallback] from a non-Flow source. @@ -148,45 +147,47 @@ interface Fetcher { fun withFallback( name: String, fallback: Fetcher, - fetch: suspend (key: Key) -> Network - ): Fetcher = - ofFlowWithFallback(name, fallback, fetch.asFlow()) + fetch: suspend (key: Key) -> Network, + ): Fetcher = ofFlowWithFallback(name, fallback, fetch.asFlow()) - private fun (suspend (key: Key) -> Network).asFlow() = { key: Key -> - flow { - emit(invoke(key)) + private fun (suspend (key: Key) -> Network).asFlow() = + { key: Key -> + flow { + emit(invoke(key)) + } } - } private class FactoryFetcher( private val factory: (Key) -> Flow>, ) : Fetcher { override val name: String? = null override val fallback: Fetcher? = null + override fun invoke(key: Key): Flow> = factory(key) } private fun tryFetch( key: Key, factory: (Key) -> Flow>, - fallback: Fetcher? - ): Flow> = channelFlow { - factory(key).collect { fetcherResult -> - when (fetcherResult) { - is FetcherResult.Data -> { - send(fetcherResult) - } - - is FetcherResult.Error -> { - if (fallback != null) { - tryFetch(key, fallback::invoke, fallback.fallback).collect { send(it) } - } else { + fallback: Fetcher?, + ): Flow> = + channelFlow { + factory(key).collect { fetcherResult -> + when (fetcherResult) { + is FetcherResult.Data -> { send(fetcherResult) } + + is FetcherResult.Error -> { + if (fallback != null) { + tryFetch(key, fallback::invoke, fallback.fallback).collect { send(it) } + } else { + send(fetcherResult) + } + } } } } - } private class FactoryFetcherWithFallback( override val name: String, diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/FetcherResult.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/FetcherResult.kt index 9b146c594..1160e5416 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/FetcherResult.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/FetcherResult.kt @@ -2,9 +2,12 @@ package org.mobilenativefoundation.store.store5 sealed class FetcherResult { data class Data(val value: Network, val origin: String? = null) : FetcherResult() + sealed class Error : FetcherResult() { data class Exception(val error: Throwable) : Error() + data class Message(val message: String) : Error() + data class Custom(val error: E) : Error() } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MemoryPolicy.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MemoryPolicy.kt index db2ce5d3b..fe1e816f4 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MemoryPolicy.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MemoryPolicy.kt @@ -10,11 +10,17 @@ fun interface Weigher { * * @return the weight of the entry; must be non-negative */ - fun weigh(key: K, value: V): Int + fun weigh( + key: K, + value: V, + ): Int } internal object OneWeigher : Weigher { - override fun weigh(key: Any, value: Any): Int = 1 + override fun weigh( + key: Any, + value: Any, + ): Int = 1 } /** @@ -27,9 +33,8 @@ class MemoryPolicy internal constructor( val expireAfterAccess: Duration, val maxSize: Long, val maxWeight: Long, - val weigher: Weigher + val weigher: Weigher, ) { - val isDefaultWritePolicy: Boolean = expireAfterWrite == DEFAULT_DURATION_POLICY val hasWritePolicy: Boolean = expireAfterWrite != DEFAULT_DURATION_POLICY @@ -70,40 +75,42 @@ class MemoryPolicy internal constructor( * * If not set, cache size will be unlimited. */ - fun setMaxSize(maxSize: Long): MemoryPolicyBuilder = apply { - check(maxWeight == DEFAULT_SIZE_POLICY && weigher == OneWeigher) { - "Cannot setMaxSize when maxWeight or weigher are already set" + fun setMaxSize(maxSize: Long): MemoryPolicyBuilder = + apply { + check(maxWeight == DEFAULT_SIZE_POLICY && weigher == OneWeigher) { + "Cannot setMaxSize when maxWeight or weigher are already set" + } + check(maxSize >= 0) { "maxSize cannot be negative" } + this.maxSize = maxSize } - check(maxSize >= 0) { "maxSize cannot be negative" } - this.maxSize = maxSize - } fun setWeigherAndMaxWeight( weigher: Weigher, - maxWeight: Long - ): MemoryPolicyBuilder = apply { - check(maxSize == DEFAULT_SIZE_POLICY) { - "Cannot setWeigherAndMaxWeight when maxSize already set" + maxWeight: Long, + ): MemoryPolicyBuilder = + apply { + check(maxSize == DEFAULT_SIZE_POLICY) { + "Cannot setWeigherAndMaxWeight when maxSize already set" + } + check(maxWeight >= 0) { "maxWeight cannot be negative" } + this.weigher = weigher + this.maxWeight = maxWeight } - check(maxWeight >= 0) { "maxWeight cannot be negative" } - this.weigher = weigher - this.maxWeight = maxWeight - } - - fun build() = MemoryPolicy( - expireAfterWrite = expireAfterWrite, - expireAfterAccess = expireAfterAccess, - maxSize = maxSize, - maxWeight = maxWeight, - weigher = weigher - ) + + fun build() = + MemoryPolicy( + expireAfterWrite = expireAfterWrite, + expireAfterAccess = expireAfterAccess, + maxSize = maxSize, + maxWeight = maxWeight, + weigher = weigher, + ) } companion object { val DEFAULT_DURATION_POLICY: Duration = Duration.INFINITE const val DEFAULT_SIZE_POLICY: Long = -1 - fun builder(): MemoryPolicyBuilder = - MemoryPolicyBuilder() + fun builder(): MemoryPolicyBuilder = MemoryPolicyBuilder() } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MutableStoreBuilder.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MutableStoreBuilder.kt index bcea1d6f5..21ec51c89 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MutableStoreBuilder.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MutableStoreBuilder.kt @@ -4,10 +4,9 @@ import kotlinx.coroutines.CoroutineScope import org.mobilenativefoundation.store.store5.impl.mutableStoreBuilderFromFetcherAndSourceOfTruth interface MutableStoreBuilder { - fun build( updater: Updater, - bookkeeper: Bookkeeper? = null + bookkeeper: Bookkeeper? = null, ): MutableStore /** @@ -31,7 +30,7 @@ interface MutableStoreBuilder - fun validator(validator: Validator): MutableStoreBuilder + fun validator(validator: Validator): MutableStoreBuilder companion object { /** @@ -40,14 +39,15 @@ interface MutableStoreBuilder from( + fun from( fetcher: Fetcher, sourceOfTruth: SourceOfTruth, - converter: Converter + converter: Converter, ): MutableStoreBuilder = mutableStoreBuilderFromFetcherAndSourceOfTruth( - fetcher = fetcher, sourceOfTruth = sourceOfTruth, - converter = converter + fetcher = fetcher, + sourceOfTruth = sourceOfTruth, + converter = converter, ) } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/OnFetcherCompletion.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/OnFetcherCompletion.kt index 1aac9e437..4ad18f3a1 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/OnFetcherCompletion.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/OnFetcherCompletion.kt @@ -2,5 +2,5 @@ package org.mobilenativefoundation.store.store5 data class OnFetcherCompletion( val onSuccess: (FetcherResult.Data) -> Unit, - val onFailure: (FetcherResult.Error) -> Unit + val onFailure: (FetcherResult.Error) -> Unit, ) diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/OnUpdaterCompletion.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/OnUpdaterCompletion.kt index feae16502..84c8544c5 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/OnUpdaterCompletion.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/OnUpdaterCompletion.kt @@ -2,5 +2,5 @@ package org.mobilenativefoundation.store.store5 data class OnUpdaterCompletion( val onSuccess: (UpdaterResult.Success) -> Unit, - val onFailure: (UpdaterResult.Error) -> Unit + val onFailure: (UpdaterResult.Error) -> Unit, ) diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruth.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruth.kt index 079cc60a3..711fd0c91 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruth.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruth.kt @@ -47,7 +47,6 @@ import kotlin.jvm.JvmName * */ interface SourceOfTruth { - /** * Used by [Store] to read records from the source of truth. * @@ -66,7 +65,10 @@ interface SourceOfTruth { * * @param key The key to update for. */ - suspend fun write(key: Key, value: Local) + suspend fun write( + key: Key, + value: Local, + ) /** * Used by [Store] to delete records in the source of truth for the given key. @@ -94,13 +96,14 @@ interface SourceOfTruth { nonFlowReader: suspend (Key) -> Output?, writer: suspend (Key, Local) -> Unit, delete: (suspend (Key) -> Unit)? = null, - deleteAll: (suspend () -> Unit)? = null - ): SourceOfTruth = PersistentNonFlowingSourceOfTruth( - realReader = nonFlowReader, - realWriter = writer, - realDelete = delete, - realDeleteAll = deleteAll - ) + deleteAll: (suspend () -> Unit)? = null, + ): SourceOfTruth = + PersistentNonFlowingSourceOfTruth( + realReader = nonFlowReader, + realWriter = writer, + realDelete = delete, + realDeleteAll = deleteAll, + ) /** * Creates a ([Flow]) source of truth that is accessed via [reader], [writer], @@ -116,25 +119,28 @@ interface SourceOfTruth { reader: (Key) -> Flow, writer: suspend (Key, Local) -> Unit, delete: (suspend (Key) -> Unit)? = null, - deleteAll: (suspend () -> Unit)? = null - ): SourceOfTruth = PersistentSourceOfTruth( - realReader = reader, - realWriter = writer, - realDelete = delete, - realDeleteAll = deleteAll - ) + deleteAll: (suspend () -> Unit)? = null, + ): SourceOfTruth = + PersistentSourceOfTruth( + realReader = reader, + realWriter = writer, + realDelete = delete, + realDeleteAll = deleteAll, + ) } /** * The exception provided when a write operation fails in SourceOfTruth. * - * see [StoreReadResponse.Error.Exception] + * @see [StoreReadResponse.Error.Exception] */ class WriteException( /** * The key for the failed write attempt + * + * TODO(yboyar): why are we not marking keys non-null ? */ - val key: Any?, // TODO why are we not marking keys non-null ? + val key: Any?, /** * The value for the failed write attempt */ @@ -142,11 +148,11 @@ interface SourceOfTruth { /** * The exception thrown from the [SourceOfTruth]'s [write] method. */ - cause: Throwable + cause: Throwable, ) : RuntimeException( - "Failed to write value to Source of Truth. key: $key", - cause - ) { + "Failed to write value to Source of Truth. key: $key", + cause, + ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false @@ -174,13 +180,15 @@ interface SourceOfTruth { class ReadException( /** * The key for the failed write attempt + * + * TODO(yboyar): shouldn't key be non-null? */ - val key: Any?, // TODO shouldn't key be non-null? - cause: Throwable + val key: Any?, + cause: Throwable, ) : RuntimeException( - "Failed to read from Source of Truth. key: $key", - cause - ) { + "Failed to read from Source of Truth. key: $key", + cause, + ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreBuilder.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreBuilder.kt index d2d959892..f91b94af4 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreBuilder.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreBuilder.kt @@ -28,7 +28,9 @@ import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherSourc interface StoreBuilder { fun build(): Store - fun toMutableStoreBuilder(converter: Converter): MutableStoreBuilder + fun toMutableStoreBuilder( + converter: Converter, + ): MutableStoreBuilder /** * A store multicasts same [Output] value to many consumers (Similar to RxJava.share()), by default @@ -59,9 +61,8 @@ interface StoreBuilder { * * @param fetcher a [Fetcher] flow of network records. */ - fun from( - fetcher: Fetcher, - ): StoreBuilder = storeBuilderFromFetcher(fetcher = fetcher) + fun from(fetcher: Fetcher): StoreBuilder = + storeBuilderFromFetcher(fetcher = fetcher) /** * Creates a new [StoreBuilder] from a [Fetcher] and a [SourceOfTruth]. @@ -71,41 +72,43 @@ interface StoreBuilder { */ fun from( fetcher: Fetcher, - sourceOfTruth: SourceOfTruth - ): StoreBuilder = - storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth) + sourceOfTruth: SourceOfTruth, + ): StoreBuilder = storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth) fun from( fetcher: Fetcher, sourceOfTruth: SourceOfTruth, memoryCache: Cache, - ): StoreBuilder = storeBuilderFromFetcherSourceOfTruthAndMemoryCache( - fetcher, - sourceOfTruth, - memoryCache - ) + ): StoreBuilder = + storeBuilderFromFetcherSourceOfTruthAndMemoryCache( + fetcher, + sourceOfTruth, + memoryCache, + ) fun from( fetcher: Fetcher, sourceOfTruth: SourceOfTruth, - converter: Converter - ): StoreBuilder = storeBuilderFromFetcherSourceOfTruthMemoryCacheAndConverter( - fetcher, - sourceOfTruth, - null, - converter - ) + converter: Converter, + ): StoreBuilder = + storeBuilderFromFetcherSourceOfTruthMemoryCacheAndConverter( + fetcher, + sourceOfTruth, + null, + converter, + ) fun from( fetcher: Fetcher, sourceOfTruth: SourceOfTruth, memoryCache: Cache, - converter: Converter - ): StoreBuilder = storeBuilderFromFetcherSourceOfTruthMemoryCacheAndConverter( - fetcher, - sourceOfTruth, - memoryCache, - converter - ) + converter: Converter, + ): StoreBuilder = + storeBuilderFromFetcherSourceOfTruthMemoryCacheAndConverter( + fetcher, + sourceOfTruth, + memoryCache, + converter, + ) } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreDefaults.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreDefaults.kt index 9a1011854..341f3e579 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreDefaults.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreDefaults.kt @@ -4,7 +4,6 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.hours internal object StoreDefaults { - /** * Cache TTL (default is 24 hours), can be overridden * @@ -19,8 +18,9 @@ internal object StoreDefaults { */ val cacheSize: Long = 100 - val memoryPolicy = MemoryPolicy.builder() - .setMaxSize(cacheSize) - .setExpireAfterWrite(cacheTTL) - .build() + val memoryPolicy = + MemoryPolicy.builder() + .setMaxSize(cacheSize) + .setExpireAfterWrite(cacheTTL) + .build() } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadRequest.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadRequest.kt index 8cf581348..d31fea5c4 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadRequest.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadRequest.kt @@ -28,18 +28,18 @@ data class StoreReadRequest private constructor( private val skippedCaches: Int, val refresh: Boolean = false, val fallBackToSourceOfTruth: Boolean = false, - val fetch: Boolean = true + val fetch: Boolean = true, ) { - internal fun shouldSkipCache(type: CacheType) = skippedCaches.and(type.flag) != 0 /** * Factories for common store requests */ companion object { - private val allCaches = CacheType.values().fold(0) { prev, next -> - prev.or(next.flag) - } + private val allCaches = + CacheType.values().fold(0) { prev, next -> + prev.or(next.flag) + } /** * Create a [StoreReadRequest] which will skip all caches and hit your fetcher @@ -50,11 +50,14 @@ data class StoreReadRequest private constructor( * data **even** if you explicitly requested fresh data. * See https://github.com/dropbox/Store/pull/194 for context. */ - fun fresh(key: Key, fallBackToSourceOfTruth: Boolean = false) = StoreReadRequest( + fun fresh( + key: Key, + fallBackToSourceOfTruth: Boolean = false, + ) = StoreReadRequest( key = key, skippedCaches = allCaches, refresh = true, - fallBackToSourceOfTruth = fallBackToSourceOfTruth + fallBackToSourceOfTruth = fallBackToSourceOfTruth, ) /** @@ -62,30 +65,37 @@ data class StoreReadRequest private constructor( * otherwise will hit your fetcher (filling your caches). * @param refresh if true then return fetcher (new) data as well (updating your caches) */ - fun cached(key: Key, refresh: Boolean) = StoreReadRequest( + fun cached( + key: Key, + refresh: Boolean, + ) = StoreReadRequest( key = key, skippedCaches = 0, - refresh = refresh + refresh = refresh, ) /** * Create a [StoreReadRequest] which will return data from memory/disk caches if present, * otherwise will return [StoreReadResponse.NoNewData] */ - fun localOnly(key: Key) = StoreReadRequest( - key = key, - skippedCaches = 0, - fetch = false - ) + fun localOnly(key: Key) = + StoreReadRequest( + key = key, + skippedCaches = 0, + fetch = false, + ) /** * Create a [StoreReadRequest] which will return data from disk cache * @param refresh if true then return fetcher (new) data as well (updating your caches) */ - fun skipMemory(key: Key, refresh: Boolean) = StoreReadRequest( + fun skipMemory( + key: Key, + refresh: Boolean, + ) = StoreReadRequest( key = key, skippedCaches = CacheType.MEMORY.flag, - refresh = refresh + refresh = refresh, ) /** @@ -97,5 +107,5 @@ data class StoreReadRequest private constructor( internal enum class CacheType(internal val flag: Int) { MEMORY(0b01), - DISK(0b10) + DISK(0b10), } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt index 97f0531a1..95453f4c7 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt @@ -55,17 +55,17 @@ sealed class StoreReadResponse { sealed class Error : StoreReadResponse() { data class Exception( val error: Throwable, - override val origin: StoreReadResponseOrigin + override val origin: StoreReadResponseOrigin, ) : Error() data class Message( val message: String, - override val origin: StoreReadResponseOrigin + override val origin: StoreReadResponseOrigin, ) : Error() data class Custom( val error: E, - override val origin: StoreReadResponseOrigin + override val origin: StoreReadResponseOrigin, ) : Error() } @@ -105,10 +105,11 @@ sealed class StoreReadResponse { /** * If there is data available, returns it; otherwise returns null. */ - fun dataOrNull(): Output? = when (this) { - is Data -> value - else -> null - } + fun dataOrNull(): Output? = + when (this) { + is Data -> value + else -> null + } private fun errorOrNull(): Throwable? { if (this is Error.Exception) { @@ -131,13 +132,14 @@ sealed class StoreReadResponse { } @Suppress("UNCHECKED_CAST") - internal fun swapType(): StoreReadResponse = when (this) { - is Error -> this - is Loading -> this - is NoNewData -> this - is Data -> throw RuntimeException("cannot swap type for StoreResponse.Data") - is Initial -> this - } + internal fun swapType(): StoreReadResponse = + when (this) { + is Error -> this + is Loading -> this + is NoNewData -> this + is Data -> throw RuntimeException("cannot swap type for StoreResponse.Data") + is Initial -> this + } } /** @@ -163,14 +165,15 @@ sealed class StoreReadResponseOrigin { object Initial : StoreReadResponseOrigin() } -fun StoreReadResponse.Error.doThrow(): Nothing = when (this) { - is StoreReadResponse.Error.Exception -> throw error - is StoreReadResponse.Error.Message -> throw RuntimeException(message) - is StoreReadResponse.Error.Custom<*> -> { - if (error is Throwable) { - throw error - } else { - throw RuntimeException("Non-throwable custom error: $error") +fun StoreReadResponse.Error.doThrow(): Nothing = + when (this) { + is StoreReadResponse.Error.Exception -> throw error + is StoreReadResponse.Error.Message -> throw RuntimeException(message) + is StoreReadResponse.Error.Custom<*> -> { + if (error is Throwable) { + throw error + } else { + throw RuntimeException("Non-throwable custom error: $error") + } } } -} diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreWriteResponse.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreWriteResponse.kt index a2878fb05..d0e6e3844 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreWriteResponse.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreWriteResponse.kt @@ -3,11 +3,13 @@ package org.mobilenativefoundation.store.store5 sealed class StoreWriteResponse { sealed class Success : StoreWriteResponse() { data class Typed(val value: Response) : Success() + data class Untyped(val value: Any) : Success() } sealed class Error : StoreWriteResponse() { data class Exception(val error: Throwable) : Error() + data class Message(val message: String) : Error() } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Updater.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Updater.kt index 04b9b2e25..8913ba619 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Updater.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Updater.kt @@ -10,7 +10,10 @@ interface Updater { /** * Makes HTTP POST request. */ - suspend fun post(key: Key, value: Output): UpdaterResult + suspend fun post( + key: Key, + value: Output, + ): UpdaterResult /** * Executes on network completion. @@ -21,9 +24,11 @@ interface Updater { fun by( post: PostRequest, onCompletion: OnUpdaterCompletion? = null, - ): Updater = RealNetworkUpdater( - post, onCompletion - ) + ): Updater = + RealNetworkUpdater( + post, + onCompletion, + ) } } @@ -31,5 +36,8 @@ internal class RealNetworkUpdater( private val realPost: PostRequest, override val onCompletion: OnUpdaterCompletion?, ) : Updater { - override suspend fun post(key: Key, value: Output): UpdaterResult = realPost(key, value) + override suspend fun post( + key: Key, + value: Output, + ): UpdaterResult = realPost(key, value) } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/UpdaterResult.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/UpdaterResult.kt index 388709c41..2d6a77614 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/UpdaterResult.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/UpdaterResult.kt @@ -1,14 +1,15 @@ package org.mobilenativefoundation.store.store5 sealed class UpdaterResult { - sealed class Success : UpdaterResult() { data class Typed(val value: Response) : Success() + data class Untyped(val value: Any) : Success() } sealed class Error : UpdaterResult() { data class Exception(val error: Throwable) : Error() + data class Message(val message: String) : Error() } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Validator.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Validator.kt index a74e6c59b..274764b12 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Validator.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Validator.kt @@ -15,8 +15,6 @@ interface Validator { suspend fun isValid(item: Output): Boolean companion object { - fun by( - validator: suspend (item: Output) -> Boolean - ): Validator = RealValidator(validator) + fun by(validator: suspend (item: Output) -> Boolean): Validator = RealValidator(validator) } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Write.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Write.kt index 497daec91..9b07fa3f1 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Write.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Write.kt @@ -6,6 +6,7 @@ import org.mobilenativefoundation.store.core5.ExperimentalStoreApi interface Write { @ExperimentalStoreApi suspend fun write(request: StoreWriteRequest): StoreWriteResponse + interface Stream { @ExperimentalStoreApi fun stream(requestStream: Flow>): Flow diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/FetcherController.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/FetcherController.kt index 1e5a4138d..ae2fcc208 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/FetcherController.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/FetcherController.kt @@ -55,73 +55,79 @@ internal class FetcherController?, + private val converter: Converter = + object : + Converter { + override fun fromNetworkToLocal(network: Network): Local { + return network as Local + } - private val converter: Converter = object : - Converter { - - override fun fromNetworkToLocal(network: Network): Local { - return network as Local - } - - override fun fromOutputToLocal(output: Output): Local { - throw IllegalStateException("Not used") - } - } + override fun fromOutputToLocal(output: Output): Local { + throw IllegalStateException("Not used") + } + }, ) { @Suppress("USELESS_CAST", "UNCHECKED_CAST") // needed for multicaster source - private val fetchers = RefCountedResource( - create = { key: Key -> - Multicaster( - scope = scope, - bufferSize = 0, - source = flow { emitAll(realFetcher(key)) }.map { - when (it) { - is FetcherResult.Data -> { - StoreReadResponse.Data( - it.value, - origin = StoreReadResponseOrigin.Fetcher(it.origin) - ) as StoreReadResponse - } + private val fetchers = + RefCountedResource( + create = { key: Key -> + Multicaster( + scope = scope, + bufferSize = 0, + source = + flow { emitAll(realFetcher(key)) }.map { + when (it) { + is FetcherResult.Data -> { + StoreReadResponse.Data( + it.value, + origin = StoreReadResponseOrigin.Fetcher(it.origin), + ) as StoreReadResponse + } - is FetcherResult.Error.Message -> StoreReadResponse.Error.Message( - it.message, - origin = StoreReadResponseOrigin.Fetcher() - ) + is FetcherResult.Error.Message -> + StoreReadResponse.Error.Message( + it.message, + origin = StoreReadResponseOrigin.Fetcher(), + ) - is FetcherResult.Error.Exception -> StoreReadResponse.Error.Exception( - it.error, - origin = StoreReadResponseOrigin.Fetcher() - ) - is FetcherResult.Error.Custom<*> -> StoreReadResponse.Error.Custom( - it.error, - StoreReadResponseOrigin.Fetcher() - ) - } - }.onEmpty { - val origin = - StoreReadResponseOrigin.Fetcher() - emit(StoreReadResponse.NoNewData(origin)) - }, - /** - * When enabled, downstream collectors are never closed, instead, they are kept active to - * receive values dispatched by fetchers created after them. This makes [FetcherController] - * act like a [SourceOfTruth] in the lack of a [SourceOfTruth] provided by the developer. - */ - piggybackingDownstream = true, - onEach = { response -> - response.dataOrNull()?.let { network: Network -> - val local: Local = converter.fromNetworkToLocal(network) - sourceOfTruth?.write(key, local) - } - } - ) - }, - onRelease = { _: Key, multicaster: Multicaster> -> - multicaster.close() - } - ) + is FetcherResult.Error.Exception -> + StoreReadResponse.Error.Exception( + it.error, + origin = StoreReadResponseOrigin.Fetcher(), + ) + + is FetcherResult.Error.Custom<*> -> + StoreReadResponse.Error.Custom( + it.error, + StoreReadResponseOrigin.Fetcher(), + ) + } + }.onEmpty { + val origin = + StoreReadResponseOrigin.Fetcher() + emit(StoreReadResponse.NoNewData(origin)) + }, + // When enabled, downstream collectors are never closed, instead, they are kept active to + // receive values dispatched by fetchers created after them. This makes [FetcherController] + // act like a [SourceOfTruth] in the lack of a [SourceOfTruth] provided by the developer. + piggybackingDownstream = true, + onEach = { response -> + response.dataOrNull()?.let { network: Network -> + val local: Local = converter.fromNetworkToLocal(network) + sourceOfTruth?.write(key, local) + } + }, + ) + }, + onRelease = { _: Key, multicaster: Multicaster> -> + multicaster.close() + }, + ) - fun getFetcher(key: Key, piggybackOnly: Boolean = false): Flow> { + fun getFetcher( + key: Key, + piggybackOnly: Boolean = false, + ): Flow> { return flow { val fetcher = acquireFetcher(key) try { @@ -147,9 +153,10 @@ internal class FetcherController Unit, - val onFailure: (StoreWriteResponse.Error) -> Unit + val onFailure: (StoreWriteResponse.Error) -> Unit, ) diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealBookkeeper.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealBookkeeper.kt index 7729f0840..c0ff2dfef 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealBookkeeper.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealBookkeeper.kt @@ -7,11 +7,14 @@ internal class RealBookkeeper( private val realGetLastFailedSync: suspend (key: Key) -> Timestamp?, private val realSetLastFailedSync: suspend (key: Key, timestamp: Timestamp) -> Boolean, private val realClear: suspend (key: Key) -> Boolean, - private val realClearAll: suspend () -> Boolean + private val realClearAll: suspend () -> Boolean, ) : Bookkeeper { override suspend fun getLastFailedSync(key: Key): Long? = realGetLastFailedSync(key) - override suspend fun setLastFailedSync(key: Key, timestamp: Long): Boolean = realSetLastFailedSync(key, timestamp) + override suspend fun setLastFailedSync( + key: Key, + timestamp: Long, + ): Boolean = realSetLastFailedSync(key, timestamp) override suspend fun clear(key: Key): Boolean = realClear(key) 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 2abd69f98..f5dbb54f9 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 @@ -11,9 +11,9 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.store5.Bookkeeper import org.mobilenativefoundation.store.store5.Clear -import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.store5.MutableStore import org.mobilenativefoundation.store.store5.StoreReadRequest import org.mobilenativefoundation.store.store5.StoreReadResponse @@ -33,7 +33,6 @@ internal class RealMutableStore, private val bookkeeper: Bookkeeper?, ) : MutableStore, Clear.Key by delegate, Clear.All by delegate { - private val storeLock = Mutex() private val keyToWriteRequestQueue = mutableMapOf>() private val keyToThreadSafety = mutableMapOf() @@ -72,25 +71,26 @@ internal class RealMutableStore - val storeWriteResponse = try { - delegate.write(writeRequest.key, writeRequest.value) - 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) + val storeWriteResponse = + try { + delegate.write(writeRequest.key, writeRequest.value) + 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) } - } catch (throwable: Throwable) { - StoreWriteResponse.Error.Exception(throwable) - } emit(storeWriteResponse) } } @@ -106,7 +106,7 @@ internal class RealMutableStore( key = request.key, created = request.created, - updaterResult = updaterResult + updaterResult = updaterResult, ) bookkeeper?.clear(request.key) } else { @@ -134,36 +134,42 @@ internal class RealMutableStore updateWriteRequestQueue(key: Key, created: Long, updaterResult: UpdaterResult.Success) { - val nextWriteRequestQueue = withWriteRequestQueueLock(key) { - val outstandingWriteRequests = ArrayDeque>() - - for (writeRequest in this) { - if (writeRequest.created <= created) { - updater.onCompletion?.onSuccess?.invoke(updaterResult) - - val storeWriteResponse = when (updaterResult) { - is UpdaterResult.Success.Typed<*> -> { - val typedValue = updaterResult.value as? Response - if (typedValue == null) { - StoreWriteResponse.Success.Untyped(updaterResult.value) - } else { - StoreWriteResponse.Success.Typed(updaterResult.value) - } - } + private suspend fun updateWriteRequestQueue( + key: Key, + created: Long, + updaterResult: UpdaterResult.Success, + ) { + val nextWriteRequestQueue = + withWriteRequestQueueLock(key) { + val outstandingWriteRequests = ArrayDeque>() + + for (writeRequest in this) { + if (writeRequest.created <= created) { + updater.onCompletion?.onSuccess?.invoke(updaterResult) + + val storeWriteResponse = + when (updaterResult) { + 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) + } - writeRequest.onCompletions?.forEach { onStoreWriteCompletion -> - onStoreWriteCompletion.onSuccess(storeWriteResponse) + writeRequest.onCompletions?.forEach { onStoreWriteCompletion -> + onStoreWriteCompletion.onSuccess(storeWriteResponse) + } + } else { + outstandingWriteRequests.add(writeRequest) } - } else { - outstandingWriteRequests.add(writeRequest) } + outstandingWriteRequests } - outstandingWriteRequests - } withThreadSafety(key) { keyToWriteRequestQueue[key] = nextWriteRequestQueue @@ -173,7 +179,7 @@ internal class RealMutableStore withWriteRequestQueueLock( key: Key, - block: suspend WriteRequestQueue.() -> Result + block: suspend WriteRequestQueue.() -> Result, ): Result = withThreadSafety(key) { writeRequests.lightswitch.lock(writeRequests.mutex) @@ -183,15 +189,19 @@ internal class RealMutableStore = withThreadSafety(key) { - writeRequests.mutex.lock() - val output = requireNotNull(keyToWriteRequestQueue[key]?.last()) - writeRequests.mutex.unlock() - output - } + private suspend fun getLatestWriteRequest(key: Key): StoreWriteRequest = + withThreadSafety(key) { + writeRequests.mutex.lock() + val output = requireNotNull(keyToWriteRequestQueue[key]?.last()) + writeRequests.mutex.unlock() + output + } @AnyThread - private suspend fun withThreadSafety(key: Key, block: suspend ThreadSafety.() -> Output): Output { + private suspend fun withThreadSafety( + key: Key, + block: suspend ThreadSafety.() -> Output, + ): Output { storeLock.lock() val threadSafety = requireNotNull(keyToThreadSafety[key]) val output = threadSafety.block() @@ -219,11 +229,12 @@ internal class RealMutableStore EagerConflictResolutionResult.Success.NoConflicts else -> { try { - val updaterResult = updater.post(key, latest).also { updaterResult -> - if (updaterResult is UpdaterResult.Success) { - updateWriteRequestQueue(key = key, created = now(), updaterResult = updaterResult) + val updaterResult = + updater.post(key, latest).also { updaterResult -> + if (updaterResult is UpdaterResult.Success) { + updateWriteRequestQueue(key = key, created = now(), updaterResult = updaterResult) + } } - } when (updaterResult) { is UpdaterResult.Error.Exception -> EagerConflictResolutionResult.Error.Exception(updaterResult.error) @@ -237,17 +248,19 @@ internal class RealMutableStore mutableStoreBuilderFromFetcher( fetcher: Fetcher, - converter: Converter -): MutableStoreBuilder = - RealMutableStoreBuilder(fetcher, converter = converter) + converter: Converter, +): MutableStoreBuilder = RealMutableStoreBuilder(fetcher, converter = converter) fun mutableStoreBuilderFromFetcherAndSourceOfTruth( fetcher: Fetcher, sourceOfTruth: SourceOfTruth, - converter: Converter -): MutableStoreBuilder = - RealMutableStoreBuilder(fetcher, sourceOfTruth, converter = converter) + converter: Converter, +): MutableStoreBuilder = RealMutableStoreBuilder(fetcher, sourceOfTruth, converter = converter) fun mutableStoreBuilderFromFetcherSourceOfTruthAndMemoryCache( fetcher: Fetcher, sourceOfTruth: SourceOfTruth, memoryCache: Cache, converter: Converter, -): MutableStoreBuilder = - RealMutableStoreBuilder(fetcher, sourceOfTruth, memoryCache, converter = converter) +): MutableStoreBuilder = RealMutableStoreBuilder(fetcher, sourceOfTruth, memoryCache, converter = converter) internal class RealMutableStoreBuilder( private val fetcher: Fetcher, private val sourceOfTruth: SourceOfTruth? = null, private val memoryCache: Cache? = null, - private val converter: Converter + private val converter: Converter, ) : MutableStoreBuilder { private var scope: CoroutineScope? = null private var cachePolicy: MemoryPolicy? = StoreDefaults.memoryPolicy @@ -69,42 +66,44 @@ internal class RealMutableStoreBuilder = RealStore( - scope = scope ?: GlobalScope, - sourceOfTruth = sourceOfTruth, - fetcher = fetcher, - converter = converter, - validator = validator, - memCache = memoryCache ?: cachePolicy?.let { - CacheBuilder().apply { - if (cachePolicy!!.hasAccessPolicy) { - expireAfterAccess(cachePolicy!!.expireAfterAccess) - } - if (cachePolicy!!.hasWritePolicy) { - expireAfterWrite(cachePolicy!!.expireAfterWrite) - } - if (cachePolicy!!.hasMaxSize) { - maximumSize(cachePolicy!!.maxSize) - } + fun build(): Store = + RealStore( + scope = scope ?: GlobalScope, + sourceOfTruth = sourceOfTruth, + fetcher = fetcher, + converter = converter, + validator = validator, + memCache = + memoryCache ?: cachePolicy?.let { + CacheBuilder().apply { + if (cachePolicy!!.hasAccessPolicy) { + expireAfterAccess(cachePolicy!!.expireAfterAccess) + } + if (cachePolicy!!.hasWritePolicy) { + expireAfterWrite(cachePolicy!!.expireAfterWrite) + } + if (cachePolicy!!.hasMaxSize) { + maximumSize(cachePolicy!!.maxSize) + } - if (cachePolicy!!.hasMaxWeight) { - weigher(cachePolicy!!.maxWeight) { key, value -> - cachePolicy!!.weigher.weigh( - key, - value - ) - } - } - }.build() - } - ) + if (cachePolicy!!.hasMaxWeight) { + weigher(cachePolicy!!.maxWeight) { key, value -> + cachePolicy!!.weigher.weigh( + key, + value, + ) + } + } + }.build() + }, + ) override fun build( updater: Updater, - bookkeeper: Bookkeeper? + bookkeeper: Bookkeeper?, ): MutableStore = build().asMutableStore( updater = updater, - bookkeeper = bookkeeper + bookkeeper = bookkeeper, ) } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealSourceOfTruth.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealSourceOfTruth.kt index ca2dadaf2..2c50aa6b7 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealSourceOfTruth.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealSourceOfTruth.kt @@ -23,12 +23,14 @@ internal class PersistentSourceOfTruth( private val realReader: (Key) -> Flow, private val realWriter: suspend (Key, Local) -> Unit, private val realDelete: (suspend (Key) -> Unit)? = null, - private val realDeleteAll: (suspend () -> Unit)? = null + private val realDeleteAll: (suspend () -> Unit)? = null, ) : SourceOfTruth { - override fun reader(key: Key): Flow = realReader.invoke(key) - override suspend fun write(key: Key, value: Local) = realWriter(key, value) + override suspend fun write( + key: Key, + value: Local, + ) = realWriter(key, value) override suspend fun delete(key: Key) { realDelete?.invoke(key) @@ -43,16 +45,18 @@ internal class PersistentNonFlowingSourceOfTruth Output?, private val realWriter: suspend (Key, Local) -> Unit, private val realDelete: (suspend (Key) -> Unit)? = null, - private val realDeleteAll: (suspend () -> Unit)? + private val realDeleteAll: (suspend () -> Unit)?, ) : SourceOfTruth { - override fun reader(key: Key): Flow = flow { val sot = realReader(key) emit(sot) } - override suspend fun write(key: Key, value: Local) { + override suspend fun write( + key: Key, + value: Local, + ) { return realWriter(key, value) } 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 c335b9155..fc1b4d186 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 @@ -28,9 +28,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.transform import org.mobilenativefoundation.store.cache5.Cache +import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.store5.CacheType import org.mobilenativefoundation.store.store5.Converter -import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.store5.Fetcher import org.mobilenativefoundation.store.store5.SourceOfTruth import org.mobilenativefoundation.store.store5.Store @@ -48,7 +48,7 @@ internal class RealStore( sourceOfTruth: SourceOfTruth? = null, private val converter: Converter, private val validator: Validator?, - private val memCache: Cache? + private val memCache: Cache?, ) : Store { /** * This source of truth is either a real database or an in memory source of truth created by @@ -66,26 +66,28 @@ internal class RealStore( * Fetcher controller maintains 1 and only 1 `Multicaster` for a given key to ensure network * requests are shared. */ - private val fetcherController = FetcherController( - scope = scope, - realFetcher = fetcher, - sourceOfTruth = this.sourceOfTruth, - converter = converter - ) + private val fetcherController = + FetcherController( + scope = scope, + realFetcher = fetcher, + sourceOfTruth = this.sourceOfTruth, + converter = converter, + ) @Suppress("UNCHECKED_CAST") override fun stream(request: StoreReadRequest): Flow> = flow { - val cachedToEmit = if (request.shouldSkipCache(CacheType.MEMORY)) { - null - } else { - val output: Output? = memCache?.getIfPresent(request.key) - val isInvalid = output != null && validator?.isValid(output) == false - when { - output == null || isInvalid -> null - else -> output + val cachedToEmit = + if (request.shouldSkipCache(CacheType.MEMORY)) { + null + } else { + val output: Output? = memCache?.getIfPresent(request.key) + val isInvalid = output != null && validator?.isValid(output) == false + when { + output == null || isInvalid -> null + else -> output + } } - } cachedToEmit?.let { it: Output -> // if we read a value from cache, dispatch it first @@ -100,30 +102,31 @@ internal class RealStore( return@flow } - val stream: Flow> = if (sourceOfTruth == null) { - // piggypack only if not specified fresh data AND we emitted a value from the cache - val piggybackOnly = !request.refresh && cachedToEmit != null - @Suppress("UNCHECKED_CAST") + val stream: Flow> = + if (sourceOfTruth == null) { + // piggypack only if not specified fresh data AND we emitted a value from the cache + val piggybackOnly = !request.refresh && cachedToEmit != null + @Suppress("UNCHECKED_CAST") - createNetworkFlow( - request = request, - networkLock = null, - piggybackOnly = piggybackOnly - ) as Flow> // when no source of truth Input == Output - } else if (request.fetch) { - diskNetworkCombined(request, sourceOfTruth) - } else { - val diskLock = CompletableDeferred() - diskLock.complete(Unit) - sourceOfTruth.reader(request.key, diskLock).transform { response -> - val data = response.dataOrNull() - if (data == null || validator?.isValid(data) == false) { - emit(StoreReadResponse.NoNewData(origin = response.origin)) - } else { - emit(StoreReadResponse.Data(value = data, origin = response.origin)) + createNetworkFlow( + request = request, + networkLock = null, + piggybackOnly = piggybackOnly, + ) as Flow> // when no source of truth Input == Output + } else if (request.fetch) { + diskNetworkCombined(request, sourceOfTruth) + } else { + val diskLock = CompletableDeferred() + diskLock.complete(Unit) + sourceOfTruth.reader(request.key, diskLock).transform { response -> + val data = response.dataOrNull() + if (data == null || validator?.isValid(data) == false) { + emit(StoreReadResponse.NoNewData(origin = response.origin)) + } else { + emit(StoreReadResponse.Data(value = data, origin = response.origin)) + } } } - } emitAll( stream.transform { output: StoreReadResponse -> emit(output) @@ -148,12 +151,12 @@ internal class RealStore( emit( StoreReadResponse.Data( value = it, - origin = StoreReadResponseOrigin.Cache - ) + origin = StoreReadResponseOrigin.Cache, + ), ) } } - } + }, ) }.onEach { // whenever a value is dispatched, save it to the memory cache @@ -201,7 +204,7 @@ internal class RealStore( */ private fun diskNetworkCombined( request: StoreReadRequest, - sourceOfTruth: SourceOfTruthWithBarrier + sourceOfTruth: SourceOfTruthWithBarrier, ): Flow> { val diskLock = CompletableDeferred() val networkLock = CompletableDeferred() @@ -210,13 +213,14 @@ internal class RealStore( if (!skipDiskCache) { diskLock.complete(Unit) } - val diskFlow = sourceOfTruth.reader(request.key, diskLock).onStart { - // wait for disk to latch first to ensure it happens before network triggers. - // after that, if we'll not read from disk, then allow network to continue - if (skipDiskCache) { - networkLock.complete(Unit) + val diskFlow = + sourceOfTruth.reader(request.key, diskLock).onStart { + // wait for disk to latch first to ensure it happens before network triggers. + // after that, if we'll not read from disk, then allow network to continue + if (skipDiskCache) { + networkLock.complete(Unit) + } } - } val requestKeyToFetcherName: MutableMap = mutableMapOf() // we use a merge implementation that gives the source of the flow so that we can decide @@ -250,18 +254,20 @@ internal class RealStore( // right, that is data from disk when (val diskData = it.value) { is StoreReadResponse.Data -> { - val responseOriginWithFetcherName = diskData.origin.let { origin -> - if (origin is StoreReadResponseOrigin.Fetcher) { - origin.copy(name = requestKeyToFetcherName[request.key]) - } else { - origin + val responseOriginWithFetcherName = + diskData.origin.let { origin -> + if (origin is StoreReadResponseOrigin.Fetcher) { + origin.copy(name = requestKeyToFetcherName[request.key]) + } else { + origin + } } - } val diskValue = diskData.value - val isValid = (validator == null && diskValue != null) || - diskData.origin is StoreReadResponseOrigin.Fetcher || - (diskValue != null && validator?.isValid(diskValue) ?: true) + val isValid = + (validator == null && diskValue != null) || + diskData.origin is StoreReadResponseOrigin.Fetcher || + (diskValue != null && validator?.isValid(diskValue) ?: true) if (isValid) { @Suppress("UNCHECKED_CAST") @@ -296,7 +302,8 @@ internal class RealStore( is StoreReadResponse.Initial, is StoreReadResponse.Loading, - is StoreReadResponse.NoNewData -> { + is StoreReadResponse.NoNewData, + -> { } } } @@ -307,7 +314,7 @@ internal class RealStore( private fun createNetworkFlow( request: StoreReadRequest, networkLock: CompletableDeferred?, - piggybackOnly: Boolean = false + piggybackOnly: Boolean = false, ): Flow> { return fetcherController .getFetcher(request.key, piggybackOnly) @@ -320,16 +327,19 @@ internal class RealStore( } } - internal suspend fun write(key: Key, value: Output): StoreDelegateWriteResult = try { - memCache?.put(key, value) - sourceOfTruth?.write(key, converter.fromOutputToLocal(value)) - StoreDelegateWriteResult.Success - } catch (error: Throwable) { - StoreDelegateWriteResult.Error.Exception(error) - } + internal suspend fun write( + key: Key, + value: Output, + ): StoreDelegateWriteResult = + try { + memCache?.put(key, value) + sourceOfTruth?.write(key, converter.fromOutputToLocal(value)) + StoreDelegateWriteResult.Success + } catch (error: Throwable) { + StoreDelegateWriteResult.Error.Exception(error) + } - internal suspend fun latestOrNull(key: Key): Output? = - fromMemCache(key) ?: fromSourceOfTruth(key) + internal suspend fun latestOrNull(key: Key): Output? = fromMemCache(key) ?: fromSourceOfTruth(key) private suspend fun fromSourceOfTruth(key: Key) = sourceOfTruth?.reader(key, CompletableDeferred(Unit))?.map { it.dataOrNull() }?.first() @@ -337,9 +347,10 @@ internal class RealStore( private fun fromMemCache(key: Key) = memCache?.getIfPresent(key) companion object { - private val logger = Logger.apply { - setLogWriters(listOf(CommonWriter())) - setTag("Store") - } + private val logger = + Logger.apply { + setLogWriters(listOf(CommonWriter())) + setTag("Store") + } } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreBuilder.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreBuilder.kt index b85f25941..7c6528431 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreBuilder.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreBuilder.kt @@ -19,21 +19,18 @@ import org.mobilenativefoundation.store.store5.Validator fun storeBuilderFromFetcher( fetcher: Fetcher, sourceOfTruth: SourceOfTruth? = null, -): StoreBuilder = - RealStoreBuilder(fetcher, sourceOfTruth) +): StoreBuilder = RealStoreBuilder(fetcher, sourceOfTruth) fun storeBuilderFromFetcherAndSourceOfTruth( fetcher: Fetcher, sourceOfTruth: SourceOfTruth, -): StoreBuilder = - RealStoreBuilder(fetcher, sourceOfTruth) +): StoreBuilder = RealStoreBuilder(fetcher, sourceOfTruth) fun storeBuilderFromFetcherSourceOfTruthAndMemoryCache( fetcher: Fetcher, sourceOfTruth: SourceOfTruth, memoryCache: Cache, -): StoreBuilder = - RealStoreBuilder(fetcher, sourceOfTruth, memoryCache) +): StoreBuilder = RealStoreBuilder(fetcher, sourceOfTruth, memoryCache) fun storeBuilderFromFetcherSourceOfTruthMemoryCacheAndConverter( fetcher: Fetcher, @@ -46,7 +43,7 @@ internal class RealStoreBuilder, private val sourceOfTruth: SourceOfTruth? = null, private val memoryCache: Cache? = null, - private val converter: Converter? = null + private val converter: Converter? = null, ) : StoreBuilder { private var scope: CoroutineScope? = null private var cachePolicy: MemoryPolicy? = StoreDefaults.memoryPolicy @@ -72,37 +69,41 @@ internal class RealStoreBuilder = RealStore( - scope = scope ?: GlobalScope, - sourceOfTruth = sourceOfTruth, - fetcher = fetcher, - converter = converter ?: defaultConverter(), - validator = validator, - memCache = memoryCache ?: cachePolicy?.let { - CacheBuilder().apply { - if (cachePolicy!!.hasAccessPolicy) { - expireAfterAccess(cachePolicy!!.expireAfterAccess) - } - if (cachePolicy!!.hasWritePolicy) { - expireAfterWrite(cachePolicy!!.expireAfterWrite) - } - if (cachePolicy!!.hasMaxSize) { - maximumSize(cachePolicy!!.maxSize) - } - - if (cachePolicy!!.hasMaxWeight) { - weigher(cachePolicy!!.maxWeight) { key, value -> - cachePolicy!!.weigher.weigh( - key, - value - ) - } - } - }.build() - } - ) - - override fun toMutableStoreBuilder(converter: Converter): MutableStoreBuilder { + override fun build(): Store = + RealStore( + scope = scope ?: GlobalScope, + sourceOfTruth = sourceOfTruth, + fetcher = fetcher, + converter = converter ?: defaultConverter(), + validator = validator, + memCache = + memoryCache ?: cachePolicy?.let { + CacheBuilder().apply { + if (cachePolicy!!.hasAccessPolicy) { + expireAfterAccess(cachePolicy!!.expireAfterAccess) + } + if (cachePolicy!!.hasWritePolicy) { + expireAfterWrite(cachePolicy!!.expireAfterWrite) + } + if (cachePolicy!!.hasMaxSize) { + maximumSize(cachePolicy!!.maxSize) + } + + if (cachePolicy!!.hasMaxWeight) { + weigher(cachePolicy!!.maxWeight) { key, value -> + cachePolicy!!.weigher.weigh( + key, + value, + ) + } + } + }.build() + }, + ) + + override fun toMutableStoreBuilder( + converter: Converter, + ): MutableStoreBuilder { fetcher as Fetcher return if (sourceOfTruth == null && memoryCache == null) { mutableStoreBuilderFromFetcher(fetcher, converter) @@ -110,14 +111,14 @@ internal class RealStoreBuilder, - converter + converter, ) } else { mutableStoreBuilderFromFetcherSourceOfTruthAndMemoryCache( fetcher, sourceOfTruth as SourceOfTruth, memoryCache, - converter + converter, ) }.apply { if (this@RealStoreBuilder.scope != null) { @@ -134,10 +135,11 @@ internal class RealStoreBuilder defaultConverter() = object : Converter { - override fun fromOutputToLocal(output: Output): Local = - throw IllegalStateException("non mutable store never call this function") + private fun defaultConverter() = + object : Converter { + override fun fromOutputToLocal(output: Output): Local = + throw IllegalStateException("non mutable store never call this function") - override fun fromNetworkToLocal(network: Network): Local = network as Local - } + override fun fromNetworkToLocal(network: Network): Local = network as Local + } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreWriteRequest.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreWriteRequest.kt index 38848d31f..bbf87fc08 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreWriteRequest.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreWriteRequest.kt @@ -6,5 +6,5 @@ data class RealStoreWriteRequest( override val key: Key, override val value: Output, override val created: Long, - override val onCompletions: List? + override val onCompletions: List?, ) : StoreWriteRequest diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealValidator.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealValidator.kt index 4c65127e4..4c956a924 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealValidator.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealValidator.kt @@ -3,7 +3,7 @@ package org.mobilenativefoundation.store.store5.impl import org.mobilenativefoundation.store.store5.Validator internal class RealValidator( - private val realValidator: suspend (item: Output) -> Boolean + private val realValidator: suspend (item: Output) -> Boolean, ) : Validator { override suspend fun isValid(item: Output): Boolean = realValidator(item) } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RefCountedResource.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RefCountedResource.kt index a9ebe75a8..d98473292 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RefCountedResource.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RefCountedResource.kt @@ -23,20 +23,24 @@ import kotlinx.coroutines.sync.withLock */ internal class RefCountedResource( private val create: suspend (Key) -> T, - private val onRelease: (suspend (Key, T) -> Unit)? = null + private val onRelease: (suspend (Key, T) -> Unit)? = null, ) { private val items = mutableMapOf() private val lock = Mutex() - suspend fun acquire(key: Key): T = lock.withLock { - items.getOrPut(key) { - Item(create(key)) - }.also { - it.refCount++ - }.value - } + suspend fun acquire(key: Key): T = + lock.withLock { + items.getOrPut(key) { + Item(create(key)) + }.also { + it.refCount++ + }.value + } - suspend fun release(key: Key, value: T) = lock.withLock { + suspend fun release( + key: Key, + value: T, + ) = lock.withLock { val existing = items[key] check(existing != null && existing.value === value) { "inconsistent release, seems like $value was leaked or never acquired" @@ -49,12 +53,13 @@ internal class RefCountedResource( } // used in tests - suspend fun size() = lock.withLock { - items.size - } + suspend fun size() = + lock.withLock { + items.size + } private inner class Item( val value: T, - var refCount: Int = 0 + var refCount: Int = 0, ) } 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 791816ba6..a0a0953b9 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 @@ -46,11 +46,12 @@ internal class SourceOfTruthWithBarrier>( - create = { - MutableStateFlow(BarrierMsg.Open.INITIAL) - } - ) + private val barriers = + RefCountedResource>( + create = { + MutableStateFlow(BarrierMsg.Open.INITIAL) + }, + ) /** * Each message gets dispatched with a version. This ensures we won't accidentally turn on the @@ -59,7 +60,10 @@ internal class SourceOfTruthWithBarrier): Flow> { + fun reader( + key: Key, + lock: CompletableDeferred, + ): Flow> { return flow { val barrier = barriers.acquire(key) val readerVersion: Long = versionCounter.incrementAndGet() @@ -69,52 +73,55 @@ internal class SourceOfTruthWithBarrier val messageArrivedAfterMe = readerVersion < barrierMessage.version - val writeError = if (messageArrivedAfterMe && barrierMessage is BarrierMsg.Open) { - barrierMessage.writeError - } else { - null - } - val readFlow: Flow> = when (barrierMessage) { - is BarrierMsg.Open -> - delegate.reader(key).mapIndexed { index, local: Output? -> - if (index == 0 && messageArrivedAfterMe) { - val firstMsgOrigin = if (writeError == null) { - // restarted barrier without an error means write succeeded - StoreReadResponseOrigin.Fetcher() + val writeError = + if (messageArrivedAfterMe && barrierMessage is BarrierMsg.Open) { + barrierMessage.writeError + } else { + null + } + val readFlow: Flow> = + when (barrierMessage) { + is BarrierMsg.Open -> + delegate.reader(key).mapIndexed { index, local: Output? -> + if (index == 0 && messageArrivedAfterMe) { + val firstMsgOrigin = + if (writeError == null) { + // restarted barrier without an error means write succeeded + StoreReadResponseOrigin.Fetcher() + } else { + // when a write fails, we still get a new reader because + // we've disabled the previous reader before starting the + // write operation. But since write has failed, we should + // use the SourceOfTruth as the origin + StoreReadResponseOrigin.SourceOfTruth + } + StoreReadResponse.Data( + origin = firstMsgOrigin, + value = local, + ) } else { - // when a write fails, we still get a new reader because - // we've disabled the previous reader before starting the - // write operation. But since write has failed, we should - // use the SourceOfTruth as the origin - StoreReadResponseOrigin.SourceOfTruth + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.SourceOfTruth, + value = local, + ) as StoreReadResponse } - StoreReadResponse.Data( - origin = firstMsgOrigin, - value = local - ) - } else { - - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.SourceOfTruth, - value = local - ) as StoreReadResponse - } - }.catch { throwable -> - this.emit( - StoreReadResponse.Error.Exception( - error = SourceOfTruth.ReadException( - key = key, - cause = throwable.cause ?: throwable + }.catch { throwable -> + this.emit( + StoreReadResponse.Error.Exception( + error = + SourceOfTruth.ReadException( + key = key, + cause = throwable.cause ?: throwable, + ), + origin = StoreReadResponseOrigin.SourceOfTruth, ), - origin = StoreReadResponseOrigin.SourceOfTruth ) - ) - } + } - is BarrierMsg.Blocked -> { - flowOf() + is BarrierMsg.Blocked -> { + flowOf() + } } - } readFlow .onStart { // if we have a pending error, make sure to dispatch it first. @@ -122,12 +129,12 @@ internal class SourceOfTruthWithBarrier Store.fresh(key: Key) = @Suppress("UNCHECKED_CAST") fun Store.asMutableStore( updater: Updater, - bookkeeper: Bookkeeper? + bookkeeper: Bookkeeper?, ): MutableStore { - val delegate = this as? RealStore - ?: throw Exception("MutableStore requires Store to be built using StoreBuilder") + val delegate = + this as? RealStore + ?: throw Exception("MutableStore requires Store to be built using StoreBuilder") return RealMutableStore( delegate = delegate, updater = updater, - bookkeeper = bookkeeper + bookkeeper = bookkeeper, ) } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/operators/MapIndexed.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/operators/MapIndexed.kt index 0ba26c82c..c3644cd0c 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/operators/MapIndexed.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/operators/MapIndexed.kt @@ -19,8 +19,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectIndexed import kotlinx.coroutines.flow.flow -internal inline fun Flow.mapIndexed(crossinline block: (Int, T) -> R) = flow { - collectIndexed { index, value -> - emit(block(index, value)) +internal inline fun Flow.mapIndexed(crossinline block: (Int, T) -> R) = + flow { + collectIndexed { index, value -> + emit(block(index, value)) + } } -} diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/concurrent/ThreadSafety.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/concurrent/ThreadSafety.kt index 1a9370106..aaae49513 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/concurrent/ThreadSafety.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/concurrent/ThreadSafety.kt @@ -4,10 +4,10 @@ import kotlinx.coroutines.sync.Mutex internal data class ThreadSafety( val writeRequests: StoreThreadSafety = StoreThreadSafety(), - val readCompletions: StoreThreadSafety = StoreThreadSafety() + val readCompletions: StoreThreadSafety = StoreThreadSafety(), ) internal data class StoreThreadSafety( val mutex: Mutex = Mutex(), - val lightswitch: Lightswitch = Lightswitch() + val lightswitch: Lightswitch = Lightswitch(), ) diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/result/EagerConflictResolutionResult.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/result/EagerConflictResolutionResult.kt index a8ef1825d..3f49c674e 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/result/EagerConflictResolutionResult.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/result/EagerConflictResolutionResult.kt @@ -3,14 +3,15 @@ package org.mobilenativefoundation.store.store5.internal.result import org.mobilenativefoundation.store.store5.UpdaterResult sealed class EagerConflictResolutionResult { - sealed class Success : EagerConflictResolutionResult() { object NoConflicts : Success() + data class ConflictsResolved(val value: UpdaterResult.Success) : Success() } sealed class Error : EagerConflictResolutionResult() { data class Message(val message: String) : Error() + data class Exception(val error: Throwable) : Error() } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/result/StoreDelegateWriteResult.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/result/StoreDelegateWriteResult.kt index 7e0dfb519..3a760e750 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/result/StoreDelegateWriteResult.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/internal/result/StoreDelegateWriteResult.kt @@ -2,8 +2,10 @@ package org.mobilenativefoundation.store.store5.internal.result sealed class StoreDelegateWriteResult { object Success : StoreDelegateWriteResult() + sealed class Error : StoreDelegateWriteResult() { data class Message(val error: String) : Error() + data class Exception(val error: Throwable) : Error() } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearAllStoreTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearAllStoreTests.kt index 62b0767a2..a871a0665 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearAllStoreTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearAllStoreTests.kt @@ -18,7 +18,6 @@ import kotlin.test.assertNull @ExperimentalCoroutinesApi @ExperimentalStoreApi class ClearAllStoreTests { - private val testScope = TestScope() private val key1 = "key1" @@ -33,126 +32,130 @@ class ClearAllStoreTests { @BeforeTest fun before() { persister = InMemoryPersister() - fetcher = Fetcher.of { key: String -> - when (key) { - key1 -> value1 - key2 -> value2 - else -> throw IllegalStateException("Unknown key") + fetcher = + Fetcher.of { key: String -> + when (key) { + key1 -> value1 + key2 -> value2 + else -> throw IllegalStateException("Unknown key") + } } - } } @Test - fun callingClearAllOnStoreWithPersisterAndNoInMemoryCacheDeletesAllEntriesFromThePersister() = testScope.runTest { - val store = StoreBuilder.from( - fetcher = fetcher, - sourceOfTruth = persister.asSourceOfTruth() - ).scope(testScope) - .disableCache() - .build() + fun callingClearAllOnStoreWithPersisterAndNoInMemoryCacheDeletesAllEntriesFromThePersister() = + testScope.runTest { + val store = + StoreBuilder.from( + fetcher = fetcher, + sourceOfTruth = persister.asSourceOfTruth(), + ).scope(testScope) + .disableCache() + .build() - // should receive data from network first time - val responseOneA = store.getData(key1) - advanceUntilIdle() - assertEquals( - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = value1 - ), - responseOneA - ) - val responseTwoA = store.getData(key2) - advanceUntilIdle() - assertEquals( - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = value2 - ), - responseTwoA - ) - // should receive data from persister - val responseOneB = store.getData(key1) - advanceUntilIdle() - assertEquals( - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.SourceOfTruth, - value = value1 - ), - responseOneB - ) - val responseTwoB = store.getData(key2) - advanceUntilIdle() - assertEquals( - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.SourceOfTruth, - value = value2 - ), - responseTwoB - ) - // clear all entries in store - store.clear() - assertNull(persister.peekEntry(key1)) - assertNull(persister.peekEntry(key2)) + // should receive data from network first time + val responseOneA = store.getData(key1) + advanceUntilIdle() + assertEquals( + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = value1, + ), + responseOneA, + ) + val responseTwoA = store.getData(key2) + advanceUntilIdle() + assertEquals( + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = value2, + ), + responseTwoA, + ) + // should receive data from persister + val responseOneB = store.getData(key1) + advanceUntilIdle() + assertEquals( + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.SourceOfTruth, + value = value1, + ), + responseOneB, + ) + val responseTwoB = store.getData(key2) + advanceUntilIdle() + assertEquals( + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.SourceOfTruth, + value = value2, + ), + responseTwoB, + ) + // clear all entries in store + store.clear() + assertNull(persister.peekEntry(key1)) + assertNull(persister.peekEntry(key2)) - // should fetch data from network again - val responseOneC = store.getData(key1) - advanceUntilIdle() - assertEquals( - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = value1 - ), - responseOneC - ) + // should fetch data from network again + val responseOneC = store.getData(key1) + advanceUntilIdle() + assertEquals( + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = value1, + ), + responseOneC, + ) - val responseTwoC = store.getData(key2) - advanceUntilIdle() - assertEquals( - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = value2 - ), - responseTwoC - ) - } + val responseTwoC = store.getData(key2) + advanceUntilIdle() + assertEquals( + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = value2, + ), + responseTwoC, + ) + } @Test fun callingClearAllOnStoreWithInMemoryCacheAndNoPersisterDeletesAllEntriesFromTheInMemoryCache() = testScope.runTest { - val store = StoreBuilder.from( - fetcher = fetcher - ).scope(testScope).build() + val store = + StoreBuilder.from( + fetcher = fetcher, + ).scope(testScope).build() // should receive data from network first time assertEquals( StoreReadResponse.Data( origin = StoreReadResponseOrigin.Fetcher(), - value = value1 + value = value1, ), - store.getData(key1) + store.getData(key1), ) assertEquals( StoreReadResponse.Data( origin = StoreReadResponseOrigin.Fetcher(), - value = value2 + value = value2, ), - store.getData(key2) + store.getData(key2), ) // should receive data from cache assertEquals( StoreReadResponse.Data( origin = StoreReadResponseOrigin.Cache, - value = value1 + value = value1, ), - store.getData(key1) + store.getData(key1), ) assertEquals( StoreReadResponse.Data( origin = StoreReadResponseOrigin.Cache, - value = value2 + value = value2, ), - store.getData(key2) + store.getData(key2), ) // clear all entries in store @@ -162,16 +165,16 @@ class ClearAllStoreTests { assertEquals( StoreReadResponse.Data( origin = StoreReadResponseOrigin.Fetcher(), - value = value1 + value = value1, ), - store.getData(key1) + store.getData(key1), ) assertEquals( StoreReadResponse.Data( origin = StoreReadResponseOrigin.Fetcher(), - value = value2 + value = value2, ), - store.getData(key2) + store.getData(key2), ) } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearStoreByKeyTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearStoreByKeyTests.kt index c0cb62101..e08556b44 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearStoreByKeyTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearStoreByKeyTests.kt @@ -15,139 +15,145 @@ import kotlin.test.assertNull @FlowPreview @ExperimentalCoroutinesApi class ClearStoreByKeyTests { - private val testScope = TestScope() private val persister = InMemoryPersister() @Test - fun callingClearWithKeyOnStoreWithPersisterWithNoInMemoryCacheDeletesTheEntryAssociatedWithTheKeyFromThePersister() = testScope.runTest { - val key = "key" - val value = 1 - val store = StoreBuilder.from( - fetcher = Fetcher.of { value }, - sourceOfTruth = persister.asSourceOfTruth() - ).scope(testScope) - .disableCache() - .build() - - // should receive data from network first time - assertEquals( - Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = value - ), - store.getData(key) - ) - - // should receive data from persister - assertEquals( - Data( - origin = StoreReadResponseOrigin.SourceOfTruth, - value = value - ), - store.getData(key) - ) - - // clear store entry by key - store.clear(key) - assertNull(persister.peekEntry(key)) - // should fetch data from network again - assertEquals( - Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = value - ), - store.getData(key) - ) - } + fun callingClearWithKeyOnStoreWithPersisterWithNoInMemoryCacheDeletesTheEntryAssociatedWithTheKeyFromThePersister() = + testScope.runTest { + val key = "key" + val value = 1 + val store = + StoreBuilder.from( + fetcher = Fetcher.of { value }, + sourceOfTruth = persister.asSourceOfTruth(), + ).scope(testScope) + .disableCache() + .build() + + // should receive data from network first time + assertEquals( + Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = value, + ), + store.getData(key), + ) + + // should receive data from persister + assertEquals( + Data( + origin = StoreReadResponseOrigin.SourceOfTruth, + value = value, + ), + store.getData(key), + ) + + // clear store entry by key + store.clear(key) + assertNull(persister.peekEntry(key)) + // should fetch data from network again + assertEquals( + Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = value, + ), + store.getData(key), + ) + } @Test - fun callingClearWithKeyOStoreWithInMemoryCacheNoPersisterDeletesTheEntryAssociatedWithTheKeyFromTheInMemoryCache() = testScope.runTest { - val key = "key" - val value = 1 - val store = StoreBuilder.from( - fetcher = Fetcher.of { value } - ).scope(testScope).build() - - // should receive data from network first time - assertEquals( - Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = value - ), - store.getData(key) - ) - - // should receive data from cache - assertEquals( - Data( - origin = StoreReadResponseOrigin.Cache, - value = value - ), - store.getData(key) - ) - - // clear store entry by key - store.clear(key) - - // should fetch data from network again - assertEquals( - Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = value - ), - store.getData(key) - ) - } + fun callingClearWithKeyOStoreWithInMemoryCacheNoPersisterDeletesTheEntryAssociatedWithTheKeyFromTheInMemoryCache() = + testScope.runTest { + val key = "key" + val value = 1 + val store = + StoreBuilder.from( + fetcher = Fetcher.of { value }, + ).scope(testScope).build() + + // should receive data from network first time + assertEquals( + Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = value, + ), + store.getData(key), + ) + + // should receive data from cache + assertEquals( + Data( + origin = StoreReadResponseOrigin.Cache, + value = value, + ), + store.getData(key), + ) + + // clear store entry by key + store.clear(key) + + // should fetch data from network again + assertEquals( + Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = value, + ), + store.getData(key), + ) + } @Test - fun callingClearWithKeyOnStoreHasNoEffectOnExistingEntriesAssociatedWithOtherKeysInTheInMemoryCacheOrPersister() = testScope.runTest { - val key1 = "key1" - val key2 = "key2" - val value1 = 1 - val value2 = 2 - val store = StoreBuilder.from( - fetcher = Fetcher.of { key -> - when (key) { - key1 -> value1 - key2 -> value2 - else -> throw IllegalStateException("Unknown key") - } - }, - sourceOfTruth = persister.asSourceOfTruth() - ).scope(testScope) - .build() - - // get data for both keys - store.getData(key1) - store.getData(key2) - - // clear store entry for key1 - store.clear(key1) - - // entry for key1 is gone - assertNull(persister.peekEntry(key1)) - - // entry for key2 should still exists - assertEquals(value2, persister.peekEntry(key2)) - - // getting data for key1 should hit the network again - assertEquals( - Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = value1 - ), + fun callingClearWithKeyOnStoreHasNoEffectOnExistingEntriesAssociatedWithOtherKeysInTheInMemoryCacheOrPersister() = + testScope.runTest { + val key1 = "key1" + val key2 = "key2" + val value1 = 1 + val value2 = 2 + val store = + StoreBuilder.from( + fetcher = + Fetcher.of { key -> + when (key) { + key1 -> value1 + key2 -> value2 + else -> throw IllegalStateException("Unknown key") + } + }, + sourceOfTruth = persister.asSourceOfTruth(), + ).scope(testScope) + .build() + + // get data for both keys store.getData(key1) - ) - - // getting data for key2 should not hit the network - assertEquals( - Data( - origin = StoreReadResponseOrigin.Cache, - value = value2 - ), store.getData(key2) - ) - } + + // clear store entry for key1 + store.clear(key1) + + // entry for key1 is gone + assertNull(persister.peekEntry(key1)) + + // entry for key2 should still exists + assertEquals(value2, persister.peekEntry(key2)) + + // getting data for key1 should hit the network again + assertEquals( + Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = value1, + ), + store.getData(key1), + ) + + // getting data for key2 should not hit the network + assertEquals( + Data( + origin = StoreReadResponseOrigin.Cache, + value = value2, + ), + store.getData(key2), + ) + } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FallbackTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FallbackTests.kt index d09f452d1..d18cc79db 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FallbackTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FallbackTests.kt @@ -35,29 +35,32 @@ class FallbackTests { val fail = false val hardcodedPagesFetcher = Fetcher.of { key -> hardcodedPages.get(key) } - val secondaryApiFetcher = Fetcher.withFallback( - secondaryApi.name, - hardcodedPagesFetcher - ) { key -> secondaryApi.get(key) } - - val store = StoreBuilder.from( - fetcher = Fetcher.withFallback(api.name, secondaryApiFetcher) { key -> api.fetch(key, fail, ttl) }, - sourceOfTruth = SourceOfTruth.of( - nonFlowReader = { key -> pagesDatabase.get(key) }, - writer = { key, page -> pagesDatabase.put(key, page) }, - delete = null, - deleteAll = null - ) - ).build() + val secondaryApiFetcher = + Fetcher.withFallback( + secondaryApi.name, + hardcodedPagesFetcher, + ) { key -> secondaryApi.get(key) } + + val store = + StoreBuilder.from( + fetcher = Fetcher.withFallback(api.name, secondaryApiFetcher) { key -> api.fetch(key, fail, ttl) }, + sourceOfTruth = + SourceOfTruth.of( + nonFlowReader = { key -> pagesDatabase.get(key) }, + writer = { key, page -> pagesDatabase.put(key, page) }, + delete = null, + deleteAll = null, + ), + ).build() val responses = store.stream(StoreReadRequest.fresh("1")).take(2).toList() assertEquals( listOf( StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Data(Page.Data("1", null), StoreReadResponseOrigin.Fetcher(api.name)) + StoreReadResponse.Data(Page.Data("1", null), StoreReadResponseOrigin.Fetcher(api.name)), ), - responses + responses, ) } @@ -68,20 +71,23 @@ class FallbackTests { val fail = true val hardcodedPagesFetcher = Fetcher.of { key -> hardcodedPages.get(key) } - val secondaryApiFetcher = Fetcher.withFallback( - secondaryApi.name, - hardcodedPagesFetcher - ) { key -> secondaryApi.get(key) } - - val store = StoreBuilder.from( - fetcher = Fetcher.withFallback(api.name, secondaryApiFetcher) { key -> api.fetch(key, fail, ttl) }, - sourceOfTruth = SourceOfTruth.of( - nonFlowReader = { key -> pagesDatabase.get(key) }, - writer = { key, page -> pagesDatabase.put(key, page) }, - delete = null, - deleteAll = null - ) - ).build() + val secondaryApiFetcher = + Fetcher.withFallback( + secondaryApi.name, + hardcodedPagesFetcher, + ) { key -> secondaryApi.get(key) } + + val store = + StoreBuilder.from( + fetcher = Fetcher.withFallback(api.name, secondaryApiFetcher) { key -> api.fetch(key, fail, ttl) }, + sourceOfTruth = + SourceOfTruth.of( + nonFlowReader = { key -> pagesDatabase.get(key) }, + writer = { key, page -> pagesDatabase.put(key, page) }, + delete = null, + deleteAll = null, + ), + ).build() val responses = store.stream(StoreReadRequest.fresh("1")).take(2).toList() @@ -90,10 +96,10 @@ class FallbackTests { StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), StoreReadResponse.Data( Page.Data("1", null), - StoreReadResponseOrigin.Fetcher(secondaryApiFetcher.name) - ) + StoreReadResponseOrigin.Fetcher(secondaryApiFetcher.name), + ), ), - responses + responses, ) } @@ -108,21 +114,24 @@ class FallbackTests { val throwingSecondaryApiFetcher = Fetcher.withFallback(secondaryApi.name, hardcodedPagesFetcher) { throw Exception() } - val store = StoreBuilder.from( - fetcher = Fetcher.withFallback(api.name, throwingSecondaryApiFetcher) { key -> - api.fetch( - key, - fail, - ttl - ) - }, - sourceOfTruth = SourceOfTruth.of( - nonFlowReader = { key -> pagesDatabase.get(key) }, - writer = { key, page -> pagesDatabase.put(key, page) }, - delete = null, - deleteAll = null - ) - ).build() + val store = + StoreBuilder.from( + fetcher = + Fetcher.withFallback(api.name, throwingSecondaryApiFetcher) { key -> + api.fetch( + key, + fail, + ttl, + ) + }, + sourceOfTruth = + SourceOfTruth.of( + nonFlowReader = { key -> pagesDatabase.get(key) }, + writer = { key, page -> pagesDatabase.put(key, page) }, + delete = null, + deleteAll = null, + ), + ).build() val responses = store.stream(StoreReadRequest.fresh("1")).take(2).toList() @@ -131,10 +140,10 @@ class FallbackTests { StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), StoreReadResponse.Data( Page.Data("1", null), - StoreReadResponseOrigin.Fetcher(hardcodedPagesFetcher.name) - ) + StoreReadResponseOrigin.Fetcher(hardcodedPagesFetcher.name), + ), ), - responses + responses, ) } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherControllerTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherControllerTests.kt index c7794f00b..e8ef19509 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherControllerTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherControllerTests.kt @@ -25,103 +25,117 @@ class FetcherControllerTests { private val testScope = TestScope() @Test - fun simple() = testScope.runTest { - val fetcherController = FetcherController( - scope = testScope, - realFetcher = Fetcher.ofResultFlow { key: Int -> - flow { - emit(FetcherResult.Data(key * key) as FetcherResult) - } - }, - sourceOfTruth = null - ) - val fetcher = fetcherController.getFetcher(3) - assertEquals(0, fetcherController.fetcherSize()) - val received = fetcher.onEach { - assertEquals(1, fetcherController.fetcherSize()) - }.first() - assertEquals( - Data( - value = 9, - origin = StoreReadResponseOrigin.Fetcher() - ), - received - ) - assertEquals(0, fetcherController.fetcherSize()) - } - - @Test - fun concurrent() = testScope.runTest { - var createdCnt = 0 - val fetcherController = FetcherController( - scope = testScope, - realFetcher = Fetcher.ofResultFlow { key: Int -> - createdCnt++ - flow { - // make sure it takes time, otherwise, we may not share - delay(1) - emit(FetcherResult.Data(key * key) as FetcherResult) - } - }, - sourceOfTruth = null - ) - val fetcherCount = 20 - fun createFetcher() = async { - fetcherController.getFetcher(3) - .onEach { + fun simple() = + testScope.runTest { + val fetcherController = + FetcherController( + scope = testScope, + realFetcher = + Fetcher.ofResultFlow { key: Int -> + flow { + emit(FetcherResult.Data(key * key) as FetcherResult) + } + }, + sourceOfTruth = null, + ) + val fetcher = fetcherController.getFetcher(3) + assertEquals(0, fetcherController.fetcherSize()) + val received = + fetcher.onEach { assertEquals(1, fetcherController.fetcherSize()) }.first() - } - - val fetchers = (0 until fetcherCount).map { - createFetcher() - } - fetchers.forEach { assertEquals( Data( value = 9, - origin = StoreReadResponseOrigin.Fetcher() + origin = StoreReadResponseOrigin.Fetcher(), ), - it.await() + received, ) + assertEquals(0, fetcherController.fetcherSize()) } - assertEquals(0, fetcherController.fetcherSize()) - assertEquals(1, createdCnt) - } @Test - fun concurrent_when_cancelled() = testScope.runTest { - var createdCnt = 0 - val job = SupervisorJob() - val scope = TestScope(StandardTestDispatcher() + job) - val fetcherController = FetcherController( - scope = scope, - realFetcher = Fetcher.ofResultFlow { key: Int -> - createdCnt++ - flow { - // make sure it takes time, otherwise, we may not share - advanceUntilIdle() - emit(FetcherResult.Data(key * key) as FetcherResult) + fun concurrent() = + testScope.runTest { + var createdCnt = 0 + val fetcherController = + FetcherController( + scope = testScope, + realFetcher = + Fetcher.ofResultFlow { key: Int -> + createdCnt++ + flow { + // make sure it takes time, otherwise, we may not share + delay(1) + emit(FetcherResult.Data(key * key) as FetcherResult) + } + }, + sourceOfTruth = null, + ) + val fetcherCount = 20 + + fun createFetcher() = + async { + fetcherController.getFetcher(3) + .onEach { + assertEquals(1, fetcherController.fetcherSize()) + }.first() } - }, - sourceOfTruth = null - ) - val fetcherCount = 20 - fun createFetcher() = scope.launch { - fetcherController.getFetcher(3) - .onEach { - assertEquals(1, fetcherController.fetcherSize()) - }.first() + val fetchers = + (0 until fetcherCount).map { + createFetcher() + } + fetchers.forEach { + assertEquals( + Data( + value = 9, + origin = StoreReadResponseOrigin.Fetcher(), + ), + it.await(), + ) + } + assertEquals(0, fetcherController.fetcherSize()) + assertEquals(1, createdCnt) } - (0 until fetcherCount).map { - createFetcher() + @Test + fun concurrent_when_cancelled() = + testScope.runTest { + var createdCnt = 0 + val job = SupervisorJob() + val scope = TestScope(StandardTestDispatcher() + job) + val fetcherController = + FetcherController( + scope = scope, + realFetcher = + Fetcher.ofResultFlow { key: Int -> + createdCnt++ + flow { + // make sure it takes time, otherwise, we may not share + advanceUntilIdle() + emit(FetcherResult.Data(key * key) as FetcherResult) + } + }, + sourceOfTruth = null, + ) + val fetcherCount = 20 + + fun createFetcher() = + scope.launch { + fetcherController.getFetcher(3) + .onEach { + assertEquals(1, fetcherController.fetcherSize()) + }.first() + } + + (0 until fetcherCount).map { + createFetcher() + } + scope.advanceUntilIdle() + job.cancelChildren() + scope.advanceUntilIdle() + assertEquals(0, fetcherController.fetcherSize()) + assertEquals(1, createdCnt) } - scope.advanceUntilIdle() - job.cancelChildren() - scope.advanceUntilIdle() - assertEquals(0, fetcherController.fetcherSize()) - assertEquals(1, createdCnt) - } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt index 8a1e2747e..4f009f849 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt @@ -18,224 +18,241 @@ class FetcherResponseTests { private val testScope = TestScope() @Test - fun givenAFetcherThatThrowsAnExceptionInInvokeWhenStreamingThenTheExceptionsShouldNotBeCaught() = testScope.runTest { - val store = StoreBuilder.from( - Fetcher.ofResult { - throw RuntimeException("don't catch me") - } - ).buildWithTestScope() + fun givenAFetcherThatThrowsAnExceptionInInvokeWhenStreamingThenTheExceptionsShouldNotBeCaught() = + testScope.runTest { + val store = + StoreBuilder.from( + Fetcher.ofResult { + throw RuntimeException("don't catch me") + }, + ).buildWithTestScope() - assertFailsWith(message = "don't catch me") { - val result = store.stream(StoreReadRequest.fresh(1)).toList() - assertEquals(0, result.size) + assertFailsWith(message = "don't catch me") { + val result = store.stream(StoreReadRequest.fresh(1)).toList() + assertEquals(0, result.size) + } } - } @Test fun givenAFetcherThatEmitsErrorAndDataWhenSteamingThenItCanEmitValueAfterAnError() { val exception = RuntimeException("first error") testScope.runTest { - val store = StoreBuilder.from( - fetcher = Fetcher.ofResultFlow { key: Int -> - flowOf( - FetcherResult.Error.Exception(exception), - FetcherResult.Data("$key") - ) - } - ).buildWithTestScope() + val store = + StoreBuilder.from( + fetcher = + Fetcher.ofResultFlow { key: Int -> + flowOf( + FetcherResult.Error.Exception(exception), + FetcherResult.Data("$key"), + ) + }, + ).buildWithTestScope() assertEmitsExactly( store.stream( - StoreReadRequest.fresh(1) + StoreReadRequest.fresh(1), ), listOf( StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), StoreReadResponse.Error.Exception(exception, StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Data("1", StoreReadResponseOrigin.Fetcher()) - ) + StoreReadResponse.Data("1", StoreReadResponseOrigin.Fetcher()), + ), ) } } @Test - fun givenTransformerWhenRawValueThenUnwrappedValueReturnedAndValueIsCached() = testScope.runTest { - val fetcher = Fetcher.ofFlow { flowOf(it * it) } - val pipeline = StoreBuilder - .from(fetcher).buildWithTestScope() - - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() + fun givenTransformerWhenRawValueThenUnwrappedValueReturnedAndValueIsCached() = + testScope.runTest { + val fetcher = Fetcher.ofFlow { flowOf(it * it) } + val pipeline = + StoreBuilder + .from(fetcher).buildWithTestScope() + + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = 9, + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Data( - value = 9, - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( - StoreReadResponse.Data( - value = 9, - origin = StoreReadResponseOrigin.Cache - ) + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)), + listOf( + StoreReadResponse.Data( + value = 9, + origin = StoreReadResponseOrigin.Cache, + ), + ), ) - ) - } + } @Test - fun givenTransformerWhenErrorMessageThenErrorReturnedToUserAndErrorIsNotCached() = testScope.runTest { - var count = 0 - val fetcher = Fetcher.ofResultFlow { _: Int -> - flowOf(count++).map { - if (it > 0) { - FetcherResult.Data(it) - } else { - FetcherResult.Error.Message("zero") + fun givenTransformerWhenErrorMessageThenErrorReturnedToUserAndErrorIsNotCached() = + testScope.runTest { + var count = 0 + val fetcher = + Fetcher.ofResultFlow { _: Int -> + flowOf(count++).map { + if (it > 0) { + FetcherResult.Data(it) + } else { + FetcherResult.Error.Message("zero") + } + } } - } - } - val pipeline = StoreBuilder.from(fetcher) - .buildWithTestScope() - - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() + val pipeline = + StoreBuilder.from(fetcher) + .buildWithTestScope() + + assertEmitsExactly( + pipeline.stream(StoreReadRequest.fresh(3)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Error.Message( + message = "zero", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Error.Message( - message = "zero", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = 1, + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Data( - value = 1, - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } @Test - fun givenTransformerWhenErrorExceptionThenErrorReturnedToUserAndErrorIsNotCached() = testScope.runTest { - val e = Exception() - var count = 0 - val fetcher = Fetcher.ofResultFlow { _: Int -> - flowOf(count++).map { - if (it > 0) { - FetcherResult.Data(it) - } else { - FetcherResult.Error.Exception(e) + fun givenTransformerWhenErrorExceptionThenErrorReturnedToUserAndErrorIsNotCached() = + testScope.runTest { + val e = Exception() + var count = 0 + val fetcher = + Fetcher.ofResultFlow { _: Int -> + flowOf(count++).map { + if (it > 0) { + FetcherResult.Data(it) + } else { + FetcherResult.Error.Exception(e) + } + } } - } - } - val pipeline = StoreBuilder - .from(fetcher) - .buildWithTestScope() - - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() + val pipeline = + StoreBuilder + .from(fetcher) + .buildWithTestScope() + + assertEmitsExactly( + pipeline.stream(StoreReadRequest.fresh(3)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Error.Exception( + error = e, + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Error.Exception( - error = e, - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = 1, + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Data( - value = 1, - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } @Test - fun givenExceptionsAsErrorsWhenExceptionThrownThenErrorReturnedToUserAndErrorIsNotCached() = testScope.runTest { - var count = 0 - val e = Exception() - val fetcher = Fetcher.of { - count++ - if (count == 1) { - throw e - } - count - 1 - } - val pipeline = StoreBuilder - .from(fetcher = fetcher) - .buildWithTestScope() - - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() + fun givenExceptionsAsErrorsWhenExceptionThrownThenErrorReturnedToUserAndErrorIsNotCached() = + testScope.runTest { + var count = 0 + val e = Exception() + val fetcher = + Fetcher.of { + count++ + if (count == 1) { + throw e + } + count - 1 + } + val pipeline = + StoreBuilder + .from(fetcher = fetcher) + .buildWithTestScope() + + assertEmitsExactly( + pipeline.stream(StoreReadRequest.fresh(3)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Error.Exception( + error = e, + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Error.Exception( - error = e, - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = 1, + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Data( - value = 1, - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } @Test - fun givenAFetcherThatEmitsCustomErrorWhenStreamingThenCustomErrorShouldBeEmitted() = testScope.runTest { - data class TestCustomError(val errorMessage: String) - val customError = TestCustomError("Test custom error") - - val store = StoreBuilder.from( - fetcher = Fetcher.ofResultFlow { _: Int -> - flowOf( - FetcherResult.Error.Custom(customError) - ) - } - ).buildWithTestScope() - - assertEmitsExactly( - store.stream(StoreReadRequest.fresh(1)), - listOf( - StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Error.Custom( - error = customError, - origin = StoreReadResponseOrigin.Fetcher() - ) + fun givenAFetcherThatEmitsCustomErrorWhenStreamingThenCustomErrorShouldBeEmitted() = + testScope.runTest { + data class TestCustomError(val errorMessage: String) + val customError = TestCustomError("Test custom error") + + val store = + StoreBuilder.from( + fetcher = + Fetcher.ofResultFlow { _: Int -> + flowOf( + FetcherResult.Error.Custom(customError), + ) + }, + ).buildWithTestScope() + + assertEmitsExactly( + store.stream(StoreReadRequest.fresh(1)), + listOf( + StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()), + StoreReadResponse.Error.Custom( + error = customError, + origin = StoreReadResponseOrigin.Fetcher(), + ), + ), ) - ) - } + } - private fun StoreBuilder.buildWithTestScope() = - scope(testScope).build() + private fun StoreBuilder.buildWithTestScope() = scope(testScope).build() } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt index 8aa82ac44..8f66a4434 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt @@ -50,457 +50,465 @@ class FlowStoreTests { private val testScope = TestScope() @Test - fun getAndFresh() = testScope.runTest { - val fetcher = FakeFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val pipeline = StoreBuilder - .from(fetcher) - .buildWithTestScope() + fun getAndFresh() = + testScope.runTest { + val fetcher = + FakeFetcher( + 3 to "three-1", + 3 to "three-2", + ) + val pipeline = StoreBuilder.from(fetcher).buildWithTestScope() - assertEquals( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)).take(2).toList(), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEquals( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).take(2).toList(), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEquals( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)).take(1).toList(), - listOf( - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Cache - ) + assertEquals( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).take(1).toList(), + listOf( + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Cache, + ), + ), ) - ) - assertEquals( - pipeline.stream(StoreReadRequest.fresh(3)).take(2).toList(), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEquals( + pipeline.stream(StoreReadRequest.fresh(3)).take(2).toList(), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEquals( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)).take(1).toList(), - listOf( - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Cache - ) + assertEquals( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).take(1).toList(), + listOf( + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Cache, + ), + ), ) - ) - } + } @Test - fun getAndFresh_withPersister() = testScope.runTest { - val fetcher = FakeFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val persister = InMemoryPersister() - val pipeline = StoreBuilder.from( - fetcher = fetcher, - sourceOfTruth = persister.asSourceOfTruth() - ).buildWithTestScope() - - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() + fun getAndFresh_withPersister() = + testScope.runTest { + val fetcher = + FakeFetcher( + 3 to "three-1", + 3 to "three-2", ) + val persister = InMemoryPersister() + val pipeline = + StoreBuilder.from( + fetcher = fetcher, + sourceOfTruth = persister.asSourceOfTruth(), + ).buildWithTestScope() + + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), + ), ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Cache + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)), + listOf( + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Cache, + ), + // note that we still get the data from persister as well as we don't listen to + // the persister for the cached items unless there is an active stream, which + // means cache can go out of sync w/ the persister + Data( + value = "three-1", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), ), - // note that we still get the data from persister as well as we don't listen to - // the persister for the cached items unless there is an active stream, which - // means cache can go out of sync w/ the persister - Data( - value = "three-1", - origin = StoreReadResponseOrigin.SourceOfTruth - ) ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.fresh(3)), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Cache + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)), + listOf( + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Cache, + ), + Data( + value = "three-2", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), ), - Data( - value = "three-2", - origin = StoreReadResponseOrigin.SourceOfTruth - ) ) - ) - } + } @Test - fun streamAndFresh_withPersister() = testScope.runTest { - val fetcher = FakeFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val persister = InMemoryPersister() + fun streamAndFresh_withPersister() = + testScope.runTest { + val fetcher = + FakeFetcher( + 3 to "three-1", + 3 to "three-2", + ) + val persister = InMemoryPersister() - val pipeline = StoreBuilder.from( - fetcher = fetcher, - sourceOfTruth = persister.asSourceOfTruth() - ).buildWithTestScope() + val pipeline = + StoreBuilder.from( + fetcher = fetcher, + sourceOfTruth = persister.asSourceOfTruth(), + ).buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Cache - ), - Data( - value = "three-1", - origin = StoreReadResponseOrigin.SourceOfTruth - ), - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)), + listOf( + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Cache, + ), + Data( + value = "three-1", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } @Test - fun streamAndFresh() = testScope.runTest { - val fetcher = FakeFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val pipeline = StoreBuilder.from(fetcher = fetcher) - .buildWithTestScope() + fun streamAndFresh() = + testScope.runTest { + val fetcher = + FakeFetcher( + 3 to "three-1", + 3 to "three-2", + ) + val pipeline = StoreBuilder.from(fetcher = fetcher).buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf - ( - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Cache - ), - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)), + listOf( + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Cache, + ), + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } @Test - fun skipCache() = testScope.runTest { - val fetcher = FakeFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val pipeline = StoreBuilder.from(fetcher = fetcher) - .buildWithTestScope() + fun skipCache() = + testScope.runTest { + val fetcher = + FakeFetcher( + 3 to "three-1", + 3 to "three-2", + ) + val pipeline = StoreBuilder.from(fetcher = fetcher).buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.skipMemory(3, refresh = false)), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.skipMemory(3, refresh = false)), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.skipMemory(3, refresh = false)), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.skipMemory(3, refresh = false)), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } @Test - fun flowingFetcher() = testScope.runTest { - val fetcher = FakeFlowingFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val persister = InMemoryPersister() + fun flowingFetcher() = + testScope.runTest { + val fetcher = + FakeFlowingFetcher( + 3 to "three-1", + 3 to "three-2", + ) + val persister = InMemoryPersister() - val pipeline = StoreBuilder.from( - fetcher = fetcher, - sourceOfTruth = persister.asSourceOfTruth() - ) - .disableCache() - .buildWithTestScope() + val pipeline = + StoreBuilder.from( + fetcher = fetcher, + sourceOfTruth = persister.asSourceOfTruth(), + ).disableCache().buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.fresh(3)), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( - Data( - value = "three-2", - origin = StoreReadResponseOrigin.SourceOfTruth - ), - Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)), + listOf( + Data( + value = "three-2", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } @Test - fun diskChangeWhileNetworkIsFlowing_simple() = testScope.runTest { - val persister = InMemoryPersister().asFlowable() - val pipeline = StoreBuilder.from( - Fetcher.ofFlow { - flow { - delay(20) - emit("three-1") - } - }, - sourceOfTruth = persister.asSourceOfTruth() - ) - .disableCache() - .buildWithTestScope() - - launch { - delay(10) - persister.flowWriter(3, "local-1") - } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - Data( - value = "local-1", - origin = StoreReadResponseOrigin.SourceOfTruth + fun diskChangeWhileNetworkIsFlowing_simple() = + testScope.runTest { + val persister = InMemoryPersister().asFlowable() + val pipeline = + StoreBuilder.from( + Fetcher.ofFlow { + flow { + delay(20) + emit("three-1") + } + }, + sourceOfTruth = persister.asSourceOfTruth(), + ).disableCache().buildWithTestScope() + + launch { + delay(10) + persister.flowWriter(3, "local-1") + } + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "local-1", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() - ) - ) - ) - } + } @Test - fun diskChangeWhileNetworkIsFlowing_overwrite() = testScope.runTest { - val persister = InMemoryPersister().asFlowable() - val pipeline = StoreBuilder.from( - fetcher = Fetcher.ofFlow { - flow { - delay(10) - emit("three-1") - delay(10) - emit("three-2") - } - }, - sourceOfTruth = persister.asSourceOfTruth() - ) - .disableCache() - .buildWithTestScope() - - launch { - delay(5) - persister.flowWriter(3, "local-1") - delay(10) // go in between two server requests - persister.flowWriter(3, "local-2") - } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - Data( - value = "local-1", - origin = StoreReadResponseOrigin.SourceOfTruth - ), - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() - ), - Data( - value = "local-2", - origin = StoreReadResponseOrigin.SourceOfTruth + fun diskChangeWhileNetworkIsFlowing_overwrite() = + testScope.runTest { + val persister = InMemoryPersister().asFlowable() + val pipeline = + StoreBuilder.from( + fetcher = + Fetcher.ofFlow { + flow { + delay(10) + emit("three-1") + delay(10) + emit("three-2") + } + }, + sourceOfTruth = persister.asSourceOfTruth(), + ).disableCache().buildWithTestScope() + + launch { + delay(5) + persister.flowWriter(3, "local-1") + delay(10) // go in between two server requests + persister.flowWriter(3, "local-2") + } + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "local-1", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "local-2", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } @Test - fun errorTest() = testScope.runTest { - val exception = IllegalArgumentException("wow") - val persister = InMemoryPersister().asFlowable() - val pipeline = StoreBuilder.from( - Fetcher.of { - throw exception - }, - sourceOfTruth = persister.asSourceOfTruth() - ) - .disableCache() - .buildWithTestScope() - - launch { - delay(10) - persister.flowWriter(3, "local-1") - } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(key = 3, refresh = true)), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - StoreReadResponse.Error.Exception( - error = exception, - origin = StoreReadResponseOrigin.Fetcher() + fun errorTest() = + testScope.runTest { + val exception = IllegalArgumentException("wow") + val persister = InMemoryPersister().asFlowable() + val pipeline = + StoreBuilder.from( + Fetcher.of { + throw exception + }, + sourceOfTruth = persister.asSourceOfTruth(), + ).disableCache().buildWithTestScope() + + launch { + delay(10) + persister.flowWriter(3, "local-1") + } + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(key = 3, refresh = true)), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Error.Exception( + error = exception, + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "local-1", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), ), - Data( - value = "local-1", - origin = StoreReadResponseOrigin.SourceOfTruth - ) ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(key = 3, refresh = true)), - listOf( - Data( - value = "local-1", - origin = StoreReadResponseOrigin.SourceOfTruth - ), - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(key = 3, refresh = true)), + listOf( + Data( + value = "local-1", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Error.Exception( + error = exception, + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Error.Exception( - error = exception, - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } @Test fun givenSourceOfTruthWhenStreamFreshDataReturnsNoDataFromFetcherThenFetchReturnsNoDataAndCachedValuesAreReceived() = testScope.runTest { val persister = InMemoryPersister().asFlowable() - val pipeline = StoreBuilder.from( - fetcher = Fetcher.ofFlow { flow {} }, - sourceOfTruth = persister.asSourceOfTruth() - ) - .buildWithTestScope() + val pipeline = + StoreBuilder.from( + fetcher = Fetcher.ofFlow { flow {} }, + sourceOfTruth = persister.asSourceOfTruth(), + ).buildWithTestScope() persister.flowWriter(3, "local-1") val firstFetch = pipeline.fresh(3) // prime the cache @@ -510,272 +518,290 @@ class FlowStoreTests { pipeline.stream(StoreReadRequest.fresh(3)), listOf( Loading( - origin = StoreReadResponseOrigin.Fetcher() + origin = StoreReadResponseOrigin.Fetcher(), ), StoreReadResponse.NoNewData( - origin = StoreReadResponseOrigin.Fetcher() + origin = StoreReadResponseOrigin.Fetcher(), ), Data( value = "local-1", - origin = StoreReadResponseOrigin.Cache + origin = StoreReadResponseOrigin.Cache, ), Data( value = "local-1", - origin = StoreReadResponseOrigin.SourceOfTruth - ) - ) + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + ), ) } @Test - fun givenSourceOfTruthWhenStreamCachedDataWithRefreshReturnsNoNewDataThenCachedValuesAreReceivedAndFetchReturnsNoData() = testScope.runTest { - val persister = InMemoryPersister().asFlowable() - val pipeline = StoreBuilder.from( - fetcher = Fetcher.ofFlow { flow {} }, - sourceOfTruth = persister.asSourceOfTruth() - ) - .buildWithTestScope() + fun givenSourceOfTruthWhenStreamCachedDataWithRefreshReturnsNoNewDataThenCachedValuesAreReceivedAndFetchReturnsNoData() = + testScope.runTest { + val persister = InMemoryPersister().asFlowable() + val pipeline = + StoreBuilder.from( + fetcher = Fetcher.ofFlow { flow {} }, + sourceOfTruth = persister.asSourceOfTruth(), + ).buildWithTestScope() - persister.flowWriter(3, "local-1") - val firstFetch = pipeline.fresh(3) // prime the cache - assertEquals("local-1", firstFetch) + persister.flowWriter(3, "local-1") + val firstFetch = pipeline.fresh(3) // prime the cache + assertEquals("local-1", firstFetch) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( - Data( - value = "local-1", - origin = StoreReadResponseOrigin.Cache - ), - Data( - value = "local-1", - origin = StoreReadResponseOrigin.SourceOfTruth - ), - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)), + listOf( + Data( + value = "local-1", + origin = StoreReadResponseOrigin.Cache, + ), + Data( + value = "local-1", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.NoNewData( + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.NoNewData( - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } @Test - fun givenNoSourceOfTruthWhenStreamFreshDataReturnsNoDataFromFetcherThenFetchReturnsNoDataAndCachedValuesAreReceived() = testScope.runTest { - var createCount = 0 - val pipeline = StoreBuilder.from( - fetcher = Fetcher.ofFlow { - if (createCount++ == 0) { - flowOf("remote-1") - } else { - flowOf() - } - } - ) - .buildWithTestScope() + fun givenNoSourceOfTruthWhenStreamFreshDataReturnsNoDataFromFetcherThenFetchReturnsNoDataAndCachedValuesAreReceived() = + testScope.runTest { + var createCount = 0 + val pipeline = + StoreBuilder.from( + fetcher = + Fetcher.ofFlow { + if (createCount++ == 0) { + flowOf("remote-1") + } else { + flowOf() + } + }, + ).buildWithTestScope() - val firstFetch = pipeline.fresh(3) // prime the cache - assertEquals("remote-1", firstFetch) + val firstFetch = pipeline.fresh(3) // prime the cache + assertEquals("remote-1", firstFetch) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( - Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - StoreReadResponse.NoNewData( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.fresh(3)), + listOf( + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.NoNewData( + origin = StoreReadResponseOrigin.Fetcher(), + ), + Data( + value = "remote-1", + origin = StoreReadResponseOrigin.Cache, + ), ), - Data( - value = "remote-1", - origin = StoreReadResponseOrigin.Cache - ) ) - ) - } + } @Test - fun givenNoSoTWhenStreamCachedDataWithRefreshReturnsNoNewDataThenCachedValuesAreReceivedAndFetchReturnsNoData() = testScope.runTest { - var createCount = 0 - val pipeline = StoreBuilder.from( - fetcher = Fetcher.ofFlow { - if (createCount++ == 0) { - flowOf("remote-1") - } else { - flowOf() - } - } - ) - .buildWithTestScope() + fun givenNoSoTWhenStreamCachedDataWithRefreshReturnsNoNewDataThenCachedValuesAreReceivedAndFetchReturnsNoData() = + testScope.runTest { + var createCount = 0 + val pipeline = + StoreBuilder.from( + fetcher = + Fetcher.ofFlow { + if (createCount++ == 0) { + flowOf("remote-1") + } else { + flowOf() + } + }, + ).buildWithTestScope() - val firstFetch = pipeline.fresh(3) // prime the cache - assertEquals("remote-1", firstFetch) + val firstFetch = pipeline.fresh(3) // prime the cache + assertEquals("remote-1", firstFetch) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( - Data( - value = "remote-1", - origin = StoreReadResponseOrigin.Cache - ), - Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)), + listOf( + Data( + value = "remote-1", + origin = StoreReadResponseOrigin.Cache, + ), + Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.NoNewData( + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.NoNewData( - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } @Test - fun givenNoSourceOfTruthAndCacheHitWhenStreamCachedDataWithoutRefreshThenNoFetchIsTriggeredAndReceivesFollowingNetworkUpdates() = testScope.runTest { - val fetcher = FakeFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val store = StoreBuilder.from(fetcher = fetcher) - .buildWithTestScope() - - val firstFetch = store.fresh(3) - assertEquals("three-1", firstFetch) - val secondCollect = mutableListOf>() - val collection = launch { - store.stream(StoreReadRequest.cached(3, refresh = false)).collect { - secondCollect.add(it) - } - } - testScope.runCurrent() - assertEquals(1, secondCollect.size) - assertContains( - secondCollect, - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Cache + fun givenNoSourceOfTruthAndCacheHitWhenStreamCachedDataWithoutRefreshThenNoFetchIsTriggeredAndReceivesFollowingNetworkUpdates() = + testScope.runTest { + val fetcher = + FakeFetcher( + 3 to "three-1", + 3 to "three-2", + ) + val store = StoreBuilder.from(fetcher = fetcher).buildWithTestScope() + + val firstFetch = store.fresh(3) + assertEquals("three-1", firstFetch) + val secondCollect = mutableListOf>() + val collection = + launch { + store.stream(StoreReadRequest.cached(3, refresh = false)).collect { + secondCollect.add(it) + } + } + testScope.runCurrent() + assertEquals(1, secondCollect.size) + assertContains( + secondCollect, + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Cache, + ), ) - ) - // trigger another fetch from network - val secondFetch = store.fresh(3) - assertEquals("three-2", secondFetch) - testScope.runCurrent() - // make sure cached also received it - assertEquals(2, secondCollect.size) - - assertContains( - secondCollect, - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Cache + // trigger another fetch from network + val secondFetch = store.fresh(3) + assertEquals("three-2", secondFetch) + testScope.runCurrent() + // make sure cached also received it + assertEquals(2, secondCollect.size) + + assertContains( + secondCollect, + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Cache, + ), ) - ) - assertContains( - secondCollect, - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() + assertContains( + secondCollect, + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ) - ) - collection.cancelAndJoin() - } + collection.cancelAndJoin() + } @Test - fun givenSourceOfTruthAndCacheHitWhenStreamCachedDataWithoutRefreshThenNoFetchIsTriggeredAndReceivesFollowingNetworkUpdates() = testScope.runTest { - val fetcher = FakeFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val persister = InMemoryPersister() - val pipeline = StoreBuilder.from( - fetcher = fetcher, - sourceOfTruth = persister.asSourceOfTruth() - ).buildWithTestScope() - - val firstFetch = pipeline.fresh(3) - assertEquals("three-1", firstFetch) - val secondCollect = mutableListOf>() - val collection = launch { - pipeline.stream(StoreReadRequest.cached(3, refresh = false)).collect { - secondCollect.add(it) - } - } - testScope.runCurrent() - assertEquals(2, secondCollect.size) - - assertContains( - secondCollect, - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Cache - ), - ) - assertContains( - secondCollect, - Data( - value = "three-1", - origin = StoreReadResponseOrigin.SourceOfTruth + fun givenSourceOfTruthAndCacheHitWhenStreamCachedDataWithoutRefreshThenNoFetchIsTriggeredAndReceivesFollowingNetworkUpdates() = + testScope.runTest { + val fetcher = + FakeFetcher( + 3 to "three-1", + 3 to "three-2", + ) + val persister = InMemoryPersister() + val pipeline = + StoreBuilder.from( + fetcher = fetcher, + sourceOfTruth = persister.asSourceOfTruth(), + ).buildWithTestScope() + + val firstFetch = pipeline.fresh(3) + assertEquals("three-1", firstFetch) + val secondCollect = mutableListOf>() + val collection = + launch { + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).collect { + secondCollect.add(it) + } + } + testScope.runCurrent() + assertEquals(2, secondCollect.size) + + assertContains( + secondCollect, + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Cache, + ), + ) + assertContains( + secondCollect, + Data( + value = "three-1", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), ) - ) - // trigger another fetch from network - val secondFetch = pipeline.fresh(3) - assertEquals("three-2", secondFetch) - testScope.runCurrent() - // make sure cached also received it - assertEquals(3, secondCollect.size) - - assertContains( - secondCollect, - Data( - value = "three-1", - origin = StoreReadResponseOrigin.Cache - ), - ) - assertContains( - secondCollect, - Data( - value = "three-1", - origin = StoreReadResponseOrigin.SourceOfTruth - ), - ) + // trigger another fetch from network + val secondFetch = pipeline.fresh(3) + assertEquals("three-2", secondFetch) + testScope.runCurrent() + // make sure cached also received it + assertEquals(3, secondCollect.size) - assertContains( - secondCollect, - Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() + assertContains( + secondCollect, + Data( + value = "three-1", + origin = StoreReadResponseOrigin.Cache, + ), ) - ) - collection.cancelAndJoin() - } + assertContains( + secondCollect, + Data( + value = "three-1", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + ) + + assertContains( + secondCollect, + Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), + ) + collection.cancelAndJoin() + } @Test - fun givenCacheAndNoSourceOfTruthWhen3CachedStreamsWithRefreshAnd1stHasSlowCollectionThen1stStreamsGets3FetchUpdatesAndOtherStreamsGetCacheResultAndFetchResult() = + fun testCachedStreamsWithRefreshAndSlowCollection() = testScope.runTest { - val fetcher = FakeFetcher( - 3 to "three-1", - 3 to "three-2", - 3 to "three-3" - ) - val pipeline = StoreBuilder.from( - fetcher = fetcher - ).buildWithTestScope() + // Given a cache and no source of truth, when 3 cached streams are requested with refresh + // and the 1st stream has a slow collection, then the 1st stream gets 3 fetch updates + // and the other streams get the cache result and the fetch result. + + val fetcher = + FakeFetcher( + 3 to "three-1", + 3 to "three-2", + 3 to "three-3", + ) + val pipeline = + StoreBuilder.from( + fetcher = fetcher, + ).buildWithTestScope() val fetcher1Collected = mutableListOf>() - val fetcher1Job = async { - pipeline.stream(StoreReadRequest.cached(3, refresh = true)).collect { - fetcher1Collected.add(it) - delay(1_000) + val fetcher1Job = + async { + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).collect { + fetcher1Collected.add(it) + delay(1_000) + } } - } testScope.advanceUntilIdle() assertEquals( listOf( Loading(origin = StoreReadResponseOrigin.Fetcher()), - Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-1") + Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-1"), ), - fetcher1Collected + fetcher1Collected, ) assertEmitsExactly( @@ -783,8 +809,8 @@ class FlowStoreTests { listOf( Data(origin = StoreReadResponseOrigin.Cache, value = "three-1"), Loading(origin = StoreReadResponseOrigin.Fetcher()), - Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-2") - ) + Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-2"), + ), ) assertEmitsExactly( @@ -792,8 +818,8 @@ class FlowStoreTests { listOf( Data(origin = StoreReadResponseOrigin.Cache, value = "three-2"), Loading(origin = StoreReadResponseOrigin.Fetcher()), - Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-3") - ) + Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-3"), + ), ) testScope.advanceUntilIdle() assertEquals( @@ -801,37 +827,42 @@ class FlowStoreTests { Loading(origin = StoreReadResponseOrigin.Fetcher()), Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-1"), Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-2"), - Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-3") + Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-3"), ), - fetcher1Collected + fetcher1Collected, ) fetcher1Job.cancelAndJoin() } @Test - fun givenCacheAndNoSourceOfTruthWhen2CachedStreamsWithRefreshThenFirstStreamsGets2FetchUpdatesAnd2ndStreamGetsCacheResultAndFetchResult() = + fun testCachedStreamsWithRefreshAndFetchUpdates() = testScope.runTest { - val fetcher = FakeFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val pipeline = StoreBuilder.from(fetcher = fetcher) - .buildWithTestScope() + // Given a cache and no source of truth, when 2 cached streams are requested with refresh, + // then the first stream gets 2 fetch updates, and the second stream gets the cache result + // and the fetch result. + + val fetcher = + FakeFetcher( + 3 to "three-1", + 3 to "three-2", + ) + val pipeline = StoreBuilder.from(fetcher = fetcher).buildWithTestScope() val fetcher1Collected = mutableListOf>() - val fetcher1Job = async { - pipeline.stream(StoreReadRequest.cached(3, refresh = true)).collect { - fetcher1Collected.add(it) + val fetcher1Job = + async { + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).collect { + fetcher1Collected.add(it) + } } - } testScope.runCurrent() assertEquals( listOf( Loading(origin = StoreReadResponseOrigin.Fetcher()), - Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-1") + Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-1"), ), - fetcher1Collected + fetcher1Collected, ) assertEmitsExactly( @@ -839,32 +870,31 @@ class FlowStoreTests { listOf( Data(origin = StoreReadResponseOrigin.Cache, value = "three-1"), Loading(origin = StoreReadResponseOrigin.Fetcher()), - Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-2") - ) + Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-2"), + ), ) testScope.runCurrent() assertEquals( listOf( Loading(origin = StoreReadResponseOrigin.Fetcher()), Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-1"), - Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-2") + Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-2"), ), - fetcher1Collected + fetcher1Collected, ) fetcher1Job.cancelAndJoin() } - suspend fun Store.get(request: StoreReadRequest) = - this.stream(request).filter { it.dataOrNull() != null }.first() + suspend fun Store.get(request: StoreReadRequest) = this.stream(request).filter { it.dataOrNull() != null }.first() - suspend fun Store.get(key: Int) = get( - StoreReadRequest.cached( - key = key, - refresh = false + suspend fun Store.get(key: Int) = + get( + StoreReadRequest.cached( + key = key, + refresh = false, + ), ) - ) - private fun StoreBuilder.buildWithTestScope() = - scope(testScope).build() + private fun StoreBuilder.buildWithTestScope() = scope(testScope).build() } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/HotFlowStoreTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/HotFlowStoreTests.kt index efb7fa8c9..fdc905e74 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/HotFlowStoreTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/HotFlowStoreTests.kt @@ -16,57 +16,60 @@ class HotFlowStoreTests { private val testScope = TestScope() @Test - fun givenAHotFetcherWhenTwoCachedAndOneFreshCallThenFetcherIsOnlyCalledTwice() = testScope.runTest { - val fetcher = FakeFlowFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val pipeline = StoreBuilder - .from(fetcher) - .scope(testScope) - .build() + fun givenAHotFetcherWhenTwoCachedAndOneFreshCallThenFetcherIsOnlyCalledTwice() = + testScope.runTest { + val fetcher = + FakeFlowFetcher( + 3 to "three-1", + 3 to "three-2", + ) + val pipeline = + StoreBuilder + .from(fetcher) + .scope(testScope) + .build() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - assertEmitsExactly( - pipeline.stream( - StoreReadRequest.cached(3, refresh = false) - ), - listOf( - StoreReadResponse.Data( - value = "three-1", - origin = StoreReadResponseOrigin.Cache - ) + assertEmitsExactly( + pipeline.stream( + StoreReadRequest.cached(3, refresh = false), + ), + listOf( + StoreReadResponse.Data( + value = "three-1", + origin = StoreReadResponseOrigin.Cache, + ), + ), ) - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() + assertEmitsExactly( + pipeline.stream(StoreReadRequest.fresh(3)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } } private class FakeFlowFetcher( - vararg val responses: Pair + vararg val responses: Pair, ) : Fetcher { private var index = 0 override val name: String? = null diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/KeyTrackerTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/KeyTrackerTests.kt index 5db5bdbd6..a38fb4d10 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/KeyTrackerTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/KeyTrackerTests.kt @@ -22,106 +22,115 @@ class KeyTrackerTests { private val subject = KeyTracker() @Test - fun dontSkipInvalidations() = scope1.runTest { - val collection = scope2.async { - subject.keyFlow('b') - .take(2) - .toList() + fun dontSkipInvalidations() = + scope1.runTest { + val collection = + scope2.async { + subject.keyFlow('b') + .take(2) + .toList() + } + scope2.advanceUntilIdle() + assertEquals(1, subject.activeKeyCount()) + scope2.advanceUntilIdle() + subject.invalidate('a') + subject.invalidate('b') + subject.invalidate('c') + scope2.advanceUntilIdle() + assertEquals(true, collection.isCompleted) + assertEquals(0, subject.activeKeyCount()) } - scope2.advanceUntilIdle() - assertEquals(1, subject.activeKeyCount()) - scope2.advanceUntilIdle() - subject.invalidate('a') - subject.invalidate('b') - subject.invalidate('c') - scope2.advanceUntilIdle() - assertEquals(true, collection.isCompleted) - assertEquals(0, subject.activeKeyCount()) - } @Test - fun multipleScopes() = scope1.runTest { - val keys = 'a'..'z' - val collections = keys.associate { key -> - key to scope2.async { - subject.keyFlow(key) - .take(2) - .toList() - } - } - scope2.advanceUntilIdle() - assertEquals(26, subject.activeKeyCount()) + fun multipleScopes() = + scope1.runTest { + val keys = 'a'..'z' + val collections = + keys.associate { key -> + key to + scope2.async { + subject.keyFlow(key) + .take(2) + .toList() + } + } + scope2.advanceUntilIdle() + assertEquals(26, subject.activeKeyCount()) - scope2.advanceUntilIdle() - keys.forEach { - subject.invalidate(it) - } - scope2.advanceUntilIdle() + scope2.advanceUntilIdle() + keys.forEach { + subject.invalidate(it) + } + scope2.advanceUntilIdle() - collections.forEach { (_, deferred) -> - assertEquals(true, deferred.isCompleted) + collections.forEach { (_, deferred) -> + assertEquals(true, deferred.isCompleted) + } + assertEquals(0, subject.activeKeyCount()) } - assertEquals(0, subject.activeKeyCount()) - } @Test - fun multipleObservers() = scope1.runTest { - val collections = (0..4).map { - scope2.async { - subject.keyFlow('b') - .take(2) - .toList() + fun multipleObservers() = + scope1.runTest { + val collections = + (0..4).map { + scope2.async { + subject.keyFlow('b') + .take(2) + .toList() + } + } + scope2.advanceUntilIdle() + assertEquals(1, subject.activeKeyCount()) + scope2.advanceUntilIdle() + subject.invalidate('a') + subject.invalidate('b') + subject.invalidate('c') + scope2.advanceUntilIdle() + collections.forEach { collection -> + assertEquals(true, collection.isCompleted) } + assertEquals(0, subject.activeKeyCount()) } - scope2.advanceUntilIdle() - assertEquals(1, subject.activeKeyCount()) - scope2.advanceUntilIdle() - subject.invalidate('a') - subject.invalidate('b') - subject.invalidate('c') - scope2.advanceUntilIdle() - collections.forEach { collection -> - assertEquals(true, collection.isCompleted) - } - assertEquals(0, subject.activeKeyCount()) - } @Test - fun keyFlow_notCollected_shouldNotBeTracked() = scope1.runTest { - val flow = subject.keyFlow('b') - assertEquals(0, subject.activeKeyCount()) - scope2.launch { - flow.collectIndexed { index, value -> - assertEquals(1, index) - assertEquals(Unit, value) - assertEquals(1, subject.activeKeyCount()) - cancel() + fun keyFlow_notCollected_shouldNotBeTracked() = + scope1.runTest { + val flow = subject.keyFlow('b') + assertEquals(0, subject.activeKeyCount()) + scope2.launch { + flow.collectIndexed { index, value -> + assertEquals(1, index) + assertEquals(Unit, value) + assertEquals(1, subject.activeKeyCount()) + cancel() + } } + assertEquals(0, subject.activeKeyCount()) } - assertEquals(0, subject.activeKeyCount()) - } @Test - fun keyFlow_trackerShouldRefCount() = scope1.runTest { - val flow = subject.keyFlow('a') - assertEquals(0, subject.activeKeyCount()) - scope2.launch { - flow.collectIndexed { index, value -> - assertEquals(1, index) - assertEquals(Unit, value) - assertEquals(1, subject.activeKeyCount()) - cancel() + fun keyFlow_trackerShouldRefCount() = + scope1.runTest { + val flow = subject.keyFlow('a') + assertEquals(0, subject.activeKeyCount()) + scope2.launch { + flow.collectIndexed { index, value -> + assertEquals(1, index) + assertEquals(Unit, value) + assertEquals(1, subject.activeKeyCount()) + cancel() + } } - } - scope2.launch { - flow.collectIndexed { index, value -> - assertEquals(1, index) - assertEquals(Unit, value) - assertEquals(1, subject.activeKeyCount()) - cancel() + scope2.launch { + flow.collectIndexed { index, value -> + assertEquals(1, index) + assertEquals(Unit, value) + assertEquals(1, subject.activeKeyCount()) + cancel() + } } - } - assertEquals(0, subject.activeKeyCount()) - } + assertEquals(0, subject.activeKeyCount()) + } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/LocalOnlyTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/LocalOnlyTests.kt index ad73b5f17..632478bda 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/LocalOnlyTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/LocalOnlyTests.kt @@ -16,137 +16,153 @@ class LocalOnlyTests { private val testScope = TestScope() @Test - fun givenEmptyMemoryCacheThenCacheOnlyRequestReturnsNoNewData() = testScope.runTest { - val store = StoreBuilder - .from(Fetcher.of { _: Int -> throw RuntimeException("Fetcher shouldn't be hit") }) - .cachePolicy( - MemoryPolicy - .builder() + fun givenEmptyMemoryCacheThenCacheOnlyRequestReturnsNoNewData() = + testScope.runTest { + val store = + StoreBuilder + .from(Fetcher.of { _: Int -> throw RuntimeException("Fetcher shouldn't be hit") }) + .cachePolicy( + MemoryPolicy + .builder() + .build(), + ) .build() - ) - .build() - val response = store.stream(StoreReadRequest.localOnly(0)).first() - assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.Cache), response) - } + val response = store.stream(StoreReadRequest.localOnly(0)).first() + assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.Cache), response) + } @Test - fun givenPrimedMemoryCacheThenCacheOnlyRequestReturnsData() = testScope.runTest { - val fetcherHitCounter = atomic(0) - val store = StoreBuilder - .from( - Fetcher.of { _: Int -> - fetcherHitCounter += 1 - "result" - } - ) - .cachePolicy( - MemoryPolicy - .builder() + fun givenPrimedMemoryCacheThenCacheOnlyRequestReturnsData() = + testScope.runTest { + val fetcherHitCounter = atomic(0) + val store = + StoreBuilder + .from( + Fetcher.of { _: Int -> + fetcherHitCounter += 1 + "result" + }, + ) + .cachePolicy( + MemoryPolicy + .builder() + .build(), + ) .build() - ) - .build() - val a = store.get(0) - assertEquals("result", a) - assertEquals(1, fetcherHitCounter.value) - val response = store.stream(StoreReadRequest.localOnly(0)).first() - assertEquals("result", response.requireData()) - assertEquals(1, fetcherHitCounter.value) - } + val a = store.get(0) + assertEquals("result", a) + assertEquals(1, fetcherHitCounter.value) + val response = store.stream(StoreReadRequest.localOnly(0)).first() + assertEquals("result", response.requireData()) + assertEquals(1, fetcherHitCounter.value) + } @Test - fun givenInvalidMemoryCacheThenCacheOnlyRequestReturnsNoNewData() = testScope.runTest { - val fetcherHitCounter = atomic(0) - val store = StoreBuilder - .from( - Fetcher.of { _: Int -> - fetcherHitCounter += 1 - "result" - } - ) - .cachePolicy( - MemoryPolicy - .builder() - .setExpireAfterWrite(Duration.ZERO) + fun givenInvalidMemoryCacheThenCacheOnlyRequestReturnsNoNewData() = + testScope.runTest { + val fetcherHitCounter = atomic(0) + val store = + StoreBuilder + .from( + Fetcher.of { _: Int -> + fetcherHitCounter += 1 + "result" + }, + ) + .cachePolicy( + MemoryPolicy + .builder() + .setExpireAfterWrite(Duration.ZERO) + .build(), + ) .build() - ) - .build() - val a = store.get(0) - assertEquals("result", a) - assertEquals(1, fetcherHitCounter.value) - val response = store.stream(StoreReadRequest.localOnly(0)).first() - assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.Cache), response) - assertEquals(1, fetcherHitCounter.value) - } + val a = store.get(0) + assertEquals("result", a) + assertEquals(1, fetcherHitCounter.value) + val response = store.stream(StoreReadRequest.localOnly(0)).first() + assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.Cache), response) + assertEquals(1, fetcherHitCounter.value) + } @Test - fun givenEmptyDiskCacheThenCacheOnlyRequestReturnsNoNewData() = testScope.runTest { - val persister = InMemoryPersister() - val store = StoreBuilder - .from( - fetcher = Fetcher.of { _: Int -> throw RuntimeException("Fetcher shouldn't be hit") }, - sourceOfTruth = persister.asSourceOfTruth() - ) - .disableCache() - .build() - val response = store.stream(StoreReadRequest.localOnly(0)).first() - assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.SourceOfTruth), response) - } + fun givenEmptyDiskCacheThenCacheOnlyRequestReturnsNoNewData() = + testScope.runTest { + val persister = InMemoryPersister() + val store = + StoreBuilder + .from( + fetcher = Fetcher.of { _: Int -> throw RuntimeException("Fetcher shouldn't be hit") }, + sourceOfTruth = persister.asSourceOfTruth(), + ) + .disableCache() + .build() + val response = store.stream(StoreReadRequest.localOnly(0)).first() + assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.SourceOfTruth), response) + } @Test - fun givenPrimedDiskCacheThenCacheOnlyRequestReturnsData() = testScope.runTest { - val fetcherHitCounter = atomic(0) - val persister = InMemoryPersister() - val store = StoreBuilder - .from( - fetcher = Fetcher.of { _: Int -> - fetcherHitCounter += 1 - "result" - }, - sourceOfTruth = persister.asSourceOfTruth() - ) - .disableCache() - .build() - val a = store.get(0) - assertEquals("result", a) - assertEquals(1, fetcherHitCounter.value) - val response = store.stream(StoreReadRequest.localOnly(0)).first() - assertEquals("result", response.requireData()) - assertEquals(StoreReadResponseOrigin.SourceOfTruth, response.origin) - assertEquals(1, fetcherHitCounter.value) - } + fun givenPrimedDiskCacheThenCacheOnlyRequestReturnsData() = + testScope.runTest { + val fetcherHitCounter = atomic(0) + val persister = InMemoryPersister() + val store = + StoreBuilder + .from( + fetcher = + Fetcher.of { _: Int -> + fetcherHitCounter += 1 + "result" + }, + sourceOfTruth = persister.asSourceOfTruth(), + ) + .disableCache() + .build() + val a = store.get(0) + assertEquals("result", a) + assertEquals(1, fetcherHitCounter.value) + val response = store.stream(StoreReadRequest.localOnly(0)).first() + assertEquals("result", response.requireData()) + assertEquals(StoreReadResponseOrigin.SourceOfTruth, response.origin) + assertEquals(1, fetcherHitCounter.value) + } @Test - fun givenInvalidDiskCacheThenCacheOnlyRequestReturnsNoNewData() = testScope.runTest { - val fetcherHitCounter = atomic(0) - val persister = InMemoryPersister() - persister.write(0, "result") - val store = StoreBuilder - .from( - fetcher = Fetcher.of { _: Int -> - fetcherHitCounter += 1 - "result" - }, - sourceOfTruth = persister.asSourceOfTruth() - ) - .disableCache() - .validator(Validator.by { false }) - .build() - val a = store.get(0) - assertEquals("result", a) - assertEquals(1, fetcherHitCounter.value) - val response = store.stream(StoreReadRequest.localOnly(0)).first() - assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.SourceOfTruth), response) - assertEquals(1, fetcherHitCounter.value) - } + fun givenInvalidDiskCacheThenCacheOnlyRequestReturnsNoNewData() = + testScope.runTest { + val fetcherHitCounter = atomic(0) + val persister = InMemoryPersister() + persister.write(0, "result") + val store = + StoreBuilder + .from( + fetcher = + Fetcher.of { _: Int -> + fetcherHitCounter += 1 + "result" + }, + sourceOfTruth = persister.asSourceOfTruth(), + ) + .disableCache() + .validator(Validator.by { false }) + .build() + val a = store.get(0) + assertEquals("result", a) + assertEquals(1, fetcherHitCounter.value) + val response = store.stream(StoreReadRequest.localOnly(0)).first() + assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.SourceOfTruth), response) + assertEquals(1, fetcherHitCounter.value) + } @Test - fun givenNoCacheThenCacheOnlyRequestReturnsNoNewData() = testScope.runTest { - val store = StoreBuilder - .from(Fetcher.of { _: Int -> throw RuntimeException("Fetcher shouldn't be hit") }) - .disableCache() - .build() - val response = store.stream(StoreReadRequest.localOnly(0)).first() - assertTrue(response is StoreReadResponse.NoNewData) - assertEquals(StoreReadResponseOrigin.Cache, response.origin) - } + fun givenNoCacheThenCacheOnlyRequestReturnsNoNewData() = + testScope.runTest { + val store = + StoreBuilder + .from(Fetcher.of { _: Int -> throw RuntimeException("Fetcher shouldn't be hit") }) + .disableCache() + .build() + val response = store.stream(StoreReadRequest.localOnly(0)).first() + assertTrue(response is StoreReadResponse.NoNewData) + assertEquals(StoreReadResponseOrigin.Cache, response.origin) + } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/MapIndexedTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/MapIndexedTests.kt index d6891c0ce..a1650e13d 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/MapIndexedTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/MapIndexedTests.kt @@ -13,7 +13,8 @@ class MapIndexedTests { private val scope = TestScope() @Test - fun mapIndexed() = scope.runTest { - assertEmitsExactly(flowOf(5, 6).mapIndexed { index, value -> index to value }, listOf(0 to 5, 1 to 6)) - } + fun mapIndexed() = + scope.runTest { + assertEmitsExactly(flowOf(5, 6).mapIndexed { index, value -> index to value }, listOf(0 to 5, 1 to 6)) + } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt index ad4d3dcd5..200b0ae2c 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt @@ -24,144 +24,158 @@ class SourceOfTruthErrorsTests { private val testScope = TestScope() @Test - fun givenSourceOfTruthWhenWriteFailsThenExceptionShouldBeSendToTheCollector() = testScope.runTest { - val persister = InMemoryPersister() - val fetcher = FakeFetcher( - 3 to "a", - 3 to "b" - ) - val pipeline = StoreBuilder - .from( - fetcher = fetcher, - sourceOfTruth = persister.asSourceOfTruth() - ) - .scope(testScope) - .build() - persister.preWriteCallback = { _, _ -> - throw TestException("i fail") - } + fun givenSourceOfTruthWhenWriteFailsThenExceptionShouldBeSendToTheCollector() = + testScope.runTest { + val persister = InMemoryPersister() + val fetcher = + FakeFetcher( + 3 to "a", + 3 to "b", + ) + val pipeline = + StoreBuilder + .from( + fetcher = fetcher, + sourceOfTruth = persister.asSourceOfTruth(), + ) + .scope(testScope) + .build() + persister.preWriteCallback = { _, _ -> + throw TestException("i fail") + } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( - StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Error.Exception( - error = WriteException( - key = 3, - value = "a", - cause = TestException("i fail") + assertEmitsExactly( + pipeline.stream(StoreReadRequest.fresh(3)), + listOf( + StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), + StoreReadResponse.Error.Exception( + error = + WriteException( + key = 3, + value = "a", + cause = TestException("i fail"), + ), + origin = StoreReadResponseOrigin.SourceOfTruth, ), - origin = StoreReadResponseOrigin.SourceOfTruth - ) + ), ) - ) - } + } @Test - fun givenSourceOfTruthWhenReadFailsThenExceptionShouldBeSendToTheCollector() = testScope.runTest { - val persister = InMemoryPersister() - val fetcher = FakeFetcher( - 3 to "a", - 3 to "b" - ) - val pipeline = StoreBuilder - .from( - fetcher = fetcher, - sourceOfTruth = persister.asSourceOfTruth() - ) - .scope(testScope) - .build() + fun givenSourceOfTruthWhenReadFailsThenExceptionShouldBeSendToTheCollector() = + testScope.runTest { + val persister = InMemoryPersister() + val fetcher = + FakeFetcher( + 3 to "a", + 3 to "b", + ) + val pipeline = + StoreBuilder + .from( + fetcher = fetcher, + sourceOfTruth = persister.asSourceOfTruth(), + ) + .scope(testScope) + .build() - persister.postReadCallback = { _, value -> - throw TestException(value ?: "null") - } + persister.postReadCallback = { _, value -> + throw TestException(value ?: "null") + } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( - StoreReadResponse.Error.Exception( - error = ReadException( - key = 3, - cause = TestException("null") + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)), + listOf( + StoreReadResponse.Error.Exception( + error = + ReadException( + key = 3, + cause = TestException("null"), + ), + origin = StoreReadResponseOrigin.SourceOfTruth, ), - origin = StoreReadResponseOrigin.SourceOfTruth - ), - // after disk fails, we should still invoke fetcher - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - // and after fetcher writes the value, it will trigger another read which will also - // fail - StoreReadResponse.Error.Exception( - error = ReadException( - key = 3, - cause = TestException("a") + // after disk fails, we should still invoke fetcher + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), ), - origin = StoreReadResponseOrigin.SourceOfTruth - ) + // and after fetcher writes the value, it will trigger another read which will also + // fail + StoreReadResponse.Error.Exception( + error = + ReadException( + key = 3, + cause = TestException("a"), + ), + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + ), ) - ) - } + } @Test - fun givenSourceOfTruthWhenFirstWriteFailsThenItShouldKeepReadingFromFetcher() = testScope.runTest { - val persister = InMemoryPersister() - val fetcher = Fetcher.ofFlow { _: Int -> - flowOf("a", "b", "c", "d") - } - val pipeline = StoreBuilder - .from( - fetcher = fetcher, - sourceOfTruth = persister.asSourceOfTruth() - ) - .disableCache() - .scope(testScope) - .build() - persister.preWriteCallback = { _, value -> - if (value in listOf("a", "c")) { - throw TestException(value) + fun givenSourceOfTruthWhenFirstWriteFailsThenItShouldKeepReadingFromFetcher() = + testScope.runTest { + val persister = InMemoryPersister() + val fetcher = + Fetcher.ofFlow { _: Int -> + flowOf("a", "b", "c", "d") + } + val pipeline = + StoreBuilder + .from( + fetcher = fetcher, + sourceOfTruth = persister.asSourceOfTruth(), + ) + .disableCache() + .scope(testScope) + .build() + persister.preWriteCallback = { _, value -> + if (value in listOf("a", "c")) { + throw TestException(value) + } + value } - value - } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - StoreReadResponse.Error.Exception( - error = WriteException( - key = 3, - value = "a", - cause = TestException("a") + assertEmitsExactly( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), ), - origin = StoreReadResponseOrigin.SourceOfTruth - ), - StoreReadResponse.Data( - value = "b", - origin = StoreReadResponseOrigin.Fetcher() - ), - StoreReadResponse.Error.Exception( - error = WriteException( - key = 3, - value = "c", - cause = TestException("c") + StoreReadResponse.Error.Exception( + error = + WriteException( + key = 3, + value = "a", + cause = TestException("a"), + ), + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + StoreReadResponse.Data( + value = "b", + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Error.Exception( + error = + WriteException( + key = 3, + value = "c", + cause = TestException("c"), + ), + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + // disk flow will restart after a failed write (because we stopped it before the + // write attempt starts, so we will get the disk value again). + StoreReadResponse.Data( + value = "b", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + StoreReadResponse.Data( + value = "d", + origin = StoreReadResponseOrigin.Fetcher(), ), - origin = StoreReadResponseOrigin.SourceOfTruth - ), - // disk flow will restart after a failed write (because we stopped it before the - // write attempt starts, so we will get the disk value again). - StoreReadResponse.Data( - value = "b", - origin = StoreReadResponseOrigin.SourceOfTruth ), - StoreReadResponse.Data( - value = "d", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - } + } // @Test // fun givenSourceOfTruthWithFailingWriteWhenAPassiveReaderArrivesThenItShouldReceiveTheNewWriteError() = testScope.runTest { @@ -227,60 +241,64 @@ class SourceOfTruthErrorsTests { // } @Test - fun givenSourceOfTruthWithFailingWriteWhenAPassiveReaderArrivesThenItShouldNotGetErrorsHappenedBefore() = testScope.runTest { - val persister = InMemoryPersister() - val fetcher = Fetcher.ofFlow { - flow { - emit("a") - emit("b") - emit("c") - // now delay, wait for the new subscriber - delay(100) - emit("d") - } - } - val pipeline = StoreBuilder - .from( - fetcher = fetcher, - sourceOfTruth = persister.asSourceOfTruth() - ) - .disableCache() - .scope(testScope) - .build() - persister.preWriteCallback = { _, value -> - if (value in listOf("a", "c")) { - throw TestException(value) + fun givenSourceOfTruthWithFailingWriteWhenAPassiveReaderArrivesThenItShouldNotGetErrorsHappenedBefore() = + testScope.runTest { + val persister = InMemoryPersister() + val fetcher = + Fetcher.ofFlow { + flow { + emit("a") + emit("b") + emit("c") + // now delay, wait for the new subscriber + delay(100) + emit("d") + } + } + val pipeline = + StoreBuilder + .from( + fetcher = fetcher, + sourceOfTruth = persister.asSourceOfTruth(), + ) + .disableCache() + .scope(testScope) + .build() + persister.preWriteCallback = { _, value -> + if (value in listOf("a", "c")) { + throw TestException(value) + } + value } - value - } - val collector = launch { - pipeline.stream( - StoreReadRequest.cached(3, refresh = true) - ).toList() // keep collection hot - } + val collector = + launch { + pipeline.stream( + StoreReadRequest.cached(3, refresh = true), + ).toList() // keep collection hot + } - // miss both failures but arrive before d is fetched - delay(70) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.skipMemory(3, refresh = true)), - listOf( - StoreReadResponse.Data( - value = "b", - origin = StoreReadResponseOrigin.SourceOfTruth - ), - // don't receive the write exception because technically it started before we - // started reading - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() + // miss both failures but arrive before d is fetched + delay(70) + assertEmitsExactly( + pipeline.stream(StoreReadRequest.skipMemory(3, refresh = true)), + listOf( + StoreReadResponse.Data( + value = "b", + origin = StoreReadResponseOrigin.SourceOfTruth, + ), + // don't receive the write exception because technically it started before we + // started reading + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = "d", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Data( - value = "d", - origin = StoreReadResponseOrigin.Fetcher() - ) ) - ) - collector.cancelAndJoin() - } + collector.cancelAndJoin() + } // @Test // fun givenSourceOfTruthWithFailingWriteWhenAFreshValueReaderArrivesThenItShouldNotGetDiskErrorsFromAPendingWrite() = testScope.runTest { @@ -333,14 +351,15 @@ class SourceOfTruthErrorsTests { testScope.runTest { val persister = InMemoryPersister() val fetcher = Fetcher.of { _: Int -> "a" } - val pipeline = StoreBuilder - .from( - fetcher = fetcher, - sourceOfTruth = persister.asSourceOfTruth() - ) - .disableCache() - .scope(testScope) - .build() + val pipeline = + StoreBuilder + .from( + fetcher = fetcher, + sourceOfTruth = persister.asSourceOfTruth(), + ) + .disableCache() + .scope(testScope) + .build() persister.postReadCallback = { _, value -> if (value == null) { throw TestException("first read") @@ -352,19 +371,20 @@ class SourceOfTruthErrorsTests { listOf( StoreReadResponse.Error.Exception( origin = StoreReadResponseOrigin.SourceOfTruth, - error = ReadException( - key = 3, - cause = TestException("first read") - ) + error = + ReadException( + key = 3, + cause = TestException("first read"), + ), ), StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() + origin = StoreReadResponseOrigin.Fetcher(), ), StoreReadResponse.Data( value = "a", - origin = StoreReadResponseOrigin.Fetcher() - ) - ) + origin = StoreReadResponseOrigin.Fetcher(), + ), + ), ) } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthWithBarrierTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthWithBarrierTests.kt index 75fb53f12..1cbab5218 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthWithBarrierTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthWithBarrierTests.kt @@ -51,163 +51,174 @@ class SourceOfTruthWithBarrierTests { }, realWriter = persister::write, realDelete = persister::deleteByKey, - realDeleteAll = persister::deleteAll + realDeleteAll = persister::deleteAll, + ) + private val source = + SourceOfTruthWithBarrier( + delegate = delegate, ) - private val source = SourceOfTruthWithBarrier( - delegate = delegate - ) @Test - fun simple() = testScope.runTest { - val collection = mutableListOf>() + fun simple() = + testScope.runTest { + val collection = mutableListOf>() - launch { - source.reader(1, CompletableDeferred(Unit)).take(2).collect { - collection.add(it) + launch { + source.reader(1, CompletableDeferred(Unit)).take(2).collect { + collection.add(it) + } } - } - delay(500) - source.write(1, "a") - advanceUntilIdle() - assertEquals( - listOf>( - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.SourceOfTruth, - value = null + delay(500) + source.write(1, "a") + advanceUntilIdle() + assertEquals( + listOf>( + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.SourceOfTruth, + value = null, + ), + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = "a", + ), ), - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = "a" - ) - ), - collection - ) - assertEquals(0, source.barrierCount()) - } + collection, + ) + assertEquals(0, source.barrierCount()) + } @Test - fun givenASourceOfTruthWhenDeleteIsCalledThenItIsDelegatedToThePersister() = testScope.runTest { - persister.write(1, "a") - source.delete(1) - assertNull(persister.read(1)) - } + fun givenASourceOfTruthWhenDeleteIsCalledThenItIsDelegatedToThePersister() = + testScope.runTest { + persister.write(1, "a") + source.delete(1) + assertNull(persister.read(1)) + } @Test - fun givenASourceOfTruthWhenDeleteAllIsCalledThenItIsDelegatedToThePersister() = testScope.runTest { - persister.write(1, "a") - persister.write(2, "b") - source.deleteAll() - assertNull(persister.read(1)) - assertNull(persister.read(2)) - } + fun givenASourceOfTruthWhenDeleteAllIsCalledThenItIsDelegatedToThePersister() = + testScope.runTest { + persister.write(1, "a") + persister.write(2, "b") + source.deleteAll() + assertNull(persister.read(1)) + assertNull(persister.read(2)) + } @Test - fun preAndPostWrites() = testScope.runTest { - val collection = mutableListOf>() - source.write(1, "a") + fun preAndPostWrites() = + testScope.runTest { + val collection = mutableListOf>() + source.write(1, "a") - launch { - source.reader(1, CompletableDeferred(Unit)).take(2).collect { - collection.add(it) + launch { + source.reader(1, CompletableDeferred(Unit)).take(2).collect { + collection.add(it) + } } - } - delay(200) + delay(200) - source.write(1, "b") + source.write(1, "b") - advanceUntilIdle() + advanceUntilIdle() - assertEquals( - listOf>( - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.SourceOfTruth, - value = "a" + assertEquals( + listOf>( + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.SourceOfTruth, + value = "a", + ), + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = "b", + ), ), - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = "b" - ) - ), - collection - ) - - assertEquals(0, source.barrierCount()) - } + collection, + ) - @Test - fun givenSourceOfTruthWhenReadFailsThenErrorShouldPropagate() = testScope.runTest { - val exception = RuntimeException("read fails") - persister.postReadCallback = { key, value -> - throw exception + assertEquals(0, source.barrierCount()) } - assertEmitsExactly( - source.reader(1, CompletableDeferred(Unit)), - listOf( - StoreReadResponse.Error.Exception( - origin = StoreReadResponseOrigin.SourceOfTruth, - error = ReadException( - key = 1, - cause = exception - ) - ) - ) - ) - } @Test - fun givenSourceOfTruthWhenReadFailsButThenSucceedsThenErrorShouldPropagateButAlsoTheValue() = testScope.runTest { - var hasThrown = false - val exception = RuntimeException("read fails") - persister.postReadCallback = { _, value -> - if (!hasThrown) { - hasThrown = true + fun givenSourceOfTruthWhenReadFailsThenErrorShouldPropagate() = + testScope.runTest { + val exception = RuntimeException("read fails") + persister.postReadCallback = { key, value -> throw exception } - value + assertEmitsExactly( + source.reader(1, CompletableDeferred(Unit)), + listOf( + StoreReadResponse.Error.Exception( + origin = StoreReadResponseOrigin.SourceOfTruth, + error = + ReadException( + key = 1, + cause = exception, + ), + ), + ), + ) } - val reader = source.reader(1, CompletableDeferred(Unit)) - val collected = mutableListOf>() - val collection = async { - reader.collect { - collected.add(it) + + @Test + fun givenSourceOfTruthWhenReadFailsButThenSucceedsThenErrorShouldPropagateButAlsoTheValue() = + testScope.runTest { + var hasThrown = false + val exception = RuntimeException("read fails") + persister.postReadCallback = { _, value -> + if (!hasThrown) { + hasThrown = true + throw exception + } + value } - } - advanceUntilIdle() - assertEquals( - StoreReadResponse.Error.Exception( - origin = StoreReadResponseOrigin.SourceOfTruth, - error = ReadException( - key = 1, - cause = exception - ) - ), - collected.first() - ) - // make sure it is not cancelled for the read error - assertEquals(true, collection.isActive) - // now insert another, it should trigger another read and emitted to the reader - source.write(1, "a") - advanceUntilIdle() - assertEquals( - listOf>( + val reader = source.reader(1, CompletableDeferred(Unit)) + val collected = mutableListOf>() + val collection = + async { + reader.collect { + collected.add(it) + } + } + advanceUntilIdle() + assertEquals( StoreReadResponse.Error.Exception( origin = StoreReadResponseOrigin.SourceOfTruth, - error = ReadException( - key = 1, - cause = exception - ) + error = + ReadException( + key = 1, + cause = exception, + ), ), - StoreReadResponse.Data( - // this is fetcher since we are using the write API - origin = StoreReadResponseOrigin.Fetcher(), - value = "a" - ) - ), - collected - ) - collection.cancelAndJoin() - } + collected.first(), + ) + // make sure it is not cancelled for the read error + assertEquals(true, collection.isActive) + // now insert another, it should trigger another read and emitted to the reader + source.write(1, "a") + advanceUntilIdle() + assertEquals( + listOf>( + StoreReadResponse.Error.Exception( + origin = StoreReadResponseOrigin.SourceOfTruth, + error = + ReadException( + key = 1, + cause = exception, + ), + ), + StoreReadResponse.Data( + // this is fetcher since we are using the write API + origin = StoreReadResponseOrigin.Fetcher(), + value = "a", + ), + ), + collected, + ) + collection.cancelAndJoin() + } @Test fun givenSourceOfTruthWhenWriteFailsThenErrorShouldPropagate() { @@ -222,34 +233,37 @@ class SourceOfTruthWithBarrierTests { } val reader = source.reader(1, CompletableDeferred(Unit)) val collected = mutableListOf>() - val collection = async { - reader.collect { - collected.add(it) + val collection = + async { + reader.collect { + collected.add(it) + } } - } advanceUntilIdle() source.write(1, failValue) advanceUntilIdle() // make sure collection does not cancel for a write error assertEquals(true, collection.isActive) - val eventsUntilFailure = listOf( - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.SourceOfTruth, - value = null - ), - StoreReadResponse.Error.Exception( - origin = StoreReadResponseOrigin.SourceOfTruth, - error = WriteException( - key = 1, - value = failValue, - cause = exception - ) - ), - StoreReadResponse.Data( - origin = StoreReadResponseOrigin.SourceOfTruth, - value = null + val eventsUntilFailure = + listOf( + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.SourceOfTruth, + value = null, + ), + StoreReadResponse.Error.Exception( + origin = StoreReadResponseOrigin.SourceOfTruth, + error = + WriteException( + key = 1, + value = failValue, + cause = exception, + ), + ), + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.SourceOfTruth, + value = null, + ), ) - ) assertEquals(eventsUntilFailure, collected) advanceUntilIdle() assertEquals(true, collection.isActive) @@ -257,11 +271,12 @@ class SourceOfTruthWithBarrierTests { source.write(1, "succeed") advanceUntilIdle() assertEquals( - eventsUntilFailure + StoreReadResponse.Data( - origin = StoreReadResponseOrigin.Fetcher(), - value = "succeed" - ), - collected + eventsUntilFailure + + StoreReadResponse.Data( + origin = StoreReadResponseOrigin.Fetcher(), + value = "succeed", + ), + collected, ) collection.cancelAndJoin() } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponseTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponseTests.kt index ed779ae89..f1d2c59ff 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponseTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponseTests.kt @@ -6,7 +6,6 @@ import kotlin.test.assertFailsWith import kotlin.test.assertNull class StoreReadResponseTests { - @Test fun requireData() { assertEquals("Foo", StoreReadResponse.Data("Foo", StoreReadResponseOrigin.Fetcher()).requireData()) diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt index f0cd08ca1..b50ec9703 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt @@ -15,26 +15,28 @@ class StoreWithInMemoryCacheTests { private val testScope = TestScope() @Test - fun storeRequestsCanCompleteWhenInMemoryCacheWithAccessExpiryIsAtTheMaximumSize() = testScope.runTest { - val store = StoreBuilder - .from(Fetcher.of { _: Int -> "result" }) - .cachePolicy( - MemoryPolicy - .builder() - .setExpireAfterAccess(1.hours) - .setMaxSize(1) + fun storeRequestsCanCompleteWhenInMemoryCacheWithAccessExpiryIsAtTheMaximumSize() = + testScope.runTest { + val store = + StoreBuilder + .from(Fetcher.of { _: Int -> "result" }) + .cachePolicy( + MemoryPolicy + .builder() + .setExpireAfterAccess(1.hours) + .setMaxSize(1) + .build(), + ) .build() - ) - .build() - val a = store.get(0) - val b = store.get(0) - val c = store.get(1) - val d = store.get(2) + val a = store.get(0) + val b = store.get(0) + val c = store.get(1) + val d = store.get(2) - assertEquals("result", a) - assertEquals("result", b) - assertEquals("result", c) - assertEquals("result", d) - } + assertEquals("result", a) + assertEquals("result", b) + assertEquals("result", c) + assertEquals("result", d) + } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StreamWithoutSourceOfTruthTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StreamWithoutSourceOfTruthTests.kt index 992c9dd3a..8610aa233 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StreamWithoutSourceOfTruthTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StreamWithoutSourceOfTruthTests.kt @@ -19,95 +19,103 @@ class StreamWithoutSourceOfTruthTests { private val testScope = TestScope() @Test - fun streamWithoutPersisterAndCacheEnabled() = testScope.runTest { - val fetcher = FakeFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val pipeline = StoreBuilder.from(fetcher) - .scope(testScope) - .build() - val twoItemsNoRefresh = async { - pipeline.stream( - StoreReadRequest.cached(3, refresh = false) - ).take(3).toList() - } - delay(1_000) // make sure the async block starts first - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - StoreReadResponse.Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() + fun streamWithoutPersisterAndCacheEnabled() = + testScope.runTest { + val fetcher = + FakeFetcher( + 3 to "three-1", + 3 to "three-2", ) + val pipeline = + StoreBuilder.from(fetcher) + .scope(testScope) + .build() + val twoItemsNoRefresh = + async { + pipeline.stream( + StoreReadRequest.cached(3, refresh = false), + ).take(3).toList() + } + delay(1_000) // make sure the async block starts first + assertEmitsExactly( + pipeline.stream(StoreReadRequest.fresh(3)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), + ), ) - ) - assertEquals( - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - StoreReadResponse.Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() + assertEquals( + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) - ), - twoItemsNoRefresh.await() - ) - } + twoItemsNoRefresh.await(), + ) + } @Test - fun streamWithoutPersisterAndCacheDisabled() = testScope.runTest { - val fetcher = FakeFetcher( - 3 to "three-1", - 3 to "three-2" - ) - val pipeline = StoreBuilder.from(fetcher) - .scope(testScope) - .disableCache() - .build() - val twoItemsNoRefresh = async { - pipeline.stream( - StoreReadRequest.cached(3, refresh = false) - ).take(3).toList() - } - delay(1_000) // make sure the async block starts first - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - StoreReadResponse.Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() + fun streamWithoutPersisterAndCacheDisabled() = + testScope.runTest { + val fetcher = + FakeFetcher( + 3 to "three-1", + 3 to "three-2", ) + val pipeline = + StoreBuilder.from(fetcher) + .scope(testScope) + .disableCache() + .build() + val twoItemsNoRefresh = + async { + pipeline.stream( + StoreReadRequest.cached(3, refresh = false), + ).take(3).toList() + } + delay(1_000) // make sure the async block starts first + assertEmitsExactly( + pipeline.stream(StoreReadRequest.fresh(3)), + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), + ), ) - ) - assertEquals( - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher() - ), - StoreReadResponse.Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher() + assertEquals( + listOf( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), + StoreReadResponse.Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), ), - StoreReadResponse.Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher() - ) - ), - twoItemsNoRefresh.await() - ) - } + twoItemsNoRefresh.await(), + ) + } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt index fa5052fb9..72ae4270b 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt @@ -44,109 +44,115 @@ class UpdaterTests { } @Test - fun givenNonEmptyMarketWhenWriteThenStoredAndAPIUpdated() = testScope.runTest { - val ttl = inHours(1) + fun givenNonEmptyMarketWhenWriteThenStoredAndAPIUpdated() = + testScope.runTest { + val ttl = inHours(1) - val converter = NotesConverterProvider().provide() - val validator = NotesValidator() - val updater = NotesUpdaterProvider(api).provide() - val bookkeeper = Bookkeeper.by( - getLastFailedSync = bookkeeping::getLastFailedSync, - setLastFailedSync = bookkeeping::setLastFailedSync, - clear = bookkeeping::clear, - clearAll = bookkeeping::clear - ) + val converter = NotesConverterProvider().provide() + val validator = NotesValidator() + val updater = NotesUpdaterProvider(api).provide() + val bookkeeper = + Bookkeeper.by( + getLastFailedSync = bookkeeping::getLastFailedSync, + setLastFailedSync = bookkeeping::setLastFailedSync, + clear = bookkeeping::clear, + clearAll = bookkeeping::clear, + ) - val store = MutableStoreBuilder.from( - fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) }, - sourceOfTruth = SourceOfTruth.of( - nonFlowReader = { key -> notes.get(key) }, - writer = { key, sot: InputNote -> notes.put(key, sot) }, - delete = { key -> notes.clear(key) }, - deleteAll = { notes.clear() } - ), - converter = converter - ) - .validator(validator) - .build( - updater = updater, - bookkeeper = bookkeeper - ) + val store = + MutableStoreBuilder.from( + fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) }, + sourceOfTruth = + SourceOfTruth.of( + nonFlowReader = { key -> notes.get(key) }, + writer = { key, sot: InputNote -> notes.put(key, sot) }, + delete = { key -> notes.clear(key) }, + deleteAll = { notes.clear() }, + ), + converter = converter, + ) + .validator(validator) + .build( + updater = updater, + bookkeeper = bookkeeper, + ) - val readRequest = StoreReadRequest.fresh(NotesKey.Single(Notes.One.id)) + val readRequest = StoreReadRequest.fresh(NotesKey.Single(Notes.One.id)) - val stream = store.stream(readRequest) + val stream = store.stream(readRequest) - // Read is success - val expected = listOf( - StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Data( - OutputNote(NoteData.Single(Notes.One), ttl = ttl), - StoreReadResponseOrigin.Fetcher() + // Read is success + val expected = + listOf( + StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()), + StoreReadResponse.Data( + OutputNote(NoteData.Single(Notes.One), ttl = ttl), + StoreReadResponseOrigin.Fetcher(), + ), + ) + assertEmitsExactly( + stream, + expected, ) - ) - assertEmitsExactly( - stream, - expected - ) - val newNote = Notes.One.copy(title = "New Title-1") - val writeRequest = StoreWriteRequest.of( - key = NotesKey.Single(Notes.One.id), - value = OutputNote(NoteData.Single(newNote), 0) - ) + val newNote = Notes.One.copy(title = "New Title-1") + val writeRequest = + StoreWriteRequest.of( + key = NotesKey.Single(Notes.One.id), + value = OutputNote(NoteData.Single(newNote), 0), + ) - val storeWriteResponse = store.write(writeRequest) + val storeWriteResponse = store.write(writeRequest) - // Write is success - assertEquals( - StoreWriteResponse.Success.Typed( - NotesWriteResponse( - NotesKey.Single(Notes.One.id), - true - ) - ), - storeWriteResponse - ) + // Write is success + assertEquals( + StoreWriteResponse.Success.Typed( + NotesWriteResponse( + NotesKey.Single(Notes.One.id), + true, + ), + ), + storeWriteResponse, + ) - val cachedReadRequest = - StoreReadRequest.cached(NotesKey.Single(Notes.One.id), refresh = false) - val cachedStream = store.stream(cachedReadRequest) + val cachedReadRequest = + StoreReadRequest.cached(NotesKey.Single(Notes.One.id), refresh = false) + val cachedStream = store.stream(cachedReadRequest) - // Cache + SOT are updated - val firstResponse: StoreReadResponse = cachedStream.first() + // Cache + SOT are updated + val firstResponse: StoreReadResponse = cachedStream.first() // assertEquals( // StoreReadResponse.Data( // OutputNote(NoteData.Single(newNote), ttl = 0), // StoreReadResponseOrigin.Cache // ), - firstResponse + firstResponse // ) - val secondResponse = cachedStream.take(2).last() - assertIs>(secondResponse) - val data: NoteData? = secondResponse.value.data - assertIs(data) - assertNotNull(data) - assertEquals(newNote, data.item) - assertEquals(StoreReadResponseOrigin.SourceOfTruth, secondResponse.origin) - assertNotNull(secondResponse.value.ttl) + val secondResponse = cachedStream.take(2).last() + assertIs>(secondResponse) + val data: NoteData? = secondResponse.value.data + assertIs(data) + assertNotNull(data) + assertEquals(newNote, data.item) + assertEquals(StoreReadResponseOrigin.SourceOfTruth, secondResponse.origin) + assertNotNull(secondResponse.value.ttl) - // API is updated - assertEquals( - StoreWriteResponse.Success.Typed( - NotesWriteResponse( - NotesKey.Single(Notes.One.id), - true - ) - ), - storeWriteResponse - ) - assertEquals( - NetworkNote(NoteData.Single(newNote), ttl = null), - api.db[NotesKey.Single(Notes.One.id)] - ) - } + // API is updated + assertEquals( + StoreWriteResponse.Success.Typed( + NotesWriteResponse( + NotesKey.Single(Notes.One.id), + true, + ), + ), + storeWriteResponse, + ) + assertEquals( + NetworkNote(NoteData.Single(newNote), ttl = null), + api.db[NotesKey.Single(Notes.One.id)], + ) + } @Test fun givenNonEmptyMarketWithValidatorWhenInvalidThenSuccessOriginatingFromFetcher() = @@ -156,44 +162,48 @@ class UpdaterTests { val converter = NotesConverterProvider().provide() val validator = NotesValidator(expiration = inHours(12)) val updater = NotesUpdaterProvider(api).provide() - val bookkeeper = Bookkeeper.by( - getLastFailedSync = bookkeeping::getLastFailedSync, - setLastFailedSync = bookkeeping::setLastFailedSync, - clear = bookkeeping::clear, - clearAll = bookkeeping::clear - ) + val bookkeeper = + Bookkeeper.by( + getLastFailedSync = bookkeeping::getLastFailedSync, + setLastFailedSync = bookkeeping::setLastFailedSync, + clear = bookkeeping::clear, + clearAll = bookkeeping::clear, + ) - val store = MutableStoreBuilder.from( - fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) }, - sourceOfTruth = SourceOfTruth.of( - nonFlowReader = { key -> notes.get(key) }, - writer = { key, sot: InputNote -> notes.put(key, sot) }, - delete = { key -> notes.clear(key) }, - deleteAll = { notes.clear() } - ), - converter = converter - ) - .validator(validator) - .build( - updater = updater, - bookkeeper = bookkeeper + val store = + MutableStoreBuilder.from( + fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) }, + sourceOfTruth = + SourceOfTruth.of( + nonFlowReader = { key -> notes.get(key) }, + writer = { key, sot: InputNote -> notes.put(key, sot) }, + delete = { key -> notes.clear(key) }, + deleteAll = { notes.clear() }, + ), + converter = converter, ) + .validator(validator) + .build( + updater = updater, + bookkeeper = bookkeeper, + ) val readRequest = StoreReadRequest.fresh(NotesKey.Single(Notes.One.id)) val stream = store.stream(readRequest) // Fetch is success and validator is not used - val expected = listOf( - StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()), - StoreReadResponse.Data( - OutputNote(NoteData.Single(Notes.One), ttl = ttl), - StoreReadResponseOrigin.Fetcher() + val expected = + listOf( + StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()), + StoreReadResponse.Data( + OutputNote(NoteData.Single(Notes.One), ttl = ttl), + StoreReadResponseOrigin.Fetcher(), + ), ) - ) assertEmitsExactly( stream, - expected + expected, ) val cachedReadRequest = @@ -211,59 +221,65 @@ class UpdaterTests { StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher(name = null)), StoreReadResponse.Data( OutputNote(NoteData.Single(Notes.One), ttl = ttl), - StoreReadResponseOrigin.Fetcher() - ) - ) + StoreReadResponseOrigin.Fetcher(), + ), + ), ) } @Test - fun givenEmptyMarketWhenWriteThenSuccessResponsesAndApiUpdated() = testScope.runTest { - val converter = NotesConverterProvider().provide() - val validator = NotesValidator() - val updater = NotesUpdaterProvider(api).provide() - val bookkeeper = Bookkeeper.by( - getLastFailedSync = bookkeeping::getLastFailedSync, - setLastFailedSync = bookkeeping::setLastFailedSync, - clear = bookkeeping::clear, - clearAll = bookkeeping::clear - ) - - val store = MutableStoreBuilder.from( - fetcher = Fetcher.ofFlow { key -> - val network = api.get(key) - flow { emit(network) } - }, - sourceOfTruth = SourceOfTruth.of( - nonFlowReader = { key -> notes.get(key) }, - writer = { key, sot -> notes.put(key, sot) }, - delete = { key -> notes.clear(key) }, - deleteAll = { notes.clear() } - ), - converter - ) - .validator(validator) - .build( - updater = updater, - bookkeeper = bookkeeper - ) + fun givenEmptyMarketWhenWriteThenSuccessResponsesAndApiUpdated() = + testScope.runTest { + val converter = NotesConverterProvider().provide() + val validator = NotesValidator() + val updater = NotesUpdaterProvider(api).provide() + val bookkeeper = + Bookkeeper.by( + getLastFailedSync = bookkeeping::getLastFailedSync, + setLastFailedSync = bookkeeping::setLastFailedSync, + clear = bookkeeping::clear, + clearAll = bookkeeping::clear, + ) - val newNote = Notes.One.copy(title = "New Title-1") - val writeRequest = StoreWriteRequest.of( - key = NotesKey.Single(Notes.One.id), - value = OutputNote(NoteData.Single(newNote), 0) - ) - val storeWriteResponse = store.write(writeRequest) + val store = + MutableStoreBuilder.from( + fetcher = + Fetcher.ofFlow { key -> + val network = api.get(key) + flow { emit(network) } + }, + sourceOfTruth = + SourceOfTruth.of( + nonFlowReader = { key -> notes.get(key) }, + writer = { key, sot -> notes.put(key, sot) }, + delete = { key -> notes.clear(key) }, + deleteAll = { notes.clear() }, + ), + converter, + ) + .validator(validator) + .build( + updater = updater, + bookkeeper = bookkeeper, + ) - assertEquals( - StoreWriteResponse.Success.Typed( - NotesWriteResponse( - NotesKey.Single(Notes.One.id), - true + val newNote = Notes.One.copy(title = "New Title-1") + val writeRequest = + StoreWriteRequest.of( + key = NotesKey.Single(Notes.One.id), + value = OutputNote(NoteData.Single(newNote), 0), ) - ), - storeWriteResponse - ) - assertEquals(NetworkNote(NoteData.Single(newNote)), api.db[NotesKey.Single(Notes.One.id)]) - } + val storeWriteResponse = store.write(writeRequest) + + assertEquals( + StoreWriteResponse.Success.Typed( + NotesWriteResponse( + NotesKey.Single(Notes.One.id), + true, + ), + ), + storeWriteResponse, + ) + assertEquals(NetworkNote(NoteData.Single(newNote)), api.db[NotesKey.Single(Notes.One.id)]) + } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ValueFetcherTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ValueFetcherTests.kt index 00f645ebe..3e138597f 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ValueFetcherTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ValueFetcherTests.kt @@ -12,45 +12,50 @@ import kotlin.test.Test @ExperimentalCoroutinesApi @FlowPreview class ValueFetcherTests { - private val testScope = TestScope() @Test - fun givenValueFetcherWhenInvokeThenResultIsWrapped() = testScope.runTest { - val fetcher = Fetcher.ofFlow { flowOf(it * it) } - assertEmitsExactly(fetcher(3), listOf(FetcherResult.Data(value = 9))) - } + fun givenValueFetcherWhenInvokeThenResultIsWrapped() = + testScope.runTest { + val fetcher = Fetcher.ofFlow { flowOf(it * it) } + assertEmitsExactly(fetcher(3), listOf(FetcherResult.Data(value = 9))) + } @Test - fun givenValueFetcherWhenExceptionInFlowThenExceptionReturnedAsResult() = testScope.runTest { - val e = Exception() - val fetcher = Fetcher.ofFlow { - flow { - throw e - } + fun givenValueFetcherWhenExceptionInFlowThenExceptionReturnedAsResult() = + testScope.runTest { + val e = Exception() + val fetcher = + Fetcher.ofFlow { + flow { + throw e + } + } + assertEmitsExactly( + fetcher(3), + listOf(FetcherResult.Error.Exception(e)), + ) } - assertEmitsExactly( - fetcher(3), - listOf(FetcherResult.Error.Exception(e)) - ) - } @Test - fun givenNonFlowValueFetcherWhenInvokeThenResultIsWrapped() = testScope.runTest { - val fetcher = Fetcher.of { it * it } - - assertEmitsExactly( - fetcher(3), - listOf(FetcherResult.Data(value = 9)) - ) - } + fun givenNonFlowValueFetcherWhenInvokeThenResultIsWrapped() = + testScope.runTest { + val fetcher = Fetcher.of { it * it } + + assertEmitsExactly( + fetcher(3), + listOf(FetcherResult.Data(value = 9)), + ) + } @Test - fun givenNonFlowValueFetcherWhenExceptionInFlowThenExceptionReturnedAsResult() = testScope.runTest { - val e = Exception() - val fetcher = Fetcher.of { - throw e + fun givenNonFlowValueFetcherWhenExceptionInFlowThenExceptionReturnedAsResult() = + testScope.runTest { + val e = Exception() + val fetcher = + Fetcher.of { + throw e + } + assertEmitsExactly(fetcher(3), listOf(FetcherResult.Error.Exception(e))) } - assertEmitsExactly(fetcher(3), listOf(FetcherResult.Error.Exception(e))) - } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/AsFlowable.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/AsFlowable.kt index 754c1b182..c13b21990 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/AsFlowable.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/AsFlowable.kt @@ -15,21 +15,24 @@ import org.mobilenativefoundation.store.store5.SourceOfTruth class SimplePersisterAsFlowable( private val reader: suspend (Key) -> Output?, private val writer: suspend (Key, Output) -> Unit, - private val delete: (suspend (Key) -> Unit)? = null + private val delete: (suspend (Key) -> Unit)? = null, ) { - val supportsDelete: Boolean get() = delete != null private val versionTracker = KeyTracker() - fun flowReader(key: Key): Flow = flow { - versionTracker.keyFlow(key).collect { - emit(reader(key)) + fun flowReader(key: Key): Flow = + flow { + versionTracker.keyFlow(key).collect { + emit(reader(key)) + } } - } - suspend fun flowWriter(key: Key, value: Output) { + suspend fun flowWriter( + key: Key, + value: Output, + ) { writer(key, value) versionTracker.invalidate(key) } @@ -46,7 +49,7 @@ fun SimplePersisterAsFlowable.asSourceOfT SourceOfTruth.of( reader = ::flowReader, writer = ::flowWriter, - delete = ::flowDelete.takeIf { supportsDelete } + delete = ::flowDelete.takeIf { supportsDelete }, ) /** @@ -80,18 +83,20 @@ internal class KeyTracker { // from). Otherwise, we might just create many of them that are never observed hence never // cleaned up return flow { - val keyChannel = lock.withLock { - channels.getOrPut(key) { - KeyChannel( - channel = BroadcastChannel(Channel.CONFLATED).apply { - // start w/ an initial value. - trySend(Unit).isSuccess - } - ) - }.also { - it.acquire() // refcount + val keyChannel = + lock.withLock { + channels.getOrPut(key) { + KeyChannel( + channel = + BroadcastChannel(Channel.CONFLATED).apply { + // start w/ an initial value. + trySend(Unit).isSuccess + }, + ) + }.also { + it.acquire() // refcount + } } - } try { emitAll(keyChannel.channel.openSubscription()) } finally { @@ -110,7 +115,7 @@ internal class KeyTracker { */ private data class KeyChannel( val channel: BroadcastChannel, - var collectors: Int = 0 + var collectors: Int = 0, ) { fun acquire() { collectors++ @@ -128,5 +133,5 @@ internal class KeyTracker { fun InMemoryPersister.asFlowable() = SimplePersisterAsFlowable( reader = this::read, - writer = this::write + writer = this::write, ) diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/AssertEmitsExactly.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/AssertEmitsExactly.kt index c15d9bdbd..c6572c9dc 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/AssertEmitsExactly.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/AssertEmitsExactly.kt @@ -5,7 +5,10 @@ import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList import kotlin.test.assertEquals -suspend inline fun assertEmitsExactly(actual: Flow, expected: List) { +suspend inline fun assertEmitsExactly( + actual: Flow, + expected: List, +) { val flow = actual.take(expected.size).toList() assertEquals(expected, flow) } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/FakeFetcher.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/FakeFetcher.kt index 93a35d0a2..dd8c88efe 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/FakeFetcher.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/FakeFetcher.kt @@ -24,11 +24,12 @@ import org.mobilenativefoundation.store.store5.FetcherResult import kotlin.test.assertEquals class FakeFetcher( - private vararg val responses: Pair + private vararg val responses: Pair, ) : Fetcher { private var index = 0 override val name: String? = null override val fallback: Fetcher? = null + override fun invoke(key: Key): Flow> { if (index >= responses.size) { throw AssertionError("unexpected fetch request") @@ -40,19 +41,21 @@ class FakeFetcher( } class FakeFlowingFetcher( - private vararg val responses: Pair + private vararg val responses: Pair, ) : Fetcher { override val name: String? = null override val fallback: Fetcher? = null - override fun invoke(key: Key) = flow { - responses.filter { - it.first == key - }.forEach { - // we delay here to avoid collapsing fetcher values, otherwise, there is a - // possibility that consumer won't be fast enough to get both values before new - // value overrides the previous one. - delay(1) - emit(FetcherResult.Data(it.second)) + + override fun invoke(key: Key) = + flow { + responses.filter { + it.first == key + }.forEach { + // we delay here to avoid collapsing fetcher values, otherwise, there is a + // possibility that consumer won't be fast enough to get both values before new + // value overrides the previous one. + delay(1) + emit(FetcherResult.Data(it.second)) + } } - } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/InMemoryPersister.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/InMemoryPersister.kt index 6a7f02ead..0ba8d1f32 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/InMemoryPersister.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/InMemoryPersister.kt @@ -45,5 +45,5 @@ fun InMemoryPersister.asSourceOfTruth() = nonFlowReader = ::read, writer = ::write, delete = ::deleteByKey, - deleteAll = ::deleteAll + deleteAll = ::deleteAll, ) diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestApi.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestApi.kt index ca937f95d..778021e9d 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestApi.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestApi.kt @@ -1,6 +1,15 @@ package org.mobilenativefoundation.store.store5.util internal interface TestApi { - fun get(key: Key, fail: Boolean = false, ttl: Long? = null): Network? - fun post(key: Key, value: Output, fail: Boolean = false): Response + fun get( + key: Key, + fail: Boolean = false, + ttl: Long? = null, + ): Network? + + fun post( + key: Key, + value: Output, + fail: Boolean = false, + ): Response } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestStoreExt.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestStoreExt.kt index 83a599a35..87e3f1679 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestStoreExt.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestStoreExt.kt @@ -13,7 +13,7 @@ import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed */ suspend fun Store.getData(key: Key) = stream( - StoreReadRequest.cached(key, refresh = false) + StoreReadRequest.cached(key, refresh = false), ).filterNot { it is StoreReadResponse.Loading }.mapIndexed { index, value -> diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NoteCollections.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NoteCollections.kt index fbbb02968..d1d27782d 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NoteCollections.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NoteCollections.kt @@ -4,7 +4,8 @@ import org.mobilenativefoundation.store.store5.util.model.NoteData internal object NoteCollections { object Keys { - const val OneAndTwo = "ONE_AND_TWO" + const val ONE_AND_TWO = "ONE_AND_TWO" } + val OneAndTwo = NoteData.Collection(listOf(Notes.One, Notes.Two)) } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesApi.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesApi.kt index c83da4bd9..567bf964e 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesApi.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesApi.kt @@ -13,7 +13,11 @@ internal class NotesApi : TestApi = mutableMapOf() - fun setLastFailedSync(key: NotesKey, timestamp: Long, fail: Boolean = false): Boolean { + + fun setLastFailedSync( + key: NotesKey, + timestamp: Long, + fail: Boolean = false, + ): Boolean { if (fail) { throw Exception() } @@ -10,7 +15,10 @@ class NotesBookkeeping { return true } - fun getLastFailedSync(key: NotesKey, fail: Boolean = false): Long? { + fun getLastFailedSync( + key: NotesKey, + fail: Boolean = false, + ): Long? { if (fail) { throw Exception() } @@ -18,7 +26,10 @@ class NotesBookkeeping { return log[key] } - fun clear(key: NotesKey, fail: Boolean = false): Boolean { + fun clear( + key: NotesKey, + fail: Boolean = false, + ): Boolean { if (fail) { throw Exception() } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesConverterProvider.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesConverterProvider.kt index 5e8efbf43..ee89216da 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesConverterProvider.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesConverterProvider.kt @@ -13,7 +13,7 @@ internal class NotesConverterProvider { .fromNetworkToLocal { value: NetworkNote -> InputNote( data = value.data, - ttl = value.ttl ?: inHours(12) + ttl = value.ttl ?: inHours(12), ) } .build() diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesDatabase.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesDatabase.kt index 0033fa160..063a0a3a9 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesDatabase.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesDatabase.kt @@ -5,7 +5,12 @@ import org.mobilenativefoundation.store.store5.util.model.OutputNote internal class NotesDatabase { private val db: MutableMap = mutableMapOf() - fun put(key: NotesKey, input: InputNote, fail: Boolean = false): Boolean { + + fun put( + key: NotesKey, + input: InputNote, + fail: Boolean = false, + ): Boolean { if (fail) { throw Exception() } @@ -14,7 +19,10 @@ internal class NotesDatabase { return true } - fun get(key: NotesKey, fail: Boolean = false): OutputNote? { + fun get( + key: NotesKey, + fail: Boolean = false, + ): OutputNote? { if (fail) { throw Exception() } @@ -22,7 +30,10 @@ internal class NotesDatabase { return db[key] } - fun clear(key: NotesKey, fail: Boolean = false): Boolean { + fun clear( + key: NotesKey, + fail: Boolean = false, + ): Boolean { if (fail) { throw Exception() } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesKey.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesKey.kt index 40f3242f4..25058d3b6 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesKey.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesKey.kt @@ -2,5 +2,6 @@ package org.mobilenativefoundation.store.store5.util.fake sealed class NotesKey { data class Single(val id: String) : NotesKey() + data class Collection(val id: String) : NotesKey() } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesUpdaterProvider.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesUpdaterProvider.kt index 419700975..35529028e 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesUpdaterProvider.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesUpdaterProvider.kt @@ -7,14 +7,15 @@ import org.mobilenativefoundation.store.store5.util.model.NotesWriteResponse import org.mobilenativefoundation.store.store5.util.model.OutputNote internal class NotesUpdaterProvider(private val api: NotesApi) { - fun provide(): Updater = Updater.by( - post = { key, input -> - val response = api.post(key, InputNote(input.data, input.ttl ?: 0)) - if (response.ok) { - UpdaterResult.Success.Typed(response) - } else { - UpdaterResult.Error.Message("Failed to sync") - } - } - ) + fun provide(): Updater = + Updater.by( + post = { key, input -> + val response = api.post(key, InputNote(input.data, input.ttl ?: 0)) + if (response.ok) { + UpdaterResult.Success.Typed(response) + } else { + UpdaterResult.Error.Message("Failed to sync") + } + }, + ) } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesValidator.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesValidator.kt index 94afac4ad..bf7296f22 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesValidator.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesValidator.kt @@ -5,8 +5,9 @@ import org.mobilenativefoundation.store.store5.impl.extensions.now import org.mobilenativefoundation.store.store5.util.model.OutputNote internal class NotesValidator(private val expiration: Long = now()) : Validator { - override suspend fun isValid(item: OutputNote): Boolean = when { - item.ttl == 0L -> true - else -> item.ttl > expiration - } + override suspend fun isValid(item: OutputNote): Boolean = + when { + item.ttl == 0L -> true + else -> item.ttl > expiration + } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/Page.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/Page.kt index d6d372579..ccc456277 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/Page.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/Page.kt @@ -3,7 +3,7 @@ package org.mobilenativefoundation.store.store5.util.fake.fallback sealed class Page { data class Data( val title: String, - val ttl: Long? = null + val ttl: Long? = null, ) : Page() object Empty : Page() diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/PagesDatabase.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/PagesDatabase.kt index 48995cdfd..3c7a0f245 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/PagesDatabase.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/PagesDatabase.kt @@ -3,7 +3,10 @@ package org.mobilenativefoundation.store.store5.util.fake.fallback class PagesDatabase { private val db: MutableMap = mutableMapOf() - fun put(key: String, input: Page): Boolean { + fun put( + key: String, + input: Page, + ): Boolean { db[key] = input return true } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/PrimaryPagesApi.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/PrimaryPagesApi.kt index fd6157401..7c8126702 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/PrimaryPagesApi.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/fallback/PrimaryPagesApi.kt @@ -15,7 +15,11 @@ class PrimaryPagesApi { db["3"] = Page.Data("3") } - fun fetch(key: String, fail: Boolean, ttl: Long?): Page { + fun fetch( + key: String, + fail: Boolean, + ttl: Long?, + ): Page { if (fail) { throw Exception() } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/model/NoteData.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/model/NoteData.kt index dc0808c1b..cce0fc5fa 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/model/NoteData.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/model/NoteData.kt @@ -4,12 +4,13 @@ import org.mobilenativefoundation.store.store5.util.fake.NotesKey internal sealed class NoteData { data class Single(val item: Note) : NoteData() + data class Collection(val items: List) : NoteData() } internal data class NotesWriteResponse( val key: NotesKey, - val ok: Boolean + val ok: Boolean, ) internal data class NetworkNote( @@ -24,11 +25,11 @@ internal data class InputNote( internal data class OutputNote( val data: NoteData? = null, - val ttl: Long + val ttl: Long, ) internal data class Note( val id: String, val title: String, - val content: String + val content: String, )