Skip to content

Commit

Permalink
Add Write + Conflict Resolution (#496)
Browse files Browse the repository at this point in the history
* Stub Store write

Signed-off-by: mramotar <[email protected]>

* Format

Signed-off-by: mramotar <[email protected]>

* Compile

Signed-off-by: mramotar <[email protected]>

* Fix tests

Signed-off-by: mramotar <[email protected]>

* Stash M1

Signed-off-by: mramotar <[email protected]>

* Make Updater and Bookkeeper optional

Signed-off-by: Matt Ramotar <[email protected]>

* Add conflict resolution

Signed-off-by: Matt Ramotar <[email protected]>

* Cover simple write

Signed-off-by: Matt Ramotar <[email protected]>

* Add MutableStore

Signed-off-by: mramotar <[email protected]>

* Add RealMutableStore

Signed-off-by: mramotar <[email protected]>

* Update workflows

Signed-off-by: mramotar <[email protected]>

* Format

Signed-off-by: mramotar <[email protected]>

* Remove references to Market

Signed-off-by: mramotar <[email protected]>

* Remove Converter interface

Signed-off-by: mramotar <[email protected]>

* Move Converter typealias

Signed-off-by: mramotar <[email protected]>

* Remove Google copyright

Signed-off-by: mramotar <[email protected]>

* Update CHANGELOG.md

Signed-off-by: mramotar <[email protected]>

Signed-off-by: mramotar <[email protected]>
Signed-off-by: Matt Ramotar <[email protected]>
  • Loading branch information
matt-ramotar authored and aclassen committed Jan 16, 2023
1 parent 1e09dac commit ab03aa1
Show file tree
Hide file tree
Showing 67 changed files with 1,722 additions and 797 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/.ci_test_and_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.ref }}

- name: Set up our JDK environment
uses: actions/setup-java@v2
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Run check with Gradle Wrapper
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Changelog

## [Unreleased]
* Introduce MutableStore
* Implement RealMutableStore with Store delegate
* Extract Store and MutableStore methods to use cases

## [5.0.0-alpha03] (2022-12-18)

Expand Down
4 changes: 4 additions & 0 deletions buildSrc/src/main/kotlin/Deps.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,8 @@ object Deps {
const val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Version.kotlinxCoroutines}"
const val junit = "junit:junit:${Version.junit}"
}

object Touchlab {
const val kermit = "co.touchlab:kermit:${Version.kermit}"
}
}
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/Version.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ object Version {
const val junit = "4.13.2"
const val kotlinxCoroutines = "1.6.4"
const val kotlinxSerialization = "1.4.0"
const val kermit = "1.2.2"
const val testCore = "1.4.0"
const val kmmBridge = "0.3.2"
const val ktlint = "0.39.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package org.mobilenativefoundation.store.cache5

import kotlin.time.Duration

class CacheBuilder<Key : Any, Value : Any> {
class CacheBuilder<Key : Any, CommonRepresentation : Any> {
internal var concurrencyLevel = 4
private set
internal val initialCapacity = 16
Expand All @@ -14,41 +14,41 @@ class CacheBuilder<Key : Any, Value : Any> {
private set
internal var expireAfterWrite: Duration = Duration.INFINITE
private set
internal var weigher: Weigher<Key, Value>? = null
internal var weigher: Weigher<Key, CommonRepresentation>? = null
private set
internal var ticker: Ticker? = null
private set

fun concurrencyLevel(producer: () -> Int): CacheBuilder<Key, Value> = apply {
fun concurrencyLevel(producer: () -> Int): CacheBuilder<Key, CommonRepresentation> = apply {
concurrencyLevel = producer.invoke()
}

fun maximumSize(maximumSize: Long): CacheBuilder<Key, Value> = apply {
fun maximumSize(maximumSize: Long): CacheBuilder<Key, CommonRepresentation> = apply {
if (maximumSize < 0) {
throw IllegalArgumentException("Maximum size must be non-negative.")
}
this.maximumSize = maximumSize
}

fun expireAfterAccess(duration: Duration): CacheBuilder<Key, Value> = apply {
fun expireAfterAccess(duration: Duration): CacheBuilder<Key, CommonRepresentation> = apply {
if (duration.isNegative()) {
throw IllegalArgumentException("Duration must be non-negative.")
}
expireAfterAccess = duration
}

fun expireAfterWrite(duration: Duration): CacheBuilder<Key, Value> = apply {
fun expireAfterWrite(duration: Duration): CacheBuilder<Key, CommonRepresentation> = apply {
if (duration.isNegative()) {
throw IllegalArgumentException("Duration must be non-negative.")
}
expireAfterWrite = duration
}

fun ticker(ticker: Ticker): CacheBuilder<Key, Value> = apply {
fun ticker(ticker: Ticker): CacheBuilder<Key, CommonRepresentation> = apply {
this.ticker = ticker
}

fun weigher(maximumWeight: Long, weigher: Weigher<Key, Value>): CacheBuilder<Key, Value> = apply {
fun weigher(maximumWeight: Long, weigher: Weigher<Key, CommonRepresentation>): CacheBuilder<Key, CommonRepresentation> = apply {
if (maximumWeight < 0) {
throw IllegalArgumentException("Maximum weight must be non-negative.")
}
Expand All @@ -57,7 +57,7 @@ class CacheBuilder<Key : Any, Value : Any> {
this.weigher = weigher
}

fun build(): Cache<Key, Value> {
fun build(): Cache<Key, CommonRepresentation> {
if (maximumSize != -1L && weigher != null) {
throw IllegalStateException("Maximum size cannot be combined with weigher.")
}
Expand Down
1 change: 1 addition & 0 deletions store/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ kotlin {
implementation(serializationCore)
implementation(dateTime)
}
implementation(Deps.Touchlab.kermit)
implementation(project(":multicast"))
implementation(project(":cache"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.mobilenativefoundation.store.store5

import org.mobilenativefoundation.store.store5.impl.RealBookkeeper
import org.mobilenativefoundation.store.store5.impl.RealMutableStore
import org.mobilenativefoundation.store.store5.impl.extensions.now

/**
* Tracks when local changes fail to sync with network.
* @see [RealMutableStore] usage to persist write request failures and eagerly resolve conflicts before completing a read request.
*/

interface Bookkeeper<Key : Any> {
suspend fun getLastFailedSync(key: Key): Long?
suspend fun setLastFailedSync(key: Key, timestamp: Long = now()): Boolean
suspend fun clear(key: Key): Boolean
suspend fun clearAll(): Boolean

companion object {
fun <Key : Any> by(
getLastFailedSync: suspend (key: Key) -> Long?,
setLastFailedSync: suspend (key: Key, timestamp: Long) -> Boolean,
clear: suspend (key: Key) -> Boolean,
clearAll: suspend () -> Boolean
): Bookkeeper<Key> = RealBookkeeper(getLastFailedSync, setLastFailedSync, clear, clearAll)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.mobilenativefoundation.store.store5

interface Clear {
interface Key<Key : Any> {
/**
* 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].
*/
suspend fun clear(key: Key)
}

interface All {
/**
* Purge all entries from memory and disk cache.
* Persistent storage will only be cleared if a clear function was passed to
* [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store].
*/
@ExperimentalStoreApi
suspend fun clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.mobilenativefoundation.store.store5

import org.mobilenativefoundation.store.store5.impl.RealItemValidator

/**
* Enables custom validation of [Store] items.
* @see [StoreReadRequest]
*/
interface ItemValidator<CommonRepresentation : Any> {
/**
* Determines whether a [Store] item is valid.
* If invalid, [MutableStore] will get the latest network value using [Fetcher].
* [MutableStore] will not validate network responses.
*/
suspend fun isValid(item: CommonRepresentation): Boolean

companion object {
fun <CommonRepresentation : Any> by(
validator: suspend (item: CommonRepresentation) -> Boolean
): ItemValidator<CommonRepresentation> = RealItemValidator(validator)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.mobilenativefoundation.store.store5

import org.mobilenativefoundation.store.cache5.Cache
import kotlin.time.Duration
import kotlin.time.ExperimentalTime

fun interface Weigher<in K : Any, in V : Any> {
/**
Expand All @@ -18,17 +18,10 @@ internal object OneWeigher : Weigher<Any, Any> {
}

/**
* MemoryPolicy holds all required info to create MemoryCache
*
*
* This class is used, in order to define the appropriate parameters for the Memory [com.dropbox.android.external.cache3.Cache]
* to be built.
*
*
* MemoryPolicy is used by a [Store]
* and defines the in-memory cache behavior.
* Defines behavior of in-memory [Cache].
* Used by [Store].
* @see [Store]
*/
@ExperimentalTime
class MemoryPolicy<in Key : Any, in Value : Any> internal constructor(
val expireAfterWrite: Duration,
val expireAfterAccess: Duration,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.mobilenativefoundation.store.store5

interface MutableStore<Key : Any, CommonRepresentation : Any> :
Read.StreamWithConflictResolution<Key, CommonRepresentation>,
Write<Key, CommonRepresentation>,
Write.Stream<Key, CommonRepresentation>,
Clear.Key<Key>,
Clear
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.mobilenativefoundation.store.store5

data class OnFetcherCompletion<NetworkRepresentation : Any>(
val onSuccess: (FetcherResult.Data<NetworkRepresentation>) -> Unit,
val onFailure: (FetcherResult.Error) -> Unit
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.mobilenativefoundation.store.store5

data class OnUpdaterCompletion<NetworkWriteResponse : Any>(
val onSuccess: (UpdaterResult.Success) -> Unit,
val onFailure: (UpdaterResult.Error) -> Unit
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.mobilenativefoundation.store.store5

import kotlinx.coroutines.flow.Flow

interface Read {
interface Stream<Key : Any, CommonRepresentation : Any> {
/**
* Return a flow for the given key
* @param request - see [StoreReadRequest] for configurations
*/
fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>>
}

interface StreamWithConflictResolution<Key : Any, CommonRepresentation : Any> {
fun <NetworkWriteResponse : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,34 +39,34 @@ import kotlin.jvm.JvmName
* a common flowing API.
*
* A source of truth is usually backed by local storage. It's purpose is to eliminate the need
* for waiting on network update before local modifications are available (via [Store.stream]).
* for waiting on network update before local modifications are available (via [Store.Stream.read]).
*
* For maximal flexibility, [writer]'s record type ([Input]] and [reader]'s record type
* ([Output]) are not identical. This allows us to read one type of objects from network and
* transform them to another type when placing them in local storage.
*
*/
interface SourceOfTruth<Key, Input, Output> {
interface SourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any> {

/**
* Used by [Store] to read records from the source of truth.
*
* @param key The key to read for.
*/
fun reader(key: Key): Flow<Output?>
fun reader(key: Key): Flow<SourceOfTruthRepresentation?>

/**
* Used by [Store] to write records **coming in from the fetcher (network)** to the source of
* truth.
*
* **Note:** [Store] currently does not support updating the source of truth with local user
* updates (i.e writing record of type [Output]). However, any changes in the local database
* will still be visible via [Store.stream] APIs as long as you are using a local storage that
* will still be visible via [Store.Stream.read] APIs as long as you are using a local storage that
* supports observability (e.g. Room, SQLDelight, Realm).
*
* @param key The key to update for.
*/
suspend fun write(key: Key, value: Input)
suspend fun write(key: Key, value: SourceOfTruthRepresentation)

/**
* Used by [Store] to delete records in the source of truth for the given key.
Expand All @@ -90,12 +90,12 @@ interface SourceOfTruth<Key, Input, Output> {
* @param delete function for deleting records in the source of truth for the given key
* @param deleteAll function for deleting all records in the source of truth
*/
fun <Key : Any, Input : Any, Output : Any> of(
nonFlowReader: suspend (Key) -> Output?,
writer: suspend (Key, Input) -> Unit,
fun <Key : Any, SourceOfTruthRepresentation : Any> of(
nonFlowReader: suspend (Key) -> SourceOfTruthRepresentation?,
writer: suspend (Key, SourceOfTruthRepresentation) -> Unit,
delete: (suspend (Key) -> Unit)? = null,
deleteAll: (suspend () -> Unit)? = null
): SourceOfTruth<Key, Input, Output> = PersistentNonFlowingSourceOfTruth(
): SourceOfTruth<Key, SourceOfTruthRepresentation> = PersistentNonFlowingSourceOfTruth(
realReader = nonFlowReader,
realWriter = writer,
realDelete = delete,
Expand All @@ -112,12 +112,12 @@ interface SourceOfTruth<Key, Input, Output> {
* @param deleteAll function for deleting all records in the source of truth
*/
@JvmName("ofFlow")
fun <Key : Any, Input : Any, Output : Any> of(
reader: (Key) -> Flow<Output?>,
writer: suspend (Key, Input) -> Unit,
fun <Key : Any, SourceOfTruthRepresentation : Any> of(
reader: (Key) -> Flow<SourceOfTruthRepresentation?>,
writer: suspend (Key, SourceOfTruthRepresentation) -> Unit,
delete: (suspend (Key) -> Unit)? = null,
deleteAll: (suspend () -> Unit)? = null
): SourceOfTruth<Key, Input, Output> = PersistentSourceOfTruth(
): SourceOfTruth<Key, SourceOfTruthRepresentation> = PersistentSourceOfTruth(
realReader = reader,
realWriter = writer,
realDelete = delete,
Expand All @@ -128,7 +128,7 @@ interface SourceOfTruth<Key, Input, Output> {
/**
* The exception provided when a write operation fails in SourceOfTruth.
*
* see [StoreResponse.Error.Exception]
* see [StoreReadResponse.Error.Exception]
*/
class WriteException(
/**
Expand Down Expand Up @@ -169,7 +169,7 @@ interface SourceOfTruth<Key, Input, Output> {
/**
* Exception created when a [reader] throws an exception.
*
* see [StoreResponse.Error.Exception]
* see [StoreReadResponse.Error.Exception]
*/
class ReadException(
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.mobilenativefoundation.store.store5

import kotlinx.coroutines.flow.Flow

/**
* A Store is responsible for managing a particular data request.
*
Expand Down Expand Up @@ -33,26 +31,7 @@ import kotlinx.coroutines.flow.Flow
* }
*
*/
interface Store<Key : Any, Output : Any> {

/**
* Return a flow for the given key
* @param request - see [StoreRequest] for configurations
*/
fun stream(request: StoreRequest<Key>): Flow<StoreResponse<Output>>

/**
* 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].
*/
suspend fun clear(key: Key)

/**
* Purge all entries from memory and disk cache.
* Persistent storage will only be cleared if a deleteAll function was passed to
* [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store].
*/
@ExperimentalStoreApi
suspend fun clearAll()
}
interface Store<Key : Any, CommonRepresentation : Any> :
Read.Stream<Key, CommonRepresentation>,
Clear.Key<Key>,
Clear.All
Loading

0 comments on commit ab03aa1

Please sign in to comment.