text: // 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);
  	})
  }))));
  