GeoKnow/Jassa-Core

View on GitHub
lib/util/PromiseUtils.js

Summary

Maintainability
C
1 day
Test Coverage
var shared = require('./shared');
var Promise = shared.Promise;

var ObjectUtils = require('./ObjectUtils');

var PromiseUtils = {

    /**
     * Set a new service as an attribute of an object, thereby cancelling
     * all prior requests
     */
    replaceService: function(obj, attr, newRawService) {

        var oldService = obj[attr];
        if(oldService && oldService.cancelAll) {
            oldService.cancelAll();
        }

        obj[attr] = newRawService != null ? PromiseUtils.lastRequestify(newRawService) : null;
    },

    /**
     * Takes an object and creates a new one where each method is wrapped
     * such that sucessive calls will cancel prior promises yeld by that method
     *
     * A cancelAll method to cancell all active requests is provided as well
     */
    lastRequestify: function(obj) {
        var result = {};

        var bind = function(val, obj) {
            var r = function() {
                var s = val.apply(obj, arguments);
                return s;
            };
            return r;
        };

        var fns = [];
        for(var key in obj) {
            var val = obj[key];

            if(ObjectUtils.isFunction(val)) {
                var fn = PromiseUtils.lastRequest(bind(val, obj));

                result[key] = fn;

                fns.push(fn);
            }
        }

        result.cancelAll = function() {
            fns.forEach(function(fn) {
                try {
                    if(fn.deferred) {
                        fn.deferred.cancel();
                    }
                } catch(e) {
                    console.log(e);
                }
            });
        };

        return result;
    },

    all: function(promises) {
        var r = Promise.all(promises)
            .cancellable()
            ['catch'](Promise.CancellationError, function(e) {
                promises.forEach(function(promise) {
                    try {
                        if(promise && promise.cancel) {
                            promise.cancel();
                        }
                    } catch(x) {
                        console.log('[ERROR] Cancelling a promise raised an exception');
                    }
                });

                throw e;
            });

        return r;
    },


    createDeferred: function(isCancellable) {
        var _resolve;
        var _reject;

        var promise = new Promise(function() {
            _resolve = arguments[0];
            _reject = arguments[1];
        });

        if(isCancellable) {
            promise.cancellable();
        }

        return {
            resolve: _resolve,
            reject: _reject,
            promise: function() { return promise; },
            isCancellable: function() {
                var r;

                if(promise.isCancellable) {
                    r = promise.isCancellable();
                } else if(promise.abort) {
                    r = true;
                }

                return r;
            },
            cancel: function() {
                if(promise.cancel) {
                    promise.cancel();
                } else if(promise.abort) {
                    promise.abort();
                }
            }
        };
    },

    defaultAbortFn: function(deferred) {
        deferred.cancel();
    },

    defaultDeferredFn: function() {
        return PromiseUtils.createDeferred(true);
    },


    /**
     * Wraps a promise supplier function (i.e. a function returning a promise)
     * that delays between requests
     */
    postpone: function(promiseSupplierFn, ms, abortFn, deferredFactoryFn) {

        ms = ms == null ? 50 : ms;
        abortFn = abortFn || this.defaultAbortFn;
        deferredFactoryFn = deferredFactoryFn || PromiseUtils.defaultDeferredFn;

        return function() {
            var args = arguments;
            var deferred = deferredFactoryFn();
            var running = null;

            var timeout = setTimeout(function() {
                running = promiseSupplierFn.apply(this, args);
                running.then(function() {
                    deferred.resolve.apply(this, arguments);
                }).fail(function() {
                    deferred.reject.apply(this, arguments);
                });
            }, ms);

            var result = deferred.promise();
            result.abort = function() {
                if(running == null) {
                    clearTimeout(timeout);
                } else if(abortFn != null) {
                    abortFn(running);
                }
            };

            return result;
        };
    },

    /**
     * Returns a function returning a promise that wraps a function returning promises.
     * Only the resolution of the most recently created promise will be resolved.
     */
    lastRequest: function(promiseSupplierFn, abortFn, deferredFactoryFn) {
        abortFn = abortFn || PromiseUtils.defaultAbortFn;
        deferredFactoryFn = deferredFactoryFn || PromiseUtils.defaultDeferredFn;

        var deferred = null;
        var r;
        var prior = null;
        var counter = 0;


        return function() {
            if(deferred == null) {
                deferred = deferredFactoryFn(); // jQuery.Deferred()

                r = deferred.promise();

                // Overwrite the deferred's 'promise's cancel method to cancel the underlying promise
                var originalCancelFn = r.cancel;
                r.cancel = function() {
                    if(originalCancelFn) {
                        originalCancelFn.call(r);
                    }

                    if(prior && deferred.isCancellable()) {
                        prior.cancel();
                    }
                };
            }

            this.deferred = deferred;

            //var args = arguments;

            var now = ++counter;
            //console.log('now ' + now + ' for ', args);
            var next = promiseSupplierFn.apply(this, arguments);

            if(abortFn != null && prior != null) {
                abortFn(prior);
            }
            prior = next;

            next.then(function() {
                if(now === counter && deferred) {
                    //console.log('resolved' + now + ' for ', args);
                    deferred.resolve.apply(this, arguments);
                    deferred = null;
                }
            }, function() {
                if(now === counter && deferred) {
                    //console.log('rejected' + now + ' for ', args);
                    deferred.reject.apply(this, arguments);
                    deferred = null;
                }
            });


            return r;
        };

    },

    /**
     * Does not request a further promise from the promise supplier as long
     * as a prior one is still running.
     */
    firstRequest: function(promiseSupplierFn, deferredFactoryFn) {
        var r = null;

        return function() {
            if(r == null) {
                var deferred = deferredFactoryFn(); // jQuery.Deferred()
                r = deferred.promise();

                var first = promiseSupplierFn.apply(this, arguments);

                // Overwrite the deferred's 'promise's cancel method to cancel the underlying promise
                var originalCancelFn = r.cancel;
                r.cancel = function() {
                    if(originalCancelFn) {
                        originalCancelFn.call(r);
                    }

                    if(first && deferred.isCancellable()) {
                        first.cancel();
                    }
                };


                first.then(function() {
                    if(deferred) {
                        deferred.resolve.apply(this, arguments);
                        r = null;
                    }
                }, function() {
                    if(deferred) {
                        deferred.reject.apply(this, arguments);
                        r = null;
                    }
                });
            }

            return r; //deferred.promise();
        };
    },
};

module.exports = PromiseUtils;