Home Reference Source Test

src/main/generic/InMemoryIndex.js

/**
 * This is a BTree based index, which is generally stored in memory.
 * It is used by transactions.
 * @implements {IIndex}
 */
class InMemoryIndex {
    /**
     * Creates a new InMemoryIndex for a given object store.
     * The key path describes the path of the secondary key within the stored objects.
     * Only objects for which the key path exists are part of the secondary index.
     *
     * A key path is defined by a key within the object or alternatively a path through the object to a specific subkey.
     * For example, ['a', 'b'] could be used to use 'key' as the key in the following object:
     * { 'a': { 'b': 'key' } }
     *
     * If a secondary index is a multi entry index, and the value at the key path is iterable,
     * every item of the iterable value will be associated with the object.
     * @param {IObjectStore} objectStore The underlying object store to use.
     * @param {string|Array.<string>} [keyPath] The key path of the indexed attribute.
     * If the keyPath is not given, this is a primary index.
     * @param {boolean} [multiEntry] Whether the indexed attribute is considered to be iterable or not.
     * @param {boolean} [unique] Whether there is a unique constraint on the attribute.
     */
    constructor(objectStore, keyPath, multiEntry=false, unique=false) {
        this._objectStore = objectStore;
        this._keyPath = keyPath;
        this._multiEntry = multiEntry;
        this._unique = unique;
        this._tree = new BTree();
    }

    /**
     * Reinitialises the index.
     */
    truncate() {
        this._tree = new BTree();
    }

    /**
     * Helper method to return the attribute associated with the key path if it exists.
     * @param {string} key The primary key of the key-value pair.
     * @param {*} obj The value of the key-value pair.
     * @returns {*} The attribute associated with the key path, if it exists, and undefined otherwise.
     * @private
     */
    _indexKey(key, obj) {
        if (obj === undefined) return undefined;
        if (this.keyPath) {
            return ObjectUtils.byKeyPath(obj, this.keyPath);
        }
        return key;
    }

    /**
     * The key path associated with this index.
     * A key path is defined by a key within the object or alternatively a path through the object to a specific subkey.
     * For example, ['a', 'b'] could be used to use 'key' as the key in the following object:
     * { 'a': { 'b': 'key' } }
     * If the keyPath is undefined, this index uses the primary key of the key-value store.
     * @type {string|Array.<string>}
     */
    get keyPath() {
        return this._keyPath;
    }

    /**
     * This value determines whether the index supports multiple secondary keys per entry.
     * If so, the value at the key path is considered to be an iterable.
     * @type {boolean}
     */
    get multiEntry() {
        return this._multiEntry;
    }

    /**
     * This value determines whether the index is a unique constraint.
     * @type {boolean}
     */
    get unique() {
        return this._unique;
    }

    /**
     * A helper method to insert a primary-secondary key pair into the tree.
     * @param {string} key The primary key.
     * @param {*} iKey The indexed key.
     * @throws if the uniqueness constraint is violated.
     */
    _insert(key, iKey) {
        const tree = this._tree;
        if (!this._multiEntry || !Array.isArray(iKey)) {
            iKey = [iKey];
        }
        // Add all keys.
        for (const component of iKey) {
            if (tree.seek(component)) {
                if (this._unique) {
                    throw new Error(`Uniqueness constraint violated for key ${key} on path ${this._keyPath}`);
                }
                (/** @type {SortedList} */ tree.currentRecord).add(key);
            } else {
                tree.insert(component, this._unique ? key : new SortedList([key], ComparisonUtils.compare));
            }
        }
    }

    /**
     * Inserts a new key-value pair into the index.
     * For replacing an existing pair, the old value has to be passed as well.
     * @param {string} key The primary key of the pair.
     * @param {*} value The value of the pair. The indexed key will be extracted from this.
     * @param {*} [oldValue] The old value associated with the primary key.
     */
    put(key, value, oldValue) {
        const oldIKey = this._indexKey(key, oldValue);
        const newIKey = this._indexKey(key, value);

        if (!ComparisonUtils.equals(oldIKey, newIKey)) {
            if (oldIKey !== undefined) {
                this._remove(key, oldIKey);
            }
            if (newIKey !== undefined) {
                this._insert(key, newIKey);
            }
        }
    }

    /**
     * Removes a key-value pair from the index.
     * @param {string} key The primary key of the pair.
     * @param {*} oldValue The old value of the pair. The indexed key will be extracted from this.
     */
    remove(key, oldValue) {
        const iKey = this._indexKey(key, oldValue);
        if (iKey !== undefined) {
            this._remove(key, iKey);
        }
    }

    /**
     * A helper method to remove a primary-secondary key pair from the tree.
     * @param {string} key The primary key.
     * @param {*} iKey The indexed key.
     */
    _remove(key, iKey) {
        const tree = this._tree;
        if (!this._multiEntry || !Array.isArray(iKey)) {
            iKey = [iKey];
        }
        // Remove all keys.
        for (const component of iKey) {
            if (tree.seek(component)) {
                if (!this._unique && (/** @type {SortedList} */ tree.currentRecord).length > 1) {
                    (/** @type {SortedList} */ tree.currentRecord).remove(key);
                } else {
                    tree.remove(component);
                }
            }
        }
    }

    /**
     * A helper method to retrieve the values corresponding to a set of keys.
     * @param {Set.<string>} keys The set of keys to get the corresponding values for.
     * @returns {Promise.<Array.<*>>} A promise of the array of values.
     * @protected
     */
    async _retrieveValues(keys) {
        const valuePromises = [];
        for (const key of keys) {
            valuePromises.push(this._objectStore.get(key));
        }
        return Promise.all(valuePromises);
    }

    /**
     * Returns a promise of an array of objects whose secondary keys fulfill the given query.
     * If the optional query is not given, it returns all objects in the index.
     * If the query is of type KeyRange, it returns all objects whose secondary keys are within this range.
     * @param {KeyRange} [query] Optional query to check secondary keys against.
     * @param {number} [limit] Limits the number of results if given.
     * @returns {Promise.<Array.<*>>} A promise of the array of objects relevant to the query.
     */
    async values(query = null, limit = null) {
        const keys = await this.keys(query, limit);
        return this._retrieveValues(keys);
    }

    /**
     * Returns a promise of a set of primary keys, whose associated objects' secondary keys are in the given range.
     * If the optional query is not given, it returns all primary keys in the index.
     * If the query is of type KeyRange, it returns all primary keys for which the secondary key is within this range.
     * @param {KeyRange} [query] Optional query to check the secondary keys against.
     * @param {number} [limit] Limits the number of results if given.
     * @returns {Promise.<Set.<string>>} A promise of the set of primary keys relevant to the query.
     */
    async keys(query = null, limit = null) {
        let resultSet = new Set();

        // Shortcut for exact match.
        if (query instanceof KeyRange && query.exactMatch) {
            if (this._tree.seek(query.lower)) {
                resultSet = Set.from(this._tree.currentRecord);
            }
            return resultSet.limit(limit);
        }

        // Find lower bound and start from there.
        if (!(query instanceof KeyRange)) {
            if (!this._tree.goTop()) {
                return resultSet; // Empty
            }
        } else {
            if (!this._tree.goToLowerBound(query.lower, query.lowerOpen)) {
                return resultSet; // empty
            }
        }

        while (!(query instanceof KeyRange) || query.includes(this._tree.currentKey)) {
            // Limit
            if (limit !== null && resultSet.size >= limit) {
                break;
            }

            resultSet = resultSet.union(Set.from(this._tree.currentRecord));
            if (!this._tree.skip()) {
                break;
            }
        }
        return resultSet.limit(limit);
    }

    /**
     * Iterates over the primary keys in a given range of secondary keys and direction.
     * The order is determined by the secondary keys first and by the primary keys second.
     * 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) {
        // Find lower bound and start from there.
        if (!(query instanceof KeyRange)) {
            if (ascending) {
                if (!this._tree.goTop()) {
                    return Promise.resolve();
                }
            } else {
                if (!this._tree.goBottom()) {
                    return Promise.resolve();
                }
            }
        } else {
            if (ascending) {
                if (!this._tree.goToLowerBound(query.lower, query.lowerOpen)) {
                    return Promise.resolve();
                }
            } else {
                if (!this._tree.goToUpperBound(query.upper, query.upperOpen)) {
                    return Promise.resolve();
                }
            }
        }

        outer:
        while (!(query instanceof KeyRange) || query.includes(this._tree.currentKey)) {
            if (this._unique) {
                // Check unique entry
                if (!callback(this._tree.currentRecord)) break;
            } else {
                // Check all entries
                const keys = this._tree.currentRecord.values();
                if (ascending) {
                    for (let i = 0; i < keys.length; i++) {
                        if (!callback(keys[i])) {
                            break outer;
                        }
                    }
                } else {
                    for (let i = keys.length - 1; i >= 0; i--) {
                        if (!callback(keys[i])) {
                            break outer;
                        }
                    }
                }
            }

            if (!this._tree.skip(ascending ? 1 : -1)) {
                break;
            }
        }
        return Promise.resolve();
    }

    /**
     * Iterates over the values of the store in a given range of secondary keys and direction.
     * The order is determined by the secondary keys first and by the primary keys second.
     * 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 resolved after all elements have been streamed.
     */
    async valueStream(callback, ascending=true, query=null) {
        // Find lower bound and start from there.
        if (!(query instanceof KeyRange)) {
            if (ascending) {
                if (!this._tree.goTop()) {
                    return;
                }
            } else {
                if (!this._tree.goBottom()) {
                    return;
                }
            }
        } else {
            if (ascending) {
                if (!this._tree.goToLowerBound(query.lower, query.lowerOpen)) {
                    return;
                }
            } else {
                if (!this._tree.goToUpperBound(query.upper, query.upperOpen)) {
                    return;
                }
            }
        }

        outer:
        while (!(query instanceof KeyRange) || query.includes(this._tree.currentKey)) {
            if (this._unique) {
                // Check unique entry
                if (!callback(await this._objectStore.get(this._tree.currentRecord), this._tree.currentRecord)) break; // eslint-disable-line no-await-in-loop
            } else {
                // Check all entries
                const keys = this._tree.currentRecord.values();
                if (ascending) {
                    for (let i = 0; i < keys.length; i++) {
                        if (!callback(await this._objectStore.get(keys[i]), keys[i])) { // eslint-disable-line no-await-in-loop
                            break outer;
                        }
                    }
                } else {
                    for (let i = keys.length - 1; i >= 0; i--) {
                        if (!callback(await this._objectStore.get(keys[i]), keys[i])) { // eslint-disable-line no-await-in-loop
                            break outer;
                        }
                    }
                }
            }

            if (!this._tree.skip(ascending ? 1 : -1)) {
                break;
            }
        }
    }

    /**
     * Returns a promise of an array of objects whose secondary key is maximal for the given range.
     * If the optional query is not given, it returns the objects whose secondary key is maximal within the index.
     * If the query is of type KeyRange, it returns the objects whose secondary key is maximal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<Array.<*>>} A promise of array of objects relevant to the query.
     */
    async maxValues(query=null) {
        const keys = await this.maxKeys(query);
        return this._retrieveValues(keys);
    }

    /**
     * Returns a promise of a set of primary keys, whose associated secondary keys are maximal for the given range.
     * If the optional query is not given, it returns the set of primary keys, whose associated secondary key is maximal within the index.
     * If the query is of type KeyRange, it returns the set of primary keys, whose associated secondary key is maximal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<Set.<*>>} A promise of the key relevant to the query.
     */
    async maxKeys(query=null) {
        const isRange = query instanceof KeyRange;
        if (!this._tree.goToUpperBound(isRange ? query.upper : undefined, isRange ? query.upperOpen : false)) {
            return new Set();
        }
        return Set.from(this._tree.currentRecord);
    }

    /**
     * Returns a promise of an array of objects whose secondary key is minimal for the given range.
     * If the optional query is not given, it returns the objects whose secondary key is minimal within the index.
     * If the query is of type KeyRange, it returns the objects whose secondary key is minimal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<Array.<*>>} A promise of array of objects relevant to the query.
     */
    async minValues(query=null) {
        const keys = await this.minKeys(query);
        return this._retrieveValues(keys);
    }

    /**
     * Returns a promise of a set of primary keys, whose associated secondary keys are minimal for the given range.
     * If the optional query is not given, it returns the set of primary keys, whose associated secondary key is minimal within the index.
     * If the query is of type KeyRange, it returns the set of primary keys, whose associated secondary key is minimal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<Set.<*>>} A promise of the key relevant to the query.
     */
    async minKeys(query=null) {
        const isRange = query instanceof KeyRange;
        if (!this._tree.goToLowerBound(isRange ? query.lower : undefined, isRange ? query.lowerOpen : false)) {
            return new Set();
        }
        return Set.from(this._tree.currentRecord);
    }

    /**
     * Returns the count of entries, whose secondary key is in the given range.
     * If the optional query is not given, it returns the count of entries in the index.
     * If the query is of type KeyRange, it returns the count of entries, whose secondary key is within the given range.
     * @param {KeyRange} [query]
     * @returns {Promise.<number>}
     */
    async count(query=null) {
        return (await this.keys(query)).size;
        // The code below does only work for unique indices.
        // if (!(query instanceof KeyRange)) {
        //     return this._tree.length;
        // }
        // if (!this._tree.goToLowerBound(query.lower, query.lowerOpen)) {
        //     return 0;
        // }
        // const start = this._tree.keynum();
        // if (!this._tree.goToUpperBound(query.upper, query.upperOpen)) {
        //     return 0;
        // }
        // const end = this._tree.keynum();
        // return end - start + 1;
    }
}
Class.register(InMemoryIndex);