96 lines
3.4 KiB
Swift
96 lines
3.4 KiB
Swift
|
/*
|
||
|
In 99,99% of time YOU DON'T NEEDED TO CHANGE ANYTHING in this file!
|
||
|
|
||
|
This file declare global state object for whole app
|
||
|
and reducers/actions/middleware types. Core of app.
|
||
|
*/
|
||
|
import Combine
|
||
|
import Foundation
|
||
|
|
||
|
typealias Stateable = Codable & Equatable
|
||
|
typealias AppStore = Store<AppState, AppAction>
|
||
|
typealias Reducer<State: Stateable, Action: Codable> = (inout State, Action) -> Void
|
||
|
typealias Middleware<State: Stateable, Action: Codable> = (State, Action) -> AnyPublisher<Action, Never>?
|
||
|
|
||
|
final class Store<State: Stateable, Action: Codable>: ObservableObject {
|
||
|
// Fake variable for be able to trigger SwiftUI redraw after app state completely changed
|
||
|
// this hack is needed because @Published wrapper sends signals on "willSet:"
|
||
|
@Published private var dumbVar: UUID = .init()
|
||
|
|
||
|
// State is read-only (changes only over reducers)
|
||
|
private(set) var state: State {
|
||
|
didSet {
|
||
|
DispatchQueue.main.async { [weak self] in
|
||
|
self?.dumbVar = UUID()
|
||
|
}
|
||
|
} // signal to SwiftUI only when new state did set
|
||
|
}
|
||
|
|
||
|
// Serial queue for performing any actions sequentially
|
||
|
private let serialQueue = DispatchQueue(label: "im.narayana.conversations.classic.serial.queue", qos: .userInteractive)
|
||
|
|
||
|
private let reducer: Reducer<State, Action>
|
||
|
private let middlewares: [Middleware<State, Action>]
|
||
|
private var middlewareCancellables: Set<AnyCancellable> = []
|
||
|
|
||
|
// Init
|
||
|
init(
|
||
|
initialState: State,
|
||
|
reducer: @escaping Reducer<State, Action>,
|
||
|
middlewares: [Middleware<State, Action>] = []
|
||
|
) {
|
||
|
state = initialState
|
||
|
self.reducer = reducer
|
||
|
self.middlewares = middlewares
|
||
|
}
|
||
|
|
||
|
// Run reducers/middlewares
|
||
|
func dispatch(_ action: Action) {
|
||
|
serialQueue.sync { [weak self] in
|
||
|
guard let wSelf = self else { return }
|
||
|
let newState = wSelf.dispatch(wSelf.state, action)
|
||
|
wSelf.state = newState
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private func dispatch(_ currentState: State, _ action: Action) -> State {
|
||
|
let startTime = CFAbsoluteTimeGetCurrent()
|
||
|
|
||
|
// Do reducing
|
||
|
var newState = currentState
|
||
|
reducer(&newState, action)
|
||
|
|
||
|
// Dispatch all middleware functions
|
||
|
for middleware in middlewares {
|
||
|
guard let middleware = middleware(newState, action) else {
|
||
|
break
|
||
|
}
|
||
|
middleware
|
||
|
.receive(on: DispatchQueue.main)
|
||
|
.sink(receiveValue: dispatch)
|
||
|
.store(in: &middlewareCancellables)
|
||
|
}
|
||
|
|
||
|
// Check performance
|
||
|
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
|
||
|
if timeElapsed > 0.05 {
|
||
|
#if DEBUG
|
||
|
print(
|
||
|
"""
|
||
|
--
|
||
|
(Ignore this warning ONLY in case, when execution is paused by your breakpoint)
|
||
|
🕐Execution time: \(timeElapsed)
|
||
|
❌WARNING! Some reducers/middlewares work too long! It will lead to issues in production build!
|
||
|
Because of execution each action is synchronous the any stuck will reduce performance dramatically.
|
||
|
Probably you need check which part of reducer/middleware should be async (wrapped with Futures, as example)
|
||
|
--
|
||
|
"""
|
||
|
)
|
||
|
#else
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
return newState
|
||
|
}
|
||
|
}
|