30 August 2010 - 15:31Synchronous and Asynchronous APIs
In designing the Firefox-facing APIs of Account Manager, thunder [sandmill.org] and I decided to make most of the interfaces take a callback/continuation. Two main reasons were 1) to allow add-ons to add extra account types like OpenID and 2) to find out if you’re logged-in to a site over multiple network requests.
For an add-on to provide a new account type, it needs to be able to tell Firefox what saved accounts are available and how to connect to them. These might need to read data from disk and/or network; or perhaps request more information from the user like a 1-time code sent over SMS. For Firefox to support these more-complex interactions, it can’t assume that a method implemented by the add-on can return a value immediately.
In this particular mockup, the site has told Firefox not to use the basic username-password account type but instead to use an account type that asks for more information. Here, an add-on has already saved the username/password and doesn’t to ask for them, but it can’t immediately connect without asking for more information, so it tells Firefox to show this popup. With the async. API, the add-on can later tell Firefox that it has finished the connect process.
The second reason for going async. overlaps with the first of supporting add-ons; Account Manager and account types need to talk over the network to other machines. These requests can take more than just a few milliseconds, so blocking the rest of Firefox when you click on the “Sign in” button using a synchronous request would be bad. In the case of finding the account status, Firefox might need to make requests for any or all of host-meta, AMCD and the session status.
The Password Manager API in Firefox happens to be synchronous—as are many other interfaces in Firefox. Converting it to be asynchronous for use in the username/password account type was fairly simple, but there’s a couple things to keep in mind: when the work is done and when the result is given.
Here, we’ve made the call to savedAccounts immediately do the work of finding the logins, so this means the caller might have to wait before the asynchronous savedAccounts call returns. We could have just as easily delayed this work to run after the function returns by moving the logic into the async function call, but one needs to be aware that if the arguments are live objects, the contents might change by the time async triggers the callback.
The second point of “when the result is given” is less flexible. The function above could have been written to just call onComplete(accounts) immediately without the async wrapper, but that could break the caller as the implementation is no longer truly asynchronous. The caller would stop working if code is supposed to execute after the call to savedAccounts but before the call to continuation, onComplete.
For web developers, implementing async is pretty simple as the global window object has setTimeout. So one implementation could look like function async(callback) setTimeout(callback, 0);
Making the choice of having the APIs be asynchronous does have some drawbacks in terms of code structure. Any function that eventually calls an async. function will be forced to take a callback as well, and if it needs to call multiple async. functions, the logic needs to be broken up into multiple callbacks. If you want to share variables across these callbacks, you’ll end up creating many nested anonymous functions (closures).
Additionally, some simple things like doing Array.reduce (fold), which visits each array item and applies a function to produce a single result, can get pretty complicated if you want to pass in an async. function. In a later post, I’ll describe a way to have asynchronous functions look like they’re synchronous so that you can avoid some of this callback craziness.