Removed StoreRoom

This commit is contained in:
fabioCollini 2019-02-23 15:46:15 +01:00
parent 4b27b64d0e
commit e0cf396a10
12 changed files with 2 additions and 664 deletions

View file

@ -33,7 +33,6 @@ android {
exclude 'META-INF/rxjava.properties'
}
}
def room_version = "1.1.0" // or, for latest rc, use "1.1.1-rc1"
dependencies {
@ -63,19 +62,9 @@ dependencies {
implementation libraries.rxAndroid2
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation "android.arch.persistence.room:runtime:$room_version"
annotationProcessor "android.arch.persistence.room:compiler:$room_version"
kapt "android.arch.persistence.room:compiler:$room_version"
// androidTestImplementation "android.arch.persistence.room:testing:$room_version"
// optional - RxJava support for Room
implementation "android.arch.persistence.room:rxjava2:$room_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.0"
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0"
}
repositories {
mavenCentral()

View file

@ -27,35 +27,15 @@ class SampleApp : Application() {
lateinit var persistedStore: Store<RedditData, BarCode>
val moshi = Moshi.Builder().build()
lateinit var persister: Persister<BufferedSource, BarCode>
// lateinit var sampleRoomStore:SampleRoomStore
override fun onCreate() {
super.onCreate()
appContext = this
// sampleRoomStore = SampleRoomStore(this)
initPersister();
nonPersistedStore = provideRedditStore();
persistedStore = providePersistedRedditStore();
//RoomSample()
}
/*private fun RoomSample() {
var foo = sampleRoomStore.store.get("")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ strings1 -> val success = strings1 != null }) { throwable -> throwable.stackTrace }
foo = Observable.timer(15, TimeUnit.SECONDS)
.subscribe { makeFetchRequest() }
}
private fun makeFetchRequest() {
val bar = sampleRoomStore.store.fetch("")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ strings1 -> val success = strings1 != null }) { throwable -> throwable.stackTrace }
}*/
private fun initPersister() {
try {
persister = newPersister()
@ -104,7 +84,7 @@ class SampleApp : Application() {
/**
* Returns a "fetcher" which will retrieve new data from the network.
*/
private fun fetcher(barCode: BarCode): Deferred<ResponseBody> {
private fun fetcher(barCode: BarCode): Deferred<ResponseBody> {
return provideRetrofit().fetchSubredditForPersister(barCode.key, "10")
}

View file

@ -1,60 +0,0 @@
//package com.nytimes.android.sample
//
//import android.arch.persistence.room.Dao
//import android.arch.persistence.room.Database
//import android.arch.persistence.room.Entity
//import android.arch.persistence.room.Insert
//import android.arch.persistence.room.PrimaryKey
//import android.arch.persistence.room.Query
//import android.arch.persistence.room.Room
//import android.arch.persistence.room.RoomDatabase
//import android.content.Context
//import com.nytimes.android.external.store3.base.Fetcher
//import com.nytimes.android.external.store3.base.impl.room.StoreRoom
//import com.nytimes.android.external.store3.base.room.RoomPersister
//import io.reactivex.Flowable
//import io.reactivex.Observable
//import io.reactivex.Single
//
//@Entity
//data class User(
// @PrimaryKey(autoGenerate = true)
// var uid: Int = 0,
// val name: String)
//
//@Dao
//interface UserDao {
// @Query("SELECT name FROM user")
// fun loadAll(): Flowable<List<String>>
//
// @Insert
// fun insertAll(user: User)
//
//}
//
//@Database(entities = arrayOf(User::class), version = 1)
//abstract class AppDatabase : RoomDatabase() {
// abstract fun userDao(): UserDao
//}
//
//class SampleRoomStore(context: Context){
// val db = Room.databaseBuilder(context, AppDatabase::class.java, "db").build()
//
// val fetcher = Fetcher<User, String> { Single.just(User(name = "Mike")) }
// val persister = object : RoomPersister<User, List<String>, String> {
//
// override fun read(key: String): Observable<List<String>> {
// return db.userDao().loadAll().toObservable()
// }
//
// override fun write(key: String, user: User) {
// db.userDao().insertAll(user)
// }
// }
//
// val store = StoreRoom.from(fetcher, persister)
//}
//
//
//
//

View file

@ -2,7 +2,6 @@ package com.nytimes.android.external.store3.base.impl
import com.nytimes.android.external.cache3.Cache
import com.nytimes.android.external.cache3.CacheBuilder
import io.reactivex.Observable
import java.util.concurrent.TimeUnit
object CacheFactory {
@ -15,16 +14,6 @@ object CacheFactory {
return createBaseInFlighter(memoryPolicy)
}
@JvmStatic
fun <Key, Parsed> createRoomCache(memoryPolicy: MemoryPolicy): Cache<Key, Observable<Parsed>> {
return createBaseCache(memoryPolicy)
}
@JvmStatic
fun <Key, Parsed> createRoomInflighter(memoryPolicy: MemoryPolicy): Cache<Key, Observable<Parsed>> {
return createBaseInFlighter(memoryPolicy)
}
private fun <Key, Value> createBaseInFlighter(memoryPolicy: MemoryPolicy?): Cache<Key, Value> {
val expireAfterToSeconds = memoryPolicy?.expireAfterTimeUnit?.toSeconds(memoryPolicy.expireAfterWrite)
?: StoreDefaults.getCacheTTLTimeUnit()

View file

@ -1,227 +0,0 @@
package com.nytimes.android.external.store3.base.impl.room;
import com.nytimes.android.external.cache3.Cache;
import com.nytimes.android.external.store3.annotations.Experimental;
import com.nytimes.android.external.store3.base.impl.CacheFactory;
import com.nytimes.android.external.store3.base.impl.MemoryPolicy;
import com.nytimes.android.external.store3.base.impl.StalePolicy;
import com.nytimes.android.external.store3.base.impl.StoreUtil;
import com.nytimes.android.external.store3.base.room.RoomFetcher;
import com.nytimes.android.external.store3.base.room.RoomPersister;
import java.util.Collection;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import io.reactivex.Observable;
/**
* Store to be used for loading an object from different data sources
*
* @param <Raw> data type before parsing, usually a String, Reader or BufferedSource
* @param <Parsed> data type after parsing
* <p>
*/
@Experimental
class RealStoreRoom<Raw, Parsed, Key> extends StoreRoom<Parsed, Key> {
private final RoomFetcher<Raw, Key> fetcher;
private final RoomPersister<Raw, Parsed, Key> persister;
private final Cache<Key, Observable<Parsed>> memCache;
private final StalePolicy stalePolicy;
private final Cache<Key, Observable<Parsed>> inFlightRequests;
RealStoreRoom(RoomFetcher<Raw, Key> fetcher,
RoomPersister<Raw, Parsed, Key> persister) {
this(fetcher, persister, null, StalePolicy.UNSPECIFIED);
}
RealStoreRoom(RoomFetcher<Raw, Key> fetcher,
RoomPersister<Raw, Parsed, Key> persister,
StalePolicy stalePolicy) {
this(fetcher, persister, null, stalePolicy);
}
RealStoreRoom(RoomFetcher<Raw, Key> fetcher,
RoomPersister<Raw, Parsed, Key> persister,
MemoryPolicy memoryPolicy,
StalePolicy stalePolicy) {
this.fetcher = fetcher;
this.persister = persister;
this.stalePolicy = stalePolicy;
this.memCache = CacheFactory.createRoomCache(memoryPolicy);
this.inFlightRequests = CacheFactory.createRoomInflighter(memoryPolicy);
}
/**
* @param key
* @return an observable from the first data source that is available
*/
@Nonnull
@Override
public Observable<Parsed> get(@Nonnull final Key key) {
return lazyCache(key).switchIfEmpty(fetch(key));
}
/**
* @return data from memory
*/
private Observable<Parsed> lazyCache(@Nonnull final Key key) {
return Observable.defer(() -> cache(key)).onErrorResumeNext(Observable.empty());
}
Observable<Parsed> cache(@Nonnull final Key key) {
try {
return memCache.get(key, () -> disk(key));
} catch (ExecutionException e) {
return Observable.empty();
}
}
@Nonnull
public Observable<Parsed> memory(@Nonnull Key key) {
Observable<Parsed> cachedValue = memCache.getIfPresent(key);
return cachedValue == null ? Observable.empty() : cachedValue;
}
/**
* Fetch data from persister and update memory after. If an error occurs, emit an empty observable
* so that the concat call in {@link #get(Key)} moves on to {@link #fetch(Key)}
*
* @param key
* @return
*/
@Nonnull
public Observable<Parsed> disk(@Nonnull final Key key) {
if (StoreUtil.shouldReturnNetworkBeforeStale(persister, stalePolicy, key)) {
return Observable.empty();
}
return readDisk(key);
}
Observable<Parsed> readDisk(@Nonnull final Key key) {
return persister()
.read(key)
.doOnNext(this::guardAgainstEmptyCollection)
.onErrorResumeNext(
Observable.empty())
.doOnNext(parsed -> {
updateMemory(key, parsed);
if (stalePolicy == StalePolicy.REFRESH_ON_STALE
&& StoreUtil.persisterIsStale(key, persister)) {
backfillCache(key);
}
}).cache();
}
@SuppressWarnings("CheckReturnValue")
void backfillCache(@Nonnull Key key) {
fetch(key).subscribe(it -> {
}, it -> {
});
}
/**
* Will check to see if there exists an in flight observable and return it before
* going to network
*
* @return data from fresh and store it in memory and persister
*/
@Nonnull
@Override
public Observable<Parsed> fetch(@Nonnull final Key key) {
return Observable.defer(() -> fetchAndPersist(key));
}
/**
* There should only be one fresh request in flight at any give time.
* <p>
* Return cached request in the form of a Behavior Subject which will emit to its subscribers
* the last value it gets. Subject/Observable is cached in a {@link ConcurrentMap} to maintain
* thread safety.
*
* @param key resource identifier
* @return observable that emits a {@link Parsed} value
*/
@Nullable
private Observable<Parsed> fetchAndPersist(@Nonnull final Key key) {
try {
return inFlightRequests.get(key, () -> response(key));
} catch (ExecutionException e) {
return Observable.error(e);
}
}
@Nonnull
private Observable<Parsed> response(@Nonnull final Key key) {
return fetcher()
.fetch(key)
.concatMapEagerDelayError(it -> {
persister().write(key, it);
return readDisk(key);
},true)
.onErrorResumeNext(throwable -> {
if (stalePolicy == StalePolicy.NETWORK_BEFORE_STALE) {
return readDisk(key).switchIfEmpty(Observable.error(throwable));
}
return Observable.error(throwable);
})
.doAfterTerminate(() -> inFlightRequests.invalidate(key))
.cache();
}
/**
* Only update memory after persister has been successfully updated
*
* @param key
* @param data
*/
void updateMemory(@Nonnull final Key key, final Parsed data) {
memCache.put(key, Observable.just(data));
}
@Override
//need to create a clearable override to clear all
// since room knows what table associated with data
public void clear() {
for (Key cachedKey : memCache.asMap().keySet()) {
clear(cachedKey);
}
}
@Override
public void clear(@Nonnull Key key) {
inFlightRequests.invalidate(key);
memCache.invalidate(key);
StoreUtil.clearPersister(persister(), key);
}
/**
* @return DiskDAO that stores and stores <Raw> data
*/
RoomPersister<Raw, Parsed, Key> persister() {
return persister;
}
/**
*
*/
RoomFetcher<Raw, Key> fetcher() {
return fetcher;
}
private void guardAgainstEmptyCollection(Parsed v) {
if (v instanceof Collection && ((Collection) v).isEmpty()) {
throw new IllegalStateException("empty result set");
}
}
}

View file

@ -1,71 +0,0 @@
package com.nytimes.android.external.store3.base.impl.room;
import com.nytimes.android.external.store3.annotations.Experimental;
import com.nytimes.android.external.store3.base.Fetcher;
import com.nytimes.android.external.store3.base.impl.MemoryPolicy;
import com.nytimes.android.external.store3.base.impl.StalePolicy;
import com.nytimes.android.external.store3.base.impl.StoreBuilder;
import com.nytimes.android.external.store3.base.room.RoomFetcher;
import com.nytimes.android.external.store3.base.room.RoomPersister;
import javax.annotation.Nonnull;
import io.reactivex.Observable;
/**
* a {@link StoreBuilder StoreBuilder}
* will return an instance of a store
* <p>
* A {@link StoreRoom Store} can
* {@link StoreRoom#get(V) Store.get() } cached data or
* force a call to {@link StoreRoom#fetch(V) Store.fresh() }
* (skipping cache)
*/
@Experimental
public abstract class StoreRoom<T, V> {
/**
* Return an Observable of T for request Barcode
* Data will be returned from oldest non expired source
* Sources are Memory Cache, Disk Cache, Inflight, Network Response
*/
@Nonnull
public abstract Observable<T> get(@Nonnull V key);
/**
* Return an Observable of T for requested Barcode skipping Memory & Disk Cache
*/
@Nonnull
public abstract Observable<T> fetch(@Nonnull V key);
/**
* purges all entries from memory and disk cache
* Persister will only be cleared if they implements Clearable
*/
public abstract void clear();
/**
* Purge a particular entry from memory and disk cache.
* Persister will only be cleared if they implements Clearable
*/
public abstract void clear(@Nonnull V key);
public static <Raw, Parsed, Key> StoreRoom<Parsed, Key> from
(RoomFetcher<Raw, Key> fetcher, RoomPersister<Raw, Parsed, Key> persister) {
return new RealStoreRoom<>(fetcher, persister);
}
public static <Raw, Parsed, Key> StoreRoom<Parsed, Key> from(
RoomFetcher<Raw, Key> fetcher,
RoomPersister<Raw, Parsed, Key> persister,
StalePolicy policy) {
return new RealStoreRoom<>(fetcher, persister, policy);
}
public static <Raw, Parsed, Key> StoreRoom<Parsed, Key> from
(RoomFetcher<Raw, Key> fetcher, RoomPersister<Raw, Parsed, Key> persister,
StalePolicy stalePolicy, MemoryPolicy memoryPolicy) {
return new RealStoreRoom<>(fetcher, persister, memoryPolicy, stalePolicy);
}
}

View file

@ -1,13 +0,0 @@
package com.nytimes.android.external.store3.base.room;
import com.nytimes.android.external.store3.annotations.Experimental;
import javax.annotation.Nonnull;
import io.reactivex.Observable;
@Experimental
public interface RoomDiskRead<Raw, Key> {
@Nonnull
Observable<Raw> read(@Nonnull Key key);
}

View file

@ -1,16 +0,0 @@
package com.nytimes.android.external.store3.base.room;
import com.nytimes.android.external.store3.annotations.Experimental;
import javax.annotation.Nonnull;
@Experimental
public interface RoomDiskWrite<Raw, Key> {
/**
* @param key to use to get data from persister
* If data is not available implementer needs to
* either return Observable.empty or throw an exception
*/
@Nonnull
void write(@Nonnull Key key, @Nonnull Raw raw);
}

View file

@ -1,22 +0,0 @@
package com.nytimes.android.external.store3.base.room;
import javax.annotation.Nonnull;
import io.reactivex.Observable;
import io.reactivex.Single;
/**
* Interface for fetching new data for a Store
*
* @param <Raw> data type before parsing
*/
public interface RoomFetcher<Raw, Key> {
/**
* @param key Container with Key and Type used as a request param
* @return Observable that emits {@link Raw} data
*/
@Nonnull
Observable<Raw> fetch(@Nonnull Key key);
}

View file

@ -1,36 +0,0 @@
package com.nytimes.android.external.store3.base.room;
import com.nytimes.android.external.store3.annotations.Experimental;
import com.nytimes.android.external.store3.base.BasePersister;
import javax.annotation.Nonnull;
import io.reactivex.Observable;
/**
* Interface for fetching data from persister
* when implementing also think about implementing PathResolver to ease in creating primary keys
*
* @param <Raw> data type before parsing
*/
@Experimental
public interface RoomPersister<Raw, Parsed, Key> extends
RoomDiskRead<Parsed, Key>, RoomDiskWrite<Raw, Key>, BasePersister {
/**
* @param key to use to get data from persister
* If data is not available implementer needs to
* either return Observable.empty or throw an exception
*/
@Override
@Nonnull
Observable<Parsed> read(@Nonnull final Key key);
/**
* @param key to use to store data to persister
* @param raw raw string to be stored
*/
@Override
@Nonnull
void write(@Nonnull final Key key, @Nonnull final Raw raw);
}

View file

@ -1,89 +0,0 @@
package com.nytimes.android.external.store3.room
import com.nhaarman.mockitokotlin2.mock
import com.nytimes.android.external.store3.base.Clearable
import com.nytimes.android.external.store3.base.impl.BarCode
import com.nytimes.android.external.store3.base.impl.StalePolicy
import com.nytimes.android.external.store3.base.impl.room.StoreRoom
import com.nytimes.android.external.store3.base.room.RoomPersister
import io.reactivex.Observable
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.mockito.Mockito.`when`
import org.mockito.Mockito.verify
import java.util.concurrent.atomic.AtomicInteger
class ClearStoreRoomTest {
private val persister: RoomClearingPersister = mock()
private val networkCalls: AtomicInteger = AtomicInteger(0)
private val store = StoreRoom.from({ Observable.fromCallable { networkCalls.incrementAndGet() } },
persister,
StalePolicy.UNSPECIFIED)
@Test
fun testClearSingleBarCode() {
// one request should produce one call
val barcode = BarCode("type", "key")
`when`(persister.read(barcode))
.thenReturn(Observable.empty()) //read from disk on get
.thenReturn(Observable.just(1)) //read from disk after fetching from network
.thenReturn(Observable.empty()) //read from disk after clearing
.thenReturn(Observable.just(1)) //read from disk after making additional network call
store.get(barcode).test().awaitTerminalEvent()
assertThat(networkCalls.toInt()).isEqualTo(1)
// after clearing the memory another call should be made
store.clear(barcode)
store.get(barcode).test().awaitTerminalEvent()
verify<RoomClearingPersister>(persister).clear(barcode)
assertThat(networkCalls.toInt()).isEqualTo(2)
}
@Test
fun testClearAllBarCodes() {
val barcode1 = BarCode("type1", "key1")
val barcode2 = BarCode("type2", "key2")
`when`(persister.read(barcode1))
.thenReturn(Observable.empty()) //read from disk
.thenReturn(Observable.just(1)) //read from disk after fetching from network
.thenReturn(Observable.empty()) //read from disk after clearing disk cache
.thenReturn(Observable.just(1)) //read from disk after making additional network call
`when`(persister.read(barcode2))
.thenReturn(Observable.empty()) //read from disk
.thenReturn(Observable.just(1)) //read from disk after fetching from network
.thenReturn(Observable.empty()) //read from disk after clearing disk cache
.thenReturn(Observable.just(1)) //read from disk after making additional network call
// each request should produce one call
store.get(barcode1).test().awaitTerminalEvent()
store.get(barcode2).test().awaitTerminalEvent()
assertThat(networkCalls.toInt()).isEqualTo(2)
store.clear()
// after everything is cleared each request should produce another 2 calls
store.get(barcode1).test().awaitTerminalEvent()
store.get(barcode2).test().awaitTerminalEvent()
assertThat(networkCalls.toInt()).isEqualTo(4)
}
//everything will be mocked
internal open class RoomClearingPersister : RoomPersister<Int, Int, BarCode>, Clearable<BarCode> {
override fun clear(key: BarCode) {
throw RuntimeException()
}
override fun read(barCode: BarCode): Observable<Int> {
throw RuntimeException()
}
override fun write(barCode: BarCode, integer: Int) {
//noop
}
}
}

View file

@ -1,86 +0,0 @@
package com.nytimes.android.external.store3.room
import com.nhaarman.mockitokotlin2.mock
import com.nytimes.android.external.store3.base.impl.BarCode
import com.nytimes.android.external.store3.base.impl.StalePolicy
import com.nytimes.android.external.store3.base.impl.room.StoreRoom
import com.nytimes.android.external.store3.base.room.RoomFetcher
import com.nytimes.android.external.store3.base.room.RoomPersister
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.BiFunction
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.mockito.Mockito.*
import java.util.concurrent.atomic.AtomicInteger
class StoreRoomTest {
val counter = AtomicInteger(0)
val fetcher: RoomFetcher<String, BarCode> = mock()
val persister: RoomPersister<String, String, BarCode> = mock()
private val barCode = BarCode("key", "value")
@Test
fun testSimple() {
val simpleStore = StoreRoom.from(
fetcher,
persister,
StalePolicy.UNSPECIFIED
)
`when`(fetcher.fetch(barCode))
.thenReturn(Observable.just(NETWORK))
`when`(persister.read(barCode))
.thenReturn(Observable.empty())
.thenReturn(Observable.just(DISK))
var value = simpleStore.get(barCode).blockingFirst()
assertThat(value).isEqualTo(DISK)
value = simpleStore.get(barCode).blockingFirst()
assertThat(value).isEqualTo(DISK)
verify(fetcher, times(1)).fetch(barCode)
}
@Test
fun testDoubleTap() {
val simpleStore = StoreRoom.from(
fetcher,
persister,
StalePolicy.UNSPECIFIED
)
val networkSingle = Single.create<String> { emitter ->
if (counter.incrementAndGet() == 1) {
emitter.onSuccess(NETWORK)
} else {
emitter.onError(RuntimeException("Yo Dawg your inflight is broken"))
}
}
`when`(fetcher.fetch(barCode))
.thenReturn(networkSingle.toObservable())
`when`(persister.read(barCode))
.thenReturn(Observable.empty())
.thenReturn(Observable.just(DISK))
val response = simpleStore.get(barCode)
.zipWith(simpleStore.get(barCode), BiFunction<String, String, String> { s, s2 -> "hello" })
.blockingFirst()
assertThat(response).isEqualTo("hello")
verify(fetcher, times(1)).fetch(barCode)
}
companion object {
private val DISK = "disk"
private val NETWORK = "fresh"
}
}