"use strict";

exports.__esModule = true;
exports.default = exports.AllOp = exports.UpdateOp = exports.FindOp = exports.FetchOp = exports.DestroyOp = exports.CreateOp = exports.SavedEvent = exports.ErrorEvent = exports.FoundEvent = exports.FetchedEvent = exports.DeletedEvent = exports.ChangeEvent = exports.SerializeVersion = undefined;

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };

var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };

var _akutils = require("akutils");

var _es6Symbol = require("es6-symbol");

var _es6Symbol2 = _interopRequireDefault(_es6Symbol);

var _lodash = require("lodash.isequal");

var _lodash2 = _interopRequireDefault(_lodash);

var _lodash3 = require("lodash.isplainobject");

var _lodash4 = _interopRequireDefault(_lodash3);

var _lodash5 = require("lodash.pick");

var _lodash6 = _interopRequireDefault(_lodash5);

var _componentEmitter = require("component-emitter");

var _componentEmitter2 = _interopRequireDefault(_componentEmitter);

var _ArrayProperty = require("../ArrayProperty");

var _ArrayProperty2 = _interopRequireDefault(_ArrayProperty);

var _createPropertyClass = require("../createPropertyClass");

var _createPropertyClass2 = _interopRequireDefault(_createPropertyClass);

var _KeyedApi = require("../KeyedApi");

var _KeyedApi2 = _interopRequireDefault(_KeyedApi);

var _MapProperty = require("../MapProperty");

var _MapProperty2 = _interopRequireDefault(_MapProperty);

var _ObjectShadowImpl = require("../ObjectShadowImpl");

var _ObjectShadowImpl2 = _interopRequireDefault(_ObjectShadowImpl);

var _PrimitiveProperty = require("../PrimitiveProperty");

var _PrimitiveProperty2 = _interopRequireDefault(_PrimitiveProperty);

var _Property2 = require("../Property");

var _Property3 = _interopRequireDefault(_Property2);

var _Shadow = require("../Shadow");

var _Shadow2 = _interopRequireDefault(_Shadow);

var _StateType = require("../StateType");

var _StateType2 = _interopRequireDefault(_StateType);

var _Store = require("../Store");

var _Store2 = _interopRequireDefault(_Store);

var _CollectionShadow = require("./CollectionShadow");

var _CollectionShadow2 = _interopRequireDefault(_CollectionShadow);

var _hashcode = require("./hashcode");

var _hashcode2 = _interopRequireDefault(_hashcode);

var _isPrimitive = require("./isPrimitive");

var _isPrimitive2 = _interopRequireDefault(_isPrimitive);

var _ModelProperty = require("./ModelProperty");

var _ModelProperty2 = _interopRequireDefault(_ModelProperty);

var _debug = require("../debug");

var _debug2 = _interopRequireDefault(_debug);

var _CollectionOptions = require("./CollectionOptions");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var debug = (0, _debug2.default)(_debug.CollectionPropertyKey);

var _endpoint = "_endpoint";
var _id2cid = "_id2cid";
var _idName = "idName";
var _lastPageSize = "lastPageSize";
var _limit = "_limit";
var _models = "_models";
var _nextOffset = "nextOffset";
var _offlineKey = "offlineKey";
var _paging = "_paging";
var _restored = "restored";
var _saveShallow = "saveShallow";
var _synced = "synced";

var _autocheckpoint = (0, _es6Symbol2.default)("autocheckpoint");
var _fetchingError = (0, _es6Symbol2.default)("fetchingError");
var _fetching = (0, _es6Symbol2.default)("fetching");
var _middleware = (0, _es6Symbol2.default)("middleware");
var _modelsHash = (0, _es6Symbol2.default)("modelsHash");
var _restoring = (0, _es6Symbol2.default)("restoring");

var SerializeVersion = exports.SerializeVersion = 1;

/**
	Event emitted on collection changes.
*/
var ChangeEvent = exports.ChangeEvent = "collection.change";
/**
	Event emitted on collection destroy() success.
*/
var DeletedEvent = exports.DeletedEvent = "collection.deleted";
/**
	Event emitted on collection fetch() success.
*/
var FetchedEvent = exports.FetchedEvent = "collection.fetched";
/**
	Event emitted on collection find() success.
*/
var FoundEvent = exports.FoundEvent = "collection.sound";
/**
	Event emitted on error during an operation.
*/
var ErrorEvent = exports.ErrorEvent = "collection.error";
/**
	Event emitted on collection save() success.
*/
var SavedEvent = exports.SavedEvent = "collection.saved";

/**
	Middleware operation for create requests.
*/
var CreateOp = exports.CreateOp = "collection.create";
/**
	Middleware operation for destroy requests.
*/
var DestroyOp = exports.DestroyOp = "collection.destroy";
/**
	Middleware operation for fetch requests.
*/
var FetchOp = exports.FetchOp = "collection.fetch";
/**
	Middleware operation for find requests.
*/
var FindOp = exports.FindOp = "collection.find";
/**
	Middleware operation for update requests.
*/
var UpdateOp = exports.UpdateOp = "collection.update";
/**
	All middleware operations.
*/
var AllOp = exports.AllOp = [CreateOp, DestroyOp, FetchOp, FindOp, UpdateOp];

/*
	Improvements
		* Paging - consider either adding an endpoint fetchNext() method or make setting the
			query parameters pluggable
		* Serialization - make an abstract structure so less brittle to internal data
			structure changes
		* Paging - sliding window support
		* Isolated objects - reduce mapping costs for large collections
		* Map models onto collection
*/

/**
	`CollectionProperty` provides a standard means for accessing remote data and was inspired by
	[Backbone collections](http://backbonejs.org/#Collection). The protocol for interacting with
	the data source is delegated to an *endpoint* so the same application logic can work with data from
	a RESTful api, graphql, local test data, or even an in-browser database like localforage. F.lux
	ships with support for two endpoints, {@link RestEndpointProperty} and {@link PojoEndpointProperty}.

	F.lux ships with a [example](https://github.com/akrumel/f.lux/tree/master/examples/todo-collections)
	using collections in a todo application utilizing an in memory data source. The example uses a local
	data source to make setup dead simple while still demonstrating the basic features of creating
	and interacting with collections.

	## Basic operations

	<ul>
		<li>`add(state, mergeOp)`</li>
		<li>`all()`</li>
		<li>`clear()`</li>
		<li>`create(model)`</li>
		<li>`destroy(id)`</li>
		<li>`fetch(filter, mergeOp)`</li>
		<li>`find(id)`</li>
		<li>`get(id)`</li>
		<li>`has(id)`</li>
		<li>`remove(id)`</li>
	</ul>


	## Utility functions

	<ul>
		<li>`entries()`</li>
		<li>`every(iteratee, context)`</li>
		<li>`filter(iteratee, context)`</li>
		<li>`groupBy(callback, context)`</li>
		<li>`keys()`</li>
		<li>`map(iteratee, context)`</li>
		<li>`reduce(iteratee, acc, context)`</li>
		<li>`some(iteratee, context)`</li>
		<li>`sortBy(...iteratee)`</li>
		<li>`values()`</li>
	</ul>


	## Model access

	Properties managed by a collection receive an f.lux accessor (`$()`) with additional
	capabilities related to the collection. The accessor class is ${@link ShadowModelAccess}.

	<ul>
		<li>`collection()` - gets the {@link Collection} managing this model</li>
		<li>`destroy()` - permanently deletes the model from the collection using
			{@link CollectionProperty#destroy}</li>
		<li>`id()` - gets the ID used by the data source for tracking the model</li>
		<li>`isWaiting()` - is a network operation in progress associated with this model</li>
		<li>`isDirty()` - has the model been modified since it was last saved/retrieved</li>
		<li>`isNew()` - gets whether the model has yet to be written to the collection's endpoint</li>
		<li>`remove()` - removes the model from the collection using {@link CollectionProperty#remove}</li>
		<li>`save()` - save the model using {@link CollectionProperty#save}</li>
	</ul>


	## Paging

	The collection provides redimentary paging support for potentially large collections. The
	current implementation simply grows the collection size wtih each call to `fetchNext()`
	and does not make any efforts to remove previous models. (A likely future enhancement)

	<ul>
		<li>`fetchNext()` - gets the next group of models</li>
		<li>`isPaging()` - gets if a paging request is outstanding (only one paging request per
				collection allowed at one time)</li>
		<li>`hasMorePages()` - does the endpoint have additional models</li>
		<li>`nextOffset()` - gets the offset for the next paging request</li>
		<li>`resetPaging()` - resets the internal paging variables</li>
		<li>`setLimit()` - change the number of models requested with each `fetchNext()` request</li>
	</ul>


	## Endpoints

	Collections represent a protocol independent means of managing persistent models that
	exist outside the application state. Endpoints implement the protocol connection to the
	data source. F.lux endpoints have been implemented for RESTful server apis, in-memory,
	GraphQL, Sqlite, and Couchbase. F.lux ships with support for RESTful servers and in-memory.

	A simple, explicit example of creating a collection and assigning an endpoint:

	```
	const rootProperty = store.root();
	const collection = new CollectionProperty(colType);
	const ep = new RestEndpointProperty.createFor("http://some-url.com/an-api");

	collection.setEndpoint(ep);

	root._keyed.addProperty("aCollection", collection);
	```

	The [Todo Collection Example](https://github.com/akrumel/f.lux/tree/master/examples/todo-collections)
	demonstrates a more typical example of setting up a collection.

	<div data-ice="see"><h4>See:</h4>
		<ul>
			<li>{@link RestEndpointProperty}</li>
			<li>{@link PojoEndpointProperty}</li>
		</ul>
	</div>


	## Offline support

	Collections support offline first capability. To enable, you must:

	<ul>
		<li></li>
		<li></li>
	</ul>

	## Query filters

	Collection models are retrieved in full using {@link CollectionProperty#fetch} that has the following
	signature:

	```
	fetch(filter=null, mergeOp=REPLACE_OPTION, replaceAll=true)
	```

	where:
	<ul>
		<li>`filter` - a query filter object or `null` for no endpoint filtering</li>
		<li>`mergeOp` - one of `DEFAULTS_OPTION`, `MERGE_OPTION`, or `REPLACE_OPTION` and specifies
				how to combine an existing model with a matching ID with a newly retrieved model.</li>
		<li>`replaceAll` - a boolean where `true` means replace the current colleciton models with
				the returned models.</li>
	</ul>

	The `filter` parameter is a specialized object generated by the endpoint method `queryBuilder()`.
	A query builder is specialized to the endpoint and expose the following methods:

	<ul>
		<li>`equals(name, value)`</li>
		<li>`gt(name, value)`</li>
		<li>`gte(name, value)`</li>
		<li>`lt(name, value)`</li>
		<li>`lte(name, value)`</li>
	</ul>

	An example of using a query builder is:

	```
	const qb = colleciton.endpoint.queryBuilder();

	qb.equals("name", "fred");

	collection.fetch(qb);
	```

	<div data-ice="see"><h4>See:</h4>
		<ul>
			<li>{@link RestQueryBuilder}</li>
			<li>{@link PojoQueryBuilder}</li>
		</ul>
	</div>


	## Errors

	Methods utilizing the collection endpoint are asynchronous and return a `Promise`. Errors generated
	during an endpoint operation reject using an `Error` with several specialized properties:

	<ul>
		<li>`status` - an HTTP status code for the error type</li>
		<li>`endpointError` - the error object generated by the endpoint</li>
	</ul>


	## Auto-checkpointing

	The `Property` class supports checkpointing state that can be reset at a later time using
	{@link Property#resetToCheckpoint}. Collections support setting an 'auto-checkpoint' flag
	that will result in managed models automatically setting a checkpoint on change. This handy
	for situations like a 'Cancel' button on a form where all changes need to be undone. The
	checkpoints are automatically cleared when a model is saved.


	<ul>
		<li>`isAutocheckpoint()`</li>
		<li>`setAutocheckpoint(auto)`</li>
	</ul>


	## Middleware

	A middleware operation is invoked before each asynchronous/network operation involving the
	endpoint. Middleware operations are functions with the following format

	```
	function middleware(collectionShadow, collectionProperty, op): Promise
	```

	Middleware functions must return a `Promise` and are invoked in the order registered. A function
	generating an error will terminate the middleware chain and the collection operation will not
	be performed.

	<ul>
		<li>`CreateOp` - invoked prior to create rquests</li>
		<li>`DestroyOp` - invoked prior to destroy rquests</li>
		<li>`FetchOp` - invoked prior to fetch rquests</li>
		<li>`FindOp` - invoked prior to find rquests</li>
		<li>`UpdateOp` - invoked prior to update rquests</li>
		<li>`` - </li>
	</ul>


	## Events

	A colleciton is an [Event Emitter](https://github.com/component/emitter) and generates
	the following events:

	<ul>
		<li>`ChangeEvent` - collection changes</li>
		<li>`DeletedEvent` - collection destroy() success</li>
		<li>`FetchedEvent` - collection fetch() success</li>
		<li>`FoundEvent` - collection find() success</li>
		<li>`ErrorEvent` - error during an operation</li>
		<li>`SavedEvent` - collection save() success</li>
	</ul>

	Events are can be utilized to handle authorization errors, offline support, logging, or
	debugging tools.


	@see {@link CollectionShadow}
	@see {@link ShadowModelAccess}
	@see {@link RestEndpointProperty}
	@see {@link PojoEndpointProperty}
	@see {@link RestQueryBuilder}
	@see {@link PojoQueryBuilder}
*/

var CollectionProperty = function (_Property) {
	_inherits(CollectionProperty, _Property);

	function CollectionProperty(stateType) {
		var _this$_middleware;

		_classCallCheck(this, CollectionProperty);

		/** @ignore */

		var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(CollectionProperty).call(this, stateType));

		_this._keyed = new _KeyedApi2.default(_this);

		_this.setImplementationClass(_ObjectShadowImpl2.default);
		_this.setShadowClass(_CollectionShadow2.default);

		/** @ignore */
		_this[_middleware] = (_this$_middleware = {}, _defineProperty(_this$_middleware, CreateOp, []), _defineProperty(_this$_middleware, DestroyOp, []), _defineProperty(_this$_middleware, FetchOp, []), _defineProperty(_this$_middleware, FindOp, []), _defineProperty(_this$_middleware, UpdateOp, []), _this$_middleware);

		// keep isFetching as an instance variable because transient data and gives immediate
		// feedback to prevent concurrent fetches
		/** @ignore */
		_this[_fetching] = false;
		/** @ignore */
		_this[_autocheckpoint] = false;

		_this._keyed.addPropertyType(_idName, _PrimitiveProperty2.default.type.initialState("id").autoshadow);
		_this._keyed.addProperty(_id2cid, new _MapProperty2.default());
		_this._keyed.addPropertyType(_saveShallow, _PrimitiveProperty2.default.type.initialState(true).autoshadowOff.readonlyOff);
		_this._keyed.addPropertyType(_synced, _PrimitiveProperty2.default.type.initialState(false).autoshadowOff.readonly);

		// pagingTime instance variable used for ensuring overlapping paging requests do not mess up offset
		// this can happen when a paging request is in progress when limit is changed
		_this.pagingTime = null;
		_this._keyed.addPropertyType(_lastPageSize, _PrimitiveProperty2.default.type.initialState(null).autoshadowOff.readonly);
		_this._keyed.addPropertyType(_limit, _PrimitiveProperty2.default.type.initialState(50).autoshadowOff.readonly);
		_this._keyed.addPropertyType(_nextOffset, _PrimitiveProperty2.default.type.initialState(0).autoshadowOff.readonly);
		_this._keyed.addPropertyType(_paging, _PrimitiveProperty2.default.type.initialState(false).autoshadowOff.readonly);

		// this._keyed.addPropertyType(_offlineKey, PrimitiveProperty.type.initialState(null).readonlyOff);
		// this._keyed.addPropertyType(_restored, PrimitiveProperty.type.initialState(false).autoshadowOff.readonly);

		// '_models' property contains ModelProperty objects which in turn keep their model state in
		// the 'data' whose type is specified by the stateType.getManagedType() value (default is
		// MapProperty.type)
		var models = new _MapProperty2.default();
		var modelsShader = models.shader();

		_this._keyed.addProperty(_models, models);
		modelsShader.setElementType(_ModelProperty2.default.type);

		// set the type for each retrieved model by explicitly adding a type to the models element shader
		var managedType = _this.stateType().getManagedType() || _MapProperty2.default.type;
		modelsShader.elementShader().addProperty("data", managedType);
		return _this;
	}

	/**
 	Factory function for creating an `CollectionProperty` subclass. The generated class will have
 	the `type` {@link StateType} descriptor set upon return.
 		Example usage:
 	```
 	class SomeShadow extends CollectionShadow {
 	    // definition here
 	}
 		export default CollectionProperty.createClass(SomeShadow, type => {
 		// configure type variable
 	});
 	```
 		@param {Object|CollectionShadow} [shadowType={}] - `Shadow` subclass or object literal api definition.
 		If object literal specified, each property and function is mapped onto a Shadow subclass.
 	@param {function(type: StateType)} [typeCallback] - a callback function that will be passed the
 		{@link StateType} spec for additional customization, such as setting autoshadow, initial state,
 		or readonly.
 	@param {Object} [initialState]={} - the initial state for the new property.
 		@return {ObjectProperty} newly defined `ObjectProperty` subclass.
 */


	_createClass(CollectionProperty, [{
		key: "onChildInvalidated",


		/**
  	Override the base functionality method, and not designed life-cycle method propertyChildInvalidated(), so
  	subclasses can do the normal override without using super.propertyWillUpdate() to preserve functionality.
  		@ignore
  */
		value: function onChildInvalidated(childProperty, sourceProperty) {
			// need to call parent classes version
			_get(Object.getPrototypeOf(CollectionProperty.prototype), "onChildInvalidated", this).call(this, childProperty, sourceProperty);

			var childName = childProperty.name();

			if (childName === _models) {
				this.cancelRestore();
			}
		}

		/**
  	Override the base functionality method, and not designed life-cycle method propertyWillUpdate(), so
  	subclasses can do the normal override without using super.propertyWillUpdate() to preserve functionality.
  		@ignore
  */

	}, {
		key: "onPropertyDidShadow",
		value: function onPropertyDidShadow() {
			// need to call parent classes version
			_get(Object.getPrototypeOf(CollectionProperty.prototype), "onPropertyDidShadow", this).call(this);

			if (!this._().restored) {
				this.restore().catch(function (error) {
					return null;
				});
			}

			var managedType = this.stateType().getManagedType();
			var updateName = managedType && managedType.getUpdateName();

			if (updateName) {
				this.store().addRemoteListener(updateName, this);
			}
		}

		/**
  	Override the base functionality method, and not designed life-cycle method propertyWillUnshadow(), so
  	subclasses can do the normal override without using super.propertyWillUnshadow() to preserve functionality.
  		@ignore
  */

	}, {
		key: "onPropertyWillUnshadow",
		value: function onPropertyWillUnshadow() {
			// need to call parent classes version
			_get(Object.getPrototypeOf(CollectionProperty.prototype), "onPropertyWillUnshadow", this).call(this);

			var managedType = this.stateType().getManagedType();
			var updateName = managedType && managedType.getUpdateName();

			if (updateName) {
				this.store().removeRemoteListener(updateName, this);
			}
		}
	}, {
		key: "onRemoteUpdate",
		value: function onRemoteUpdate(type, op, key, values) {
			var _this2 = this;

			var managedType = this.stateType().getManagedType();

			if (managedType && !managedType.shouldUpdate(this._(), type, op, key, values)) {
				return;
			}

			switch (op) {
				case _Store.CascadeRemoteOp:
					var keys = Object.keys(values);

					this.modelsArray(this._()).forEach(function (m) {
						if ((0, _lodash2.default)((0, _lodash6.default)(m, keys), values)) {
							_this2.remove(m.id);
						}
					});
					break;
				case _Store.CreateRemoteOp:
				case _Store.UpdateRemoteOp:
					this.addModel(values);
					break;
				case _Store.DeleteRemoteOp:
					this.remove(+key);
					break;
			}
		}

		/**
  	Override the base functionality method, and not designed life-cycle method propertyWillUpdate(), so
  	subclasses can do the normal override without using super.propertyWillUpdate() to preserve functionality.
  		@ignore
  */

	}, {
		key: "onPropertyDidUpdate",
		value: function onPropertyDidUpdate() {
			// need to call parent classes version
			_get(Object.getPrototypeOf(CollectionProperty.prototype), "onPropertyDidUpdate", this).call(this);

			this.storeData().catch(function (error) {
				return null;
			});
			this.emitEvent(ChangeEvent);
		}
	}, {
		key: "emitEvent",
		value: function emitEvent(event, data) {
			var store = this.store();

			this.emit(event, data, this._(), this);
			store.emit(event, data, this._(), this);
		}

		//------------------------------------------------------------------------------------------------------
		// Checkpoint support API
		//------------------------------------------------------------------------------------------------------

		/**
  	Sets whether auto-checkpointing is enabled.
  		@return {boolean}
  */

	}, {
		key: "isAutocheckpoint",
		value: function isAutocheckpoint() {
			return this[_autocheckpoint];
		}

		/**
  	Sets whether auto-checkpointing should be enabled.
  		@param {boolean} auto - `true` to enable.
  */

	}, {
		key: "setAutocheckpoint",
		value: function setAutocheckpoint(auto) {
			this[_autocheckpoint] = auto;
		}

		//------------------------------------------------------------------------------------------------------
		// Middleware API
		//------------------------------------------------------------------------------------------------------

		/**
  	Registers collection middleware operation. A middleware operation is invoked before each asynchronous/
  	network operation. Middleware operations are functions with the following format:
  		```
  	function mwCallback(collectionShadow, collectionProperty)
  	```
  		Middleware functions must return a promise and are invoked in the order registered. A function
  	generating an error will terminate the middleware chain and the collection operation will not
  	be performed.
  */

	}, {
		key: "use",
		value: function use(op, mw) {
			var _this3 = this;

			op = Array.isArray(op) ? op : [op];

			(0, _akutils.assert)(function (a) {
				for (var i = 0, len = op.length; i < len; i++) {
					a.has(_this3[_middleware], op[i]);
				}
			});

			for (var i = 0, len = op.length; i < len; i++) {
				this[_middleware][op[i]].push(mw);
			}
		}

		/**
  	Serially invokes each middleware function for the specified operation.
  		@ignore
  */

	}, {
		key: "_on",
		value: function _on(op) {
			var _this4 = this;

			var idx = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1];

			(0, _akutils.assert)(function (a) {
				return a.is(_this4[_middleware][op], "Unknown middleware operation: " + op);
			});

			var mw = this[_middleware][op];
			var next = mw[idx];

			if (!next) {
				return _Store2.default.resolve(this._());
			}

			try {
				return next(this._(), this, op).then(function () {
					return _this4._on(op, idx + 1);
				});
			} catch (error) {
				return _Store2.default.reject(error);
			}
		}

		/**
  	Experimental feature to update values with any modifications from the server. Assumes immutable server
  	objects.
  		Question: should this be delegated to the endpoint?
  		@experimental
  	@ignore
  */

	}, {
		key: "resync",
		value: function resync() {
			var _this5 = this;

			var values = this._().valuesArray();
			var currSync;
			var lastUpdate = null;
			var initialId = Number.MAX_SAFE_INTEGER;
			var filter, id, v, updatedAt;

			// Todo: break out filter parameters to a pluggable function
			// iterate all values and determine the greatest updated_at
			for (var i = 0, len = values.length; i < len; i++) {
				v = values[i];

				if (!v) {
					continue;
				}

				id = v.id;
				updatedAt = v.updated_at;

				if (id < initialId) {
					initialId = id;
				}

				if (!lastUpdate || lastUpdate < updatedAt) {
					lastUpdate = updatedAt;
				}
			}

			// setup the refresh criteria
			if (lastUpdate) {
				filter = this.endpoint.queryBuilder();

				filter.gte("initial_id", initialId);
				filter.gt("updated_at", lastUpdate);
			}

			debug(function (d) {
				return d("resync() - path=" + _this5.dotPath() + ", initial_id=" + initialId + ", updated_at=" + lastUpdate);
			});

			// Todo: rework network call to return two arrays: new/modified models and deleted model IDs
			//       as currently structured there is no delete support.
			//       What about dirty models? Perhaps they should not be touched if there is a conflict.

			return this.fetch(filter, _CollectionOptions.MERGE_OPTION, !lastUpdate);
		}

		//------------------------------------------------------------------------------------------------------
		// Paging API (could improve by adding two offsets and maintain max pages)
		//------------------------------------------------------------------------------------------------------

		/**
  	Fetches the next set of models from the endpoint. This method differs from
  	{@link CollectionProperty#fetch} by passing `offset` and `limit` filter criteria to the
  	endpoint.
  		@param {string} [mergeOp=REPLACE_OPTION] - one of `DEFAULTS_OPTION`, `MERGE_OPTION`, or
  		`REPLACE_OPTION` and specifies how to combine an existing model with a matching ID
  		with a newly retrieved model.
  		@return {Promise} - resolves with a single argument of an array of model json objects as
  		returned from the endpoint.
  */

	}, {
		key: "fetchNext",
		value: function fetchNext() {
			var _this6 = this;

			var mergeOp = arguments.length <= 0 || arguments[0] === undefined ? _CollectionOptions.REPLACE_OPTION : arguments[0];

			if (this.pagingTime) {
				throw new Error("Paging operation in progress");
			} else if (!this.hasMorePages()) {
				return _Store2.default.resolve(this._());
			}

			var filter = this.endpoint.queryBuilder();
			var time = this.pagingTime = Date.now();
			var replaceAll = !this._()[_nextOffset];

			this._keyed.set(_paging, true);

			filter.equals("offset", this._()[_nextOffset]);
			filter.equals("limit", this._()[_limit]);

			return this.fetch(filter, mergeOp, replaceAll, function (error, models) {
				// bail if paging times do not match (offset likely reset)
				if (time !== _this6.pagingTime) {
					return;
				}

				_this6.pagingTime = null;
				_this6._keyed.set(_paging, false);
				if (error) {
					return;
				}

				_this6._keyed.set(_nextOffset, _this6._()[_nextOffset] + models.length);
				_this6._keyed.set(_lastPageSize, models.length);

				if (models.length < _this6._()[_limit]) {
					_this6._keyed.set(_synced, true);
				}
			});
		}

		/**
  	Gets if a paging operation ({@link CollectionProperty#fetchNext}) call is in progress.
  */

	}, {
		key: "isPaging",
		value: function isPaging() {
			var state = arguments.length <= 0 || arguments[0] === undefined ? this._() : arguments[0];

			// use pagingTime instance variable (instant) and _paging state variable (tied to state) to
			// return the most conservative value
			return !!this.pagingTime || state[_paging];
		}

		/**
  	Gets if additional paging calls will return additional results.
  */

	}, {
		key: "hasMorePages",
		value: function hasMorePages() {
			var state = arguments.length <= 0 || arguments[0] === undefined ? this._() : arguments[0];

			return this.isConnected() && !state[_synced] && !state[_restored] && (state[_lastPageSize] == null || this._()[_lastPageSize] >= this._()[_limit]);
		}

		/**
  	Gets the offset for the next paging request.
  */

	}, {
		key: "nextOffset",
		value: function nextOffset() {
			return this._keyed.get(_nextOffset);
		}

		/**
  	Resets all internal paging tracking variables but does not affect currently stored models.
  */

	}, {
		key: "resetPaging",
		value: function resetPaging() {
			this.pagingTime = null;
			this._keyed.set(_lastPageSize, null);
			this._keyed.set(_nextOffset, 0);
			this._keyed.set(_paging, false);
		}

		/**
  	Sets the number of models to request with each paging request. The default paging size is
  	50.
  		@param {number} limit
  */

	}, {
		key: "setLimit",
		value: function setLimit(limit) {
			this.pagingTime = null;

			this._keyed.set(_limit, limit);
			this._keyed.set(_lastPageSize, null);
			this._keyed.set(_nextOffset, 0);
			this._keyed.set(_paging, false);
		}

		//------------------------------------------------------------------------------------------------------
		// Endpoint methods
		//------------------------------------------------------------------------------------------------------

		/**
  	Gets the endpoint for accessing the remote data source.
  		@return {Object}
  */

	}, {
		key: "clearEndpoint",


		/**
  	Removes the endpoint from the collection. This has the side-effect of clearing the mdoels.
  */
		value: function clearEndpoint() {
			this.removeAllModels();
			this._keyed.removeProperty(_endpoint);
			this.setFetching(false);
		}

		/**
  	Sets the {@link CollectionProperty#endpoint}.
  */

	}, {
		key: "setEndpoint",
		value: function setEndpoint(endPoint) {
			var _this7 = this;

			var store = this.store();

			this.setFetching(false);
			this.resetPaging();
			this.removeAllModels();
			this._keyed.addProperty(_endpoint, endPoint);

			store && store.waitFor(function () {
				return _this7.restore().catch(function (error) {
					return null;
				});
			});
		}

		//------------------------------------------------------------------------------------------------------
		// Experimental offline support APIs
		//------------------------------------------------------------------------------------------------------

		/**
  	Cancels outstanding data restoration from offline storage.
  */

	}, {
		key: "cancelRestore",
		value: function cancelRestore() {
			if (this[_restoring]) {
				this[_restoring] = false;
				this.touch("CollectionProperty.cancelRestore()");
			}
		}

		/**
  	Removes offline data for this collection.
  		@experimental
  */

	}, {
		key: "clearStoredData",
		value: function clearStoredData() {
			var epId = this.endpointId;
			var offlineKey = this._keyed.get(_offlineKey);
			var offline = this.store().offlineStore();
			var dataId = epId && encodeURIComponent(epId);

			if (dataId && offlineKey && offline) {
				return offline.setOfflineData(offlineKey, dataId, null, this);
			}
		}
	}, {
		key: "clearAllStoredData",
		value: function clearAllStoredData() {
			var offlineKey = this._keyed.get(_offlineKey);
			var offline = this.store().offlineStore();

			if (offlineKey && offline) {
				return offline.deleteBackups(offlineKey, this);
			}
		}

		/**
  	Gets if the collection is in the process of restoring data from offline storage.
  */

	}, {
		key: "isRestoring",
		value: function isRestoring() {
			return this[_restoring];
		}

		/**
  	Sets a flag for whether to keep previous values for the paging tracking properties (`lastPageSize`
  	and `nextOffset`) from restored data. Default is to reset them to the default values for when a
  	collection is created.
  		The reasoning behind restting the tracking properties is that models added since last persistent
  	operation would make them invalid.
  */

	}, {
		key: "setPreservePagingOnRestore",
		value: function setPreservePagingOnRestore(preserve) {
			this.preservePagingOnRestore = preserve;
		}

		/**
  	Restores the collection to the last offline stored state. Method does not restore state if the
  	collection contains any items or the `synced` flag is set.
  		@return {Promise}
  		@experimental
  */

	}, {
		key: "restore",
		value: function restore() {
			var _this8 = this;

			if (!this.isActive()) {
				return _Store2.default.resolve(null);
			}

			var epId = this.endpointId;
			var offlineKey = this._keyed.get(_offlineKey);
			var synced = this._keyed.get(_synced);
			var offline = this.store().offlineStore();
			var dataId = encodeURIComponent(epId);

			// Must have an EP, offline data key, not synced, not restoring, and collection is empty
			if (!epId || !offlineKey || !offline || this.size || this.synced || this[_restoring]) {
				return _Store2.default.resolve(null);
			}

			this[_restoring] = true;
			this.touch("Collection[_restoring] = true");

			return offline.getOfflineData(offlineKey, dataId, this).then(function (data) {
				var _extends2;

				// ensure have data, same EP, still active, and collection is empty, versions match
				if (!data || _this8.endpointId !== epId || !_this8.isActive() || _this8.size || !_this8[_restoring] || data.version !== SerializeVersion) {
					return _this8.cancelRestore();
				}

				var nextState = _extends({}, data.state, (_extends2 = {}, _defineProperty(_extends2, _paging, !!_this8.pagingTime), _defineProperty(_extends2, _restored, true), _defineProperty(_extends2, _synced, false), _extends2));

				if (!_this8.preservePagingOnRestore) {
					nextState[_lastPageSize] = null;
					nextState[_nextOffset] = 0;
				}

				// disable cancelRestore()
				_this8[_restoring] = false;

				_this8.update(function (state) {
					return {
						name: "restore()",
						nextState: nextState,
						replace: true
					};
				});

				// restore flag
				_this8[_restoring] = true;

				// reset flag after store updates state
				_this8.store().waitFor(function () {
					// mark models
					_this8[_modelsHash] = (0, _hashcode2.default)(JSON.stringify(_this8[_models]));

					_this8[_restoring] = false;
				});

				return _this8.nextState();
			}).catch(function (error) {
				_this8[_restoring] = false;
				_this8.touch("Collection[_restoring] = false");

				(0, _debug.warn)("Restore Error: epId=" + _this8.endpointId + ", " + (error.message || error), error);

				return _Store2.default.reject(error);
			});
		}

		/**
  	Saves the current collection state for offline access. Method invoked on collection content changes.
  		@return {Promise}
  		@experimental
  */

	}, {
		key: "storeData",
		value: function storeData() {
			var _this9 = this;

			var state = this.state();
			var epId = this.endpointId;
			var offlineKey = this._keyed.get(_offlineKey);
			var offline = this.store().offlineStore();
			var dataId = epId && encodeURIComponent(epId);
			var modelsHash = (0, _hashcode2.default)(JSON.stringify(this._()[_models]));
			var canStore = dataId && offlineKey && offline && (this.size || state[_synced]);
			var data = {
				state: state,
				version: SerializeVersion
			};

			// Must have an EP, offline data key
			if (!canStore || this[_modelsHash] === modelsHash) {
				return _Store2.default.resolve(null);
			}

			debug(function (d) {
				return "storeData() - models changed";
			}, this.endpontId);

			this[_modelsHash] = modelsHash;

			return offline.setOfflineData(offlineKey, dataId, data, this).then(function () {
				debug(function (d) {
					return "Collection backup successful - " + _this9.endpointId + ", size=" + _this9.size;
				});

				return _this9;
			}).catch(function (error) {
				debug(function (d) {
					return "Collection backup error: collection=" + _this9.endpointId + ", error=" + error;
				});

				return _Store2.default.reject(error);
			});
		}

		//------------------------------------------------------------------------------------------------------
		// Collection methods
		//------------------------------------------------------------------------------------------------------

		/**
  	Gets the number of models contained in the collection.
  */

	}, {
		key: "addModel",


		/**
  	Synchronously adds a new model object to the collection. Call `model.$().save()` to persist the newly
  	added object.
  		@param {object} state - the json model to add to the collection
  	@param {string} [mergeOp=REPLACE_OPTION] - one of `DEFAULTS_OPTION`, `MERGE_OPTION`, or
  		`REPLACE_OPTION` and specifies how to combine an existing model with a matching ID
  		with a newly retrieved model.
  		@return the object's ID. An ID is assigned if the 'id' parameter was not set and it could not
  		be found in the `state` parameter.
  */
		value: function addModel(state) {
			var mergeOp = arguments.length <= 1 || arguments[1] === undefined ? _CollectionOptions.REPLACE_OPTION : arguments[1];

			if (!this.isConnected()) {
				throw new Error("Collection is not connected.");
			}

			var id = this.extractId(state);
			var modelId;

			// just add the model
			if (!id || !this.hasModel(id) /*|| mergeOp === REPLACE_OPTION*/) {
					var modelDefn = _ModelProperty2.default.modelDefinitionFor(state, this);
					var models = this._()[_models];

					if (id && this.hasModel(id)) {
						this.remove(id);
					}

					models.set(modelDefn.cid, modelDefn);

					modelId = modelDefn.id;
				} else {
				var currModel = this._getModel(id);

				switch (mergeOp) {
					case _CollectionOptions.NONE_OPTION:
						break;
					case _CollectionOptions.MERGE_OPTION:
						currModel.$().latest().merge(state);
						break;
					case _CollectionOptions.REPLACE_OPTION:
						currModel.$().latest().setData(state);
						break;
					case _CollectionOptions.DEFAULTS_OPTION:
						currModel.$().latest().defaults(state);
						break;
					default:
						throw new Error("Invalid post-save option: " + mergeOp);
				}

				modelId = currModel.id;
			}

			return modelId;
		}

		/**
  	Bulk adds multiple models. Models must have an ID as it is assumed they have been previously
  	saved.
  		@param {array} models - array of model values
  	@param {string} [mergeOp=REPLACE_OPTION] - one of `DEFAULTS_OPTION`, `MERGE_OPTION`, or
  		`REPLACE_OPTION` and specifies how to combine an existing model with a matching ID
  		with a newly retrieved model.
  	@param {boolean} [syncOp=true] - sets the `synced` flag to true if this parameter is true
  */

	}, {
		key: "addModels",
		value: function addModels(models) {
			var mergeOp = arguments.length <= 1 || arguments[1] === undefined ? _CollectionOptions.REPLACE_OPTION : arguments[1];
			var syncOp = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];

			if (!this.isConnected()) {
				throw new ("Collection " + this.slashPath() + " is not connected")();
			}

			var id, state;

			if (syncOp) {
				this._keyed.set(_synced, true);
			}

			for (var i = 0, len = models.length; i < len; i++) {
				this.addModel(models[i], mergeOp);
			}
		}

		/**
  	Combines an {@link CollectionProperty#add} and {@link CollectionProperty#save} actions.
  		@param {Object} model - the json data
  		@return {Promise} resolves with the f.lux shadow state for the new model
  */

	}, {
		key: "create",
		value: function create(model) {
			var _this10 = this;

			if (!this.isConnected()) {
				return _Store2.default.reject("Collection " + this.slashPath() + " is not connected");
			}

			var cid = this.addModel(model);

			return this.store().waitThen().then(function () {
				return _this10.save(cid);
			});
		}

		/**
  	Permantently deletes the model with the endpoint and removes it from the collection.
  		@param id - the model ID to delete
  		@return {Promise} resolves with the ID for the removed mdoel
  */

	}, {
		key: "destroy",
		value: function destroy(id) {
			var _this11 = this;

			if (!this.hasModel(id)) {
				return _Store2.default.resolve(this._());
			}

			try {
				var _ret = function () {
					var model = _this11._getModel(id);
					var epId = _this11.endpointId;

					if (!model) {
						return {
							v: _Store2.default.resolve(id)
						};
					} else if (model.isNew()) {
						_this11._()[_models].delete(model.cid);
						_this11._()[_id2cid].delete(model.id);

						return {
							v: _Store2.default.resolve(id)
						};
					}

					return {
						v: _this11._on(DestroyOp).then(function () {
							model.setWaiting(true);

							return _this11.endpoint.doDelete(model.id);
						}).then(function () {
							// ensure endpoint did not change
							if (epId !== _this11.endpointId) {
								return id;
							}

							_this11._()[_models].delete(model.cid);
							_this11._()[_id2cid].delete(model.id);

							_this11.store().waitFor(function () {
								return _this11.emitEvent(DeletedEvent);
							});

							return id;
						}).catch(function (error) {
							var currModel = model.$().latest();

							if (currModel) {
								currModel.setWaiting(false);
								currModel.setValidationErrors(error.validationErrors);
							}

							return _this11.onError(error, "Destroy model: " + id);
						})
					};
				}();

				if ((typeof _ret === "undefined" ? "undefined" : _typeof(_ret)) === "object") return _ret.v;
			} catch (error) {
				return this.onError(error, "Destroy model: " + id);
			}
		}

		/**
  	Fetches all the models from the endpoint.
  		@param [filter=null] - filter object created using the endpoint.
  	@param {string} [mergeOp=REPLACE_OPTION] - one of `DEFAULTS_OPTION`, `MERGE_OPTION`, or
  		`REPLACE_OPTION` and specifies how to combine an existing model with a matching ID
  		with a newly retrieved model.
  	@param {boolean} [replaceAll=true] - set to `true` replace any current models by the ones
  		returned by the endpoint.
  	@param callback - invoked before retrieved models are process or in case of an error
  		@return {Promise} resolves with the json models from the endpoint
  */

	}, {
		key: "fetch",
		value: function fetch() {
			var filter = arguments.length <= 0 || arguments[0] === undefined ? null : arguments[0];
			var mergeOp = arguments.length <= 1 || arguments[1] === undefined ? _CollectionOptions.REPLACE_OPTION : arguments[1];

			var _this12 = this;

			var replaceAll = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];
			var callback = arguments[3];

			if (!this.isConnected()) {
				return _Store2.default.reject("Collection " + this.slashPath() + " is not connected");
			}

			var syncOp = !filter;
			var epId = this.endpointId;

			if (replaceAll) {
				this.setFetching(true);
			}

			try {
				return this._on(FetchOp).then(function () {
					// ensure endpoint did not change
					if (epId !== _this12.endpointId) {
						return null;
					}

					return _this12.endpoint.doFetch(filter);
				}).then(function (models) {
					try {
						// bail on fetch if endpoint changed
						if (epId !== _this12.endpointId) {
							return models;
						}

						// invoke the callback before processing models
						callback && callback(null, models);

						// callback could have changed the sync flag
						syncOp = syncOp || _this12.nextState()[_synced];

						if (replaceAll) {
							return _this12.setModels(models, syncOp).then(function () {
								_this12.setFetching(false);

								return models;
							});
						} else {
							_this12.addModels(models, mergeOp, syncOp);
						}

						return models;
					} catch (error) {
						return _this12.onError(error, "Fetch error while setting models");
					}
				}).then(function (models) {
					// fire event if endpoint same
					if (epId === _this12.endpointId) {
						_this12.store().waitFor(function () {
							return _this12.emitEvent(FetchedEvent);
						});
					}

					return models;
				}).catch(function (error) {
					// change fetching flag only if same endpoint
					if (replaceAll && epId === _this12.endpointId) {
						_this12.setFetching(false, error);
					}

					// invoke the callback with the error
					callback && callback(error, null);

					return _this12.onError(error, "Fetch all models");
				});
			} catch (error) {
				if (replaceAll) {
					this.setFetching(false, error);
				}

				// invoke the callback with the error
				callback && callback(error, null);

				debug(function (d) {
					return d("fetch() top level error: " + (error.stack || error));
				});

				throw _Store2.default.reject(error);
			}
		}

		/**
  	Gets a model by ID. The method looks for a matching model in the collection. If one
  	is not found then one is requested from the endpoint.
  		@param id - the model ID
  	@param force - determines if locally stored model is returned if found using get().
  		true=refetch model
  		@return {Promise} reolves with the model or undefined if one is not found
  */

	}, {
		key: "find",
		value: function find(id) {
			var _this13 = this;

			var force = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1];

			if (!this.isConnected()) {
				return _Store2.default.reject("Collection " + this.slashPath() + " is not connected");
			}

			try {
				var _ret2 = function () {
					var model = _this13._getModel(id);
					var epId = _this13.endpointId;

					if (model && !force) {
						return {
							v: _Store2.default.resolve(model.data)
						};
					} else {
						return {
							v: _this13._on(FindOp).then(function () {
								// ensure endpoint did not change
								if (epId !== _this13.endpointId) {
									return null;
								}

								return _this13.endpoint.doFind(id);
							}).then(function (state) {
								// ensure endpoint did not change
								if (epId !== _this13.endpointId) {
									return null;
								}

								if (!(0, _lodash4.default)(state)) {
									throw new Error("Endpoint returned non object");
								}

								_this13.addModel(state, _CollectionOptions.REPLACE_OPTION);

								return _this13.store().wait();
							}).then(function () {
								_this13.emitEvent(FoundEvent);

								return _this13.getModel(id);
							}).catch(function (error) {
								return _this13.onError(error, "Find model " + id);
							})
						};
					}
				}();

				if ((typeof _ret2 === "undefined" ? "undefined" : _typeof(_ret2)) === "object") return _ret2.v;
			} catch (error) {
				return this.onError(error, "Find model " + id);
			}
		}

		/**
  	Fetches all the models from the endpoint and adds them to the collection.
  		@param [filter] - filter object created using the endpoint.
  		@return {Promise} resolves with the json models from the endpoint
  */

	}, {
		key: "search",
		value: function search(filter) {
			var _this14 = this;

			if (!this.isConnected()) {
				return _Store2.default.reject("Collection " + this.slashPath() + " is not connected");
			}

			try {
				var _ret3 = function () {
					var epId = _this14.endpointId;

					return {
						v: _this14._on(FindOp).then(function () {
							// ensure endpoint did not change
							if (epId !== _this14.endpointId) {
								return null;
							}

							return _this14.endpoint.doFetch(filter);
						}).then(function (models) {
							// ensure endpoint did not change
							if (epId !== _this14.endpointId) {
								return null;
							}

							_this14.addModels(models, _CollectionOptions.REPLACE_OPTION, false);

							return _this14.store().wait().then(function () {
								return models;
							});
						}).then(function (models) {
							_this14.emitEvent(FoundEvent);

							return models.map(function (m) {
								return _this14.getModel(m.id);
							});
						}).catch(function (error) {
							return _this14.onError(error, "Search models error: " + filter.toString());
						})
					};
				}();

				if ((typeof _ret3 === "undefined" ? "undefined" : _typeof(_ret3)) === "object") return _ret3.v;
			} catch (error) {
				return this.onError(error, "Find model " + id);
			}
		}

		/**
  	Gets a model by ID. The method looks for a matching model in the collection and never
  	looks for one remotely through the endpoint.
  		@param id - the model ID
  		@return {Object} the model or undefined if one is not found
  */

	}, {
		key: "getModel",
		value: function getModel(id) {
			var state = arguments.length <= 1 || arguments[1] === undefined ? this._() : arguments[1];

			var model = this._getModel(id, state);

			return model && model.data;
		}

		/**
  	Gets if the last fetch operation resulted in an error.
  		@return {boolean}
  */

	}, {
		key: "hadSyncError",
		value: function hadSyncError() {
			return !!this[_fetchingError];
		}

		/**
  	Gets if the collection contains a matching model.
  		@param id - the model ID
  		@return {boolean}
  */

	}, {
		key: "hasModel",
		value: function hasModel(id) {
			var state = arguments.length <= 1 || arguments[1] === undefined ? this._() : arguments[1];

			return state[_id2cid].has(id) || state[_models].has(id);
		}

		/**
  	Gets if the collection is active (has a shadow) and an endpoint.
  */

	}, {
		key: "isConnected",
		value: function isConnected() {
			return this._() && this._()[_endpoint] && this._()[_endpoint].isConnected();
		}

		/**
  	Gets if a fetching operation that will replace ALL models is in progress.
  */

	}, {
		key: "isFetching",
		value: function isFetching() {
			return this[_fetching];
		}

		/**
  	Gets if a model has never been persisted to the endpoint.
  */

	}, {
		key: "isNew",
		value: function isNew(id) {
			var state = arguments.length <= 1 || arguments[1] === undefined ? this._() : arguments[1];

			var model = this._getModel(id, state);

			return !model || model.isNew();
		}

		/**
  	Gets if a model shadow state model represents a model that has never been persisted
  	to the endpoint.
  */

	}, {
		key: "isNewModel",
		value: function isNewModel(shadow) {
			var id = this.extractId(shadow);

			return !id;
		}

		/**
  	Gets the shadow models currently managed by the collection.
  		@return {array} all models being managed by the collection
  */

	}, {
		key: "modelsArray",
		value: function modelsArray(state) {
			if (!state && !this.isConnected()) {
				throw new Error("Collection is not connected.");
			}

			state = state || this._();

			var models = state[_models];
			var keys = Object.keys(models);
			var result = [];

			for (var i = 0, len = keys.length; i < len; i++) {
				result.push(models[keys[i]].data);
			}

			return result;
		}

		/**
  	Gets an iterator with [ID, model] paris.
  		@return {Array}
  */

	}, {
		key: "modelEntries",
		value: function modelEntries(state) {
			var _this15 = this;

			if (!state && !this.isActive()) {
				return _akutils.doneIterator;
			}

			state = state || this._();

			var models = state[_models];
			var keys = this.modelKeysArray(state);

			return (0, _akutils.iterateOver)(keys, function (key) {
				return [key, _this15.getModel(key)];
			});
		}

		/**
  	Gets an iterator containing each model IDs.
  		@return {Iterator}
  */

	}, {
		key: "modelKeys",
		value: function modelKeys() {
			var state = arguments.length <= 0 || arguments[0] === undefined ? this._() : arguments[0];

			// too brute force but quick and sure to work
			return (0, _akutils.iteratorFor)(this.modelKeysArray(state));
		}

		/**
  	Gets an array containing each model iDs.
  		@return {Array}
  */

	}, {
		key: "modelKeysArray",
		value: function modelKeysArray(state) {
			if (!state && !this.isActive()) {
				return [];
			}

			state = state || this._();

			return Object.keys(state[_models]);
		}

		/**
  	Gets an iterator containing each f.lux shadow state model.
  		@return {Iterator}
  */

	}, {
		key: "modelValues",
		value: function modelValues() {
			var _this16 = this;

			var state = arguments.length <= 0 || arguments[0] === undefined ? this._() : arguments[0];

			return (0, _akutils.iterateOver)(this.modelKeysArray(state), function (key) {
				return _this16.getModel(key, state);
			});
		}

		/**
  	Synchronously removes the model from the collection without performing an endpoint operation.
  		@param id - the model id or cid
  */

	}, {
		key: "remove",
		value: function remove(id) {
			if (!this.hasModel(id)) {
				return;
			}

			var model = this._getModel(id);
			var prevShadow = this._();

			this._()[_models].delete(model.cid);
			this._()[_id2cid].delete(model.id);
		}

		/**
  	Removes all models from the collection and marks the collection as having not synched with the
  	endpoint.
  */

	}, {
		key: "removeAllModels",
		value: function removeAllModels() {
			if (!this.isActive()) {
				return;
			}

			this._keyed.set(_restored, false);
			this._keyed.set(_synced, false);
			this._()[_models].clear();
			this._()[_id2cid].clear();
		}

		/**
  	Saves a model through the endpoint.
  		@param id - the model id or cid
  	@param {string} [mergeOp=REPLACE_OPTION] - one of `DEFAULTS_OPTION`, `MERGE_OPTION`, or
  		`REPLACE_OPTION` and specifies how to combine an existing model with a matching ID
  		with a newly retrieved model.
  		@return {Promise} resolves to the f.lux shadow state for the saved model
  */

	}, {
		key: "save",
		value: function save(id) {
			var _this17 = this;

			var mergeOp = arguments.length <= 1 || arguments[1] === undefined ? _CollectionOptions.MERGE_OPTION : arguments[1];

			if (!this.isConnected()) {
				return _Store2.default.reject("Collection " + this.slashPath() + " is not connected");
			}

			var model = this._getModel(id);
			var cid = model && model.cid;
			var shadow = model && model.data;

			if (!model) {
				return _Store2.default.reject("Collection " + this.slashPath() + " model not found: id=" + id);
			}

			try {
				var _ret4 = function () {
					var shallow = _this17._()[_saveShallow];
					var nextState = shadow.__().nextState();
					var shadowState = shallow ? Object.keys(nextState).filter(function (k) {
						return (0, _isPrimitive2.default)(nextState[k]);
					}).reduce(function (acc, k) {
						acc[k] = nextState[k];
						return acc;
					}, {}) : nextState;
					var opName = _this17.isNewModel(shadow) ? CreateOp : UpdateOp;
					var epId = _this17.endpointId;

					return {
						v: _this17._on(opName).then(function () {
							// ensure endpoint did not change
							if (epId !== _this17.endpointId) {
								return null;
							}

							model.setWaiting(true);

							if (_this17.isNewModel(shadow)) {
								return _this17.endpoint.doCreate(shadow, shadowState);
							} else {
								return _this17.endpoint.doUpdate(model.id, shadow, shadowState);
							}
						}).then(function (savedState) {
							// ensure endpoint did not change
							if (epId !== _this17.endpointId) {
								return model;
							}

							var currModel = model.$().latest();
							var savedId = _this17.extractId(savedState);

							// ensure remote update did not come in first
							if (_this17.hasModel(savedId)) {
								_this17.remove(savedId);
							}

							currModel.setWaiting(false);
							currModel.clearValidationErrors();

							// Put an entry in id->cid mapping
							if (savedId != id) {
								_this17._()[_id2cid].set(savedId, cid);
							}

							switch (mergeOp) {
								case _CollectionOptions.NONE_OPTION:
									if (savedId !== id) {
										currModel.changeId(savedId);
									}

									break;
								case _CollectionOptions.MERGE_OPTION:
									currModel.merge(savedState);
									break;
								case _CollectionOptions.REPLACE_OPTION:
									currModel.setData(savedState);
									break;
								case _CollectionOptions.DEFAULTS_OPTION:
									currModel.defaults(savedState);
									break;
								default:
									return _Store2.default.reject("Invalid post-save option: " + mergeOp);
							}

							currModel.$$().clearDirty();

							return _this17.store().waitThen().then(function () {
								return currModel.$().latest();
							});
						}).then(function (model) {
							// ensure endpoint did not change
							if (epId === _this17.endpointId) {
								_Store2.default.setTimeout(function () {
									return _this17.emitEvent(SavedEvent);
								});
							}

							return model && model.data;
						}).catch(function (error) {
							var currModel = model.$().latest();

							if (currModel) {
								currModel.setWaiting(false);
								currModel.setValidationErrors(error.validationErrors);
							}

							return _this17.onError(error, "Save " + id + " - cid=" + currModel.cid);
						})
					};
				}();

				if ((typeof _ret4 === "undefined" ? "undefined" : _typeof(_ret4)) === "object") return _ret4.v;
			} catch (error) {
				return this.onError(error, "Save " + id + " - cid=" + model.cid);
			}
		}

		/**
  	TODO: make private
  		@ignore
  */

	}, {
		key: "setFetching",
		value: function setFetching(fetching, error) {
			var nextEp = this.__() && this.__().nextState()._endpoint;
			var nextId = nextEp && nextEp.url;

			// setting instance variable is immediate so make sure not setting fetching=true when endpoint
			// will change next tick
			if (fetching && nextId != this.endpointId) {
				return;
			}

			this[_fetching] = fetching;
			this[_fetchingError] = !fetching && error;

			this.touch("CollectionProperty.setFetching(" + fetching + ")");
		}

		/**
  	Sets the property name containing the model ID. Default is `id`.
  */

	}, {
		key: "setIdName",
		value: function setIdName(idName) {
			this._()[_idName] = idName;
		}

		/**
  	Bulk replaces current models with an array of new models.
  		@param {array} models - array of model values
  	@param {boolean} [syncOp=true] - sets the `synced` flag to true if this parameter is true
  */

	}, {
		key: "setModels",
		value: function setModels(models) {
			var _this18 = this;

			var syncOp = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1];

			this.removeAllModels();

			this.store().updateNow(function () {
				_this18.addModels(models, _CollectionOptions.REPLACE_OPTION, syncOp);
			});

			return this.$().wait();
		}

		//------------------------------------------------------------------------------------------------------
		// Methods Collection subclasses may want to override
		//------------------------------------------------------------------------------------------------------

		/*
  	Generates an ID for a new object. The default implementation generates: temp-[UUID].
  */

	}, {
		key: "makeId",
		value: function makeId() {
			return "temp_" + (0, _akutils.uuid)('_');
		}

		/*
  	Subclasses can override this method to remove underlying transport specific properties.
  */

	}, {
		key: "clean",
		value: function clean(json) {
			return json;
		}

		/**
  	Used by the private _shadow() method to get the id from the model JSON representation as returned by the
  	subclass doXXX() apis. The default implementation simply returns the 'id' model property.
  */

	}, {
		key: "extractId",
		value: function extractId(model) {
			var idName = this._()[_idName];

			return (0, _lodash4.default)(model) ? model[idName] : model instanceof _Shadow2.default ? model.toJSON()[idName] : model;
		}

		/** @ignore */

	}, {
		key: "onError",
		value: function onError(error, opMsg) {
			var msg;

			if (error.status) {
				var statusText = error.status ? error.response.statusText || "HTTP Code=" + error.status : "HTTP status code not specified";
				msg = "Error during collection operation '" + opMsg + " (" + this.endpointId + ")' - " + ("Server Error: status=" + statusText);
			} else if (error.message) {
				msg = "Error during collection operation '" + opMsg + " (" + this.endpointId + ")' - Collection Error: " + error.message;
			} else {
				msg = "Error during collection operation '" + opMsg + " (" + this.endpointId + ")' - Error: " + error;
			}

			debug(function (d) {
				return d("Error: " + msg, error);
			});

			var collectionError = new Error(msg);

			collectionError.status = error.status;
			collectionError.endpointError = error;

			this.emitEvent(ErrorEvent, collectionError);

			return _Store2.default.reject(collectionError);
		}

		//------------------------------------------------------------------------------------------------------
		// Private methods
		//------------------------------------------------------------------------------------------------------

		/**
  	Gets the Model container object NOT the actual model.
  		@ignore
  */

	}, {
		key: "_getModel",
		value: function _getModel(id) {
			var state = arguments.length <= 1 || arguments[1] === undefined ? this._() : arguments[1];

			var id2cid = state[_id2cid];
			var cid = id2cid.has(id) ? id2cid.get(id) : id;

			return state[_models].get(cid);
		}
	}, {
		key: "endpoint",
		get: function get() {
			return this.isActive() && this._()[_endpoint];
		}

		/**
  	Gets the endpoint ID, which for {@link RestEndpointProperty} is its URL.
  		@return {string}
  */

	}, {
		key: "endpointId",
		get: function get() {
			var endpoint = this.endpoint;

			return endpoint && endpoint.id;
		}
	}, {
		key: "size",
		get: function get() {
			return this.isActive() && this._().size;
		}
	}], [{
		key: "createClass",
		value: function createClass() {
			var shadowType = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
			var typeCallback = arguments[1];
			var initialState = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];

			return (0, _createPropertyClass2.default)(shadowType, initialState, typeCallback, CollectionProperty, _CollectionShadow2.default);
		}

		/**
  	Factory function for setting up the {@link StateType} `type` class variable with an appropriately
  	configured intial state.
  		Example usage:
  	```
         export default class TodosCollection extends CollectionProperty {
  		// implement property here
         }
          class TodosShadow extends CollectionShadow {
  		// implement shadow api here
         }
          ObjectProperty.defineType(TodosCollection, TodosShadow, type => {
  		// configure type variable
         });
  	```
  		@param {CollectionShadow} PropClass - `CollectionShadow` subclass
  	@param {Object|CollectionShadow} [ShadowType] - Shadow` subclass or object literal api definition.
  		If object literal specified, each property and function is mapped onto a Shadow subclass.
  	@param {function(type: StateType)} [typeCallback] - a callback function that will be passed the
  		{@link StateType} spec for additional customization, such as setting autoshadow, initial state, or
  		readonly.
  	@param {Object} [initialState={}] - the initial state for the new property.
  */

	}, {
		key: "defineType",
		value: function defineType(PropClass, ShadowType, typeCallback) {
			var initialState = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3];

			(0, _akutils.assert)(function (a) {
				return a.is(CollectionProperty.isPrototypeOf(PropClass), "PropClass must subclass CollectionProperty");
			});

			return _StateType2.default.defineTypeEx(PropClass, ShadowType, typeCallback, initialState);
		}

		/**
  	Used by {@link StateType} to determine if a keyed property type.
  		@ignore
  */

	}, {
		key: "supportsKeyedChildProperties",
		value: function supportsKeyedChildProperties() {
			return true;
		}
	}]);

	return CollectionProperty;
}(_Property3.default);

exports.default = CollectionProperty;


_StateType2.default.defineType(CollectionProperty, function (spec) {
	var _spec$initialState$au;

	spec.initialState({}).autoshadowOff.managedType(_MapProperty2.default.type).properties((_spec$initialState$au = {}, _defineProperty(_spec$initialState$au, _offlineKey, _PrimitiveProperty2.default.type.initialState(null).readonlyOff), _defineProperty(_spec$initialState$au, _restored, _PrimitiveProperty2.default.type.initialState(false).readonly), _spec$initialState$au)).typeName("CollectionProperty");
});

// Mix in `Emitter`
(0, _componentEmitter2.default)(CollectionProperty.prototype);