Abstract
As most sophisticated web apps, Orbeon Forms has to deal with lots of asynchronous events in the browser, like Ajax requests, timers, and user interactions. We'll go over a real-life example of logic involving several asynchronous events, see the difficulty involved in implementing it with observers, and explore 3 alternatives: state machines,await
, and Functional Reactive Programming (FRP). We'll see why, based on our experience, we don't consider state machines to be a good solution, and through an example explore the two remaining alternatives, await
and FRP.Example of the loading indicator
Orbeon Forms can show on the page a "loading indicator" to inform users that something is happening: data was sent to the server, and we're waiting for a response, maybe because we need to validate user input, save data, or call a service. Its logic works is as follows:- In essence, when a request is in progress, we want the loading indicator to show.
- However, if the request takes very little time, we don't want to show the loading indicator.
- Also, when a request comes back, we don't want to hide the indicator if we already know we'll right away send another request to the server.
The problem with observers
The browser gives us events, and we can run code when those events happen. Code running upon some event is called an observer. For the loading indicator, we would have an observer when an Ajax request starts, and another one when it ends. Since we don't want to show the indicator right away, the observer on the "Ajax request started" starts a timer for say 200 ms. So we would have an observer on that timer, which on completion shows the indicator if the request is still in progress. But to know that, we need to have state (isRequestInProgress
), shared between the two observers.But a boolean isn't enough. Say you start a first request at
t = 0 ms
. At that point, you also start a timer for 200 ms. Say that first request ends at t = 100 ms
. Now say a second request starts at t = 150 ms
. At t = 200 ms
, the first timer ends: a request is still in progress, so it shows the loading indicator. Bug! At that point the request has been going on for only 50 ms, so we shouldn't show the loading indicator just yet. One way to solve this problem is to use a counter, instead of a boolean, an increment/decrement that counter at the right time.I won't go here through all the details of the implementation, but the end result is that the above logic is spread across a number of observers that share and update some global state. Logic written this way is surprisingly hard to maintain, and subtle bugs can lead to a loading indicator that stays "stuck", i.e. shown even if there no Ajax request in progress, or that doesn't show anymore.
Possible solutions
We're aware of 3 alternatives to observers:- State machines
await
- Functional reactive programming (FRP)
State machines
In Form Builder, we started using a state machine about 2 years ago, and had a mixed experience with that approach. A state machine solves part of the observer problem, by putting the high level logic in a single place, in a fairly declarative form. However, we found that complex state machines are hard to debug, as you can't debug the declaration of the state machine itself, but instead have to debug the generic implementation of the state machine.
Experimenting with await
and FRP
Before going for await
or FRP in production code, we wanted to do some experimentation. For this, we took the example described in the excellent paper Deprecating the Observer Pattern. The example uses the canvas and the mousedown
, mousemove
, and mouseup
events to let the user draw on the canvas. You'll find below the CoffeeScript code for 3 implementations:- With observers, using plain jQuery.
- With
await
, using IcedCoffeeScript. - With FRP, using Bacon.js.
With observers
isDown = false
canvas.on 'mousedown', (e) ->
isDown = true
startLine(e)
canvas.on 'mousemove', (e) ->
if isDown
continueLine(e)
canvas.on 'mouseup', (e) ->
isDown = false
See and run the code
With await
while true
await canvas.one('mousedown', defer(e))
startLine(e)
until e.type == 'mouseup'
rv = new iced.Rendezvous
canvas.one('mousemove', rv.id().defer(e))
canvas.one('mouseup' , rv.id().defer(e))
await rv.wait(defer())
continueLine(e)
See and run the codeWith FRP
downStream = canvas.asEventStream('mousedown')
upStream = canvas.asEventStream('mouseup')
moveStream = canvas.asEventStream('mousemove')
isDownProp = downStream.map(true)
.merge(upStream.map(false))
.toProperty(false)
moveWhenDownStream = moveStream.filter(isDownProp)
downStream .onValue(startLine)
moveWhenDownStream .onValue(continueLine)
See and run the codeConclusion
We believe that bothawait
and FRP are significant improvement over observers or a state machine. At this point, we have a slight preference for await
, as we think that:- Most developers will find it easier to understand code using
await
than code using FRP, and we believe in Erik Meijer's mantra: "write baby code". - We think that code using
await
will be easier to debug than code using FRP, as it will be easier to put breakpoints in the right place, and follow the code execution.
No comments:
Post a Comment