Skip to content

Commit

Permalink
Allow re-entrant actions to be processed (#1352)
Browse files Browse the repository at this point in the history
* wip

* wip

* clean up

* wip

* wip

* wip

* wip

* fix

Co-authored-by: Brandon Williams <mbrandonw@hey.com>
  • Loading branch information
stephencelis and mbrandonw committed Sep 8, 2022
1 parent 4c98b43 commit 5b78fbc
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 4 deletions.
Expand Up @@ -105,5 +105,6 @@ final class LoginCoreTests: XCTestCase {
await store.send(.twoFactorDismissed) {
$0.twoFactor = nil
}
await store.finish()
}
}
15 changes: 12 additions & 3 deletions Sources/ComposableArchitecture/Store.swift
Expand Up @@ -327,15 +327,24 @@ public final class Store<State, Action> {

self.isSending = true
var currentState = self.state.value
let tasks = Box<[Task<Void, Never>]>(wrappedValue: [])
defer {
withExtendedLifetime(self.bufferedActions) {
self.bufferedActions.removeAll()
}
self.isSending = false
self.state.value = currentState
// NB: Handle any re-entrant actions
if !self.bufferedActions.isEmpty {
if let task = self.send(
self.bufferedActions.removeLast(), originatingFrom: originatingAction
) {
tasks.wrappedValue.append(task)
}
}
}

let tasks = Box<[Task<Void, Never>]>(wrappedValue: [])

var index = self.bufferedActions.startIndex
defer { self.bufferedActions = [] }
while index < self.bufferedActions.endIndex {
defer { index += 1 }
let action = self.bufferedActions[index]
Expand Down
14 changes: 13 additions & 1 deletion Sources/ComposableArchitecture/TestStore.swift
Expand Up @@ -238,7 +238,19 @@

self.store = Store(
initialState: initialState,
reducer: Reducer<State, TestAction, Void> { [unowned self] state, action, _ in
reducer: Reducer<State, TestAction, Void> { [weak self] state, action, _ in
guard let self = self
else {
XCTFail(
"""
An effect sent an action to the store after the store was deallocated.
""",
file: file,
line: line
)
return .none
}

let effects: Effect<Action, Never>
switch action.origin {
case let .send(scopedAction):
Expand Down
85 changes: 85 additions & 0 deletions Tests/ComposableArchitectureTests/CompatibilityTests.swift
@@ -0,0 +1,85 @@
import Combine
import CombineSchedulers
import ComposableArchitecture
import XCTest

@MainActor
final class CompatibilityTests: XCTestCase {
func testCaseStudy_ReentrantEffect() {
let cancelID = UUID()

struct State: Equatable {}
enum Action: Equatable {
case start
case kickOffAction
case actionSender(OnDeinit)
case stop

var description: String {
switch self {
case .start:
return "start"
case .kickOffAction:
return "kickOffAction"
case .actionSender:
return "actionSender"
case .stop:
return "stop"
}
}
}
let passThroughSubject = PassthroughSubject<Action, Never>()

var handledActions: [String] = []

let reducer = Reducer<State, Action, Void> { state, action, env in
handledActions.append(action.description)

switch action {
case .start:
return passThroughSubject
.eraseToEffect()
.cancellable(id: cancelID)

case .kickOffAction:
return Effect(value: .actionSender(OnDeinit { passThroughSubject.send(.stop) }))

case .actionSender:
return .none

case .stop:
return .cancel(id: cancelID)
}
}

let store = Store(
initialState: .init(),
reducer: reducer,
environment: ()
)

let viewStore = ViewStore(store)

viewStore.send(.start)
viewStore.send(.kickOffAction)

XCTAssertNoDifference(
handledActions,
[
"start",
"kickOffAction",
"actionSender",
"stop",
]
)
}
}

private final class OnDeinit: Equatable {
private let onDeinit: () -> ()
init(onDeinit: @escaping () -> ()) {
self.onDeinit = onDeinit
}
deinit { self.onDeinit() }
static func == (lhs: OnDeinit, rhs: OnDeinit) -> Bool { true }
}

0 comments on commit 5b78fbc

Please sign in to comment.