All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.revolut.rxdata.dod.DataObservableDelegate.kt Maven / Gradle / Ivy

There is a newer version: 1.5.16
Show newest version
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