packageDocumentation module import deepAssign from "deep-assign" ; import { AccessToken , GuidString , Id64String , IModelHubStatus , Logger } from "@itwin/core-bentley" ; import { ResponseError } from "../itwin-client/Request" ; import { ECJsonTypeMap , WsgInstance } from "../wsg/ECJsonTypeMap" ; import { WsgQuery } from "../wsg/WsgQuery" ; import { HttpRequestOptions , WsgRequestOptions } from "../wsg/WsgClient" ; import { IModelHubClientLoggerCategory } from "../IModelHubClientLoggerCategories" ; import { IModelBaseHandler } from "./BaseHandler" ; import { AggregateResponseError , ArgumentCheck , IModelHubError } from "./Errors" ; const loggerCategory = IModelHubClientLoggerCategory . IModelHub ; internal export enum LockType { /** Lock for the entire file. This is a global Lock that can only be taken with objectId 1. */ Db , /** Lock for a model. It should be acquired together with a [[LockLevel.Shared]] Db Lock. */ Model , /** Lock for a single element. It should be acquired together with a [[LockLevel.Shared]] Model Lock. */ Element , /** Lock used to change schemas. This is a global Lock that can only be taken with objectId 1. This lock cannot have [[LockLevel.Shared]]. */ Schemas , /** Lock used to change CodeSpecs. This is a global Lock that can only be taken with objectId 1. This lock cannot have [[LockLevel.Shared]]. */ CodeSpecs } internal export enum LockLevel { /** Lock is not owned. */ None = 0 , /** Lock can be owned by multiple [[Briefcase]]s. Shared Lock is usually acquired together with Locks for lower level objects that depend on this object. */ Shared = 1 , /** Lock can only be owned by a single briefcase. Exclusive Lock is required to modify model or element when using pessimistic concurrency. */ Exclusive = 2 } param lock returns internal function getLockInstanceId ( lock ) { if ( ! lock || lock . briefcaseId === undefined || lock . lockType === undefined || ! lock . objectId ) return undefined ; return `${ lock . lockType }-${ lock . objectId }-${ lock . briefcaseId }` ; } internal export interface LockUpdateOptions { /** Return [[Lock]]s that could not be acquired. Conflicting Locks will be set to [[ConflictingLocksError.conflictingLocks]]. If unlimitedReporting is enabled and locksPerRequest value is high, some conflicting Locks could be missed. */ deniedLocks ? : boolean ; /** Attempt to get all failed [[Lock]]s, ignoring iModelHub limits. Server responses might fail when trying to return large number of conflicting Locks. */ unlimitedReporting ? : boolean ; /** Number of [[Lock]]s per single request. Multiple requests will be sent if there are more Locks. If an error happens on a subsequent request, previous successful updates will not be reverted. */ locksPerRequest ? : number ; /** Don't fail request on a conflict. If conflict occurs, [[Lock]]s that didn't have conflicts will be updated and any remaining subsequent requests will still be sent. */ continueOnConflict ? : boolean ; } internal export class DefaultLockUpdateOptionsProvider { protected _defaultOptions : LockUpdateOptions ; /** Creates an instance of DefaultRequestOptionsProvider and sets up the default options. */ constructor ( ) { this . _defaultOptions = { locksPerRequest : 2000 } ; } note param options public async assignOptions ( options ) { const clonedOptions = { ... options } ; deepAssign ( options , this . _defaultOptions ) ; deepAssign ( options , clonedOptions ) ; } } internal export class ConflictingLocksError extends IModelHubError { /** Locks that couldn't be updated due to other users owning them. */ public conflictingLocks ? : Lock [ ] ; param error returns internal public static fromError ( error ) { if ( error . errorNumber !== IModelHubStatus . LockOwnedByAnotherBriefcase && error . errorNumber !== IModelHubStatus . ConflictsAggregate ) { return undefined ; } const result = new ConflictingLocksError ( error . errorNumber ) ; deepAssign ( result , error ) ; result . addLocks ( error ) ; return result ; } param error internal public addLocks ( error ) { if ( ! error . data || ! error . data . ConflictingLocks ) { return ; } if ( ! this . conflictingLocks ) { this . conflictingLocks = [ ] ; } for ( const value of ( error . data . ConflictingLocks as any [ ] ) ) { const instance = { className : "Lock" , schemaName : "iModelScope" , properties : value } ; const lock = ECJsonTypeMap . fromJson < Lock > ( Lock , "wsg" , instance ) ; if ( lock ) { this . conflictingLocks . push ( lock ) ; } } } } internal export class LockBase extends WsgInstance { /** Type of the Lock. It describes what kind of object is locked. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.LockType" ) public lockType ? : LockType ; /** Level of the Lock. It describes how restrictive the Lock is. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.LockLevel" ) public lockLevel ? : LockLevel ; /** Id of the [[Briefcase]] that owns the Lock. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.BriefcaseId" ) public briefcaseId ? : number ; /** Id of the file, that the Lock belongs to. See [[Briefcase.fileId]]. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.SeedFileId" ) public seedFileId ? : GuidString ; /** Id of the [[ChangeSet]] that the Lock was last used with. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.ReleasedWithChangeSet" ) public releasedWithChangeSet ? : string ; /** Index of the [[ChangeSet]] that the Lock was last used with. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.ReleasedWithChangeSetIndex" ) public releasedWithChangeSetIndex ? : string ; } internal @ ECJsonTypeMap . classToJson ( "wsg" , "iModelScope.Lock" , { schemaPropertyName : "schemaName" , classPropertyName : "className" } ) export class Lock extends LockBase { /** Id of the locked object. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.ObjectId" ) public objectId ? : Id64String ; } internal @ ECJsonTypeMap . classToJson ( "wsg" , "iModelScope.MultiLock" , { schemaPropertyName : "schemaName" , classPropertyName : "className" } ) export class MultiLock extends LockBase { @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.ObjectIds" ) public objectIds ? : Id64String [ ] ; } internal export class LockQuery extends WsgQuery { private _isMultiLockQuery = true ; internal public static defaultPageSize : number = 10000 ; /** Constructor that sets default page size. */ constructor ( ) { super ( ) ; this . pageSize ( LockQuery . defaultPageSize ) ; } internal public get isMultiLockQuery ( ) { return this . _isMultiLockQuery ; } param briefcaseId returns throws public byBriefcaseId ( briefcaseId ) { ArgumentCheck . validBriefcaseId ( "briefcaseId" , briefcaseId ) ; this . addFilter ( `BriefcaseId+eq+${ briefcaseId }` ) ; return this ; } param lockType returns public byLockType ( lockType ) { this . addFilter ( `LockType+eq+${ lockType }` ) ; return this ; } param lockLevel returns public byLockLevel ( lockLevel ) { this . addFilter ( `LockLevel+eq+${ lockLevel }` ) ; return this ; } param objectId returns throws public byObjectId ( objectId ) { ArgumentCheck . defined ( "objectId" , objectId ) ; this . _isMultiLockQuery = false ; this . addFilter ( `ObjectId+eq+'${ objectId }'` ) ; return this ; } param changesetId returns throws public byReleasedWithChangeSet ( changeSetId ) { ArgumentCheck . validChangeSetId ( "changeSetId" , changeSetId ) ; this . addFilter ( `ReleasedWithChangeSet+eq+'${ changeSetId }'` ) ; return this ; } param changeSetIndex returns throws public byReleasedWithChangeSetIndex ( changeSetIndex ) { ArgumentCheck . definedNumber ( "changeSetIndex" , changeSetIndex ) ; this . addFilter ( `ReleasedWithChangeSetIndex+eq+${ changeSetIndex }` ) ; return this ; } param locks returns throws public byLocks ( locks ) { ArgumentCheck . nonEmptyArray ( "locks" , locks ) ; let filter = "$id+in+[" ; let index = 0 ; for ( const lock of locks ) { const id = getLockInstanceId ( lock ) ; ArgumentCheck . valid ( `locks[${ index }]` , id ) ; if ( 0 !== index ++ ) filter += "," ; filter += `'${ id }'` ; } filter += "]" ; this . addFilter ( filter ) ; this . _isMultiLockQuery = false ; return this ; } param briefcaseId param lastChangeSetIndex returns throws public unavailableLocks ( briefcaseId , lastChangeSetIndex ) { ArgumentCheck . validBriefcaseId ( "briefcaseId" , briefcaseId ) ; ArgumentCheck . defined ( "lastChangeSetIndex" , lastChangeSetIndex ) ; let filter = `BriefcaseId+ne+${ briefcaseId }` ; filter += `+and+(LockLevel+gt+0+or+ReleasedWithChangeSetIndex+gt+${ lastChangeSetIndex })` ; this . addFilter ( filter ) ; return this ; } } internal export class LockHandler { private _handler : IModelBaseHandler ; private static _defaultUpdateOptionsProvider : DefaultLockUpdateOptionsProvider ; param handler internal constructor ( handler ) { this . _handler = handler ; } private getRelativeUrl ( iModelId , multilock = true , lockId ? ) { return `/Repositories/iModel--${ iModelId }/iModelScope/${ multilock ? "MultiLock" : "Lock" }/${ lockId || "" }` ; } private static convertLocksToMultiLocks ( locks ) { const map = new Map < string , MultiLock > ( ) ; for ( const lock of locks ) { const id = `${ lock . lockType }-${ lock . lockLevel }` ; if ( map . has ( id ) ) { map . get ( id ) ! . objectIds ! . push ( lock . objectId ! ) ; } else { const multiLock = new MultiLock ( ) ; multiLock . changeState = "new" ; multiLock . briefcaseId = lock . briefcaseId ; multiLock . seedFileId = lock . seedFileId ; multiLock . releasedWithChangeSet = lock . releasedWithChangeSet ; multiLock . releasedWithChangeSetIndex = lock . releasedWithChangeSetIndex ; multiLock . lockLevel = lock . lockLevel ; multiLock . lockType = lock . lockType ; multiLock . objectIds = [ lock . objectId ! ] ; map . set ( id , multiLock ) ; } } return Array . from ( map . values ( ) ) ; } private static convertMultiLocksToLocks ( multiLocks ) { const result = [ ] ; for ( const multiLock of multiLocks ) { for ( const value of multiLock . objectIds ! ) { const lock = new Lock ( ) ; lock . objectId = value ; lock . briefcaseId = multiLock . briefcaseId ; if ( lock . seedFileId ) lock . seedFileId = multiLock . seedFileId ; lock . lockLevel = multiLock . lockLevel ; lock . lockType = multiLock . lockType ; result . push ( lock ) ; } } return result ; } param options private async setupOptionDefaults ( options ) { if ( ! LockHandler . _defaultUpdateOptionsProvider ) LockHandler . _defaultUpdateOptionsProvider = new DefaultLockUpdateOptionsProvider ( ) ; return LockHandler . _defaultUpdateOptionsProvider . assignOptions ( options ) ; } /** Send partial request for lock updates */ private async updateInternal ( accessToken , iModelId , locks , updateOptions ? ) { let requestOptions ; if ( updateOptions ) { requestOptions = { } ; requestOptions . CustomOptions = { } ; if ( updateOptions . deniedLocks === false ) { requestOptions . CustomOptions . DetailedError_Codes = "false" ; } if ( updateOptions . unlimitedReporting ) { requestOptions . CustomOptions . DetailedError_MaximumInstances = "-1" ; } if ( updateOptions . continueOnConflict ) { requestOptions . CustomOptions . ConflictStrategy = "Continue" ; } if ( Object . getOwnPropertyNames ( requestOptions . CustomOptions ) . length === 0 ) { requestOptions = undefined ; } } const result = await this . _handler . postInstances < MultiLock > ( accessToken , MultiLock , `/Repositories/iModel--${ iModelId }/$changeset` , LockHandler . convertLocksToMultiLocks ( locks ) , requestOptions ) ; return LockHandler . convertMultiLocksToLocks ( result ) ; } param iModelId param locks param updateOptions returns throws throws throws throws throws throws public async update ( accessToken , iModelId , locks , updateOptions ? ) { Logger . logInfo ( loggerCategory , "Requesting locks" , ( ) => ( { iModelId } ) ) ; ArgumentCheck . validGuid ( "iModelId" , iModelId ) ; ArgumentCheck . nonEmptyArray ( "locks" , locks ) ; updateOptions = updateOptions || { } ; await this . setupOptionDefaults ( updateOptions ) ; const result = [ ] ; let conflictError ; const aggregateError = new AggregateResponseError ( ) ; for ( let i = 0 ; i < locks . length ; i += updateOptions . locksPerRequest ! ) { const chunk = locks . slice ( i , i + updateOptions . locksPerRequest ! ) ; try { result . push ( ... await this . updateInternal ( accessToken , iModelId , chunk , updateOptions ) ) ; } catch ( error ) { if ( error instanceof ResponseError ) { if ( updateOptions && updateOptions . deniedLocks && error instanceof IModelHubError && ( error . errorNumber === IModelHubStatus . LockOwnedByAnotherBriefcase || error . errorNumber === IModelHubStatus . ConflictsAggregate ) ) { if ( conflictError ) { conflictError . addLocks ( error ) ; } else { conflictError = ConflictingLocksError . fromError ( error ) ; } if ( ! updateOptions . continueOnConflict ) { throw conflictError ; } } else { aggregateError . errors . push ( error ) ; } } } } if ( conflictError ) { throw conflictError ; } if ( aggregateError . errors . length > 0 ) { throw aggregateError . errors . length > 1 ? aggregateError : aggregateError . errors [ 0 ] ; } Logger . logTrace ( loggerCategory , `Requested ${ locks . length } locks for iModel` , ( ) => ( { iModelId } ) ) ; return result ; } param iModelId param query returns throws public async get ( accessToken , iModelId , query = new LockQuery ( ) ) { Logger . logInfo ( loggerCategory , "Querying locks" , ( ) => ( { iModelId } ) ) ; ArgumentCheck . validGuid ( "iModelId" , iModelId ) ; let locks ; if ( query . isMultiLockQuery ) { const result = await this . _handler . getInstances < MultiLock > ( accessToken , MultiLock , this . getRelativeUrl ( iModelId ) , query . getQueryOptions ( ) ) ; locks = LockHandler . convertMultiLocksToLocks ( result ) ; } else { locks = await this . _handler . postQuery < Lock > ( accessToken , Lock , this . getRelativeUrl ( iModelId , false ) , query . getQueryOptions ( ) ) ; locks = locks . map ( ( value ) => { const result = new Lock ( ) ; result . briefcaseId = value . briefcaseId ; result . lockLevel = value . lockLevel ; result . lockType = value . lockType ; result . objectId = value . objectId ; if ( value . seedFileId ) result . seedFileId = value . seedFileId ; return result ; } ) ; } Logger . logTrace ( loggerCategory , `Queried ${ locks . length } locks for iModel` , ( ) => ( { iModelId } ) ) ; return locks ; } param iModelId param briefcaseId throws throws throws public async deleteAll ( accessToken , iModelId , briefcaseId ) { Logger . logInfo ( loggerCategory , "Deleting all locks from briefcase" , ( ) => ( { iModelId , briefcaseId } ) ) ; ArgumentCheck . validGuid ( "iModelId" , iModelId ) ; ArgumentCheck . validBriefcaseId ( "briefcaseId" , briefcaseId ) ; await this . deleteAllLocksInChunks ( accessToken , iModelId , briefcaseId ) ; Logger . logTrace ( loggerCategory , "Deleted all locks from briefcase" , ( ) => ( { iModelId , briefcaseId } ) ) ; } param iModelId param briefcaseId private async deleteAllLocksInChunks ( accessToken , iModelId , briefcaseId ) { const relativeUrl = this . getRelativeUrl ( iModelId , false , `DeleteChunk-${ briefcaseId }` ) ; const requestOptions = { timeout : { response : 45000 } } ; let shouldDeleteAnotherChunk = true ; do { try { await this . _handler . delete ( accessToken , relativeUrl , requestOptions ) ; Logger . logTrace ( loggerCategory , "Deleted a chunk of locks from briefcase" , ( ) => ( { iModelId , briefcaseId } ) ) ; } catch ( error ) { if ( error instanceof IModelHubError && error . errorNumber === IModelHubStatus . LockChunkDoesNotExist ) shouldDeleteAnotherChunk = false ; else throw error ; } } while ( shouldDeleteAnotherChunk ) ; Logger . logTrace ( loggerCategory , "Deleted all locks from briefcase" , ( ) => ( { iModelId , briefcaseId } ) ) ; } }