2024-07-10 13:00:54 +00:00
|
|
|
// This file declare global state object for whole app
|
|
|
|
// and reducers/actions/middleware types. Core of app.
|
2024-06-19 15:15:27 +00:00
|
|
|
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) {
|
2024-07-22 18:36:31 +00:00
|
|
|
if !Thread.isMainThread {
|
|
|
|
print("❌WARNING!: AppStore.dispatch should be called from the main thread")
|
|
|
|
}
|
2024-06-19 15:15:27 +00:00
|
|
|
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 {
|
|
|
|
// Do reducing
|
2024-07-22 12:18:42 +00:00
|
|
|
var startTime = CFAbsoluteTimeGetCurrent()
|
2024-06-19 15:15:27 +00:00
|
|
|
var newState = currentState
|
|
|
|
reducer(&newState, action)
|
2024-07-22 12:18:42 +00:00
|
|
|
var timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
|
2024-06-19 15:15:27 +00:00
|
|
|
if timeElapsed > 0.05 {
|
|
|
|
#if DEBUG
|
|
|
|
print(
|
|
|
|
"""
|
|
|
|
--
|
|
|
|
(Ignore this warning ONLY in case, when execution is paused by your breakpoint)
|
|
|
|
🕐Execution time: \(timeElapsed)
|
2024-07-22 12:18:42 +00:00
|
|
|
❌WARNING! Some reducers work too long! It will lead to issues in production build!
|
2024-06-19 15:15:27 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-07-22 12:18:42 +00:00
|
|
|
// Dispatch all middleware functions
|
|
|
|
for middleware in middlewares {
|
|
|
|
guard let middleware = middleware(newState, action) else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
startTime = CFAbsoluteTimeGetCurrent()
|
|
|
|
middleware
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink(receiveValue: dispatch)
|
|
|
|
.store(in: &middlewareCancellables)
|
|
|
|
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! Middleware 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-19 15:15:27 +00:00
|
|
|
return newState
|
|
|
|
}
|
|
|
|
}
|