diff --git a/ReSwift/CoreTypes/Store.swift b/ReSwift/CoreTypes/Store.swift index 0d3ba9d9..d76045cb 100644 --- a/ReSwift/CoreTypes/Store.swift +++ b/ReSwift/CoreTypes/Store.swift @@ -32,14 +32,15 @@ open class Store: StoreType { } } + ///The dispatch function to be used. + /// + ///Note: The default dispatch function runs alls reductions within a thread-safe synchronized block. public var dispatchFunction: DispatchFunction! private var reducer: Reducer var subscriptions: [SubscriptionType] = [] - private var isDispatching = false - public required init( reducer: @escaping Reducer, state: State?, @@ -109,20 +110,10 @@ open class Store: StoreType { // swiftlint:disable:next identifier_name open func _defaultDispatch(action: Action) { - guard !isDispatching else { - raiseFatalError( - "ReSwift:ConcurrentMutationError- Action has been dispatched while" + - " a previous action is action is being processed. A reducer" + - " is dispatching an action, or ReSwift is used in a concurrent context" + - " (e.g. from multiple threads)." - ) - } - - isDispatching = true + objc_sync_enter(self) let newState = reducer(action, state) - isDispatching = false - state = newState + objc_sync_exit(self) } open func dispatch(_ action: Action) { diff --git a/ReSwiftTests/StoreDispatchTests.swift b/ReSwiftTests/StoreDispatchTests.swift index 2f65f14f..d2d5a23f 100644 --- a/ReSwiftTests/StoreDispatchTests.swift +++ b/ReSwiftTests/StoreDispatchTests.swift @@ -24,14 +24,33 @@ class StoreDispatchTests: XCTestCase { } /** - it throws an exception when a reducer dispatches an action + it accepts actions coming from multiple threads and handles them synchronously */ - func testThrowsExceptionWhenReducersDispatch() { - // Expectation lives in the `DispatchingReducer` class - let reducer = DispatchingReducer() - store = Store(reducer: reducer.handleAction, state: TestAppState()) - reducer.store = store - store.dispatch(SetValueAction(10)) + func testDispatchInMultipleThreadsAreSynchronized() { + store.dispatch(SetValueAction(5)) + + let firstAction = DelayedAction(3) + let secondAction = DelayedAction(10) + dispatchAsync { + self.store.dispatch(firstAction) + } + + dispatchAsync { + self.store.dispatch(secondAction) + } + + XCTAssertFalse(secondAction.called) + waitFor { firstAction.called == true } + + XCTAssertEqual(store.state.testValue, 5) + firstAction.unblock() + + waitFor { store.state.testValue == 3 } + + waitFor { secondAction.called == true } + secondAction.unblock() + + waitFor { store.state.testValue == 10 } } /** @@ -111,6 +130,17 @@ class StoreDispatchTests: XCTestCase { } } } + + func waitFor(timeout: TimeInterval = 1, + file: StaticString = #file, + line: UInt = #line, + block: () -> Bool) { + let maxTime = Date().addingTimeInterval(timeout).timeIntervalSince1970 + while Date().timeIntervalSince1970 < maxTime { + if block() { return } + } + XCTFail("Timed out waiting for condition to be true", file: file, line: line) + } } // Needs to be class so that shared reference can be modified to inject store diff --git a/ReSwiftTests/TestFakes.swift b/ReSwiftTests/TestFakes.swift index 1976a851..6ac9753c 100644 --- a/ReSwiftTests/TestFakes.swift +++ b/ReSwiftTests/TestFakes.swift @@ -59,6 +59,18 @@ struct SetValueAction: Action { } } +class DelayedAction: Action { + var blocked: Bool = true + var called: Bool = false + let value: Int? + + func unblock() { blocked = false } + + init(_ value: Int) { + self.value = value + } +} + struct SetValueStringAction: Action { var value: String @@ -87,6 +99,11 @@ struct TestReducer { case let action as SetValueAction: state.testValue = action.value return state + case let action as DelayedAction: + action.called = true + while action.blocked { RunLoop.main.run(until: Date()) } + state.testValue = action.value + return state default: return state }