src/main/generic/InMemoryBackend.js
/**
* Transactions are created by calling the transaction method on an ObjectStore object.
* Transactions ensure read-isolation.
* On a given state, only *one* transaction can be committed successfully.
* Other transactions based on the same state will end up in a conflicted state if committed.
* Transactions opened after the successful commit of another transaction will be based on the
* new state and hence can be committed again.
* @implements {IBackend}
* @implements {ISynchronousObjectStore}
*/
class InMemoryBackend {
constructor(tableName, codec=null) {
this._cache = new Map();
/** @type {Map.<string,InMemoryIndex>} */
this._indices = new Map();
this._primaryIndex = new InMemoryIndex(this, /*keyPath*/ undefined, /*multiEntry*/ false, /*unique*/ true);
this._tableName = tableName;
this._codec = codec;
}
/** @type {boolean} */
get connected() {
return true;
}
/**
* @type {Map.<string,IIndex>}
*/
get indices() {
return this._indices;
}
/**
* Returns the object stored under the given primary key.
* Resolves to undefined if the key is not present in the object store.
* @abstract
* @param {string} key The primary key to look for.
* @param {SyncRetrievalConfig} [options] Advanced retrieval options.
* @returns {*} The object stored under the given key, or undefined if not present.
*/
getSync(key, options = {}) {
// Ignore expectPresence here, since it is a non-cached synchronous backend!
const value = this._cache.get(key);
return (options && options.raw) ? value : this.decode(value, key);
}
/**
* @param {string} key
* @param {RetrievalConfig} [options] Advanced retrieval options.
* @returns {Promise.<*>}
*/
get(key, options = {}) {
try {
return Promise.resolve(this.getSync(key, options));
} catch(e) {
return Promise.reject(e);
}
}
/**
* @param {Query|KeyRange} [query]
* @param {number} [limit]
* @returns {Promise.<Array.<*>>}
*/
async values(query = null, limit = null) {
if (query !== null && query instanceof Query) {
return query.values(this, limit);
}
const values = [];
for (const key of await this.keys(query, limit)) {
values.push(await this.get(key));
}
return Promise.resolve(values);
}
/**
* @param {Query|KeyRange} [query]
* @param {number} [limit]
* @returns {Promise.<Set.<string>>}
*/
keys(query = null, limit = null) {
if (query !== null && query instanceof Query) {
return query.keys(this, limit);
}
return this._primaryIndex.keys(query, limit);
}
/**
* Iterates over the keys in a given range and direction.
* The callback is called for each primary key fulfilling the query
* until it returns false and stops the iteration.
* @param {function(key:string):boolean} callback A predicate called for each key until returning false.
* @param {boolean} ascending Determines the direction of traversal.
* @param {KeyRange} query An optional KeyRange to narrow down the iteration space.
* @returns {Promise} The promise resolves after all elements have been streamed.
*/
keyStream(callback, ascending=true, query=null) {
return this._primaryIndex.keyStream(callback, ascending, query);
}
/**
* Iterates over the keys and values in a given range and direction.
* The callback is called for each value and primary key fulfilling the query
* until it returns false and stops the iteration.
* @param {function(value:*, key:string):boolean} callback A predicate called for each value and key until returning false.
* @param {boolean} ascending Determines the direction of traversal.
* @param {KeyRange} query An optional KeyRange to narrow down the iteration space.
* @returns {Promise} The promise resolves after all elements have been streamed.
*/
valueStream(callback, ascending=true, query=null) {
return this._primaryIndex.valueStream(callback, ascending, query);
}
/**
* @param {KeyRange} [query]
* @returns {Promise.<*>}
*/
async maxValue(query=null) {
const maxKey = await this.maxKey(query);
return this.get(maxKey);
}
/**
* @param {KeyRange} [query]
* @returns {Promise.<string>}
*/
async maxKey(query=null) {
const keys = await this._primaryIndex.maxKeys(query);
return Set.sampleElement(keys);
}
/**
* @param {KeyRange} [query]
* @returns {Promise.<*>}
*/
async minValue(query=null) {
const minKey = await this.minKey(query);
return this.get(minKey);
}
/**
* @param {KeyRange} [query]
* @returns {Promise.<string>}
*/
async minKey(query=null) {
const keys = await this._primaryIndex.minKeys(query);
return Set.sampleElement(keys);
}
/**
* @param {KeyRange} [query]
* @returns {Promise.<number>}
*/
async count(query=null) {
return (await this.keys(query)).size;
}
/**
* @param {string} indexName
* @returns {IIndex}
*/
index(indexName) {
return this._indices.get(indexName);
}
/**
* @param {Transaction} tx
* @returns {Promise.<boolean>}
* @protected
*/
async _apply(tx) {
if (tx._truncated) {
this.truncateSync();
}
const originalValues = new Map();
for (const key of tx._removed) {
const oldValue = this.getSync(key);
if (oldValue) {
originalValues.set(key, oldValue);
}
this._cache.delete(key);
}
for (const [key, value] of tx._modified) {
const oldValue = this.getSync(key);
if (oldValue) {
originalValues.set(key, oldValue);
}
this._cache.set(key, this.encode(value));
}
// Update all indices.
InMemoryBackend._indexApply(this._primaryIndex, tx, originalValues);
for (const index of this._indices.values()) {
InMemoryBackend._indexApply(index, tx, originalValues);
}
}
/**
* @param {InMemoryIndex} index
* @param {Transaction} tx
* @param {Map} originalValues
* @private
*/
static _indexApply(index, tx, originalValues) {
if (tx._truncated) {
index.truncate();
}
for (const key of tx._removed) {
index.remove(key, originalValues.get(key));
}
for (const [key, value] of tx._modified) {
index.put(key, value, originalValues.get(key));
}
}
/**
* @returns {Promise}
*/
async truncate() {
this.truncateSync();
}
truncateSync() {
this._cache.clear();
// Truncate all indices.
this._primaryIndex.truncate();
for (const index of this._indices.values()) {
index.truncate();
}
}
/**
* @param {function(key:string, value:*)} func
* @returns {Promise}
*/
async map(func) {
for (const [key, value] of this._cache) {
func(key, value);
}
}
/**
* @param {string} indexName The name of the index.
* @param {string|Array.<string>} [keyPath] The path to the key within the object. May be an array for multiple levels.
* @param {IndexConfig} [options] An options object.
*/
createIndex(indexName, keyPath, options = {}) {
let { multiEntry = false, unique = false, upgradeCondition = null } = options || {};
keyPath = keyPath || indexName;
const index = new InMemoryIndex(this, keyPath, multiEntry, unique);
this._indices.set(indexName, index);
}
/**
* Deletes a secondary index from the object store.
* @param indexName
* @param {{upgradeCondition:?boolean|?function(oldVersion:number, newVersion:number):boolean}} [options]
*/
deleteIndex(indexName, options = {}) {
let { upgradeCondition = null } = options || {};
this._indices.delete(indexName);
}
/**
* Method called to decode a single value.
* @param {*} value Value to be decoded.
* @param {string} key Key corresponding to the value.
* @returns {*} The decoded value.
*/
decode(value, key) {
if (value === undefined) {
return undefined;
}
if (this._codec !== null && this._codec !== undefined) {
return this._codec.decode(value, key);
}
return value;
}
/**
* Method called to encode a single value.
* @param {*} value Value to be encoded.
* @returns {*} The encoded value.
*/
encode(value) {
if (value === undefined) {
return undefined;
}
if (this._codec !== null && this._codec !== undefined) {
return this._codec.encode(value);
}
return value;
}
/** @type {string} The own table name. */
get tableName() {
return this._tableName;
}
/**
* Returns the necessary information in order to flush a combined transaction.
* @param {Transaction} tx The transaction that should be applied to this backend.
* @returns {Promise.<*|function():Promise>} Either the tableName if this is a native, persistent backend
* or a function that effectively applies the transaction to non-persistent backends.
*/
async applyCombined(tx) {
return () => this._apply(tx);
}
/**
* Checks whether an object store implements the ISynchronousObjectStore interface.
* @returns {boolean} The transaction object.
*/
isSynchronous() {
return true;
}
/**
* A check whether a certain key is cached.
* @param {string} key The key to check.
* @return {boolean} A boolean indicating whether the key is already in the cache.
*/
isCached(key) {
return true;
}
}
Class.register(InMemoryBackend);