"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module iModels
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.TxnManager = exports.RebaseManager = void 0;
exports.setMaxEntitiesPerEvent = setMaxEntitiesPerEvent;
const touch = require("touch");
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const BackendLoggerCategory_1 = require("./BackendLoggerCategory");
const IpcHost_1 = require("./IpcHost");
const Symbols_1 = require("./internal/Symbols");
const ChangesetConflictArgs_1 = require("./internal/ChangesetConflictArgs");
const BriefcaseManager_1 = require("./BriefcaseManager");
/** Strictly for tests. @internal */
function setMaxEntitiesPerEvent(max) {
    const prevMax = ChangedEntitiesProc.maxPerEvent;
    ChangedEntitiesProc.maxPerEvent = max;
    return prevMax;
}
/** Maintains an ordered array of entity Ids and a parallel array containing the index of the corresponding entity's class Id. */
class ChangedEntitiesArray {
    entityIds = new core_bentley_1.OrderedId64Array();
    _classIndices = [];
    _classIds;
    constructor(classIds) {
        this._classIds = classIds;
    }
    insert(entityId, classId) {
        const entityIndex = this.entityIds.insert(entityId);
        const classIndex = this._classIds.insert(classId);
        (0, core_bentley_1.assert)(classIndex >= 0);
        if (this.entityIds.length !== this._classIndices.length) {
            // New entity - insert corresponding class index entry.
            this._classIndices.splice(entityIndex, 0, classIndex);
        }
        else {
            // Existing entity - update corresponding class index.
            // (We do this because apparently connectors can (very rarely) change the class Id of an existing element).
            this._classIndices[entityIndex] = classIndex;
        }
        (0, core_bentley_1.assert)(this.entityIds.length === this._classIndices.length);
    }
    clear() {
        this.entityIds.clear();
        this._classIndices.length = 0;
    }
    addToChangedEntities(entities, type) {
        if (this.entityIds.length > 0)
            entities[type] = core_bentley_1.CompressedId64Set.compressIds(this.entityIds);
        entities[`${type}Meta`] = this._classIndices;
    }
    iterable(classIds) {
        function* iterator(entityIds, classIndices) {
            const entity = { id: "", classId: "" };
            for (let i = 0; i < entityIds.length; i++) {
                entity.id = entityIds[i];
                entity.classId = classIds[classIndices[i]];
                yield entity;
            }
        }
        return {
            [Symbol.iterator]: () => iterator(this.entityIds.array, this._classIndices),
        };
    }
}
class ChangedEntitiesProc {
    _classIds = new core_bentley_1.IndexMap((lhs, rhs) => (0, core_bentley_1.compareStrings)(lhs, rhs));
    _inserted = new ChangedEntitiesArray(this._classIds);
    _deleted = new ChangedEntitiesArray(this._classIds);
    _updated = new ChangedEntitiesArray(this._classIds);
    _currSize = 0;
    static maxPerEvent = 1000;
    static process(iModel, mgr) {
        if (mgr.isDisposed) {
            // The iModel is being closed. Do not prepare new sqlite statements.
            return;
        }
        this.processChanges(iModel, mgr.onElementsChanged, "notifyElementsChanged");
        this.processChanges(iModel, mgr.onModelsChanged, "notifyModelsChanged");
    }
    populateMetadata(db, classIds) {
        // Ensure metadata for all class Ids is loaded. Loading metadata for a derived class loads metadata for all of its superclasses.
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        const classIdsToLoad = classIds.filter((x) => undefined === db.classMetaDataRegistry.findByClassId(x));
        if (classIdsToLoad.length > 0) {
            const classIdsStr = classIdsToLoad.join(",");
            const sql = `SELECT ec_class.Name, ec_class.Id, ec_schema.Name FROM ec_class JOIN ec_schema WHERE ec_schema.Id = ec_class.SchemaId AND ec_class.Id IN (${classIdsStr})`;
            db.withPreparedSqliteStatement(sql, (stmt) => {
                while (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) {
                    const classFullName = `${stmt.getValueString(2)}:${stmt.getValueString(0)}`;
                    // eslint-disable-next-line @typescript-eslint/no-deprecated
                    db.tryGetMetaData(classFullName);
                }
            });
        }
        // Define array indices for the metadata array entries correlating to the class Ids in the input list.
        const nameToIndex = new Map();
        for (const classId of classIds) {
            // eslint-disable-next-line @typescript-eslint/no-deprecated
            const meta = db.classMetaDataRegistry.findByClassId(classId);
            nameToIndex.set(meta?.ecclass ?? "", nameToIndex.size);
        }
        const result = [];
        function addMetadata(name, index) {
            const bases = [];
            result[index] = { name, bases };
            // eslint-disable-next-line @typescript-eslint/no-deprecated
            const meta = db.tryGetMetaData(name);
            if (!meta) {
                return;
            }
            for (const baseClassName of meta.baseClasses) {
                let baseClassIndex = nameToIndex.get(baseClassName);
                if (undefined === baseClassIndex) {
                    baseClassIndex = nameToIndex.size;
                    nameToIndex.set(baseClassName, baseClassIndex);
                    addMetadata(baseClassName, baseClassIndex);
                }
                bases.push(baseClassIndex);
            }
        }
        for (const [name, index] of nameToIndex) {
            if (index >= classIds.length) {
                // Entries beyond this are base classes for the classes in `classIds` - don't reprocess them.
                break;
            }
            addMetadata(name, index);
        }
        return result;
    }
    sendEvent(iModel, evt, evtName) {
        if (this._currSize === 0)
            return;
        const classIds = this._classIds.toArray();
        // Notify backend listeners.
        const txnEntities = {
            inserts: this._inserted.iterable(classIds),
            deletes: this._deleted.iterable(classIds),
            updates: this._updated.iterable(classIds),
        };
        evt.raiseEvent(txnEntities);
        // Notify frontend listeners.
        const entities = {
            insertedMeta: [],
            updatedMeta: [],
            deletedMeta: [],
            meta: this.populateMetadata(iModel, classIds),
        };
        this._inserted.addToChangedEntities(entities, "inserted");
        this._deleted.addToChangedEntities(entities, "deleted");
        this._updated.addToChangedEntities(entities, "updated");
        IpcHost_1.IpcHost.notifyTxns(iModel, evtName, entities);
        // Reset state.
        this._inserted.clear();
        this._deleted.clear();
        this._updated.clear();
        this._classIds.clear();
        this._currSize = 0;
    }
    static processChanges(iModel, changedEvent, evtName) {
        try {
            const maxSize = this.maxPerEvent;
            const changes = new ChangedEntitiesProc();
            const select = "notifyElementsChanged" === evtName
                ? "SELECT ElementId, ChangeType, ECClassId FROM temp.txn_Elements"
                : "SELECT ModelId, ChangeType, ECClassId FROM temp.txn_Models";
            iModel.withPreparedSqliteStatement(select, (sql) => {
                const stmt = sql.stmt;
                while (sql.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) {
                    const id = stmt.getValueId(0);
                    const classId = stmt.getValueId(2);
                    switch (stmt.getValueInteger(1)) {
                        case 0:
                            changes._inserted.insert(id, classId);
                            break;
                        case 1:
                            changes._updated.insert(id, classId);
                            break;
                        case 2:
                            changes._deleted.insert(id, classId);
                            break;
                    }
                    if (++changes._currSize >= maxSize)
                        changes.sendEvent(iModel, changedEvent, evtName);
                }
            });
            changes.sendEvent(iModel, changedEvent, evtName);
        }
        catch (err) {
            core_bentley_1.Logger.logError(BackendLoggerCategory_1.BackendLoggerCategory.IModelDb, core_bentley_1.BentleyError.getErrorMessage(err));
        }
    }
}
/**
 * Manages the process of merging and rebasing local changes (transactions) in a [[BriefcaseDb]] or [[StandaloneDb]].
 *
 * The `RebaseManager` coordinates the rebase of local transactions when pulling and merging changes from other sources,
 * such as remote repositories or other users. It provides mechanisms to handle transaction conflicts, register custom conflict
 * handlers, and manage the rebase workflow. This includes resuming rebases, invoking user-defined handlers for conflict resolution,
 * and tracking the current merge/rebase state.
 *
 * Key responsibilities:
 * - Orchestrates the rebase of local transactions after a pull/merge operation.
 * - Allows registration and removal of custom conflict handlers to resolve changeset conflicts during rebase.
 * - Provides methods to check the current merge/rebase state.
 * - Raises events before and after each transaction is rebased.
 * - Ensures changes are saved or aborted appropriately based on the outcome of the rebase process.
 *
 * @alpha
 */
class RebaseManager {
    _iModel;
    _conflictHandlers;
    _customHandler;
    _aborting = false;
    /** Event raised before pull merge process begins.
     * @alpha
     */
    onPullMergeBegin = new core_bentley_1.BeEvent();
    /** Event raised before a rebase operation begins.
     * @alpha
     */
    onRebaseBegin = new core_bentley_1.BeEvent();
    /** Event raised before a transaction is rebased.
     * @alpha
     */
    onRebaseTxnBegin = new core_bentley_1.BeEvent();
    /** Event raised after a transaction is rebased.
     * @alpha
     */
    onRebaseTxnEnd = new core_bentley_1.BeEvent();
    /** Event raised after a rebase operation ends.
     * @alpha
     */
    onRebaseEnd = new core_bentley_1.BeEvent();
    /** Event raised after pull merge process ends.
     * @alpha
     */
    onPullMergeEnd = new core_bentley_1.BeEvent();
    /** Event raised before applying incoming changes.
     * @alpha
     */
    onApplyIncomingChangesBegin = new core_bentley_1.BeEvent();
    /** Event raised after applying incoming changes.
     * @alpha
     */
    onApplyIncomingChangesEnd = new core_bentley_1.BeEvent();
    /** Event raised before reversing local changes.
    * @alpha
     */
    onReverseLocalChangesBegin = new core_bentley_1.BeEvent();
    /** Event raised after reversing local changes.
     * @alpha
     */
    onReverseLocalChangesEnd = new core_bentley_1.BeEvent();
    /** Event raised before downloading changesets.
     * @alpha
     */
    onDownloadChangesetsBegin = new core_bentley_1.BeEvent();
    /** Event raised after downloading changesets.
     * @alpha
     */
    onDownloadChangesetsEnd = new core_bentley_1.BeEvent();
    /** @internal */
    notifyPullMergeBegin(changeset) {
        this.onPullMergeBegin.raiseEvent(changeset);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyPullMergeBegin", changeset);
    }
    /** @internal */
    notifyPullMergeEnd(changeset) {
        this.onPullMergeEnd.raiseEvent(changeset);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyPullMergeEnd", changeset);
    }
    /** @internal */
    notifyApplyIncomingChangesBegin(changes) {
        this.onApplyIncomingChangesBegin.raiseEvent(changes);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyApplyIncomingChangesBegin", changes);
    }
    /** @internal */
    notifyApplyIncomingChangesEnd(changes) {
        this.onApplyIncomingChangesEnd.raiseEvent(changes);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyApplyIncomingChangesEnd", changes);
    }
    /** @internal */
    notifyReverseLocalChangesBegin() {
        this.onReverseLocalChangesBegin.raiseEvent();
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyReverseLocalChangesBegin");
    }
    /** @internal */
    notifyReverseLocalChangesEnd(txns) {
        this.onReverseLocalChangesEnd.raiseEvent(txns);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyReverseLocalChangesEnd", txns);
    }
    /** @internal */
    notifyDownloadChangesetsBegin() {
        this.onDownloadChangesetsBegin.raiseEvent();
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyDownloadChangesetsBegin");
    }
    /** @internal */
    notifyDownloadChangesetsEnd() {
        this.onDownloadChangesetsEnd.raiseEvent();
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyDownloadChangesetsEnd");
    }
    /** @internal */
    notifyRebaseBegin(txns) {
        this.onRebaseBegin.raiseEvent(txns);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyRebaseBegin", txns);
    }
    /** @internal */
    notifyRebaseEnd(txns) {
        this.onRebaseEnd.raiseEvent(txns);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyRebaseEnd", txns);
    }
    /** @internal */
    notifyRebaseTxnBegin(txnProps) {
        this.onRebaseTxnBegin.raiseEvent(txnProps);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyRebaseTxnBegin", txnProps);
    }
    /** @internal */
    notifyRebaseTxnEnd(txnProps) {
        this.onRebaseTxnEnd.raiseEvent(txnProps);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyRebaseTxnEnd", txnProps);
    }
    constructor(_iModel) {
        this._iModel = _iModel;
    }
    /**
     * Resumes the rebase process for the current iModel, applying any pending local changes
     * on top of the latest pulled changes from the remote source.
     *
     * This method performs the following steps:
     * 1. Begins the rebase process using the native database.
     * 2. Iterates through each transaction that needs to be rebased:
     *    - Retrieves transaction properties.
     *    - Raises events before and after rebasing each transaction.
     *    - Optionally reinstates local changes based on the rebase handler.
     *    - Optionally recomputes transaction data using the rebase handler.
     *    - Updates the transaction in the native database.
     * 3. Ends the rebase process and saves changes if the database is not read-only.
     * 4. Drops any restore point associated with the pull-merge operation.
     *
     * If an error occurs during the process, the rebase is aborted and the error is rethrown.
     *
     * @throws {Error} If a transaction cannot be found or if any step in the rebase process fails.
     */
    async resume() {
        const nativeDb = this._iModel[Symbols_1._nativeDb];
        const txns = this._iModel.txns;
        try {
            const reversedTxns = nativeDb.pullMergeRebaseBegin();
            const reversedTxnProps = reversedTxns.map((_) => txns.getTxnProps(_)).filter((_) => _ !== undefined);
            this.notifyRebaseBegin(reversedTxnProps);
            let txnId = nativeDb.pullMergeRebaseNext();
            while (txnId) {
                const txnProps = txns.getTxnProps(txnId);
                if (!txnProps) {
                    throw new Error(`Transaction ${txnId} not found`);
                }
                this.notifyRebaseTxnBegin(txnProps);
                core_bentley_1.Logger.logInfo(BackendLoggerCategory_1.BackendLoggerCategory.IModelDb, `Rebasing local changes for transaction ${txnId}`);
                const shouldReinstate = this._customHandler?.shouldReinstate(txnProps) ?? true;
                if (shouldReinstate) {
                    nativeDb.pullMergeRebaseReinstateTxn();
                    core_bentley_1.Logger.logInfo(BackendLoggerCategory_1.BackendLoggerCategory.IModelDb, `Reinstated local changes for transaction ${txnId}`);
                }
                if (this._customHandler) {
                    await this._customHandler.recompute(txnProps);
                }
                nativeDb.pullMergeRebaseUpdateTxn();
                this.notifyRebaseTxnEnd(txnProps);
                txnId = nativeDb.pullMergeRebaseNext();
            }
            nativeDb.pullMergeRebaseEnd();
            this.notifyRebaseEnd(reversedTxnProps);
            if (!nativeDb.isReadonly) {
                nativeDb.saveChanges("Merge.");
            }
            if (BriefcaseManager_1.BriefcaseManager.containsRestorePoint(this._iModel, BriefcaseManager_1.BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME)) {
                BriefcaseManager_1.BriefcaseManager.dropRestorePoint(this._iModel, BriefcaseManager_1.BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME);
            }
            this.notifyPullMergeEnd(this._iModel.changeset);
        }
        catch (err) {
            nativeDb.pullMergeRebaseAbortTxn();
            throw err;
        }
    }
    /**
     * Determines whether the current transaction can be aborted.
     *
     * This method checks if a transaction is currently in progress and if a specific restore point,
     * identified by `BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME`, exists in the briefcase manager.
     *
     * @returns {boolean} Returns `true` if a transaction is in progress and the required restore point exists; otherwise, returns `false`.
     */
    canAbort() {
        return this.inProgress() && BriefcaseManager_1.BriefcaseManager.containsRestorePoint(this._iModel, BriefcaseManager_1.BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME);
    }
    /**
     * Aborts the current transaction by restoring the iModel to a predefined restore point. This method will
     * automatically discard any unsaved changes before performing the restore.
     *
     * If a restore point is available (as determined by `canAbort()`), this method restores the iModel
     * to the state saved at the restore point named by `BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME`.
     * If no restore point is available, an error is thrown.
     *
     * @returns A promise that resolves when the restore operation is complete.
     * @throws {Error} If there is no restore point to abort to.
     */
    async abort() {
        if (this.canAbort()) {
            this._aborting = true;
            try {
                if (this._iModel.txns.hasUnsavedChanges) {
                    this._iModel.abandonChanges();
                }
                await BriefcaseManager_1.BriefcaseManager.restorePoint(this._iModel, BriefcaseManager_1.BriefcaseManager.PULL_MERGE_RESTORE_POINT_NAME);
            }
            finally {
                this._aborting = false;
            }
        }
        else {
            throw new Error("No restore point to abort to");
        }
    }
    /**
     * Sets the handler to be invoked for rebase operations.
     *
     * @param handler - The {@link RebaseHandler} to handle rebase events.
     */
    setCustomHandler(handler) {
        if (this._customHandler) {
            core_bentley_1.Logger.logWarning(BackendLoggerCategory_1.BackendLoggerCategory.IModelDb, "Rebase handler already set");
        }
        this._customHandler = handler;
    }
    /**
     * Determines whether a transaction is currently in progress.
     *
     * @returns {boolean} Returns `true` if there is an active transaction stage, otherwise `false`.
     */
    inProgress() {
        return this._iModel[Symbols_1._nativeDb].pullMergeGetStage() !== "None";
    }
    /**
     * Indicates whether the current transaction manager is in the process of aborting a transaction.
     *
     * @returns `true` if the transaction manager is currently aborting; otherwise, `false`.
     */
    get isAborting() {
        return this._aborting;
    }
    /**
     * Indicates whether the current transaction manager is in the "Rebasing" stage.
     *
     * This property checks the internal native database's merge stage to determine if a rebase operation is in progress.
     *
     * @returns `true` if the transaction manager is currently rebasing; otherwise, `false`.
     */
    get isRebasing() {
        return this._iModel[Symbols_1._nativeDb].pullMergeGetStage() === "Rebasing";
    }
    /**
     * Indicates whether the current iModel is in the process of merging changes from a pull operation.
     *
     * @returns `true` if the iModel is currently merging changes; otherwise, `false`.
     */
    get isMerging() {
        return this._iModel[Symbols_1._nativeDb].pullMergeGetStage() === "Merging";
    }
    /**
     * Attempts to resolve a changeset conflict by invoking registered conflict handlers in sequence.
     *
     * Iterates through the linked list of conflict handlers, passing the provided conflict arguments to each handler.
     * If a handler returns a defined resolution, logs the resolution and returns it immediately.
     * If no handler resolves the conflict, returns `undefined`.
     *
     * @param args - The arguments describing the changeset conflict to resolve.
     * @returns The conflict resolution provided by a handler, or `undefined` if no handler resolves the conflict.
     */
    onConflict(args) {
        let curr = this._conflictHandlers;
        while (curr) {
            const resolution = curr.handler(args);
            if (resolution !== undefined) {
                core_bentley_1.Logger.logTrace(BackendLoggerCategory_1.BackendLoggerCategory.IModelDb, `Conflict handler ${curr.id} resolved conflict`);
                return resolution;
            }
            curr = curr.next;
        }
        return undefined;
    }
    /**
     * Registers a new conflict handler for rebase changeset conflicts.
     *
     * @param args - An object containing:
     *   - `id`: A unique identifier for the conflict handler.
     *   - `handler`: A function that handles rebase changeset conflicts and returns a `DbConflictResolution` or `undefined`.
     * @throws IModelError if a conflict handler with the same `id` already exists.
     *
     * @remarks
     * Conflict handlers are used during changeset rebase operations to resolve conflicts.
     * Each handler must have a unique `id`. Attempting to register a handler with a duplicate `id` will result in an error.
     */
    addConflictHandler(args) {
        const idExists = (id) => {
            let curr = this._conflictHandlers;
            while (curr) {
                if (curr.id === id)
                    return true;
                curr = curr.next;
            }
            return false;
        };
        if (idExists(args.id))
            throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, `Conflict handler with id ${args.id} already exists`);
        this._conflictHandlers = { ...args, next: this._conflictHandlers };
    }
    /**
     * Removes a conflict handler from the internal linked list by its identifier.
     *
     * @param id - The unique identifier of the conflict handler to remove.
     *
     * If the handler with the specified `id` exists in the list, it will be removed.
     * If no handler with the given `id` is found, the method does nothing.
     */
    removeConflictHandler(id) {
        if (!this._conflictHandlers)
            return;
        if (this._conflictHandlers?.id === id) {
            this._conflictHandlers = this._conflictHandlers.next;
            return;
        }
        let prev = this._conflictHandlers;
        let curr = this._conflictHandlers?.next;
        while (curr) {
            if (curr.id === id) {
                prev.next = curr.next;
                return;
            }
            prev = curr;
            curr = curr.next;
        }
    }
}
exports.RebaseManager = RebaseManager;
/** Manages local changes to a [[BriefcaseDb]] or [[StandaloneDb]] via [Txns]($docs/learning/InteractiveEditing.md)
 * @public @preview
 */
class TxnManager {
    _iModel;
    /** @internal */
    _isDisposed = false;
    /** @internal */
    _withIndirectChangeRefCounter = 0;
    /** @internal */
    get isDisposed() {
        return this._isDisposed;
    }
    /** @internal */
    rebaser;
    /** @internal */
    constructor(_iModel) {
        this._iModel = _iModel;
        this.rebaser = new RebaseManager(_iModel);
        _iModel.onBeforeClose.addOnce(() => {
            this._isDisposed = true;
        });
    }
    /** Array of errors from dependency propagation */
    validationErrors = [];
    get _nativeDb() { return this._iModel[Symbols_1._nativeDb]; }
    _getElementClass(elClassName) {
        return this._iModel.getJsClass(elClassName);
    }
    _getRelationshipClass(relClassName) {
        return this._iModel.getJsClass(relClassName);
    }
    /** If a -watch file exists for this iModel, update its timestamp so watching processes can be
     * notified that we've modified the briefcase.
     * @internal Used by IModelDb on push/pull.
     */
    touchWatchFile() {
        // This is an async call. We don't have any reason to await it.
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
        touch(this._iModel.watchFilePathName, { nocreate: true });
    }
    /** @internal */
    _onBeforeOutputsHandled(elClassName, elId) {
        this._getElementClass(elClassName).onBeforeOutputsHandled(elId, this._iModel);
    }
    /** @internal */
    _onAllInputsHandled(elClassName, elId) {
        this._getElementClass(elClassName).onAllInputsHandled(elId, this._iModel);
    }
    /** @internal */
    _onRootChanged(props) {
        this._getRelationshipClass(props.classFullName).onRootChanged(props, this._iModel);
    }
    /** @internal */
    _onDeletedDependency(props) {
        this._getRelationshipClass(props.classFullName).onDeletedDependency(props, this._iModel);
    }
    /** @internal */
    _onBeginValidate() { this.validationErrors.length = 0; }
    /** called from native code after validation of a Txn, either from saveChanges or apply changeset.
     * @internal
     */
    _onEndValidate() {
        ChangedEntitiesProc.process(this._iModel, this);
        this.onEndValidation.raiseEvent();
        // TODO: if (this.validationErrors.length !== 0) throw new IModelError(validation ...)
    }
    /** @internal */
    _onGeometryChanged(modelProps) {
        this.onGeometryChanged.raiseEvent(modelProps);
        IpcHost_1.IpcHost.notifyEditingScope(this._iModel, "notifyGeometryChanged", modelProps); // send to frontend
    }
    /** @internal */
    _onGeometryGuidsChanged(changes) {
        this.onModelGeometryChanged.raiseEvent(changes);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyGeometryGuidsChanged", changes);
    }
    /** @internal */
    _onCommit() {
        this.onCommit.raiseEvent();
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyCommit");
    }
    /** @internal */
    _onCommitted() {
        this.touchWatchFile();
        this.onCommitted.raiseEvent();
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyCommitted", this.hasPendingTxns, Date.now());
    }
    /** @internal */
    _onReplayExternalTxns() {
        this.onReplayExternalTxns.raiseEvent();
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyReplayExternalTxns");
    }
    /** @internal */
    _onReplayedExternalTxns() {
        this.onReplayedExternalTxns.raiseEvent();
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyReplayedExternalTxns");
    }
    /** @internal */
    _onChangesApplied() {
        // Should only clear instance caches, not all caches
        this._iModel.clearCaches({ instanceCachesOnly: true });
        ChangedEntitiesProc.process(this._iModel, this);
        this.onChangesApplied.raiseEvent();
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyChangesApplied");
    }
    /** @internal */
    _onBeforeUndoRedo(isUndo) {
        this.onBeforeUndoRedo.raiseEvent(isUndo);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyBeforeUndoRedo", isUndo);
    }
    /** @internal */
    _onAfterUndoRedo(isUndo) {
        this.touchWatchFile();
        this.onAfterUndoRedo.raiseEvent(isUndo);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyAfterUndoRedo", isUndo);
    }
    /** @internal */
    // eslint-disable-next-line @typescript-eslint/naming-convention
    _onChangesPushed(changeset) {
        this.touchWatchFile();
        this.onChangesPushed.raiseEvent(changeset);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyPushedChanges", changeset);
    }
    /** @internal */
    // eslint-disable-next-line @typescript-eslint/naming-convention
    _onChangesPulled(changeset) {
        this.touchWatchFile();
        this.onChangesPulled.raiseEvent(changeset);
        IpcHost_1.IpcHost.notifyTxns(this._iModel, "notifyPulledChanges", changeset);
    }
    _onRebaseLocalTxnConflict(internalArg) {
        const args = new ChangesetConflictArgs_1.RebaseChangesetConflictArgs(internalArg, this._iModel);
        const getChangeMetaData = () => {
            return {
                parent: this._iModel.changeset,
                txn: args.txn,
                table: args.tableName,
                op: args.opcode,
                cause: args.cause,
                indirect: args.indirect,
                primarykey: args.getPrimaryKeyValues(),
                fkConflictCount: args.cause === "ForeignKey" ? args.getForeignKeyConflicts() : undefined,
            };
        };
        // Default conflict resolution for which custom handler is never called.
        if (args.cause === "Data" && !args.indirect) {
            if (args.tableName === "be_Prop") {
                if (args.getValueText(0, "Old") === "ec_Db" && args.getValueText(1, "Old") === "localDbInfo") {
                    return core_bentley_1.DbConflictResolution.Skip;
                }
            }
            if (args.tableName.startsWith("ec_")) {
                return core_bentley_1.DbConflictResolution.Skip;
            }
        }
        if (args.cause === "Conflict") {
            if (args.tableName.startsWith("ec_")) {
                return core_bentley_1.DbConflictResolution.Skip;
            }
        }
        try {
            const resolution = this.rebaser.onConflict(args);
            if (resolution !== undefined)
                return resolution;
        }
        catch (err) {
            const msg = `Rebase failed. Custom conflict handler should not throw exception. Aborting txn. ${core_bentley_1.BentleyError.getErrorMessage(err)}`;
            core_bentley_1.Logger.logError(BackendLoggerCategory_1.BackendLoggerCategory.IModelDb, msg, getChangeMetaData());
            args.setLastError(msg);
            return core_bentley_1.DbConflictResolution.Abort;
        }
        if (args.cause === "Data" && !args.indirect) {
            core_bentley_1.Logger.logInfo(BackendLoggerCategory_1.BackendLoggerCategory.IModelDb, "UPDATE/DELETE before value do not match with one in db or CASCADE action was triggered. Local change will replace existing.", getChangeMetaData());
            return core_bentley_1.DbConflictResolution.Replace;
        }
        if (args.cause === "Conflict") {
            const msg = "PRIMARY KEY insert conflict. Aborting rebase.";
            core_bentley_1.Logger.logError(BackendLoggerCategory_1.BackendLoggerCategory.IModelDb, msg, getChangeMetaData());
            args.setLastError(msg);
            return core_bentley_1.DbConflictResolution.Abort;
        }
        if (args.cause === "ForeignKey") {
            const msg = `Foreign key conflicts in ChangeSet. Aborting rebase.`;
            core_bentley_1.Logger.logInfo(BackendLoggerCategory_1.BackendLoggerCategory.IModelDb, msg, getChangeMetaData());
            args.setLastError(msg);
            return core_bentley_1.DbConflictResolution.Abort;
        }
        if (args.cause === "NotFound") {
            core_bentley_1.Logger.logInfo(BackendLoggerCategory_1.BackendLoggerCategory.IModelDb, "PRIMARY KEY not found. Skipping local change.", getChangeMetaData());
            return core_bentley_1.DbConflictResolution.Skip;
        }
        if (args.cause === "Constraint") {
            core_bentley_1.Logger.logInfo(BackendLoggerCategory_1.BackendLoggerCategory.IModelDb, "Constraint violation detected. Generally caused by db constraints like UNIQUE index. Skipping local change.", getChangeMetaData());
            return core_bentley_1.DbConflictResolution.Skip;
        }
        return core_bentley_1.DbConflictResolution.Replace;
    }
    /**
     * @alpha
     * Retrieves the txn properties for a given txn ID.
     *
     * @param id - The unique identifier of the transaction.
     * @returns The properties of the transaction if found; otherwise, `undefined`.
     */
    getTxnProps(id) {
        return this._iModel[Symbols_1._nativeDb].getTxnProps(id);
    }
    /**
     * @alpha
     * Iterates over all transactions in the sequence, yielding each transaction's properties.
     *
     * @yields {TxnProps} The properties of each transaction in the sequence.
     */
    *queryTxns() {
        let txn = this.getTxnProps(this.queryFirstTxnId());
        while (txn) {
            yield txn;
            txn = txn.nextId ? this.getTxnProps(txn.nextId) : undefined;
        }
    }
    /**
     * @alpha
     * Retrieves the properties of the last saved txn via `IModelDb.saveChanges()`, if available.
     *
     * @returns The properties of the last saved txn, or `undefined` if none exist.
     */
    getLastSavedTxnProps() {
        return this.getTxnProps(this.queryPreviousTxnId(this.getCurrentTxnId()));
    }
    /** Dependency handlers may call method this to report a validation error.
     * @param error The error. If error.fatal === true, the transaction will cancel rather than commit.
     */
    reportError(error) {
        this.validationErrors.push(error);
        this._nativeDb.logTxnError(error.fatal);
    }
    /** Determine whether any fatal validation errors have occurred during dependency propagation.  */
    get hasFatalError() { return this._nativeDb.hasFatalTxnError(); }
    /** @internal */
    onEndValidation = new core_bentley_1.BeEvent();
    /** Called after validation completes from [[IModelDb.saveChanges]].
     * The argument to the event holds the list of elements that were inserted, updated, and deleted.
     * @note If there are many changed elements in a single Txn, the notifications are sent in batches so this event *may be called multiple times* per Txn.
     */
    onElementsChanged = new core_bentley_1.BeEvent();
    /** Called after validation completes from [[IModelDb.saveChanges]].
     * The argument to the event holds the list of models that were inserted, updated, and deleted.
     * @note If there are many changed models in a single Txn, the notifications are sent in batches so this event *may be called multiple times* per Txn.
     */
    onModelsChanged = new core_bentley_1.BeEvent();
    /** Event raised after the geometry within one or more [[GeometricModel]]s is modified by applying a changeset or validation of a transaction.
     * A model's geometry can change as a result of:
     *  - Insertion or deletion of a geometric element within the model; or
     *  - Modification of an existing element's geometric properties; or
     *  - An explicit request to flag it as changed via [[IModelDb.Models.updateModel]].
     */
    onModelGeometryChanged = new core_bentley_1.BeEvent();
    onGeometryChanged = new core_bentley_1.BeEvent();
    /** Event raised before a commit operation is performed. Initiated by a call to [[IModelDb.saveChanges]], unless there are no changes to save. */
    onCommit = new core_bentley_1.BeEvent();
    /** Event raised after a commit operation has been performed. Initiated by a call to [[IModelDb.saveChanges]], even if there were no changes to save. */
    onCommitted = new core_bentley_1.BeEvent();
    /** Event raised after a ChangeSet has been applied to this briefcase */
    onChangesApplied = new core_bentley_1.BeEvent();
    /** Event raised before an undo/redo operation is performed. */
    onBeforeUndoRedo = new core_bentley_1.BeEvent();
    /** Event raised after an undo/redo operation has been performed.
     * @param _action The action that was performed.
     */
    onAfterUndoRedo = new core_bentley_1.BeEvent();
    /** Event raised for a read-only briefcase that was opened with the `watchForChanges` flag enabled when changes made by another connection are applied to the briefcase.
     * @see [[onReplayedExternalTxns]] for the event raised after all such changes have been applied.
     */
    onReplayExternalTxns = new core_bentley_1.BeEvent();
    /** Event raised for a read-only briefcase that was opened with the `watchForChanges` flag enabled when changes made by another connection are applied to the briefcase.
     * @see [[onReplayExternalTxns]] for the event raised before the changes are applied.
     */
    onReplayedExternalTxns = new core_bentley_1.BeEvent();
    /** Event raised after changes are pulled from iModelHub.
     * @see [[BriefcaseDb.pullChanges]].
     */
    onChangesPulled = new core_bentley_1.BeEvent();
    /** Event raised after changes are pushed to iModelHub.
     * @see [[BriefcaseDb.pushChanges]].
     */
    onChangesPushed = new core_bentley_1.BeEvent();
    /**
     * if handler is set and it does not return undefined then default handler will not be called
     * @internal
     * */
    appCustomConflictHandler;
    /**
     * Restart the current TxnManager session. This causes all Txns in the current session to no longer be undoable (as if the file was closed
     * and reopened.)
     * @note This can be quite disconcerting to the user expecting to be able to undo previously made changes. It should only be used
     * under extreme circumstances where damage to the file or session could happen if the currently committed are reversed. Use sparingly and with care.
     * Probably a good idea to alert the user it happened.
     */
    restartSession() {
        this._nativeDb.restartTxnSession();
    }
    /** Determine whether current txn is propagating indirect changes or not. */
    get isIndirectChanges() { return this.getMode() === "indirect"; }
    /** Determine if there are currently any reversible (undoable) changes from this editing session. */
    get isUndoPossible() { return this._nativeDb.isUndoPossible(); }
    /** Determine if there are currently any reinstatable (redoable) changes */
    get isRedoPossible() { return this._nativeDb.isRedoPossible(); }
    /** Get the description of the operation that would be reversed by calling reverseTxns(1).
     * This is useful for showing the operation that would be undone, for example in a menu.
     */
    getUndoString() { return this._nativeDb.getUndoString(); }
    /** Get a description of the operation that would be reinstated by calling reinstateTxn.
     * This is useful for showing the operation that would be redone, in a pull-down menu for example.
     */
    getRedoString() { return this._nativeDb.getRedoString(); }
    /** Begin a new multi-Txn operation. This can be used to cause a series of Txns that would normally
     * be considered separate actions for undo to be grouped into a single undoable operation. This means that when reverseTxns(1) is called,
     * the entire group of changes are undone together. Multi-Txn operations can be nested and until the outermost operation is closed
     * all changes constitute a single operation.
     * @note This method must always be paired with a call to endMultiTxnAction.
     */
    beginMultiTxnOperation() { return this._nativeDb.beginMultiTxnOperation(); }
    /** End a multi-Txn operation */
    endMultiTxnOperation() { return this._nativeDb.endMultiTxnOperation(); }
    /** Return the depth of the multi-Txn stack. Generally for diagnostic use only. */
    getMultiTxnOperationDepth() { return this._nativeDb.getMultiTxnOperationDepth(); }
    /** Reverse (undo) the most recent operation(s) to this IModelDb.
     * @param numOperations the number of operations to reverse. If this is greater than 1, the entire set of operations will
     *  be reinstated together when/if ReinstateTxn is called.
     * @note If there are any outstanding uncommitted changes, they are reversed.
     * @note The term "operation" is used rather than Txn, since multiple Txns can be grouped together via [[beginMultiTxnOperation]]. So,
     * even if numOperations is 1, multiple Txns may be reversed if they were grouped together when they were made.
     * @note If numOperations is too large only the operations are reversible are reversed.
     */
    reverseTxns(numOperations) {
        return this._nativeDb.reverseTxns(numOperations);
    }
    /** Reverse the most recent operation. */
    reverseSingleTxn() { return this.reverseTxns(1); }
    /** Reverse all changes back to the beginning of the session. */
    reverseAll() { return this._nativeDb.reverseAll(); }
    /** Reverse all changes back to a previously saved TxnId.
     * @param txnId a TxnId obtained from a previous call to GetCurrentTxnId.
     * @returns Success if the transactions were reversed, error status otherwise.
     * @see  [[getCurrentTxnId]] [[cancelTo]]
     */
    reverseTo(txnId) { return this._nativeDb.reverseTo(txnId); }
    /** Reverse and then cancel (make non-reinstatable) all changes back to a previous TxnId.
     * @param txnId a TxnId obtained from a previous call to [[getCurrentTxnId]]
     * @returns Success if the transactions were reversed and cleared, error status otherwise.
     */
    cancelTo(txnId) { return this._nativeDb.cancelTo(txnId); }
    /** Reinstate the most recently reversed transaction. Since at any time multiple transactions can be reversed, it
     * may take multiple calls to this method to reinstate all reversed operations.
     * @returns Success if a reversed transaction was reinstated, error status otherwise.
     * @note If there are any outstanding uncommitted changes, they are canceled before the Txn is reinstated.
     */
    reinstateTxn() { return this._iModel.reinstateTxn(); }
    /** Get the Id of the first transaction, if any.
     */
    queryFirstTxnId() { return this._nativeDb.queryFirstTxnId(); }
    /** Get the successor of the specified TxnId */
    queryNextTxnId(txnId) { return this._nativeDb.queryNextTxnId(txnId); }
    /** Get the predecessor of the specified TxnId */
    queryPreviousTxnId(txnId) { return this._nativeDb.queryPreviousTxnId(txnId); }
    /** Get the Id of the current (tip) transaction.  */
    getCurrentTxnId() { return this._nativeDb.getCurrentTxnId(); }
    /**
     * @alpha
     * Get the Id of the current session.
     */
    getCurrentSessionId() { return this._nativeDb.currentTxnSessionId(); }
    /** Get the description that was supplied when the specified transaction was saved. */
    getTxnDescription(txnId) { return this._nativeDb.getTxnDescription(txnId); }
    /** Test if a TxnId is valid */
    isTxnIdValid(txnId) { return this._nativeDb.isTxnIdValid(txnId); }
    /** Query if there are any pending Txns in this IModelDb that are waiting to be pushed.
     * @see [[IModelDb.pushChanges]]
     */
    get hasPendingTxns() { return this._nativeDb.hasPendingTxns(); }
    /**
     * Query if there are any changes in memory that have yet to be saved to the IModelDb.
     * @see [[IModelDb.saveChanges]]
     */
    get hasUnsavedChanges() { return this._nativeDb.hasUnsavedChanges(); }
    /**
     * @alpha
     * Query if there are any pending schema changes in this IModelDb.
     */
    get hasPendingSchemaChanges() { return this._nativeDb.hasPendingSchemaChanges(); }
    /**
     * Query if there are changes in memory that have not been saved to the iModelDb or if there are Txns that are waiting to be pushed.
     * @see [[IModelDb.saveChanges]]
     * @see [[IModelDb.pushChanges]]
     */
    get hasLocalChanges() { return this.hasUnsavedChanges || this.hasPendingTxns; }
    /** Destroy the record of all local changes that have yet to be saved and/or pushed.
     * This permanently eradicates your changes - use with caution!
     * Typically, callers will want to subsequently use [[LockControl.releaseAllLocks]].
     * After calling this function, [[hasLocalChanges]], [[hasPendingTxns]], and [[hasUnsavedChanges]] will all be `false`.
     */
    deleteAllTxns() {
        this._nativeDb.deleteAllTxns();
    }
    /** Obtain a list of the EC instances that have been changed locally by the [[BriefcaseDb]] associated with this `TxnManager` and have not yet been pushed to the iModel.
     * @beta
    */
    queryLocalChanges(args) {
        if (!args) {
            args = { includedClasses: [], includeUnsavedChanges: false };
        }
        return this._nativeDb.getLocalChanges(args.includedClasses ?? [], args.includeUnsavedChanges ?? false);
    }
    /** Query the number of bytes of memory currently allocated by SQLite to keep track of
     * changes to the iModel, for debugging/diagnostic purposes, as reported by [sqlite3session_memory_used](https://www.sqlite.org/session/sqlite3session_memory_used.html).
     */
    getChangeTrackingMemoryUsed() {
        return this._iModel[Symbols_1._nativeDb].getChangeTrackingMemoryUsed();
    }
    /**
     * @alpha
     * Get the current transaction mode.
     * @returns The current transaction mode, either "direct" or "indirect".
     */
    getMode() {
        return this._nativeDb.getTxnMode();
    }
    /**
     * @alpha
     * Execute a series of changes in an indirect transaction.
     * @param callback The function containing the changes to make.
     */
    withIndirectTxnMode(callback) {
        if (this._withIndirectChangeRefCounter === 0) {
            this._nativeDb.setTxnMode("indirect");
        }
        this._withIndirectChangeRefCounter++;
        try {
            callback();
        }
        finally {
            this._withIndirectChangeRefCounter--;
            if (this._withIndirectChangeRefCounter === 0) {
                this._nativeDb.setTxnMode("direct");
            }
        }
    }
    /**
     * @alpha
     * Execute a series of changes in an indirect transaction.
     * @param callback The function containing the changes to make.
     */
    async withIndirectTxnModeAsync(callback) {
        if (this._withIndirectChangeRefCounter === 0) {
            this._nativeDb.setTxnMode("indirect");
        }
        this._withIndirectChangeRefCounter++;
        try {
            await callback();
        }
        finally {
            this._withIndirectChangeRefCounter--;
            if (this._withIndirectChangeRefCounter === 0) {
                this._nativeDb.setTxnMode("direct");
            }
        }
    }
}
exports.TxnManager = TxnManager;
//# sourceMappingURL=TxnManager.js.map