/* 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 typealias Reducer = (inout State, Action) -> Void typealias Middleware = (State, Action) -> AnyPublisher? final class Store: 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 private let middlewares: [Middleware] private var middlewareCancellables: Set = [] // Init init( initialState: State, reducer: @escaping Reducer, middlewares: [Middleware] = [] ) { 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 } }