Skip to content

Commit

Permalink
Fix forked Workflow invoking didComplete multiple times
Browse files Browse the repository at this point in the history
Because didComplete is added at the end of the Workflow Rx side-effect, if a Workflow is forked, multiple Rx chains with the side-effect are created and subscribed to. This means the didComplete is invoked duplicately.

Fixes #211
  • Loading branch information
neakor committed Feb 7, 2018
1 parent 58fecc5 commit de33e01
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 22 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
### Version 0.1.0

* Initial release

### Version 0.9.2

* Fix forked Workflow invoking didComplete multiple times
2 changes: 1 addition & 1 deletion RIBs.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'RIBs'
s.version = '0.9.1'
s.version = '0.9.2'
s.summary = 'Uber\'s cross-platform mobile architecture.'
s.description = <<-DESC
RIBs is the cross-platform architecture behind many mobile apps at Uber. This architecture framework is designed for mobile apps with a large number of engineers and nested states.
Expand Down
21 changes: 19 additions & 2 deletions ios/RIBs/Classes/Workflow/Workflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,25 @@ open class Workflow<ActionableItemType> {
return compositeDisposable
}

// MARK: - Private Interface
// MARK: - Private

private let subject = PublishSubject<(ActionableItemType, ())>()
private var didInvokeComplete = false

/// The composite disposable that contains all subscriptions including the original workflow
/// as well as all the forked ones.
fileprivate let compositeDisposable = CompositeDisposable()

fileprivate func didCompleteIfNotYet() {
// Since a workflow may be forked to produce multiple subscribed Rx chains, we should
// ensure the didComplete method is only invoked once per Workflow instance. See `Step.commit`
// on why the side-effects must be added at the end of the Rx chains.
guard !didInvokeComplete else {
return
}
didInvokeComplete = true
didComplete()
}
}

/// Defines a single step in a `Workflow`.
Expand Down Expand Up @@ -145,8 +160,10 @@ open class Step<WorkflowActionableItemType, ActionableItemType, ValueType> {
/// - returns: The committed `Workflow`.
@discardableResult
public final func commit() -> Workflow<WorkflowActionableItemType> {
// Side-effects must be chained at the last observable sequence, since errors and complete
// events can be emitted by any observables on any steps of the workflow.
let disposable = observable
.do(onError: workflow.didReceiveError, onCompleted: workflow.didComplete)
.do(onError: workflow.didReceiveError, onCompleted: workflow.didCompleteIfNotYet)
.subscribe()
_ = workflow.compositeDisposable.insert(disposable)
return workflow
Expand Down
58 changes: 39 additions & 19 deletions ios/RIBsTests/Workflow/WorkflowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ import RxSwift

final class WorkerflowTests: XCTestCase {

// MARK: - Tests

func test_nestedStepDoNotRepeat() {
func test_nestedStepsDoNotRepeat() {
var outerStep1RunCount = 0
var outerStep2RunCount = 0
var outerStep3RunCount = 0
Expand Down Expand Up @@ -79,7 +77,7 @@ final class WorkerflowTests: XCTestCase {
XCTAssertEqual(innerStep3RunCount, 1, "Inner step 3 should not have been run more than once")
}

func test_workflowRecivesError() {
func test_workflowReceivesError() {
let workflow = TestWorkflow()

let emptyObservable = Observable.just(((), ()))
Expand Down Expand Up @@ -126,6 +124,37 @@ final class WorkerflowTests: XCTestCase {
XCTAssertEqual(0, workflow.errorCallCount)
}

func test_workflowDidFork() {
let workflow = TestWorkflow()

let emptyObservable = Observable.just(((), ()))
_ = workflow
.onStep { _ -> Observable<((), ())> in
return emptyObservable
}
.onStep { _, _ -> Observable<((), ())> in
return emptyObservable
}
.onStep { _, _ -> Observable<((), ())> in
return emptyObservable
}
.onStep { _, _ -> Observable<((), ())> in
let forkedStep: Step<(), (), ()>? = emptyObservable.fork(workflow)
forkedStep?
.onStep { _, _ -> Observable<((), ())> in
return emptyObservable
}
.commit()
return emptyObservable
}
.commit()
.subscribe(())

XCTAssertEqual(1, workflow.completeCallCount)
XCTAssertEqual(1, workflow.forkCallCount)
XCTAssertEqual(0, workflow.errorCallCount)
}

func test_fork_verifySingleInvocationAtRoot() {
let workflow = TestWorkflow()

Expand All @@ -135,7 +164,7 @@ final class WorkerflowTests: XCTestCase {
.onStep { _ -> Observable<((), ())> in
rootCallCount += 1
return emptyObservable
}
}

let firstFork: Step<(), (), ()>? = rootStep.asObservable().fork(workflow)
_ = firstFork?
Expand Down Expand Up @@ -163,29 +192,20 @@ private enum WorkflowTestError: Error {
case error
}

private final class TestWorkflow: Workflow<()> {

private(set) var completeCallCount: Int = 0
private(set) var errorCallCount: Int = 0
private(set) var forkCallCount: Int = 0

// MARK: - Overrides
private class TestWorkflow: Workflow<()> {
var completeCallCount = 0
var errorCallCount = 0
var forkCallCount = 0

override func didComplete() {
super.didComplete()

completeCallCount += 1
}

override func didFork() {
super.didFork()

errorCallCount += 1
forkCallCount += 1
}

override func didReceiveError(_ error: Error) {
super.didReceiveError(error)

errorCallCount += 1
}
}

0 comments on commit de33e01

Please sign in to comment.