Skip to content

Instantly share code, notes, and snippets.

@jagt
Forked from domenic/README.md
Created May 23, 2013 16:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jagt/5637347 to your computer and use it in GitHub Desktop.
Save jagt/5637347 to your computer and use it in GitHub Desktop.

The scenario:

  • We are writing a digital textbook-reading app.
  • Most of the time you have a "basic" license for your textbook, but one (and only one) of your computers can request an "enhanced" license.
  • You can only print from the computer with the enhanced license.

The problem statement:

  • We want a simple printAsync() method that we can attach to our click handler for a print button somewhere in the UI.

The complications:

  • If the user has the basic license, we need to check if it's possible for them to acquire the enhanced license:
    • If so, ask the user if they want to upgrade, and then continue the print operation if they agree.
    • If not, error out: printing is impossible.
  • Printing is an asynchronous operation, provided to us via window.cPlusPlusApis.print, that could fail (e.g. if the printer is out of paper).
  • Due to some horrible threaded C++ code, we get deadlocks if the user navigates through the book at the same time as the book is printing, so we need to block the UI while it prints.

The plan:

  • Use a bunch of promise methods that work together to encapsulate all this complexity into different layers.
  • The simplest of the bunch is the dialogs module: it just gives you some nice promise-returning, jQuery-UI-dialog-using versions of alert, confirm, and prompt. It exemplifies the use of deferreds to create promise-returning methods, even in situations where the asynchronous nature of the task is indeterminate (i.e. depends on the user clicking a button).
  • The drm module talks to the server (via Ajax) to figure out if a user's computer can upgrade to the enhanced license, or to perform the upgrade process itself. It exemplifies a few more advanced promise-consumption techniques.
  • The printingUI module uses the former two to create a (private) function, ensureEnhancedAsync, that carries out all the UI and business logic around upgrading to enhanced if at all possible. It exports a printAsync function, which makes use of ensureEnhancedAsync to do its very best to print, or error out to the user if it's not. Here we see the true power of error bubbling and promise piping.
  • Finally, some other part of the app will wire up printingUI's printAsync export to a button somewhere. That's the easy part ;).
define(function (require, exports, module) {
var Q = require("q");
var $ = require("jquery");
exports.alertAsync = function (text) {
var deferred = Q.defer();
$("<div />").text(text).dialog({
buttons: {
OK: deferred.resolve
}
});
return deferred.promise;
};
exports.confirmAsync = function (text) {
var deferred = Q.defer();
$("<div />").text(text).dialog({
buttons: {
OK: function () {
deferred.resolve(true);
},
Cancel: function () {
deferred.resolve(false);
}
},
close: function () {
deferred.resolve(false);
}
});
return deferred.promise;
};
exports.promptAsync = function (text) {
var deferred = Q.defer();
var $input = $("<input type='text' />");
$("<div />").text(text).append($input).dialog({
buttons: {
OK: function () {
deferred.resolve($input.val());
},
Cancel: function () {
deferred.resolve(null);
}
},
close: function () {
deferred.resolve(null);
}
});
return deferred.promise;
};
});
define(function (require, exports, module) {
var Q = require("q");
var $ = require("jquery");
var system = require("./system"); // Implementation not shown. It has a computerId export.
// Returns a promise for a boolean.
exports.getCouldUpgradeAsync = function (book) {
var jQueryPromise = $.get("/book/" + book.id + "/drm/status?computerId=" + system.computerId, "json");
var qPromise = Q.when(jQueryPromise);
// pluck the isEnhancedAvailable property off of the JSON result.
return qPromise.get("isEnhancedAvailable");
};
// Returns a promise with no fulfillment value (equivalent to a synchronous function that doesn't return anything).
// The returned promise will be fulfilled if the HTTP POST succeeds, meaning the enhanced license has been acquired
// for this computer.
// It will be broken if the HTTP POST fails (e.g. server sends back 500 error), which we interpret as meaning that
// for some reason we couldn't assign an enhanced license to this computer.
exports.upgradeAsync = function (book) {
var jQueryPromise = $.post("/book/" + book.id + "/drm/acquire-enhanced-license?computerId=" + system.computerId);
var qPromise = Q.when(jQueryPromise);
// Convert a HTTP error into a meaningful error object.
return qPromise.then(
null, // pass through success
function () {
throw new Error("Could not acquire enhanced license for this computer.");
}
);
};
});
define(function (require, exports, module) {
var Q = require("q");
var dialogs = require("./dialogs");
var drm = require("./drm");
var spinner = require("./spinner"); // Implementation not shown; has start() and stop() methods.
// C++ has nicely tacked this method onto the window object for us.
// Signature: printImpl(bookId, pageNumber, onPrintDone(err))
// (For simplicity let's say the user can only print one page through our interface.)
var printImpl = window.cPlusPlusApis.print;
// Promisify the C++ method, which happily follows the Node-style callback-with-error-as-last-argument convention.
var printImplAsync = Q.node(printImpl);
// Returns a promise that will be fulfilled if and only if the book becomes (or already is) an enhanced copy.
function ensureEnhancedAsync(book) {
if (book.isEnhanced) {
// Return an already-fulfilled promise (with no fulfillment value): we're good to go.
return Q.resolve();
} else {
// First see if we could possibly upgrade.
return drm.getCouldUpgradeAsync().then(function (couldUpgrade) {
// Now act on that knowledge: either give up if we can't, or upgrade if we can.
if (!couldUpgrade) {
// This will bubble up, resulting in the promise returned from ensurePrintableAsync being broken.
throw new Error("You cannot print from the basic version, and have already downloaded "
+ "the enhanced version on another computer. Use that computer instead.");
} else {
// Ask the user if they want to upgrade.
return dialogs.confirmAsync("Do you want to upgrade to enhanced?").then(function (result) {
// If they do, perform the upgrade.
if (result) {
return drm.upgradeAsync(book);
} else {
throw new Error("Since you have not chosen to upgrade, printing is not available.");
}
});
}
});
}
}
// The promise returned here will always be fulfilled, since we transform errors into user notifications.
// But, when the promise is fulfilled, you can be sure that the entire printing process is done, one way or another.
exports.printAsync = function (book) {
return ensurePrintableAsync(book)
.then(function () {
return dialogs.promptAsync("What page number would you like to print?").then(function (pageNumber) {
// If they gave us a page number, print it!
if (pageNumber) {
// Start up the spinner so as to block the UI and avoid deadlocks!
spinner.start();
return printImplAsync(book.id, pageNumber);
}
// If they cancelled out of this prompt, then this method has done its job, and we can just let it return a fulfilled promise.
});
})
.then(
null, // Pass through success
function (error) {
// If we got an error, either in the ensurePrintableAsync processing or bubbled up from printImplAsync,
// alert the user.
return dialogs.alertAsync(error.message);
}
)
.fin(spinner.stop); // Make sure to stop the spinner on either success or failure to unblock the UI.
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment