Let's use promises

Dmitry Pashkevich

Agenda

  • Intro
  • Problems solved
  • Usage ideas
  • Grey areas

Intro

Promise

  • A value in the future
  • Can be resolved or rejected (once)
  • Can subscribe to an already fulfilled promise

Example: load user info

// Load user information

$.get({
    url: userUrl,
    success: function(user) {
        $('.user-indicator').text(user.name);
    },
    error: function(xhr, reason) {
        growl("Something went wrong", reason);
    }
});
// Load user information

var promise = $.get({
    url: userUrl
});

promise.done(function(user) {
    $('.user-indicator').text(user.name);
}).fail(function(xhr, reason) {
    growl('Something went wrong', reason);
});

Problems with callback arguments

Problem #1

Where do I pass the callback?

/** WITHOUT PROMISES **/

// is there an error callback?
lucid.EditorPlugin.prototype.addHotKey = function(keys, callback, context) {}

// what if I don't care to handle failure here?
lucid.EditorClient.prototype.publish = function(format, pageId, selectArea, DPI,
    successCallback, failureCallback) {}

lucid.net.LinkHelper.generateShareLink = function(id, isDocument, multipleUse, 
    role, callback, folderOwnerId) {}
// Three callbacks!
lucid.view.renderData.renderToContext = function(c, renderData, bb, doCrop, gradientFactor, 
    imageMissingCallback, imageLoadedCallback, imageErrorCallback, uniqueId, isBlock, 
    outlineColors, im) {}

Problem #1

Where do I pass the callback?

/** WITH PROMISES **/

functionToGetUserIHaveNeverSeen(userId).done(function(user) {
    console.log("Yay!", user);
});

Problem #2

Need to add an error callback

/** WITHOUT PROMISES **/

function getUser(userId, callback, useCache) {}
/** WITH PROMISES **/

function getUser(userId, useCache) {
    ...

    // added later
    if(!ok) {
        deferred.reject("Bummer!");
    }
    // /added later

    return deferred.promise();
}

// crap, now I need to add an error callback!
function getUser(userId, success_callback, 
    error_callback, useCache) {}

// change all the places where this is used
// or have an ugly signature

Problem #3a

// Load the document fonts
getDocumentFonts(function(documentFamilies) {
    documentFontsLoaded = true;
    if(defaultFontsLoaded) {
        afterAllFontsLoaded();
    }
});

// Load the default fonts
getDefaultFonts(function(publicFamilies) {
   defaultFontsLoaded = true;
   if(documentFontsLoaded) {
        afterAllFontsLoaded();
   }
});

function afterAllFontsLoaded() {
    // If there are open range requests, wait until they are done
    // before refreshing the font lists and doing the callback
    waitForRangeRequests(function() {
        lucid.listen.set('optionBar.refresh');
        lucid.listen.set('managefonts.refresh');
    });
}
var docFontsPromise = getDocumentFonts();
var defaultFontsPromise = getDefaultFonts();

Compose async operations

// Load the document fonts
function getDocumentFonts(callback) {};

// Load the default fonts
function getDefaultFonts(callback) {};

/**
 * Problem: refresh the UI 
 * after all fonts are loaded
 */
// When everything has been completed
$.when(docFontsPromise, defaultFontsPromise)
    // waitForRangeRequests returns promise too
    .done(waitForRangeRequests)
    .done(function() {
        lucid.listen.set('optionBar.refresh');
        lucid.listen.set('managefonts.refresh');
    });

Problem #3b

function getUserPic() {
    // what if getUser becomes async?
    var user = this.getUser();

    return $.get(user.picUrl);
}

Compose async operations

function getUserPic() {
    return this.getUser().done(function(user) {
        // chain promises
        return $.get(user.url); 
    };
    // returned promise resolved after the user
    // is fetched AND the picture is retrieved
}
  • Outside code doesn't change
  • Don't have to handle getUser() failure

Problem #3c

function refreshVersions() {
    $.get(versionsListUrl).done(function(result) {
        var allImagesReady = [];    // will save promises here
        container.empty();

        result.forEach(function(version) {
            var img = $('<img>');
            container.append(img);

            var downloadUrl = version['_links']['download'];

            var downloadPromise = $.get(downloadUrl)
                .done(function(result) {
                    img.attr('src', result['thumb']);
                });

            allImagesReady.push(downloadPromise);
        });

        $.when.apply($, allImagesReady).always(function() {
            updateHeight();
        });
    });
}

Compose async operations

Bonus

// Load user information
$('.spinner').show();

$.get({
    url: userUrl,
    success: function(user) {
        $('.user-indicator').text(user.name);
        $('.spinner').hide();
    },
    error: function(xhr, reason) {
        growl("Something went wrong", reason);
        $('.spinner').hide();
    }
});
// Load user information
$('.spinner').show();

$.get({
    url: userUrl
}).done(function(user) {
    $('.user-indicator').text(user.name);
}).fail(function(xhr, reason) {
    growl("Something went wrong", reason);
}).always(function() {
    $('.spinner').hide();
};

Always clean up

A uniform interface

  • A uniform interface for async operations
  • Cleanly separates the input arguments from control flow structures
  • Less coupling

Great uses

  • Network requests
  • Many network requests!
  • Animation hooks
  • Dialogs (dialog.show().then(...))

Grey areas

  • Pagination
  • Progress (exists in some implementations)
  • Exceptions

Thanks!

Let's use promises

By dpashkevich

Let's use promises

  • 1,258