import { TypedEventEmitter } from "@fluidframework/common-utils" ; import { IEvent } from "@fluidframework/common-definitions" ; import { addProperties , Client , ConflictAction , createMap , ICombiningOp , IInterval , IntervalConflictResolver , IntervalNode , IntervalTree , IntervalType , LocalReference , MergeTree , MergeTreeDeltaType , ordinalToArray , PropertiesManager , PropertySet , RedBlackTree , ReferenceType , reservedRangeLabelsKey , UnassignedSequenceNumber , } from "@fluidframework/merge-tree" ; import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions" ; import { v4 as uuid } from "uuid" ; import { IValueFactory , IValueOpEmitter , IValueOperation , IValueType } from "./mapKernelInterfaces" ; const reservedIntervalIdKey = "intervalId" ; export interface ISerializedInterval { sequenceNumber : number ; start : number ; end : number ; intervalType : IntervalType ; properties ? : PropertySet ; } export interface ISerializableInterval extends IInterval { properties : PropertySet ; propertyManager : PropertiesManager ; serialize ( client ) : ISerializedInterval ; addProperties ( props , collaborating ? , seq ? ) : PropertySet | undefined ; getIntervalId ( ) : string | undefined ; } export interface IIntervalHelpers < TInterval extends ISerializableInterval > { compareEnds ( a , b ) : number ; create ( label , start , end , client , intervalType ? ) : TInterval ; } export class Interval implements ISerializableInterval { public properties : PropertySet ; public auxProps : PropertySet [ ] ; public propertyManager : PropertiesManager ; constructor ( public start , public end , props ? ) { if ( props ) { this . addProperties ( props ) ; } } public getIntervalId ( ) { const id = this . properties ?. [ reservedIntervalIdKey ] ; if ( id === undefined ) { return undefined ; } return `${ id }` ; } public getAdditionalPropertySets ( ) { return this . auxProps ; } public addPropertySet ( props ) { if ( this . auxProps === undefined ) { this . auxProps = [ ] ; } this . auxProps . push ( props ) ; } public serialize ( client ) { let seq = 0 ; if ( client ) { seq = client . getCurrentSeq ( ) ; } const serializedInterval = { end : this . end , intervalType : 0 , sequenceNumber : seq , start : this . start } ; if ( this . properties ) { serializedInterval . properties = this . properties ; } return serializedInterval ; } public clone ( ) { return new Interval ( this . start , this . end , this . properties ) ; } public compare ( b ) { const startResult = this . compareStart ( b ) ; if ( startResult === 0 ) { const endResult = this . compareEnd ( b ) ; if ( endResult === 0 ) { const thisId = this . getIntervalId ( ) ; if ( thisId ) { const bId = b . getIntervalId ( ) ; if ( bId ) { return thisId > bId ? 1 : thisId < bId ? - 1 : 0 ; } return 0 ; } return 0 ; } else { return endResult ; } } else { return startResult ; } } public compareStart ( b ) { return this . start - b . start ; } public compareEnd ( b ) { return this . end - b . end ; } public overlaps ( b ) { const result = ( this . start < b . end ) && ( this . end >= b . start ) ; return result ; } public union ( b ) { return new Interval ( Math . min ( this . start , b . start ) , Math . max ( this . end , b . end ) , this . properties ) ; } public getProperties ( ) { return this . properties ; } public addProperties ( newProps , collaborating = false , seq ? , op ? ) { if ( newProps ) { if ( ! this . propertyManager ) { this . propertyManager = new PropertiesManager ( ) ; } if ( ! this . properties ) { this . properties = createMap < any > ( ) ; } return this . propertyManager . addProperties ( this . properties , newProps , op , seq , collaborating ) ; } } public modify ( label , start , end ) { const startPos = start ?? this . start ; const endPos = end ?? this . end ; if ( this . start === startPos && this . end === endPos ) { return ; } return new Interval ( startPos , endPos , this . properties ) ; } } export class SequenceInterval implements ISerializableInterval { public properties : PropertySet ; public propertyManager : PropertiesManager ; private readonly checkMergeTree : MergeTree ; constructor ( public start , public end , public intervalType , props ? ) { if ( props ) { this . addProperties ( props ) ; } } public serialize ( client ) { const startPosition = this . start . toPosition ( ) ; const endPosition = this . end . toPosition ( ) ; const serializedInterval = { end : endPosition , intervalType : this . intervalType , sequenceNumber : client . getCurrentSeq ( ) , start : startPosition } ; if ( this . properties ) { serializedInterval . properties = this . properties ; } return serializedInterval ; } public clone ( ) { return new SequenceInterval ( this . start , this . end , this . intervalType , this . properties ) ; } public compare ( b ) { const startResult = this . compareStart ( b ) ; if ( startResult === 0 ) { const endResult = this . compareEnd ( b ) ; if ( endResult === 0 ) { const thisId = this . getIntervalId ( ) ; if ( thisId ) { const bId = b . getIntervalId ( ) ; if ( bId ) { return thisId > bId ? 1 : thisId < bId ? - 1 : 0 ; } return 0 ; } return 0 ; } else { return endResult ; } } else { return startResult ; } } public compareStart ( b ) { return this . start . compare ( b . start ) ; } public compareEnd ( b ) { return this . end . compare ( b . end ) ; } public overlaps ( b ) { const result = ( this . start . compare ( b . end ) < 0 ) && ( this . end . compare ( b . start ) >= 0 ) ; if ( this . checkMergeTree ) { this . checkOverlaps ( b , result ) ; } return result ; } public getIntervalId ( ) { const id = this . properties ?. [ reservedIntervalIdKey ] ; if ( id === undefined ) { return undefined ; } return `${ id }` ; } public union ( b ) { return new SequenceInterval ( this . start . min ( b . start ) , this . end . max ( b . end ) , this . intervalType ) ; } public addProperties ( newProps , collab = false , seq ? , op ? ) { if ( ! this . propertyManager ) { this . propertyManager = new PropertiesManager ( ) ; } if ( ! this . properties ) { this . properties = createMap < any > ( ) ; } return this . propertyManager . addProperties ( this . properties , newProps , op , seq , collab ) ; } public overlapsPos ( bstart , bend ) { const startPos = this . start . toPosition ( ) ; const endPos = this . start . toPosition ( ) ; return ( endPos > bstart ) && ( startPos < bend ) ; } private checkOverlaps ( b , result ) { const astart = this . start . toPosition ( ) ; const bstart = b . start . toPosition ( ) ; const aend = this . end . toPosition ( ) ; const bend = b . end . toPosition ( ) ; const checkResult = ( ( astart < bend ) && ( bstart < aend ) ) ; if ( checkResult !== result ) { console . log ( `check mismatch: res ${ result } ${ this . start . segment === b . end . segment } ${ b . start . segment === this . end . segment }` ) ; console . log ( `as ${ astart } ae ${ aend } bs ${ bstart } be ${ bend }` ) ; console . log ( `as ${ ordinalToArray ( this . start . segment . ordinal ) }@${ this . start . offset }` ) ; console . log ( `ae ${ ordinalToArray ( this . end . segment . ordinal ) }@${ this . end . offset }` ) ; console . log ( `bs ${ ordinalToArray ( b . start . segment . ordinal ) }@${ b . start . offset }` ) ; console . log ( `be ${ ordinalToArray ( b . end . segment . ordinal ) }@${ b . end . offset }` ) ; console . log ( this . checkMergeTree . nodeToString ( b . start . segment . parent , "" ) ) ; } } public modify ( label , start , end ) { const startPos = start ?? this . start . getOffset ( ) ; const endPos = end ?? this . end . getOffset ( ) ; if ( this . start . getOffset ( ) === startPos && this . end . getOffset ( ) === endPos ) { return ; } const newInterval = createSequenceInterval ( label , startPos , endPos , this . start . getClient ( ) , this . intervalType ) ; if ( this . properties ) { newInterval . addProperties ( this . properties ) ; } return newInterval ; } } function createPositionReference ( client , pos , refType ) { const segoff = client . getContainingSegment ( pos ) ; if ( segoff && segoff . segment ) { const lref = new LocalReference ( client , segoff . segment , segoff . offset , refType ) ; if ( refType !== ReferenceType . Transient ) { client . addLocalReference ( lref ) ; } return lref ; } return new LocalReference ( client , undefined ) ; } function createSequenceInterval ( label , start , end , client , intervalType ) { let beginRefType = ReferenceType . RangeBegin ; let endRefType = ReferenceType . RangeEnd ; if ( intervalType === IntervalType . Nest ) { beginRefType = ReferenceType . NestBegin ; endRefType = ReferenceType . NestEnd ; } else if ( intervalType === IntervalType . Transient ) { beginRefType = ReferenceType . Transient ; endRefType = ReferenceType . Transient ; } if ( intervalType & IntervalType . SlideOnRemove ) { beginRefType |= ReferenceType . SlideOnRemove ; endRefType |= ReferenceType . SlideOnRemove ; } const startLref = createPositionReference ( client , start , beginRefType ) ; const endLref = createPositionReference ( client , end , endRefType ) ; if ( startLref && endLref ) { startLref . pairedRef = endLref ; endLref . pairedRef = startLref ; const rangeProp = { [ reservedRangeLabelsKey ] : [ label ] } ; startLref . addProperties ( rangeProp ) ; endLref . addProperties ( rangeProp ) ; const ival = new SequenceInterval ( startLref , endLref , intervalType , rangeProp ) ; return ival ; } } export function defaultIntervalConflictResolver ( a , b ) { a . addPropertySet ( b . properties ) ; return a ; } export function createIntervalIndex ( conflict ? ) { const helpers = { compareEnds : compareIntervalEnds , create : createInterval } ; const lc = new LocalIntervalCollection < Interval > ( undefined , "" , helpers ) ; if ( conflict ) { lc . addConflictResolver ( conflict ) ; } else { lc . addConflictResolver ( defaultIntervalConflictResolver ) ; } return lc ; } export class LocalIntervalCollection < TInterval extends ISerializableInterval > { private readonly intervalTree = new IntervalTree < TInterval > ( ) ; private readonly endIntervalTree : RedBlackTree < TInterval , TInterval > ; private conflictResolver : IntervalConflictResolver < TInterval > ; private endConflictResolver : ConflictAction < TInterval , TInterval > ; private static readonly legacyIdPrefix = "legacy" ; constructor ( private readonly client , private readonly label , private readonly helpers ) { this . endIntervalTree = new RedBlackTree < TInterval , TInterval > ( helpers . compareEnds ) ; } public addConflictResolver ( conflictResolver ) { this . conflictResolver = conflictResolver ; this . endConflictResolver = ( key , currentKey ) => { const ival = this . conflictResolver ( key , currentKey ) ; return { data : ival , key : ival } ; } ; } public map ( fn ) { this . intervalTree . map ( fn ) ; } public createLegacyId ( start , end ) { return `${ LocalIntervalCollection . legacyIdPrefix }${ start }-${ end }` ; } public ensureSerializedId ( serializedInterval ) { if ( serializedInterval . properties ?. [ reservedIntervalIdKey ] === undefined ) { const newProps = { [ reservedIntervalIdKey ] : this . createLegacyId ( serializedInterval . start , serializedInterval . end ) } ; serializedInterval . properties = addProperties ( serializedInterval . properties , newProps ) ; } Object . defineProperty ( serializedInterval . properties , reservedIntervalIdKey , { configurable : false , enumerable : true , writable : false } ) ; } public mapUntil ( fn ) { this . intervalTree . mapUntil ( fn ) ; } public gatherIterationResults ( results , iteratesForward , start ? , end ? ) { if ( this . intervalTree . intervals . isEmpty ( ) ) { return ; } if ( start === undefined && end === undefined ) { if ( iteratesForward ) { this . intervalTree . map ( ( interval ) => { results . push ( interval ) ; } ) ; } else { this . intervalTree . mapBackward ( ( interval ) => { results . push ( interval ) ; } ) ; } } else { const transientInterval = this . helpers . create ( "transient" , start , end , this . client , IntervalType . Transient ) ; if ( start === undefined ) { if ( iteratesForward ) { this . intervalTree . map ( ( interval ) => { if ( transientInterval . compareEnd ( interval ) === 0 ) { results . push ( interval ) ; } } ) ; } else { this . intervalTree . mapBackward ( ( interval ) => { if ( transientInterval . compareEnd ( interval ) === 0 ) { results . push ( interval ) ; } } ) ; } } else { const compareFn = end === undefined ? ( node ) => { return transientInterval . compareStart ( node . key ) ; } : ( node ) => { return transientInterval . compare ( node . key ) ; } ; const continueLeftFn = ( cmpResult ) => cmpResult <= 0 ; const continueRightFn = ( cmpResult ) => cmpResult >= 0 ; const actionFn = ( node ) => { results . push ( node . key ) ; } ; if ( iteratesForward ) { this . intervalTree . intervals . walkExactMatchesForward ( compareFn , actionFn , continueLeftFn , continueRightFn ) ; } else { this . intervalTree . intervals . walkExactMatchesBackward ( compareFn , actionFn , continueLeftFn , continueRightFn ) ; } } } } public findOverlappingIntervals ( startPosition , endPosition ) { if ( ! this . intervalTree . intervals . isEmpty ( ) ) { const transientInterval = this . helpers . create ( "transient" , startPosition , endPosition , this . client , IntervalType . Transient ) ; const overlappingIntervalNodes = this . intervalTree . match ( transientInterval ) ; return overlappingIntervalNodes . map ( ( node ) => node . key ) ; } else { return [ ] ; } } public previousInterval ( pos ) { const transientInterval = this . helpers . create ( "transient" , pos , pos , this . client , IntervalType . Transient ) ; const rbNode = this . endIntervalTree . floor ( transientInterval ) ; if ( rbNode ) { return rbNode . data ; } } public nextInterval ( pos ) { const transientInterval = this . helpers . create ( "transient" , pos , pos , this . client , IntervalType . Transient ) ; const rbNode = this . endIntervalTree . ceil ( transientInterval ) ; if ( rbNode ) { return rbNode . data ; } } public removeInterval ( startPosition , endPosition ) { const transientInterval = this . helpers . create ( "transient" , startPosition , endPosition , this . client , IntervalType . Transient ) ; this . intervalTree . remove ( transientInterval ) ; this . endIntervalTree . remove ( transientInterval ) ; return transientInterval ; } public removeExistingInterval ( interval ) { this . intervalTree . removeExisting ( interval ) ; this . endIntervalTree . remove ( interval ) ; } public createInterval ( start , end , intervalType ) { return this . helpers . create ( this . label , start , end , this . client , intervalType ) ; } public addInterval ( start , end , intervalType , props ? ) { const interval = this . createInterval ( start , end , intervalType ) ; if ( interval ) { if ( ! interval . properties ) { interval . properties = createMap < any > ( ) ; } if ( props ) { interval . addProperties ( props ) ; } if ( interval . properties [ reservedIntervalIdKey ] === undefined ) { interval . properties [ reservedIntervalIdKey ] = uuid ( ) ; } Object . defineProperty ( interval . properties , reservedIntervalIdKey , { configurable : false , enumerable : true , writable : false } ) ; this . add ( interval ) ; } return interval ; } public add ( interval ) { this . intervalTree . put ( interval , this . conflictResolver ) ; this . endIntervalTree . put ( interval , interval , this . endConflictResolver ) ; } public getIntervalById ( id ) { let result ; this . mapUntil ( ( interval ) => { if ( interval . getIntervalId ( ) === id ) { result = interval ; return false ; } return true ; } ) ; return result ; } public changeInterval ( interval , start , end ) { const newInterval = interval . modify ( this . label , start , end ) as TInterval | undefined ; if ( newInterval ) { this . removeExistingInterval ( interval ) ; this . add ( newInterval ) ; } return newInterval ; } public serialize ( ) { const client = this . client ; const intervals = this . intervalTree . intervals . keys ( ) ; return intervals . map ( ( interval ) => interval . serialize ( client ) ) ; } } const compareSequenceIntervalEnds = ( a , b ) : number => a . end . compare ( b . end ) ; class SequenceIntervalCollectionFactory implements IValueFactory < IntervalCollection < SequenceInterval > > { public load ( emitter , raw = [ ] ) { const helpers = { compareEnds : compareSequenceIntervalEnds , create : createSequenceInterval } ; return new IntervalCollection < SequenceInterval > ( helpers , true , emitter , raw ) ; } public store ( value ) { return value . serializeInternal ( ) ; } } export class SequenceIntervalCollectionValueType implements IValueType < IntervalCollection < SequenceInterval > > { public static Name = "sharedStringIntervalCollection" ; public get name ( ) : string { return SequenceIntervalCollectionValueType . Name ; } public get factory ( ) : IValueFactory < IntervalCollection < SequenceInterval > > { return SequenceIntervalCollectionValueType . _factory ; } public get ops ( ) : Map < string , IValueOperation < IntervalCollection < SequenceInterval > > > { return SequenceIntervalCollectionValueType . _ops ; } private static readonly _factory : IValueFactory < IntervalCollection < SequenceInterval > > = new SequenceIntervalCollectionFactory ( ) ; private static readonly _ops : Map < string , IValueOperation < IntervalCollection < SequenceInterval > > > = new Map < string , IValueOperation < IntervalCollection < SequenceInterval > > > ( [ [ "add" , { process : ( value , params , local , op ) => { if ( local ) { return ; } value . addInternal ( params , local , op ) ; } } , ] , [ "delete" , { process : ( value , params , local , op ) => { if ( local ) { return ; } value . deleteInterval ( params , local , op ) ; } } , ] , [ "change" , { process : ( value , params , local , op ) => { value . changeInterval ( params , local , op ) ; } } , ] ] ) ; } const compareIntervalEnds = ( a , b ) => a . end - b . end ; function createInterval ( label , start , end , client ) { let rangeProp ; if ( label && ( label . length > 0 ) ) { rangeProp = { [ reservedRangeLabelsKey ] : [ label ] } ; } return new Interval ( start , end , rangeProp ) ; } class IntervalCollectionFactory implements IValueFactory < IntervalCollection < Interval > > { public load ( emitter , raw = [ ] ) { const helpers = { compareEnds : compareIntervalEnds , create : createInterval } ; const collection = new IntervalCollection < Interval > ( helpers , false , emitter , raw ) ; collection . attachGraph ( undefined , "" ) ; return collection ; } public store ( value ) { return value . serializeInternal ( ) ; } } export class IntervalCollectionValueType implements IValueType < IntervalCollection < Interval > > { public static Name = "sharedIntervalCollection" ; public get name ( ) : string { return IntervalCollectionValueType . Name ; } public get factory ( ) : IValueFactory < IntervalCollection < Interval > > { return IntervalCollectionValueType . _factory ; } public get ops ( ) : Map < string , IValueOperation < IntervalCollection < Interval > > > { return IntervalCollectionValueType . _ops ; } private static readonly _factory : IValueFactory < IntervalCollection < Interval > > = new IntervalCollectionFactory ( ) ; private static readonly _ops : Map < string , IValueOperation < IntervalCollection < Interval > > > = new Map < string , IValueOperation < IntervalCollection < Interval > > > ( [ [ "add" , { process : ( value , params , local , op ) => { if ( local ) { return ; } value . addInternal ( params , local , op ) ; } } , ] , [ "delete" , { process : ( value , params , local , op ) => { if ( local ) { return ; } value . deleteInterval ( params , local , op ) ; } } , ] , [ "change" , { process : ( value , params , local , op ) => { value . changeInterval ( params , local , op ) ; } } , ] ] ) ; } export type DeserializeCallback = ( properties : PropertySet ) => void ; export class IntervalCollectionIterator < TInterval extends ISerializableInterval > { private readonly results : TInterval [ ] ; private index : number ; constructor ( collection , iteratesForward = true , start ? , end ? ) { this . results = [ ] ; this . index = 0 ; collection . gatherIterationResults ( this . results , iteratesForward , start , end ) ; } public next ( ) { let _value ; let _done = true ; if ( this . index < this . results . length ) { _value = this . results [ this . index ++ ] ; _done = false ; } return { value : _value , done : _done } ; } } export interface IIntervalCollectionEvent < TInterval extends ISerializableInterval > extends IEvent { ( event , listener ) ; ( event , listener ) ; } export class IntervalCollection < TInterval extends ISerializableInterval > extends TypedEventEmitter < IIntervalCollectionEvent < TInterval > > { private savedSerializedIntervals ? : ISerializedInterval [ ] ; private localCollection : LocalIntervalCollection < TInterval > ; private onDeserialize : DeserializeCallback ; private client : Client ; private pendingChangeStart : Map < string , ISerializedInterval [ ] > ; private pendingChangeEnd : Map < string , ISerializedInterval [ ] > ; public get attached ( ) : boolean { return ! ! this . localCollection ; } constructor ( private readonly helpers , private readonly requiresClient , private readonly emitter , serializedIntervals ) { super ( ) ; this . savedSerializedIntervals = serializedIntervals ; } public attachGraph ( client , label ) { if ( this . attached ) { throw new Error ( "Only supports one Sequence attach" ) ; } if ( ( client === undefined ) && ( this . requiresClient ) ) { throw new Error ( "Client required for this collection" ) ; } this . client = client ; this . localCollection = new LocalIntervalCollection < TInterval > ( client , label , this . helpers ) ; if ( this . savedSerializedIntervals ) { for ( const serializedInterval of this . savedSerializedIntervals ) { this . localCollection . ensureSerializedId ( serializedInterval ) ; this . localCollection . addInterval ( serializedInterval . start , serializedInterval . end , serializedInterval . intervalType , serializedInterval . properties ) ; } } this . savedSerializedIntervals = undefined ; } public getIntervalById ( id ) { if ( ! this . attached ) { throw new Error ( "attach must be called before accessing intervals" ) ; } return this . localCollection . getIntervalById ( id ) ; } public add ( start , end , intervalType , props ? ) { if ( ! this . attached ) { throw new Error ( "attach must be called prior to adding intervals" ) ; } const interval = this . localCollection . addInterval ( start , end , intervalType , props ) ; if ( interval ) { const serializedInterval = { end , intervalType , properties : interval . properties , sequenceNumber : this . client ?. getCurrentSeq ( ) ?? 0 , start } ; this . emitter . emit ( "add" , undefined , serializedInterval ) ; } this . emit ( "addInterval" , interval , true , undefined ) ; return interval ; } private deleteExistingInterval ( interval , local , op ) { this . localCollection . removeExistingInterval ( interval ) ; if ( interval ) { if ( local ) { this . emitter . emit ( "delete" , undefined , interval . serialize ( this . client ) ) ; } else { if ( this . onDeserialize ) { this . onDeserialize ( interval ) ; } } } this . emit ( "deleteInterval" , interval , local , op ) ; } public removeIntervalById ( id ) { const interval = this . localCollection . getIntervalById ( id ) ; if ( interval ) { this . deleteExistingInterval ( interval , true , undefined ) ; } return interval ; } public changeProperties ( id , props ) { if ( ! this . attached ) { throw new Error ( "Attach must be called before accessing intervals" ) ; } if ( typeof ( id ) !== "string" ) { throw new Error ( "Change API requires an ID that is a string" ) ; } if ( ! props ) { throw new Error ( "changeProperties should be called with a property set" ) ; } const interval = this . getIntervalById ( id ) ; if ( interval ) { const deltaProps = interval . addProperties ( props , true , UnassignedSequenceNumber ) ; const serializedInterval = interval . serialize ( this . client ) ; serializedInterval . start = undefined ; serializedInterval . end = undefined ; serializedInterval . properties = props ; serializedInterval . properties [ reservedIntervalIdKey ] = interval . getIntervalId ( ) ; this . emitter . emit ( "change" , undefined , serializedInterval ) ; this . emit ( "propertyChanged" , interval , deltaProps ) ; } this . emit ( "change" , interval , true , undefined ) ; } public change ( id , start ? , end ? ) { if ( ! this . attached ) { throw new Error ( "Attach must be called before accessing intervals" ) ; } if ( typeof ( id ) !== "string" ) { throw new Error ( "Change API requires an ID that is a string" ) ; } const interval = this . getIntervalById ( id ) ; if ( interval ) { this . localCollection . changeInterval ( interval , start , end ) ; const serializedInterval = interval . serialize ( this . client ) ; serializedInterval . start = start ; serializedInterval . end = end ; serializedInterval . properties = { [ reservedIntervalIdKey ] : interval . getIntervalId ( ) } ; this . emitter . emit ( "change" , undefined , serializedInterval ) ; this . addPendingChange ( id , serializedInterval ) ; } this . emit ( "change" , interval , true , undefined ) ; return interval ; } private addPendingChange ( id , serializedInterval ) { if ( serializedInterval . start !== undefined ) { if ( ! this . pendingChangeStart ) { this . pendingChangeStart = new Map < string , ISerializedInterval [ ] > ( ) ; } this . addPendingChangeHelper ( id , this . pendingChangeStart , serializedInterval ) ; } if ( serializedInterval . end !== undefined ) { if ( ! this . pendingChangeEnd ) { this . pendingChangeEnd = new Map < string , ISerializedInterval [ ] > ( ) ; } this . addPendingChangeHelper ( id , this . pendingChangeEnd , serializedInterval ) ; } } private addPendingChangeHelper ( id , pendingChanges , serializedInterval ) { let entries = pendingChanges . get ( id ) ; if ( ! entries ) { entries = [ ] ; pendingChanges . set ( id , entries ) ; } entries . push ( serializedInterval ) ; } private removePendingChange ( serializedInterval ) { const id = serializedInterval . properties [ reservedIntervalIdKey ] ; if ( serializedInterval . start !== undefined ) { this . removePendingChangeHelper ( id , this . pendingChangeStart , serializedInterval ) ; } if ( serializedInterval . end !== undefined ) { this . removePendingChangeHelper ( id , this . pendingChangeEnd , serializedInterval ) ; } } private removePendingChangeHelper ( id , pendingChanges , serializedInterval ) { const entries = pendingChanges ?. get ( id ) ; if ( entries ) { const pendingChange = entries . shift ( ) ; if ( entries . length === 0 ) { pendingChanges . delete ( id ) ; } if ( pendingChange . start !== serializedInterval . start || pendingChange . end !== serializedInterval . end ) { throw new Error ( "Mismatch in pending changes" ) ; } } } private hasPendingChangeStart ( id ) { const entries = this . pendingChangeStart ?. get ( id ) ; return entries && entries . length !== 0 ; } private hasPendingChangeEnd ( id ) { const entries = this . pendingChangeEnd ?. get ( id ) ; return entries && entries . length !== 0 ; } public changeInterval ( serializedInterval , local , op ) { if ( ! this . attached ) { throw new Error ( "Attach must be called before accessing intervals" ) ; } if ( local ) { this . removePendingChange ( serializedInterval ) ; const id = serializedInterval . properties [ reservedIntervalIdKey ] ; const interval = this . getIntervalById ( id ) ; if ( interval ) { interval . propertyManager ?. ackPendingProperties ( { type : MergeTreeDeltaType . ANNOTATE , props : serializedInterval . properties } ) ; } } else { const id = serializedInterval . properties [ reservedIntervalIdKey ] ; let interval = this . getIntervalById ( id ) ; if ( interval ) { let start ; let end ; if ( ! this . hasPendingChangeStart ( id ) ) { start = serializedInterval . start ; } if ( ! this . hasPendingChangeEnd ( id ) ) { end = serializedInterval . end ; } if ( start !== undefined || end !== undefined ) { interval = this . localCollection . changeInterval ( interval , start , end ) ?? interval ; } const deltaProps = interval . addProperties ( serializedInterval . properties , true , op . sequenceNumber ) ; if ( this . onDeserialize ) { this . onDeserialize ( interval ) ; } this . emit ( "propertyChanged" , interval , deltaProps ) ; } this . emit ( "changeInterval" , interval , local , op ) ; } } public addConflictResolver ( conflictResolver ) { if ( ! this . attached ) { throw new Error ( "attachSequence must be called" ) ; } this . localCollection . addConflictResolver ( conflictResolver ) ; } public attachDeserializer ( onDeserialize ) { if ( ! onDeserialize ) { return ; } this . onDeserialize = onDeserialize ; this . localCollection . map ( ( interval ) => { this . onDeserialize ( interval ) ; } ) ; } public addInternal ( serializedInterval , local , op ) { if ( ! this . attached ) { throw new Error ( "attachSequence must be called" ) ; } this . localCollection . ensureSerializedId ( serializedInterval ) ; const interval = this . localCollection . addInterval ( serializedInterval . start , serializedInterval . end , serializedInterval . intervalType , serializedInterval . properties ) ; if ( interval ) { if ( local ) { this . emitter . emit ( "add" , undefined , serializedInterval ) ; } else { if ( this . onDeserialize ) { this . onDeserialize ( interval ) ; } } } this . emit ( "addInterval" , interval , local , op ) ; return interval ; } public deleteInterval ( serializedInterval , local , op ) { if ( ! this . attached ) { throw new Error ( "attach must be called prior to deleting intervals" ) ; } this . localCollection . ensureSerializedId ( serializedInterval ) ; const interval = this . localCollection . getIntervalById ( serializedInterval . properties [ reservedIntervalIdKey ] ) ; if ( interval ) { this . deleteExistingInterval ( interval , local , op ) ; } } public serializeInternal ( ) { if ( ! this . attached ) { throw new Error ( "attachSequence must be called" ) ; } return this . localCollection . serialize ( ) ; } public [ Symbol . iterator ] ( ) { const iterator = new IntervalCollectionIterator < TInterval > ( this ) ; return iterator ; } public CreateForwardIteratorWithStartPosition ( startPosition ) { const iterator = new IntervalCollectionIterator < TInterval > ( this , true , startPosition ) ; return iterator ; } public CreateBackwardIteratorWithStartPosition ( startPosition ) { const iterator = new IntervalCollectionIterator < TInterval > ( this , false , startPosition ) ; return iterator ; } public CreateForwardIteratorWithEndPosition ( endPosition ) { const iterator = new IntervalCollectionIterator < TInterval > ( this , true , undefined , endPosition ) ; return iterator ; } public CreateBackwardIteratorWithEndPosition ( endPosition ) { const iterator = new IntervalCollectionIterator < TInterval > ( this , false , undefined , endPosition ) ; return iterator ; } public gatherIterationResults ( results , iteratesForward , start ? , end ? ) { if ( ! this . attached ) { return ; } this . localCollection . gatherIterationResults ( results , iteratesForward , start , end ) ; } public findOverlappingIntervals ( startPosition , endPosition ) { if ( ! this . attached ) { throw new Error ( "attachSequence must be called" ) ; } return this . localCollection . findOverlappingIntervals ( startPosition , endPosition ) ; } public map ( fn ) { if ( ! this . attached ) { throw new Error ( "attachSequence must be called" ) ; } this . localCollection . map ( fn ) ; } public previousInterval ( pos ) { if ( ! this . attached ) { throw new Error ( "attachSequence must be called" ) ; } return this . localCollection . previousInterval ( pos ) ; } public nextInterval ( pos ) { if ( ! this . attached ) { throw new Error ( "attachSequence must be called" ) ; } return this . localCollection . nextInterval ( pos ) ; } }