Skip to content

Instantly share code, notes, and snippets.

@raimohanska
Last active November 1, 2018 09:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save raimohanska/aaa4a6f3cafc92c201eab745f1c71c69 to your computer and use it in GitHub Desktop.
Save raimohanska/aaa4a6f3cafc92c201eab745f1c71c69 to your computer and use it in GitHub Desktop.
Bacon.js glitch prevention

The key to glitch prevention is the UpdateBarrier. This component takes care of starting and finishing event transactions. The inTransaction function is used to wrap all event handling, creating a transaction that lasts until the original event and all derived events (those created when passing the event through all the maps, combines etc) have been processed.

In the end of the transaction, a flush occurs. During flush, all pending actions are performed. And what are these action then? They are actions registered using the whenDoneWith function, and are basically requests like "please run this function when the dependencies certain observable have been fully flushed". To satisfy these request, the UpdateBarrier then executes these delayed actions, from roots to leaves, making sure that the observables get flushed in the correct order, from the roots up. And by roots I mean the observables that have no dependencies on other ones, while leaves are the observables that no other observable depends on. To optimize performance, UpdateBarrier has a double bookkeeping of the pending actions.

Certain combinators, such as the combine* family and takeUntil, (both indirectly though) call the whenDoneWith method to make sure glitch don't occur. In the case of combine, that means emitting only one output event regardless of how many of its dependencies have emitted an event during the transaction. In the case of takeUntil it means that nothing is emitted if both the "source" and "stopper" observables have emitted during the transaction.

@MisterD123
Copy link

MisterD123 commented Oct 26, 2018

I am slightly confused by this description. Does one transaction encompass the processing of an original and all derived events, and a flush occurs only at the end of that entire transaction, or do flushes occur per recomputed observable? The last paragraph states, that combine uses whenDoneWith to prevent glitches, which means it would only execute during the flushes and not inside the regular transaction execution. The first paragraph though says, that combine is part of the event processing that happens inside of transactions.

Clicking through the code a little, I found two graph traversals:
I can see that whenDoneWith can call into flushDepsOf, which seems like it might implement a reverse depth-first graph traversal that would prevent glitches due achieving topological order of the dependency graph.

Dispatcher.prototype.pushIt, which appears to be part of the normal in-transaction processing, seems to implement a forwards breadth-first traversal of the dependency graph, which would not prevent glitches.

I have seen some libraries that implement "observer-only" glitch prevention so to speak, where glitches are allowed during change propagation but observers are scheduled to execute only at the end of transactions, meaning after all glitches have been settled, so that glitches that ocurr internally between events never become visible to outside observers. So far, my impression is that bacon.js implements this same phase separation, but then does not limit the "observer" phase to only observers. Instead, it allows regular events with dependencies between them to be part of the second phase, and then implements actual glitch prevention for that second phase only. Is that the case?

@raimohanska
Copy link
Author

raimohanska commented Nov 1, 2018

Flush occurs at the end of the transaction including all the derived events.

The badly named pushIt method doesn't traverse into dependencies, but merely flushes possibly queued events out. This is something that occurs in the scope of a single observable, within a transaction.

I'll try to clarify the overall algorithm below.

  1. When an event is triggered from the "outside", a transaction is started and the event is pushed forward through all the deps all the way to the leaves. Exceptions to this flow include
    1.1 Updates to external subscribers are deferred by the subscribe method, to be flushed after the transaction is complete, in phase 3
    1.2 In some multi-dependent combinators like combine, also internal processing is deferred to phase 2 using whenDoneWith to prevent glitches.
  2. At the end of the transaction, all the "waiters" registered with whenDoneWith are processed in a roots-first order, as described in the Gist.
  3. After this phase is done all the "afters" are processed. This includes flushing to the external subscribers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment