UNPKG

dbjs-persistence

Version:
514 lines (491 loc) 18.1 kB
// Abstract Storage Persistence driver 'use strict'; var aFrom = require('es5-ext/array/from') , isCopy = require('es5-ext/array/#/is-copy') , customError = require('es5-ext/error/custom') , ensureIterable = require('es5-ext/iterable/validate-object') , 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') , ensureString = require('es5-ext/object/validate-stringifiable-value') , Map = require('es6-map') , ensureMap = require('es6-map/valid-map') , ensureSet = require('es6-set/valid-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') , ee = require('event-emitter') , genStamp = require('time-uuid/time') , unserializeValue = require('dbjs/_setup/unserialize/value') , serializeValue = require('dbjs/_setup/serialize/value') , ensureStorage = require('./ensure-storage') , Storage = require('./storage') , nextTick = process.nextTick, isArray = Array.isArray, stringify = JSON.stringify , resolved = deferred(undefined) , isObjectId = RegExp.prototype.test.bind(/^[0-9a-z][0-9a-zA-Z]*$/) , compareNames = function (a, b) { return a.name.localeCompare(b.name); } , storeMany = Storage.prototype._storeMany , create = Object.create, keys = Object.keys; 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 ReductionStorage = module.exports = function (driver) { if (!(this instanceof ReductionStorage)) return new ReductionStorage(driver); this.driver = driver; }; 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(ReductionStorage.prototype, assign({ 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[ownerId]; if (uncertain && uncertain[path || '']) return uncertain[path || '']; ++this._runningOperations; return this._get(ownerId, path).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 = ensureSet(options.keyPaths); return this._getReducedObject(ns, keyPaths).finally(this._onOperationEnd); }), 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._handleStore(ownerId, path, value, stamp, directEvent) .finally(this._onOperationEnd); }), storeManyReduced: d(function (data) { return storeMany.call(this, data, this._handleStore); }), trackSize: d(function (name, storages, keyPath/*, searchValue*/) { var searchValue = arguments[3]; name = ensureString(name); storages = aFrom(ensureIterable(storages), ensureStorage).sort(compareNames); if (keyPath != null) keyPath = ensureString(keyPath); return this._trackSize(name, new Map(storages.map(function (storage) { return [storage, storage._trackDirectSize('$' + name, keyPath, searchValue)]; })), { sizeType: 'direct', storages: storages, keyPath: keyPath, searchValue: searchValue }); }), trackComputedSize: d(function (name, storages, keyPath/*, searchValue*/) { var searchValue = arguments[3]; name = ensureString(name); storages = aFrom(ensureIterable(storages), ensureStorage).sort(compareNames); keyPath = ensureString(keyPath); return this._trackSize(name, new Map(storages.map(function (storage) { return [storage, storage._trackComputedSize('$' + name, keyPath, searchValue)]; })), { sizeType: 'computed', storages: storages, keyPath: keyPath, searchValue: searchValue }); }), trackCollectionSize: d(function (name, storageSetMap) { var storages = []; name = ensureString(name); storageSetMap = aFrom(ensureMap(storageSetMap)); storageSetMap.forEach(function (data) { storages.push(ensureStorage(data[0])); ensureSet(data[1]); }); storages.sort(compareNames); return this._trackSize(name, new Map(storageSetMap.map(function (data) { var storage = data[0], set = data[1]; return [storage, storage._trackCollectionSize('$' + name, set)]; })), { sizeType: 'computed', storages: storages, keyPath: 'sizeIndex/' + name, searchValue: '11' }); }), trackMultipleSize: d(function (name, sizeIndexes) { var storages; name = ensureString(name); sizeIndexes = aFrom(ensureIterable(sizeIndexes), function (name, index) { var meta; name = ensureString(name); meta = this._indexes[name]; if (!meta) throw new Error("There's no index registered for " + name); if (!index) { storages = meta.storages; } else if (!isCopy.call(storages, meta.storages)) { throw new Error("Storages for provided indexes do not match"); } return '$' + name; }, this); if (sizeIndexes.length < 2) throw new Error("At least 2 sizeIndexes should be provided"); return this._trackSize(name, new Map(storages.map(function (storage) { return [ storage, storage._trackMultipleSize('$' + name, sizeIndexes) ]; })), { sizeType: 'multiple', storages: storages, sizeIndexes: sizeIndexes }); }), recalculateSize: d(function (name) { var meta = this._indexes[ensureString(name)]; 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"); } ++this._runningOperations; return deferred.map(meta.storages, function (storage) { return storage.recalculateSize('$' + name); })(Function.prototype).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).forEach(function (key) { delete transient[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).forEach(function (key) { delete transient[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 + ':') : '') + '_reduced_]'; }), _get: d(function (ns, path) { if (this._transient[ns] && this._transient[ns][path || '']) { return deferred(this._transient[ns][path || '']); } return this.__get(ns, path); }), _getReducedObject: d(function (ns, keyPaths) { var transientData = create(null), uncertainData = create(null), uncertainPromise; if (this._transient[ns]) { forEach(this._transient[ns], function (data, path) { if (keyPaths && path && !keyPaths.has(path)) return; transientData[ns + (path && ('/' + path))] = data; }); } if (this._uncertain[ns]) { uncertainPromise = deferred.map(keys(this._uncertain[ns]), function (path) { if (keyPaths && path && !keyPaths.has(path)) return; return this[path](function (data) { uncertainData[ns + (path && ('/' + path))] = data; }); }, this._uncertain[ns]); } return this._safeGet(function () { return (uncertainPromise || resolved)(this.__getObject(ns, keyPaths))(function (data) { return toArray(assign(data, transientData, uncertainData), function (data, id) { return { id: id, data: data }; }, null, byStamp); }.bind(this)); }); }), _storeRaw: d(function (ns, path, data) { var transient = this._transient; 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(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 (ns, path, data) { var id = ns + (path ? ('/' + path) : ''), def, promise; if (this._storeInProgress[id]) { def = deferred(); this._storeInProgress[id].finally(function () { def.resolve(this.__store(ns, path, data)); }.bind(this)); this._storeInProgress[id] = promise = def.promise; } else { this._storeInProgress[id] = promise = this.__store(ns, path, data); } return promise.finally(function () { if (this._storeInProgress[id] === promise) delete this._storeInProgress[id]; }.bind(this)); }), _handleStore: d(function (ns, path, value, stamp, directEvent) { var uncertain = this._uncertain, result, uncertainPromise , batchPromise = this._eventsBatchPromise; if (!uncertain[ns]) uncertain[ns] = create(null); uncertain = uncertain[ns]; if (uncertain[path || '']) { result = uncertain[path || ''].direct(function (result) { return this._storeReduced(result.data, ns, path, value, stamp, directEvent); }.bind(this)); } else { result = this._get(ns, path)(function (data) { return this._storeReduced(data, ns, path, value, stamp, directEvent); }.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[result.id] || result.data; }); }); }), _storeReduced: d(function (old, ownerId, keyPath, value, stamp, directEvent) { var id = ownerId + (keyPath ? ('/' + keyPath) : ''), 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 } }; }), _trackSize: d(function (name, storagesMap, meta) { var index, ownerId, path, listener, size = 0, isInitialised = false; 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 (event) { var nu = unserializeValue(event.data.value), old = unserializeValue(event.old.value); if (nu === old) return; size += (nu - old); if (!isInitialised) return; ++this._runningOperations; return this._handleStore(ownerId, path, serializeValue(size), event.data.stamp, event) .finally(this._onOperationEnd).done(); }.bind(this); this._indexes[name] = meta; meta.type = 'size'; meta.name = name; ++this._runningOperations; return (meta.promise = deferred.map(aFrom(storagesMap), function (data) { var storage = data[0], promise = data[1]; return promise(function (result) { size += result; storage.on('keyid:$' + name, listener); }); })(function () { isInitialised = true; return this._handleStore(ownerId, path, serializeValue(size))(function () { return size; }); }.bind(this))).finally(this._onOperationEnd); }), _ensureOpen: d(function () { if (this.isClosed) throw customError("Database not accessible", 'DB_DISCONNECTED'); }), _safeGet: d(function (method) { ++this._writeLock; return this.onWriteDrain(method.bind(this)) .finally(function () { --this._writeLock; }.bind(this)); }), _runningOperations: d(0), _runningWriteOperations: d(0), _writeLockCounter: d(0), _writeLock: d.gs(function () { return this._writeLockCounter; }, function (value) { this._writeLockCounter = value; if (!value && this._writeLockCache) { this._writeLockCache.forEach(function (data) { this._storeRaw.apply(this, data); }, this); delete this._writeLockCache; if (this._onWriteLockDrain) this._onWriteLockDrain.resolve(this.onWriteDrain); } }), __get: d(notImplemented), __getObject: d(notImplemented), __store: d(notImplemented), __exportAll: d(notImplemented), __clear: d(notImplemented), __drop: d(notImplemented), __close: d(notImplemented) }, autoBind({ emitError: d(emitError), _onOperationEnd: d(function () { var def; if (--this._runningOperations) return; if (this._onDrain) { def = this._onDrain; delete this._onDrain; def.resolve(); } if (!this._closeDeferred) return; this._closeDeferred.resolve(this.__close()); }) }), lazy({ _cleanupCalls: d(function () { return []; }), _indexes: d(function () { return create(null); }), _transient: d(function () { return create(null); }), _uncertain: d(function () { return create(null); }), _storeInProgress: d(function () { return create(null); }), _eventsBatchPool: d(function () { return []; }), _eventsBatchPromise: d(function () { var def = deferred(); ++this._runningOperations; nextTick(function () { var promises = this._eventsBatchPool; delete this._eventsBatchPool; delete this._eventsBatchPromise; deferred.map(promises).done(function (results) { var toStore = {}, eventsMap = {}; results .map(function (result) { var event = result && result.event; if (!event) return; if (eventsMap[event.id]) event.old = eventsMap[event.id].old; eventsMap[event.id] = event; return event; }) .filter(function (event) { if (!event) return false; return (eventsMap[event.id] === event); }) .forEach(function (event) { var storePromise; if (!event) return; debug("reduced update %s %s %s", event.id, trimValue(event.data.value), event.data.stamp); this.emit('update:reduced', event); this.driver.emit('update:reduced', event); this.emit('key:' + (event.keyPath || '&'), event); this.emit('owner:' + event.ownerId, event); this.emit('keyid:' + event.ownerId + (event.keyPath ? ('/' + event.keyPath) : ''), event); storePromise = this._storeRaw(event.ownerId, event.path, event.data); storePromise = storePromise(event.data); toStore[event.id] = storePromise; }, this); def.resolve(toStore); }.bind(this)); }.bind(this)); return def.promise.finally(this._onOperationEnd); }) }))));