import { strict as assert } from "assert" ; import { DebugLogger } from "@fluidframework/telemetry-utils" ; import { ISequencedDocumentMessage , ISummaryTree , ITree , MessageType } from "@fluidframework/protocol-definitions" ; import { IFluidDataStoreRuntime } from "@fluidframework/datastore-definitions" ; import { MockStorage } from "@fluidframework/test-runtime-utils" ; import random from "random-js" ; import { Client } from "../client" ; import { List , ListMakeHead , } from "../collections" ; import { UnassignedSequenceNumber } from "../constants" ; import { ISegment , Marker , MergeTree } from "../mergeTree" ; import { createInsertSegmentOp , createRemoveRangeOp } from "../opBuilder" ; import { IJSONSegment , IMarkerDef , IMergeTreeOp , MergeTreeDeltaType , ReferenceType } from "../ops" ; import { PropertySet } from "../properties" ; import { SnapshotLegacy } from "../snapshotlegacy" ; import { MergeTreeTextHelper , TextSegment } from "../textSegment" ; import { TestSerializer } from "./testSerializer" ; import { nodeOrdinalsHaveIntegrity } from "./testUtils" ; export function specToSegment ( spec ) { const maybeText = TextSegment . fromJSONObject ( spec ) ; if ( maybeText ) { return maybeText ; } const maybeMarker = Marker . fromJSONObject ( spec ) ; if ( maybeMarker ) { return maybeMarker ; } throw new Error ( `Unrecognized IJSONSegment type: '${ JSON . stringify ( spec ) }'` ) ; } const mt = random . engines . mt19937 ( ) ; mt . seedWithArray ( [ 0xDEADBEEF , 0xFEEDBED ] ) ; export class TestClient extends Client { public static searchChunkSize = 256 ; public static readonly serializer = new TestSerializer ( ) ; /**
     * Used for in-memory testing.  This will queue a reference string for each client message.
     */ public static useCheckQ = false ; public static async createFromClientSnapshot ( client1 , newLongClientId ) { const snapshot = new SnapshotLegacy ( client1 . mergeTree , DebugLogger . create ( "fluid:snapshot" ) ) ; snapshot . extractSync ( ) ; const summaryTree = snapshot . emit ( [ ] , TestClient . serializer , undefined ! ) . summary ; return TestClient . createFromSummary ( summaryTree , newLongClientId , client1 . specToSegment ) ; } public static async createFromSnapshot ( snapshotTree , newLongClientId , specToSeg ) { return TestClient . createFromStorage ( new MockStorage ( snapshotTree ) , newLongClientId , specToSeg ) ; } public static async createFromSummary ( summaryTree , newLongClientId , specToSeg ) { return TestClient . createFromStorage ( MockStorage . createFromSummary ( summaryTree ) , newLongClientId , specToSeg ) ; } public static async createFromStorage ( storage , newLongClientId , specToSeg ) { const client2 = new TestClient ( undefined , specToSeg ) ; const { catchupOpsP } = await client2 . load ( { logger : client2 . logger , clientId : newLongClientId } as IFluidDataStoreRuntime , storage , TestClient . serializer ) ; await catchupOpsP ; return client2 ; } declare public mergeTree : MergeTree ; public readonly checkQ : List < string > = ListMakeHead < string > ( ) ; protected readonly q : List < ISequencedDocumentMessage > = ListMakeHead < ISequencedDocumentMessage > ( ) ; private readonly textHelper : MergeTreeTextHelper ; constructor ( options ? , specToSeg = specToSegment ) { super ( specToSeg , DebugLogger . create ( "fluid:testClient" ) , options ) ; this . textHelper = new MergeTreeTextHelper ( this . mergeTree ) ; this . mergeTree . mergeTreeDeltaCallback = ( o , d ) => { d . deltaSegments . forEach ( ( s ) => { if ( d . operation === MergeTreeDeltaType . INSERT ) { assert . notEqual ( s . segment . parent , undefined ) ; } } ) ; } ; } public getText ( start ? , end ? ) { return this . textHelper . getText ( this . getCurrentSeq ( ) , this . getClientId ( ) , "" , start , end ) ; } public enqueueTestString ( ) { this . checkQ . enqueue ( this . getText ( ) ) ; } public getMessageCount ( ) { return this . q . count ( ) ; } public enqueueMsg ( msg ) { this . q . enqueue ( msg ) ; } public dequeueMsg ( ) { return this . q . dequeue ( ) ; } public applyMessages ( msgCount ) { let currMsgCount = msgCount ; while ( currMsgCount > 0 ) { const msg = this . q . dequeue ( ) ; if ( msg ) { this . applyMsg ( msg ) ; } else { break ; } currMsgCount -- ; } return true ; } public insertTextLocal ( pos , text , props ? ) { const segment = new TextSegment ( text ) ; if ( props ) { segment . addProperties ( props ) ; } return this . insertSegmentLocal ( pos , segment ) ; } public insertTextRemote ( pos , text , props , seq , refSeq , longClientId ) { const segment = new TextSegment ( text ) ; if ( props ) { segment . addProperties ( props ) ; } this . applyMsg ( this . makeOpMessage ( createInsertSegmentOp ( pos , segment ) , seq , refSeq , longClientId ) ) ; } public removeRangeRemote ( start , end , seq , refSeq , longClientId ) { this . applyMsg ( this . makeOpMessage ( createRemoveRangeOp ( start , end ) , seq , refSeq , longClientId ) ) ; } public insertMarkerLocal ( pos , behaviors , props ? ) { const segment = new Marker ( behaviors ) ; if ( props ) { segment . addProperties ( props ) ; } return this . insertSegmentLocal ( pos , segment ) ; } public insertMarkerRemote ( pos , markerDef , props , seq , refSeq , longClientId ) { const segment = new Marker ( markerDef . refType ) ; if ( props ) { segment . addProperties ( props ) ; } this . applyMsg ( this . makeOpMessage ( createInsertSegmentOp ( pos , segment ) , seq , refSeq , longClientId ) ) ; } public relText ( clientId , refSeq ) { return `cli: ${ this . getLongClientId ( clientId ) } refSeq: ${ refSeq }: ${ this . textHelper . getText ( refSeq , clientId ) }` ; } public makeOpMessage ( op , seq = UnassignedSequenceNumber , refSeq = this . getCurrentSeq ( ) , longClientId ? , minSeqNumber = 0 ) { const msg = { clientId : longClientId === undefined ? this . longClientId : longClientId , clientSequenceNumber : 1 , contents : op , metadata : undefined , minimumSequenceNumber : minSeqNumber , origin : null , referenceSequenceNumber : refSeq , sequenceNumber : seq , timestamp : Date . now ( ) , term : 1 , traces : [ ] , type : MessageType . Operation } ; return msg ; } public validate ( ) { assert ( nodeOrdinalsHaveIntegrity ( this . mergeTree . root ) ) ; } public searchFromPos ( pos , target ) { let start = pos ; let chunk = "" ; while ( start < this . getLength ( ) ) { chunk = this . getText ( start , start + TestClient . searchChunkSize ) ; const result = chunk . match ( target ) ; if ( result !== null ) { return { text : result [ 0 ] , pos : ( result . index + start ) } ; } start += TestClient . searchChunkSize ; } } public findRandomWord ( ) { const len = this . getLength ( ) ; const pos = random . integer ( 0 , len ) ( mt ) ; const nextWord = this . searchFromPos ( pos , /\s\w+\b/ ) ; return nextWord ; } }