Home Reference Source Test

src/main/generic/Transaction.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.
 * Transactions do *not* check unique constraints of secondary indices before commiting them.
 * @implements {ISynchronousWritableObjectStore}
 * @implements {ICommittable}
 */
class Transaction {
    /**
     * This constructor should only be called by an ObjectStore object.
     * Our transactions have a watchdog enabled by default,
     * logging a warning after a certain time specified by WATCHDOG_TIMER.
     * This helps to detect unclosed transactions preventing to store the state in
     * the persistent backend.
     * @param {ObjectStore} objectStore The object store this transaction belongs to.
     * @param {IObjectStore} parent The backend on which the transaction is based,
     * i.e., another transaction or the real database.
     * @param {ICommittable} [managingBackend] The object store managing the transactions,
     * i.e., the ObjectStore object.
     * @param {boolean} [enableWatchdog] If this is is set to true (default),
     * a warning will be logged if left open for longer than WATCHDOG_TIMER.
     * @protected
     */
    constructor(objectStore, parent, managingBackend, enableWatchdog=true) {
        this._id = Transaction._instanceCount++;
        this._objectStore = objectStore;
        this._parent = parent;
        /** @type {ICommittable} */
        this._managingBackend = managingBackend || parent;
        this._modified = new Map();
        this._removed = new Set();
        this._truncated = false;
        this._indices = TransactionIndex.derive(this, parent);

        this._state = Transaction.STATE.OPEN;

        // Keep track of nested transactions.
        /** @type {Set.<Transaction>} */
        this._nested = new Set();
        this._nestedCommitted = false;

        // Handle dependencies due to cross-objectstore transactions.
        /** @type {CombinedTransaction} */
        this._dependency = null;

        this._snapshotManager = new SnapshotManager();

        this._startTime = Date.now();
        this._enableWatchdog = enableWatchdog;
        if (this._enableWatchdog) {
            this._watchdog = setTimeout(() => {
                Log.w(Transaction, `Violation: tx id ${this._id} took longer than expected (still open after ${Transaction.WATCHDOG_TIMER/1000}s), ${this.toString()}.`);
            }, Transaction.WATCHDOG_TIMER);
        }
    }

    /** @type {ObjectStore} */
    get objectStore() {
        return this._objectStore;
    }

    /** @type {boolean} */
    get nested() {
        return this._managingBackend instanceof Transaction;
    }

    /**
     * @type {CombinedTransaction} If existent, a combined transaction encompassing this object.
     */
    get dependency() {
        return this._dependency;
    }

    /** @type {boolean} */
    get connected() {
        return this._managingBackend.connected;
    }

    /** @type {number} A unique transaction id. */
    get id() {
        return this._id;
    }

    /**
     * A map of index names to indices.
     * The index names can be used to access an index.
     * @type {Map.<string,IIndex>}
     */
    get indices() {
        return this._indices;
    }

    /**
     * The transaction's current state.
     * @returns {Transaction.STATE}
     */
    get state() {
        return this._state;
    }

    /**
     * Non-async version of _apply that does not update snapshots.
     * Internally applies a transaction to the transaction's state.
     * This needs to be done in batch (as a db level transaction), i.e., either the full state is updated
     * or no changes are applied.
     * @param {Transaction} tx The transaction to apply.
     * @protected
     */
    _applySync(tx) {
        if (tx._truncated) {
            this.truncateSync();
        }
        for (const [key, value] of tx._modified) {
            this._put(key, value);
        }
        for (const key of tx._removed) {
            this._remove(key);
        }
    }

    /**
     * Empties the object store.
     * @returns {Promise} The promise resolves after emptying the object store.
     */
    async truncate() {
        return this.truncateSync();
    }

    /**
     * Non-async variant to empty the object store.
     * @protected
     */
    truncateSync() {
        if (this._state !== Transaction.STATE.OPEN) {
            throw new Error('Transaction already closed');
        }

        this._truncated = true;
        this._modified.clear();
        this._removed.clear();

        // Update indices.
        for (const index of this._indices.values()) {
            index.truncate();
        }
    }

    /**
     * Returns a promise of the object stored under the given primary key.
     * Resolves to undefined if the key is not present in the object store.
     * @param {string} key The primary key to look for.
     * @param {RetrievalConfig} [options] Advanced retrieval options.
     * @returns {Promise.<*>} A promise of the object stored under the given key, or undefined if not present.
     */
    async get(key, options = {}) {
        // Order is as follows:
        // 1. check if removed,
        // 2. check if modified,
        // 3. check if truncated
        // 4. request from backend
        if (this._removed.has(key)) {
            return undefined;
        }
        if (this._modified.has(key)) {
            if (options && options.raw) {
                return this.encode(this._modified.get(key));
            }
            return this._modified.get(key);
        }
        if (this._truncated) {
            return undefined;
        }
        return this._parent.get(key, options);
    }

    /**
     * Inserts or replaces a key-value pair.
     * @param {string} key The primary key to associate the value with.
     * @param {*} value The value to write.
     * @returns {Promise} The promise resolves after writing to the current object store finished.
     */
    async put(key, value) {
        if (this._state !== Transaction.STATE.OPEN) {
            throw new Error('Transaction already closed');
        }

        // Check indices.
        const constraints = [];
        for (const index of this._indices.values()) {
            constraints.push(index.checkUniqueConstraint(key, value, /*isInStore*/ false));
        }
        await Promise.all(constraints);

        this._put(key, value);
    }

    /**
     * Inserts or replaces a key-value pair.
     * @param {string} key The primary key to associate the value with.
     * @param {*} value The value to write.
     */
    putSync(key, value) {
        if (this._state !== Transaction.STATE.OPEN) {
            throw new Error('Transaction already closed');
        }

        this._put(key, value);
    }

    /**
     * Removes the key-value pair of the given key from the object store.
     * @param {string} key The primary key to delete along with the associated object.
     * @returns {Promise} The promise resolves after writing to the current object store finished.
     */
    async remove(key) {
        if (this._state !== Transaction.STATE.OPEN) {
            throw new Error('Transaction already closed');
        }

        this._remove(key);
    }

    /**
     * Removes the key-value pair of the given key from the object store.
     * @param {string} key The primary key to delete along with the associated object.
     */
    removeSync(key) {
        if (this._state !== Transaction.STATE.OPEN) {
            throw new Error('Transaction already closed');
        }

        this._remove(key);
    }

    /**
     * Returns a promise of a set of keys fulfilling the given query.
     * If the optional query is not given, it returns all keys in the object store.
     * If the query is of type KeyRange, it returns all keys of the object store being within this range.
     * If the query is of type Query, it returns all keys fulfilling the query.
     * @param {Query|KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<Set.<string>>} A promise of the set of keys relevant to the query.
     */
    async keys(query=null) {
        if (query !== null && query instanceof Query) {
            return query.keys(this);
        }
        let keys = new Set();
        if (!this._truncated) {
            keys = await this._parent.keys(query);
        }
        keys = keys.difference(this._removed);
        for (const key of this._modified.keys()) {
            if (query === null || query.includes(key)) {
                keys.add(key);
            }
        }
        return keys;
    }

    /**
     * Returns a promise of an array of objects whose primary keys fulfill the given query.
     * If the optional query is not given, it returns all objects in the object store.
     * If the query is of type KeyRange, it returns all objects whose primary keys are within this range.
     * If the query is of type Query, it returns all objects whose primary keys fulfill the query.
     * @param {Query|KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<Array.<*>>} A promise of the array of objects relevant to the query.
     */
    async values(query=null) {
        if (query !== null && query instanceof Query) {
            return query.values(this);
        }
        const keys = await this.keys(query);
        const valuePromises = [];
        for (const key of keys) {
            valuePromises.push(this.get(key));
        }
        return Promise.all(valuePromises);
    }

    /**
     * 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.
     */
    async keyStream(callback, ascending=true, query=null) {
        // TODO Optimize this sorting step.
        let keys = Array.from(this._modified.keys());
        if (query instanceof KeyRange) {
            keys = keys.filter(key => query.includes(key));
        }
        keys = keys.sort();

        let txIt = keys.iterator(ascending);
        if (!this._truncated) {
            let stopped = false;

            await this._parent.keyStream(key => {
                // Iterate over TxKeys as long as they are smaller (ascending) or larger (descending).
                while (txIt.hasNext() && ((ascending && ComparisonUtils.compare(txIt.peek(), key) < 0) || (!ascending && ComparisonUtils.compare(txIt.peek(), key) > 0))) {
                    const currentTxKey = txIt.next();
                    if (!callback(currentTxKey)) {
                        // Do not continue iteration.
                        stopped = true;
                        return false;
                    }
                }
                // Special case: what if next key is identical (-> modified)?
                // Present modified version and continue.
                if (txIt.hasNext() && ComparisonUtils.equals(txIt.peek(), key)) {
                    const currentTxKey = txIt.next();
                    if (!callback(currentTxKey)) {
                        // Do not continue iteration.
                        stopped = true;
                        return false;
                    }
                    return true;
                }
                // Then give key of the backend's key stream.
                // But only if it hasn't been removed (lazy operator prevents calling callback in this case).
                if (!this._removed.has(key) && !callback(key)) {
                    // Do not continue iteration.
                    stopped = true;
                    return false;
                }
                return true;
            }, ascending, query);

            // Do not continue, if already stopped.
            if (stopped) {
                return;
            }
        }

        // Iterate over the remaining TxKeys.
        while (txIt.hasNext()) {
            if (!callback(txIt.next())) {
                break;
            }
        }
    }

    /**
     * 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.
     */
    async valueStream(callback, ascending=true, query=null) {
        // TODO Optimize this sorting step.
        let keys = Array.from(this._modified.keys());
        if (query instanceof KeyRange) {
            keys = keys.filter(key => query.includes(key));
        }
        keys = keys.sort();

        let txIt = keys.iterator(ascending);
        if (!this._truncated) {
            let stopped = false;

            await this._parent.valueStream((value, key) => {
                // Iterate over TxKeys as long as they are smaller (ascending) or larger (descending).
                while (txIt.hasNext() && ((ascending && ComparisonUtils.compare(txIt.peek(), key) < 0) || (!ascending && ComparisonUtils.compare(txIt.peek(), key) > 0))) {
                    const currentTxKey = txIt.next();
                    const value = this._modified.get(currentTxKey);
                    if (!callback(value, currentTxKey)) {
                        // Do not continue iteration.
                        stopped = true;
                        return false;
                    }
                }
                // Special case: what if next key is identical (-> modified)?
                // Present modified version and continue.
                if (txIt.hasNext() && ComparisonUtils.equals(txIt.peek(), key)) {
                    const currentTxKey = txIt.next();
                    const value = this._modified.get(currentTxKey);
                    if (!callback(value, currentTxKey)) {
                        // Do not continue iteration.
                        stopped = true;
                        return false;
                    }
                    return true;
                }
                // Then give key of the backend's key stream.
                // But only if it hasn't been removed (lazy operator prevents calling callback in this case).
                if (!this._removed.has(key) && !callback(value, key)) {
                    // Do not continue iteration.
                    stopped = true;
                    return false;
                }
                return true;
            }, ascending, query);

            // Do not continue, if already stopped.
            if (stopped) {
                return;
            }
        }

        // Iterate over the remaining TxKeys.
        while (txIt.hasNext()) {
            const key = txIt.next();
            const value = await this.get(key);
            if (!callback(value, key)) {
                break;
            }
        }
    }

    /**
     * Returns a promise of the object whose primary key is maximal for the given range.
     * If the optional query is not given, it returns the object whose key is maximal.
     * If the query is of type KeyRange, it returns the object whose primary key is maximal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<*>} A promise of the object relevant to the query.
     */
    async maxValue(query=null) {
        const maxKey = await this.maxKey(query);
        return this.get(maxKey);
    }

    /**
     * Returns a promise of the key being maximal for the given range.
     * If the optional query is not given, it returns the maximal key.
     * If the query is of type KeyRange, it returns the key being maximal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<string>} A promise of the key relevant to the query.
     */
    async maxKey(query=null) {
        // Take underlying maxKey.
        let maxKey = undefined;
        if (!this._truncated) {
            maxKey = await this._parent.maxKey(query);
        }

        // If this key has been removed, find next best key.
        while (maxKey !== undefined && this._removed.has(maxKey)) {
            const tmpQuery = KeyRange.upperBound(maxKey, true);
            maxKey = await this._parent.maxKey(tmpQuery);

            // If we get out of the range, stop here.
            if (query !== null && !query.includes(maxKey)) {
                maxKey = undefined;
                break;
            }
        }

        for (const key of this._modified.keys()) {
            // Find better maxKey in modified data.
            if ((query === null || query.includes(key)) && (maxKey === undefined || ComparisonUtils.compare(key, maxKey) > 0)) {
                maxKey = key;
            }
        }
        return maxKey;
    }

    /**
     * Returns a promise of the object whose primary key is minimal for the given range.
     * If the optional query is not given, it returns the object whose key is minimal.
     * If the query is of type KeyRange, it returns the object whose primary key is minimal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<*>} A promise of the object relevant to the query.
     */
    async minValue(query=null) {
        const minKey = await this.minKey(query);
        return this.get(minKey);
    }

    /**
     * Returns a promise of the key being minimal for the given range.
     * If the optional query is not given, it returns the minimal key.
     * If the query is of type KeyRange, it returns the key being minimal for the given range.
     * @param {KeyRange} [query] Optional query to check keys against.
     * @returns {Promise.<string>} A promise of the key relevant to the query.
     */
    async minKey(query=null) {
        // Take underlying minKey.
        let minKey = undefined;
        if (!this._truncated) {
            minKey = await this._parent.minKey(query);
        }

        // If this key has been removed, find next best key.
        while (minKey !== undefined && this._removed.has(minKey)) {
            const tmpQuery = KeyRange.lowerBound(minKey, true);
            minKey = await this._parent.minKey(tmpQuery);

            // If we get out of the range, stop here.
            if (query !== null && !query.includes(minKey)) {
                minKey = undefined;
                break;
            }
        }

        for (const key of this._modified.keys()) {
            // Find better maxKey in modified data.
            if ((query === null || query.includes(key)) && (minKey === undefined || key < minKey)) {
                minKey = key;
            }
        }
        return minKey;
    }

    /**
     * Returns the count of entries in the given range.
     * If the optional query is not given, it returns the count of entries in the object store.
     * If the query is of type KeyRange, it returns the count of entries within the given range.
     * @param {KeyRange} [query]
     * @returns {Promise.<number>}
     */
    async count(query=null) {
        // Unfortunately, we cannot do better than getting keys + counting.
        return (await this.keys(query)).size;
    }

    /**
     * Returns the index of the given name.
     * If the index does not exist, it returns undefined.
     * @param {string} indexName The name of the requested index.
     * @returns {IIndex} The index associated with the given name.
     */
    index(indexName) {
        return this._indices.get(indexName);
    }

    /**
     * Alias for abort.
     * @returns {Promise} The promise resolves after successful abortion of the transaction.
     */
    close() {
        return this.abort();
    }

    /**
     * Creates a nested transaction, ensuring read isolation.
     * This makes the current transaction read-only until all sub-transactions have been closed (committed/aborted).
     * The same semantic for commits applies: Only the first transaction that commits will be applied. Subsequent transactions will be conflicted.
     * This behaviour has one exception: If all nested transactions are closed, the outer transaction returns to a normal state and new nested transactions can again be created and committed.
     * @param {boolean} [enableWatchdog]
     * @returns {Transaction} The transaction object.
     */
    transaction(enableWatchdog = true) {
        if (this._state !== Transaction.STATE.OPEN && this._state !== Transaction.STATE.NESTED) {
            throw new Error('Transaction already closed');
        }
        const tx = new Transaction(this._objectStore, this, this, enableWatchdog);
        this._nested.add(tx);
        this._state = Transaction.STATE.NESTED;
        return tx;
    }


    /**
     * Creates a nested synchronous transaction, ensuring read isolation.
     * This makes the current transaction read-only until all sub-transactions have been closed (committed/aborted).
     * The same semantic for commits applies: Only the first transaction that commits will be applied. Subsequent transactions will be conflicted.
     * This behaviour has one exception: If all nested transactions are closed, the outer transaction returns to a normal state and new nested transactions can again be created and committed.
     * @param {boolean} [enableWatchdog]
     * @returns {SynchronousTransaction} The transaction object.
     */
    synchronousTransaction(enableWatchdog = true) {
        if (this._state !== Transaction.STATE.OPEN && this._state !== Transaction.STATE.NESTED) {
            throw new Error('Transaction already closed');
        }
        const tx = new SynchronousTransaction(this._objectStore, this, this, enableWatchdog);
        this._nested.add(tx);
        this._state = Transaction.STATE.NESTED;
        return tx;
    }

    /**
     * Checks whether an object store implements the ISynchronousObjectStore interface.
     * @returns {boolean} The transaction object.
     */
    isSynchronous() {
        return false;
    }

    /**
     * Creates an in-memory snapshot of this state.
     * This snapshot only maintains the differences between the state at the time of the snapshot
     * and the current state.
     * To stop maintaining the snapshot, it has to be aborted.
     * @returns {Snapshot}
     */
    snapshot() {
        if (this.state !== Transaction.STATE.COMMITTED) {
            const snapshot = this._managingBackend.snapshot();
            snapshot.inherit(this);
            return snapshot;
        }
        return this._snapshotManager.createSnapshot(this._objectStore, this);
    }

    toString() {
        return `Transaction{id=${this._id}, changes=±${this._modified.size+this._removed.size}, truncated=${this._truncated}, objectStore=${this._objectStore}, state=${this._state}, dependency=${this._dependency}}`;
    }

    toStringShort() {
        return `Transaction{id=${this._id}, changes=±${this._modified.size+this._removed.size}, truncated=${this._truncated}, state=${this._state}, dependency=${this._dependency}}`;
    }

    /**
     * Commits a transaction to the underlying backend.
     * The state is only written to the persistent backend if no other transaction is open.
     * If the commit was successful, new transactions will always be based on the new state.
     * There are two outcomes for a commit:
     * If there was no other transaction committed that was based on the same state,
     * it will be successful and change the transaction's state to COMMITTED (returning true).
     * Otherwise, the state will be CONFLICTED and the method will return false.
     *
     * Note that transactions may fail since secondary index constraints are *not* checked in transactions.
     * @param {Transaction} [tx] The transaction to be applied, only used internally.
     * @returns {Promise.<boolean>} A promise of the success outcome.
     */
    async commit(tx) {
        // Transaction is given, so check whether this is a nested one.
        if (tx !== undefined) {
            if (!this._isCommittable(tx)) {
                await this.abort(tx);
                return false;
            }

            await this._commitInternal(tx);
            return true;
        }

        if (this._dependency !== null) {
            return this._dependency.commit();
        }

        await this._checkConstraints();

        return this._commitBackend();
    }

    /**
     * Aborts a transaction and (if this was the last open transaction) potentially
     * persists the most recent, committed state.
     * @param {Transaction} [tx] The transaction to be applied, only used internally.
     * @returns {Promise.<boolean>} A promise of the success outcome.
     */
    async abort(tx) {
        // Transaction is given, so check whether this is a nested one.
        if (tx !== undefined) {
            // Handle snapshots.
            if (tx instanceof Snapshot) {
                return this._snapshotManager.abortSnapshot(tx);
            }

            // Make sure transaction is based on this transaction.
            if (!this._nested.has(tx) || tx.state !== Transaction.STATE.OPEN) {
                throw new Error('Can only abort open, nested transactions');
            }
            this._nested.delete(tx);
            // If there are no more nested transactions, change back to OPEN state.
            if (this._nested.size === 0) {
                this._state = Transaction.STATE.OPEN;
                this._nestedCommitted = false;
            }
            return true;
        }

        if (this._dependency !== null) {
            return this._dependency.abort();
        }

        return this._abortBackend();
    }

    /**
     * Internally applies a transaction to the transaction's state.
     * This needs to be done in batch (as a db level transaction), i.e., either the full state is updated
     * or no changes are applied.
     * @param {Transaction} tx The transaction to apply.
     * @returns {Promise} The promise resolves after applying the transaction.
     * @protected
     */
    async _apply(tx) {
        if (!(tx instanceof Transaction)) {
            throw new Error('Can only apply transactions');
        }

        // First handle snapshots.
        await this._snapshotManager.applyTx(tx, this);

        this._applySync(tx);
    }

    /**
     * Commits the transaction to the backend.
     * @returns {Promise.<boolean>} A promise of the success outcome.
     * @protected
     */
    async _commitBackend() {
        if (this._state !== Transaction.STATE.OPEN) {
            throw new Error('Transaction already closed or in nested state');
        }
        if (this._enableWatchdog) {
            clearTimeout(this._watchdog);
        }

        const commitStart = Date.now();
        if (await this._managingBackend.commit(this)) {
            this._state = Transaction.STATE.COMMITTED;
            this._performanceCheck(commitStart, 'commit');
            this._performanceCheck();
            return true;
        } else {
            this._state = Transaction.STATE.CONFLICTED;
            this._performanceCheck(commitStart, 'commit');
            this._performanceCheck();
            return false;
        }
    }

    /**
     * @param {number} [startTime]
     * @param {string} [functionName]
     * @private
     */
    _performanceCheck(startTime=this._startTime, functionName=null) {
        const executionTime = Date.now() - startTime;
        functionName = functionName ? ` function '${functionName}'` : '';
        if (executionTime > Transaction.WATCHDOG_TIMER) {
            Log.w(Transaction, `Violation: tx id ${this._id}${functionName} took ${(executionTime/1000).toFixed(2)}s (${this.toString()}).`);
        }
    }

    /**
     * Is used to probe whether a transaction can be committed.
     * This, for example, includes a check whether another transaction has already been committed.
     * @protected
     * @param {Transaction} [tx] The transaction to be applied, if not given checks for the this transaction.
     * @returns {boolean} Whether a commit will be successful.
     */
    _isCommittable(tx) {
        if (tx !== undefined) {
            // Make sure transaction is based on this transaction.
            if (!this._nested.has(tx) || tx.state !== Transaction.STATE.OPEN) {
                throw new Error('Can only commit open, nested transactions');
            }
            return !this._nestedCommitted;
        }
        return this._managingBackend._isCommittable(this);
    }

    /**
     * Is used to commit the transaction to the in memory state.
     * @protected
     * @param {Transaction} tx The transaction to be applied.
     * @returns {Promise} A promise that resolves upon successful application of the transaction.
     */
    async _commitInternal(tx) {
        this._nested.delete(tx);
        // Apply nested transaction.
        this._nestedCommitted = true;
        await this._apply(tx);
        // If there are no more nested transactions, change back to OPEN state.
        if (this._nested.size === 0) {
            this._state = Transaction.STATE.OPEN;
            this._nestedCommitted = false;
        }
    }

    /**
     * Allows to change the backend of a Transaction when the state has been flushed.
     * @param parent
     * @protected
     */
    _setParent(parent) {
        this._parent = parent;
    }

    /**
     * Aborts a transaction on the backend.
     * @returns {Promise.<boolean>} A promise of the success outcome.
     */
    async _abortBackend() {
        if (this._state === Transaction.STATE.ABORTED || this._state === Transaction.STATE.CONFLICTED) {
            return true;
        }
        if (this._state !== Transaction.STATE.OPEN && this._state !== Transaction.STATE.NESTED) {
            throw new Error('Transaction already closed');
        }
        if (this._state === Transaction.STATE.NESTED) {
            await Promise.all(Array.from(this._nested).map(tx => tx.abort()));
        }
        if (this._enableWatchdog) {
            clearTimeout(this._watchdog);
        }
        const abortStart = Date.now();
        await this._managingBackend.abort(this);
        this._setAborted();
        this._performanceCheck(abortStart, 'abort');
        this._performanceCheck();
        return true;
    }

    /**
     * Sets the state to aborted.
     */
    _setAborted() {
        this._state = Transaction.STATE.ABORTED;
    }

    /**
     * Internal method for inserting/replacing a key-value pair.
     * @param {string} key The primary key to associate the value with.
     * @param {*} value The value to write.
     * @protected
     */
    _put(key, value) {
        this._removed.delete(key);
        const localOldValue = this._modified.get(key);
        this._modified.set(key, value);

        // Update indices.
        for (const index of this._indices.values()) {
            index.put(key, value, localOldValue);
        }
    }

    /**
     * Internal method for removing a key-value pair.
     * @param {string} key The primary key to delete along with the associated object.
     * @protected
     */
    _remove(key) {
        this._removed.add(key);
        const localOldValue = this._modified.get(key);
        this._modified.delete(key);

        // Update indices.
        for (const index of this._indices.values()) {
            index.remove(key, localOldValue);
        }
    }

    /**
     * Is used to check constraints before committing.
     * If a constraint is not satisfied, the commitable is aborted and an exception is thrown.
     * @returns {Promise.<boolean>}
     * @throws
     * @protected
     */
    async _checkConstraints() {
        // Check unique indices.
        // TODO: Improve performance (|modified| count queries).
        const constraintChecks = [];
        for (const /** @type {TransactionIndex} */ index of this._indices.values()) {
            if (!index.unique) continue;
            for (const [key, value] of this._modified) {
                constraintChecks.push(index.checkUniqueConstraint(key, value));
            }
        }
        if (constraintChecks.length > 0) {
            try {
                await Promise.all(constraintChecks);
            } catch (e) {
                await this.abort();
                throw e;
            }
        }
    }

    /**
     * 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) {
        return this._objectStore.decode(value, key);
    }

    /**
     * Method called to encode a single value.
     * @param {*} value Value to be encoded.
     * @returns {*} The encoded value.
     */
    encode(value) {
        return this._objectStore.encode(value);
    }
}
/** @type {number} Milliseconds to wait until automatically aborting transaction. */
Transaction.WATCHDOG_TIMER = 5000 /*ms*/;
/**
 * The states of a transaction.
 * New transactions are in the state OPEN until they are aborted, committed or a nested transaction is created.
 * Aborted transactions move to the state ABORTED.
 * Committed transactions move to the state COMMITTED,
 * if no other transaction has been applied to the same state.
 * Otherwise, they change their state to CONFLICTED.
 * When creating a nested (not read-isolated) transaction on top of a transaction,
 * the outer transaction moves to the state NESTED until the inner transaction is either aborted or committed.
 * Again, only one inner transaction may be committed.
 * @enum {number}
 */
Transaction.STATE = {
    OPEN: 0,
    COMMITTED: 1,
    ABORTED: 2,
    CONFLICTED: 3,
    NESTED: 4
};
Transaction._instanceCount = 0;
Class.register(Transaction);