Or maybe this post should have been titled: “Taming asynchronous RPC”.
Programming with asynchronous I/O is tricky. The proper solution IMO, is to have full coroutine or generator support in the browser and in the server. This has been in the works from some time now (see ES6 Harmony). In short: coroutines and generators can “yield”, which means something that takes time can suspend the current stack state until it gets resumed. A bit like green (non-preemptive) threads, and with very little overhead.
But we ain’t there yet, and waiting for all the major browsers to reach that point might be a bit of a stretch (see the “Generators (yield)” entry on this page). The latest Node.js v0.11.7 development release does seem to support it, but that’s only half the story.
So promises it is for now. On the server side, this is available via Kris Kowal’s q package. On the client side AngularJS includes promises as $q
.
And as mentioned before, I’m also using q-connection as promise-based Remote Procedure Call RPC mechanism.
The result is really neat, IMO. First, RPC from the client, calling a function on the server. This is app/admin/client.coffee
for the client side:
ng = angular.module 'myApp' ng.config ($stateProvider) -> $stateProvider.state 'admin', url: '/admin' templateUrl: 'admin/view.html' controller: 'AdminCtrl' ng.controller 'AdminCtrl', ($scope, host) -> $scope.hello = host 'admin_dbinfo'
This Angular boilerplate defines an Admin page with app/admin/view.jade
template:
h3 Storage use table tr th Group th Size tr(ng-repeat='(group,size) in hello') td {{group}} td(align='right') ≈ {{size}} b
The key is this one line:
$scope.hello = host 'admin_dbinfo'
That’s all there is to doing RPC with a round-trip over the network. And here’s the result, as shown in the browser (formatted with the Zurb Foundation CSS framework):
There’s more to it than meets the eye, but it’s all nicely tucked away: the host
call does the request, and returns a promise. Angular knows how to deal with promises, so it will fill in the “hello” field when the reply arrives, asynchronously. IOW, the above code looks like synchronous code, but does lots of things at another point in time than you might think.
On the server, this is the app/admin/host.coffee
code:
Q = require 'q' module.exports = (app, plugin) -> app.on 'setup', -> app.host.admin_dbinfo = -> q = Q.defer() app.db.getPrefixDetails q.resolve q.promise
It’s slightly more involved because promises are more exposed. First, the RPC call is defined on application setup. When called, it creates a new promise, calls getPrefixDetails
with a callback, and immediately returns this promise to fill in the result later.
At some point, getPrefixDetails
will call the q.resolve
callback with the result as argument, and the promise becomes fulfilled. The reply is then sent to the client by the q-connection package.
Note that there are three asynchronous calls involved in total:
rpc call -> network send -> database action -> network reply -> show
============ =============== =============
Each of the underlined steps is asynchronous and based on callbacks. Yet the only step we had to deal with explicitly on the server was that custom database action.
Tomorrow, I’ll show RPC in the other direction, i.e. the server calling client code.