Saturday, October 06, 2012

WinJS Promise Any and Join

WinJS.Promise.any function will return another Promise object when any of the Promise objects defined in an array completes. The object passed to next Promise function from Promise.Any function has key and value properties. The key property is the array index (zero-based) of the first completed Promise, and the value property is another Promise object which wraps the return of first completed Promise, as demoed in following code snippet:
    function addAsync(number1, number2, delayInSeconds) {
        return new WinJS.Promise(function (c, m, p) {
            setTimeout(function () {
                c(number1 + number2);
            }, delayInSeconds * 1000); 
        });
    }

    function test() {
        var promise1 = addAsync(1, 1, 3);
        var promise2 = addAsync(2, 2, 2);
        var promise3 = addAsync(3, 3, 1);

        WinJS.Promise.any([promise1, promise2, promise3]).then(function (data) {
            var key = data.key;            // key = 2
            var value = data.value;        // value is a Promise
            var realValue = value._value;  // realValue = 6
        });
    }
On the other hand, Promise.join function will wait for all Promises in an array to complete before the then function starts. The same size of array holding each Promise's return will be passed to the then function:
    function addAsync(number1, number2, delayInSeconds) {
        return new WinJS.Promise(function (c, m, p) {
            setTimeout(function () {
                c(number1 + number2);
            }, delayInSeconds * 1000); 
        });
    }

    function test() {
        var promise1 = addAsync(1, 1, 3);
        var promise2 = addAsync(2, 2, 2);
        var promise3 = addAsync(3, 3, 1);

        WinJS.Promise.join([promise1, promise2, promise3]).then(function (data) {
            var promise1Return = data[0];    // promise1Return = 2
            var promise2Return = data[1];    // promise2Return = 4
            var promise3Return = data[2];    // promise3Return = 6
        });
    }
What happens if exception occurs inside the Promise? Let's change a little of the test code:
    function test() {
        var promise1 = addAsync(1, 1, 3);
        var promise2 = addAsync(2, 2, 2).then(function () { throw "promise2 error" });
        var promise3 = addAsync(3, 3, 1);

        WinJS.Promise.any([promise1, promise2, promise3]).done(function () {
            console.log("Promise.any completed");
        }, function (err) {
            console.log("Promise.any err: " + err);
        });

        WinJS.Promise.join([promise1, promise2, promise3]).done(function () {
            console.log("Promise.join completed");
        }, function (err) {
            console.log("Promise.join err: " + err);
        });
    }
The result is:
Promise.any completed
Promise.join err: ,promise2 error
We can see Promise.any function would complete successfully if any of the Promise object is completed without error. But Promise.join would fail and jump to error handler if any Promise object throws exception. There's a comma "," in error message because like the return data in okay case, the errors are wrapped to an array and mapped to the Promise index, in above case: err[0] is undefined and err[1] is "promise2 error" message.

Reference: the source of Promise.any and Promise.join is defined in base.js from WinJS library:
  any: function Promise_any(values) {
        /// <signature helpKeyword="WinJS.Promise.any">
        /// <summary locid="WinJS.Promise.any">
        /// Returns a promise that is fulfilled when one of the input promises
        /// has been fulfilled.
        /// </summary>
        /// <param name="values" type="Array" locid="WinJS.Promise.any_p:values">
        /// An array that contains promise objects or objects whose property
        /// values include promise objects.
        /// </param>
        /// <returns type="WinJS.Promise" locid="WinJS.Promise.any_returnValue">
        /// A promise that on fulfillment yields the value of the input (complete or error).
        /// </returns>
        /// </signature>
        return new Promise(
            function (complete, error, progress) {
                var keys = Object.keys(values);
                var errors = Array.isArray(values) ? [] : {};
                if (keys.length === 0) {
                    complete();
                }
                var canceled = 0;
                keys.forEach(function (key) {
                    Promise.as(values[key]).then(
                        function () { complete({ key: key, value: values[key] }); },
                        function (e) {
                            if (e instanceof Error && e.name === canceledName) {
                                if ((++canceled) === keys.length) {
                                    complete(WinJS.Promise.cancel);
                                }
                                return;
                            }
                            error({ key: key, value: values[key] });
                        }
                    );
                });
            },
            function () {
                var keys = Object.keys(values);
                keys.forEach(function (key) {
                    var promise = Promise.as(values[key]);
                    if (typeof promise.cancel === "function") {
                        promise.cancel();
                    }
                });
            }
        );
    },
  join: function Promise_join(values) {
        /// <signature helpKeyword="WinJS.Promise.join">
        /// <summary locid="WinJS.Promise.join">
        /// Creates a promise that is fulfilled when all the values are fulfilled.
        /// </summary>
        /// <param name="values" type="Object" locid="WinJS.Promise.join_p:values">
        /// An object whose fields contain values, some of which may be promises.
        /// </param>
        /// <returns type="WinJS.Promise" locid="WinJS.Promise.join_returnValue">
        /// A promise whose value is an object with the same field names as those of the object in the values parameter, where
        /// each field value is the fulfilled value of a promise.
        /// </returns>
        /// </signature>
        return new Promise(
            function (complete, error, progress) {
                var keys = Object.keys(values);
                var errors = Array.isArray(values) ? [] : {};
                var results = Array.isArray(values) ? [] : {};
                var undefineds = 0;
                var pending = keys.length;
                var argDone = function (key) {
                    if ((--pending) === 0) {
                        var errorCount = Object.keys(errors).length;
                        if (errorCount === 0) {
                            complete(results);
                        } else {
                            var canceledCount = 0;
                            keys.forEach(function (key) {
                                var e = errors[key];
                                if (e instanceof Error && e.name === canceledName) {
                                    canceledCount++;
                                }
                            });
                            if (canceledCount === errorCount) {
                                complete(WinJS.Promise.cancel);
                            } else {
                                error(errors);
                            }
                        }
                    } else {
                        progress({ Key: key, Done: true });
                    }
                };
                keys.forEach(function (key) {
                    var value = values[key];
                    if (value === undefined) {
                        undefineds++;
                    } else {
                        Promise.then(value,
                            function (value) { results[key] = value; argDone(key); },
                            function (value) { errors[key] = value; argDone(key); }
                        );
                    }
                });
                pending -= undefineds;
                if (pending === 0) {
                    complete(results);
                    return;
                }
            },
            function () {
                Object.keys(values).forEach(function (key) {
                    var promise = Promise.as(values[key]);
                    if (typeof promise.cancel === "function") {
                        promise.cancel();
                    }
                });
            }
        );
    }