diff --git a/Workflow/Sources/WorkflowNode.swift b/Workflow/Sources/WorkflowNode.swift index 2a0c9fc88..36d47f551 100644 --- a/Workflow/Sources/WorkflowNode.swift +++ b/Workflow/Sources/WorkflowNode.swift @@ -84,7 +84,6 @@ final class WorkflowNode { /// allowing the underlying conformance to be applied to the Workflow's State let outputEvent = openAndApply( action, - to: &state, isExternal: source == .external ) @@ -182,13 +181,10 @@ private extension WorkflowNode { /// Applies an appropriate `WorkflowAction` to advance the underlying Workflow `State` /// - Parameters: /// - action: The `WorkflowAction` to apply - /// - state: The `State` to which the action will be applied - /// - observerInfo: Optional observation info to notify registered `WorkflowObserver`s /// - isExternal: Whether the handled action came from the 'outside world' vs being bubbled up from a child node /// - Returns: An optional `Output` produced by the action application func openAndApply( _ action: A, - to state: inout WorkflowType.State, isExternal: Bool ) -> WorkflowType.Output? where A.WorkflowType == WorkflowType { let output: WorkflowType.Output? diff --git a/Workflow/Tests/WorkflowObserverTests.swift b/Workflow/Tests/WorkflowObserverTests.swift index c9f1b9607..3bd620e84 100644 --- a/Workflow/Tests/WorkflowObserverTests.swift +++ b/Workflow/Tests/WorkflowObserverTests.swift @@ -163,6 +163,35 @@ final class WorkflowObserverTests: XCTestCase { XCTAssertEqual(actions, [.toggle]) } + // added to exercise a simultaneous memory access bug when observing action + // application for some Workflow's where State == Void + func test_didReceiveActionCallbacks_voidState() { + var actions: [VoidStateWorkflow.Action] = [] + observer.onDidReceiveAction = { action, workflow, session in + guard let action = action as? VoidStateWorkflow.Action else { + XCTFail("unexpected action. expecting \(VoidStateWorkflow.Action.self), got \(type(of: action))") + return + } + + actions.append(action) + } + + let node = WorkflowNode( + workflow: VoidStateWorkflow(), + parentSession: nil, + observer: observer + ) + + let rendering = node.render() + node.enableEvents() + + XCTAssertEqual(actions, []) + + rendering.onTapped() + + XCTAssertEqual(actions, [.actionValue]) + } + func test_didReceiveActionCallbacks_onlyInvokedForExternalEvents() { var actionsReceived: [InjectableWorkflow.Action] = [] observer.onDidReceiveAction = { action, workflow, session in @@ -687,3 +716,32 @@ private struct DefaultObservers: ObserversInterceptor { observers + initialObservers } } + +// MARK: Void State Observation Crash Example + +/// Example that cause a memory exclusivity violation in prior observation code +private struct VoidStateWorkflow: Workflow { + typealias State = Void + typealias Output = Never + + enum Action: WorkflowAction { + typealias WorkflowType = VoidStateWorkflow + + case actionValue + + func apply(toState state: inout VoidStateWorkflow.State) -> VoidStateWorkflow.Output? { + return nil + } + } + + struct Rendering { + var onTapped: () -> Void + } + + func render(state: VoidStateWorkflow.State, context: RenderContext) -> Rendering { + let sink = context.makeSink(of: VoidStateWorkflow.Action.self) + return Rendering( + onTapped: { sink.send(.actionValue) } + ) + } +}