UNPKG

falcor

Version:

A JavaScript library for efficient data fetching.

1,517 lines (1,275 loc) 292 kB
/*! * Copyright 2020 Netflix, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing * permissions and limitations under the License. */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.falcor = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ var falcor = require(33); var jsong = require(120); falcor.atom = jsong.atom; falcor.ref = jsong.ref; falcor.error = jsong.error; falcor.pathValue = jsong.pathValue; falcor.HttpDataSource = require(115); module.exports = falcor; },{"115":115,"120":120,"33":33}],2:[function(require,module,exports){ var ModelRoot = require(4); var ModelDataSourceAdapter = require(3); var RequestQueue = require(43); var ModelResponse = require(51); var CallResponse = require(49); var InvalidateResponse = require(50); var TimeoutScheduler = require(65); var ImmediateScheduler = require(64); var collectLru = require(39); var pathSyntax = require(124); var getSize = require(77); var isObject = require(89); var isPrimitive = require(91); var isJSONEnvelope = require(87); var isJSONGraphEnvelope = require(88); var setCache = require(67); var setJSONGraphs = require(66); var jsong = require(120); var ID = 0; var validateInput = require(105); var noOp = function() {}; var getCache = require(18); var get = require(23); var GET_VALID_INPUT = require(58); module.exports = Model; Model.ref = jsong.ref; Model.atom = jsong.atom; Model.error = jsong.error; Model.pathValue = jsong.pathValue; /** * This callback is invoked when the Model's cache is changed. * @callback Model~onChange */ /** * This function is invoked on every JSONGraph Error retrieved from the DataSource. This function allows Error objects * to be transformed before being stored in the Model's cache. * @callback Model~errorSelector * @param {Object} jsonGraphError - the JSONGraph Error object to transform before it is stored in the Model's cache. * @returns {Object} the JSONGraph Error object to store in the Model cache. */ /** * This function is invoked every time a value in the Model cache is about to be replaced with a new value. If the * function returns true, the existing value is replaced with a new value and the version flag on all of the value's * ancestors in the tree are incremented. * @callback Model~comparator * @param {Object} existingValue - the current value in the Model cache. * @param {Object} newValue - the value about to be set into the Model cache. * @returns {Boolean} the Boolean value indicating whether the new value and the existing value are equal. */ /** * @typedef {Object} Options * @property {DataSource} [source] A data source to retrieve and manage the {@link JSONGraph} * @property {JSONGraph} [cache] Initial state of the {@link JSONGraph} * @property {number} [maxSize] The maximum size of the cache before cache pruning is performed. The unit of this value * depends on the algorithm used to calculate the `$size` field on graph nodes by the backing source for the Model's * DataSource. If no DataSource is used, or the DataSource does not provide `$size` values, a naive algorithm is used * where the cache size is calculated in terms of graph node count and, for arrays and strings, element count. * @property {number} [collectRatio] The ratio of the maximum size to collect when the maxSize is exceeded. * @property {number} [maxRetries] The maximum number of times that the Model will attempt to retrieve the value from * its DataSource. Defaults to `3`. * @property {Model~errorSelector} [errorSelector] A function used to translate errors before they are returned * @property {Model~onChange} [onChange] A function called whenever the Model's cache is changed * @property {Model~comparator} [comparator] A function called whenever a value in the Model's cache is about to be * replaced with a new value. * @property {boolean} [disablePathCollapse] Disables the algorithm that collapses paths on GET requests. The algorithm * is enabled by default. This is a relatively computationally expensive feature. * @property {boolean} [disableRequestDeduplication] Disables the algorithm that deduplicates paths across in-flight GET * requests. The algorithm is enabled by default. This is a computationally expensive feature. */ /** * A Model object is used to execute commands against a {@link JSONGraph} object. {@link Model}s can work with a local JSONGraph cache, or it can work with a remote {@link JSONGraph} object through a {@link DataSource}. * @constructor * @param {Options} [o] - a set of options to customize behavior */ function Model(o) { var options = o || {}; this._root = options._root || new ModelRoot(options); this._path = options.path || options._path || []; this._source = options.source || options._source; this._request = options.request || options._request || new RequestQueue(this, options.scheduler || new ImmediateScheduler()); this._ID = ID++; if (typeof options.maxSize === "number") { this._maxSize = options.maxSize; } else { this._maxSize = options._maxSize || Model.prototype._maxSize; } if (typeof options.maxRetries === "number") { this._maxRetries = options.maxRetries; } else { this._maxRetries = options._maxRetries || Model.prototype._maxRetries; } if (typeof options.collectRatio === "number") { this._collectRatio = options.collectRatio; } else { this._collectRatio = options._collectRatio || Model.prototype._collectRatio; } if (options.boxed || options.hasOwnProperty("_boxed")) { this._boxed = options.boxed || options._boxed; } if (options.materialized || options.hasOwnProperty("_materialized")) { this._materialized = options.materialized || options._materialized; } if (typeof options.treatErrorsAsValues === "boolean") { this._treatErrorsAsValues = options.treatErrorsAsValues; } else if (options.hasOwnProperty("_treatErrorsAsValues")) { this._treatErrorsAsValues = options._treatErrorsAsValues; } else { this._treatErrorsAsValues = false; } if (typeof options.disablePathCollapse === "boolean") { this._enablePathCollapse = !options.disablePathCollapse; } else if (options.hasOwnProperty("_enablePathCollapse")) { this._enablePathCollapse = options._enablePathCollapse; } else { this._enablePathCollapse = true; } if (typeof options.disableRequestDeduplication === "boolean") { this._enableRequestDeduplication = !options.disableRequestDeduplication; } else if (options.hasOwnProperty("_enableRequestDeduplication")) { this._enableRequestDeduplication = options._enableRequestDeduplication; } else { this._enableRequestDeduplication = true; } this._useServerPaths = options._useServerPaths || false; this._allowFromWhenceYouCame = options.allowFromWhenceYouCame || options._allowFromWhenceYouCame || false; this._treatDataSourceErrorsAsJSONGraphErrors = options._treatDataSourceErrorsAsJSONGraphErrors || false; if (options.cache) { this.setCache(options.cache); } } Model.prototype.constructor = Model; Model.prototype._materialized = false; Model.prototype._boxed = false; Model.prototype._progressive = false; Model.prototype._treatErrorsAsValues = false; Model.prototype._maxSize = Math.pow(2, 53) - 1; Model.prototype._maxRetries = 3; Model.prototype._collectRatio = 0.75; Model.prototype._enablePathCollapse = true; Model.prototype._enableRequestDeduplication = true; /** * The get method retrieves several {@link Path}s or {@link PathSet}s from a {@link Model}. The get method loads each value into a JSON object and returns in a ModelResponse. * @function * @param {...PathSet} path - the path(s) to retrieve * @return {ModelResponse.<JSONEnvelope>} - the requested data as JSON */ Model.prototype.get = require(57); /** * _getOptimizedBoundPath is an extension point for internal users to polyfill * legacy soft-bind behavior, as opposed to deref (hardBind). Current falcor * only supports deref, and assumes _path to be a fully optimized path. * @function * @private * @return {Path} - fully optimized bound path for the model */ Model.prototype._getOptimizedBoundPath = function _getOptimizedBoundPath() { return this._path ? this._path.slice() : this._path; }; /** * The get method retrieves several {@link Path}s or {@link PathSet}s from a {@link Model}. The get method loads each value into a JSON object and returns in a ModelResponse. * @function * @private * @param {Array.<PathSet>} paths - the path(s) to retrieve * @return {ModelResponse.<JSONEnvelope>} - the requested data as JSON */ Model.prototype._getWithPaths = require(56); /** * Sets the value at one or more places in the JSONGraph model. The set method accepts one or more {@link PathValue}s, each of which is a combination of a location in the document and the value to place there. In addition to accepting {@link PathValue}s, the set method also returns the values after the set operation is complete. * @function * @return {ModelResponse.<JSONEnvelope>} - an {@link Observable} stream containing the values in the JSONGraph model after the set was attempted */ Model.prototype.set = require(60); /** * The preload method retrieves several {@link Path}s or {@link PathSet}s from a {@link Model} and loads them into the Model cache. * @function * @param {...PathSet} path - the path(s) to retrieve * @return {ModelResponse.<JSONEnvelope>} - a ModelResponse that completes when the data has been loaded into the cache. */ Model.prototype.preload = function preload() { var out = validateInput(arguments, GET_VALID_INPUT, "preload"); if (out !== true) { return new ModelResponse(function(o) { o.onError(out); }); } var args = Array.prototype.slice.call(arguments); var self = this; return new ModelResponse(function(obs) { return self.get.apply(self, args).subscribe( function() {}, function(err) { obs.onError(err); }, function() { obs.onCompleted(); } ); }); }; /** * Invokes a function in the JSON Graph. * @function * @param {Path} functionPath - the path to the function to invoke * @param {Array.<Object>} args - the arguments to pass to the function * @param {Array.<PathSet>} refPaths - the paths to retrieve from the JSON Graph References in the message returned from the function * @param {Array.<PathSet>} extraPaths - additional paths to retrieve after successful function execution * @return {ModelResponse.<JSONEnvelope> - a JSONEnvelope contains the values returned from the function */ Model.prototype.call = function call() { var args; var argsIdx = -1; var argsLen = arguments.length; args = new Array(argsLen); while (++argsIdx < argsLen) { var arg = arguments[argsIdx]; args[argsIdx] = arg; var argType = typeof arg; if ( (argsIdx > 1 && !Array.isArray(arg)) || (argsIdx === 0 && !Array.isArray(arg) && argType !== "string") || (argsIdx === 1 && !Array.isArray(arg) && !isPrimitive(arg)) ) { /* eslint-disable no-loop-func */ return new ModelResponse(function(o) { o.onError(new Error("Invalid argument")); }); /* eslint-enable no-loop-func */ } } return new CallResponse(this, args[0], args[1], args[2], args[3]); }; /** * The invalidate method synchronously removes several {@link Path}s or {@link PathSet}s from a {@link Model} cache. * @function * @param {...PathSet} path - the paths to remove from the {@link Model}'s cache. */ Model.prototype.invalidate = function invalidate() { var args; var argsIdx = -1; var argsLen = arguments.length; args = []; while (++argsIdx < argsLen) { args[argsIdx] = pathSyntax.fromPath(arguments[argsIdx]); if (!Array.isArray(args[argsIdx]) || !args[argsIdx].length) { throw new Error("Invalid argument"); } } // creates the obs, subscribes and will throw the errors if encountered. new InvalidateResponse(this, args).subscribe(noOp, function(e) { throw e; }); }; /** * Returns a new {@link Model} bound to a location within the {@link * JSONGraph}. The bound location is never a {@link Reference}: any {@link * Reference}s encountered while resolving the bound {@link Path} are always * replaced with the {@link Reference}s target value. For subsequent operations * on the {@link Model}, all paths will be evaluated relative to the bound * path. Deref allows you to: * - Expose only a fragment of the {@link JSONGraph} to components, rather than * the entire graph * - Hide the location of a {@link JSONGraph} fragment from components * - Optimize for executing multiple operations and path looksup at/below the * same location in the {@link JSONGraph} * @method * @param {Object} responseObject - an object previously retrieved from the * Model * @return {Model} - the dereferenced {@link Model} * @example var Model = falcor.Model; var model = new Model({ cache: { users: [ Model.ref(["usersById", 32]) ], usersById: { 32: { name: "Steve", surname: "McGuire" } } } }); model. get(['users', 0, 'name']). subscribe(function(jsonEnv) { var userModel = model.deref(jsonEnv.json.users[0]); console.log(model.getPath()); console.log(userModel.getPath()); }); }); // prints the following: // [] // ["usersById", 32] - because userModel refers to target of reference at ["users", 0] */ Model.prototype.deref = require(6); /** * A dereferenced model can become invalid when the reference from which it was * built has been removed/collected/expired/etc etc. To fix the issue, a from * the parent request should be made (no parent, then from the root) for a valid * path and re-dereference performed to update what the model is bound too. * * @method * @private * @return {Boolean} - If the currently deref'd model is still considered a * valid deref. */ Model.prototype._hasValidParentReference = require(5); /** * Get data for a single {@link Path}. * @param {Path} path - the path to retrieve * @return {Observable.<*>} - the value for the path * @example var model = new falcor.Model({source: new HttpDataSource("/model.json") }); model. getValue('user.name'). subscribe(function(name) { console.log(name); }); // The code above prints "Jim" to the console. */ Model.prototype.getValue = require(20); /** * Set value for a single {@link Path}. * @param {Path} path - the path to set * @param {Object} value - the value to set * @return {Observable.<*>} - the value for the path * @example var model = new falcor.Model({source: new HttpDataSource("/model.json") }); model. setValue('user.name', 'Jim'). subscribe(function(name) { console.log(name); }); // The code above prints "Jim" to the console. */ Model.prototype.setValue = require(69); // TODO: Does not throw if given a PathSet rather than a Path, not sure if it should or not. // TODO: Doc not accurate? I was able to invoke directly against the Model, perhaps because I don't have a data source? // TODO: Not clear on what it means to "retrieve objects in addition to JSONGraph values" /** * Synchronously retrieves a single path from the local {@link Model} only and will not retrieve missing paths from the {@link DataSource}. This method can only be invoked when the {@link Model} does not have a {@link DataSource} or from within a selector function. See {@link Model.prototype.get}. The getValueSync method differs from the asynchronous get methods (ex. get, getValues) in that it can be used to retrieve objects in addition to JSONGraph values. * @method * @private * @arg {Path} path - the path to retrieve * @return {*} - the value for the specified path */ Model.prototype._getValueSync = require(28); /** * @private */ Model.prototype._setValueSync = require(70); /** * @private */ Model.prototype._derefSync = require(7); /** * Set the local cache to a {@link JSONGraph} fragment. This method can be a useful way of mocking a remote document, or restoring the local cache from a previously stored state. * @param {JSONGraph} jsonGraph - the {@link JSONGraph} fragment to use as the local cache */ Model.prototype.setCache = function modelSetCache(cacheOrJSONGraphEnvelope) { var cache = this._root.cache; if (cacheOrJSONGraphEnvelope !== cache) { var modelRoot = this._root; var boundPath = this._path; this._path = []; this._root.cache = {}; if (typeof cache !== "undefined") { collectLru(modelRoot, modelRoot.expired, getSize(cache), 0); } var out; if (isJSONGraphEnvelope(cacheOrJSONGraphEnvelope)) { out = setJSONGraphs(this, [cacheOrJSONGraphEnvelope])[0]; } else if (isJSONEnvelope(cacheOrJSONGraphEnvelope)) { out = setCache(this, [cacheOrJSONGraphEnvelope])[0]; } else if (isObject(cacheOrJSONGraphEnvelope)) { out = setCache(this, [{ json: cacheOrJSONGraphEnvelope }])[0]; } // performs promotion without producing output. if (out) { get.getWithPathsAsPathMap(this, out, []); } this._path = boundPath; } else if (typeof cache === "undefined") { this._root.cache = {}; } return this; }; /** * Get the local {@link JSONGraph} cache. This method can be a useful to store the state of the cache. * @param {...Array.<PathSet>} [pathSets] - The path(s) to retrieve. If no paths are specified, the entire {@link JSONGraph} is returned. * @return {JSONGraph} all of the {@link JSONGraph} data in the {@link Model} cache. * @example // Storing the boxshot of the first 10 titles in the first 10 genreLists to local storage. localStorage.setItem('cache', JSON.stringify(model.getCache("genreLists[0...10][0...10].boxshot"))); */ Model.prototype.getCache = function _getCache() { var paths = Array.prototype.slice.call(arguments); if (paths.length === 0) { return getCache(this._root.cache); } var result = [{}]; var path = this._path; get.getWithPathsAsJSONGraph(this, paths, result); this._path = path; return result[0].jsonGraph; }; /** * Reset cache maxSize. When the new maxSize is smaller than the old force a collect. * @param {Number} maxSize - the new maximum cache size */ Model.prototype._setMaxSize = function setMaxSize(maxSize) { var oldMaxSize = this._maxSize; this._maxSize = maxSize; if (maxSize < oldMaxSize) { var modelRoot = this._root; var modelCache = modelRoot.cache; // eslint-disable-next-line no-cond-assign var currentVersion = modelCache.$_version; collectLru( modelRoot, modelRoot.expired, getSize(modelCache), this._maxSize, this._collectRatio, currentVersion ); } }; /** * Retrieves a number which is incremented every single time a value is changed underneath the Model or the object at an optionally-provided Path beneath the Model. * @param {Path?} path - a path at which to retrieve the version number * @return {Number} a version number which changes whenever a value is changed underneath the Model or provided Path */ Model.prototype.getVersion = function getVersion(pathArg) { var path = (pathArg && pathSyntax.fromPath(pathArg)) || []; if (Array.isArray(path) === false) { throw new Error("Model#getVersion must be called with an Array path."); } if (this._path.length) { path = this._path.concat(path); } return this._getVersion(this, path); }; Model.prototype._syncCheck = function syncCheck(name) { if (Boolean(this._source) && this._root.syncRefCount <= 0 && this._root.unsafeMode === false) { throw new Error("Model#" + name + " may only be called within the context of a request selector."); } return true; }; /* eslint-disable guard-for-in */ Model.prototype._clone = function cloneModel(opts) { var clone = new this.constructor(this); for (var key in opts) { var value = opts[key]; if (value === "delete") { delete clone[key]; } else { clone[key] = value; } } clone.setCache = void 0; return clone; }; /* eslint-enable */ /** * Returns a clone of the {@link Model} that enables batching. Within the configured time period, * paths for get operations are collected and sent to the {@link DataSource} in a batch. Batching * can be more efficient if the {@link DataSource} access the network, potentially reducing the * number of HTTP requests to the server. * * @param {?Scheduler|number} schedulerOrDelay - Either a {@link Scheduler} that determines when to * send a batch to the {@link DataSource}, or the number in milliseconds to collect a batch before * sending to the {@link DataSource}. If this parameter is omitted, then batch collection ends at * the end of the next tick. * @return {Model} a Model which schedules a batch of get requests to the DataSource. */ Model.prototype.batch = function batch(schedulerOrDelay) { var scheduler; if (typeof schedulerOrDelay === "number") { scheduler = new TimeoutScheduler(Math.round(Math.abs(schedulerOrDelay))); } else if (!schedulerOrDelay || !schedulerOrDelay.schedule) { scheduler = new TimeoutScheduler(1); } else { scheduler = schedulerOrDelay; } var clone = this._clone(); clone._request = new RequestQueue(clone, scheduler); return clone; }; /** * Returns a clone of the {@link Model} that disables batching. This is the default mode. Each get operation will be executed on the {@link DataSource} separately. * @name unbatch * @memberof Model.prototype * @function * @return {Model} a {@link Model} that batches requests of the same type and sends them to the data source together */ Model.prototype.unbatch = function unbatch() { var clone = this._clone(); clone._request = new RequestQueue(clone, new ImmediateScheduler()); return clone; }; /** * Returns a clone of the {@link Model} that treats errors as values. Errors will be reported in the same callback used to report data. Errors will appear as objects in responses, rather than being sent to the {@link Observable~onErrorCallback} callback of the {@link ModelResponse}. * @return {Model} */ Model.prototype.treatErrorsAsValues = function treatErrorsAsValues() { return this._clone({ _treatErrorsAsValues: true }); }; /** * Adapts a Model to the {@link DataSource} interface. * @return {DataSource} * @example var model = new falcor.Model({ cache: { user: { name: "Steve", surname: "McGuire" } } }), proxyModel = new falcor.Model({ source: model.asDataSource() }); // Prints "Steve" proxyModel.getValue("user.name"). then(function(name) { console.log(name); }); */ Model.prototype.asDataSource = function asDataSource() { return new ModelDataSourceAdapter(this); }; Model.prototype._materialize = function materialize() { return this._clone({ _materialized: true }); }; Model.prototype._dematerialize = function dematerialize() { return this._clone({ _materialized: "delete" }); }; /** * Returns a clone of the {@link Model} that boxes values returning the wrapper ({@link Atom}, {@link Reference}, or {@link Error}), rather than the value inside it. This allows any metadata attached to the wrapper to be inspected. * @return {Model} */ Model.prototype.boxValues = function boxValues() { return this._clone({ _boxed: true }); }; /** * Returns a clone of the {@link Model} that unboxes values, returning the value inside of the wrapper ({@link Atom}, {@link Reference}, or {@link Error}), rather than the wrapper itself. This is the default mode. * @return {Model} */ Model.prototype.unboxValues = function unboxValues() { return this._clone({ _boxed: "delete" }); }; /** * Returns a clone of the {@link Model} that only uses the local {@link JSONGraph} and never uses a {@link DataSource} to retrieve missing paths. * @return {Model} */ Model.prototype.withoutDataSource = function withoutDataSource() { return this._clone({ _source: "delete" }); }; Model.prototype.toJSON = function toJSON() { return { $type: "ref", value: this._path }; }; /** * Returns the {@link Path} to the object within the JSON Graph that this Model references. * @return {Path} * @example var Model = falcor.Model; var model = new Model({ cache: { users: [ Model.ref(["usersById", 32]) ], usersById: { 32: { name: "Steve", surname: "McGuire" } } } }); model. get(['users', 0, 'name']). subscribe(function(jsonEnv) { var userModel = model.deref(jsonEnv.json.users[0]); console.log(model.getPath()); console.log(userModel.getPath()); }); }); // prints the following: // [] // ["usersById", 32] - because userModel refers to target of reference at ["users", 0] */ Model.prototype.getPath = function getPath() { return this._path ? this._path.slice() : this._path; }; /** * This one is actually private. I would not use this without talking to * jhusain, sdesai, or michaelbpaulson (github). * @private */ Model.prototype._fromWhenceYouCame = function fromWhenceYouCame(allow) { return this._clone({ _allowFromWhenceYouCame: allow === undefined ? true : allow }); }; Model.prototype._getBoundValue = require(17); Model.prototype._getVersion = require(22); Model.prototype._getPathValuesAsPathMap = get.getWithPathsAsPathMap; Model.prototype._getPathValuesAsJSONG = get.getWithPathsAsJSONGraph; Model.prototype._setPathValues = require(68); Model.prototype._setPathMaps = require(67); Model.prototype._setJSONGs = require(66); Model.prototype._setCache = require(67); Model.prototype._invalidatePathValues = require(38); Model.prototype._invalidatePathMaps = require(37); },{"105":105,"120":120,"124":124,"17":17,"18":18,"20":20,"22":22,"23":23,"28":28,"3":3,"37":37,"38":38,"39":39,"4":4,"43":43,"49":49,"5":5,"50":50,"51":51,"56":56,"57":57,"58":58,"6":6,"60":60,"64":64,"65":65,"66":66,"67":67,"68":68,"69":69,"7":7,"70":70,"77":77,"87":87,"88":88,"89":89,"91":91}],3:[function(require,module,exports){ function ModelDataSourceAdapter(model) { this._model = model._materialize().treatErrorsAsValues(); } ModelDataSourceAdapter.prototype.get = function get(pathSets) { return this._model.get.apply(this._model, pathSets)._toJSONG(); }; ModelDataSourceAdapter.prototype.set = function set(jsongResponse) { return this._model.set(jsongResponse)._toJSONG(); }; ModelDataSourceAdapter.prototype.call = function call(path, args, suffixes, paths) { var params = [path, args, suffixes]; Array.prototype.push.apply(params, paths); return this._model.call.apply(this._model, params)._toJSONG(); }; module.exports = ModelDataSourceAdapter; },{}],4:[function(require,module,exports){ var isFunction = require(85); var hasOwn = require(80); function ModelRoot(o) { var options = o || {}; this.syncRefCount = 0; this.expired = options.expired || []; this.unsafeMode = options.unsafeMode || false; this.cache = {}; if (isFunction(options.comparator)) { this.comparator = options.comparator; } if (isFunction(options.errorSelector)) { this.errorSelector = options.errorSelector; } if (isFunction(options.onChange)) { this.onChange = options.onChange; } } ModelRoot.prototype.errorSelector = function errorSelector(x, y) { return y; }; ModelRoot.prototype.comparator = function comparator(cacheNode, messageNode) { if (hasOwn(cacheNode, "value") && hasOwn(messageNode, "value")) { // They are the same only if the following fields are the same. return cacheNode.value === messageNode.value && cacheNode.$type === messageNode.$type && cacheNode.$expires === messageNode.$expires; } return cacheNode === messageNode; }; module.exports = ModelRoot; },{"80":80,"85":85}],5:[function(require,module,exports){ module.exports = function fromWhenceYeCame() { var reference = this._referenceContainer; // Always true when this mode is false. if (!this._allowFromWhenceYouCame) { return true; } // If fromWhenceYouCame is true and the first set of keys did not have // a reference, this case can happen. They are always valid. if (reference === true) { return true; } // was invalid before even derefing. if (reference === false) { return false; } // Its been disconnected (set over or collected) from the graph. // eslint-disable-next-line camelcase if (reference && reference.$_parent === undefined) { return false; } // The reference has expired but has not been collected from the graph. // eslint-disable-next-line camelcase if (reference && reference.$_invalidated) { return false; } return true; }; },{}],6:[function(require,module,exports){ var InvalidDerefInputError = require(9); var getCachePosition = require(19); var CONTAINER_DOES_NOT_EXIST = "e"; var $ref = require(110); module.exports = function deref(boundJSONArg) { var absolutePath = boundJSONArg && boundJSONArg.$__path; var refPath = boundJSONArg && boundJSONArg.$__refPath; var toReference = boundJSONArg && boundJSONArg.$__toReference; var referenceContainer; // We deref and then ensure that the reference container is attached to // the model. if (absolutePath) { var validContainer = CONTAINER_DOES_NOT_EXIST; if (toReference) { validContainer = false; referenceContainer = getCachePosition(this, toReference); // If the reference container is still a sentinel value then compare // the reference value with refPath. If they are the same, then the // model is still valid. if (refPath && referenceContainer && referenceContainer.$type === $ref) { var containerPath = referenceContainer.value; var i = 0; var len = refPath.length; validContainer = true; for (; validContainer && i < len; ++i) { if (containerPath[i] !== refPath[i]) { validContainer = false; } } } } // Signal to the deref'd model that it has been disconnected from the // graph or there is no _fromWhenceYouCame if (!validContainer) { referenceContainer = false; } // The container did not exist, therefore there is no reference // container and fromWhenceYouCame should always return true. else if (validContainer === CONTAINER_DOES_NOT_EXIST) { referenceContainer = true; } return this._clone({ _path: absolutePath, _referenceContainer: referenceContainer }); } throw new InvalidDerefInputError(); }; },{"110":110,"19":19,"9":9}],7:[function(require,module,exports){ var pathSyntax = require(124); var getBoundValue = require(17); var InvalidModelError = require(10); module.exports = function derefSync(boundPathArg) { var boundPath = pathSyntax.fromPath(boundPathArg); if (!Array.isArray(boundPath)) { throw new Error("Model#derefSync must be called with an Array path."); } var boundValue = getBoundValue(this, this._path.concat(boundPath), false); var path = boundValue.path; var node = boundValue.value; var found = boundValue.found; // If the node is not found or the node is found but undefined is returned, // this happens when a reference is expired. if (!found || node === undefined) { return undefined; } if (node.$type) { throw new InvalidModelError(path, path); } return this._clone({ _path: path }); }; },{"10":10,"124":124,"17":17}],8:[function(require,module,exports){ var applyErrorPrototype = require(14); /** * When a bound model attempts to retrieve JSONGraph it should throw an * error. * * @private */ function BoundJSONGraphModelError() { var instance = new Error("It is not legal to use the JSON Graph " + "format from a bound Model. JSON Graph format" + " can only be used from a root model."); instance.name = "BoundJSONGraphModelError"; if (Object.setPrototypeOf) { Object.setPrototypeOf(instance, Object.getPrototypeOf(this)); } if (Error.captureStackTrace) { Error.captureStackTrace(instance, BoundJSONGraphModelError); } return instance; } applyErrorPrototype(BoundJSONGraphModelError); module.exports = BoundJSONGraphModelError; },{"14":14}],9:[function(require,module,exports){ var applyErrorPrototype = require(14); /** * An invalid deref input is when deref is used with input that is not generated * from a get, set, or a call. * * @private */ function InvalidDerefInputError() { var instance = new Error("Deref can only be used with a non-primitive object from get, set, or call."); instance.name = "InvalidDerefInputError"; if (Object.setPrototypeOf) { Object.setPrototypeOf(instance, Object.getPrototypeOf(this)); } if (Error.captureStackTrace) { Error.captureStackTrace(instance, InvalidDerefInputError); } return instance; } applyErrorPrototype(InvalidDerefInputError); module.exports = InvalidDerefInputError; },{"14":14}],10:[function(require,module,exports){ var applyErrorPrototype = require(14); /** * An InvalidModelError can only happen when a user binds, whether sync * or async to shorted value. See the unit tests for examples. * * @param {*} boundPath * @param {*} shortedPath * * @private */ function InvalidModelError(boundPath, shortedPath) { var instance = new Error("The boundPath of the model is not valid since a value or error was found before the path end."); instance.name = "InvalidModelError"; instance.boundPath = boundPath; instance.shortedPath = shortedPath; if (Object.setPrototypeOf) { Object.setPrototypeOf(instance, Object.getPrototypeOf(this)); } if (Error.captureStackTrace) { Error.captureStackTrace(instance, InvalidModelError); } return instance; } applyErrorPrototype(InvalidModelError); module.exports = InvalidModelError; },{"14":14}],11:[function(require,module,exports){ var applyErrorPrototype = require(14); /** * InvalidSourceError happens when a dataSource syncronously throws * an exception during a get/set/call operation. * * @param {Error} error - The error that was thrown. * * @private */ function InvalidSourceError(error) { var instance = new Error("An exception was thrown when making a request."); instance.name = "InvalidSourceError"; instance.innerError = error; if (Object.setPrototypeOf) { Object.setPrototypeOf(instance, Object.getPrototypeOf(this)); } if (Error.captureStackTrace) { Error.captureStackTrace(instance, InvalidSourceError); } return instance; } applyErrorPrototype(InvalidSourceError); module.exports = InvalidSourceError; },{"14":14}],12:[function(require,module,exports){ var applyErrorPrototype = require(14); /** * A request can only be retried up to a specified limit. Once that * limit is exceeded, then an error will be thrown. * * @param {*} missingOptimizedPaths * * @private */ function MaxRetryExceededError(missingOptimizedPaths) { var instance = new Error("The allowed number of retries have been exceeded."); instance.name = "MaxRetryExceededError"; instance.missingOptimizedPaths = missingOptimizedPaths || []; if (Object.setPrototypeOf) { Object.setPrototypeOf(instance, Object.getPrototypeOf(this)); } if (Error.captureStackTrace) { Error.captureStackTrace(instance, MaxRetryExceededError); } return instance; } applyErrorPrototype(MaxRetryExceededError); MaxRetryExceededError.is = function(e) { return e && e.name === "MaxRetryExceededError"; }; module.exports = MaxRetryExceededError; },{"14":14}],13:[function(require,module,exports){ var applyErrorPrototype = require(14); /** * Does not allow null in path * * @private */ function NullInPathError() { var instance = new Error("`null` and `undefined` are not allowed in branch key positions"); instance.name = "NullInPathError"; if (Object.setPrototypeOf) { Object.setPrototypeOf(instance, Object.getPrototypeOf(this)); } if (Error.captureStackTrace) { Error.captureStackTrace(instance, NullInPathError); } return instance; } applyErrorPrototype(NullInPathError); module.exports = NullInPathError; },{"14":14}],14:[function(require,module,exports){ function applyErrorPrototype(errorType) { errorType.prototype = Object.create(Error.prototype, { constructor: { value: Error, enumerable: false, writable: true, configurable: true } }); if (Object.setPrototypeOf) { Object.setPrototypeOf(errorType, Error); } else { // eslint-disable-next-line errorType.__proto__ = Error; } } module.exports = applyErrorPrototype; },{}],15:[function(require,module,exports){ var createHardlink = require(73); var onValue = require(26); var isExpired = require(30); var $ref = require(110); var promote = require(40); /* eslint-disable no-constant-condition */ function followReference(model, root, nodeArg, referenceContainerArg, referenceArg, seed, isJSONG) { var node = nodeArg; var reference = referenceArg; var referenceContainer = referenceContainerArg; var depth = 0; var k, next; while (true) { if (depth === 0 && referenceContainer.$_context) { depth = reference.length; next = referenceContainer.$_context; } else { k = reference[depth++]; next = node[k]; } if (next) { var type = next.$type; var value = type && next.value || next; if (depth < reference.length) { if (type) { node = next; break; } node = next; continue; } // We need to report a value or follow another reference. else { node = next; if (type && isExpired(next)) { break; } if (!referenceContainer.$_context) { createHardlink(referenceContainer, next); } // Restart the reference follower. if (type === $ref) { // Nulls out the depth, outerResults, if (isJSONG) { onValue(model, next, seed, null, null, null, null, reference, reference.length, isJSONG); } else { promote(model._root, next); } depth = 0; reference = value; referenceContainer = next; node = root; continue; } break; } } else { node = void 0; } break; } if (depth < reference.length && node !== void 0) { var ref = []; for (var i = 0; i < depth; i++) { ref[i] = reference[i]; } reference = ref; } return [node, reference, referenceContainer]; } /* eslint-enable */ module.exports = followReference; },{"110":110,"26":26,"30":30,"40":40,"73":73}],16:[function(require,module,exports){ var getCachePosition = require(19); var InvalidModelError = require(10); var BoundJSONGraphModelError = require(8); function mergeInto(target, obj) { /* eslint guard-for-in: 0 */ if (target === obj) { return; } if (target === null || typeof target !== "object" || target.$type) { return; } if (obj === null || typeof obj !== "object" || obj.$type) { return; } for (var key in obj) { // When merging over a temporary branch structure (for example, as produced by an error selector) // with references, we don't want to mutate the path, particularly because it's also $_absolutePath // on cache nodes if (key === "$__path") { continue; } var targetValue = target[key]; if (targetValue === undefined) { target[key] = obj[key]; } else { mergeInto(targetValue, obj[key]); } } } function defaultEnvelope(isJSONG) { return isJSONG ? {jsonGraph: {}, paths: []} : {json: {}}; } module.exports = function get(walk, isJSONG) { return function innerGet(model, paths, seed) { // Result valueNode not immutable for isJSONG. var nextSeed = isJSONG ? seed : [{}]; var valueNode = nextSeed[0]; var results = { values: nextSeed, optimizedPaths: [] }; var cache = model._root.cache; var boundPath = model._path; var currentCachePosition = cache; var optimizedPath, optimizedLength; var i, len; var requestedPath = []; var derefInfo = []; var referenceContainer; // If the model is bound, then get that cache position. if (boundPath.length) { // JSONGraph output cannot ever be bound or else it will // throw an error. if (isJSONG) { return { criticalError: new BoundJSONGraphModelError() }; } // using _getOptimizedPath because that's a point of extension // for polyfilling legacy falcor optimizedPath = model._getOptimizedBoundPath(); optimizedLength = optimizedPath.length; // We need to get the new cache position path. currentCachePosition = getCachePosition(model, optimizedPath); // If there was a short, then we 'throw an error' to the outside // calling function which will onError the observer. if (currentCachePosition && currentCachePosition.$type) { return { criticalError: new InvalidModelError(boundPath, optimizedPath) }; } referenceContainer = model._referenceContainer; } // Update the optimized path if we else { optimizedPath = []; optimizedLength = 0; } for (i = 0, len = paths.length; i < len; i++) { walk(model, cache, currentCachePosition, paths[i], 0, valueNode, results, derefInfo, requestedPath, optimizedPath, optimizedLength, isJSONG, false, referenceContainer); } // Merge in existing results. // Default to empty envelope if no results were emitted mergeInto(valueNode, paths.length ? seed[0] : defaultEnvelope(isJSONG)); return results; }; }; },{"10":10,"19":19,"8":8}],17:[function(require,module,exports){ var getValueSync = require(21); var InvalidModelError = require(10); module.exports = function getBoundValue(model, pathArg, materialized) { var path = pathArg; var boundPath = pathArg; var boxed, treatErrorsAsValues, value, shorted, found; boxed = model._boxed; materialized = model._materialized; treatErrorsAsValues = model._treatErrorsAsValues; model._boxed = true; model._materialized = materialized === undefined || materialized; model._treatErrorsAsValues = true; value = getValueSync(model, path.concat(null), true); model._boxed = boxed; model._materialized = materialized; model._treatErrorsAsValues = treatErrorsAsValues; path = value.optimizedPath; shorted = value.shorted; found = value.found; value = value.value; while (path.length && path[path.length - 1] === null) { path.pop(); } if (found && shorted) { throw new InvalidModelError(boundPath, path); } return { path: path, value: value, shorted: shorted, found: found }; }; },{"10":10,"21":21}],18:[function(require,module,exports){ var isInternalKey = require(86); /** * decends and copies the cache. */ module.exports = function getCache(cache) { var out = {}; _copyCache(cache, out); return out; }; function cloneBoxedValue(boxedValue) { var clonedValue = {}; var keys = Object.keys(boxedValue); var key; var i; var l; for (i = 0, l = keys.length; i < l; i++) { key = keys[i]; if (!isInternalKey(key)) { clonedValue[key] = boxedValue[key]; } } return clonedValue; } function _copyCache(node, out, fromKey) { // copy and return Object. keys(node). filter(function(k) { // Its not an internal key and the node has a value. In the cache // there are 3 possibilities for values. // 1: A branch node. // 2: A $type-value node. // 3: undefined // We will strip out 3 return !isInternalKey(k) && node[k] !== undefined; }). forEach(function(key) { var cacheNext = node[key]; var outNext = out[key]; if (!outNext) { outNext = out[key] = {}; } // Paste the node into the out cache. if (cacheNext.$type) { var isObject = cacheNext.value && typeof cacheNext.value === "object"; var isUserCreatedcacheNext = !cacheNext.$_modelCreated; var value; if (isObject || isUserCreatedcacheNext) { value = cloneBoxedValue(cacheNext); } else { value = cacheNext.value; } out[key] = value; return; } _copyCache(cacheNext, outNext, key); }); } },{"86":86}],19:[function(require,module,exports){ /** * getCachePosition makes a fast walk to the bound value since all bound * paths are the most possible optimized path. * * @param {Model} model - * @param {Array} path - * @returns {Mixed} - undefined if there is nothing in this position. * @private */ module.exports = function getCachePosition(model, path) { var currentCachePosition = model._root.cache; var depth = -1; var maxDepth = path.length; // The loop is simple now, we follow the current cache position until // while (++depth < maxDepth && currentCachePosition && !currentCachePosition.$type) { currentCachePosition = currentCachePosition[path[depth]]; } return currentCachePosition; }; },{}],20:[function(require,module,exports){ var ModelResponse = require(51); var pathSyntax = require(124); module.exports = function getValue(path) { var parsedPath = pathSyntax.fromPath(path); var pathIdx = 0; var pathLen = parsedPath.length; while (++pathIdx < pathLen) { if (typeof parsedPath[pathIdx] === "object") { /* eslint-disable no-loop-func */ return new ModelResponse(function(o) { o.onError(new Error("Paths must be simple paths")); }); /* eslint-enable no-loop-func */ } } var self = this; return new ModelResponse(function(obs) { return self.get(parsedPath).subscribe(function(data) { var curr = data.json; var depth = -1; var length = parsedPath.length; while (curr && ++depth < length) { curr = curr[parsedPath[depth]]; } obs.onNext(curr); }, function(err) { obs.onError(err); }, function() { obs.onComp