Skip to content

Instantly share code, notes, and snippets.

@ha1f
Created December 8, 2019 12:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ha1f/f7135fd0b9732c2ddba58ff42cac85ed to your computer and use it in GitHub Desktop.
Save ha1f/f7135fd0b9732c2ddba58ff42cac85ed to your computer and use it in GitHub Desktop.
enum CachableRequestState {
/// 読み込み前
case possible
/// 読込中
case loading
/// 直近のリクエストがエラー
case error(Error)
/// 直近のリクエストが成功
case loaded
}
/// 結果をキャッシュする
/// 読込中には重複してリクエストを送らない
/// stateとcacheを混ぜたストリームを作らないこと(どっちが先に更新されるかは問わない)
final class CachableRequest<T> {
/// 状態
let state = BehaviorRelay<CachableRequestState>(value: .possible)
/// キャッシュされた値
let cache = BehaviorRelay<T?>(value: nil)
/// リクエストの結果
private let _publisher = PublishRelay<Swift.Result<T, Error>>()
private let isLoadingFlag = IsLoadingFlag()
private let _disposeBag = DisposeBag()
private var _request: Single<T>
private let _requestBuilder: AnyCachableRequestRequestBuilder<T>
convenience init(_ request: Single<T>) {
self.init(
requestBuilder: CachableRequestRequestBuilderConstant(request),
cacheStrategy: CachableRequestCacheUpdateStrategyReplace()
)
}
init<B: CachableRequestRequestBuilder, S: CachableRequestCacheUpdateStrategy>(
requestBuilder: B,
cacheStrategy: S
) where T == B.T, T == S.T {
_requestBuilder = AnyCachableRequestRequestBuilder(requestBuilder)
_request = requestBuilder.buildRequest(for: nil)
// 直前の結果によってstateを更新
let latestRequestResult = _publisher
.map { result -> CachableRequestState in
switch result {
case .success:
return .loaded
case let .failure(error):
return .error(error)
}
}
Observable
.combineLatest(latestRequestResult, isLoadingFlag.value)
.map { combined -> CachableRequestState in
if combined.1 {
return .loading
}
return combined.0
}
.bind(to: state)
.disposed(by: _disposeBag)
// キャッシュにどんどん反映
// キャッシュのread/writeをtransactionalにしたい
_publisher
.compactMap { $0.value }
.withLatestFrom(cache) { cacheStrategy.combine(current: $1, received: $0) }
.bind(to: cache)
.disposed(by: _disposeBag)
}
/// 値の取得を開始
func fetch() {
_fetch()
}
/// 結果を直接受け取りたいときに使う(retryWhenとかできる)
func request() -> Observable<T> {
return _publisher
.take(1)
.flatMap(Observable.from(result:))
.do(onSubscribed: { [weak self] in
self?._fetch()
})
}
private func _fetch() {
guard isLoadingFlag.startLoading() else {
return
}
_request
.subscribe(onSuccess: { [weak self] value in
guard let self = self else {
return
}
self._publisher.accept(.success(value))
self.isLoadingFlag.finishLoading()
}, onError: { [weak self] error in
guard let self = self else {
return
}
self._publisher.accept(.failure(error))
self.isLoadingFlag.finishLoading()
})
.disposed(by: _disposeBag)
}
}
struct AnyCachableRequestRequestBuilder<T>: CachableRequestRequestBuilder {
private let _buildRequest: (T?) -> Single<T>
init<B: CachableRequestRequestBuilder>(
_ requestBuilder: B
) where T == B.T {
_buildRequest = requestBuilder.buildRequest
}
func buildRequest(for cache: T?) -> Single<T> {
return _buildRequest(cache)
}
}
protocol CachableRequestRequestBuilder {
associatedtype T
func buildRequest(for cache: T?) -> Single<T>
}
struct CachableRequestRequestBuilderConstant<T>: CachableRequestRequestBuilder {
private let _request: Single<T>
init(_ request: Single<T>) {
_request = request
}
func buildRequest(for cache: T?) -> Single<T> {
return _request
}
}
protocol CachableRequestCacheUpdateStrategy {
associatedtype T
func combine(current: T?, received: T) -> T
}
struct CachableRequestCacheUpdateStrategyReplace<T>: CachableRequestCacheUpdateStrategy {
func combine(current: T?, received: T) -> T {
return received
}
}
struct CachableRequestCacheUpdateStrategyAppend<E>: CachableRequestCacheUpdateStrategy {
func combine(current: [E]?, received: [E]) -> [E] {
guard let current = current else {
return received
}
return current + received
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment