com.revolut.rxdata.dod.DataObservableDelegate.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dod Show documentation
Show all versions of dod Show documentation
RxData DataObservableDelegate
package com.revolut.rxdata.dod
import com.revolut.data.model.Data
import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.Observable.concat
import io.reactivex.Observable.just
import io.reactivex.Single
import io.reactivex.internal.disposables.DisposableContainer
import io.reactivex.internal.observers.LambdaObserver
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
/*
* Copyright (C) 2019 Revolut
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/**
* Provides a strategy of retrieving data from network and caching it.
*
* Support two levels of cache memory and storage (usually database or shared preferences)
*
*/
class DataObservableDelegate constructor(
fromNetwork: DataObservableDelegate.(params: Params) -> Single,
private val fromMemory: (params: Params) -> Domain? = { _ -> null },
private val toMemory: (params: Params, Domain) -> Unit = { _, _ -> Unit },
private val fromStorage: ((params: Params) -> Domain?) = { _ -> null },
private val toStorage: ((params: Params, Domain) -> Unit) = { _, _ -> Unit },
private val onRemove: (params: Params) -> Unit = { _ -> Unit },
private val fromStorageSingle: ((params: Params) -> Single>) =
{ params -> Single.fromCallable { Data(content = fromStorage(params)) } },
private val networkSubscriptionsContainer: DisposableContainer = DodGlobal.disposableContainer
) {
private val subjectsMap = ConcurrentHashMap>>()
private val sharedNetworkRequest: SharedSingleRequest =
SharedSingleRequest { params ->
this.fromNetwork(params)
.doOnSuccess { domain ->
toMemory(params, domain)
toStorage(params, domain)
}
.doAfterSuccess { domain ->
failedNetworkRequests.remove(params)
val data = Data(content = domain)
subject(params).onNext(data)
}
}
private val sharedStorageRequest: SharedSingleRequest> =
SharedSingleRequest { params ->
val subject = subject(params)
this.fromStorageSingle(params)
.doOnSuccess { cachedValue ->
cachedValue.content?.let { value ->
toMemory(params, value)
}
}
.map { storageData ->
val data = storageData.copy(loading = true)
subject.onNext(data)
data
}
.onErrorResumeNext { error: Throwable ->
val data = Data(content = null, loading = true, error = error)
subject.onNext(data)
Single.just(data)
}
}
/**
* Previously failed network requests will be reloaded on next observe
* even if forceReload == false and memory is not empty
*/
private val failedNetworkRequests = ConcurrentHashMap()
/**
* Requests data from network and subscribes to updates
* (can be triggered by other subscribers or manual cache overrides)
*
* @param loadingStrategy - [LoadingStrategy]
*/
fun observe(params: Params, loadingStrategy: LoadingStrategy): Observable> =
Observable.defer {
val memCache = fromMemory(params)
val memoryIsEmpty = memCache == null
val subject = subject(params)
val loading = loadingStrategy.refreshMemory || memoryIsEmpty || failedNetworkRequests.containsKey(params)
val observable: Observable> = if (memCache != null) {
concat(
just(Data(content = memCache, loading = loading)),
subject
).doAfterSubscribe {
if (loading) {
subject.onNext(Data(content = memCache, loading = true))
fetchFromNetwork(memCache, params)
}
}
} else {
sharedStorageRequest.getOrLoad(params)
.flatMapObservable { cached ->
concat(
just(cached),
subject
).doAfterSubscribe {
if (loadingStrategy.refreshStorage || cached.content == null) {
fetchFromNetwork(cached.content, params)
}
}
}
.startWith(Data(null, loading = true))
}
observable
.distinctUntilChanged()
.muteRepetitiveReloading()
}
/**
* Requests data from network and subscribes to updates
* (can be triggered by other subscribers or manual cache overrides)
*
* @param forceReload - if true network request will be made even if data exists in caches
*/
@Deprecated(
message = "please migrate to the method with the LoadingStrategy",
replaceWith = ReplaceWith("fun observe(params: Params, loadingStrategy: LoadingStrategy)")
)
fun observe(params: Params, forceReload: Boolean = true): Observable> =
observe(params, loadingStrategy = if (forceReload) LoadingStrategy.ForceReload else LoadingStrategy.Auto)
private fun Observable>.muteRepetitiveReloading(): Observable> =
this.scan(ReloadingDataScanner()) { scanner, newEmit ->
scanner.registerData(newEmit)
}.skip(1)
.filter { scanner -> scanner.shouldEmitCurrentData() }
.map { it.currentData() }
/**
* Replaces the data in both caches (Memory, Persistent storage)
* and emits an update.
*/
fun updateAll(params: Params, domain: Domain) {
toMemory(params, domain)
toStorage(params, domain)
subject(params).onNext(Data(content = domain))
}
/**
* Replaces the data and emits an update in memory cache.
*/
fun updateMemory(params: Params, domain: Domain) {
toMemory(params, domain)
subject(params).onNext(Data(content = domain))
}
/**
* Subscribers observing this DOD will be notified with
* Data(fromMemory(params), loading = false, error = null).
* @param where must return true if subscriber requires notification.
*/
fun notifyFromMemory(
error: Throwable? = null,
loading: Boolean = false,
where: (Params) -> Boolean
) {
subjectsMap.forEach { (params, subject) ->
if (where(params)) {
subject.onNext(Data(content = fromMemory(params), error = error, loading = loading))
}
}
}
/**
* Replaces the data and emits an update in persistent storage cache.
*
* /!\ Memory cache won't be dropped or replaced /!\
*/
fun updateStorage(params: Params, domain: Domain) {
toStorage(params, domain)
subject(params).onNext(Data(content = domain))
}
fun remove(params: Params) {
onRemove(params)
subject(params).onNext(Data(content = null))
}
fun reload(params: Params, await: Boolean = false): Completable = if (!await) Completable.fromAction {
notifyFromMemory(loading = true) { it == params}
fetchFromNetwork(cachedData = fromMemory(params), params = params)
} else Completable.create { emitter ->
notifyFromMemory(loading = true) { it == params}
fetchFromNetwork(cachedData = fromMemory(params), params = params, onComplete = { emitter.onComplete()}, onError = { emitter.onError(it) })
}
@Deprecated(
message = "Don't use it as it could cause inconsistent state of the Delegate",
replaceWith = ReplaceWith("notifyFromMemory { it == params }")
)
fun notifyUpdated(params: Params, domain: Domain) {
subject(params).onNext(Data(content = domain))
}
@Suppress("CheckResult")
private fun fetchFromNetwork(cachedData: Domain?, params: Params, onComplete: () -> Unit = {}, onError:(Throwable) -> Unit = {}) {
val observer = LambdaObserver(
{
//all done in sharedRequest
onComplete()
}, { error ->
//error handling is here and not in sharedRequest
//because timeout also generates an error that needs to be handled
val data = Data(content = fromMemory(params) ?: cachedData, error = error)
if (error is NoSuchElementException) {
failedNetworkRequests.remove(params)
} else {
failedNetworkRequests[params] = error
}
subject(params).onNext(data)
onError(error)
}, {}, {}
)
sharedNetworkRequest.getOrLoad(params)
.toObservable()
.timeout(DodGlobal.networkTimeoutSeconds, TimeUnit.SECONDS, Schedulers.io())
.doFinally { networkSubscriptionsContainer.remove(observer) }
.subscribeWith(observer)
networkSubscriptionsContainer.add(observer)
}
private fun subject(params: Params): Subject> = subjectsMap.getOrCreate(
params,
creator = { PublishSubject.create>().toSerialized() })
private fun Observable.doAfterSubscribe(action: () -> Unit) = this.mergeWith(
Completable.fromAction { action() }
)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy