UNPKG

dbjs-persistence

Version:
1,359 lines (1,332 loc) 54.8 kB
// Abstract Storage Persistence driver 'use strict'; var aFrom = require('es5-ext/array/from') , compact = require('es5-ext/array/#/compact') , flatten = require('es5-ext/array/#/flatten') , isCopy = require('es5-ext/array/#/is-copy') , uniq = require('es5-ext/array/#/uniq') , ensureArray = require('es5-ext/array/valid-array') , customError = require('es5-ext/error/custom') , ensureIterable = require('es5-ext/iterable/validate-object') , iterableForEach = require('es5-ext/iterable/for-each') , assign = require('es5-ext/object/assign') , ensureNaturalNumber = require('es5-ext/object/ensure-natural-number') , forEach = require('es5-ext/object/for-each') , toArray = require('es5-ext/object/to-array') , ensureCallable = require('es5-ext/object/valid-callable') , ensureObject = require('es5-ext/object/valid-object') , ensureString = require('es5-ext/object/validate-stringifiable-value') , capitalize = require('es5-ext/string/#/capitalize') , startsWith = require('es5-ext/string/#/starts-with') , isSet = require('es6-set/is-set') , deferred = require('deferred') , emitError = require('event-emitter/emit-error') , d = require('d') , autoBind = require('d/auto-bind') , lazy = require('d/lazy') , debug = require('debug')('db') , Set = require('es6-set') , ee = require('event-emitter') , genStamp = require('time-uuid/time') , ensureObservableSet = require('observable-set/valid-observable-set') , unserializeValue = require('dbjs/_setup/unserialize/value') , serializeValue = require('dbjs/_setup/serialize/value') , serializeKey = require('dbjs/_setup/serialize/key') , isGetter = require('dbjs/_setup/utils/is-getter') , resolveKeyPath = require('dbjs/_setup/utils/resolve-key-path') , resolvePropertyPath = require('dbjs/_setup/utils/resolve-property-path') , ensureStorage = require('./ensure-storage') , getSearchValueFilter = require('./lib/get-search-value-filter') , filterComputedValue = require('./lib/filter-computed-value') , isObjectPart = require('./lib/is-object-part') , resolveValue = require('./lib/resolve-direct-value') , resolveFilter = require('./lib/resolve-filter') , resolveDirectFilter = require('./lib/resolve-direct-filter') , resolveMultipleEvents = require('./lib/resolve-multiple-events') , resolveEventKeys = require('./lib/resolve-event-keys') , isArray = Array.isArray, defineProperty = Object.defineProperty, stringify = JSON.stringify , nextTick = process.nextTick , resolved = deferred(undefined) , isDigit = RegExp.prototype.test.bind(/[0-9]/) , isObjectId = RegExp.prototype.test.bind(/^[0-9a-z][0-9a-zA-Z]*$/) , isDbId = RegExp.prototype.test.bind(/^[0-9a-z][^\n]*$/) , isModelId = RegExp.prototype.test.bind(/^[A-Z]/) , incrementStamp = genStamp.increment , tokenize = resolvePropertyPath.tokenize, resolveObject = resolvePropertyPath.resolveObject , create = Object.create, defineProperties = Object.defineProperties, keys = Object.keys , dataByStampRev = function (a, b) { return b.data.stamp - a.data.stamp; }; var byStamp = function (a, b) { var aStamp = this[a] ? this[a].stamp : 0, bStamp = this[b] ? this[b].stamp : 0; return (aStamp - bStamp) || a.toLowerCase().localeCompare(b.toLowerCase()); }; var Storage = Object.defineProperties(function (driver, name/*, options*/) { var options, autoSaveFilter; if (!(this instanceof Storage)) return new Storage(driver, name, arguments[2]); this.driver = driver; this.name = name; if (this.driver.database) { options = Object(arguments[2]); autoSaveFilter = (options.autoSaveFilter != null) ? ensureCallable(options.autoSaveFilter) : this.constructor.defaultAutoSaveFilter; this._registerDatabase(autoSaveFilter); } }, { defaultAutoSaveFilter: d(function (event) { return !isModelId(event.object.master.__id__); }) }); module.exports = Storage; var notImplemented = function () { throw customError("Not implemented", 'NOT_IMPLEMENTED'); }; var ensureOwnerId = function (ownerId) { ownerId = ensureString(ownerId); if (!isObjectId(ownerId)) throw new TypeError(ownerId + " is not a database object id"); return ownerId; }; var trimValue = function (value) { if (isArray(value)) value = '[' + String(value) + ']'; if (value.length > 200) return value.slice(0, 200) + '…'; return value; }; ee(Object.defineProperties(Storage.prototype, assign({ get: d(function (id) { var index, ownerId, path, uncertain; id = ensureString(id); if (!isDbId(id)) throw new TypeError(id + " is not a database value id"); index = id.indexOf('/'); ownerId = (index !== -1) ? id.slice(0, index) : id; path = (index !== -1) ? id.slice(index + 1) : null; this._ensureOpen(); uncertain = this._uncertain.direct[ownerId]; if (uncertain && uncertain[path || '']) return uncertain[path || '']; ++this._runningOperations; return this._getRaw('direct', ownerId, path).finally(this._onOperationEnd); }), getComputed: d(function (id) { var ownerId, keyPath, index, uncertain; id = ensureString(id); index = id.indexOf('/'); if (index === -1) { throw customError("Invalid computed id " + stringify(id), 'INVALID_COMPUTED_ID'); } ownerId = id.slice(0, index); keyPath = id.slice(index + 1); this._ensureOpen(); uncertain = this._uncertain.computed[keyPath]; if (uncertain && uncertain[ownerId]) return uncertain[ownerId]; ++this._runningOperations; return this._getRaw('computed', ensureString(keyPath), ensureOwnerId(ownerId)) .finally(this._onOperationEnd); }), getReduced: d(function (key) { var index, ownerId, path, uncertain; key = ensureString(key); index = key.indexOf('/'); ownerId = (index !== -1) ? key.slice(0, index) : key; path = (index !== -1) ? key.slice(index + 1) : null; this._ensureOpen(); uncertain = this._uncertain.reduced[ownerId]; if (uncertain && uncertain[path || '']) return uncertain[path || '']; ++this._runningOperations; return this._getRaw('reduced', ownerId, path).finally(this._onOperationEnd); }), getObject: d(function (objectId/*, options*/) { var keyPaths, options = arguments[1]; objectId = ensureString(objectId); this._ensureOpen(); ++this._runningOperations; if (options && (options.keyPaths != null)) { keyPaths = new Set(aFrom(ensureIterable(options.keyPaths), ensureString)); } return this._getObject(objectId, keyPaths).finally(this._onOperationEnd); }), deleteObject: d(function (objectId) { objectId = ensureString(objectId); this._ensureOpen(); ++this._runningOperations; return this._getObject(objectId)(function (data) { return this.storeMany(data.reverse().map(function (data) { return { id: data.id, data: { value: '' } }; })); }.bind(this)).finally(this._onOperationEnd); }), deleteManyObjects: d(function (objectIds) { objectIds = aFrom(ensureIterable(objectIds), ensureString); this._ensureOpen(); ++this._runningOperations; return deferred.map(objectIds, function (objectId) { return this._getObject(objectId); }, this)(function (data) { return this.storeMany(flatten.call(data).sort(dataByStampRev).map(function (data) { return { id: data.id, data: { value: '' } }; })); }.bind(this)).finally(this._onOperationEnd); }), getObjectKeyPath: d(function (id) { var index, ownerId, keyPath, uncertain; id = ensureString(id); this._ensureOpen(); index = id.indexOf('/'); if (index === -1) { uncertain = this._uncertain.direct[id]; if (uncertain && uncertain['']) return uncertain['']; return this._getRaw('direct', id)(function (data) { if (!data) return []; return [data]; }); } ownerId = id.slice(0, index); keyPath = id.slice(index + 1); ++this._runningOperations; return this._getObject(ownerId, new Set([keyPath])).finally(this._onOperationEnd); }), getAllObjectIds: d(function () { var transientData = create(null), uncertainData = create(null), uncertainPromise; this._ensureOpen(); ++this._runningOperations; forEach(this._transient.direct, function (ownerData, ownerId) { transientData[ownerId] = ownerData[''] || null; }); uncertainPromise = deferred.map(keys(this._uncertain.direct), function (ownerId) { if (this[ownerId]['']) { return this[ownerId][''](function (data) { uncertainData[ownerId] = data; }); } uncertainData[ownerId] = null; }, this._uncertain.direct); return this._safeGet(function () { return uncertainPromise(this.__getAllObjectIds())(function (data) { forEach(transientData, function (record, ownerId) { if (!record && data[ownerId]) delete transientData[ownerId]; }); forEach(uncertainData, function (record, ownerId) { if (!record && (data[ownerId] || transientData[ownerId])) delete uncertainData[ownerId]; }); return toArray(assign(data, transientData, uncertainData), function (el, id) { return id; }, null, byStamp); }); }).finally(this._onOperationEnd); }), getAll: d(function () { ++this._runningOperations; return this._getAll().finally(this._onOperationEnd); }), getReducedObject: d(function (ns/*, options*/) { var keyPaths, options = arguments[1]; ns = ensureOwnerId(ns); this._ensureOpen(); ++this._runningOperations; if (options && (options.keyPaths != null)) { keyPaths = new Set(aFrom(ensureIterable(options.keyPaths), ensureString)); } return this._getReducedObject(ns, keyPaths).finally(this._onOperationEnd); }), load: d(function (id) { if (!this.driver.database) throw new Error("No database registered to load data in"); return this.get(id)(function (data) { if (!data) return null; return this.driver._load(id, data.value, data.stamp); }.bind(this)); }), loadObject: d(function (ownerId) { if (!this.driver.database) throw new Error("No database registered to load data in"); return this.getObject(ownerId)(function (data) { return compact.call(data.map(function (data) { return this.driver._load(data.id, data.data.value, data.data.stamp); }, this)); }.bind(this)); }), loadAll: d(function () { var promise, progress = 0; if (!this.driver.database) throw new Error("No database registered to load data in"); this._ensureOpen(); ++this._runningOperations; promise = this._getAll()(function (data) { return compact.call(data.map(function (data) { if (!(++progress % 1000)) promise.emit('progress'); return this.driver._load(data.id, data.data.value, data.data.stamp); }, this)); }.bind(this)).finally(this._onOperationEnd); return promise; }), storeEvent: d(function (event) { event = ensureObject(event); this._ensureOpen(); ++this._runningOperations; return this._storeEvent(event).finally(this._onOperationEnd); }), storeEvents: d(function (events) { events = ensureArray(events); this._ensureOpen(); ++this._runningOperations; return deferred.map(events, this._storeEvent, this).finally(this._onOperationEnd); }), store: d(function (id, value, stamp) { var index, ownerId, path; id = ensureString(id); value = ensureString(value); stamp = (stamp != null) ? ensureNaturalNumber(stamp) : genStamp(); this._ensureOpen(); index = id.indexOf('/'); ownerId = (index !== -1) ? id.slice(0, index) : id; path = (index !== -1) ? id.slice(index + 1) : null; ++this._runningOperations; return this._handleStoreDirect(ownerId, path, value, stamp).finally(this._onOperationEnd); }), storeMany: d(function (data) { return this._storeMany(data, this._handleStoreDirect); }), storeReduced: d(function (id, value, stamp, directEvent) { var index, ownerId, path; id = ensureString(id); value = ensureString(value); stamp = (stamp != null) ? ensureNaturalNumber(stamp) : genStamp(); this._ensureOpen(); index = id.indexOf('/'); ownerId = (index !== -1) ? id.slice(0, index) : id; path = (index !== -1) ? id.slice(index + 1) : null; ++this._runningOperations; return this._handleStoreReduced(ownerId, path, value, stamp, directEvent) .finally(this._onOperationEnd); }), storeManyReduced: d(function (data) { return this._storeMany(data, this._handleStoreReduced); }), search: d(function (query, callback) { var keyPath, value; if (typeof query === 'function') { callback = query; } else { ensureObject(query); callback = ensureCallable(callback); if (query.keyPath !== undefined) { keyPath = (query.keyPath === null) ? null : ensureString(query.keyPath); } if (query.value != null) value = ensureString(query.value); } return this._search(keyPath, value, callback); }), searchOne: d(function (query, callback) { var keyPath, value; if (typeof query === 'function') { callback = query; } else { ensureObject(query); callback = ensureCallable(callback); if (query.keyPath !== undefined) { keyPath = (query.keyPath === null) ? null : ensureString(query.keyPath); } if (query.value != null) value = ensureString(query.value); } return this._search(keyPath, value, function (id, data, stream) { var result = callback.apply(this, arguments); if (result === undefined) return; stream.destroy(); return result; })(function (data) { return data[0]; }); }), searchComputed: d(function (query, callback) { var keyPath, value; if (typeof query === 'function') { callback = query; } else { ensureObject(query); callback = ensureCallable(callback); if (query.keyPath !== undefined) { keyPath = (query.keyPath === null) ? null : ensureString(query.keyPath); } if (query.value != null) value = ensureString(query.value); } return this._searchComputed(keyPath, value, ensureCallable(callback)); }), indexKeyPath: d(function (name, set/*, options*/) { var options = Object(arguments[2]), keyPath; if (options.keyPath != null) keyPath = ensureString(options.keyPath); else keyPath = name; return this._trackComputed(name, set, keyPath); }), indexCollection: d(function (name, set) { return this._trackComputed(name, set); }), trackSize: d(function (name, keyPath/*, searchValue*/) { var searchValue = arguments[2]; name = ensureString(name); if (keyPath != null) keyPath = ensureString(keyPath); return this._trackDirectSize(name, keyPath, searchValue); }), trackComputedSize: d(function (name, keyPath/*, searchValue*/) { var searchValue = arguments[2]; name = ensureString(name); keyPath = ensureString(keyPath); return this._trackComputedSize(name, keyPath, searchValue); }), trackCollectionSize: d(function (name, set) { return this._trackCollectionSize(ensureString(name), set); }), trackMultipleSize: d(function (name, sizeIndexes) { name = ensureString(name); sizeIndexes = aFrom(ensureIterable(sizeIndexes)); if (sizeIndexes.length < 2) throw new Error("At least two size indexes should be provided"); return this._trackMultipleSize(name, sizeIndexes); }), recalculateSize: d(function (name/*, getUpdate*/) { var meta = this._indexes[ensureString(name)], getUpdate = arguments[1], promise; if (!meta) throw new Error("There's no index registered for " + stringify(name)); if (meta.type !== 'size') { throw new Error("Registered " + stringify(name) + " index is not size index"); } if (getUpdate != null) ensureCallable(getUpdate); ++this._runningOperations; if (meta.sizeType === 'direct') { promise = this._recalculateDirectSet(meta.keyPath, meta.searchValue); } else if (meta.sizeType === 'multiple') { promise = this._recalculateMultipleSet(meta.sizeIndexes); } else { promise = this._recalculateComputedSet(meta.keyPath, meta.searchValue); } return promise(function (result) { var index = name.indexOf('/') , ownerId = (index !== -1) ? name.slice(0, index) : name , path = (index !== -1) ? name.slice(index + 1) : null; return this._handleStoreReduced(ownerId, path, serializeValue(result.size + (getUpdate ? getUpdate() : 0))); }.bind(this)).finally(this._onOperationEnd); }), recalculateAllSizes: d(function () { return deferred.map(keys(this._indexes), function (name) { if (this._indexes[name].type !== 'size') return; return this.recalculateSize(name); }, this)(Function.prototype); }), export: d(function (externalStore) { ensureStorage(externalStore); this._ensureOpen(); ++this._runningOperations; return this._safeGet(function () { return this.__exportAll(externalStore); }).finally(this._onOperationEnd); }), clear: d(function () { var transient; this._ensureOpen(); ++this._runningOperations; transient = this._transient; keys(transient.direct).forEach(function (key) { delete transient.direct[key]; }); keys(transient.computed).forEach(function (key) { delete transient.computed[key]; }); keys(transient.reduced).forEach(function (key) { delete transient.reduced[key]; }); return this._safeGet(function () { ++this._runningWriteOperations; return this.__clear(); }).finally(function () { var def; if (--this._runningWriteOperations) return; if (this._onWriteDrain) { def = this._onWriteDrain; delete this._onWriteDrain; def.resolve(); } }.bind(this)).finally(this._onOperationEnd); }), drop: d(function () { var transient = this._transient; keys(transient.direct).forEach(function (key) { delete transient.direct[key]; }); keys(transient.computed).forEach(function (key) { delete transient.computed[key]; }); keys(transient.reduced).forEach(function (key) { delete transient.reduced[key]; }); if (this.isClosed) { return deferred(this._closeDeferred.promise)(function () { return this.__drop(); }.bind(this)); } return this.close()(function () { return this.__drop()(function () { delete this.driver._storages[this.name]; }.bind(this)); }.bind(this)); }), isClosed: d(false), close: d(function () { this._ensureOpen(); this.isClosed = true; if (this.hasOwnProperty('_cleanupCalls')) { this._cleanupCalls.forEach(function (cb) { cb(); }); } delete this._cleanupCalls; if (this._runningOperations) { this._closeDeferred = deferred(); return this._closeDeferred.promise; } return this.__close(); }), onDrain: d.gs(function () { if (!this._runningOperations) return deferred(undefined); if (!this._onDrain) this._onDrain = deferred(); return this._onDrain.promise; }), onWriteDrain: d.gs(function () { if (!this._runningWriteOperations) return deferred(undefined); if (!this._onWriteDrain) this._onWriteDrain = deferred(); return this._onWriteDrain.promise; }), onWriteLockDrain: d.gs(function () { if (!this._writeLockCounter) return this.onWriteDrain; if (!this._onWriteLockDrain) this._onWriteLockDrain = deferred(); return this._onWriteLockDrain.promise; }), toString: d(function () { return '[dbjs-storage ' + (this.driver.name ? (this.driver.name + ':') : '') + this.name + ']'; }), _getRaw: d(function (cat, ns, path) { if (this._transient[cat][ns] && this._transient[cat][ns][path || '']) { return deferred(this._transient[cat][ns][path || '']); } return this.__getRaw(cat, ns, path); }), _getObject: d(function (objectId, keyPaths) { var transientData = create(null), uncertainData = create(null) , ownerId = objectId.split('/', 1)[0], uncertainPromise, objectPath, tmpKeyPaths; if (objectId !== ownerId) { objectPath = objectId.slice(ownerId.length + 1); if (keyPaths) { tmpKeyPaths = new Set(); keyPaths.forEach(function (keyPath) { tmpKeyPaths.add(objectPath + '/' + keyPath); }); keyPaths = tmpKeyPaths; } } if (this._transient.direct[ownerId]) { forEach(this._transient.direct[ownerId], function (data, path) { if (!isObjectPart(objectPath, path)) return; if (keyPaths && path && !keyPaths.has(resolveKeyPath(ownerId + '/' + path))) return; transientData[ownerId + (path && ('/' + path))] = data; }); } if (this._uncertain.direct[ownerId]) { uncertainPromise = deferred.map(keys(this._uncertain.direct[ownerId]), function (path) { if (!isObjectPart(objectPath, path)) return; if (keyPaths && path && !keyPaths.has(resolveKeyPath(ownerId + '/' + path))) return; return this[path](function (data) { uncertainData[ownerId + (path && ('/' + path))] = data; }); }, this._uncertain.direct[ownerId]); } return this._safeGet(function () { var promise = (uncertainPromise || resolved)(this.__getObject(ownerId, objectPath, keyPaths)); return promise(function (data) { return toArray(assign(data, transientData, uncertainData), function (data, id) { return { id: id, data: data }; }, null, byStamp); }.bind(this)); }); }), _getAll: d(function () { var transientData = create(null), uncertainData = create(null), uncertainPromise; forEach(this._transient.direct, function (ownerData, ownerId) { forEach(ownerData, function (data, id) { transientData[ownerId + (id && ('/' + id))] = data; }); }); uncertainPromise = deferred.map(keys(this._uncertain.direct), function (ownerId) { return deferred.map(keys(this[ownerId]), function (path) { return this[path](function (data) { uncertainData[ownerId + (path && ('/' + path))] = data; }); }, this[ownerId]); }, this._uncertain.direct); return this._safeGet(function () { return uncertainPromise(this.__getAll())(function (data) { return toArray(assign(data, transientData, uncertainData), function (data, id) { return { id: id, data: data }; }, null, byStamp); }.bind(this)); }); }), _getReducedObject: d(function (ns, keyPaths) { var transientData = create(null), uncertainData = create(null), uncertainPromise; if (this._transient.reduced[ns]) { forEach(this._transient.reduced[ns], function (data, path) { if (keyPaths && path && !keyPaths.has(path)) return; transientData[ns + (path && ('/' + path))] = data; }); } if (this._uncertain.reduced[ns]) { uncertainPromise = deferred.map(keys(this._uncertain.reduced[ns]), function (path) { if (keyPaths && path && !keyPaths.has(path)) return; return this[path](function (data) { uncertainData[ns + (path && ('/' + path))] = data; }); }, this._uncertain.reduced[ns]); } return this._safeGet(function () { return (uncertainPromise || resolved)(this.__getReducedObject(ns, keyPaths))(function (data) { return toArray(assign(data, transientData, uncertainData), function (data, id) { return { id: id, data: data }; }, null, byStamp); }.bind(this)); }); }), _registerDatabase: d(function (autoSaveFilter) { var listener, database = this.driver.database; database.objects.on('update', listener = function (event, previous) { if (event.sourceId === 'persistentLayer') return; if (!autoSaveFilter(event, previous)) return; this.driver._loadedEventsMap[event.object.__valueId__ + '.' + event.stamp] = true; ++this._runningOperations; this._storeEvent(event).finally(this._onOperationEnd).done(); }.bind(this)); this._cleanupCalls.push(database.objects.off.bind(database.objects, 'update', listener)); }), _storeMany: d(function (data, method) { var isStampGenerated, records = []; iterableForEach(data, function (data) { var record = {}; ensureObject(data); record.id = ensureString(data.id); ensureObject(data.data); record.data = {}; record.data.value = ensureString(data.data.value); if (data.data.stamp == null) { if (!isStampGenerated) { record.data.stamp = genStamp(); isStampGenerated = true; } else { record.data.stamp = incrementStamp(); } } else { record.data.stamp = ensureNaturalNumber(data.data.stamp); } records.push(record); }); ++this._runningOperations; return deferred.map(records, function (record) { var index = record.id.indexOf('/'); var ownerId = (index !== -1) ? record.id.slice(0, index) : record.id; var path = (index !== -1) ? record.id.slice(index + 1) : null; return method.call(this, ownerId, path, record.data.value, record.data.stamp); }, this).finally(this._onOperationEnd); }), _storeRaw: d(function (cat, ns, path, data) { var transient = this._transient[cat]; if (!transient[ns]) transient[ns] = create(null); transient = transient[ns]; transient[path || ''] = data; if (this._writeLockCounter) { if (!this._writeLockCache) this._writeLockCache = []; this._writeLockCache.push(arguments); return this.onWriteLockDrain; } ++this._runningWriteOperations; return this._handleStoreRaw(cat, ns, path, data).finally(function () { var def; if (transient[path || ''] === data) delete transient[path || '']; if (--this._runningWriteOperations) return; if (this._onWriteDrain) { def = this._onWriteDrain; delete this._onWriteDrain; def.resolve(); } }.bind(this)); }), _handleStoreRaw: d(function (cat, ns, path, data) { var id = cat + ':' + ns + (path ? ('/' + path) : ''), def, promise; if (this._storeInProgress[id]) { def = deferred(); this._storeInProgress[id].finally(function () { def.resolve(this.__storeRaw(cat, ns, path, data)); }.bind(this)); this._storeInProgress[id] = promise = def.promise; } else { this._storeInProgress[id] = promise = this.__storeRaw(cat, ns, path, data); } return promise.finally(function () { if (this._storeInProgress[id] === promise) delete this._storeInProgress[id]; }.bind(this)); }), _storeEvent: d(function (event) { var ownerId, id; id = event.object.__valueId__; ownerId = event.object.master.__id__; return this._handleStoreDirect(ownerId, id.slice(ownerId.length + 1) || null, serializeValue(event.value), event.stamp); }), _handleStoreDirect: d(function (ns, path, value, stamp) { return this._handleStore('direct', ns, path, value, stamp); }), _handleStoreComputed: d(function (ns, path, value, stamp, isOwnEvent) { return this._handleStore('computed', ns, path, value, stamp, isOwnEvent); }), _handleStoreReduced: d(function (ns, path, value, stamp, directEvent) { return this._handleStore('reduced', ns, path, value, stamp, directEvent); }), _handleStore: d(function (cat, ns, path, value, stamp, extraParam) { var uncertain = this._uncertain[cat], result, uncertainPromise , batchPromise = this._eventsBatchPromise , methodName = '_store' + capitalize.call(cat); if (!uncertain[ns]) uncertain[ns] = create(null); uncertain = uncertain[ns]; if (uncertain[path || '']) { result = uncertain[path || ''].direct(function (result) { return this[methodName](result.data, ns, path, value, stamp, extraParam); }.bind(this)); } else { result = this._getRaw(cat, ns, path)(function (data) { return this[methodName](data, ns, path, value, stamp, extraParam); }.bind(this)); } this._eventsBatchPool.push(result.catch(Function.prototype)); uncertainPromise = uncertain[path || ''] = result(function (result) { return batchPromise(result.data); }); uncertainPromise.direct = result; uncertainPromise.finally(function () { if (uncertain[path || ''] === uncertainPromise) delete uncertain[path || '']; }); return result(function (result) { return batchPromise(function (storeMap) { return storeMap[cat + ':' + result.id] || result.data; }); }); }), _storeDirect: d(function (old, ownerId, path, value, stamp) { var id = ownerId + (path ? ('/' + path) : '') , nu = { value: value, stamp: stamp } , keyPath = path ? resolveKeyPath(id) : null; if (old && (old.stamp >= nu.stamp)) return { data: old, id: id }; return { data: nu, id: id, event: { storage: this, type: 'direct', id: id, ownerId: ownerId, keyPath: keyPath, path: path, data: nu, old: old } }; }), _storeComputed: d(function (old, ns, path, value, stamp, isOwnEvent) { var id = path + '/' + ns; var nu, hasChanged = true; if (old) { if (isArray(value)) { if (isArray(old.value) && isCopy.call(resolveEventKeys(old.value), value)) { hasChanged = false; } } else { if (old.value === value) hasChanged = false; } if (!hasChanged) { // Value didn't change if (isOwnEvent) { // Direct update to observed property if (old.stamp === stamp) { // Only if stamp hasn't change we do not proceed with update // Otherwise we want the stamp to be on pair with direct record return { data: old, id: id }; } } else { // Computed update if ((old.stamp > 100000) || (typeof stamp === 'function')) { // No model stamp, or expensive stamp calculation, therefore abort update return { data: old, id: id }; } } } } return deferred((typeof stamp === 'function') ? stamp() : stamp)(function (stamp) { if (!hasChanged && stamp) { if ((old.stamp === stamp) || (stamp < 100000)) { // Value and stamp are same, or stamp resolved from model, take no action return { data: old, id: id }; } } if (!stamp) stamp = genStamp(); if (old && (old.stamp >= stamp)) { stamp = old.stamp + 1; // most likely model update } nu = { value: isArray(value) ? resolveMultipleEvents(stamp, value, old && old.value) : value, stamp: stamp }; return { data: nu, id: id, event: { storage: this, type: 'computed', id: id, ownerId: path, keyPath: ns, path: ns, data: nu, old: old } }; }.bind(this)); }), _storeReduced: d(function (old, ownerId, keyPath, value, stamp, directEvent) { var id = ownerId + (keyPath ? ('/' + keyPath) : ''); var nu; if (old) { if (old.value === value) return { data: old, id: id }; if (!stamp || (stamp <= old.stamp)) stamp = old.stamp + 1; } else if (!stamp) { stamp = genStamp(); } nu = { value: value, stamp: stamp }; return { data: nu, id: id, event: { storage: this, type: 'reduced', id: id, ownerId: ownerId, keyPath: keyPath, path: keyPath, data: nu, old: old, directEvent: directEvent } }; }), _search: d(function (keyPath, value, callback, certainOnly) { var done = create(null), def = deferred(), transientData = [], uncertainPromise , stream = def.promise, extPromises = []; stream.destroy = function () { defineProperty(stream, '_isDestroyed', d('', true)); }; forEach(this._transient.direct, function (ownerData, ownerId) { forEach(ownerData, function (data, path) { var id, recordValue; if (keyPath !== undefined) { if (!keyPath) { if (path) return; } else { if (!path) return; if (keyPath !== path) { if (!startsWith.call(path, keyPath + '*')) return; } } } if (value != null) { recordValue = resolveValue(ownerId, path, data.value); if (recordValue !== value) return; } id = ownerId + (path ? '/' + path : ''); transientData.push({ id: id, data: data }); }); }); if (!certainOnly) { uncertainPromise = deferred.map(keys(this._uncertain.direct), function (ownerId) { return deferred.map(keys(this[ownerId]), function (path) { var id; if (stream._isDestroyed) return; if (keyPath !== undefined) { if (!keyPath) { if (path) return; } else { if (!path) return; if (keyPath !== path) { if (!startsWith.call(path, keyPath + '*')) return; } } } id = ownerId + (path ? '/' + path : ''); done[id] = true; return this[path](function (data) { var recordValue, result; if (stream._isDestroyed) return; if (value != null) { recordValue = resolveValue(ownerId, path, data.value); if (recordValue !== value) return; } result = callback(id, data, stream); if (result !== undefined) extPromises.push(result); }); }, this[ownerId]); }, this._uncertain.direct); } def.resolve(this._safeGet(function () { return (uncertainPromise || resolved)(function () { if (stream._isDestroyed) return; transientData.some(function (data) { var result; if (done[data.id]) return; done[data.id] = true; result = callback(data.id, data.data, stream); if (result !== undefined) extPromises.push(result); return stream._isDestroyed; }); if (stream._isDestroyed) return; return this.__search(keyPath, value, function (id, data) { var result; if (done[id]) return; result = callback(id, data, stream); if (result !== undefined) extPromises.push(result); return stream._isDestroyed; }); }.bind(this)); }.bind(this))(function () { return deferred.map(extPromises); })); return stream; }), _searchComputed: d(function (keyPath, value, callback, certainOnly) { var done = create(null), def = deferred(), uncertain = this._uncertain.computed , transient = this._transient.computed, transientData = [], uncertainPromise , stream = def.promise, extPromises = []; stream.destroy = function () { defineProperty(stream, '_isDestroyed', d('', true)); }; if (keyPath) { transient = transient[keyPath]; if (transient) { forEach(transient, function (data, ownerId) { if ((value != null) && !filterComputedValue(value, data.value)) return; transientData.push({ id: ownerId + '/' + keyPath, data: data }); }); } } else { forEach(transient, function (data, keyPath) { forEach(data, function (data, ownerId) { if ((value != null) && !filterComputedValue(value, data.value)) return; transientData.push({ id: ownerId + '/' + keyPath, data: data }); }); }); } if (!certainOnly) { if (keyPath) { uncertain = uncertain[keyPath]; if (uncertain) { uncertainPromise = deferred.map(keys(uncertain), function (ownerId) { var id = ownerId + '/' + keyPath; done[id] = true; return this[ownerId](function (data) { var result; if (stream._isDestroyed) return; if ((value != null) && !filterComputedValue(value, data.value)) return; result = callback(id, data, stream); if (result !== undefined) extPromises.push(result); }); }, uncertain); } } else { uncertainPromise = deferred.map(keys(uncertain), function (keyPath) { return deferred.map(keys(this[keyPath]), function (ownerId) { var id = ownerId + '/' + keyPath; done[id] = true; return this[ownerId](function (data) { var result; if (stream._isDestroyed) return; if ((value != null) && !filterComputedValue(value, data.value)) return; result = callback(id, data, stream); if (result !== undefined) extPromises.push(result); }); }, this[keyPath]); }, this); } } def.resolve(this._safeGet(function () { return (uncertainPromise || resolved)(function () { if (stream._isDestroyed) return; transientData.some(function (data) { var result; if (done[data.id]) return; done[data.id] = true; result = callback(data.id, data.data, stream); if (result !== undefined) extPromises.push(result); return stream._isDestroyed; }); if (stream._isDestroyed) return; return this.__searchComputed(keyPath, value, function (id, data) { var result; if (done[id]) return; result = callback(id, data, stream); if (result !== undefined) extPromises.push(result); return stream._isDestroyed; }); }.bind(this)); }.bind(this))(function () { return deferred.map(extPromises); })); return stream; }), _trackComputed: d(function (name, set, keyPath) { var names, key, onAdd, onDelete, listener, setListener; name = ensureString(name); if (this._indexes[name]) { throw customError("Index of " + stringify(name) + " was already registered", 'DUPLICATE_INDEX'); } set = ensureObservableSet(set); if (keyPath != null) { keyPath = ensureString(keyPath); names = tokenize(ensureString(keyPath)); key = names[names.length - 1]; } this._ensureOpen(); this._indexes[name] = { type: 'computed', name: name, keyPath: keyPath }; listener = function (event) { var sValue, stamp, owner = event.target.object.master, ownerId = owner.__id__ , dbjsEvent = event.dbjs , isOwnEvent, dbjsId; if (!set.has(owner)) { // Can happen if deletion from set was invoked by event from observable we were attached to // (even though we've unbound listener, listener will still be called, as unbinding // was done after event was emitted, but before all listeners have propagated) return; } stamp = dbjsEvent ? dbjsEvent.stamp : genStamp(); if (dbjsEvent) { dbjsId = (dbjsEvent.object._kind_ === 'item') ? dbjsEvent.object.master.__id__ + '/' + dbjsEvent.object._pSKey_ : dbjsEvent.object.__valueId__; isOwnEvent = (dbjsId === (ownerId + '/' + keyPath)); } if (isSet(event.target)) { sValue = []; event.target.forEach(function (value) { sValue.push(serializeKey(value)); }); } else { sValue = serializeValue(event.newValue); } ++this._runningOperations; this._handleStoreComputed(name, ownerId, sValue, stamp, isOwnEvent) .finally(this._onOperationEnd).done(); }.bind(this); onAdd = function (owner, event) { var ownerId = owner.__id__, obj = owner, observable, value, stamp = 0, sValue, desc , isOwnEvent, observableListener; if (event) stamp = event.stamp; if (keyPath) { obj = resolveObject(owner, names); if (!obj) throw new Error("Cannot resolve object for " + name + " at " + ownerId); if (obj.isKeyStatic(key)) { value = obj[key]; } else { value = obj._get_(key); observable = obj._getObservable_(key); desc = obj._getDescriptor_(key); if ((desc.__valueId__ === (ownerId + '/' + keyPath)) && desc.hasOwnProperty('_value') && !isGetter(desc._value_)) { if (desc._lastOwnEvent_) { stamp = desc._lastOwnEvent_.stamp; isOwnEvent = true; } } if (!stamp) { stamp = function () { return observable.lastModified; }; } if (isSet(value)) { value.on('change', listener); observable.on('change', observableListener = function (event) { if (value) value.off('change', listener); if (isSet(event.newValue)) { value = event.newValue; value.on('change', listener); listener({ target: value, dbjs: event.dbjs }); } else { value = null; listener(event); } }); this._cleanupCalls.push( observable.off.bind(observable, 'change', observableListener), function () { if (value) value.off('change', listener); } ); } else { observable.on('change', listener); this._cleanupCalls.push(observable.off.bind(observable, 'change', listener)); } owner.on('update', function (dbjsEvent) { var dbjsId = (dbjsEvent.object._kind_ === 'item') ? ownerId + '/' + dbjsEvent.object._pSKey_ : dbjsEvent.object.__valueId__; if (dbjsId !== (ownerId + '/' + keyPath)) return; var value = obj._get_(key), sValue; if (isSet(value)) { sValue = []; value.forEach(function (value) { sValue.push(serializeKey(value)); }); } else { sValue = serializeValue(value); } ++this._runningOperations; this._handleStoreComputed(name, ownerId, sValue, dbjsEvent.stamp, true) .finally(this._onOperationEnd).done(); }.bind(this)); } if (isSet(value)) { sValue = []; value.forEach(function (value) { sValue.push(serializeKey(value)); }); } else { sValue = serializeValue(value); } } else { sValue = '11'; } return this._handleStoreComputed(name, ownerId, sValue, stamp, isOwnEvent); }.bind(this); onDelete = function (owner, event) { var obj, stamp = 0; if (event) stamp = event.stamp; if (keyPath) { obj = resolveObject(owner, names); if (obj && !obj.isKeyStatic(key)) obj._getObservable_(key).off('change', listener); } return this._handleStoreComputed(name, owner.__id__, '', stamp); }.bind(this); set.on('change', setListener = function (event) { if (event.type === 'add') { ++this._runningOperations; onAdd(event.value, event.dbjs).finally(this._onOperationEnd).done(); return; } if (event.type === 'delete') { ++this._runningOperations; onDelete(event.value, event.dbjs).finally(this._onOperationEnd).done(); return; } if (event.type === 'batch') { if (event.added) { ++this._runningOperations; deferred.map(aFrom(event.added), function (value) { return onAdd(value, event.dbjs); }) .finally(this._onOperationEnd).done(); } if (event.deleted) { ++this._runningOperations; deferred.map(aFrom(event.deleted), function (value) { return onDelete(value, event.dbjs); }).finally(this._onOperationEnd).done(); } } }.bind(this)); this._cleanupCalls.push(set.off.bind(set, 'change', setListener)); ++this._runningOperations; return deferred.map(aFrom(set), function (value) { return onAdd(value); }) .finally(this._onOperationEnd); }), _trackDirectSize: d(function (name, keyPath, searchValue) { return this._trackSize(name, { eventName: 'key:' + (keyPath || '&'), meta: { type: 'size', sizeType: 'direct', name: name, keyPath: keyPath, searchValue: searchValue }, resolveEvent: function (event) { return { nu: resolveDirectFilter(searchValue, event.data.value, event.id), old: Boolean(event.old && resolveDirectFilter(searchValue, event.old.value, event.id)) }; } }); }), _trackComputedSize: d(function (name, keyPath, searchValue) { return this._trackSize(name, { eventName: 'key:' + keyPath, meta: { type: 'size', sizeType: 'computed', name: name, keyPath: keyPath, searchValue: searchValue }, resolveEvent: function (event) { return { nu: resolveFilter(searchValue, event.data.value), old: Boolean(event.old && resolveFilter(searchValue, event.old.value)) }; } }); }), _trackCollectionSize: d(function (name, set) { var indexName = 'sizeIndex/' + name; return this.indexCollection(indexName, set)(this._trackComputedSize(name, indexName, '11')); }), _trackMultipleSize: d(function (name, sizeIndexes) { var dependencyPromises = [], metas = create(null); sizeIndexes.forEach(function self(name) { var meta = this._indexes[ensureString(name)], keyPath; if (!meta) { throw customError("No index for " + stringify(name) + " was setup", 'DUPLICATE_INDEX'); } if (meta.type !== 'size') { throw customError("Index " + stringify(name) + " is not of \"size\" type as expected", 'NOT_SUPPORTED_INDEX'); } if (meta.sizeType === 'multiple') { meta.sizeIndexes.forEach(self, this); return; } keyPath = meta.keyPath || '&'; if (metas[keyPath]) { if (!isArray(metas[keyPath])) metas[keyPath] = [metas[keyPath]]; metas[keyPath].push(meta); } else { metas[keyPath] = meta; } dependencyPromises.push(meta.promise); }, this); return this._trackSize(name, { initPromise: deferred.map(dependencyPromises), eventNames: uniq.call(flatten.call(sizeIndexes.map(function self(name) { var meta = this._indexes[name]; if (meta.sizeType === 'multiple') return meta.sizeIndexes.map(self, this); return { name: 'key:' + (meta.keyPath || '&'), type: meta.sizeType }; }, this))), meta: { type: 'size', sizeType: 'multiple', name: name, sizeIndexes: sizeIndexes }, resolveEvent: function (event) { var ownerId = event.ownerId, nu, old, meta = metas[event.keyPath || '&'], diff; var checkMeta = function (meta) { if (event.type === 'direct') { nu = resolveDirectFilter(meta.searchValue, event.data.value, event.id); old = Boolean(event.old && resolveDirectFilter(meta.searchValue, event.old.value, event.id)); } else { old = resolveFilter(meta.searchValue, event.old ? event.old.value : ''); nu = resolveFilter(meta.searchValue, event.data.value); } return nu - old; }; if (isArray(meta)) { diff = meta.map(checkMeta).filter(Boolean).reduce(function (a, b) { if (a == null) return a; if (b && a && (b !== a)) return null; return b; }, 0); } else { diff = checkMeta(meta); } if (!diff) return; return deferred.every(sizeIndexes, function self(name) { var meta = this._indexes[name], keyPath; if (event.keyPath === meta.keyPath) return true; if (meta.sizeType === 'multiple') return deferred.every(meta.sizeIndexes, self, this); if (meta.sizeType === 'direct') { keyPath = meta.keyPath; return this._getRaw('direct', ownerId, keyPath)(function (data) { var searchValue; if (data) { return resolveDirectFilter(meta.searchValue, data.value, ownerId + (keyPath ? ('/' + keyPath) : '')); } if (!keyPath) return false; if (meta.searchValue == null) return false; if (typeof meta.searchValue === 'function') return false; searchValue = meta.searchValue; if (searchValue[0] === '3') searchValue = serializeKey(unserializeValue(searchValue)); return this._getRaw('direct', ownerId, keyPath + '*' + searchValue)(function (data) { if (!data) return false; return data.value === '11'; }); }.bind(this)); } return this._getRaw('computed', meta.keyPath, ownerId)(function (data) { return resolveFilter(meta.searchValue, data ? data.value : ''); }); }, this)(function (isEffective) { if (!isEffective) return; return { old: (diff < 0), nu: (diff > 0) }; }); }.bind(this) }); }), _trackSize: d(function (name, conf) { var index, ownerId, path, listener, size = 0, isInitialised = false, current, stamp; if (this._indexes[name]) { throw customError("Index of " + stringify(name) + " was already registered", 'DUPLICATE_INDEX'); } index = name.indexOf('/'); ownerId = (index !== -1) ? name.slice(0, index) : name; path = (index !== -1) ? name.slice(index + 1) : null; listener = function (type, event) { if (type === 'computed') { if (event.type !== 'computed') return; } else if (type === 'direct') { if (event.type !== 'direct') return; } ++this._runningOperations; deferred(conf.resolveEvent(event))(function (result) { var nu, old, oldData, nuData; if (!result) return; nu = result.nu; old = result.old; if (nu === old) return; if (nu) ++size; else --size; stamp = event.data.stamp; if (!isInitialised) return; oldData = current; if (stamp <= oldData.stamp) stamp = oldData.stamp + 1; nuData = current = { value: serializeValue(size), stamp: stamp }; return this._handleStoreReduced(ownerId, path, nuData.value, nuData.stamp, event); }.bind(this)).finally(this._onOperationEnd).done(); }; var initialize = function (data) { size = unserializeValue(data.value); current = data; isInitialised = true; return size; }; var getSize = function () { return size; }; this._indexes[name] = conf.meta; ++this._runningOperations; return (conf.meta.promise = deferred(conf.initPromise)(function () { if (conf.eventNames) { conf.eventNames.forEach(function (data) { this.on(data.name, listener.bind(this, data.type)); }, this); } else { this.on(conf.eventName, listener.bind(this, conf.meta.sizeType)); } return this._getRaw('reduced', ownerId, path)(function (data) { if (data) return data; size = 0; return this.recalculateSize(name); }.bind(this))(function (data) { if (!size) return initialize(data); data = { value: serializeValue(unserializeValue(data.value) + size), stamp: (stamp < data.stamp) ? (data.stamp + 1) : stamp }; initialize(data); return this._handleStoreReduced(ownerId, path, data.value, data.stamp)(getSize); }.bind(this)); }.bind(this)).finally(this._onOperationEnd)); }), _recalculateDirectSet: d(function (keyPath, searchValue) { var filter = getSearchValueFilter(searchValue), result = new Set(); return this._search(keyPath, null, function (id, data) { var index = id.indexOf('/'), path, sValue, ownerId; if (!keyPath) { sValue = data.value; ownerId = id; } else { path = id.slice(id.indexOf('/') + 1); if (path !== keyPath) { // Multiple if (searchValue == null) return; // No support for multiple size check if (typeof searchValue === 'function') return; // No support for function filter if (data.value !== '11') return; sValue = path.slice(keyPath.length + 1); if (!isDigit(sValue[0])) sValue = '3' + sValue; } else { // Singular sValue = data.value; } ownerId = id.slice(0, index); } if (filter(sValue)) result.add(ownerId); }, true)(result); }), _recalculateComputedSet: d(function (keyPath, searchValue) { var result = new Set(); return this._searchComputed(keyPath, null, function (id, data) { if (resolveFilter(searchValue, data.value)) result.add(id.split('/', 1)[0]); }, true)(result); }), _recalculateMultipleSet: d(function (sizeIndexes) { return deferred.map(sizeIndexes, function self(name) { var meta = this._indexes[name]; if (meta.sizeType === 'multiple') return deferred.map(meta.sizeIndexes, self, this); if (meta.sizeType === 'direct') { return this._recalculateDirectSet(meta.keyPath, meta.searchValue); } return this._recalculateComputedSet(meta.keyPath, meta.searchValue); }, this).invoke(flatte