Skip to content

Commit

Permalink
Async Middleware (#93)
Browse files Browse the repository at this point in the history
* wip

* wip

* wip

* wip
  • Loading branch information
stephtelolahy authored Dec 21, 2024
1 parent 697b506 commit 046f00a
Show file tree
Hide file tree
Showing 4 changed files with 28 additions and 39 deletions.
2 changes: 1 addition & 1 deletion WildWestOnline/Core/Bang/Sources/Game/GameAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Hugues Telolahy on 27/10/2024.
//

public struct GameAction: Action, Equatable, Codable, Sendable {
public struct GameAction: Action, Equatable, Codable {
public var kind: Kind
public var payload: Payload

Expand Down
50 changes: 18 additions & 32 deletions WildWestOnline/Core/Bang/Sources/Redux/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import Combine
import Foundation
import SwiftUI
// swiftlint:disable private_subject unowned_variable_capture

/// ``Action`` is a plain object that describes what happened.
Expand All @@ -22,7 +21,7 @@ public typealias Reducer<State> = (State, Action) throws -> State
/// ``Middleware`` is a plugin, or a composition of several plugins,
/// that are assigned to the app global state pipeline in order to
/// handle each action received action, to execute side-effects in response, and eventually dispatch more actions
public typealias Middleware<State> = @Sendable (State, Action) -> Action?
public typealias Middleware<State> = (State, Action) async -> Action?

/// Namespace for Middlewares
public enum Middlewares {}
Expand All @@ -31,15 +30,13 @@ public enum Middlewares {}
/// It defines two roles of a "Store":
/// - receive/distribute `Action`;
/// - and publish changes of the the current app `State` to possible subscribers.
public class Store<State: Sendable>: ObservableObject {
public class Store<State>: ObservableObject, @unchecked Sendable {
@Published public internal(set) var state: State
public internal(set) var eventPublisher: PassthroughSubject<Action, Never>
public internal(set) var errorPublisher: PassthroughSubject<Error, Never>

private let reducer: Reducer<State>
private let middlewares: [Middleware<State>]
private var cancellables: Set<AnyCancellable> = []
private let queue = DispatchQueue(label: "store-\(UUID())")
private var completion: (() -> Void)?
private var subscribedEffects: Int = 0
private var completedEffects: Int = 0
Expand All @@ -63,36 +60,25 @@ public class Store<State: Sendable>: ObservableObject {
let newState = try reducer(state, action)
eventPublisher.send(action)
state = newState
runSideEfects(action, newState: newState)
} catch {
errorPublisher.send(error)
completion?()
}
}
subscribedEffects += middlewares.count
Task.detached { [unowned self] in
for middleware in middlewares {
let output = await middleware(newState, action)
DispatchQueue.main.async { [unowned self] in
if let output {
dispatch(output)
}

private func runSideEfects(_ action: Action, newState: State) {
middlewares.forEach { middleware in
subscribedEffects += 1
Deferred {
Future<Action?, Never> { promise in
let output = middleware(newState, action)
promise(.success(output))
completedEffects += 1
if completedEffects == subscribedEffects {
completion?()
}
}
}
}
.eraseToAnyPublisher()
.subscribe(on: queue)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [unowned self] _ in
completedEffects += 1
if completedEffects == subscribedEffects {
completion?()
}
}, receiveValue: { [unowned self] value in
if let value {
dispatch(value)
}
})
.store(in: &cancellables)
} catch {
errorPublisher.send(error)
completion?()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ struct Choice {
let selectionIndex: Int
}

private final class ChoicesWrapper: @unchecked Sendable {
private class ChoicesWrapper {
var choices: [Choice]

init(choices: [Choice]) {
Expand Down
13 changes: 8 additions & 5 deletions WildWestOnline/Core/Bang/Tests/SimulationTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,16 @@ private extension Middlewares {
/// Middleare reproducting state according to received event
static func verifyState(_ prevState: StateWrapper) -> Middleware<GameState> {
{ state, action in
guard let nextState = try? GameReducer().reduce(prevState.value, action) else {
fatalError("Failed reducing \(action)")
}
DispatchQueue.main.async {
guard let nextState = try? GameReducer().reduce(prevState.value, action) else {
fatalError("Failed reducing \(action)")
}

assert(nextState == state, "Inconsistent state after applying \(action)")

assert(nextState == state, "Inconsistent state after applying \(action)")
prevState.value = nextState
}

prevState.value = nextState
return nil
}
}
Expand Down

0 comments on commit 046f00a

Please sign in to comment.