www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 2d7d72fb2ab0f9b68196fe9990d9f5c6209db660
parent 81e94b4475b587b75a2ccede68bb274abf837df8
Author: Dan Stillman <dstillman@zotero.org>
Date:   Fri, 16 Aug 2013 18:15:00 -0400

Upgrade Q, and change allResolved() to allSettled()

Not fully tested

Diffstat:
Mchrome/content/zotero/xpcom/storage.js | 94++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mchrome/content/zotero/xpcom/storage/queueManager.js | 34+++++++++++++++++-----------------
Mresource/concurrent-caller.js | 4++--
Mresource/q.js | 1620++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
4 files changed, 1093 insertions(+), 659 deletions(-)

diff --git a/chrome/content/zotero/xpcom/storage.js b/chrome/content/zotero/xpcom/storage.js @@ -149,7 +149,7 @@ Zotero.Sync.Storage = new function () { else { var promise = mode.cacheCredentials(); } - promises.push(Q.allResolved([mode, promise])); + promises.push(Q.allSettled([mode, promise])); } } @@ -161,12 +161,12 @@ Zotero.Sync.Storage = new function () { // Mark WebDAV verification failure as user library error. // We ignore credentials-caching errors for ZFS and let the // later requests fail. - cacheCredentialsPromises.forEach(function (promise) { - let mode = promise[0].valueOf(); + cacheCredentialsPromises.forEach(function (results) { + let mode = results[0].value; if (mode == Zotero.Sync.Storage.WebDAV) { - if (promise[1].isRejected()) { - promises.push(Q.allResolved( - [0, promise[1]] + if (results[1].state == "rejected") { + promises.push(Q.allSettled( + [0, Q.reject(results[1].reason)] )); // Skip further syncing of user library delete libraryModes[0]; @@ -179,15 +179,13 @@ Zotero.Sync.Storage = new function () { // Get the last sync time for each library if (self.downloadOnSync(libraryID)) { - promises.push(Q.allResolved( + promises.push(Q.allSettled( [libraryID, libraryModes[libraryID].getLastSyncTime(libraryID)] )); } // If download-as-needed, we don't need the last sync time else { - promises.push(Q.allResolved( - [libraryID, null] - )); + promises.push(Q.allSettled([libraryID, null])); } } return Q.all(promises); @@ -202,20 +200,17 @@ Zotero.Sync.Storage = new function () { var libraryQueues = []; // Get the libraries we have sync times for - promises.forEach(function (promise) { - let libraryID = promise[0].valueOf(); - let lastSyncTime = promise[1].valueOf(); - if (promise[1].isFulfilled()) { + promises.forEach(function (results) { + let libraryID = results[0].value; + let lastSyncTime = results[1].value; + if (results[1].state == "fulfilled") { librarySyncTimes[libraryID] = lastSyncTime; } else { - let e = lastSyncTime.exception; - Zotero.debug(e); - Components.utils.reportError(e); + Zotero.debug(lastSyncTime.reason); + Components.utils.reportError(lastSyncTime.reason); // Pass rejected promise through - libraryQueues.push(Q.allResolved( - [libraryID, lastSyncTime] - )); + libraryQueues.push(results); } }); @@ -316,7 +311,7 @@ Zotero.Sync.Storage = new function () { // Start queues for each library for (let libraryID in librarySyncTimes) { libraryID = parseInt(libraryID); - libraryQueues.push(Q.allResolved( + libraryQueues.push(Q.allSettled( [libraryID, Zotero.Sync.Storage.QueueManager.start(libraryID)] )); } @@ -331,20 +326,20 @@ Zotero.Sync.Storage = new function () { var changedLibraries = []; var finalPromises = []; - promises.forEach(function (promise) { - var libraryID = promise[0].valueOf(); - var libraryQueues = promise[1].valueOf(); + promises.forEach(function (results) { + var libraryID = results[0].value; + var libraryQueues = results[1].value; - if (promise[1].isFulfilled()) { + if (results[1].state == "fulfilled") { libraryQueues.forEach(function (queuePromise) { - let result = queuePromise.valueOf(); if (queuePromise.isFulfilled()) { + let result = queuePromise.inspect().value; Zotero.debug("File " + result.type + " sync finished " + "for library " + libraryID); if (result.localChanges) { changedLibraries.push(libraryID); } - finalPromises.push(Q.allResolved([ + finalPromises.push(Q.allSettled([ libraryID, libraryModes[libraryID].setLastSyncTime( libraryID, @@ -353,10 +348,12 @@ Zotero.Sync.Storage = new function () { ])); } else { - result = result.exception; - Zotero.debug("File " + result.type + " sync failed " + let e = queuePromise.inspect().reason; + Zotero.debug("File " + e.type + " sync failed " + "for library " + libraryID); - finalPromises.push([libraryID, queuePromise]); + finalPromises.push(Q.allSettled( + [libraryID, Q.reject(e)] + )); } }); } @@ -389,27 +386,26 @@ Zotero.Sync.Storage = new function () { } return Q.all(finalPromises) - .then(function (promises) { - var results = { - changesMade: !!changedLibraries.length, - errors: [] - }; - - promises.forEach(function (promise) { - var libraryID = promise[0].valueOf(); - if (promise[1].isRejected()) { - var result = promise[1].valueOf(); - result = result.exception; - if (typeof result == 'string') { - result = new Error(result); - } - result.libraryID = libraryID; - results.errors.push(result); + .then(function (promises) { + var results = { + changesMade: !!changedLibraries.length, + errors: [] + }; + + promises.forEach(function (promiseResults) { + var libraryID = promiseResults[0].value; + if (promiseResults[1].state == "rejected") { + let e = promiseResults[1].reason; + if (typeof e == 'string') { + e = new Error(e); } - }); - - return results; + e.libraryID = libraryID; + results.errors.push(e); + } }); + + return results; + }); }); } diff --git a/chrome/content/zotero/xpcom/storage/queueManager.js b/chrome/content/zotero/xpcom/storage/queueManager.js @@ -53,26 +53,26 @@ Zotero.Sync.Storage.QueueManager = new function () { Zotero.debug("No files to sync" + suffix); } - return Q.allResolved(promises) - .then(function (promises) { - Zotero.debug("All storage queues are finished" + suffix); - - promises.forEach(function (promise) { - // Check for conflicts to resolve - if (promise.isFulfilled()) { - var result = promise.valueOf(); - if (result.conflicts.length) { - Zotero.debug("Reconciling conflicts for library " + result.libraryID); - Zotero.debug(result.conflicts); - var data = _reconcileConflicts(result.conflicts); - if (data) { - _processMergeData(data); - } + return Q.allSettled(promises) + .then(function (results) { + Zotero.debug("All storage queues are finished" + suffix); + + results.forEach(function (result) { + // Check for conflicts to resolve + if (result.state == "fulfilled") { + result = result.value; + if (result.conflicts.length) { + Zotero.debug("Reconciling conflicts for library " + result.libraryID); + Zotero.debug(result.conflicts); + var data = _reconcileConflicts(result.conflicts); + if (data) { + _processMergeData(data); } } - }); - return promises; + } }); + return promises; + }); }; this.stop = function (libraryID) { diff --git a/resource/concurrent-caller.js b/resource/concurrent-caller.js @@ -26,7 +26,7 @@ EXPORTED_SYMBOLS = ["ConcurrentCaller"]; Components.utils.import("resource://zotero/q.js"); -/** +f/** * Call a fixed number of functions at once, queueing the rest until slots * open and returning a promise for the final completion. The functions do * not need to return promises, but they should if they have asynchronous @@ -79,7 +79,7 @@ ConcurrentCaller.prototype.fcall = function (func) { //this._log("Running fcall on function"); promises.push(this.fcall(func[i])); } - return this.stopOnError ? Q.all(promises) : Q.allResolved(promises); + return this.stopOnError ? Q.all(promises) : Q.allSettled(promises); } // If we're at the maximum number of concurrent functions, diff --git a/resource/q.js b/resource/q.js @@ -28,7 +28,7 @@ (function (definition) { // Turn off strict mode for this function so we can assign to global.Q - /*jshint strict: false*/ + /* jshint strict: false */ // This file will function properly as a <script> tag, or a module // using CommonJS and NodeJS or RequireJS module formats. In @@ -44,7 +44,7 @@ module.exports = definition(); // RequireJS - } else if (typeof define === "function") { + } else if (typeof define === "function" && define.amd) { define(definition); // SES (Secure EcmaScript) @@ -55,7 +55,7 @@ ses.makeQ = definition; } - // Mozilla JSM + // Mozilla JSM (added by Zotero) } else if (~String(this).indexOf('BackstagePass')) { EXPORTED_SYMBOLS = ["Q"]; @@ -70,9 +70,6 @@ timer = Components.classes["@mozilla.org/timer;1"]. createInstance(Components.interfaces.nsITimer); timer.initWithCallback({"notify":function() { - // XXX Remove when we drop support for Fx <24 - if(useMethodjit !== undefined) Components.utils.methodjit = useMethodjit; - // Remove timer from array so it can be garbage collected _runningTimers.splice(_runningTimers.indexOf(timer), 1); @@ -112,6 +109,13 @@ })(function () { "use strict"; +var hasStacks = false; +try { + throw new Error(); +} catch (e) { + hasStacks = !!e.stack; +} + // All code after this point will be filtered from stack traces reported // by Q. var qStartingLine = captureLine(); @@ -122,78 +126,118 @@ var qFileName; // used for fallback in "allResolved" var noop = function () {}; -// use the fastest possible means to execute a task in a future turn +// Use the fastest possible means to execute a task in a future turn // of the event loop. -var nextTick; -if (typeof process !== "undefined") { - // node - nextTick = process.nextTick; -} else if (typeof setImmediate === "function") { - // In IE10, or use https://github.com/NobleJS/setImmediate - if (typeof window !== "undefined") { - nextTick = setImmediate.bind(window); - } else { - nextTick = setImmediate; - } -} else { - (function () { - // linked list of tasks (single, with head node) - var head = {task: void 0, next: null}, tail = head, - maxPendingTicks = 2, pendingTicks = 0, queuedTasks = 0, usedTicks = 0, - requestTick; - - function onTick() { - // In case of multiple tasks ensure at least one subsequent tick - // to handle remaining tasks in case one throws. - --pendingTicks; - - if (++usedTicks >= maxPendingTicks) { - // Amortize latency after thrown exceptions. - usedTicks = 0; - maxPendingTicks *= 4; // fast grow! - var expectedTicks = queuedTasks && Math.min(queuedTasks - 1, maxPendingTicks); - while (pendingTicks < expectedTicks) { - ++pendingTicks; - requestTick(); - } +var nextTick =(function () { + // linked list of tasks (single, with head node) + var head = {task: void 0, next: null}; + var tail = head; + var flushing = false; + var requestTick = void 0; + var isNodeJS = false; + + function flush() { + /* jshint loopfunc: true */ + + while (head.next) { + head = head.next; + var task = head.task; + head.task = void 0; + var domain = head.domain; + + if (domain) { + head.domain = void 0; + domain.enter(); } - while (queuedTasks) { - --queuedTasks; // decrement here to ensure it's never negative - head = head.next; - var task = head.task; - head.task = void 0; + try { task(); + + } catch (e) { + if (isNodeJS) { + // In node, uncaught exceptions are considered fatal errors. + // Re-throw them synchronously to interrupt flushing! + + // Ensure continuation if the uncaught exception is suppressed + // listening "uncaughtException" events (as domains does). + // Continue in next event to avoid tick recursion. + if (domain) { + domain.exit(); + } + setTimeout(flush, 0); + if (domain) { + domain.enter(); + } + + throw e; + + } else { + // In browsers, uncaught exceptions are not fatal. + // Re-throw them asynchronously to avoid slow-downs. + setTimeout(function() { + throw e; + }, 0); + } } - usedTicks = 0; + if (domain) { + domain.exit(); + } } - nextTick = function (task) { - tail = tail.next = {task: task, next: null}; - if (pendingTicks < ++queuedTasks && pendingTicks < maxPendingTicks) { - ++pendingTicks; - requestTick(); - } + flushing = false; + } + + nextTick = function (task) { + tail = tail.next = { + task: task, + domain: isNodeJS && process.domain, + next: null }; - if (typeof MessageChannel !== "undefined") { - // modern browsers - // http://www.nonblocking.io/2011/06/windownexttick.html - var channel = new MessageChannel(); - channel.port1.onmessage = onTick; - requestTick = function () { - channel.port2.postMessage(0); - }; + if (!flushing) { + flushing = true; + requestTick(); + } + }; + if (typeof process !== "undefined" && process.nextTick) { + // Node.js before 0.9. Note that some fake-Node environments, like the + // Mocha test runner, introduce a `process` global without a `nextTick`. + isNodeJS = true; + + requestTick = function () { + process.nextTick(flush); + }; + + } else if (typeof setImmediate === "function") { + // In IE10, Node.js 0.9+, or https://github.com/NobleJS/setImmediate + if (typeof window !== "undefined") { + requestTick = setImmediate.bind(window, flush); } else { - // old browsers requestTick = function () { - setTimeout(onTick, 0); + setImmediate(flush); }; } - })(); -} + + } else if (typeof MessageChannel !== "undefined") { + // modern browsers + // http://www.nonblocking.io/2011/06/windownexttick.html + var channel = new MessageChannel(); + channel.port1.onmessage = flush; + requestTick = function () { + channel.port2.postMessage(0); + }; + + } else { + // old browsers + requestTick = function () { + setTimeout(flush, 0); + }; + } + + return nextTick; +})(); // Attempt to make generics safe in the face of downstream // modifications. @@ -206,8 +250,8 @@ if (typeof process !== "undefined") { // hard-to-minify characters. // See Mark Miller’s explanation of what this does. // http://wiki.ecmascript.org/doku.php?id=conventions:safe_meta_programming +var call = Function.call; function uncurryThis(f) { - var call = Function.call; return function () { return call.apply(f, arguments); }; @@ -290,8 +334,13 @@ var object_keys = Object.keys || function (object) { var object_toString = uncurryThis(Object.prototype.toString); +function isObject(value) { + return value === Object(value); +} + // generator related shims +// FIXME: Remove this function once ES6 generators are in SpiderMonkey. function isStopIteration(exception) { return ( object_toString(exception) === "[object StopIteration]" || @@ -299,6 +348,8 @@ function isStopIteration(exception) { ); } +// FIXME: Remove this helper and Q.return once ES6 generators are in +// SpiderMonkey. var QReturnValue; if (typeof ReturnValue !== "undefined") { QReturnValue = ReturnValue; @@ -308,25 +359,46 @@ if (typeof ReturnValue !== "undefined") { }; } -// long stack traces +// Until V8 3.19 / Chromium 29 is released, SpiderMonkey is the only +// engine that has a deployed base of browsers that support generators. +// However, SM's generators use the Python-inspired semantics of +// outdated ES6 drafts. We would like to support ES6, but we'd also +// like to make it possible to use generators in deployed browsers, so +// we also support Python-style generators. At some point we can remove +// this block. +var hasES6Generators; +try { + /* jshint evil: true, nonew: false */ + new Function("(function* (){ yield 1; })"); + hasES6Generators = true; +} catch (e) { + hasES6Generators = false; +} -Q.longStackJumpLimit = 1; +// long stack traces var STACK_JUMP_SEPARATOR = "From previous event:"; function makeStackTraceLong(error, promise) { - // If possible (that is, if in V8), transform the error stack - // trace by removing Node and Q cruft, then concatenating with - // the stack trace of the promise we are ``done``ing. See #57. - if (promise.stack && + // If possible, transform the error stack trace by removing Node and Q + // cruft, then concatenating with the stack trace of `promise`. See #57. + if (hasStacks && + promise.stack && typeof error === "object" && error !== null && error.stack && error.stack.indexOf(STACK_JUMP_SEPARATOR) === -1 ) { - error.stack = filterStackString(error.stack) + - "\n" + STACK_JUMP_SEPARATOR + "\n" + - filterStackString(promise.stack); + var stacks = []; + for (var p = promise; !!p; p = p.source) { + if (p.stack) { + stacks.unshift(p.stack); + } + } + stacks.unshift(error.stack); + + var concatedStacks = stacks.join("\n" + STACK_JUMP_SEPARATOR + "\n"); + error.stack = filterStackString(concatedStacks); } } @@ -336,7 +408,7 @@ function filterStackString(stackString) { for (var i = 0; i < lines.length; ++i) { var line = lines[i]; - if (!isInternalFrame(line) && !isNodeFrame(line)) { + if (!isInternalFrame(line) && !isNodeFrame(line) && line) { desiredLines.push(line); } } @@ -348,15 +420,36 @@ function isNodeFrame(stackLine) { stackLine.indexOf("(node.js:") !== -1; } +function getFileNameAndLineNumber(stackLine) { + // Named functions: "at functionName (filename:lineNumber:columnNumber)" + // In IE10 function name can have spaces ("Anonymous function") O_o + var attempt1 = /at .+ \((.+):(\d+):(?:\d+)\)$/.exec(stackLine); + if (attempt1) { + return [attempt1[1], Number(attempt1[2])]; + } + + // Anonymous functions: "at filename:lineNumber:columnNumber" + var attempt2 = /at ([^ ]+):(\d+):(?:\d+)$/.exec(stackLine); + if (attempt2) { + return [attempt2[1], Number(attempt2[2])]; + } + + // Firefox style: "function@filename:lineNumber or @filename:lineNumber" + var attempt3 = /.*@(.+):(\d+)$/.exec(stackLine); + if (attempt3) { + return [attempt3[1], Number(attempt3[2])]; + } +} + function isInternalFrame(stackLine) { - var pieces = /at .+ \((.*):(\d+):\d+\)/.exec(stackLine); + var fileNameAndLineNumber = getFileNameAndLineNumber(stackLine); - if (!pieces) { + if (!fileNameAndLineNumber) { return false; } - var fileName = pieces[1]; - var lineNumber = pieces[2]; + var fileName = fileNameAndLineNumber[0]; + var lineNumber = fileNameAndLineNumber[1]; return fileName === qFileName && lineNumber >= qStartingLine && @@ -366,31 +459,31 @@ function isInternalFrame(stackLine) { // discover own file name and line number range for filtering stack // traces function captureLine() { - if (Error.captureStackTrace) { - var fileName, lineNumber; - - var oldPrepareStackTrace = Error.prepareStackTrace; - - Error.prepareStackTrace = function (error, frames) { - fileName = frames[1].getFileName(); - lineNumber = frames[1].getLineNumber(); - }; + if (!hasStacks) { + return; + } - // teases call of temporary prepareStackTrace - // JSHint and Closure Compiler generate known warnings here - /*jshint expr: true */ - new Error().stack; + try { + throw new Error(); + } catch (e) { + var lines = e.stack.split("\n"); + var firstLine = lines[0].indexOf("@") > 0 ? lines[1] : lines[2]; + var fileNameAndLineNumber = getFileNameAndLineNumber(firstLine); + if (!fileNameAndLineNumber) { + return; + } - Error.prepareStackTrace = oldPrepareStackTrace; - qFileName = fileName; - return lineNumber; + qFileName = fileNameAndLineNumber[0]; + return fileNameAndLineNumber[1]; } } function deprecate(callback, name, alternative) { return function () { - if (typeof console !== "undefined" && typeof console.warn === "function") { - console.warn(name + " is deprecated, use " + alternative + " instead.", new Error("").stack); + if (typeof console !== "undefined" && + typeof console.warn === "function") { + console.warn(name + " is deprecated, use " + alternative + + " instead.", new Error("").stack); } return callback.apply(callback, arguments); }; @@ -400,13 +493,26 @@ function deprecate(callback, name, alternative) { // beginning of real work /** - * Creates fulfilled promises from non-promises, - * Passes Q promises through, - * Coerces CommonJS/Promises/A+ promises to Q promises. + * Constructs a promise for an immediate reference, passes promises through, or + * coerces promises from different systems. + * @param value immediate reference or promise */ function Q(value) { - return resolve(value); + // If the object is already a Promise, return it directly. This enables + // the resolve function to both be used to created references from objects, + // but to tolerably coerce non-promises to promises. + if (isPromise(value)) { + return value; + } + + // assimilate thenables + if (isPromiseAlike(value)) { + return coerce(value); + } else { + return fulfill(value); + } } +Q.resolve = Q; /** * Performs a task in a future turn of the event loop. @@ -415,88 +521,131 @@ function Q(value) { Q.nextTick = nextTick; /** - * Constructs a {promise, resolve} object. + * Controls whether or not long stack traces will be on + */ +Q.longStackSupport = false; + +/** + * Constructs a {promise, resolve, reject} object. * - * The resolver is a callback to invoke with a more resolved value for the - * promise. To fulfill the promise, invoke the resolver with any value that is - * not a function. To reject the promise, invoke the resolver with a rejection - * object. To put the promise in the same state as another promise, invoke the - * resolver with that other promise. + * `resolve` is a callback to invoke with a more resolved value for the + * promise. To fulfill the promise, invoke `resolve` with any value that is + * not a thenable. To reject the promise, invoke `resolve` with a rejected + * thenable, or invoke `reject` with the reason directly. To resolve the + * promise to another thenable, thus putting it in the same state, invoke + * `resolve` with that other thenable. */ Q.defer = defer; function defer() { - // if "pending" is an "Array", that indicates that the promise has not yet + // if "messages" is an "Array", that indicates that the promise has not yet // been resolved. If it is "undefined", it has been resolved. Each - // element of the pending array is itself an array of complete arguments to + // element of the messages array is itself an array of complete arguments to // forward to the resolved promise. We coerce the resolution value to a - // promise using the ref promise because it handles both fully - // resolved values and other promises gracefully. - var pending = [], progressListeners = [], value; + // promise using the `resolve` function because it handles both fully + // non-thenable values and other thenables gracefully. + var messages = [], progressListeners = [], resolvedPromise; var deferred = object_create(defer.prototype); - var promise = object_create(makePromise.prototype); + var promise = object_create(Promise.prototype); promise.promiseDispatch = function (resolve, op, operands) { var args = array_slice(arguments); - if (pending) { - pending.push(args); + if (messages) { + messages.push(args); if (op === "when" && operands[1]) { // progress operand progressListeners.push(operands[1]); } } else { nextTick(function () { - value.promiseDispatch.apply(value, args); + resolvedPromise.promiseDispatch.apply(resolvedPromise, args); }); } }; - promise.valueOf = function () { - if (pending) { + // XXX deprecated + promise.valueOf = deprecate(function () { + if (messages) { return promise; } - value = valueOf(value); // shorten chain - return value; - }; + var nearerValue = nearer(resolvedPromise); + if (isPromise(nearerValue)) { + resolvedPromise = nearerValue; // shorten chain + } + return nearerValue; + }, "valueOf", "inspect"); - if (Error.captureStackTrace && Q.longStackJumpLimit > 0) { - Error.captureStackTrace(promise, defer); + promise.inspect = function () { + if (!resolvedPromise) { + return { state: "pending" }; + } + return resolvedPromise.inspect(); + }; - // Reify the stack into a string by using the accessor; this prevents - // memory leaks as per GH-111. At the same time, cut off the first line; - // it's always just "[object Promise]\n", as per the `toString`. - promise.stack = promise.stack.substring(promise.stack.indexOf("\n") + 1); + if (Q.longStackSupport && hasStacks) { + try { + throw new Error(); + } catch (e) { + // NOTE: don't try to use `Error.captureStackTrace` or transfer the + // accessor around; that causes memory leaks as per GH-111. Just + // reify the stack trace as a string ASAP. + // + // At the same time, cut off the first line; it's always just + // "[object Promise]\n", as per the `toString`. + promise.stack = e.stack.substring(e.stack.indexOf("\n") + 1); + } } - function become(resolvedValue) { - if (!pending) { - return; - } - value = resolve(resolvedValue); - array_reduce(pending, function (undefined, pending) { + // NOTE: we do the checks for `resolvedPromise` in each method, instead of + // consolidating them into `become`, since otherwise we'd create new + // promises with the lines `become(whatever(value))`. See e.g. GH-252. + + function become(newPromise) { + resolvedPromise = newPromise; + promise.source = newPromise; + + array_reduce(messages, function (undefined, message) { nextTick(function () { - value.promiseDispatch.apply(value, pending); + newPromise.promiseDispatch.apply(newPromise, message); }); }, void 0); - pending = void 0; + + messages = void 0; progressListeners = void 0; } deferred.promise = promise; - deferred.resolve = become; + deferred.resolve = function (value) { + if (resolvedPromise) { + return; + } + + become(Q(value)); + }; + deferred.fulfill = function (value) { + if (resolvedPromise) { + return; + } + become(fulfill(value)); }; - deferred.reject = function (exception) { - become(reject(exception)); + deferred.reject = function (reason) { + if (resolvedPromise) { + return; + } + + become(reject(reason)); }; deferred.notify = function (progress) { - if (pending) { - array_reduce(progressListeners, function (undefined, progressListener) { - nextTick(function () { - progressListener(progress); - }); - }, void 0); + if (resolvedPromise) { + return; } + + array_reduce(progressListeners, function (undefined, progressListener) { + nextTick(function () { + progressListener(progress); + }); + }, void 0); }; return deferred; @@ -521,27 +670,91 @@ defer.prototype.makeNodeResolver = function () { }; /** - * @param makePromise {Function} a function that returns nothing and accepts + * @param resolver {Function} a function that returns nothing and accepts * the resolve, reject, and notify functions for a deferred. * @returns a promise that may be resolved with the given resolve and reject - * functions, or rejected by a thrown exception in makePromise + * functions, or rejected by a thrown exception in resolver */ Q.promise = promise; -function promise(makePromise) { +function promise(resolver) { + if (typeof resolver !== "function") { + throw new TypeError("resolver must be a function."); + } var deferred = defer(); - fcall( - makePromise, - deferred.resolve, - deferred.reject, - deferred.notify - ).fail(deferred.reject); + try { + resolver(deferred.resolve, deferred.reject, deferred.notify); + } catch (reason) { + deferred.reject(reason); + } return deferred.promise; } +// XXX experimental. This method is a way to denote that a local value is +// serializable and should be immediately dispatched to a remote upon request, +// instead of passing a reference. +Q.passByCopy = function (object) { + //freeze(object); + //passByCopies.set(object, true); + return object; +}; + +Promise.prototype.passByCopy = function () { + //freeze(object); + //passByCopies.set(object, true); + return this; +}; + +/** + * If two promises eventually fulfill to the same value, promises that value, + * but otherwise rejects. + * @param x {Any*} + * @param y {Any*} + * @returns {Any*} a promise for x and y if they are the same, but a rejection + * otherwise. + * + */ +Q.join = function (x, y) { + return Q(x).join(y); +}; + +Promise.prototype.join = function (that) { + return Q([this, that]).spread(function (x, y) { + if (x === y) { + // TODO: "===" should be Object.is or equiv + return x; + } else { + throw new Error("Can't join: not the same: " + x + " " + y); + } + }); +}; + +/** + * Returns a promise for the first of an array of promises to become fulfilled. + * @param answers {Array[Any*]} promises to race + * @returns {Any*} the first promise to be fulfilled + */ +Q.race = race; +function race(answerPs) { + return promise(function(resolve, reject) { + // Switch to this once we can assume at least ES5 + // answerPs.forEach(function(answerP) { + // Q(answerP).then(resolve, reject); + // }); + // Use this in the meantime + for (var i = 0, len = answerPs.length; i < len; i++) { + Q(answerPs[i]).then(resolve, reject); + } + }); +} + +Promise.prototype.race = function () { + return this.then(Q.race); +}; + /** * Constructs a Promise with a promise descriptor object and optional fallback * function. The descriptor contains methods like when(rejected), get(name), - * put(name, value), post(name, args), and delete(name), which all + * set(name, value), post(name, args), and delete(name), which all * return either a value, a promise for a value, or a rejection. The fallback * accepts the operation name, a resolver, and any further arguments that would * have been forwarded to the appropriate method above had a method been @@ -549,15 +762,22 @@ function promise(makePromise) { * of the returned object, apart from that it is usable whereever promises are * bought and sold. */ -Q.makePromise = makePromise; -function makePromise(descriptor, fallback, valueOf, exception, isException) { +Q.makePromise = Promise; +function Promise(descriptor, fallback, inspect) { if (fallback === void 0) { fallback = function (op) { - return reject(new Error("Promise does not support operation: " + op)); + return reject(new Error( + "Promise does not support operation: " + op + )); + }; + } + if (inspect === void 0) { + inspect = function () { + return {state: "unknown"}; }; } - var promise = object_create(makePromise.prototype); + var promise = object_create(Promise.prototype); promise.promiseDispatch = function (resolve, op, args) { var result; @@ -575,63 +795,138 @@ function makePromise(descriptor, fallback, valueOf, exception, isException) { } }; - if (valueOf) { - promise.valueOf = valueOf; - } + promise.inspect = inspect; + + // XXX deprecated `valueOf` and `exception` support + if (inspect) { + var inspected = inspect(); + if (inspected.state === "rejected") { + promise.exception = inspected.reason; + } - if (isException) { - promise.exception = exception; + promise.valueOf = deprecate(function () { + var inspected = inspect(); + if (inspected.state === "pending" || + inspected.state === "rejected") { + return promise; + } + return inspected.value; + }); } return promise; } -// provide thenables, CommonJS/Promises/A -makePromise.prototype.then = function (fulfilled, rejected, progressed) { - return when(this, fulfilled, rejected, progressed); -}; - -makePromise.prototype.thenResolve = function (value) { - return when(this, function () { return value; }); -}; - -// Chainable methods -array_reduce( - [ - "isFulfilled", "isRejected", "isPending", - "dispatch", - "when", "spread", - "get", "put", "set", "del", "delete", - // .send() disabled by Zotero for Mozilla Task.jsm compatibility - //"post", "send", "invoke", - "post", "invoke", - "keys", - "fapply", "fcall", "fbind", - "all", "allResolved", - "timeout", "delay", - "catch", "finally", "fail", "fin", "progress", "done", - "nfcall", "nfapply", "nfbind", "denodeify", "nbind", - "ncall", "napply", "nbind", - "npost", "nsend", "ninvoke", - "nodeify" - ], - function (undefined, name) { - makePromise.prototype[name] = function () { - return Q[name].apply( - Q, - [this].concat(array_slice(arguments)) - ); - }; - }, - void 0 -); +Promise.prototype.toString = function () { + return "[object Promise]"; +}; + +Promise.prototype.then = function (fulfilled, rejected, progressed) { + var self = this; + var deferred = defer(); + var done = false; // ensure the untrusted promise makes at most a + // single call to one of the callbacks + + function _fulfilled(value) { + try { + return typeof fulfilled === "function" ? fulfilled(value) : value; + } catch (exception) { + return reject(exception); + } + } + + function _rejected(exception) { + if (typeof rejected === "function") { + makeStackTraceLong(exception, self); + try { + return rejected(exception); + } catch (newException) { + return reject(newException); + } + } + return reject(exception); + } + + function _progressed(value) { + return typeof progressed === "function" ? progressed(value) : value; + } + + nextTick(function () { + self.promiseDispatch(function (value) { + if (done) { + return; + } + done = true; + + deferred.resolve(_fulfilled(value)); + }, "when", [function (exception) { + if (done) { + return; + } + done = true; + + deferred.resolve(_rejected(exception)); + }]); + }); + + // Progress propagator need to be attached in the current tick. + self.promiseDispatch(void 0, "when", [void 0, function (value) { + var newValue; + var threw = false; + try { + newValue = _progressed(value); + } catch (e) { + threw = true; + if (Q.onerror) { + Q.onerror(e); + } else { + throw e; + } + } + + if (!threw) { + deferred.notify(newValue); + } + }]); -makePromise.prototype.toSource = function () { - return this.toString(); + return deferred.promise; }; -makePromise.prototype.toString = function () { - return "[object Promise]"; +/** + * Registers an observer on a promise. + * + * Guarantees: + * + * 1. that fulfilled and rejected will be called only once. + * 2. that either the fulfilled callback or the rejected callback will be + * called, but not both. + * 3. that fulfilled and rejected will not be called in this turn. + * + * @param value promise or immediate reference to observe + * @param fulfilled function to be called with the fulfilled value + * @param rejected function to be called with the rejection exception + * @param progressed function to be called on any progress notifications + * @return promise for the return value from the invoked callback + */ +Q.when = when; +function when(value, fulfilled, rejected, progressed) { + return Q(value).then(fulfilled, rejected, progressed); +} + +Promise.prototype.thenResolve = function (value) { + return this.then(function () { return value; }); +}; + +Q.thenResolve = function (promise, value) { + return Q(promise).thenResolve(value); +}; + +Promise.prototype.thenReject = function (reason) { + return this.then(function () { throw reason; }); +}; + +Q.thenReject = function (promise, reason) { + return Q(promise).thenReject(reason); }; /** @@ -643,10 +938,15 @@ makePromise.prototype.toString = function () { * @param object * @returns most resolved (nearest) form of the object */ -Q.nearer = valueOf; -function valueOf(value) { + +// XXX should we re-do this? +Q.nearer = nearer; +function nearer(value) { if (isPromise(value)) { - return value.valueOf(); + var inspected = value.inspect(); + if (inspected.state === "fulfilled") { + return inspected.value; + } } return value; } @@ -657,12 +957,14 @@ function valueOf(value) { */ Q.isPromise = isPromise; function isPromise(object) { - return object && typeof object.promiseDispatch === "function"; + return isObject(object) && + typeof object.promiseDispatch === "function" && + typeof object.inspect === "function"; } Q.isPromiseAlike = isPromiseAlike; function isPromiseAlike(object) { - return object && typeof object.then === "function"; + return isObject(object) && typeof object.then === "function"; } /** @@ -671,89 +973,154 @@ function isPromiseAlike(object) { */ Q.isPending = isPending; function isPending(object) { - return !isFulfilled(object) && !isRejected(object); + return isPromise(object) && object.inspect().state === "pending"; } +Promise.prototype.isPending = function () { + return this.inspect().state === "pending"; +}; + /** * @returns whether the given object is a value or fulfilled * promise. */ Q.isFulfilled = isFulfilled; function isFulfilled(object) { - return !isPromiseAlike(valueOf(object)); + return !isPromise(object) || object.inspect().state === "fulfilled"; } +Promise.prototype.isFulfilled = function () { + return this.inspect().state === "fulfilled"; +}; + /** * @returns whether the given object is a rejected promise. */ Q.isRejected = isRejected; function isRejected(object) { - object = valueOf(object); - return isPromise(object) && 'exception' in object; + return isPromise(object) && object.inspect().state === "rejected"; } -var rejections = []; -var errors = []; -var errorsDisplayed; -function displayErrors() { +Promise.prototype.isRejected = function () { + return this.inspect().state === "rejected"; +}; + +//// BEGIN UNHANDLED REJECTION TRACKING + +// This promise library consumes exceptions thrown in handlers so they can be +// handled by a subsequent promise. The exceptions get added to this array when +// they are created, and removed when they are handled. Note that in ES6 or +// shimmed environments, this would naturally be a `Set`. +var unhandledReasons = []; +var unhandledRejections = []; +var unhandledReasonsDisplayed = false; +var trackUnhandledRejections = true; +function displayUnhandledReasons() { if ( - !errorsDisplayed && + !unhandledReasonsDisplayed && typeof window !== "undefined" && !window.Touch && window.console ) { - // This promise library consumes exceptions thrown in handlers so - // they can be handled by a subsequent promise. The rejected - // promises get added to this array when they are created, and - // removed when they are handled. - console.log("Should be empty:", errors); + console.warn("[Q] Unhandled rejection reasons (should be empty):", + unhandledReasons); } - errorsDisplayed = true; + + unhandledReasonsDisplayed = true; } -// Show unhandled rejection if Node exits without handling an outstanding -// rejection. (Note that Browserify presently produces a process global -// without the Emitter on interface) -if (typeof process !== "undefined" && process.on) { - process.on("exit", function () { - for (var i = 0; i < errors.length; i++) { - var error = errors[i]; - if (error && typeof error.stack !== "undefined") { - console.warn("Unhandled rejected promise:", error.stack); - } else { - console.warn("Unhandled rejected promise (no stack):", error); - } +function logUnhandledReasons() { + for (var i = 0; i < unhandledReasons.length; i++) { + var reason = unhandledReasons[i]; + if (reason && typeof reason.stack !== "undefined") { + console.warn("Unhandled rejection reason:", reason.stack); + } else { + console.warn("Unhandled rejection reason (no stack):", reason); } - }); + } } -/** - * Constructs a rejected promise. - * @param exception value describing the failure - */ -Q.reject = reject; -function reject(exception) { - var rejection = makePromise({ - "when": function (rejected) { - // note that the error has been handled - if (rejected) { - var at = array_indexOf(rejections, this); - if (at !== -1) { - errors.splice(at, 1); - rejections.splice(at, 1); - } - } - return rejected ? rejected(exception) : this; +function resetUnhandledRejections() { + unhandledReasons.length = 0; + unhandledRejections.length = 0; + unhandledReasonsDisplayed = false; + + if (!trackUnhandledRejections) { + trackUnhandledRejections = true; + + // Show unhandled rejection reasons if Node exits without handling an + // outstanding rejection. (Note that Browserify presently produces a + // `process` global without the `EventEmitter` `on` method.) + if (typeof process !== "undefined" && process.on) { + process.on("exit", logUnhandledReasons); } - }, function fallback() { - return reject(exception); - }, function valueOf() { + } +} + +function trackRejection(promise, reason) { + if (!trackUnhandledRejections) { + return; + } + + unhandledRejections.push(promise); + unhandledReasons.push(reason); + displayUnhandledReasons(); +} + +function untrackRejection(promise) { + if (!trackUnhandledRejections) { + return; + } + + var at = array_indexOf(unhandledRejections, promise); + if (at !== -1) { + unhandledRejections.splice(at, 1); + unhandledReasons.splice(at, 1); + } +} + +Q.resetUnhandledRejections = resetUnhandledRejections; + +Q.getUnhandledReasons = function () { + // Make a copy so that consumers can't interfere with our internal state. + return unhandledReasons.slice(); +}; + +Q.stopUnhandledRejectionTracking = function () { + resetUnhandledRejections(); + if (typeof process !== "undefined" && process.on) { + process.removeListener("exit", logUnhandledReasons); + } + trackUnhandledRejections = false; +}; + +resetUnhandledRejections(); + +//// END UNHANDLED REJECTION TRACKING + +/** + * Constructs a rejected promise. + * @param reason value describing the failure + */ +Q.reject = reject; +function reject(reason) { + var rejection = Promise({ + "when": function (rejected) { + // note that the error has been handled + if (rejected) { + untrackRejection(this); + } + return rejected ? rejected(reason) : this; + } + }, function fallback() { return this; - }, exception, true); - // note that the error has not been handled - displayErrors(); - rejections.push(rejection); - errors.push(exception); + }, function inspect() { + return { state: "rejected", reason: reason }; + }); + + // Note that the reason has not been handled. + trackRejection(rejection, reason); + return rejection; } @@ -762,71 +1129,41 @@ function reject(exception) { * @param value immediate reference */ Q.fulfill = fulfill; -function fulfill(object) { - return makePromise({ +function fulfill(value) { + return Promise({ "when": function () { - return object; + return value; }, "get": function (name) { - return object[name]; + return value[name]; }, - "set": function (name, value) { - object[name] = value; + "set": function (name, rhs) { + value[name] = rhs; }, "delete": function (name) { - delete object[name]; + delete value[name]; }, "post": function (name, args) { // Mark Miller proposes that post with no name should apply a // promised function. - if (name == null) { // iff name is null or undefined - return object.apply(void 0, args); + if (name === null || name === void 0) { + return value.apply(void 0, args); } else { - return object[name].apply(object, args); + return value[name].apply(value, args); } }, - "apply": function (thisP, args) { - return object.apply(thisP, args); + "apply": function (thisp, args) { + return value.apply(thisp, args); }, "keys": function () { - return object_keys(object); + return object_keys(value); } - }, void 0, function valueOf() { - return object; + }, void 0, function inspect() { + return { state: "fulfilled", value: value }; }); } /** - * Constructs a promise for an immediate reference, passes promises through, or - * coerces promises from different systems. - * @param value immediate reference or promise - */ -Q.resolve = resolve; -function resolve(value) { - // If the object is already a Promise, return it directly. This enables - // the resolve function to both be used to created references from objects, - // but to tolerably coerce non-promises to promises. - if (isPromise(value)) { - return value; - } - // In order to break infinite recursion or loops between `then` and - // `resolve`, it is necessary to attempt to extract fulfilled values - // out of foreign promise implementations before attempting to wrap - // them as unresolved promises. It is my hope that other - // implementations will implement `valueOf` to synchronously extract - // the fulfillment value from their fulfilled promises. If the - // other promise library does not implement `valueOf`, the - // implementations on primordial prototypes are harmless. - value = valueOf(value); - // assimilate thenables, CommonJS/Promises/A+ - if (isPromiseAlike(value)) { - return coerce(value); - } else { - return fulfill(value); - } -} - -/** * Converts thenables to Q promises. * @param promise thenable promise * @returns a Q promise @@ -854,101 +1191,13 @@ function coerce(promise) { */ Q.master = master; function master(object) { - return makePromise({ + return Promise({ "isDef": function () {} }, function fallback(op, args) { return dispatch(object, op, args); }, function () { - return valueOf(object); - }); -} - -/** - * Registers an observer on a promise. - * - * Guarantees: - * - * 1. that fulfilled and rejected will be called only once. - * 2. that either the fulfilled callback or the rejected callback will be - * called, but not both. - * 3. that fulfilled and rejected will not be called in this turn. - * - * @param value promise or immediate reference to observe - * @param fulfilled function to be called with the fulfilled value - * @param rejected function to be called with the rejection exception - * @param progressed function to be called on any progress notifications - * @return promise for the return value from the invoked callback - */ -Q.when = when; -function when(value, fulfilled, rejected, progressed) { - var deferred = defer(); - var done = false; // ensure the untrusted promise makes at most a - // single call to one of the callbacks - - function _fulfilled(value) { - try { - return typeof fulfilled === "function" ? fulfilled(value) : value; - } catch (exception) { - return reject(exception); - } - } - - function _rejected(exception) { - if (typeof rejected === "function") { - makeStackTraceLong(exception, resolvedValue); - try { - return rejected(exception); - } catch (newException) { - return reject(newException); - } - } - return reject(exception); - } - - function _progressed(value) { - return typeof progressed === "function" ? progressed(value) : value; - } - - var resolvedValue = resolve(value); - nextTick(function () { - resolvedValue.promiseDispatch(function (value) { - if (done) { - return; - } - done = true; - - deferred.resolve(_fulfilled(value)); - }, "when", [function (exception) { - if (done) { - return; - } - done = true; - - deferred.resolve(_rejected(exception)); - }]); + return Q(object).inspect(); }); - - // Progress propagator need to be attached in the current tick. - resolvedValue.promiseDispatch(void 0, "when", [void 0, function (value) { - var newValue; - var threw = false; - try { - newValue = _progressed(value); - } catch (e) { - threw = true; - if (Q.onerror) { - Q.onerror(e); - } else { - throw e; - } - } - - if (!threw) { - deferred.notify(newValue); - } - }]); - - return deferred.promise; } /** @@ -962,20 +1211,27 @@ function when(value, fulfilled, rejected, progressed) { * either callback. */ Q.spread = spread; -function spread(promise, fulfilled, rejected) { - return when(promise, function (valuesOrPromises) { - return all(valuesOrPromises).then(function (values) { - return fulfilled.apply(void 0, values); - }, rejected); - }, rejected); +function spread(value, fulfilled, rejected) { + return Q(value).spread(fulfilled, rejected); } +Promise.prototype.spread = function (fulfilled, rejected) { + return this.all().then(function (array) { + return fulfilled.apply(void 0, array); + }, rejected); +}; + /** * The async function is a decorator for generator functions, turning - * them into asynchronous generators. This presently only works in - * Firefox/Spidermonkey, however, this code does not cause syntax - * errors in older engines. This code should continue to work and - * will in fact improve over time as the language improves. + * them into asynchronous generators. Although generators are only part + * of the newest ECMAScript 6 drafts, this code does not cause syntax + * errors in older engines. This code should continue to work and will + * in fact improve over time as the language improves. + * + * ES6 generators are currently part of V8 version 3.19 with the + * --harmony-generators runtime flag enabled. SpiderMonkey has had them + * for longer, but under an older Python-inspired form. This function + * works on both kinds of generators. * * Decorates a generator function such that: * - it may yield promises @@ -990,18 +1246,6 @@ function spread(promise, fulfilled, rejected) { * every following yield until it is caught, or until it escapes * the generator function altogether, and is translated into a * rejection for the promise returned by the decorated generator. - * - in present implementations of generators, when a generator - * function is complete, it throws ``StopIteration``, ``return`` is - * a syntax error in the presence of ``yield``, so there is no - * observable return value. There is a proposal[1] to add support - * for ``return``, which would permit the value to be carried by a - * ``StopIteration`` instance, in which case it would fulfill the - * promise returned by the asynchronous generator. This can be - * emulated today by throwing StopIteration explicitly with a value - * property. - * - * [1]: http://wiki.ecmascript.org/doku.php?id=strawman:async_functions#reference_implementation - * */ Q.async = async; function async(makeGenerator) { @@ -1010,45 +1254,83 @@ function async(makeGenerator) { // when verb is "throw", arg is an exception function continuer(verb, arg) { var result; - try { - result = generator[verb](arg); - } catch (exception) { - if (isStopIteration(exception)) { - return exception.value; - } else { + if (hasES6Generators) { + try { + result = generator[verb](arg); + } catch (exception) { return reject(exception); } + if (result.done) { + return result.value; + } else { + return when(result.value, callback, errback); + } + } else { + // FIXME: Remove this case when SM does ES6 generators. + try { + result = generator[verb](arg); + } catch (exception) { + if (isStopIteration(exception)) { + return exception.value; + } else { + return reject(exception); + } + } + return when(result, callback, errback); } - return when(result, callback, errback); } var generator = makeGenerator.apply(this, arguments); - var callback = continuer.bind(continuer, "send"); + var callback = continuer.bind(continuer, "next"); var errback = continuer.bind(continuer, "throw"); return callback(); }; } /** + * The spawn function is a small wrapper around async that immediately + * calls the generator and also ends the promise chain, so that any + * unhandled errors are thrown instead of forwarded to the error + * handler. This is useful because it's extremely common to run + * generators at the top-level to work with libraries. + */ +Q.spawn = spawn; +function spawn(makeGenerator) { + Q.done(Q.async(makeGenerator)()); +} + +// FIXME: Remove this interface once ES6 generators are in SpiderMonkey. +/** * Throws a ReturnValue exception to stop an asynchronous generator. - * Only useful presently in Firefox/SpiderMonkey since generators are - * implemented. + * + * This interface is a stop-gap measure to support generator return + * values in older Firefox/SpiderMonkey. In browsers that support ES6 + * generators like Chromium 29, just use "return" in your generator + * functions. + * * @param value the return value for the surrounding generator * @throws ReturnValue exception with the value. * @example + * // ES6 style + * Q.async(function* () { + * var foo = yield getFooPromise(); + * var bar = yield getBarPromise(); + * return foo + bar; + * }) + * // Older SpiderMonkey style * Q.async(function () { * var foo = yield getFooPromise(); * var bar = yield getBarPromise(); * Q.return(foo + bar); * }) */ -Q['return'] = _return; +Q["return"] = _return; function _return(value) { throw new QReturnValue(value); } /** * The promised function decorator ensures that any promise arguments - * are resolved and passed as values (`this` is also resolved and passed + * are settled and passed as values (`this` is also settled and passed * as a value). It will also ensure that the result of a function is * always a promise. * @@ -1056,7 +1338,7 @@ function _return(value) { * var add = Q.promised(function (a, b) { * return a + b; * }); - * add(Q.resolve(a), Q.resolve(B)); + * add(Q(a), Q(B)); * * @param {function} callback The function to decorate * @returns {function} a function that has been decorated. @@ -1079,26 +1361,17 @@ function promised(callback) { */ Q.dispatch = dispatch; function dispatch(object, op, args) { + return Q(object).dispatch(op, args); +} + +Promise.prototype.dispatch = function (op, args) { + var self = this; var deferred = defer(); nextTick(function () { - resolve(object).promiseDispatch(deferred.resolve, op, args); + self.promiseDispatch(deferred.resolve, op, args); }); return deferred.promise; -} - -/** - * Constructs a promise method that can be used to safely observe resolution of - * a promise for an arbitrarily named method like "propfind" in a future turn. - * - * "dispatcher" constructs methods like "get(promise, name)" and "put(promise)". - */ -Q.dispatcher = dispatcher; -function dispatcher(op) { - return function (object) { - var args = array_slice(arguments, 1); - return dispatch(object, op, args); - }; -} +}; /** * Gets the value of a property in a future turn. @@ -1106,7 +1379,13 @@ function dispatcher(op) { * @param name name of property to get * @return promise for the property value */ -Q.get = dispatcher("get"); +Q.get = function (object, key) { + return Q(object).dispatch("get", [key]); +}; + +Promise.prototype.get = function (key) { + return this.dispatch("get", [key]); +}; /** * Sets the value of a property in a future turn. @@ -1115,7 +1394,13 @@ Q.get = dispatcher("get"); * @param value new value of property * @return promise for the return value */ -Q.set = dispatcher("set"); +Q.set = function (object, key, value) { + return Q(object).dispatch("set", [key, value]); +}; + +Promise.prototype.set = function (key, value) { + return this.dispatch("set", [key, value]); +}; /** * Deletes a property in a future turn. @@ -1123,8 +1408,15 @@ Q.set = dispatcher("set"); * @param name name of property to delete * @return promise for the return value */ -Q["delete"] = // XXX experimental -Q.del = dispatcher("delete"); +Q.del = // XXX legacy +Q["delete"] = function (object, key) { + return Q(object).dispatch("delete", [key]); +}; + +Promise.prototype.del = // XXX legacy +Promise.prototype["delete"] = function (key) { + return this.dispatch("delete", [key]); +}; /** * Invokes a method in a future turn. @@ -1139,7 +1431,15 @@ Q.del = dispatcher("delete"); * @return promise for the return value */ // bound locally because it is used by other methods -var post = Q.post = dispatcher("post"); +Q.mapply = // XXX As proposed by "Redsandro" +Q.post = function (object, name, args) { + return Q(object).dispatch("post", [name, args]); +}; + +Promise.prototype.mapply = // XXX As proposed by "Redsandro" +Promise.prototype.post = function (name, args) { + return this.dispatch("post", [name, args]); +}; /** * Invokes a method in a future turn. @@ -1148,35 +1448,46 @@ var post = Q.post = dispatcher("post"); * @param ...args array of invocation arguments * @return promise for the return value */ -// Disabled by Zotero for Mozilla Task.jsm compatibility -//Q.send = send; -Q.invoke = send; // synonyms -function send(value, name) { - var args = array_slice(arguments, 2); - return post(value, name, args); -} +// .send() disabled by Zotero for Mozilla Task.jsm compatibility +//Q.send = // XXX Mark Miller's proposed parlance +Q.mcall = // XXX As proposed by "Redsandro" +Q.invoke = function (object, name /*...args*/) { + return Q(object).dispatch("post", [name, array_slice(arguments, 2)]); +}; + +// .send() disabled by Zotero for Mozilla Task.jsm compatibility +//Promise.prototype.send = // XXX Mark Miller's proposed parlance +Promise.prototype.mcall = // XXX As proposed by "Redsandro" +Promise.prototype.invoke = function (name /*...args*/) { + return this.dispatch("post", [name, array_slice(arguments, 1)]); +}; /** * Applies the promised function in a future turn. * @param object promise or immediate reference for target function * @param args array of application arguments */ -Q.fapply = fapply; -function fapply(value, args) { - return dispatch(value, "apply", [void 0, args]); -} +Q.fapply = function (object, args) { + return Q(object).dispatch("apply", [void 0, args]); +}; + +Promise.prototype.fapply = function (args) { + return this.dispatch("apply", [void 0, args]); +}; /** * Calls the promised function in a future turn. * @param object promise or immediate reference for target function * @param ...args array of application arguments */ -Q["try"] = fcall; // XXX experimental -Q.fcall = fcall; -function fcall(value) { - var args = array_slice(arguments, 1); - return fapply(value, args); -} +Q["try"] = +Q.fcall = function (object /* ...args*/) { + return Q(object).dispatch("apply", [void 0, array_slice(arguments, 1)]); +}; + +Promise.prototype.fcall = function (/*...args*/) { + return this.dispatch("apply", [void 0, array_slice(arguments)]); +}; /** * Binds the promised function, transforming return values into a fulfilled @@ -1184,22 +1495,40 @@ function fcall(value) { * @param object promise or immediate reference for target function * @param ...args array of application arguments */ -Q.fbind = fbind; -function fbind(value) { +Q.fbind = function (object /*...args*/) { + var promise = Q(object); var args = array_slice(arguments, 1); return function fbound() { - var allArgs = args.concat(array_slice(arguments)); - return dispatch(value, "apply", [this, allArgs]); + return promise.dispatch("apply", [ + this, + args.concat(array_slice(arguments)) + ]); }; -} +}; +Promise.prototype.fbind = function (/*...args*/) { + var promise = this; + var args = array_slice(arguments); + return function fbound() { + return promise.dispatch("apply", [ + this, + args.concat(array_slice(arguments)) + ]); + }; +}; /** * Requests the names of the owned properties of a promised * object in a future turn. * @param object promise or immediate reference for target object - * @return promise for the keys of the eventually resolved object + * @return promise for the keys of the eventually settled object */ -Q.keys = dispatcher("keys"); +Q.keys = function (object) { + return Q(object).dispatch("keys", []); +}; + +Promise.prototype.keys = function () { + return this.dispatch("keys", []); +}; /** * Turns an array of promises into a promise for an array. If any of @@ -1213,33 +1542,45 @@ Q.keys = dispatcher("keys"); Q.all = all; function all(promises) { return when(promises, function (promises) { - var countDown = promises.length; - if (countDown === 0) { - return resolve(promises); - } + var countDown = 0; var deferred = defer(); array_reduce(promises, function (undefined, promise, index) { - if (isFulfilled(promise)) { - promises[index] = valueOf(promise); - if (--countDown === 0) { - deferred.resolve(promises); - } + var snapshot; + if ( + isPromise(promise) && + (snapshot = promise.inspect()).state === "fulfilled" + ) { + promises[index] = snapshot.value; } else { - when(promise, function (value) { - promises[index] = value; - if (--countDown === 0) { - deferred.resolve(promises); + ++countDown; + when( + promise, + function (value) { + promises[index] = value; + if (--countDown === 0) { + deferred.resolve(promises); + } + }, + deferred.reject, + function (progress) { + deferred.notify({ index: index, value: progress }); } - }) - .fail(deferred.reject); + ); } }, void 0); + if (countDown === 0) { + deferred.resolve(promises); + } return deferred.promise; }); } +Promise.prototype.all = function () { + return all(this); +}; + /** - * Waits for all promises to be resolved, either fulfilled or + * Waits for all promises to be settled, either fulfilled or * rejected. This is distinct from `all` since that would stop * waiting at the first rejection. The promise returned by * `allResolved` will never be rejected. @@ -1247,10 +1588,10 @@ function all(promises) { * (or values) * @return a promise for an array of promises */ -Q.allResolved = allResolved; +Q.allResolved = deprecate(allResolved, "allResolved", "allSettled"); function allResolved(promises) { return when(promises, function (promises) { - promises = array_map(promises, resolve); + promises = array_map(promises, Q); return when(all(array_map(promises, function (promise) { return when(promise, noop, noop); })), function () { @@ -1259,6 +1600,37 @@ function allResolved(promises) { }); } +Promise.prototype.allResolved = function () { + return allResolved(this); +}; + +/** + * @see Promise#allSettled + */ +Q.allSettled = allSettled; +function allSettled(promises) { + return Q(promises).allSettled(); +} + +/** + * Turns an array of promises into a promise for an array of their states (as + * returned by `inspect`) when they have all settled. + * @param {Array[Any*]} values an array (or promise for an array) of values (or + * promises for values) + * @returns {Array[State]} an array of states for the respective values. + */ +Promise.prototype.allSettled = function () { + return this.then(function (promises) { + return all(array_map(promises, function (promise) { + promise = Q(promise); + function regardless() { + return promise.inspect(); + } + return promise.then(regardless, regardless); + })); + }); +}; + /** * Captures the failure of a promise, giving an oportunity to recover * with a callback. If the given promise is fulfilled, the returned @@ -1268,11 +1640,15 @@ function allResolved(promises) { * given promise is rejected * @returns a promise for the return value of the callback */ -Q["catch"] = // XXX experimental -Q.fail = fail; -function fail(promise, rejected) { - return when(promise, void 0, rejected); -} +Q.fail = // XXX legacy +Q["catch"] = function (object, rejected) { + return Q(object).then(void 0, rejected); +}; + +Promise.prototype.fail = // XXX legacy +Promise.prototype["catch"] = function (rejected) { + return this.then(void 0, rejected); +}; /** * Attaches a listener that can respond to progress notifications from a @@ -1283,12 +1659,16 @@ function fail(promise, rejected) { * @returns the given promise, unchanged */ Q.progress = progress; -function progress(promise, progressed) { - return when(promise, void 0, void 0, progressed); +function progress(object, progressed) { + return Q(object).then(void 0, void 0, progressed); } +Promise.prototype.progress = function (progressed) { + return this.then(void 0, void 0, progressed); +}; + /** - * Provides an opportunity to observe the rejection of a promise, + * Provides an opportunity to observe the settling of a promise, * regardless of whether the promise is fulfilled or rejected. Forwards * the resolution to the returned promise when the callback is done. * The callback can return a promise to defer completion. @@ -1298,19 +1678,25 @@ function progress(promise, progressed) { * @returns a promise for the resolution of the given promise when * ``fin`` is done. */ -Q["finally"] = // XXX experimental -Q.fin = fin; -function fin(promise, callback) { - return when(promise, function (value) { - return when(callback(), function () { +Q.fin = // XXX legacy +Q["finally"] = function (object, callback) { + return Q(object)["finally"](callback); +}; + +Promise.prototype.fin = // XXX legacy +Promise.prototype["finally"] = function (callback) { + callback = Q(callback); + return this.then(function (value) { + return callback.fcall().then(function () { return value; }); - }, function (exception) { - return when(callback(), function () { - return reject(exception); + }, function (reason) { + // TODO attempt to recycle the rejection with "this". + return callback.fcall().then(function () { + throw reason; }); }); -} +}; /** * Terminates a chain of promises, forcing rejections to be @@ -1318,14 +1704,16 @@ function fin(promise, callback) { * @param {Any*} promise at the end of a chain of promises * @returns nothing */ -Q.done = done; -function done(promise, fulfilled, rejected, progress) { +Q.done = function (object, fulfilled, rejected, progress) { + return Q(object).done(fulfilled, rejected, progress); +}; + +Promise.prototype.done = function (fulfilled, rejected, progress) { var onUnhandledError = function (error) { // forward to a future turn so that ``when`` // does not catch it and turn it into a rejection. nextTick(function () { makeStackTraceLong(error, promise); - if (Q.onerror) { Q.onerror(error); } else { @@ -1335,62 +1723,73 @@ function done(promise, fulfilled, rejected, progress) { }; // Avoid unnecessary `nextTick`ing via an unnecessary `when`. - var promiseToHandle = fulfilled || rejected || progress ? - when(promise, fulfilled, rejected, progress) : - promise; + var promise = fulfilled || rejected || progress ? + this.then(fulfilled, rejected, progress) : + this; if (typeof process === "object" && process && process.domain) { onUnhandledError = process.domain.bind(onUnhandledError); } - fail(promiseToHandle, onUnhandledError); -} + + promise.then(void 0, onUnhandledError); +}; /** * Causes a promise to be rejected if it does not get fulfilled before * some milliseconds time out. * @param {Any*} promise * @param {Number} milliseconds timeout + * @param {String} custom error message (optional) * @returns a promise for the resolution of the given promise if it is * fulfilled before the timeout, otherwise rejected. */ -Q.timeout = timeout; -function timeout(promise, ms) { +Q.timeout = function (object, ms, message) { + return Q(object).timeout(ms, message); +}; + +Promise.prototype.timeout = function (ms, message) { var deferred = defer(); var timeoutId = setTimeout(function () { - deferred.reject(new Error("Timed out after " + ms + " ms")); + deferred.reject(new Error(message || "Timed out after " + ms + " ms")); }, ms); - when(promise, function (value) { + this.then(function (value) { clearTimeout(timeoutId); deferred.resolve(value); }, function (exception) { clearTimeout(timeoutId); deferred.reject(exception); - }); + }, deferred.notify); return deferred.promise; -} +}; /** - * Returns a promise for the given value (or promised value) after some - * milliseconds. + * Returns a promise for the given value (or promised value), some + * milliseconds after it resolved. Passes rejections immediately. * @param {Any*} promise * @param {Number} milliseconds - * @returns a promise for the resolution of the given promise after some - * time has elapsed. + * @returns a promise for the resolution of the given promise after milliseconds + * time has elapsed since the resolution of the given promise. + * If the given promise rejects, that is passed immediately. */ -Q.delay = delay; -function delay(promise, timeout) { +Q.delay = function (object, timeout) { if (timeout === void 0) { - timeout = promise; - promise = void 0; + timeout = object; + object = void 0; } - var deferred = defer(); - setTimeout(function () { - deferred.resolve(promise); - }, timeout); - return deferred.promise; -} + return Q(object).delay(timeout); +}; + +Promise.prototype.delay = function (timeout) { + return this.then(function (value) { + var deferred = defer(); + setTimeout(function () { + deferred.resolve(value); + }, timeout); + return deferred.promise; + }); +}; /** * Passes a continuation to a Node function, which is called with the given @@ -1401,75 +1800,86 @@ function delay(promise, timeout) { * }) * */ -Q.nfapply = nfapply; -function nfapply(callback, args) { - var nodeArgs = array_slice(args); +Q.nfapply = function (callback, args) { + return Q(callback).nfapply(args); +}; + +Promise.prototype.nfapply = function (args) { var deferred = defer(); + var nodeArgs = array_slice(args); nodeArgs.push(deferred.makeNodeResolver()); - - fapply(callback, nodeArgs).fail(deferred.reject); + this.fapply(nodeArgs).fail(deferred.reject); return deferred.promise; -} +}; /** * Passes a continuation to a Node function, which is called with the given * arguments provided individually, and returns a promise. - * - * Q.nfcall(FS.readFile, __filename) - * .then(function (content) { - * }) + * @example + * Q.nfcall(FS.readFile, __filename) + * .then(function (content) { + * }) * */ -Q.nfcall = nfcall; -function nfcall(callback/*, ...args */) { - var nodeArgs = array_slice(arguments, 1); +Q.nfcall = function (callback /*...args*/) { + var args = array_slice(arguments, 1); + return Q(callback).nfapply(args); +}; + +Promise.prototype.nfcall = function (/*...args*/) { + var nodeArgs = array_slice(arguments); var deferred = defer(); nodeArgs.push(deferred.makeNodeResolver()); - - fapply(callback, nodeArgs).fail(deferred.reject); + this.fapply(nodeArgs).fail(deferred.reject); return deferred.promise; -} +}; /** * Wraps a NodeJS continuation passing function and returns an equivalent * version that returns a promise. - * - * Q.nfbind(FS.readFile, __filename)("utf-8") - * .then(console.log) - * .done() - * + * @example + * Q.nfbind(FS.readFile, __filename)("utf-8") + * .then(console.log) + * .done() */ -Q.nfbind = nfbind; -Q.denodeify = Q.nfbind; // synonyms -function nfbind(callback/*, ...args */) { +Q.nfbind = +Q.denodeify = function (callback /*...args*/) { var baseArgs = array_slice(arguments, 1); return function () { var nodeArgs = baseArgs.concat(array_slice(arguments)); var deferred = defer(); nodeArgs.push(deferred.makeNodeResolver()); - - fapply(callback, nodeArgs).fail(deferred.reject); + Q(callback).fapply(nodeArgs).fail(deferred.reject); return deferred.promise; }; -} +}; -Q.nbind = nbind; -function nbind(callback/*, ... args*/) { - var baseArgs = array_slice(arguments, 1); +Promise.prototype.nfbind = +Promise.prototype.denodeify = function (/*...args*/) { + var args = array_slice(arguments); + args.unshift(this); + return Q.denodeify.apply(void 0, args); +}; + +Q.nbind = function (callback, thisp /*...args*/) { + var baseArgs = array_slice(arguments, 2); return function () { var nodeArgs = baseArgs.concat(array_slice(arguments)); var deferred = defer(); nodeArgs.push(deferred.makeNodeResolver()); - - var thisArg = this; function bound() { - return callback.apply(thisArg, arguments); + return callback.apply(thisp, arguments); } - - fapply(bound, nodeArgs).fail(deferred.reject); + Q(bound).fapply(nodeArgs).fail(deferred.reject); return deferred.promise; }; -} +}; + +Promise.prototype.nbind = function (/*thisp, ...args*/) { + var args = array_slice(arguments, 0); + args.unshift(this); + return Q.nbind.apply(void 0, args); +}; /** * Calls a method of a Node-style object that accepts a Node-style @@ -1480,15 +1890,19 @@ function nbind(callback/*, ... args*/) { * will be provided by Q and appended to these arguments. * @returns a promise for the value or error */ -Q.npost = npost; -function npost(object, name, args) { +Q.nmapply = // XXX As proposed by "Redsandro" +Q.npost = function (object, name, args) { + return Q(object).npost(name, args); +}; + +Promise.prototype.nmapply = // XXX As proposed by "Redsandro" +Promise.prototype.npost = function (name, args) { var nodeArgs = array_slice(args || []); var deferred = defer(); nodeArgs.push(deferred.makeNodeResolver()); - - post(object, name, nodeArgs).fail(deferred.reject); + this.dispatch("post", [name, nodeArgs]).fail(deferred.reject); return deferred.promise; -} +}; /** * Calls a method of a Node-style object that accepts a Node-style @@ -1500,20 +1914,44 @@ function npost(object, name, args) { * be provided by Q and appended to these arguments. * @returns a promise for the value or error */ -Q.nsend = nsend; -Q.ninvoke = Q.nsend; // synonyms -function nsend(object, name /*, ...args*/) { +Q.nsend = // XXX Based on Mark Miller's proposed "send" +Q.nmcall = // XXX Based on "Redsandro's" proposal +Q.ninvoke = function (object, name /*...args*/) { var nodeArgs = array_slice(arguments, 2); var deferred = defer(); nodeArgs.push(deferred.makeNodeResolver()); - post(object, name, nodeArgs).fail(deferred.reject); + Q(object).dispatch("post", [name, nodeArgs]).fail(deferred.reject); return deferred.promise; -} +}; +Promise.prototype.nsend = // XXX Based on Mark Miller's proposed "send" +Promise.prototype.nmcall = // XXX Based on "Redsandro's" proposal +Promise.prototype.ninvoke = function (name /*...args*/) { + var nodeArgs = array_slice(arguments, 1); + var deferred = defer(); + nodeArgs.push(deferred.makeNodeResolver()); + this.dispatch("post", [name, nodeArgs]).fail(deferred.reject); + return deferred.promise; +}; + +/** + * If a function would like to support both Node continuation-passing-style and + * promise-returning-style, it can end its internal promise chain with + * `nodeify(nodeback)`, forwarding the optional nodeback argument. If the user + * elects to use a nodeback, the result will be sent there. If they do not + * pass a nodeback, they will receive the result promise. + * @param object a result (or a promise for a result) + * @param {Function} nodeback a Node.js-style callback + * @returns either the promise or nothing + */ Q.nodeify = nodeify; -function nodeify(promise, nodeback) { +function nodeify(object, nodeback) { + return Q(object).nodeify(nodeback); +} + +Promise.prototype.nodeify = function (nodeback) { if (nodeback) { - promise.then(function (value) { + this.then(function (value) { nextTick(function () { nodeback(null, value); }); @@ -1523,9 +1961,9 @@ function nodeify(promise, nodeback) { }); }); } else { - return promise; + return this; } -} +}; // All code before this point will be filtered from stack traces. var qEndingLine = captureLine();