Promises

Promise APIs for Common Asynchronous Operations

Due to the performance and stability costs of synchronous IO, many APIs which rely on it have been deprecated. The following page contains examples of many Promise-based replacement APIs for common operations. These APIs allow asynchronous operation to be achieved with a coding style similar to synchronous variants.

The following examples make use of the Task API, which harnesses generator functions to remove some of the syntactic clutter of raw Promises, such that asynchronous promise code more closely resembles synchronous, procedural code. A Task example like the following:

Components.utils.import("resource://gre/modules/Task.jsm");

Task.spawn(function* () {
    var response = yield Request("login", { username: user, password: password });
    if (response.messages) {
        try {
            yield Publish({ username: user, messages: response.messages });
        }
        catch (e) {
            self.reportError("Publication failed", e);
        }
    }
});

Can be converted to a pure Promise-based equivalent as such:

Request("login", { username: user, password: password })
    .then(response => {
        if (response.messages)
            return Publish({ username: user, messages: response.messages });
    })
    .then(null, (e) => {
        self.reportError("Publication failed", e);
    });

File IO

File IO in add-ons should be done via the OS.File API, which provides a simple, but powerful, interface for reading, writing, and manipulating both text and binary files. It is also available for use off-main-thread in Workers as a synchronous API.

This interface replaces the previous, complicated XPCOM nsIFile and streams APIs, and their related JavaScript helper modules. These older interfaces should be avoided, even in their asynchronous forms, due to their performance penalties and needless complexity.

Representative Example Usage

Components.utils.import("resource://gre/modules/osfile.jsm");

Task.spawn(function* () {
    // Retrieve file metadata to check modification time.
    let info = yield OS.File.stat(configPath);

    if (info.lastModificationDate <= timestamp)
        return;
    timestamp = info.lastModificationDate;

    // Read the file as a UTF-8 string, parse as JSON.
    let config = JSON.parse(
        yield OS.File.read(configPath, { encoding: "utf-8" }));

    let files = [];

    // Get the directory contents from a list of directories.
    for (let dir of config.directories) {
        // Iterate over the contents of the directory.
        let iter = new OS.File.DirectoryIterator(dir);
        yield iter.forEach(entry => {
            if (!entry.isDir)
                files.push(entry.path);
        });
        iter.close();
    }

    // Read the files as binary blobs and process them.
    let processor = new FileProcessor();
    for (let file of files) {
        let data = yield OS.File.read(file);
        processor.add(data);
    }

    // Now write the processed files back out, as a binary blob.
    yield OS.File.writeAtomic(config.processedPath,
                              processor.process(),
                              { tmpPath: config.processedPath + "." + Math.random() });

    // And write out a new config file.
    config.indexStats = processor.stats;

    yield OS.File.writeAtomic(configPath, JSON.stringify(config),
                              { tmpPath: configPath + "." + Math.random(),
                                encoding: "UTF-8" })
    timestamp = new Date;
});

HTTP Requests

HTTP requests should, in nearly all circumstances, be made via the standard XMLHttpRequest API. While this API does not have direct support for promises, its standard usage is very easy to adapt to a promise-based approach. Moreover, many third-party wrappers for the XMLHttpRequest API now support Promises out of the box.

Example Direct Usage

Task.spawn(function* () {
    // Make the initial request.
    let resp = yield new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest;
        xhr.onload = resolve;
        xhr.onerror = reject;
        xhr.open("GET", dataURL);
        xhr.responseType = "json";
        xhr.send();
    });

    let data = resp.target.response;

    // Use the response to construct form data object for the
    // second request.
    let form = new FormData;
    form.append("id", data.id);
    form.append("content", data.content);

    // Make the second request.
    resp = yield new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest;
        xhr.onload = resolve;
        xhr.onerror = reject;
        xhr.open("POST", updateURL);
        xhr.send(form);
    });

    // Use the response of the second request.
    notifyUser(resp.target.responseText);
});

Example Using Promise-Based Helper

The following example relies on the helper function defined below.

Task.spawn(function* () {
    // Make the initial request.
    let xhr = yield Request(dataURL, { responseType: "json" });

    let data = xhr.response;

    // Use the response to construct form data object for the
    // second request.
    let form = new FormData;
    form.append("id", data.id);
    form.append("content", data.content);

    // Make the second request.
    xhr = yield Request(updateURL, { data: form });

    // Use the response of the second request.
    notifyUser(xhr.responseText);
});

Downloading Remote Files

Nearly all previous methods of downloading remote files have been superseded by the much simpler Downloads.jsm module. The Downloads object provides a Promise-based API for downloading remote files, with full support for progress tracking, pause and resume, and, optionally, integration with the download manager UI.

Representative Example Usage

Components.utils.import("resource://gre/modules/Downloads.jsm");
Task.spawn(function* () {
    // Fetch a file in the background.
    let download_1 = Downloads.fetch(URL_1, PATH_1);

    // Fetch a file visible in the download manager.
    let download_2 = yield Downloads.createDownload({
        source: URL_2,
        target: PATH_2,
    });

    // Add it to the downloads list used by the download manager UI.
    let list = yield Downloads.getList(Downloads.ALL);
    list.add(download_2);

    // Start the second download, and wait for both
    // downloads to complete.
    // This will raise an error if either download fails.
    yield Promise.all([download_1,
                       download_2.start()]);

    // Do something with the saved files.
    doStuffWith(PATH_1, PATH_2);
});

SQLite

First, it’s important to note that SQLite should be avoided in favor of simpler solutions, such as flat JSON files, under most circumstances. The IO, memory, and CPU overhead added by SQLite is substantial, and in most cases outweighs the cost of dealing with flat files directly.

For use cases which are not easily served by other options, or for legacy code which cannot easily be upgraded to non-relational models, the Sqlite.jsm module provides a clean, Promise-based interface to SQLite databases.

Representative Example Usage

Components.utils.import("resource://gre/modules/Sqlite.jsm");

Task.spawn(function* () {
    // Open the connection.
    let db = yield Sqlite.openConnection({ path: DATABASE_PATH });

    try {
        // Start a transaction to insert the data.
        yield db.executeTransaction(function* () {
            for (let node of nodes)
                // Insert the node's data, using an automatically-cached,
                // pre-compiled statement, and parameter placeholders.
                yield db.executeCached(
                    "INSERT INTO nodes (id, owner, key, value) \
                     VALUES (:id, :owner, :key, :value);",
                    { params: { id: node.id,
                                owner: node.owner.id,
                                key: node.key,
                                value: node.value }});
        });

        // Perform a bulk update.
        yield db.execute(
            "UPDATE owners, nodes \
             SET owners.name = nodes.name \
             WHERE owners.id = nodes.owner AND nodes.key = 'name';");

        // Process some results.
        yield db.execute(
            "SELECT owners.group AS group, nodes.value AS task \
             FROM nodes \
             INNER JOIN owners ON owner.id = nodes.owner \
             WHERE nodes.key = 'task';",
            {
                onRow: row => {
                    runTask(row.getResultByName("task"),
                            row.getResultByName("group"));
                }
            });

        // And quickly grab a single row value.
        let [row] = yield db.execute(
            "SELECT value FROM nodes WHERE key = 'timestamp' \
             ORDER BY value DESC LIMIT 1");

        latestTimestamp = row.getResultByIndex(0);
    }
    finally {
        // Make sure to close the database when finished.
        // Failure to do this will prevent Firefox from shutting down
        // cleanly.
        yield db.close();
    }
});

Promise Wrappers and Helpers

The following are some example Promise-based wrappers for common callback-based asynchronous APIs.

AddonManager

var AOM = {
    __proto__: AddonManager,

    Addon: function Addon(addon) {
        if (!(addon && "getDataDirectory" in addon))
            return addon;

        return {
            __proto__: addon,

            getDataDirectory: function getDataDirectory() {
                return new Promise((accept, reject) => {
                    return addon.getDataDirectory((directory, error) => {
                        if (error)
                            reject(error);
                        else
                            accept(directory);
                    });
                });
            },
        };
    },

    getInstallForURL: function getInstallForURL(url, mimetype, hash, name,
                                                iconURL, version, loadGroup) {
        return new Promise(accept =>
            this.AddonManager.getInstallForURL(url, accept, mimetype, hash,
                                               iconURL, version, loadGroup));
    },

    getInstallForFile: function getInstallForFile(url, mimetype) {
        return new Promise(accept =>
            this.AddonManager.getInstallForFile(url, accept, mimetype));
    },

    getAllInstalls: function getAllInstalls() {
        return new Promise(accept => this.AddonManager.getAllInstalls(accept));
    },

    _replaceMethod: function replaceMethod(method, callback) {
        Object.defineProperty(this, method, {
            enumerable: true, configurable: true,
            value: key => {
                return new Promise(accept =>
                    this.AddonManager[method](key,
                                              addon => accept(callback(addon))));
            }
        });
    },
};

for (let method of ["getAddonByID",
                    "getAddonBySyncGUID"])
    AOM._replaceMethod(method, addon => AOM.Addon(addon));

for (let method of ["getAllAddons",
                    "getAddonsByIDs",
                    "getAddonsByTypes",
                    "getAddonsWithOperationsByTypes"])
    AOM._replaceMethod(method, addons => addons.map(AOM.Addon));

AOM._replaceMethod("getInstallsByTypes", installs => installs);

Components.utils.import("resource://gre/modules/AddonManager.jsm", AOM);

Example usage:

Task.spawn(function* () {
    // Get an extension instance, and its data directory.
    let addon = yield AOM.getAddonByID(ADDON_ID);
    let path = yield addon.getDataDirectory();

    writer.writeDataTo(path);

    // Disable all extensions.
    for (let extension of yield AOM.getAddonsByTypes(["extension"]))
        extension.userDisabled = true;
});

JSON File Storage

This helper simplifies the use of JSON data storage files with asynchronous IO. The JSONStore object must be instantiated with the base name of the storage file and, optionally, a JSON-compatible object to be used if the file does not yet exist. The constructor returns a Promise which resolves when the file’s contents have been loaded.

The contents of the file will be initially loaded into the JSON store’s data property. After any changes are made to this property, the save() method must be called to queue the changes to be written to disk. Unless the flush() method is called, no writes will happen until a full second has elapsed between save() calls.

The variable ADDON_ID must be defined to the ID of the add-on the code is being used in.

This code makes use of the Add-on Manager helper defined above, though it can be adapted to work without it.

Components.utils.import("resource://gre/modules/DeferredSave.jsm");

/**
 * Handles the asynchronous reading and writing of add-on-specific JSON
 * data files.
 *
 * @param {string} The basename of the file.
 * @param {object} A JSON-compatible object which will be used in place
 *      of the file's data, if the file does not already exist.
 *      @optional
 *
 * @return {Promise<JSONStore>}
 */
function JSONStore(name, default_={}) {
    return Task.spawn(function* () {
        // Determine the correct path for the file.
        let addon = yield AOM.getAddonByID(ADDON_ID);
        let dir = yield addon.getDataDirectory();
        this.path = OS.Path.join(dir, name + ".json");

        // Read the file's contents, or fall back to defaults.
        try {
            this.data = JSON.parse(
                yield OS.File.read(this.path,
                                   { encoding: "utf-8" }));
        }
        catch (e if e.becauseNoSuchFile) {
            this.data = JSON.parse(JSON.stringify(default_));
        }

        // Create a saver to write our JSON-stringified data to our
        // path, at 1000ms minimum intervals.
        this.saver = new DeferredSave(this.path,
                                      () => JSON.stringify(this.data),
                                      1000);

        return this;
    }.bind(this));
}
/**
 * Immediately save the data to disk.
 *
 * @return {Promise} A promise which resolves when the file's contents
 * have been written.
 */
JSONStore.prototype.flush = function () {
    return this.saver.flush();
};
/**
 * Queue a save operation. The operation will commence after a full
 * second has passed without further calls to this method.
 *
 * @return {Promise} A promise which resolves when the file's contents
 * have been written.
 */
JSONStore.prototype.save = function () {
    return this.saver.saveChanges();
};

Example usage:

var ADDON_ID = "extension@example.com";

var CONFIG_DEFAULT = {
    "foo": "bar",
};

new JSONStore("config", CONFIG_DEFAULT).then(store => {
    console.log(store.data);

    store.data.baz = "quux";
    store.save();
})

The following changes will remove the dependency on AOM wrappers:

        let addon = yield new Promise(accept =>
            AddonManager.getAddonByID(ADDON_ID, accept));

        let dir = yield new Promise(accept =>
            addon.getDataDirectory(accept));

XMLHttpRequest

function Request(url, options) {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest;
        xhr.onload = event => resolve(event.target);
        xhr.onerror = reject;

        let defaultMethod = options.data ? "POST" : "GET";

        if (options.mimeType)
            xhr.overrideMimeType(params.options);

        xhr.open(options.method || defaultMethod, url);

        if (options.responseType)
            xhr.responseType = options.responseType;

        for (let header of Object.keys(options.headers || {}))
            xhr.setRequestHeader(header, options.headers[header]);

        let data = options.data;
        if (data && Object.getPrototypeOf(data).constructor.name == "Object") {
            options.data = new FormData;
            for (let key of Object.keys(data))
                options.data.append(key, data[key]);
        }

        xhr.send(options.data);
    });
}

Example usage:

Task.spawn(function* () {
    let request = yield Request("http://example.com/", {
        method: "PUT",
        mimeType: "application/json",
        headers: {
            "X-Species": "Hobbit"
        },
        data: {
            foo: new File(path),
            thing: "stuff"
        },
        responseType: "json"
    });

    console.log(request.response["json-key"]);
});

Document Tags and Contributors

 Contributors to this page: CAAP, wbamberg, sooraj_v, kmaglione, kscarfone
 Last updated by: CAAP,