import { assert , assertNotUndefined , copyPropertyIfDefined , fail } from '../Common' ; import { NodeId , DetachedSequenceId , TraitLabel } from '../Identifiers' ; import { GenericTransaction , BuildNode , EditStatus } from '../generic' ; import { Snapshot , SnapshotNode } from '../Snapshot' ; import { EditValidationResult } from '../Checkout' ; import { Build , Change , ChangeType , Constraint , ConstraintEffect , Detach , Insert , SetValue } from './PersistedTypes' ; import { detachRange , insertIntoTrait , rangeFromStableRange , validateStablePlace , validateStableRange , isDetachedSequenceId , } from './EditUtilities' ; /**
 * A mutable transaction for applying sequences of changes to a Snapshot.
 * Allows viewing the intermediate states.
 *
 * Contains necessary state to apply changes within an edit to a Snapshot.
 *
 * May have any number of changes applied to make up the edit.
 * Use `close` to complete the transaction, returning the array of changes and an EditingResult showing the
 * results of applying the changes as an Edit to the initial Snapshot (passed to the constructor).
 *
 * No data outside the Transaction is modified by Transaction:
 * the results from `close` must be used to actually submit an `Edit`.
 */ export class Transaction extends GenericTransaction < Change > { protected readonly detached : Map < DetachedSequenceId , readonly NodeId [ ] > = new Map ( ) ; public static factory ( snapshot ) { return new Transaction ( snapshot ) ; } protected validateOnClose ( ) { return this . detached . size !== 0 ? EditStatus . Malformed : EditStatus . Applied ; } protected dispatchChange ( change ) { switch ( change . type ) { case ChangeType . Build : return this . applyBuild ( change ) ; case ChangeType . Insert : return this . applyInsert ( change ) ; case ChangeType . Detach : return this . applyDetach ( change ) ; case ChangeType . Constraint : return this . applyConstraint ( change ) ; case ChangeType . SetValue : return this . applySetValue ( change ) ; default : return fail ( 'Attempted to apply unsupported change' ) ; } } private applyBuild ( change ) { if ( this . detached . has ( change . destination ) ) { return EditStatus . Malformed ; } let idAlreadyPresent = false ; let duplicateIdInBuild = false ; const map = new Map < NodeId , SnapshotNode > ( ) ; let detachedSequenceNotFound = false ; const newIds = this . createSnapshotNodesForTree ( change . source , ( id , snapshotNode ) => { if ( map . has ( id ) ) { duplicateIdInBuild = true ; return true ; } if ( this . view . hasNode ( id ) ) { idAlreadyPresent = true ; return true ; } map . set ( id , snapshotNode ) ; return false ; } , ( ) => { detachedSequenceNotFound = true ; } ) ; if ( detachedSequenceNotFound || duplicateIdInBuild ) { return EditStatus . Malformed ; } if ( idAlreadyPresent ) { return EditStatus . Invalid ; } const view = this . view . addNodes ( map . values ( ) ) ; this . _view = view ; this . detached . set ( change . destination , assertNotUndefined ( newIds ) ) ; return EditStatus . Applied ; } private applyInsert ( change ) { const source = this . detached . get ( change . source ) ; if ( source === undefined ) { return EditStatus . Malformed ; } const destinationChangeResult = validateStablePlace ( this . view , change . destination ) ; if ( destinationChangeResult !== EditValidationResult . Valid ) { return destinationChangeResult === EditValidationResult . Invalid ? EditStatus . Invalid : EditStatus . Malformed ; } this . detached . delete ( change . source ) ; this . _view = insertIntoTrait ( this . view , source , change . destination ) ; return EditStatus . Applied ; } private applyDetach ( change ) { const sourceChangeResult = validateStableRange ( this . view , change . source ) ; if ( sourceChangeResult !== EditValidationResult . Valid ) { return sourceChangeResult === EditValidationResult . Invalid ? EditStatus . Invalid : EditStatus . Malformed ; } const result = detachRange ( this . view , change . source ) ; let modifiedView = result . snapshot ; const { detached } = result ; if ( change . destination !== undefined ) { if ( this . detached . has ( change . destination ) ) { return EditStatus . Malformed ; } this . detached . set ( change . destination , detached ) ; } else { modifiedView = modifiedView . deleteNodes ( detached ) ; } this . _view = modifiedView ; return EditStatus . Applied ; } private applyConstraint ( change ) { assert ( change . identityHash === undefined , 'identityHash constraint is not implemented' ) ; assert ( change . contentHash === undefined , 'contentHash constraint is not implemented' ) ; const sourceChangeResult = validateStableRange ( this . view , change . toConstrain ) ; const onViolation = change . effect === ConstraintEffect . ValidRetry ? EditStatus . Applied : EditStatus . Invalid ; if ( sourceChangeResult !== EditValidationResult . Valid ) { return sourceChangeResult === EditValidationResult . Invalid ? onViolation : EditStatus . Malformed ; } const { start , end } = rangeFromStableRange ( this . view , change . toConstrain ) ; const startIndex = this . view . findIndexWithinTrait ( start ) ; const endIndex = this . view . findIndexWithinTrait ( end ) ; if ( change . length !== undefined && change . length !== endIndex - startIndex ) { return onViolation ; } if ( change . parentNode !== undefined && change . parentNode !== end . trait . parent ) { return onViolation ; } if ( change . label !== undefined && change . label !== end . trait . label ) { return onViolation ; } return EditStatus . Applied ; } private applySetValue ( change ) { if ( ! this . view . hasNode ( change . nodeToModify ) ) { return EditStatus . Invalid ; } this . _view = this . view . setNodeValue ( change . nodeToModify , change . payload ) ; return EditStatus . Applied ; } returns protected createSnapshotNodesForTree ( sequence , onCreateNode , onInvalidDetachedId ) { const topLevelIds = [ ] ; const unprocessed = [ ] ; for ( const buildNode of sequence ) { if ( isDetachedSequenceId ( buildNode ) ) { const detachedIds = this . getDetachedNodeIds ( buildNode , onInvalidDetachedId ) ; if ( detachedIds === undefined ) { return undefined ; } topLevelIds . push ( ... detachedIds ) ; } else { unprocessed . push ( buildNode ) ; topLevelIds . push ( buildNode . identifier ) ; } } while ( unprocessed . length > 0 ) { const node = unprocessed . pop ( ) ; assert ( node !== undefined && ! isDetachedSequenceId ( node ) ) ; const traits = new Map < TraitLabel , readonly NodeId [ ] > ( ) ; for ( const key in node . traits ) { if ( Object . prototype . hasOwnProperty . call ( node . traits , key ) ) { const children = node . traits [ key ] ; if ( children . length > 0 ) { const childIds = [ ] ; for ( const child of children ) { if ( isDetachedSequenceId ( child ) ) { const detachedIds = this . getDetachedNodeIds ( child , onInvalidDetachedId ) ; if ( detachedIds === undefined ) { return undefined ; } childIds . push ( ... detachedIds ) ; } else { childIds . push ( child . identifier ) ; unprocessed . push ( child ) ; } } traits . set ( key as TraitLabel , childIds ) ; } } } const newNode = { identifier : node . identifier , definition : node . definition , traits } ; copyPropertyIfDefined ( node , newNode , 'payload' ) ; if ( onCreateNode ( newNode . identifier , newNode ) ) { return undefined ; } } return topLevelIds ; } private getDetachedNodeIds ( detachedId , onInvalidDetachedId ) { const detachedNodeIds = this . detached . get ( detachedId ) ; if ( detachedNodeIds === undefined ) { onInvalidDetachedId ( ) ; return undefined ; } this . detached . delete ( detachedId ) ; return detachedNodeIds ; } }