lib/datastores/memorydatastore.js
/*********************************************************************************
* Copyright 2015 Yahoo Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
********************************************************************************/
'use strict';
import uuid from 'uuid';
import jsonpatch from 'fast-json-patch';
import Datastore from './datastore';
import ChangeWatcher from '../helpers/change-watcher';
import clone from '../helpers/clone';
import debug from 'debug';
// jscs:disable maximumLineLength
export let ERROR_UNKNOWN_MODEL = 'Unknown model "${model}" passed to #${method}.';
export let ERROR_NO_UPSTREAM = 'No upstream store to commit to.';
export let ERROR_NO_STATE_GIVEN = 'No state passed to #${method}';
export let ERROR_CANNOT_FIND_ON_PROP = '"${property}" not a linked property on "${lastModel}".';
export let ERROR_CANNOT_CREATE_WITH_ID = 'Newly created records must not specify an id.';
export let ERROR_CANNOT_UPDATE_WITHOUT_ID = 'You must specify an id in order to modify a record.';
export let ERROR_CANNOT_UPDATE_MISSING_RECORD = 'The "${model}" passed to #${method} does not exist.';
export let ERROR_CANNOT_LINK_TO_MISSING_RECORD = 'The "${model}":${id} passed to #${method} does not exist.';
// jscs:enable maximumLineLength
let OWNS = '>';
let OWNED_BY = '<';
let HAS_ONE = 'hasOne';
let HAS_MANY = 'hasMany';
const log = debug('elide:memorystore');
class MemoryDatastore extends Datastore {
constructor(Promise, ttl, baseURL, models) {
super(Promise, ttl, baseURL, models);
this._data = {};
this._meta = {};
this._ttlCache = {};
let annotatedModels = clone(this._models);
Object.keys(annotatedModels).forEach((modelName) => {
let model = annotatedModels[modelName];
this._data[modelName] = {};
this._meta[modelName] = {};
this._ttlCache[modelName] = {};
Object.keys(model.links).forEach(function(link) {
let linkage = model.links[link];
annotatedModels[modelName][link] = linkage.type + OWNS + linkage.model;
if (linkage.inverse) {
annotatedModels[modelName][link] += '.' + linkage.inverse;
annotatedModels[linkage.model][linkage.inverse] =
linkage.type + OWNED_BY + modelName + '.' + link;
}
});
delete annotatedModels[modelName].links;
});
this._models = annotatedModels;
this._snapshot = clone(this._data);
}
/**
* checks to see if the model that we're looking for exists in our schema
* throws an error if the model isn't present
* @param {string} model - the name of the model we're looking for
* @param {string} method - the name of the method we're calling from
*/
_checkModel(model, method) {
if (!this._models.hasOwnProperty(model)) {
throw new Error(ERROR_UNKNOWN_MODEL
.replace('${model}', model)
.replace('${method}', method));
}
}
/**
* get a model template
* @param {string} model - the name of the model
* @return {object} a copy of the model definition
*/
_getModelTemplate(model) {
return clone(this._models[model]);
}
/**
* get data from the store
* @param {string} model - the name of the model
* @param {number} id - the model's id (could be string in case of uuid)
* @return {object} - a copy of the model
*/
_getData(model, id) {
let ttlExpiry = (new Date()).getTime() - this._ttl;
id = this._meta[model][id] || id;
if (this._ttlCache[model][id] && this._ttlCache[model][id] < ttlExpiry) {
return undefined;
}
return clone(this._data[model][id]);
}
/**
* set data into the store
*
* @param {string} model - the name of the model
* @param {number} instance - the instance data to set
*/
_setData(model, instance) {
this._data[model][instance.id] = clone(instance);
}
/**
* copy data returned from upstream into this store so we won't refetch
* it if it gets queried again
*
* @param {Query} query - the query executed upstream
* @param {Array} results - the results returned from the upstream datastore
*/
_setDataFromUpstreamQuery(query, results) {
let model;
if (results === undefined || results.length === 0) {
return;
}
query._params.forEach((param) => {
if (param.model) {
model = param.model;
} else {
let modelTemplate = this._getModelTemplate(model);
let linkDef = modelTemplate[param.field];
[ , , model, ] = this._getLinkAttrs(linkDef);
}
});
if (results instanceof Array) {
for (let i = 0; i < results.length; i++) {
let result = results[i];
this._data[model][result.id] = result;
this._ttlCache[model][result.id] = (new Date()).getTime();
this._snapshot[model][result.id] = result;
}
} else {
this._data[model][results.id] = results;
this._ttlCache[model][results.id] = (new Date()).getTime();
this._snapshot[model][results.id] = results;
}
}
/**
* delete data from the store
* @param {string} model - the name of the model
* @param {number} id - the model's id (could be string in case of uuid)
*/
_deleteData(model, id) {
id = this._meta[model][id] || id;
delete this._data[model][id];
}
/**
* determine if the specified property is a link
* @param {string} model - the name of the model
* @param {string} prop - the property to check
* @return {boolean} if the property is linked
*/
_isLinkedProp(model, prop) {
if (prop === 'meta') { return; }
return this._models[model][prop].search(/hasOne|hasMany/) !== -1;
}
/**
* get various attribues of a linkage between two models
* @return [type, direction, toModel, toProp]
* type - what kind of link (hasOne|hasMany)
* direction - OWNS or OWNED_BY
* toModel - the name of a model
* toProp - the name of the property on that model (may be `undefined`)
*/
_getLinkAttrs(linkDef) {
let matches = linkDef.match(/(hasOne|hasMany)(<|>)(\w+)(\.\w+)?/);
let [ , type, direction, otherModel, otherProp] = matches;
if (otherProp) {
otherProp = otherProp.substr(1);
}
return [type, direction, otherModel, otherProp];
}
/**
* check to ensure that the referenced object(s) exist
* @param {string} model - the model we are verifying
* @param {number|Array} value - an id or an array of ids
* @param {string} method - the method checking these ids
* @return [willReject, withReason]
* willReject - false if all of the specified ids exist
* withReason - the error if `willReject == true`
*/
_ensureReferencesExist(model, value, method) {
let willReject = false;
let withReason;
if (!value) {
return [willReject, withReason];
}
if (value instanceof Array) {
for (let i = 0; i < value.length; i++) {
if (!this._getData(model, value[i])) {
willReject = true;
withReason = ERROR_CANNOT_LINK_TO_MISSING_RECORD
.replace('${model}', model)
.replace('${id}', value[i])
.replace('${method}', method);
break;
}
}
} else if (!this._getData(model, value)) {
willReject = true;
withReason = ERROR_CANNOT_LINK_TO_MISSING_RECORD
.replace('${model}', model)
.replace('${id}', value)
.replace('${method}', method);
}
return [willReject, withReason];
}
/**
* copy the atomic properties from `state` to `instance`
* @param {string} model - the type of `instance`
* @param {object} instance - the object to modify
* @param {object} state - the new values for atomic properties of `instance`
*/
_updateSimpleProps(model, instance, state) {
let template = this._getModelTemplate(model);
Object.keys(template).forEach((prop) => {
if (this._isLinkedProp(model, prop) || prop === 'meta') {
return;
}
if (instance.hasOwnProperty(prop) && !state.hasOwnProperty(prop)) {
return;
}
instance[prop] = state[prop];
});
}
/**
* create the keys on `instance` with empty values so they can be iterated
* @param {object} instance - the object to receive the property
* @param {string} prop - the property
* @param {string} type - the type of the property
* @param {string} direction - if the object is the root or leaf of the relationship
*/
_setEmptyLinkedProperty(instance, prop, type, direction) {
if (type === 'hasMany' && direction === OWNS) {
instance[prop] = [];
} else {
instance[prop] = undefined;
}
}
/**
* add a leaf model to the root model. if the leaf is currently linked to a root we
* will unlink it from it's current root
* @param {string} rootModel - the name of the root model
* @param {string} rootProp - the name of the property on the root
* @param {Object} rootInstance - the instance of the root model to link
* @param {string} leafModel - the name of the leaf model
* @param {string} leafProp - the name of the property on the leaf model
* @param {Object} leafInstance - the instance of the leaf model to link
* @param {ChangeWatcher} watcher - the watcher which will gather changes to the involved objects
*/
// jscs:disable maximumLineLength
_addSingleLeaf(rootModel, rootProp, rootInstance, leafModel, leafProp, leafInstance, watcher) {
// jscs:enable maximumLineLength
if (rootInstance[rootProp] !== undefined) {
// we need to unlink the current value
let curLeaf = this._getData(leafModel, rootInstance[rootProp]);
watcher.watchModel(curLeaf, leafModel);
this._removeSingleLeaf(rootModel, rootProp, rootInstance,
leafModel, leafProp, curLeaf);
}
if (leafInstance === undefined) {
rootInstance[rootProp] = undefined;
return;
}
rootInstance[rootProp] = leafInstance.id;
if (leafProp !== undefined) {
// link the inverse
if (leafInstance[leafProp]) {
let curRoot = this._getData(rootModel, leafInstance[leafProp]);
watcher.watchModel(curRoot, rootModel);
this._removeSingleRoot(rootModel, rootProp, curRoot,
leafModel, leafProp, leafInstance);
}
leafInstance[leafProp] = rootInstance.id;
}
}
/**
* add a root model to the leaf model. if the root is currently linked to a leaf we
* will unlink it from it's current leaf
* @param {string} rootModel - the name of the root model
* @param {string} rootProp - the name of the property on the root
* @param {Object} rootInstance - the instance of the root model to link
* @param {string} leafModel - the name of the leaf model
* @param {string} leafProp - the name of the property on the leaf model
* @param {Object} leafInstance - the instance of the leaf model to link
* @param {ChangeWatcher} watcher - the watcher which will gather changes to the involved objects
*/
// jscs:disable maximumLineLength
_addSingleRoot(rootModel, rootProp, rootInstance, leafModel, leafProp, leafInstance, watcher) {
// jscs:enable maximumLineLength
if (leafInstance[leafProp] !== undefined) {
let curRoot = this._getData(rootModel, leafInstance[leafProp]);
watcher.watchModel(curRoot, rootModel);
this._removeSingleRoot(rootModel, rootProp, curRoot,
leafModel, leafProp, leafInstance);
}
if (rootInstance === undefined) {
leafInstance[leafProp] = undefined;
return;
}
leafInstance[leafProp] = rootInstance.id;
if (rootInstance[rootProp] !== undefined) {
// we need to unlink the current value
let curLeaf = this._getData(leafModel, rootInstance[rootProp]);
watcher.watchModel(curLeaf, leafModel);
this._removeSingleLeaf(rootModel, rootProp, rootInstance,
leafModel, leafProp, curLeaf);
}
rootInstance[rootProp] = leafInstance.id;
}
/**
* remove a leaf from its current root
* @param {string} rootModel - the name of the root model
* @param {string} rootProp - the name of the property on the root
* @param {Object} rootInstance - the instance of the root model to link
* @param {string} leafModel - the name of the leaf model
* @param {string} leafProp - the name of the property on the leaf model
* @param {Object} leafInstance - the instance of the leaf model to link
* @param {ChangeWatcher} watcher - the watcher which will gather changes to the involved objects
*/
// jscs:disable maximumLineLength
_removeSingleLeaf(rootModel, rootProp, rootInstance, leafModel, leafProp, leafInstance, watcher) {
// jscs:enable maximumLineLength
if (leafProp !== undefined) {
leafInstance[leafProp] = undefined;
}
rootInstance[rootProp] = undefined;
}
/**
* remove a root from its current leaf
* @param {string} rootModel - the name of the root model
* @param {string} rootProp - the name of the property on the root
* @param {Object} rootInstance - the instance of the root model to link
* @param {string} leafModel - the name of the leaf model
* @param {string} leafProp - the name of the property on the leaf model
* @param {Object} leafInstance - the instance of the leaf model to link
* @param {ChangeWatcher} watcher - the watcher which will gather changes to the involved objects
*/
// jscs:disable maximumLineLength
_removeSingleRoot(rootModel, rootProp, rootInstance, leafModel, leafProp, leafInstance, watcher) {
// jscs:enable maximumLineLength
rootInstance[rootProp] = undefined;
leafInstance[leafProp] = undefined;
}
/**
* add a leaf to a root in a one-to-many property
* @param {string} rootModel - the name of the root model
* @param {string} rootProp - the name of the property on the root
* @param {Object} rootInstance - the instance of the root model to link
* @param {string} leafModel - the name of the leaf model
* @param {string} leafProp - the name of the property on the leaf model
* @param {Object} leafInstance - the instance of the leaf model to link
* @param {ChangeWatcher} watcher - the watcher which will gather changes to the involved objects
*/
// jscs:disable maximumLineLength
_addMultiLeaf(rootModel, rootProp, rootInstance, leafModel, leafProp, leafInstance, watcher) {
// jscs:enable maximumLineLength
if (rootInstance[rootProp].indexOf(leafInstance.id) === -1) {
rootInstance[rootProp].push(leafInstance.id);
}
if (leafProp !== undefined) {
if (leafInstance[leafProp] !== rootInstance.id) {
let curRoot = this._getData(rootModel, leafInstance[leafProp]);
watcher.watchModel(curRoot, rootModel);
this._removeMultiRoot(rootModel, rootProp, curRoot,
leafModel, leafProp, leafInstance);
}
leafInstance[leafProp] = rootInstance.id;
}
}
/**
* add a root to a leaf in a one-to-many property
* @param {string} rootModel - the name of the root model
* @param {string} rootProp - the name of the property on the root
* @param {Object} rootInstance - the instance of the root model to link
* @param {string} leafModel - the name of the leaf model
* @param {string} leafProp - the name of the property on the leaf model
* @param {Object} leafInstance - the instance of the leaf model to link
* @param {ChangeWatcher} watcher - the watcher which will gather changes to the involved objects
*/
// jscs:disable maximumLineLength
_addMultiRoot(rootModel, rootProp, rootInstance, leafModel, leafProp, leafInstance, watcher) {
// jscs:enable maximumLineLength
if (rootInstance === undefined) {
leafInstance[leafProp] = undefined;
return;
}
if (rootInstance[rootProp].indexOf(leafInstance.id) === -1) {
rootInstance[rootProp].push(leafInstance.id);
}
if (leafInstance[leafProp] !== rootInstance.id) {
let curRoot = this._getData(rootModel, leafInstance[leafProp]);
watcher.watchModel(curRoot, rootModel);
this._removeMultiLeaf(rootModel, rootProp, curRoot,
leafModel, leafProp, leafInstance);
}
leafInstance[leafProp] = rootInstance.id;
}
/**
* remove a leaf in a one-to-many property
* @param {string} rootModel - the name of the root model
* @param {string} rootProp - the name of the property on the root
* @param {Object} rootInstance - the instance of the root model to link
* @param {string} leafModel - the name of the leaf model
* @param {string} leafProp - the name of the property on the leaf model
* @param {Object} leafInstance - the instance of the leaf model to link
* @param {ChangeWatcher} watcher - the watcher which will gather changes to the involved objects
*/
// jscs:disable maximumLineLength
_removeMultiLeaf(rootModel, rootProp, rootInstance, leafModel, leafProp, leafInstance, watcher) {
// jscs:enable maximumLineLength
if (rootInstance === undefined) {
return;
}
if (rootInstance[rootProp].indexOf(leafInstance.id) !== -1) {
rootInstance[rootProp] = rootInstance[rootProp].filter((ele) => {
return ele !== leafInstance.id;
});
}
if (leafProp !== undefined) {
leafInstance[leafProp] = undefined;
}
}
/**
* unlink a leaf from a root in a one-to-many property
* @param {string} rootModel - the name of the root model
* @param {string} rootProp - the name of the property on the root
* @param {Object} rootInstance - the instance of the root model to link
* @param {string} leafModel - the name of the leaf model
* @param {string} leafProp - the name of the property on the leaf model
* @param {Object} leafInstance - the instance of the leaf model to link
* @param {ChangeWatcher} watcher - the watcher which will gather changes to the involved objects
*/
// jscs:disable maximumLineLength
_removeMultiRoot(rootModel, rootProp, rootInstance, leafModel, leafProp, leafInstance, watcher) {
// jscs:enable maximumLineLength
if (rootInstance !== undefined) {
rootInstance[rootProp] = rootInstance[rootProp].filter((ele) => {
return ele !== leafInstance.id;
});
}
if (leafProp !== undefined) {
leafInstance[leafProp] = undefined;
}
}
/**
* updates the properties on an object that are links to other models
* @param {string} model - the name of the model we are modifying
* @param {Object} instance - the instance of `model` we are updating
* @param {Object} state - the new state for `instance`
* @param {boolean} createProps - a flag that will create empty instances of the properties on `instance`
* @param {string} method - where this is being called from (for error tracking)
*/
_updateLinkProperties(model, instance, state, createProps, method) {
let willReject = false;
let withReason;
let watcher = new ChangeWatcher();
let template = this._getModelTemplate(model);
watcher.watchModel(instance, model);
// relational properties in the second pass
Object.keys(template).some((prop) => {
if (!this._isLinkedProp(model, prop)) {
return false;
}
let value = instance[prop];
if (state.hasOwnProperty(prop)) {
value = state[prop];
}
// properties which link to other models
let [
type,
direction,
otherModel,
otherProp
] = this._getLinkAttrs(template[prop]);
if (createProps) {
this._setEmptyLinkedProperty(instance, prop, type, direction);
}
if (!value && type === HAS_MANY && direction === OWNS) {
value = [];
}
[
willReject,
withReason
] = this._ensureReferencesExist(otherModel, value, method);
if (willReject) {
return true;
}
if (type === HAS_ONE) {
if (direction === OWNS) {
let leaf = this._getData(otherModel, value);
watcher.watchModel(leaf, otherModel);
this._addSingleLeaf(model, prop, instance,
otherModel, otherProp, leaf, watcher);
} else {
let root = this._getData(otherModel, value);
watcher.watchModel(root, otherModel);
this._addSingleRoot(otherModel, otherProp, root,
model, prop, instance, watcher);
}
} else if (type === HAS_MANY) {
if (direction === OWNS) {
let toRemove = instance[prop].filter((element) => {
return value.indexOf(element) === -1;
});
for (let i = 0; i < toRemove.length; i++) {
let leaf = this._getData(otherModel, toRemove[i]);
watcher.watchModel(leaf, otherModel);
this._removeMultiLeaf(model, prop, instance,
otherModel, otherProp, leaf, watcher);
}
for (let i = 0; i < value.length; i++) {
let leaf = this._getData(otherModel, value[i]);
watcher.watchModel(leaf, otherModel);
this._addMultiLeaf(model, prop, instance,
otherModel, otherProp, leaf, watcher);
}
} else {
let root = this._getData(otherModel, value);
watcher.watchModel(root, otherModel);
this._addMultiRoot(otherModel, otherProp, root,
model, prop, instance, watcher);
}
}
});
let patches = watcher.getPatches();
return [patches, willReject, withReason];
}
//
// Datastore implementation
//
find(query) {
let willReject = false;
let withReason;
let foundObject;
let foundObjects = [];
let wantsCollection = false;
let lastModel;
let modelTemplate;
// call for error handling, ignore return value
super.find(query);
query._params.some((param) => {
let field = param.field;
if (param.model) {
lastModel = param.model;
modelTemplate = this._getModelTemplate(lastModel);
} else if (!this._isLinkedProp(lastModel, field)) {
willReject = true;
withReason = ERROR_CANNOT_FIND_ON_PROP
.replace('${model}', lastModel)
.replace('${property}', field);
return true;
} else {
let linkDef = modelTemplate[param.field];
[ , , lastModel, ] = this._getLinkAttrs(linkDef);
modelTemplate = this._getModelTemplate(lastModel);
}
this._checkModel(lastModel, 'find');
});
if (willReject) {
return this._promise.reject(new Error(withReason));
}
let getIdsFromField = function(obj, field) {
if (obj[field] instanceof Array) {
return obj[field];
} else {
return [obj[field]];
}
};
for (let i = 0; i < query._params.length; i++) {
let param = query._params[i];
let field = param.field;
wantsCollection = param.id === undefined;
if (i === 0) {
lastModel = param.model;
} else {
let linkDef = this._getModelTemplate(lastModel)[field];
[ , , lastModel, ] = this._getLinkAttrs(linkDef);
}
let allIds = [];
if (foundObject) {
allIds = getIdsFromField(foundObject, field);
} else if (foundObjects.length > 0) {
for (let i = 0; i < foundObjects.length; i++) {
let obj = foundObjects[i];
allIds = allIds.concat(getIdsFromField(obj, field));
}
} else if (wantsCollection) {
allIds = Object.keys(this._data[lastModel]);
} else {
allIds = [param.id];
}
if (wantsCollection) {
foundObjects = [];
foundObject = undefined;
for (let i = 0; i < allIds.length; i++) {
let id = allIds[i];
let obj = this._getData(lastModel, id);
if (obj) {
foundObjects.push(obj);
}
}
} else {
let foundId;
if (allIds.indexOf(param.id) !== -1) {
foundId = param.id;
}
foundObject = this._getData(lastModel, foundId);
foundObjects = [];
}
if (!wantsCollection && foundObject === undefined ||
wantsCollection && foundObjects.length === 0) {
break;
}
}
if (this._upstream !== undefined &&
foundObject === undefined &&
foundObjects.length === 0) {
log('Query not fulfilled locally, going upstream');
return this._upstream.find(query).then((upstreamResults) => {
this._setDataFromUpstreamQuery(query, upstreamResults);
return upstreamResults;
}, (upstreamErr) => {
throw upstreamErr;
});
}
let result = wantsCollection ? foundObjects : foundObject;
return this._promise.resolve(result);
}
create(model, state) {
this._checkModel(model, 'create');
let willReject = false;
let withReason;
let toReturn;
let toCreate = {};
if (!state) {
let err = new Error(ERROR_NO_STATE_GIVEN.replace('${method}', 'create'));
return this._promise.reject(err);
} else if (state.id) {
let err = new Error(ERROR_CANNOT_CREATE_WITH_ID);
return this._promise.reject(err);
} else {
let id = uuid.v4();
toCreate.id = id;
let relationPatches = [];
this._updateSimpleProps(model, toCreate, state);
// we link objects by applying patches after we determine that the operation
// will succeed, so we need to create a copy so that we can insert the unlinked
// version into our store and return the linked version to the client
toReturn = clone(toCreate);
[
relationPatches,
willReject,
withReason
] = this._updateLinkProperties(model, toReturn, state, true, 'create');
if (!willReject) {
this._setData(model, toCreate);
jsonpatch.apply(this._data, relationPatches);
} else {
return this._promise.reject(new Error(withReason));
}
}
return this._promise.resolve(toReturn);
}
update(model, state) {
this._checkModel(model, 'update');
let willReject = false;
let withReason;
let toUpdate;
if (!state) {
let err = new Error(ERROR_NO_STATE_GIVEN
.replace('${method}', 'update'));
return this._promise.reject(err);
} else if (!state.id) {
let err = new Error(ERROR_CANNOT_UPDATE_WITHOUT_ID);
return this._promise.reject(err);
} else if (!this._getData(model, state.id)) {
let err = new Error(ERROR_CANNOT_UPDATE_MISSING_RECORD
.replace('${model}', model)
.replace('${method}', 'update'));
return this._promise.reject(err);
} else {
let relationPatches = [];
toUpdate = this._getData(model, state.id);
this._updateSimpleProps(model, toUpdate, state);
[
relationPatches,
willReject,
withReason
] = this._updateLinkProperties(model, toUpdate, state, false, 'update');
if (!willReject) {
jsonpatch.apply(this._data, relationPatches);
this._setData(model, toUpdate);
} else {
return this._promise.reject(new Error(withReason));
}
}
return this._promise.resolve(toUpdate);
}
delete(model, state) {
this._checkModel(model, 'delete');
let willReject = false;
let withReason;
if (!state) {
let err = new Error(ERROR_NO_STATE_GIVEN.replace('${method}', 'delete'));
return this._promise.reject(err);
} else if (!state.id) {
let err = new Error(ERROR_CANNOT_UPDATE_WITHOUT_ID);
return this._promise.reject(err);
} else if (!this._getData(model, state.id)) {
let err = new Error(ERROR_CANNOT_UPDATE_MISSING_RECORD
.replace('${model}', model)
.replace('${method}', 'delete'));
return this._promise.reject(err);
} else {
this._deleteData(model, state.id);
}
return this._promise.resolve();
}
/**
* commit pushes all pending operations upstream
*
* @param {none} _ - we ignore this value in the memorydatastore
* @return {Promise} - the promise resolves if the commit succeedes and rejects if the
* commit fails, in either case there is no return value. If the promise
* local copys of objects be refetched after the commit if the promise
* rejects or if the promise succeedes and is called with `true`
*/
commit(_) {
let willReject = false;
if (!this._upstream) {
return this._promise.reject(new Error(ERROR_NO_UPSTREAM));
}
let patches = jsonpatch.compare(this._snapshot, this._data);
return this._upstream.commit(patches).then((upstreamResults) => {
if (upstreamResults) {
for (let i = 0; i < upstreamResults.length; i++) {
let result = upstreamResults[i];
let type = result.type;
let data = result.data;
let curId = data.id;
let lastId = result.oldId;
if (lastId && lastId !== curId) {
this._meta[type][lastId] = curId;
delete this._data[type][lastId];
}
this._data[type][curId] = data;
}
}
this._snapshot = clone(this._data);
return this._promise.resolve();
}, (upstreamError) => {
this._data = clone(this._snapshot);
throw upstreamError;
});
}
dehydrate() {
let dehydratedData = JSON.stringify(this._data);
let dehydratedSnapshot = JSON.stringify(this._snapshot);
if (dehydratedData !== dehydratedSnapshot) {
throw new Error('Cannot dehydrate MemoryStore with uncommitted data');
}
let dehydratedMeta = JSON.stringify(this._meta);
return {
dehydratedMeta,
dehydratedData
};
}
rehydrate(state) {
let {
dehydratedMeta,
dehydratedData
} = state;
this._data = JSON.parse(dehydratedData);
this._meta = JSON.parse(dehydratedMeta);
this._snapshot = clone(this._data);
}
}
export default MemoryDatastore;