michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #ifndef MERGED_COMPARTMENT michael@0: michael@0: this.EXPORTED_SYMBOLS = ["Async"]; michael@0: michael@0: const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; michael@0: michael@0: #endif michael@0: michael@0: // Constants for makeSyncCallback, waitForSyncCallback. michael@0: const CB_READY = {}; michael@0: const CB_COMPLETE = {}; michael@0: const CB_FAIL = {}; michael@0: michael@0: const REASON_ERROR = Ci.mozIStorageStatementCallback.REASON_ERROR; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: /* michael@0: * Helpers for various async operations. michael@0: */ michael@0: this.Async = { michael@0: michael@0: /** michael@0: * Execute an arbitrary number of asynchronous functions one after the michael@0: * other, passing the callback arguments on to the next one. All functions michael@0: * must take a callback function as their last argument. The 'this' object michael@0: * will be whatever chain()'s is. michael@0: * michael@0: * @usage this._chain = Async.chain; michael@0: * this._chain(this.foo, this.bar, this.baz)(args, for, foo) michael@0: * michael@0: * This is equivalent to: michael@0: * michael@0: * let self = this; michael@0: * self.foo(args, for, foo, function (bars, args) { michael@0: * self.bar(bars, args, function (baz, params) { michael@0: * self.baz(baz, params); michael@0: * }); michael@0: * }); michael@0: */ michael@0: chain: function chain() { michael@0: let funcs = Array.slice(arguments); michael@0: let thisObj = this; michael@0: return function callback() { michael@0: if (funcs.length) { michael@0: let args = Array.slice(arguments).concat(callback); michael@0: let f = funcs.shift(); michael@0: f.apply(thisObj, args); michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Helpers for making asynchronous calls within a synchronous API possible. michael@0: * michael@0: * If you value your sanity, do not look closely at the following functions. michael@0: */ michael@0: michael@0: /** michael@0: * Create a sync callback that remembers state, in particular whether it has michael@0: * been called. michael@0: */ michael@0: makeSyncCallback: function makeSyncCallback() { michael@0: // The main callback remembers the value it was passed, and that it got data. michael@0: let onComplete = function onComplete(data) { michael@0: onComplete.state = CB_COMPLETE; michael@0: onComplete.value = data; michael@0: }; michael@0: michael@0: // Initialize private callback data in preparation for being called. michael@0: onComplete.state = CB_READY; michael@0: onComplete.value = null; michael@0: michael@0: // Allow an alternate callback to trigger an exception to be thrown. michael@0: onComplete.throw = function onComplete_throw(data) { michael@0: onComplete.state = CB_FAIL; michael@0: onComplete.value = data; michael@0: michael@0: // Cause the caller to get an exception and stop execution. michael@0: throw data; michael@0: }; michael@0: michael@0: return onComplete; michael@0: }, michael@0: michael@0: /** michael@0: * Wait for a sync callback to finish. michael@0: */ michael@0: waitForSyncCallback: function waitForSyncCallback(callback) { michael@0: // Grab the current thread so we can make it give up priority. michael@0: let thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread; michael@0: michael@0: // Keep waiting until our callback is triggered (unless the app is quitting). michael@0: while (Async.checkAppReady() && callback.state == CB_READY) { michael@0: thread.processNextEvent(true); michael@0: } michael@0: michael@0: // Reset the state of the callback to prepare for another call. michael@0: let state = callback.state; michael@0: callback.state = CB_READY; michael@0: michael@0: // Throw the value the callback decided to fail with. michael@0: if (state == CB_FAIL) { michael@0: throw callback.value; michael@0: } michael@0: michael@0: // Return the value passed to the callback. michael@0: return callback.value; michael@0: }, michael@0: michael@0: /** michael@0: * Check if the app is still ready (not quitting). michael@0: */ michael@0: checkAppReady: function checkAppReady() { michael@0: // Watch for app-quit notification to stop any sync calls michael@0: Services.obs.addObserver(function onQuitApplication() { michael@0: Services.obs.removeObserver(onQuitApplication, "quit-application"); michael@0: Async.checkAppReady = function() { michael@0: throw Components.Exception("App. Quitting", Cr.NS_ERROR_ABORT); michael@0: }; michael@0: }, "quit-application", false); michael@0: // In the common case, checkAppReady just returns true michael@0: return (Async.checkAppReady = function() { return true; })(); michael@0: }, michael@0: michael@0: /** michael@0: * Return the two things you need to make an asynchronous call synchronous michael@0: * by spinning the event loop. michael@0: */ michael@0: makeSpinningCallback: function makeSpinningCallback() { michael@0: let cb = Async.makeSyncCallback(); michael@0: function callback(error, ret) { michael@0: if (error) michael@0: cb.throw(error); michael@0: cb(ret); michael@0: } michael@0: callback.wait = function() Async.waitForSyncCallback(cb); michael@0: return callback; michael@0: }, michael@0: michael@0: // Prototype for mozIStorageCallback, used in querySpinningly. michael@0: // This allows us to define the handle* functions just once rather michael@0: // than on every querySpinningly invocation. michael@0: _storageCallbackPrototype: { michael@0: results: null, michael@0: michael@0: // These are set by queryAsync. michael@0: names: null, michael@0: syncCb: null, michael@0: michael@0: handleResult: function handleResult(results) { michael@0: if (!this.names) { michael@0: return; michael@0: } michael@0: if (!this.results) { michael@0: this.results = []; michael@0: } michael@0: let row; michael@0: while ((row = results.getNextRow()) != null) { michael@0: let item = {}; michael@0: for each (let name in this.names) { michael@0: item[name] = row.getResultByName(name); michael@0: } michael@0: this.results.push(item); michael@0: } michael@0: }, michael@0: handleError: function handleError(error) { michael@0: this.syncCb.throw(error); michael@0: }, michael@0: handleCompletion: function handleCompletion(reason) { michael@0: michael@0: // If we got an error, handleError will also have been called, so don't michael@0: // call the callback! We never cancel statements, so we don't need to michael@0: // address that quandary. michael@0: if (reason == REASON_ERROR) michael@0: return; michael@0: michael@0: // If we were called with column names but didn't find any results, michael@0: // the calling code probably still expects an array as a return value. michael@0: if (this.names && !this.results) { michael@0: this.results = []; michael@0: } michael@0: this.syncCb(this.results); michael@0: } michael@0: }, michael@0: michael@0: querySpinningly: function querySpinningly(query, names) { michael@0: // 'Synchronously' asyncExecute, fetching all results by name. michael@0: let storageCallback = {names: names, michael@0: syncCb: Async.makeSyncCallback()}; michael@0: storageCallback.__proto__ = Async._storageCallbackPrototype; michael@0: query.executeAsync(storageCallback); michael@0: return Async.waitForSyncCallback(storageCallback.syncCb); michael@0: }, michael@0: };