Last active
May 12, 2017 10:08
-
-
Save alexdrone/cfc40de1c360042d7db51572cd292458 to your computer and use it in GitHub Desktop.
DispatchStore + Firebase
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import DispatchStore | |
import Firebase | |
import FirebaseDatabase | |
import Wrap | |
import Unbox | |
// MARK: - Firebase specific logic | |
public protocol FirebaseBackedStoreType: class { | |
/** Path to the Firebase resource. */ | |
var path: String { get } | |
/** A reference to the dispatch store. */ | |
var storeRef: StoreType { get } | |
/** The firebase handle. */ | |
var ref: FIRDatabaseReference? { get } | |
} | |
/** Specialization for the 'ActionType'. */ | |
public protocol FirebaseActionType: ActionType { | |
/** Action dispatched whenever there's a remote change from the server. */ | |
static var actionForRemoteDatabaseChange: FirebaseActionType { get } | |
var isActionForRemoteDatabaseChange: Bool { get } | |
} | |
open class FirebaseBackedStore<S: FirebaseStateType, | |
A: FirebaseActionType>: FirebaseBackedStoreType { | |
public let path: String | |
public var ref: FIRDatabaseReference? = nil | |
/** A reference to the dispatch store. */ | |
public let store: Store<S, A> | |
public var storeRef: StoreType { | |
return self.store | |
} | |
public init(path: String, reducer: FirebaseReducer<S, A>) { | |
self.path = path | |
self.store = Store<S, A>(identifier: path, reducer: reducer) | |
Dispatcher.default.register(middleware: FirebaseSynchronizer(firebaseStore: self)) | |
self.startRemoteObservation() | |
reducer.ref = self.ref | |
} | |
public func startRemoteObservation() { | |
self.ref = FIRDatabase.database().reference().child(path) | |
self.ref?.observe(FIRDataEventType.value, with: { (snapshot) in | |
guard let dictianary = snapshot.value as? [String: Any] else { | |
return | |
} | |
let state: S = StateEncoder.decode(dictionary: dictianary) | |
self.store.inject(state: state, action: A.actionForRemoteDatabaseChange) | |
Dispatcher.default.dispatch(action: A.actionForRemoteDatabaseChange) | |
}) | |
} | |
public func stopRemoteObservation() { | |
self.ref = nil | |
} | |
} | |
// MARK: - FirebaseReducer | |
public class FirebaseReducer<S: FirebaseStateType, A: FirebaseActionType>: Reducer<S, A> { | |
/** The firebase handler. */ | |
var ref: FIRDatabaseReference? | |
} | |
// MARK: - Transformation from/to JSON | |
public protocol FirebaseStateType: StateType, Unboxable { } | |
public extension FirebaseStateType { | |
/** Infer the state target. */ | |
public func decoder() -> ([String: Any]) -> StateType { | |
return { dictionary in | |
do { | |
let state: Self = try unbox(dictionary: dictionary) | |
return state | |
} catch { | |
return Self() | |
} | |
} | |
} | |
} | |
public class StateEncoder { | |
/** Wraps the state into a dictionary. */ | |
public static func encode(state: StateType) -> [String: Any] { | |
do { | |
return try wrap(state) | |
} catch { | |
return [:] | |
} | |
} | |
/** Unmarshal the state from the dictionary */ | |
public static func decode<S: FirebaseStateType>(dictionary: [String: Any]) -> S { | |
do { | |
return try unbox(dictionary: dictionary) | |
} catch { | |
return S() | |
} | |
} | |
/** Flatten down the dictionary into a map from 'path' to value. */ | |
public static func merge(encodedState: [String: Any]) -> [String: Any] { | |
func flatten(path: String, dictionary: [String: Any], result: inout [String: Any]) { | |
let formattedPath = path.isEmpty ? "" : "\(path)/" | |
for (key, value) in dictionary { | |
if let nestedDictionary = value as? [String: Any] { | |
flatten(path: "\(formattedPath)\(key)", | |
dictionary: nestedDictionary, | |
result: &result) | |
} else { | |
result["\(formattedPath)\(key)"] = value | |
} | |
} | |
} | |
var result: [String: Any] = [:] | |
flatten(path: "", dictionary: encodedState, result: &result) | |
return result | |
} | |
} | |
// MARK: - Syncronization Middleware | |
final private class FirebaseSynchronizer<S: FirebaseStateType, | |
A: FirebaseActionType>: MiddlewareType { | |
private var firebaseStore: FirebaseBackedStore<S, A> | |
private var currentFlattenState: [String: Any] = [:] | |
init(firebaseStore: FirebaseBackedStore<S, A>) { | |
self.firebaseStore = firebaseStore | |
} | |
public func willDispatch(transaction: String, action: ActionType, in store: StoreType) { | |
// Nothing to do. | |
} | |
public func didDispatch(transaction: String, action: ActionType, in store: StoreType) { | |
guard let store = store as? Store<S, A>, store === self.firebaseStore.storeRef, | |
let action = action as? A else { | |
return | |
} | |
let new = StateEncoder.merge(encodedState: StateEncoder.encode(state: store.state)) | |
let old = self.currentFlattenState | |
self.currentFlattenState = new | |
guard !action.isActionForRemoteDatabaseChange else { | |
return | |
} | |
var diff: [String: Any] = [:] | |
for (key, value) in new { | |
if old[key] == nil || new[key].debugDescription != old[key].debugDescription { | |
diff[key] = value | |
} | |
} | |
print("⤷ \(self.firebaseStore.path).push \(diff.count): \(diff)…") | |
self.firebaseStore.ref?.updateChildValues(diff) | |
} | |
} | |
public func uuid() -> String { | |
return NSUUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment