import { strict as assert } from "assert" ; import { ContainerRuntimeFactoryWithDefaultDataStore } from "@fluidframework/aqueduct" ; import { IContainer } from "@fluidframework/container-definitions" ; import { Container , Loader } from "@fluidframework/container-loader" ; import { IContainerRuntime } from "@fluidframework/container-runtime-definitions" ; import { IFluidCodeDetails , IRequest } from "@fluidframework/core-interfaces" ; import { LocalDocumentServiceFactory , LocalResolver , } from "@fluidframework/local-driver" ; import { SharedMap } from "@fluidframework/map" ; import { requestFluidObject } from "@fluidframework/runtime-utils" ; import { ILocalDeltaConnectionServer , LocalDeltaConnectionServer } from "@fluidframework/server-local-server" ; import { createAndAttachContainer , ITestFluidObject , LoaderContainerTracker , LocalCodeLoader , TestFluidObjectFactory , } from "@fluidframework/test-utils" ; import { IContainerRuntimeBase } from "@fluidframework/runtime-definitions" ; describe ( "Document Dirty" , ( ) => { const documentId = "documentDirtyTest" ; const mapId = "mapKey" ; const codeDetails = { package : "documentDirtyTestPackage" , config : { } } ; let deltaConnectionServer ; let documentServiceFactory ; let loaderContainerTracker ; let container ; let dataObject ; let containerRuntime ; let sharedMap ; let wasMarkedDirtyRuntimeCount ; let wasMarkedCleanRuntimeCount ; let wasMarkedDirtyContainerCount ; let wasMarkedCleanContainerCount ; /**
     * Waits for the "connected" event from the given container.
     */ async function waitForContainerReconnection ( c ) { assert . equal ( c . connected , false ) ; return new Promise ( ( resolve ) => c . once ( "connected" , ( ) => resolve ( ) ) ) ; } async function ensureContainerConnected ( c ) { if ( ! c . connected ) { return waitForContainerReconnection ( c ) ; } } /**
     * Increments clean count when the "saved" event is fired
     */ function registerSavedContainerHandler ( ) { containerRuntime . on ( "saved" , ( ) => { wasMarkedCleanRuntimeCount += 1 ; assert . equal ( containerRuntime . isDirty , false , "Document is marked clean" ) ; assert . equal ( wasMarkedDirtyRuntimeCount , wasMarkedCleanRuntimeCount , "No superfluous transition event, dirty and clean count should match when state is clean" ) ; } ) ; container . on ( "saved" , ( ) => { wasMarkedCleanContainerCount += 1 ; assert . equal ( container . isDirty , false , "Document is marked clean" ) ; assert . equal ( wasMarkedDirtyContainerCount , wasMarkedCleanContainerCount , "No superfluous transition event, dirty and clean count should match when state is clean" ) ; } ) ; } /**
     * Increments dirty count when the "dirty" event is fired
     */ function registerDirtyContainerHandler ( ) { containerRuntime . on ( "dirty" , ( ) => { wasMarkedDirtyRuntimeCount += 1 ; assert . equal ( containerRuntime . isDirty , true , "Document is marked dirty" ) ; assert . equal ( wasMarkedDirtyRuntimeCount - wasMarkedCleanRuntimeCount , 1 , "No superfluous transition event, dirty should be only one more then clean when state is dirty" ) ; } ) ; container . on ( "dirty" , ( ) => { wasMarkedDirtyContainerCount += 1 ; assert . equal ( container . isDirty , true , "Document is marked dirty" ) ; assert . equal ( wasMarkedDirtyContainerCount - wasMarkedCleanContainerCount , 1 , "No superfluous transition event, dirty should be only one more then clean when state is dirty" ) ; } ) ; } async function createContainer ( ) { const factory = new TestFluidObjectFactory ( [ [ mapId , SharedMap . getFactory ( ) ] , ] , "default" ) ; const innerRequestHandler = async ( request , runtime ) => runtime . IFluidHandleContext . resolveHandle ( request ) ; const runtimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore ( factory , [ [ factory . type , Promise . resolve ( factory ) ] , ] , undefined , [ innerRequestHandler ] ) ; const urlResolver = new LocalResolver ( ) ; const codeLoader = new LocalCodeLoader ( [ [ codeDetails , runtimeFactory ] ] ) ; const loader = new Loader ( { urlResolver , documentServiceFactory , codeLoader } ) ; loaderContainerTracker . add ( loader ) ; return createAndAttachContainer ( codeDetails , loader , urlResolver . createCreateNewRequest ( documentId ) ) ; } beforeEach ( async ( ) => { deltaConnectionServer = LocalDeltaConnectionServer . create ( ) ; documentServiceFactory = new LocalDocumentServiceFactory ( deltaConnectionServer ) ; loaderContainerTracker = new LoaderContainerTracker ( ) ; container = await createContainer ( ) as Container ; dataObject = await requestFluidObject < ITestFluidObject > ( container , "default" ) ; containerRuntime = dataObject . context . containerRuntime as IContainerRuntime ; sharedMap = await dataObject . getSharedObject < SharedMap > ( mapId ) ; sharedMap . set ( "setup" , "done" ) ; await loaderContainerTracker . ensureSynchronized ( ) ; wasMarkedDirtyRuntimeCount = 0 ; wasMarkedCleanRuntimeCount = 0 ; wasMarkedDirtyContainerCount = 0 ; wasMarkedCleanContainerCount = - 1 ; registerSavedContainerHandler ( ) ; registerDirtyContainerHandler ( ) ; } ) ; afterEach ( ( ) => { loaderContainerTracker . reset ( ) ; } ) ; function checkDirtyState ( when , expectedDirty , expectedCleanCount ) { assert . equal ( containerRuntime . isDirty , expectedDirty , `Runtime dirty state not expected ${ when }` ) ; assert . equal ( wasMarkedCleanRuntimeCount , expectedCleanCount , `Runtime clean transition count not expected ${ when }` ) ; assert . equal ( container . isDirty , expectedDirty , `Container dirty state not expected ${ when }` ) ; assert . equal ( wasMarkedCleanContainerCount , expectedCleanCount , `Container clean transition count not expected ${ when }` ) ; } describe ( "Connected state" , ( ) => { it ( "marks state as dirty when ops are sent and clean when acks are received" , async ( ) => { sharedMap . set ( "key" , "value" ) ; checkDirtyState ( "after value set" , true , 0 ) ; await loaderContainerTracker . ensureSynchronized ( ) ; checkDirtyState ( "after processing value set" , false , 1 ) ; } ) ; it ( "marks state as dirty when batch ops are sent and clean when acks are received" , async ( ) => { dataObject . context . containerRuntime . orderSequentially ( ( ) => { sharedMap . set ( "key1" , "value1" ) ; sharedMap . set ( "key2" , "value2" ) ; } ) ; checkDirtyState ( "after batch value set" , true , 0 ) ; await loaderContainerTracker . ensureSynchronized ( ) ; checkDirtyState ( "after processing batch value set" , false , 1 ) ; } ) ; it ( `doesn't affect document state while reconnecting` , async ( ) => { assert ( container . clientId ) ; documentServiceFactory . disconnectClient ( container . clientId , "Disconnected for testing" ) ; checkDirtyState ( "after disconnect" , false , 0 ) ; await waitForContainerReconnection ( container ) ; checkDirtyState ( "after reconnect" , false , 0 ) ; } ) ; } ) ; describe ( "Disconnected state" , ( ) => { it ( `sets operations when disconnected and then reconnects to process them` , async ( ) => { assert ( container . clientId ) ; documentServiceFactory . disconnectClient ( container . clientId , "Disconnected for testing" ) ; sharedMap . set ( "key" , "value" ) ; checkDirtyState ( "after value set while disconnected" , true , 0 ) ; await waitForContainerReconnection ( container ) ; checkDirtyState ( "after reconnect and replayed ops" , true , 0 ) ; await loaderContainerTracker . ensureSynchronized ( ) ; checkDirtyState ( "after processing replayed ops" , false , 1 ) ; } ) ; it ( `sets ops while connected, but disconnects before sending ops, then reconnects to process them` , async ( ) => { sharedMap . set ( "key" , "value" ) ; checkDirtyState ( "after value set" , true , 0 ) ; ( dataObject . context . containerRuntime as IContainerRuntime ) . flush ( ) ; assert ( container . clientId ) ; documentServiceFactory . disconnectClient ( container . clientId , "Disconnected for testing" ) ; checkDirtyState ( "after disconnect with value set" , true , 0 ) ; await waitForContainerReconnection ( container ) ; checkDirtyState ( "after reconnect and replayed ops" , true , 0 ) ; await loaderContainerTracker . ensureSynchronized ( ) ; checkDirtyState ( "after processing replayed ops" , false , 1 ) ; } ) ; } ) ; describe ( "Disconnected state with batch operations" , ( ) => { it ( `sets operations when disconnected and then reconnects to process them` , async ( ) => { assert ( container . clientId ) ; documentServiceFactory . disconnectClient ( container . clientId , "Disconnected for testing" ) ; dataObject . context . containerRuntime . orderSequentially ( ( ) => { sharedMap . set ( "key1" , "value1" ) ; sharedMap . set ( "key2" , "value2" ) ; } ) ; checkDirtyState ( "after batch value set" , true , 0 ) ; await waitForContainerReconnection ( container ) ; checkDirtyState ( "after reconnect and replayed ops" , true , 0 ) ; await loaderContainerTracker . ensureSynchronized ( ) ; checkDirtyState ( "after processing replayed ops" , false , 1 ) ; } ) ; it ( `sets ops while connected, but disconnects before sending ops, then reconnects to process them` , async ( ) => { assert ( container . clientId ) ; dataObject . context . containerRuntime . orderSequentially ( ( ) => { sharedMap . set ( "key1" , "value1" ) ; sharedMap . set ( "key2" , "value2" ) ; } ) ; checkDirtyState ( "after batch value set" , true , 0 ) ; ( dataObject . context . containerRuntime as IContainerRuntime ) . flush ( ) ; documentServiceFactory . disconnectClient ( container . clientId , "Disconnected for testing" ) ; checkDirtyState ( "after disconnect with value set" , true , 0 ) ; await waitForContainerReconnection ( container ) ; checkDirtyState ( "after reconnect and replayed ops" , true , 0 ) ; await loaderContainerTracker . ensureSynchronized ( ) ; checkDirtyState ( "after processing replayed ops" , false , 1 ) ; } ) ; } ) ; describe ( "Force readonly" , ( ) => { it ( `sets operations when force readonly and then turn off force readonly to process them` , async ( ) => { container . forceReadonly ( true ) ; await ensureContainerConnected ( container ) ; sharedMap . set ( "key" , "value" ) ; await loaderContainerTracker . ensureSynchronized ( ) ; checkDirtyState ( "after value set while force readonly" , true , 0 ) ; container . forceReadonly ( false ) ; assert ( container . connected , "Setting readonly to false should not cause disconnection" ) ; checkDirtyState ( "after clear readonly and replayed ops" , true , 0 ) ; await loaderContainerTracker . ensureSynchronized ( ) ; checkDirtyState ( "after processing replayed ops" , false , 1 ) ; } ) ; it ( `sets ops then force readonly before sending ops, then turn off force readonly to process them` , async ( ) => { sharedMap . set ( "key" , "value" ) ; checkDirtyState ( "after value set" , true , 0 ) ; ( dataObject . context . containerRuntime as IContainerRuntime ) . flush ( ) ; container . forceReadonly ( true ) ; await ensureContainerConnected ( container ) ; await loaderContainerTracker . ensureSynchronized ( ) ; checkDirtyState ( "after value set while force readonly" , true , 0 ) ; container . forceReadonly ( false ) ; assert ( container . connected , "Setting readonly to false should not cause disconnection" ) ; checkDirtyState ( "after reconnect and replayed ops" , true , 0 ) ; await loaderContainerTracker . ensureSynchronized ( ) ; checkDirtyState ( "after processing replayed ops" , false , 1 ) ; } ) ; } ) ; afterEach ( async ( ) => { await deltaConnectionServer . webSocketServer . close ( ) ; } ) ; } ) ;