import { strict as assert } from "assert" ; import { FluidObject } from "@fluidframework/core-interfaces" ; import { IDocumentStorageService } from "@fluidframework/driver-definitions" ; import { BlobCacheStorageService } from "@fluidframework/driver-utils" ; import { IBlob , ISnapshotTree , ISummaryBlob , SummaryType , } from "@fluidframework/protocol-definitions" ; import { IFluidDataStoreChannel , IFluidDataStoreContext , IFluidDataStoreFactory , IFluidDataStoreRegistry , IGarbageCollectionData , IGarbageCollectionDetailsBase , SummarizeInternalFn , CreateChildSummarizerNodeFn , CreateSummarizerNodeSource , channelsTreeName , } from "@fluidframework/runtime-definitions" ; import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils" ; import { createRootSummarizerNodeWithGC , IRootSummarizerNodeWithGC } from "@fluidframework/runtime-utils" ; import { stringToBuffer , TelemetryNullLogger } from "@fluidframework/common-utils" ; import { LocalFluidDataStoreContext , RemotedFluidDataStoreContext , } from "../dataStoreContext" ; import { ContainerRuntime } from "../containerRuntime" ; import { dataStoreAttributesBlobName , ReadFluidDataStoreAttributes , WriteFluidDataStoreAttributes , } from "../summaryFormat" ; describe ( "Data Store Context Tests" , ( ) => { const dataStoreId = "Test1" ; const emptyGCData = { gcNodes : { } } ; let createSummarizerNodeFn ; describe ( "LocalFluidDataStoreContext" , ( ) => { let localDataStoreContext ; let storage ; let scope ; const attachCb = ( mR ) => { } ; let containerRuntime ; let summarizerNode ; beforeEach ( async ( ) => { summarizerNode = createRootSummarizerNodeWithGC ( new TelemetryNullLogger ( ) , ( ( ) => undefined ) as unknown as SummarizeInternalFn , 0 , 0 ) ; summarizerNode . startSummary ( 0 , new TelemetryNullLogger ( ) ) ; createSummarizerNodeFn = ( summarizeInternal , getGCDataFn , getBaseGCDetailsFn ) => summarizerNode . createChild ( summarizeInternal , dataStoreId , { type : CreateSummarizerNodeSource . Local } , { throwOnFailure : true } , getGCDataFn , getBaseGCDetailsFn ) ; const factory = { type : "store-type" , get IFluidDataStoreFactory ( ) { return factory ; } , instantiateDataStore : async ( context ) => new MockFluidDataStoreRuntime ( ) } ; const registry = { get IFluidDataStoreRegistry ( ) { return registry ; } , get : async ( pkg ) => Promise . resolve ( factory ) } ; containerRuntime = { IFluidDataStoreRegistry : registry , on : ( event , listener ) => { } , logger : new TelemetryNullLogger ( ) } as ContainerRuntime ; } ) ; describe ( "Initialization" , ( ) => { it ( "can initialize correctly and generate attributes" , async ( ) => { localDataStoreContext = new LocalFluidDataStoreContext ( dataStoreId , [ "TestDataStore1" ] , containerRuntime , storage , scope , createSummarizerNodeFn , attachCb , undefined , true ) ; await localDataStoreContext . realize ( ) ; const attachMessage = localDataStoreContext . generateAttachMessage ( ) ; const attributesEntry = attachMessage . snapshot . entries . find ( ( e ) => e . path === dataStoreAttributesBlobName ) ; assert ( attributesEntry !== undefined , "There is no attributes blob in the summary tree" ) ; const contents = JSON . parse ( ( attributesEntry . value as IBlob ) . contents ) as WriteFluidDataStoreAttributes ; const dataStoreAttributes = { pkg : JSON . stringify ( [ "TestDataStore1" ] ) , summaryFormatVersion : 2 , isRootDataStore : true } ; assert . strictEqual ( contents . pkg , dataStoreAttributes . pkg , "Local DataStore package does not match." ) ; assert . strictEqual ( contents . summaryFormatVersion , dataStoreAttributes . summaryFormatVersion , "Local DataStore snapshot version does not match." ) ; assert . strictEqual ( contents . isRootDataStore , dataStoreAttributes . isRootDataStore , "Local DataStore root state does not match" ) ; assert . strictEqual ( attachMessage . type , "TestDataStore1" , "Attach message type does not match." ) ; } ) ; it ( "should generate exception when incorrectly created with array of packages" , async ( ) => { let exception = false ; localDataStoreContext = new LocalFluidDataStoreContext ( dataStoreId , [ "TestComp" , "SubComp" ] , containerRuntime , storage , scope , createSummarizerNodeFn , attachCb , undefined , false ) ; await localDataStoreContext . realize ( ) . catch ( ( error ) => { exception = true ; } ) ; assert . strictEqual ( exception , true , "Exception did not occur." ) ; } ) ; it ( "can initialize and generate attributes when correctly created with array of packages" , async ( ) => { const registryWithSubRegistries = { } ; registryWithSubRegistries . IFluidDataStoreFactory = registryWithSubRegistries ; registryWithSubRegistries . IFluidDataStoreRegistry = registryWithSubRegistries ; registryWithSubRegistries . get = async ( pkg ) => Promise . resolve ( registryWithSubRegistries ) ; registryWithSubRegistries . instantiateDataStore = async ( context ) => new MockFluidDataStoreRuntime ( ) ; containerRuntime = { IFluidDataStoreRegistry : registryWithSubRegistries , on : ( event , listener ) => { } } as ContainerRuntime ; localDataStoreContext = new LocalFluidDataStoreContext ( dataStoreId , [ "TestComp" , "SubComp" ] , containerRuntime , storage , scope , createSummarizerNodeFn , attachCb , undefined , false ) ; await localDataStoreContext . realize ( ) ; const attachMessage = localDataStoreContext . generateAttachMessage ( ) ; const attributesEntry = attachMessage . snapshot . entries . find ( ( e ) => e . path === dataStoreAttributesBlobName ) ; assert ( attributesEntry !== undefined , "There is no attributes blob in the summary tree" ) ; const contents = JSON . parse ( ( attributesEntry . value as IBlob ) . contents ) as WriteFluidDataStoreAttributes ; const dataStoreAttributes = { pkg : JSON . stringify ( [ "TestComp" , "SubComp" ] ) , summaryFormatVersion : 2 , isRootDataStore : false } ; assert . strictEqual ( contents . pkg , dataStoreAttributes . pkg , "Local DataStore package does not match." ) ; assert . strictEqual ( contents . summaryFormatVersion , dataStoreAttributes . summaryFormatVersion , "Local DataStore snapshot version does not match." ) ; assert . strictEqual ( contents . isRootDataStore , dataStoreAttributes . isRootDataStore , "Local DataStore root state does not match" ) ; assert . strictEqual ( attachMessage . type , "SubComp" , "Attach message type does not match." ) ; } ) ; it ( "can correctly initialize root context" , async ( ) => { localDataStoreContext = new LocalFluidDataStoreContext ( dataStoreId , [ "TestDataStore1" ] , containerRuntime , storage , scope , createSummarizerNodeFn , attachCb , undefined , true ) ; const isRootNode = await localDataStoreContext . isRoot ( ) ; assert . strictEqual ( isRootNode , true , "The data store should be root." ) ; } ) ; it ( "can correctly initialize non-root context" , async ( ) => { localDataStoreContext = new LocalFluidDataStoreContext ( dataStoreId , [ "TestDataStore1" ] , containerRuntime , storage , scope , createSummarizerNodeFn , attachCb , undefined , false ) ; const isRootNode = await localDataStoreContext . isRoot ( ) ; assert . strictEqual ( isRootNode , false , "The data store should not be root." ) ; } ) ; } ) ; describe ( "Garbage Collection" , ( ) => { it ( "can generate correct GC data" , async ( ) => { localDataStoreContext = new LocalFluidDataStoreContext ( dataStoreId , [ "TestDataStore1" ] , containerRuntime , storage , scope , createSummarizerNodeFn , attachCb , undefined , true ) ; const gcData = await localDataStoreContext . getGCData ( ) ; assert . deepStrictEqual ( gcData , emptyGCData , "GC data from getGCData should be empty." ) ; } ) ; it ( "can successfully update referenced state" , ( ) => { localDataStoreContext = new LocalFluidDataStoreContext ( dataStoreId , [ "TestComp" , "SubComp" ] , containerRuntime , storage , scope , createSummarizerNodeFn , attachCb , undefined , false ) ; const dataStoreSummarizerNode = summarizerNode . getChild ( dataStoreId ) ; assert . strictEqual ( dataStoreSummarizerNode ?. isReferenced ( ) , true , "Data store should be referenced by default" ) ; localDataStoreContext . updateUsedRoutes ( [ ] ) ; assert . strictEqual ( dataStoreSummarizerNode ?. isReferenced ( ) , false , "Data store should now be unreferenced" ) ; localDataStoreContext . updateUsedRoutes ( [ "" ] ) ; assert . strictEqual ( dataStoreSummarizerNode ?. isReferenced ( ) , true , "Data store should now be referenced" ) ; } ) ; } ) ; } ) ; describe ( "RemoteDataStoreContext" , ( ) => { let remotedDataStoreContext ; let dataStoreAttributes ; const storage = { } ; let scope ; let summarizerNode ; function mockContainerRuntime ( disableIsolatedChannels = true ) { const factory = { } ; factory . IFluidDataStoreFactory = factory ; factory . instantiateDataStore = ( context ) => new MockFluidDataStoreRuntime ( ) ; const registry = { } ; registry . IFluidDataStoreRegistry = registry ; registry . get = async ( pkg ) => Promise . resolve ( factory ) ; return { disableIsolatedChannels , IFluidDataStoreRegistry : registry , on : ( event , listener ) => { } } as ContainerRuntime ; } beforeEach ( async ( ) => { summarizerNode = createRootSummarizerNodeWithGC ( new TelemetryNullLogger ( ) , ( ( ) => undefined ) as unknown as SummarizeInternalFn , 0 , 0 ) ; summarizerNode . startSummary ( 0 , new TelemetryNullLogger ( ) ) ; } ) ; describe ( "Initialization - can correctly initialize and generate attributes" , ( ) => { beforeEach ( ( ) => { createSummarizerNodeFn = ( summarizeInternal , getGCDataFn , getBaseGCDetailsFn ) => summarizerNode . createChild ( summarizeInternal , dataStoreId , { type : CreateSummarizerNodeSource . FromSummary } , { gcDisabled : true } , getGCDataFn , getBaseGCDetailsFn ) ; } ) ; const pkgName = "TestDataStore1" ; param writeIsolatedChannels param expected function testGenerateAttributes ( writeIsolatedChannels , expected ) { param hasIsolatedChannels param attributes async function testGenerateAttributesCore ( hasIsolatedChannels , attributes ) { const buffer = stringToBuffer ( JSON . stringify ( attributes ) , "utf8" ) ; const blobCache = new Map < string , ArrayBufferLike > ( [ [ "fluidDataStoreAttributes" , buffer ] ] ) ; const snapshotTree = { blobs : { [ dataStoreAttributesBlobName ] : "fluidDataStoreAttributes" } , commits : { } , trees : { } } ; if ( hasIsolatedChannels ) { snapshotTree . trees [ channelsTreeName ] = { blobs : { } , commits : { } , trees : { } } ; } remotedDataStoreContext = new RemotedFluidDataStoreContext ( dataStoreId , snapshotTree , async ( ) => undefined , mockContainerRuntime ( ! writeIsolatedChannels ) , new BlobCacheStorageService ( storage as IDocumentStorageService , blobCache ) , scope , createSummarizerNodeFn ) ; const isRootNode = await remotedDataStoreContext . isRoot ( ) ; assert . strictEqual ( isRootNode , true , "The data store should be root." ) ; const summarizeResult = await remotedDataStoreContext . summarize ( true ) ; assert ( summarizeResult . summary . type === SummaryType . Tree , "summarize should always return a tree when fullTree is true" ) ; const blob = summarizeResult . summary . tree [ dataStoreAttributesBlobName ] as ISummaryBlob ; const contents = JSON . parse ( blob . content as string ) as WriteFluidDataStoreAttributes ; assert . deepStrictEqual ( contents , expected , "Unexpected datastore attributes written" ) ; } it ( "can read from latest with isolated channels" , async ( ) => testGenerateAttributesCore ( true , { pkg : JSON . stringify ( [ pkgName ] ) , summaryFormatVersion : 2 , isRootDataStore : true } ) ) ; it ( "can read from latest without isolated channels" , async ( ) => testGenerateAttributesCore ( false , { pkg : JSON . stringify ( [ pkgName ] ) , summaryFormatVersion : 2 , isRootDataStore : true , disableIsolatedChannels : true } ) ) ; it ( "can read from previous snapshot format" , async ( ) => testGenerateAttributesCore ( false , { pkg : JSON . stringify ( [ pkgName ] ) , snapshotFormatVersion : "0.1" , isRootDataStore : true } ) ) ; it ( "can read from oldest snapshot format" , async ( ) => testGenerateAttributesCore ( false , { pkg : pkgName } ) ) ; } describe ( "writing with isolated channels disabled" , ( ) => testGenerateAttributes ( false , { pkg : JSON . stringify ( [ pkgName ] ) , snapshotFormatVersion : "0.1" , isRootDataStore : true } ) ) ; describe ( "writing with isolated channels enabled" , ( ) => testGenerateAttributes ( true , { pkg : JSON . stringify ( [ pkgName ] ) , summaryFormatVersion : 2 , isRootDataStore : true } ) ) ; } ) ; describe ( "Garbage Collection" , ( ) => { beforeEach ( ( ) => { createSummarizerNodeFn = ( summarizeInternal , getGCDataFn , getBaseGCDetailsFn ) => summarizerNode . createChild ( summarizeInternal , dataStoreId , { type : CreateSummarizerNodeSource . FromSummary } , undefined , getGCDataFn , getBaseGCDetailsFn ) ; } ) ; it ( "can generate GC data without GC details in initial summary" , async ( ) => { dataStoreAttributes = { pkg : "TestDataStore1" , summaryFormatVersion : undefined } ; const buffer = stringToBuffer ( JSON . stringify ( dataStoreAttributes ) , "utf8" ) ; const blobCache = new Map < string , ArrayBufferLike > ( [ [ "fluidDataStoreAttributes" , buffer ] ] ) ; const snapshotTree = { blobs : { [ dataStoreAttributesBlobName ] : "fluidDataStoreAttributes" } , commits : { } , trees : { } } ; remotedDataStoreContext = new RemotedFluidDataStoreContext ( dataStoreId , snapshotTree , async ( ) => undefined , mockContainerRuntime ( ) , new BlobCacheStorageService ( storage as IDocumentStorageService , blobCache ) , scope , createSummarizerNodeFn ) ; const gcData = await remotedDataStoreContext . getGCData ( ) ; assert . deepStrictEqual ( gcData , emptyGCData , "GC data from getGCData should be empty." ) ; } ) ; it ( "can generate GC data with emtpy GC details in initial summary" , async ( ) => { dataStoreAttributes = { pkg : "TestDataStore1" , summaryFormatVersion : undefined } ; const attributesBuffer = stringToBuffer ( JSON . stringify ( dataStoreAttributes ) , "utf8" ) ; const blobCache = new Map < string , ArrayBufferLike > ( [ [ "fluidDataStoreAttributes" , attributesBuffer ] , ] ) ; const snapshotTree = { blobs : { [ dataStoreAttributesBlobName ] : "fluidDataStoreAttributes" } , commits : { } , trees : { } } ; const gcDetails = { usedRoutes : [ ] , gcData : emptyGCData } ; remotedDataStoreContext = new RemotedFluidDataStoreContext ( dataStoreId , snapshotTree , async ( ) => gcDetails , mockContainerRuntime ( ) , new BlobCacheStorageService ( storage as IDocumentStorageService , blobCache ) , scope , createSummarizerNodeFn ) ; const gcData = await remotedDataStoreContext . getGCData ( ) ; assert . deepStrictEqual ( gcData , gcDetails . gcData , "GC data from getGCData is incorrect." ) ; } ) ; it ( "can generate GC data with GC details in initial summary" , async ( ) => { dataStoreAttributes = { pkg : "TestDataStore1" , summaryFormatVersion : undefined } ; const attributesBuffer = stringToBuffer ( JSON . stringify ( dataStoreAttributes ) , "utf8" ) ; const blobCache = new Map < string , ArrayBufferLike > ( [ [ "fluidDataStoreAttributes" , attributesBuffer ] , ] ) ; const snapshotTree = { blobs : { [ dataStoreAttributesBlobName ] : "fluidDataStoreAttributes" } , commits : { } , trees : { } } ; const gcDetails = { usedRoutes : [ ] , gcData : { gcNodes : { "/" : [ "dds1" , "dds2" ] , "dds1" : [ "dds2" , "/" ] } } } ; remotedDataStoreContext = new RemotedFluidDataStoreContext ( dataStoreId , snapshotTree , async ( ) => gcDetails , mockContainerRuntime ( ) , new BlobCacheStorageService ( storage as IDocumentStorageService , blobCache ) , scope , createSummarizerNodeFn ) ; const gcData = await remotedDataStoreContext . getGCData ( ) ; assert . deepStrictEqual ( gcData , gcDetails . gcData , "GC data from getGCData is incorrect." ) ; } ) ; it ( "should not reuse summary data when used state changed since last summary" , async ( ) => { dataStoreAttributes = { pkg : "TestDataStore1" , summaryFormatVersion : undefined } ; const attributesBuffer = stringToBuffer ( JSON . stringify ( dataStoreAttributes ) , "utf8" ) ; const blobCache = new Map < string , ArrayBufferLike > ( [ [ "fluidDataStoreAttributes" , attributesBuffer ] , ] ) ; const snapshotTree = { id : "dummy" , blobs : { [ dataStoreAttributesBlobName ] : "fluidDataStoreAttributes" } , commits : { } , trees : { } } ; const gcDetails = { usedRoutes : [ "" ] } ; remotedDataStoreContext = new RemotedFluidDataStoreContext ( dataStoreId , snapshotTree , async ( ) => gcDetails , mockContainerRuntime ( ) , new BlobCacheStorageService ( storage as IDocumentStorageService , blobCache ) , scope , createSummarizerNodeFn ) ; const gcData = await remotedDataStoreContext . getGCData ( ) ; assert . deepStrictEqual ( gcData , emptyGCData , "GC data from getGCData should be empty." ) ; remotedDataStoreContext . updateUsedRoutes ( [ "" ] ) ; let summarizeResult = await remotedDataStoreContext . summarize ( false ) ; assert ( summarizeResult . summary . type === SummaryType . Handle , "summarize should return a handle since nothing changed" ) ; remotedDataStoreContext . updateUsedRoutes ( [ ] ) ; summarizeResult = await remotedDataStoreContext . summarize ( false ) ; assert ( summarizeResult . summary . type === SummaryType . Tree , "summarize should return a tree since used state changed" ) ; } ) ; function updateReferencedStateTest ( ) { const buffer = stringToBuffer ( JSON . stringify ( dataStoreAttributes ) , "utf8" ) ; const blobCache = new Map < string , ArrayBufferLike > ( [ [ "fluidDataStoreAttributes" , buffer ] ] ) ; const snapshotTree = { id : "dummy" , blobs : { [ ".component" ] : "fluidDataStoreAttributes" } , commits : { } , trees : { } } ; remotedDataStoreContext = new RemotedFluidDataStoreContext ( dataStoreId , snapshotTree , async ( ) => undefined , mockContainerRuntime ( ) , new BlobCacheStorageService ( storage as IDocumentStorageService , blobCache ) , scope , createSummarizerNodeFn ) ; const dataStoreSummarizerNode = summarizerNode . getChild ( dataStoreId ) ; assert . strictEqual ( dataStoreSummarizerNode ?. isReferenced ( ) , true , "Data store should be referenced by default" ) ; remotedDataStoreContext . updateUsedRoutes ( [ ] ) ; assert . strictEqual ( dataStoreSummarizerNode ?. isReferenced ( ) , false , "Data store should now be unreferenced" ) ; remotedDataStoreContext . updateUsedRoutes ( [ "" ] ) ; assert . strictEqual ( dataStoreSummarizerNode ?. isReferenced ( ) , true , "Data store should now be referenced" ) ; } it ( "can successfully update referenced state from format version 0" , ( ) => { dataStoreAttributes = { pkg : "TestDataStore1" } ; updateReferencedStateTest ( ) ; } ) ; it ( "can successfully update referenced state from format version 1" , ( ) => { dataStoreAttributes = { pkg : "[\"TestDataStore1\"]" , snapshotFormatVersion : "0.1" } ; updateReferencedStateTest ( ) ; } ) ; it ( "can successfully update referenced state from format version 2" , ( ) => { dataStoreAttributes = { pkg : "[\"TestDataStore1\"]" , summaryFormatVersion : 2 } ; updateReferencedStateTest ( ) ; } ) ; } ) ; } ) ; } ) ;