import { assert } from "@fluidframework/common-utils" ; import { ChildLogger } from "@fluidframework/telemetry-utils" ; import { IFluidDataStoreRuntime , IChannelStorageService } from "@fluidframework/datastore-definitions" ; import { ITelemetryBaseLogger } from "@fluidframework/common-definitions" ; import { BaseSegment , ISegment , LocalReferenceCollection , Client , IMergeTreeDeltaOpArgs , IMergeTreeDeltaCallbackArgs , MergeTreeDeltaType , IMergeTreeMaintenanceCallbackArgs , MergeTreeMaintenanceType , LocalReference , ReferenceType , } from "@fluidframework/merge-tree" ; import { IFluidHandle } from "@fluidframework/core-interfaces" ; import { IFluidSerializer } from "@fluidframework/shared-object-base" ; import { ISummaryTreeWithStats } from "@fluidframework/runtime-definitions" ; import { ObjectStoragePartition , SummaryTreeBuilder } from "@fluidframework/runtime-utils" ; import { HandleTable , Handle , isHandleValid } from "./handletable" ; import { deserializeBlob } from "./serialization" ; import { HandleCache } from "./handlecache" ; import { VectorUndoProvider } from "./undoprovider" ; const enum SnapshotPath { segments = "segments" , handleTable = "handleTable" } type PermutationSegmentSpec = [ number , number ] ; export class PermutationSegment extends BaseSegment { public static readonly typeString : string = "PermutationSegment" ; private _start = Handle . unallocated ; public static fromJSONObject ( spec ) { const [ length , start ] = spec as PermutationSegmentSpec ; return new PermutationSegment ( length , start ) ; } public readonly type = PermutationSegment . typeString ; constructor ( length , start = Handle . unallocated ) { super ( ) ; this . _start = start ; this . cachedLength = length ; } public get start ( ) { return this . _start ; } public set start ( value ) { assert ( this . _start === Handle . unallocated , 0x024 ) ; assert ( isHandleValid ( value ) , 0x025 ) ; this . _start = value ; } /**
     * Invoked by '_undoRow/ColRemove' to prepare the newly inserted destination
     * segment to serve as the replacement for this removed segment.  This moves handle
     * allocations from this segment to the replacement as well as maintains tracking
     * groups for the undo/redo stack.
     */ public transferToReplacement ( destination ) { for ( const group of this . trackingCollection . trackingGroups ) { group . link ( destination ) ; } for ( const group of this . trackingCollection . trackingGroups ) { group . unlink ( this ) ; } destination . _start = this . _start ; this . reset ( ) ; } public reset ( ) { this . _start = Handle . unallocated ; } public toJSONObject ( ) { return [ this . cachedLength , this . start ] ; } public clone ( start = 0 , end = this . cachedLength ) { const b = new PermutationSegment ( end - start , this . start + start ) ; this . cloneInto ( b ) ; return b ; } public canAppend ( segment ) { const asPerm = segment as PermutationSegment ; return this . start === Handle . unallocated ? asPerm . start === Handle . unallocated : asPerm . start === this . start + this . cachedLength ; } public append ( segment ) { LocalReferenceCollection . append ( this , segment ) ; this . cachedLength += segment . cachedLength ; } protected createSplitSegmentAt ( pos ) { assert ( 0 < pos && pos < this . cachedLength , 0x026 ) ; const leafSegment = new PermutationSegment ( this . cachedLength - pos , this . start === Handle . unallocated ? Handle . unallocated : this . start + pos ) ; this . cachedLength = pos ; return leafSegment ; } public toString ( ) { return this . start === Handle . unallocated ? `<${ this . cachedLength } empty>` : `<${ this . cachedLength }: ${ this . start }..${ this . start + this . cachedLength - 1 }>` ; } } export class PermutationVector extends Client { private handleTable = new HandleTable < never > ( ) ; public readonly handleCache = new HandleCache ( this ) ; public undo : VectorUndoProvider | undefined ; constructor ( path , logger , runtime , private readonly deltaCallback , private readonly handlesRecycledCallback ) { super ( PermutationSegment . fromJSONObject , ChildLogger . create ( logger , `Matrix.${ path }.MergeTreeClient` ) , { ... runtime . options , newMergeTreeSnapshotFormat : true } ) ; this . mergeTreeDeltaCallback = this . onDelta ; this . mergeTreeMaintenanceCallback = this . onMaintenance ; } public insert ( start , length ) { return this . insertSegmentLocal ( start , new PermutationSegment ( length ) ) ; } public insertRelative ( segment , length ) { const inserted = new PermutationSegment ( length ) ; return { op : this . insertAtReferencePositionLocal ( new LocalReference ( this , segment , 0 , ReferenceType . Transient ) , inserted ) , inserted } ; } public remove ( start , length ) { return this . removeRangeLocal ( start , start + length ) ; } public getMaybeHandle ( pos ) { assert ( 0 <= pos && pos < this . getLength ( ) , 0x027 ) ; return this . handleCache . getHandle ( pos ) ; } public getAllocatedHandle ( pos ) { let handle = this . getMaybeHandle ( pos ) ; if ( isHandleValid ( handle ) ) { return handle ; } this . walkSegments ( ( segment ) => { const asPerm = segment as PermutationSegment ; asPerm . start = handle = this . handleTable . allocate ( ) ; return true ; } , pos , pos + 1 , undefined , true ) ; this . handleCache . addHandle ( pos , handle ) ; return handle ; } public adjustPosition ( pos , fromSeq , clientId ) { const { segment , offset } = this . mergeTree . getContainingSegment ( pos , fromSeq , clientId ) ; if ( segment === undefined || segment . removedSeq !== undefined ) { return undefined ; } return this . getPosition ( segment ) + offset ! ; } public handleToPosition ( handle , localSeq = this . mergeTree . collabWindow . localSeq ) { assert ( localSeq <= this . mergeTree . collabWindow . localSeq , 0x028 ) ; let containingSegment ! ; let containingOffset ; this . mergeTree . walkAllSegments ( this . mergeTree . root , ( segment ) => { const { start , cachedLength } = segment as PermutationSegment ; if ( ! isHandleValid ( start ) ) { return true ; } const end = start + cachedLength ; if ( start <= handle && handle < end ) { containingSegment = segment as PermutationSegment ; containingOffset = handle - start ; return false ; } return true ; } ) ; assert ( isHandleValid ( containingSegment . start ) , 0x029 ) ; return this . findReconnectionPostition ( containingSegment , localSeq ) + containingOffset ! ; } public summarize ( runtime , handle , serializer ) { const builder = new SummaryTreeBuilder ( ) ; builder . addWithStats ( SnapshotPath . segments , super . summarize ( runtime , handle , serializer , [ ] ) ) ; builder . addBlob ( SnapshotPath . handleTable , serializer . stringify ( this . handleTable . getSummaryContent ( ) , handle ) ) ; return builder . getSummaryTree ( ) ; } public async load ( runtime , storage , serializer ) { const handleTableData = await deserializeBlob ( storage , SnapshotPath . handleTable , serializer ) ; this . handleTable = HandleTable . load < never > ( handleTableData ) ; return super . load ( runtime , new ObjectStoragePartition ( storage , SnapshotPath . segments ) , serializer ) ; } private readonly onDelta = ( opArgs , { operation , deltaSegments } ) => { const ranges = deltaSegments . map ( ( { segment } ) => ( { segment : segment as PermutationSegment , position : this . getPosition ( segment ) } ) ) . sort ( ( left , right ) => left . position - right . position ) ; const isLocal = opArgs . sequencedMessage === undefined ; if ( this . undo !== undefined && isLocal ) { this . undo . record ( operation , ranges ) ; } switch ( operation ) { case MergeTreeDeltaType . INSERT : for ( const { segment , position } of ranges ) { segment . reset ( ) ; this . handleCache . itemsChanged ( position , 0 , segment . cachedLength ) ; } for ( const { segment , position } of ranges ) { this . deltaCallback ( position , 0 , segment . cachedLength ) ; } break ; case MergeTreeDeltaType . REMOVE : { for ( const { segment , position } of ranges ) { this . handleCache . itemsChanged ( position , segment . cachedLength , 0 ) ; } for ( const { segment , position } of ranges ) { this . deltaCallback ( position , segment . cachedLength , 0 ) ; } break ; } default : throw new Error ( "Unhandled MergeTreeDeltaType" ) ; } } ; private readonly onMaintenance = ( args ) => { if ( args . operation === MergeTreeMaintenanceType . UNLINK ) { let freed = [ ] ; for ( const { segment } of args . deltaSegments ) { const asPerm = segment as PermutationSegment ; if ( isHandleValid ( asPerm . start ) ) { freed = freed . concat ( new Array ( asPerm . cachedLength ) . fill ( 0 ) . map ( ( value , index ) => index + asPerm . start ) ) ; } } this . handlesRecycledCallback ( freed ) ; for ( const handle of freed ) { this . handleTable . free ( handle ) ; } } } ; public toString ( ) { const s = [ ] ; this . walkSegments ( ( segment ) => { s . push ( `${ segment }` ) ; return true ; } ) ; return s . join ( "" ) ; } }