Source: data/service/data-stream.js

// Note: Montage's promises are used even if ECMAScript 6 promises are available.
var DataProvider = require("data/service/data-provider").DataProvider,
    DataObjectDescriptor = require("data/model/data-object-descriptor").DataObjectDescriptor,
    DataQuery = require("data/model/data-query").DataQuery,
    Promise = require("core/promise").Promise,
    deprecate = require("core/deprecate"),
    parse = require("frb/parse"),
    Scope = require("frb/scope"),
    compile = require("frb/compile-evaluator");

/**
 * A [DataProvider]{@link DataProvider} whose data is received sequentially.
 * A DataStream is also a [promise]{@linkcode external:Promise} which is
 * fulfilled when all the data it expects has been received.
 *
 * Objects receiving data from a stream will use its
 * [data]{@link DataStream#data} property to access that data. Alternatively
 * they can use its [then()]{@link DataStream#then} method to get that data or
 * to handle errors, or its [catch()]{@link DataStream#catch} method to handle
 * errors.
 *
 * Objects feeding data to a stream will use its
 * [addData()]{@link DataStream#addData} method to add that data and its
 * [dataDone()]{@link DataStream#dataDone} method to indicate that all available
 * data has been added or its [dataError()]{@link DataStream#dataError} method
 * to indicate an error occurred.
 *
 * Objects can either receive data from a stream or add data to it, but not
 * both. Additionally, only one object can ever add data to a particular
 * stream. Typically that object will be a [Service]{@link DataService}.
 *
 * Each stream is also a [promise]{@linkcode external:Promise} that becomes
 * fulfilled when all the data expected for it is first received and
 * [dataDone()]{@link DataStream#dataDone} is called, or rejected when an
 * error is first encountered and [dataError()]{@link DataStream#dataError}
 * is called. Each such promise is fulfilled or rejected only once and will
 * not be fulfilled or rejected again if the stream's data changes or if an
 * error is encountered subsequently for any reason.
 *
 * @class
 * @extends DataProvider
 *
 */
exports.DataStream = DataProvider.specialize(/** @lends DataStream.prototype */ {

    /***************************************************************************
     * Basic properties
     */

    /**
     * The query defining the data returned in this stream.
     *
     * @type {DataQuery}
     */
    query: {
        value: undefined
    },

    /**
     * The selector defining the data returned in this stream.
     *
     * @type {DataQuery}
     */
    selector: {
        get: deprecate.deprecateMethod(void 0, function () {
            return this.query;
        }, "selector", "query"),
        set: deprecate.deprecateMethod(void 0, function (value) {
            this.query = value;
        }, "selector", "query")
    },
    /***************************************************************************
     * DataProvider behavior
     */

    /**
     * The objects that have been added to the stream, as defined in this class'
     * [DataProvider]{@link DataProvider} superclass. This array is created
     * lazilly the first time it is needed and then not allowed to change,
     * though its contents can and typically will change.
     *
     * @type {Array}
     */
    data: {
        get: function() {
            if (!this._data) {
                this._data = [];
            }
            return this._data;
        }
    },

    /**
     * Request specific data, as defined in this class'
     * [DataProvider]{@link DataProvider} superclass. Calling this method has
     * no effect as data will come in the order in which it is added to the
     * stream and this order cannot be changed.
     *
     * @method
     * @argument {int} start  - See [superclass]{@link DataProvider#requestData}.
     * @argument {int} length - See [superclass]{@link DataProvider#requestData}.
     */
    requestData: {
        value: function (start, length) {
            // Don't do anything.
            return this;
        }
    },

    /***************************************************************************
     * Promise behavior
     */

    _resolve: {
        value: function (value) {
            if (!this.__promise) {
                this.__promise = Promise.resolve(value);
            }
        }
    },

    _reject: {
        value: function (reason) {
            // Defers the creation of the rejection promise by setting __promise
            // to a function that will create the appropriate promise when it
            // is needed. This way if the promise is not needed it won't be
            // created. This avoids the "unhandled rejection" error that
            // Montage's Promises logs for promises that are rejected but whose rejection
            // is not handled.
            if (!this.__promise) {
                this.__promise = function () {return Promise.reject(reason);};
            }
        }
    },

    _promise: {
        get: function () {
            var self = this;
            if (typeof this.__promise === "function") {
                this.__promise = this.__promise();
            } else if (!this.__promise) {
                this.__promise = new Promise(function(resolve, reject) {
                    self._resolve = resolve;
                    self._reject = reject;
                });
            }
            return this.__promise;
        }
    },

    /**
     * Method of the [Promise]{@linkcode external:Promise} class used to
     * kick off additional processing when all the data expected by this
     * stream has been received or when an error has been encountered.
     *
     * @method
     * @argument {OnFulfilled} onFulfilled - Called when the stream's
     *                                       [dataDone()]{@link DataStream#dataDone}
     *                                       method is called, usually after all
     *                                       the data expected for the stream
     *                                       has been sent to it. Because a
     *                                       stream's selector can change after
     *                                       that, or changes in the service
     *                                       data can occur for other reasons,
     *                                       it is possible for a stream's
     *                                       [data]{@link DataStream#data} array
     *                                       contents to change after this
     *                                       callback is called. If that happens
     *                                       this callback will not be called
     *                                       again. This callback therefore only
     *                                       provides an indication of when the
     *                                       first set of data expected by a
     *                                       stream is received. The value
     *                                       passed in to this callback is the
     *                                       stream's {@link DataStream#data}.
     * @argument {OnRejected} [onRejected] - Called when the stream's
     *                                       [dataError()]{@link DataStream#dataError}
     *                                       method is called, usually after
     *                                       an error is encountered while
     *                                       fetching data for the stream.
     *                                       The value passed in to this
     *                                       callback will be the `reason`
     *                                       received by the stream's
     *                                       [dataError()]{@link DataStream#dataError}
     *                                       method. Because
     *                                       [catch()]{@link DataStream#catch}
     *                                       also handles the case where
     *                                       exceptions are encountered
     *                                       in the `onFulfilled`
     *                                       callback, this argument is
     *                                       usually not provided and
     *                                       [catch()]{@link DataStream#catch}
     *                                       is usually used instead to specify
     *                                       the `onRejected` callback.
     */
    then: {
        value: function (onFulfilled, onRejected) {
            return this._promise.then(onFulfilled, onRejected);
        }
    },

    /**
     * Method of the [Promise]{@linkcode external:Promise} class used to
     * kick off additional processing when an error has been encountered.
     *
     * @method
     * @argument {OnRejected} onRejected   - Called when the stream's
     *                                       [dataError()]{@link DataStream#dataError}
     *                                       method is called, usually after
     *                                       an error is encountered while
     *                                       fetching data for the stream.
     *                                       The value passed in to this
     *                                       callback will be the `reason`
     *                                       received by the stream's
     *                                       [dataError()]{@link DataStream#dataError}
     *                                       method.
     */
    catch: {
        value: function (onRejected) {
            return this._promise.catch(onRejected);
        }
    },

    /***************************************************************************
     * Feeding the stream
     */

    /**
     * Add some object to the stream's [data]{@link DataStream#data} array.
     *
     * @method
     * @argument {Array} objects - An array of objects to add to the stream's
     *                             data. If this array is empty, `null`, or
     *                             `undefined`, no objects are added.
     */
    addData: {
        value: function (objects) {
            var data = objects;

            if (this.dataExpression && objects) {
                data = this._compiledDataExpression(new Scope(objects));
            }

            if (data && Array.isArray(data)) {
                this.data.push.apply(this.data, data);
            } else if (data) {
                this.data.push(data);
            }
        }
    },

    /**
     * To be called when all the data expected by this stream has been added
     * to its [data]{@link DataStream#data} array. After this is called
     * all subsequent calls to [dataDone()]{@link DataStream#dataDone}
     * or [dataError()]{@link DataStream#dataError} will be ignored.
     *
     * @method
     */
    dataDone: {
        value: function () {
            this._resolve(this.data);
            delete this._resolve;
            delete this._reject;
        }
    },

    /**
     * To be called when a problem is encountered while trying to
     * fetch data for this stream. After this is called all subsequent
     * calls to [dataError()]{@link DataStream#dataError} or
     * [dataDone()]{@link DataStream#dataDone} will be ignored.
     *
     * @method
     * @argument {Object} [reason] - An object, usually an {@link Error},
     *                               indicating what caused the problem.
     *                               This will be passed in to any
     *                               {@link external:onRejected}
     *                               callback specified in
     *                               [then()]{@link DataStream#then} or
     *                               [catch()]{@link DataStream#catch} calls
     *                               to the stream.
     */
    dataError: {
        value: function (reason) {
            this._reject(reason);
            delete this._reject;
            delete this._resolve;
        }
    },

    _compiledDataExpression: {
        get: function () {
            return this.__compiledDataExpression || (this.__compiledDataExpression = compile(this._dataExpressionSyntax));
        }
    },

    _dataExpressionSyntax: {
        get: function () {
            return this.__dataExpressionSyntax || (this.__dataExpressionSyntax = parse(this.dataExpression));
        }
    },

    dataExpression: {
        value: undefined
    },

    evaluateDataExpression: {
        value: function(value) {
            return this._compiledDataExpression(value);
        }
    },

    /**
     * The time at which data was received by the DataStream
     *
     * @type {Date}
     */
    dataReceptionTime: {
        value: undefined
    },

    /**
     * The maximum amount of time a DataStream's data will be considered fresh.
     * This should take precedence over an ObjectDescriptor's maxAge which should
     * take precedence over a DataService's dataMaxAge global default value.
     *
     * @type {Number}
     */
    _dataMaxAge: {
        value: undefined
    },
    dataMaxAge: {
        get: function() {
            //The third default should be the service's dataMaxAge, but:
            //DataService.[mainService||rootService].dataServiceForDataStream(this) should work
            //but is maintained on a per DataService basis and there's no cascading lookup.
            //#FixMe So we need to fix this as a DataStream doesn't know which service created it.
            return this._dataMaxAge || this.query.type.maxAge;
        },
        set: function(value) {
            this._dataMaxAge = value;
        }
    }


}, /** @lends DataStream */ {

    /**
     * @todo Document.
     */
    withTypeOrSelector: {
        value: function (typeOrSelector) {
            var type = typeOrSelector instanceof DataObjectDescriptor && typeOrSelector,
                selector = type && DataQuery.withTypeAndCriteria(type) || typeOrSelector,
                stream = new this();
            stream.query = selector;
            return stream;
        }
    }

});