Skip to content
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

Synchronous emission of the initially fetched values #28

Merged
merged 7 commits into from
Mar 24, 2018

Conversation

groue
Copy link
Collaborator

@groue groue commented Mar 23, 2018

This PR restores a very useful behavior that has been lost after the transition to RxSwift schedulers in v0.8.0, which is the guarantee, for database observables that target the main thread, of the synchronous emission of the initially fetched database values.

This feature is very important for view controllers that subscribe to database values in their viewDidLoad() method and rely on synchronous view setup:

override func viewDidLoad() {
    super.viewDidLoad()
    
    Player.all().rx
        .fetchAll(in: dbQueue)
        .subscribe(onNext: { [weak self] players in
            self?.updateView(players)
        })
        .disposed(by: disposeBag)
    
    // <- Here the view has been updated
}

Since v0.8.0, this useful behavior has been provided by the good old MainScheduler.instance.

But this is not enough.

Applications also need, sometimes, to update the database and then take immediate action without waiting for the database change notification that will eventually come (changes notifications are always asynchronously dispatched).

For example, an app may need to insert a row in the database, update the view, and then select and scroll to the matching row in a table view.

This use case is difficult when the delivery of updated database values is asynchronous. It requires heavy Rx setup in order to provide specific handling of the expected database change notification, only this one, and not others (considering any background thread may also perform database changes in the mean time).

It is not only difficult, but also error-prone. It's very easy to do it in a wrong way.

Conversely, this use case can be handled in a much easier way by simply resubscribing to the database content right after the database change. Here is the suggested setup:

override func viewDidLoad() {
    super.viewDidLoad()
    trackDatabase()
    // <- Here the view has been updated (1)
}

@IBAction func doStuff(_ sender: Any?) {
    try! dbQueue.inDatabase { db in
        // update database
    }
    
    // Resubscribe to the database content
    trackDatabase()
    // <- Here the view has been updated (2)
    
    // Further process updated view
    ...
}

private func trackDatabase() {
    databaseSubscription = Player.all().rx
        .fetchAll(in: dbQueue)
        .subscribe(onNext: { [weak self] players in
            self?.updateView(players)
        })
}

// The subscription to the database
private var databaseSubscription: Disposable? {
    willSet { databaseSubscription?.dispose() }
    didSet { databaseSubscription?.disposed(by: disposeBag) }
}

Did you notice the two "Here the view has been updated" comments above? The first one was already guaranteed. The second one is only guaranteed by this PR.

To achieve this behavior, RxGRDB no longer defaults to MainScheduler.instance.

To understand why MainScheduler.instance is not a correct fit, let's analyse what happens in the doStuff method:

  1. When the database is changed, MainScheduler.instance would asynchronously schedule a database change handler (the one that has been setup in viewDidLoad > trackDatabase).
  2. The subsequent call to trackDatabase immediately schedules a database change handler with MainScheduler.instance. But this handler can't run synchronously because of the previously scheduled one which has not yet been executed. It is asynchronously dispatched.
  3. We now have a problem: the view has not been updated after the call to trackDatabase.
  4. Unsubscribing from database changes before updating the database is not a sufficient fix: there may exist other database subscriptions that would make MainScheduler.instance asynchronously schedule database change handlers anyway.

This is not a bug of MainScheduler.instance, which makes very sure that scheduled items are executed in the same order as they are scheduled. It just happens that this ordering goes against the interest of the scenario detailed above: when an app needs synchronous notification, it needs to opt out from the strict scheduler ordering.

Applications that rely on strict ordering can still provide an explicit scheduler, as below. But they lose synchronous notifications:

Player.all().rx
    .fetchAll(in: dbQueue, scheduler: MainScheduler.instance)
    .subscribe(onNext: { [weak self] players in
        self?.updateView(players)
    })
// <- Here the view may not be updated yet

@groue
Copy link
Collaborator Author

groue commented Mar 24, 2018

I feel that this PR buys convenience at the cost of a reduced integration with RxSwift fundamentals. Adding support for schedulers in #19 and #23 was an important step towards integration in the family of the RxSwiftCommunity libraries. And now this PR makes a step back by avoiding schedulers by default, just to add a handy scheduling guarantee (but a really important one).

But helping developers easily write correct apps is more important than telling them that they're holding RxGRDB or RxSwift wrong. Maybe, as my RxSwift experience grows, will I become able to address the use case described above in a more "official" way. But as long as I scratch my own head solving it with pure RxSwift concepts, I'll expect that some other users to scratch their heads, too. Maybe some @RxSwiftCommunity/contributors will teach me the skills I need :-)

@groue groue merged commit 98f26cd into RxSwiftCommunity:develop Mar 24, 2018
@groue groue deleted the synchronousSubscription branch March 24, 2018 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant