falcor
Version:
A JavaScript library for efficient data fetching.
1,517 lines (1,275 loc) • 292 kB
JavaScript
/*!
* 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