Skip to content

Allow re-entrant actions to be processed #1352

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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 }
}