@itwin/core-backend
Version:
iTwin.js backend components
626 lines • 30.5 kB
JavaScript
"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.ChangeMergeManager = 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");
/** 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));
}
}
}
/**
* @internal
* Manages conflict resolution during a merge operation.
*/
class ChangeMergeManager {
_iModel;
_conflictHandlers;
constructor(_iModel) {
this._iModel = _iModel;
}
resume() {
this._iModel[Symbols_1._nativeDb].pullMergeResume();
}
inProgress() {
return this._iModel[Symbols_1._nativeDb].pullMergeInProgress();
}
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;
}
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 };
}
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.ChangeMergeManager = ChangeMergeManager;
/** Manages local changes to a [[BriefcaseDb]] or [[StandaloneDb]] via [Txns]($docs/learning/InteractiveEditing.md)
* @public @preview
*/
class TxnManager {
_iModel;
/** @internal */
_isDisposed = false;
/** @internal */
get isDisposed() {
return this._isDisposed;
}
/** @internal */
changeMergeManager;
/** @internal */
constructor(_iModel) {
this._iModel = _iModel;
this.changeMergeManager = new ChangeMergeManager(_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() {
this._iModel.clearCaches();
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);
}
_onRebaseTxnBegin(txn) {
this.onRebaseTxnBegin.raiseEvent(txn);
}
_onRebaseTxnEnd(txn) {
this.onRebaseTxnEnd.raiseEvent(txn);
}
_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.changeMergeManager.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 voilation 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;
}
/** 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();
/** @internal */
onRebaseTxnBegin = new core_bentley_1.BeEvent();
/** @internal */
onRebaseTxnEnd = new core_bentley_1.BeEvent();
/**
* if handler is set and it does not return undefiend 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._nativeDb.isIndirectChanges(); }
/** 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(); }
/** 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(); }
/**
* 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();
}
}
exports.TxnManager = TxnManager;
//# sourceMappingURL=TxnManager.js.map