Asynchronous Methods

Table of Contents

!!! This tutorial is still being updated for version 2.1. Please check back later.

1 Asynchronous concepts

1.1 Asynchronous execution

When methods are executed synchronously it means that they are run one after another, and that each method must complete before the next method can begin. This can be problematic if a method takes a long time to finish — e.g., a method which needs to make a request to a server. Such methods, if run synchronously, can cause the interface to "hang" or become unresponsive.

When methods are executed asynchronously, the system does not wait for them to finish, but instead resumes normal operation while methods continue to run. Methods begin execution as soon as their inputs are ready and signal when they are completed. In this way, the interface remains responsive even when methods take a long time to execute.

Note that, in order to be useful, an asynchronous method requires some way of executing code "in the background" so that it does not tie up the main event-handling thread. JavaScript provides several mechanisms in support of this. For example, AJAX allows asynchronous server requests, and web workers allow JavaScript to run on a different thread. HotDrink is designed to work regardless of which mechanism you use for asynchronous code execution (though it does provide some light-weight wrappers to facilitate simple AJAX and web worker tasks — see Sections 6 and 7). This gives you the freedom to make use of whatever mechanism you wish to implement your methods.

1.2 Promises

A promise represents a value that will be supplied later. Promises are a common programming idiom for asynchronous programming. HotDrink promises follow the Promises/A+ specification — an open standard for interoperable JavaScript promises. There are many good sources of documentation on using promises in JavaScript, therefore, we show only the basics here.

 1: // "p" is a new promise
 2: var p = new hd.Promise();
 3: 
 4: // Schedule function "f1" to be called with the value of "p" once it's ready
 5: // Returns a promise for the result of "f1" which we store in "q"
 6: var q = p.then(
 7:   function f1( v ) {
 8:     return v + 10;
 9:   }
10: );
11: 
12: // Schedule function "f2" to be called with the value of "q" once it's ready
13: // This, too, returns a promise, but we ignore it
14: //   (we're not interested in the result of f2)
15: q.then(
16:   function f2( w ) {
17:     console.log( 'Value: ' + w );
18:   }
19: );
20: 
21: // Give "p" a value
22: p.resolve( 17 );
23: 
24: // This causes "f1" to be called with 17 as the argument...
25: //   ...which returns 27...
26: //   ...which resolves promise "q"...
27: //   ...which causes "f2" to be called with 27 as the argument...
28: //   ...resulting in "Value: 27" being printed to the log

Using promises involves three steps.

  1. Create a new promise object. This is a simple construction using the new operator, as shown in line 2.
  2. Register one or more functions to be executed once the promise has been given a value. (We call this scheduling the functions.) This is done using the then function, as shown in line 6. When you schedule a function, you get back a promise for the result of that function. In this way you can chain together asynchronous calculations — the value produced by one will be passed to the next. You can see this on line 15 where we schedule f2 to be run using the result of f1.
  3. Resolve the promise with a value. This is done using the resolve function, as shown in line 22.

Note: It is not required that you give names to functions you schedule for promises; we only did so to make it easier to refer to them in other places.

2 Asynchronous methods

2.1 The example

View

1: <div id="ex1" style="display:inline-block; text-align:right">
2:   <style type="text/css" scoped>
3:     .pending { background: right center no-repeat url(spinner.gif); }
4:   </style>
5:   Total Cost: <input type="text" data-bind="hd.numVar( total, 2 )"/><br/>
6:   Tip Percentage: <input type="text" data-bind="hd.numVar( percent, 0 )"/><br/>
7:   Tip: <input type="text" data-bind="hd.numVar( tip, 2 )" readonly/>
8: </div>

View-Model

 1: var model = new hd.ModelBuilder()
 2:   .vs( 'total, percent, tip', {total: 50, percent: 20} )
 3: 
 4:   .c( 'total, percent, tip' )
 5:   .asyncMethod( 'total, percent -> tip', function( pTotal, pPercent ) {
 6:     var pTip = new hd.Promise();
 7: 
 8:     pTotal.then( function f1( total ) {
 9:       pPercent.then( function f2( percent ) {
10:         setTimeout( function() { pTip.resolve( total * percent / 100 ); }, 3000 );
11:       } );
12:     } );
13: 
14:     return pTip;
15:   } )
16: 
17:   .end();
18: 
19: var system = new hd.ConstraintSystem();
20: system.addComponent( model );

Binding

1: window.addEventListener( 'load', function() {
2:   hd.performDeclaredBindings( model, document.getElementById( 'ex1' ) );
3: } )

Result

Total Cost:
Tip Percentage:
Tip:

Notes

This example implements a simple tip calculator. If you modify the cost or the percentage, you will notice that there is a delay in calculating the tip. We used an asynchronous method to allow the user interface to remain responsive even during this delay.

Because the code for this asynchronous method is a little bulky, we have written only a single method for our constraint. (That's why you cannot edit the tip directly: the only option for solving the constraint is to calculate the tip.) Later we will see how to make the code for asynchronous methods more compact.

2.2 Activations and activating functions

We refer to a single execution of a method as an activation of that method. The activation of a synchronous method corresponds to a single function call. For asynchronous methods this is no longer the case: the activation of an asynchronous method may involve multiple callback functions running in response to things such as Ajax calls, web workers, etc. However, there must still be one single function which initiates the activation and which tells us how to get the results when it is finished. We call this function the activating function for the method.

We can create an asynchronous method using the asyncMethod member function of the model builder. (Note that, as with other builder member functions, we can abbreviate this as simply a.) The parameter to asyncMethod is the activating function for the method.

Whereas the functions for synchronous methods take the values of input variables as parameters and return new values for output variables, activating functions take promises for the values of input variables as parameters, and return promises for new values for output variables. Thus, the activating function for the method "total, percent -> tip" (on line 5 of the view-model) takes two parameters: a promise for the value of total and a promise for the value of percent; and it returns a promise for a new value of tip.

In general, then, an activating function should behave as follows:

  1. Take input promises as parameters. Note that, on line 5 of the view-model source, we named our parameters \(pTotal\) and \(pPercent\) to remind us that these are promises, not values.
  2. Create promise(s) for the outputs of the method. You can see this on line 6 of the view-model source.
  3. Schedule a function or functions to be run once the input promises have been resolved; these functions should compute values for the output promises. You can see this in the view-model source. We start by scheduling \(f1\) to run when \(x\) is ready (line 8); \(f1\) will schedule \(f2\) to run when \(y\) is ready (line 9); and \(f2\) will calculate the result and resolve the promise we made for \(z\) (line 10). In order to make the delay noticeable, we use a three-second timer to resolve \(z\) instead of resolving right away.
  4. Return the output promise(s). You can see this on line 14 of the view-model source. As with ordinary methods, if there is more than one output you should return an array of promises.

To reiterate, the activating function itself is not asynchronous. Any function is inherently synchronous: once you start it, you must wait for it to finish. Your activating function should not take long to run, or else the system will hang. However, the purpose of the activating function is not to perform the work of the method; rather, its purpose is to schedule the work so that it will proceed on its own once the inputs are ready. (In our example it is function \(f2\) where the actual work of the method is performed.)

2.3 Pending variables

Every variable has a read-only Boolean property named pending. This property is true when the variable's value is scheduled to be changed once a promise has been resolved. The suggested use for this variable is with a CSS binding (e.g., the hd.bindCssClass binder) to give some sort of visual indication that this variable will be updated soon.

The hd.numVar factory function we use, e.g., on line 5 of the view, creates a binding which adds the pending CSS class to an element when the variable's pending property is true, and adds the complete CSS class when it is false. We refer to this in the CSS block on line 2 of the view to show a spinner when the variable is pending.

For more information on binding specifications, see the Advanced Binding Concepts document.

2.4 Caveat

There are some subtleties to writing your own asynchronous method that are beyond the scope of this document. (We will discuss them later in our Programming Guide.) Thus, for the time being, we suggest you use HotDrink's function lifting mechanism to create activating functions for asynchronous methods. We'll talk about that next.

3 Lifting functions

3.1 The example

View

1: <div id="ex2" style="display:inline-block; text-align:right">
2:   Dividend: <input type="text" data-bind="hd.numVar( n, 0 )"/><br/>
3:   Divisor: <input type="text" data-bind="hd.numVar( d, 0 )"/><br/>
4:   Quotient: <input type="text" data-bind="hd.numVar( q, 0 )"/><br/>
5:   Remainder: <input type="text" data-bind="hd.numVar( r, 0 )"/>
6: </div>

View-Model

 1: function divideRemainder( n, d ) {
 2:   return [Math.floor( n / d ), n % d];
 3: }
 4: 
 5: function multiplyRemainder( d, q, r ) {
 6:   return q*d + r;
 7: }
 8: 
 9: 
10: var model = new hd.ModelBuilder()
11:   .vs( 'n, d, q, r', {n: 50, q: 6} )
12: 
13:   .c( 'n, d, q, r' )
14:   .a( 'n, d -> q, r', hd.liftFunction( divideRemainder, 2 ) )
15:   .a( 'n, q -> d, r', hd.liftFunction( divideRemainder, 2 ) )
16:   .a( 'd, q, r -> n', hd.liftFunction( multiplyRemainder ) )
17: 
18:   .end();
19: 
20: var system = new hd.ConstraintSystem();
21: system.addComponent( model );

Binding

1: window.addEventListener( 'load', function() {
2:   hd.performDeclaredBindings( model, document.getElementById( 'ex2' ) );
3: } )

Result

Dividend:
Divisor:
Quotient:
Remainder:

Notes

Notice that all methods in this example are created as asynchronous methods. However, rather than writing our own activating functions, we lifted functions which operate on values.

3.2 Using liftFunction

The purpose of liftFunction is to transform a function which operates on values into a function which operates on promises. Thus, the parameter for liftFunction is a function which takes values and returns values, and the return value is a function which takes promises and returns promises. This lifted function behaves as follows.

  1. Takes input promises as parameters.
  2. Creates output promises.
  3. Schedules a function to be run once all input promises are resolved. This function will pass the input values to the original function (which was passed to liftFunction) and use the result to resolve the output promises.
  4. Returns the output promises.

Note that such a function works perfectly as an activating function for an asynchronous method. Thus, in effect, liftFunction can turn a synchronous method into an asynchronous method.

The lifted function does not need to know ahead of time how many inputs to pass to the original function: it simply passes as many inputs as it receives promises. However, it does need to know how many outputs the original function will return ahead of time: it must create output promises before it can examine the output of the original function. By default, liftFunction creates a lifted function which has only one output. If your lifted function will have more than one output, you must provide the number of outputs as the second parameter to liftFunction; you can see this on line 14 and line 15 of the view-model source.

3.3 The truth about "ordinary" methods

It's time to come clean. Up to now we've been discussing asynchronous methods as if they were somehow different from other methods. In fact, if you're going to allow some methods to be asynchronous, then they must all be asynchronous (because if one method is going to take a while to execute, then other methods are going to have to be prepared to wait until it is done).

The truth of the matter is, all methods in HotDrink are asynchronous methods. The only difference between creating a method with the method builder function and creating a method with the asyncMethod builder function is that the method builder function will use liftFunction to create an activating function for your method, whereas asyncMethod assumes you have given it the activating function.

Thus, in the example above, we could have saved ourselves some trouble and defined our view-model as follows.

1: var model = new hd.ModelBuilder()
2:   .vs( 'n, d, q, r', {n: 50, q: 6} )
3: 
4:   .c( 'n, d, q, r' )
5:   .m( 'n, d -> q, r', divideRemainder )
6:   .m( 'n, q -> d, r', divideRemainder )
7:   .m( 'q, r, d -> n', multiplyRemainder )
8: 
9:   .end();

This produces exactly the same view-model as the source in the example. (Note that we do not need to specify the number of outputs here, as the model builder can deduce it from the signature.) Thus, there really is no reason to call liftFunction directly; it is simpler just to use the method (a.k.a. m) member function.

And so, we've come full circle. It turns out we've been writing asynchronous methods all along and didn't realize it. However, now that we know what's really going on with our methods, let's discuss a few tricks that we can do.

4 Partial lifting: returning promises

4.1 The example

View

1: <div id="ex3" style="display:inline-block; text-align:right">
2:   <style type="text/css" scoped>
3:     .pending { background: right center no-repeat url(spinner.gif); }
4:   </style>
5:   Width: <input type="text" data-bind="hd.numVar( w )"/><br/>
6:   Height: <input type="text" data-bind="hd.numVar( h )"/><br/>
7:   Area: <input type="text" data-bind="hd.numVar( a )"/><br/>
8:   Perimeter: <input type="text" data-bind="hd.numVar( p )"/>
9: </div>

View-Model

 1: function delayedPromise( val ) {
 2:   var p = new hd.Promise();
 3:   setTimeout( function() { p.resolve( val ) }, 5000 );
 4:   return p;
 5: }
 6: 
 7: var model = new hd.ModelBuilder()
 8:   .vs( 'w, h, a, p', {w: 80, h: 60} )
 9: 
10:   .c( 'w, h, a' )
11:   .m( 'w, h -> a', function( w, h ) { return w*h; } )
12:   .m( 'a, h -> w', function( a, h ) { return delayedPromise( a/h ); } )
13:   .m( 'a, w -> h', function( a, w ) { return delayedPromise( a/w ); } )
14: 
15:   .c( 'w, h, p' )
16:   .m( 'w, h -> p', function( w, h ) { return 2*w + 2*h; } )
17:   .m( 'p, h -> w', function( p, h ) { return delayedPromise( p/2 - h ); } )
18:   .m( 'p, w -> h', function( p, w ) { return delayedPromise( p/2 - w ); } )
19: 
20:   .end();
21: 
22: var system = new hd.ConstraintSystem();
23: system.addComponent( model );

Binding

1: window.addEventListener( 'load', function() {
2:   hd.performDeclaredBindings( model, document.getElementById( 'ex3' ) );
3: } )

Result

Width:
Height:
Area:
Perimeter:

Notes

This example enforces the expected relations between the width, height, area, and perimeter of a rectangle. However, any calculation of width or height is delayed by five seconds. Note that, during this delay, you may continue to edit values in the form.

4.2 A promise for a promise

Notice that all methods in the example are created using the method builder function, and yet some of them return promises as if they were activating functions for asynchronous methods. How does this work?

According to the Promises/A+ specification, if you resolve a promise \(p\) using a second promise \(q\), the behavior is as follows: \(p\) remains in a pending state until \(q\) is resolved; as soon as \(q\) is resolved, then \(p\) will automatically be resolved using the same value as \(q\).

Consider what this means for functions lifted with liftFunction. When you lift a function, the return values of the original function are used to resolve the corresponding output promises of the lifted function. Thus, if your original function returns a promise, that promise will be used to resolve the corresponding output promise — and later when you resolve that promise, the output promise will automatically be resolved using the same value.

Thus, the function for a synchronous method — one which will be lifted with liftFunction — can return a promise just as an activating function can. In both cases, the end result is the same: whatever value eventually given to the promise will become the output of the method.

4.3 Coordinating promises

One common problem with implementing asynchronous behavior in a user interface is managing edits that are made before all asynchronous calculations have completed. On the one hand, you don't want your interface to lock up, refusing to allow any user input until the current calculations have complete. But, on the other hand, you also don't want to start new calculations using stale values which are scheduled to be updated.

Fortunately, this type of organization is completely handled for you by HotDrink. If you play around with the example above — rapidly making edits to multiple fields — you can see that the user interface remains fully responsive even as it waits on promises to be resolved. When an edit is made to a HotDrink variable, HotDrink will immediately run the activating functions for all methods which should be executed in response. (Remember, every method is asynchronous, so every method has a activating function.) Later, once all input promises have been resolved, the actual work of the method will take place.

Perhaps a helpful analogy would be water flowing through a pipeline. Promises are like the pipes, defining where values will eventually be, and values are like the water flowing through the pipes. The activating function for an asynchronous method constructs a small pipeline for the method: input pipes and output pipes are connected to some calculation, creating a pipeline suitable for conducting water at some point in the future. HotDrink then connects those small pipelines into one large, continuous pipeline. New pipelines can be added on to this, even before there is water in the pipes. Eventually the water will flow forward, filling all the pipes; how quickly the water comes does not affect the results.

5 Partial lifting: promise parameters

5.1 The example

View

 1: <div id="ex4">
 2:   <style type="text/css" scoped>
 3:     .pending { background: #eee right center no-repeat url(spinner.gif); }
 4:   </style>
 5:   <table>
 6:     <tr>
 7:       <td>Quantity:</td>
 8:       <td><input type="text" data-bind="hd.numVar( qty, 0 )"/></td>
 9:     </tr><tr>
10:       <td>Unit price:</td>
11:       <td><input type="text" data-bind="hd.numVar( unit, 2 )"/></td>
12:     </tr><tr>
13:       <td>Subtotal:</td>
14:       <td data-bind="hd.text( sub, hd.fix( 2 ) )"></td>
15:     </tr><tr>
16:       <td colspan="2"><hr/></td>
17:     </tr><tr>
18:       <td>
19:         <input id="shipcheck" type="checkbox" data-bind="hd.checked( doship )"/>
20:         <label for="shipcheck">Ship to:</label>
21:       </td>
22:       <td>
23:         <select data-bind="hd.value( state ), hd.enabled( state.relevant )">
24:           <option>Texas</option>
25:           <option>Oklahoma</option>
26:           <option>Arkansas</option>
27:           <option>Louisiana</option>
28:         </select>
29:       </td>
30:     </tr><tr>
31:       <td>Shipping:</td>
32:       <td data-bind="hd.text( ship, hd.fix( 2 ) ), hd.cssClass( ship )"></td>
33:     </tr><tr>
34:       <td colspan="2"><hr/></td>
35:     </tr><tr>
36:       <td>Total:</td>
37:       <td data-bind="hd.text( total, hd.fix( 2 ) ), hd.cssClass( total )"></td>
38:     </tr>
39:   </table>
40: </div>

View-Model

 1: var model = new hd.ModelBuilder()
 2:   .vs( 'qty, unit, sub, doship, state, ship',
 3:        {qty: 4, unit: 10, doship: true, state: 'Texas'}
 4:      )
 5:   .outputVariable( 'total' )
 6: 
 7:   .c( 'qty, unit, sub' )
 8:   .m( 'qty, unit -> sub', function( qty, unit ) { return qty * unit; } )
 9: 
10:   .c( 'state, qty, ship' )
11:   .m( 'state, qty -> ship', function( state, qty ) {
12:     var cost;
13:     switch (state) {
14:     case 'Texas':     cost = 1; break;
15:     case 'Oklahoma':  cost = 2; break;
16:     case 'Arkansas':  cost = 3; break;
17:     case 'Louisiana': cost = 2; break;
18:     }
19:     var p = new hd.Promise();
20:     setTimeout( function() { p.resolve( qty * cost ); }, 2000 );
21:     return p;
22:   } )
23: 
24:   .c( 'sub, doship, ship, total' )
25:   .m( 'sub, doship, *ship -> total', function f3( sub, doship, pShip ) {
26:     if (doship) {
27:       return pShip.then( function( ship ) {
28:         return sub + ship;
29:       } );
30:     }
31:     else {
32:       return sub;
33:     }
34:   } )
35: 
36:   .end();
37: 
38: var system = new hd.ConstraintSystem();
39: system.addComponent( model );

Binding

1: window.addEventListener( 'load', function() {
2:   hd.performDeclaredBindings( model, document.getElementById( 'ex4' ) );
3: } )

Result

Quantity:
Unit price:
Subtotal:

Shipping:

Total:

Notes

This example simulates a lengthy calculation: for example, determining shipping costs for an order. In this example, shipping cost is a simple function which we have deliberately delayed with a timer; in a more realistic scenario, calculating shipping costs may require a call to a server resulting in a noticeable delay.

Try editing the quantity. Notice how behavior changes when the shipping cost is not used: the system no longer waits for shipping cost before calculating the total. Furthermore, the drop-down box is disabled as it is no longer relevant.

5.2 Requesting input promises

In some cases you may want your method to wait on all but one or two input promises. In such cases, you can request that the function lifting mechanism pass to your function, not the value of the promise, but the promise itself. This allows you to decide if and when you will wait on this promise.

The full signature of liftFunction is as follows. Note that the last two parameters are optional.

liftFunction( fn, numOutputs, promiseMask )

We explained the first two parameters in Section 3. The final parameter, if provided, should be an array of Boolean values, one for each parameter, where true means to pass the corresponding parameter's promise directly, and false means to wait for the corresponding parameter's promise to resolve and pass its value.

However, as we mentioned earlier, there's really no reason to call liftFunction directly; you should simply use the method builder member function which calls liftFunction for you. To indicate to the model builder that you want to recieve a promise for a parameter, you place an asterisk ("*") in front of the parameter name in the method signature. You can see this in the method declared in line 25 of the view-model.

If you decide you need this parameter, you must use it's then method to schedule a function to be run when its value is ready. On line 27 of the view-model we schedule a function to calculate the total using the shipping cost as soon as it is ready. Remember that the return value for the then function is a promise for the return value of the function we scheduled. We return this as the promise for the output of our method.

Observe what happens if the checkbox for doship is unchecked: Since doship is false, we do not subscribe to the promise for ship. Since we do not subscribe to that promise, we do not have to wait for it to be ready. Thus, we can calculate the total cost before the shipping cost is even ready. This is convenient when the calculation for shipping cost is so slow.

5.3 Output variables

Typically the purpose of a user interface is to allow the user to enter values for variables that will be used elsewhere in the program — e.g., in the model. (If you don't know what that is, see Introduction to HotDrink for an explanation of MVVM.) We refer to these variables as output variables.

It is possible that a user interface contains some additional variables — variables which are not used elsewhere in the program, but are there to help the user calculate other variables which are. We refer to these variables as interface variables.

By default, HotDrink considers all variables as interface variables. If this is not the case, then you will need to explicitly tell HotDrink which ones are. You can change a single variable to an output variable using the outputVariable member function (abbreviated ov), or change a list of variables to output variables using the outputVariables member function (abbreviated ovs). Similarly, you can change a single variable to an interface variable using the outputVariable member function (abbreviated ov), or change a list of variables to interface variables using the outputVariables member function (abbreviated ovs).

The parameter for these functions is the name, or a comma-delimited list of the names, to be changed. For example:

builder.outputVariable( 'x' );

builder.interfaceVariables( 'x, y, z' );

As a convenience for you, these functions are overloaded so that, if there is no variable with the name given, they will create one first. Just as with the variable and variables builder functions, you may specify an initial value for the variables as well. Thus, on line 5 of the view-model we both create a new variable named "output" and also mark the variable as an output variable.

5.4 Contributing and relevant

There is another advantage to taking an input promise when you might not need its value: HotDrink can tell if a promise was never subscribed to, and can conclude that the corresponding input was not used. As mentioned previously, interface variables are not used elsewhere in the program; thus, their only possible use is in calculating output variables. HotDrink can check to see if these variables were, in fact, used this way.

An interface variable is said to be contributing if, during the last time the constraint system was updated, its value was used to calculate the value of at least one output variable. An interface variable is said to be relevant if editing it would have any effect on an output variable. Every contributing variable is automatically relevant; however, a non-contributing variable can still be relevant if editing it would cause it to become contributing.

Note that output variables are always both contributing and relevant.

5.5 Automatic disabling

Every variable has two Boolean, read-only properties named contributing and a relevant that report whether the variable is contributing and relevant, respectively. HotDrink sets these properties; however, it does not use these properties for anything. The recommended use is to bind to them in the interface to give useful information to the user.

In particular, the Enabled view adapter automatically disables a user-interface element when the corresponding value is false. In line 23 of the view source, we use this binder to automatically disable the state drop-down box whenever the corresponding variable is irrelevant.

Note that the hd.enabled factory function creates an hd.Enabled binding specification. Also, the hd.*Var factories (hd.editVar, hd.numVar, etc.) add a hd.nabled binding to their specifications. For more information on binding specifications, see the Advanced Binding Concepts document.

Let's walk through what happens when the "Ship to" checkbox is unchecked.

  • The doShip variable becomes false.
  • The method on line 25 of the view-model is evaluated, causing function f3 to be executed.
  • Function f3 does not subscribe to the promise for ship.
  • HotDrink notices that ship was not used, and is therefore non-contributing.
  • Not only that, the method of f3 is the only possible way that ship could affect an output variable — and f3 is currently not using ship.
  • HotDrink concludes that editing ship cannot affect an output variable, and is therefore irrelevant.
  • The bindEnabled binding disables the drop-down box.

Note the assumption that HotDrink makes: if a parameter is unused, then changing it cannot make it become used. That is certainly the case in function f3: if doship is false, then it doesn't matter what value you give ship — it's never going to be used. The only way ship can be used is if doship changes.

What if you have a parameter that you only use in certain cases? For example, you use it if it's a positive number, but not if it's a negative number. In that case, you must subscribe to the promise in order to see the number so that you can test whether it is positive or negative. Since you subscribe to the promise, HotDrink will consider it used. This is consistent with our definition of relevant: the parameter is relevant because changing it could potentially affect the output (i.e. if you changed it to a positive number).

5.6 Explicit control over usage

HotDrink's default behavior is to consider a variable used if its promise was subscribed to, and unused if it was not. For most cases, that is reasonable behavior. However, it is possible that you might want to subscribe to a parameter without committing to use its value, or to skip subscribing to a promise but still treat it as being used. In this case, you can explicitly set the usage of a parameter using the following two functions.

  • hd.markUsed( promise ) marks a promise as being used, even if it was never subscribed to.
  • hd.markUnused( promise ) marks a promise as being unused, even if it was subscribed to.

6 Helper: AJAX requests

6.1 The example

View

 1: <table id="ex5" style="text-align:right">
 2:   <style type="text/css" scoped>
 3:     .pending { background: #eee right center no-repeat url(spinner.gif); }
 4:     .noncontributing { background-color: #eee; }
 5:   </style>
 6:   <tr>
 7:     <td>Symbol:</td>
 8:     <td style="text-align:left">
 9:       <select data-bind="hd.value( symbol ), hd.cssClass( symbol )">
10:         <option value="FB">Facebook</option>
11:         <option value="ADBE">Adobe</option>
12:         <option value="MS">Microsoft</option>
13:         <option value="YHOO">Yahoo!</option>
14:       </select>
15:     </td>
16:   </tr><tr>
17:     <td>Value:</td>
18:     <td><input type="text" data-bind="hd.numVar( value, 2 )"/></td>
19:   </tr><tr>
20:     <td>Quantity:</td>
21:     <td><input type="text" data-bind="hd.numVar( quantity, 0 )"/></td>
22:   </tr><tr>
23:     <td>Total:</td>
24:     <td><input type="text" data-bind="hd.numVar( total, 2 )"/></td>
25:   </tr>
26: </table>

View-Model

 1: function fetch_stock_value( symbol ) {
 2:   var url = 'http://query.yahooapis.com/v1/public/yql';
 3:   var params = {
 4:     q: 'select * from yahoo.finance.quotes where symbol="' + symbol + '"',
 5:     format: 'json',
 6:     env: 'store://datatables.org/alltableswithkeys',
 7:     callback: ''
 8:   };
 9:   return hd.ajaxJSON( url, params ).then( function( data ) {
10:     return Number( data.query.results.quote.LastTradePriceOnly );
11:   } );
12: }
13: 
14: var model = new hd.ModelBuilder()
15:   .vs( 'symbol, value, quantity', {symbol: 'ADBE', quantity: 10} )
16:   .ov( 'total' )
17: 
18:   .c( 'symbol, value' )
19:   .m( 'symbol -> value', fetch_stock_value )
20:   .m( 'symbol -> symbol', function( x ) { return x; } )
21: 
22:   .eq( 'total == value * quantity' )
23: 
24:   .end();
25: 
26: var system = new hd.ConstraintSystem();
27: system.addComponent( model );

Binding

1: window.addEventListener( 'load', function() {
2:   hd.performDeclaredBindings( model, document.getElementById( 'ex5' ) );
3: } )

Result

Symbol:
Value:
Quantity:
Total:

Notes

This example uses a Yahoo! web service to look up the stock price for the given symbol. Selecting a new company results in a new stock price look up. Note that editing the stock price directly causes the drop-down list to be grayed out, indicating that it is not being used.

6.2 Simple AJAX wrappers

AJAX refers to the technique of using JavaScript to make an asynchronous HTTP request to another server. A full description of AJAX is beyond the scope of this document, but many such descriptions can be found online.

There are many JavaScript frameworks which simplify the process of making an AJAX request, allowing the programmer a great deal of control over how the request is made and how the results of the request are processed. HotDrink is not one of those frameworks. However, we would like to make it easy for you to experiment with asynchronous methods that make AJAX requests, so we have included a wrapper for simple AJAX requests. If you need more control over the request process, we recommend you look into other frameworks, or simply write your own wrapper.

HotDrink provides the following functions.

  1. ajax( url, params )

    This function makes an asynchronous HTTP GET request to the provided URL. If provided, the params should be an object used as a map of parameters to be included in the request; these will be added to the query string following the GET request protocol.

    The return value of this function is a promise which will be resolved with the XMLHttpRequest object once the request has been successfully completed.

  2. ajaxXML( url, params )

    This function is identical to ajax, except that the returned promise will be resolved with the contents of the request as an XML DOM (i.e., the responseXML property of the XMLHttpRequest object).

  3. ajaxText( url, params )

    This function is identical to ajax, except that the returned promise will be resolved with the contents of the request as a string (i.e. the responseText property of the XMLHttpRequest object).

  4. ajaxJSON( url, params )

    This function is identical to ajax, except that the returned promise will be resolved with an object that is the result of parsing the contents of the request as JSON.

7 Helper: web workers

7.1 The example

View

 1: <div id="ex6">
 2:   <style type="text/css" scoped>
 3:     .pending { background: #eee right center no-repeat url(spinner.gif); }
 4:     .error { color: #900; }
 5:   </style>
 6:   <table style="text-align:right">
 7:     <tr>
 8:       <td>Start position:</td>
 9:       <td><input type="text" data-bind="hd.numVar( start )"/></td>
10:       <td class="error" data-bind="hd.text( start.error )"></td>
11:     </tr><tr>
12:       <td>End position:</td>
13:       <td><input type="text" data-bind="hd.numVar( end )"/></td>
14:       <td class="error" data-bind="hd.text( end.error )"></td>
15:     </tr><tr>
16:       <td>Length:</td>
17:       <td><input type="text" data-bind="hd.numVar( length )"/></td>
18:       <td class="error" data-bind="hd.text( length.error )"></td>
19:     </tr>
20:   </table>
21: </div>

View-Model

 1: var model = new hd.ModelBuilder()
 2:   .vs( 'start, end, length', {start: 10, end: 25} )
 3: 
 4:   .c( 'start, end, length' )
 5:   .m( 'start, length -> end', hd.worker( 'worker.js', 'slowSum' ) )
 6:   .m( 'end, length -> start', hd.worker( 'worker.js', 'slowDiff' ) )
 7:   .m( 'end, start -> length', hd.worker( 'worker.js', 'slowDiff' ) )
 8: 
 9:   .end();
10: 
11: var system = new hd.ConstraintSystem();
12: system.addComponent( model );

Binding

1: window.addEventListener( 'load', function() {
2:   hd.performDeclaredBindings( model, document.getElementById( 'ex6' ) );
3: } )

Worker (file: worker.js)

 1: importScripts( 'fn-worker.js' );
 2: 
 3: function slowSum( a, b ) {
 4:   var n;
 5:   if (b < 0) {
 6:     n = -1;
 7:     b = -b;
 8:   }
 9:   else {
10:     n = 1;
11:   }
12:   for (var i = 0; i < b; ++i, ++n) {
13:     a += n - i;
14:   }
15:   return a;
16: }
17: 
18: function slowDiff( a, b ) {
19:   return slowSum( a, -b );
20: }

Result

Start position:
End position:
Length:

Notes

This example uses an extremely poor algorithm for addition which is very slow for large numbers. (Try entering "1e9" for all values.) Yet, because the slow process is performed on a web worker with an asynchronous method, it can never cause the interface to become unresponsive.

7.2 Simple web worker wrapper

A web worker is a thread created by the web browser which can run JavaScript code. A web worker runs in parallel with the user-interface thread, meaning it won't block the user interface, making it unresponsive. As with AJAX, a full description of web workers is beyond the scope of this document, and readers are encouraged to look online for more information.

And, just as HotDrink is not a full-featured AJAX framework, neither is it a full-featured web worker framework. However, we would like to make it easy for you to experiment with asynchronous methods run in web workers, so we have included a wrapper for running functions on web workers. If you need more control over the process, we recommend you look into other frameworks, or simply write your own wrapper.

To use HotDrink's wrapper requires two steps.

  1. Place the JavaScript functions to be executed by the worker in a separate file.

    As you can see on line 1 of the worker source, the first line of this separate file should be this:

    importScripts( 'fn-worker.js' );
    

    This line causes the JavaScript code in the file fn-worker.js to be included in your web worker. This file is included with HotDrink, and provides the framework necessary for communication with the main thread.

    Remember that the functions in your web worker will not have access to the user interface, nor to any libraries being used in the main thread. If you want to use other JavaScript libraries, you will need to import them using the importScript function.

  2. Use hd.worker to create a wrapper which will invoke the worker

    The function hd.worker takes two string parameters: the URL for the file containing the worker code, and the name of the function. Note that the URL may be relative to your main file.

    The hd.worker function returns a new function which behaves as follows: when called, it initiates a web worker to execute your function, passes the parameters it received to the web worker, then returns a promise for the result. When the function on the web worker completes, its return value is passed back to the main thread and then used to resolve the output promise.

    This function is just right for use as a method; you can see it used as a method on line 5 of the view-model source.

7.3 Security restrictions

There are several security restrictions where the worker file must be located. Again, a full description of web worker security is beyond the scope of this document; in brief, the worker code must come from the same origin as your main file.

Note, however, that most web browsers do not, by default, allow code from web workers to come directly from the file system; thus, web workers require a web server to work.

7.4 Worker pools

We could create a new web worker for every method call; however, that would introduce a lot of overhead. Not only that, most browsers place a limit on the number of web workers that can be running at one time. At the other extreme, we could create a single web worker and use it for every method call; however, that requires waiting for one function to finish before initiating the next.

The typical compromise to this dilemma is to create a pool of workers. When a new method needs to be executed, we attempt to find an unused worker in the pool. If we find one, we use it to execute the method; if we cannot find one, we wait until one becomes available. This is the approach HotDrink takes.

HotDrink keeps a separate pool for each worker source file. (So, in our example, there is a single pool for the "worker.js" source file.) Each pool has a maximum number of allowable workers. HotDrink creates a new worker only when there are none available in the pool, and the pool has not yet reached is maximum size.

The default maximum size for a worker pool is twenty — that number seems to work well with the browsers we tested on. If you decide you would like to change the maximum for a pool, you can use the hd.setMaxWorkers function to do so. This function takes two parameters: the source file for the pool to modify, and the value for the new maximum.

In the example for this section, we could set the maximum number of web workers spawned to three like so:

hd.setMaxWorkers( 'worker.js', 3 )

7.5 Killing workers

Sometimes the output of a method becomes irrelevant before it has even been produced. For example, suppose a method was scheduled to calculate the value for a variable, but before it could complete, the user edited that variable directly. Now the output of the method no longer matters: it has been replaced with the user's edit.

HotDrink can detect when these situations occur and react to them. We will save the full details of how this works for another document, but for now we want to point out that, if the output of a method running on a web worker becomes irrelevant, HotDrink's worker wrapper will kill the worker.

This behavior is necessary to ensure that the user interface will never become unresponsive: if we did not do this, then it would be possible for the worker pool to become exhausted with web workers which were not returning but whose output are no longer needed.

However, to suddenly kill a thread with no warning is generally considered to be unsafe: the thread may have been in the middle of doing something important and need to clean up before exiting. If this is the case for your thread, then you should not use the web worker wrapper provided with HotDrink; you should write your own instead. To do this, check out the Programming Guide.

Created: 2015-10-29 Thu 10:02