Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Write + Conflict Resolution #496

Merged
merged 17 commits into from
Dec 21, 2022
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
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
@@ -1,11 +1,8 @@
package org.mobilenativefoundation.store.store5

import kotlinx.coroutines.flow.Flow

interface MutableStore<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> : Store<Key, CommonRepresentation> {
@ExperimentalStoreApi
fun stream(stream: Flow<StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>>): Flow<StoreWriteResponse<NetworkWriteResponse>>

@ExperimentalStoreApi
suspend fun write(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): StoreWriteResponse<NetworkWriteResponse>
}
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
@@ -1,6 +1,6 @@
package org.mobilenativefoundation.store.store5

data class OnUpdaterCompletion<NetworkWriteResponse : Any>(
val onSuccess: (UpdaterResult.Success<NetworkWriteResponse>) -> Unit,
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 {
digitalbuddha marked this conversation as resolved.
Show resolved Hide resolved
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
@@ -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,25 +31,7 @@ import kotlinx.coroutines.flow.Flow
* }
*
*/
interface Store<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>>

/**
* 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 clear function was passed to
* [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store].
*/
@ExperimentalStoreApi
suspend fun clear()
}
interface Store<Key : Any, CommonRepresentation : Any> :
Read.Stream<Key, CommonRepresentation>,
Clear.Key<Key>,
Clear.All
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,43 @@
package org.mobilenativefoundation.store.store5

import kotlinx.coroutines.CoroutineScope
import org.mobilenativefoundation.store.store5.impl.RealStoreBuilder
import org.mobilenativefoundation.store.store5.impl.storeBuilderFrom
import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcher
import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherAndSourceOfTruth

/**
* Main entry point for creating a [Store].
*/
interface StoreBuilder<Key : Any, CommonRepresentation : Any> {
interface StoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> {
fun build(): Store<Key, CommonRepresentation>

fun <NetworkWriteResponse : Any> build(
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>,
bookkeeper: Bookkeeper<Key>
): MutableStore<Key, CommonRepresentation>

/**
* A store multicasts same [CommonRepresentation] value to many consumers (Similar to RxJava.share()), by default
* [Store] will open a global scope for management of shared responses, if instead you'd like to control
* the scope that sharing/multicasting happens in you can pass a @param [scope]
*
* @param scope - scope to use for sharing
*/
fun scope(scope: CoroutineScope): StoreBuilder<Key, CommonRepresentation>
fun scope(scope: CoroutineScope): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>

/**
* controls eviction policy for a store cache, use [MemoryPolicy.MemoryPolicyBuilder] to configure a TTL
* or size based eviction
* Example: MemoryPolicy.builder().setExpireAfterWrite(10.seconds).build()
*/
fun cachePolicy(memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?): StoreBuilder<Key, CommonRepresentation>
fun cachePolicy(memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>

/**
* by default a Store caches in memory with a default policy of max items = 100
*/
fun disableCache(): StoreBuilder<Key, CommonRepresentation>
fun disableCache(): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>

fun converter(converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>):
StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>

companion object {

Expand All @@ -53,62 +61,20 @@ interface StoreBuilder<Key : Any, CommonRepresentation : Any> {
*
* @param fetcher a [Fetcher] flow of network records.
*/
fun <Key : Any, CommonRepresentation : Any> from(
fetcher: Fetcher<Key, *>,
): StoreBuilder<Key, CommonRepresentation> = storeBuilderFrom(fetcher = fetcher)
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any> from(
fetcher: Fetcher<Key, NetworkRepresentation>,
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, *> = storeBuilderFromFetcher(fetcher = fetcher)

/**
* Creates a new [StoreBuilder] from a [Fetcher] and a [SourceOfTruth].
*
* @param fetcher a function for fetching a flow of network records.
* @param sourceOfTruth a [SourceOfTruth] for the store.
*/
fun <Key : Any, CommonRepresentation : Any> from(
fetcher: Fetcher<Key, *>,
sourceOfTruth: SourceOfTruth<Key, *>
): StoreBuilder<Key, CommonRepresentation> = storeBuilderFrom(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
}
}

interface MutableStoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any, NetworkWriteResponse : Any> :
StoreBuilder<Key, CommonRepresentation> {
override fun build(): MutableStore<Key, CommonRepresentation, NetworkWriteResponse>

/**
* @see [StoreBuilder.scope]
*/
override fun scope(scope: CoroutineScope): MutableStoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation, NetworkWriteResponse>

/**
* @see [StoreBuilder.cachePolicy]
*/
override fun cachePolicy(memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?): MutableStoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation, NetworkWriteResponse>

/**
* @see [StoreBuilder.disableCache]
*/
override fun disableCache(): MutableStoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation, NetworkWriteResponse>

fun converter(converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>): MutableStoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation, NetworkWriteResponse>

companion object {
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any, NetworkWriteResponse : Any> from(
fetcher: Fetcher<Key, NetworkRepresentation>,
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>? = null,
bookkeeper: Bookkeeper<Key>? = null,
): MutableStoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation, NetworkWriteResponse> =
RealStoreBuilder(fetcher, updater, bookkeeper)

fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any, NetworkWriteResponse : Any> from(
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> from(
fetcher: Fetcher<Key, NetworkRepresentation>,
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>? = null,
bookkeeper: Bookkeeper<Key>? = null,
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>
): MutableStoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation, NetworkWriteResponse> = RealStoreBuilder(
fetcher = fetcher,
updater = updater,
bookkeeper = bookkeeper,
sourceOfTruth = sourceOfTruth
)
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> =
storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ interface StoreWriteRequest<Key : Any, CommonRepresentation : Any, NetworkWriteR
val key: Key
val input: CommonRepresentation
val created: Long
val onCompletions: List<OnStoreWriteCompletion<NetworkWriteResponse>>?
val onCompletions: List<OnStoreWriteCompletion>?

companion object {
fun <Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> of(
key: Key,
input: CommonRepresentation,
onCompletions: List<OnStoreWriteCompletion<NetworkWriteResponse>>? = null,
onCompletions: List<OnStoreWriteCompletion>? = null,
created: Long = Clock.System.now().toEpochMilliseconds(),
): StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse> = RealStoreWriteRequest(key, input, created, onCompletions)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.mobilenativefoundation.store.store5

sealed class StoreWriteResponse<out NetworkWriteResponse : Any> {
data class Success<NetworkWriteResponse : Any>(val value: NetworkWriteResponse) : StoreWriteResponse<NetworkWriteResponse>()
sealed class Error : StoreWriteResponse<Nothing>() {
sealed class StoreWriteResponse {
sealed class Success : StoreWriteResponse() {
data class Typed<NetworkWriteResponse : Any>(val value: NetworkWriteResponse) : 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()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package org.mobilenativefoundation.store.store5

typealias PostRequest<Key, CommonRepresentation, NetworkWriteResponse> = suspend (key: Key, input: CommonRepresentation) -> UpdaterResult<NetworkWriteResponse>
typealias PostRequest<Key, CommonRepresentation> = suspend (key: Key, input: CommonRepresentation) -> UpdaterResult

/**
* Posts data to remote data source.
* @see [WriteRequest]
* @see [StoreWriteRequest]
*/
interface Updater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> {
/**
* Makes HTTP POST request.
*/
suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult<NetworkWriteResponse>
suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult

/**
* Executes on network completion.
Expand All @@ -19,7 +19,7 @@ interface Updater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse :

companion object {
fun <Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> by(
post: PostRequest<Key, CommonRepresentation, NetworkWriteResponse>,
post: PostRequest<Key, CommonRepresentation>,
onCompletion: OnUpdaterCompletion<NetworkWriteResponse>? = null,
): Updater<Key, CommonRepresentation, NetworkWriteResponse> = RealNetworkUpdater(
post, onCompletion
Expand All @@ -28,8 +28,8 @@ interface Updater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse :
}

internal class RealNetworkUpdater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any>(
private val realPost: PostRequest<Key, CommonRepresentation, NetworkWriteResponse>,
private val realPost: PostRequest<Key, CommonRepresentation>,
override val onCompletion: OnUpdaterCompletion<NetworkWriteResponse>?,
) : Updater<Key, CommonRepresentation, NetworkWriteResponse> {
override suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult<NetworkWriteResponse> = realPost(key, input)
override suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult = realPost(key, input)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package org.mobilenativefoundation.store.store5

sealed class UpdaterResult<out NetworkWriteResponse : Any> {
data class Success<NetworkWriteResponse : Any>(val value: NetworkWriteResponse) : UpdaterResult<NetworkWriteResponse>()
sealed class Error : UpdaterResult<Nothing>() {
sealed class UpdaterResult {

sealed class Success : UpdaterResult() {
data class Typed<NetworkWriteResponse : Any>(val value: NetworkWriteResponse) : 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()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.mobilenativefoundation.store.store5

import kotlinx.coroutines.flow.Flow

interface Write<Key : Any, CommonRepresentation : Any> {
@ExperimentalStoreApi
suspend fun <NetworkWriteResponse : Any> write(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): StoreWriteResponse
interface Stream<Key : Any, CommonRepresentation : Any> {
@ExperimentalStoreApi
fun <NetworkWriteResponse : Any> stream(requestStream: Flow<StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>>): Flow<StoreWriteResponse>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package org.mobilenativefoundation.store.store5.impl

import org.mobilenativefoundation.store.store5.StoreWriteResponse

data class OnStoreWriteCompletion<CommonRepresentation : Any>(
val onSuccess: (StoreWriteResponse.Success<CommonRepresentation>) -> Unit,
data class OnStoreWriteCompletion(
val onSuccess: (StoreWriteResponse.Success) -> Unit,
val onFailure: (StoreWriteResponse.Error) -> Unit
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.mobilenativefoundation.store.store5.impl

import org.mobilenativefoundation.store.store5.Bookkeeper
import org.mobilenativefoundation.store.store5.impl.definition.Timestamp
import org.mobilenativefoundation.store.store5.internal.definition.Timestamp

internal class RealBookkeeper<Key : Any>(
private val realGetLastFailedSync: suspend (key: Key) -> Timestamp?,
Expand Down
Loading