Home Reference Source Test

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);