Skip to content

Instantly share code, notes, and snippets.

@alexdrone
Last active May 12, 2017 10:08
Show Gist options
  • Save alexdrone/cfc40de1c360042d7db51572cd292458 to your computer and use it in GitHub Desktop.
Save alexdrone/cfc40de1c360042d7db51572cd292458 to your computer and use it in GitHub Desktop.
DispatchStore + Firebase
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