import { strict as assert } from "assert" ; import { SinonFakeTimers , useFakeTimers } from "sinon" ; import { concatGarbageCollectionStates } from "@fluidframework/garbage-collector" ; import { ISnapshotTree , SummaryType } from "@fluidframework/protocol-definitions" ; import { gcBlobKey , IGarbageCollectionData , IGarbageCollectionNodeData , IGarbageCollectionState , IGarbageCollectionDetailsBase , } from "@fluidframework/runtime-definitions" ; import { MockLogger } from "@fluidframework/telemetry-utils" ; import { GarbageCollector , gcBlobPrefix , gcTreeKey , IGarbageCollectionRuntime , IGarbageCollector , } from "../garbageCollection" ; describe ( "Garbage Collection Tests" , ( ) => { const nodes = [ "/node1" , "/node2" , "/node3" , "/node4" , ] ; let clock ; let mockLogger ; let closeCalled = false ; const deleteTimeoutMs = 500 ; const defaultGCData = { gcNodes : { } } ; const getGCData = async ( fullGC ? ) => defaultGCData ; const updateUsedRoutes = ( usedRoutes ) => { return { totalNodeCount : 0 , unusedNodeCount : 0 } ; } ; const gcRuntime = { updateStateBeforeGC : async ( ) => { } , getGCData , updateUsedRoutes } ; const emptyGCDetails = { } ; const createGarbageCollector = ( baseSnapshot = undefined , getNodeGCDetails = ( ) => emptyGCDetails , gcTestSessionTimeoutMs ? ) => { mockLogger = new MockLogger ( ) ; return GarbageCollector . create ( gcRuntime , { gcAllowed : true , deleteTimeoutMs , gcTestSessionTimeoutMs } , ( unusedRoutes ) => { } , ( ) => Date . now ( ) , ( ) => { closeCalled = true ; } , baseSnapshot , async < T > ( id ) => getNodeGCDetails ( id ) as T , mockLogger , false ) ; } ; before ( ( ) => { clock = useFakeTimers ( ) ; } ) ; afterEach ( ( ) => { clock . reset ( ) ; } ) ; after ( ( ) => { clock . restore ( ) ; } ) ; describe ( "Session expiry is called" , ( ) => { beforeEach ( ( ) => { closeCalled = false ; } ) ; it ( "Session expiry is called" , async ( ) => { createGarbageCollector ( undefined , undefined , 0 ) ; clock . tick ( 1 ) ; assert ( closeCalled , "Close should have been called." ) ; } ) ; } ) ; describe ( "Inactive events" , ( ) => { const inactiveObjectRevivedEvent = "GarbageCollector:inactiveObjectRevived" ; const inactiveObjectChangedEvent = "GarbageCollector:inactiveObjectChanged" ; function validateNoInactiveEvents ( ) { assert ( ! mockLogger . matchAnyEvent ( [ { eventName : inactiveObjectRevivedEvent } , { eventName : inactiveObjectChangedEvent } , ] ) , "inactive object events should not have been logged" ) ; } function changeAllNodes ( garbageCollector ) { nodes . forEach ( ( nodeId ) => { garbageCollector . nodeChanged ( nodeId ) ; } ) ; } const getDummySnapshotTree = ( ) : ISnapshotTree => { return { blobs : { } , commits : { } , trees : { } } ; } ; beforeEach ( async ( ) => { defaultGCData . gcNodes [ "/" ] = [ nodes [ 0 ] ] ; defaultGCData . gcNodes [ nodes [ 0 ] ] = [ nodes [ 1 ] ] ; defaultGCData . gcNodes [ nodes [ 1 ] ] = [ nodes [ 0 ] , nodes [ 2 ] ] ; defaultGCData . gcNodes [ nodes [ 2 ] ] = [ nodes [ 3 ] ] ; defaultGCData . gcNodes [ nodes [ 3 ] ] = [ nodes [ 0 ] ] ; } ) ; it ( "doesn't generate events for referenced nodes" , async ( ) => { const garbageCollector = createGarbageCollector ( ) ; await garbageCollector . collectGarbage ( { runGC : true } ) ; changeAllNodes ( garbageCollector ) ; validateNoInactiveEvents ( ) ; clock . tick ( deleteTimeoutMs + 1 ) ; changeAllNodes ( garbageCollector ) ; validateNoInactiveEvents ( ) ; } ) ; it ( "generates events when inactive node is changed or revived" , async ( ) => { const garbageCollector = createGarbageCollector ( ) ; defaultGCData . gcNodes [ nodes [ 1 ] ] = [ ] ; await garbageCollector . collectGarbage ( { runGC : true } ) ; changeAllNodes ( garbageCollector ) ; validateNoInactiveEvents ( ) ; clock . tick ( deleteTimeoutMs + 1 ) ; changeAllNodes ( garbageCollector ) ; assert ( mockLogger . matchEvents ( [ { eventName : inactiveObjectChangedEvent , timeout : deleteTimeoutMs , id : nodes [ 2 ] } , { eventName : inactiveObjectChangedEvent , timeout : deleteTimeoutMs , id : nodes [ 3 ] } , ] ) , "inactiveObjectChanged event not generated as expected" ) ; defaultGCData . gcNodes [ nodes [ 1 ] ] = [ nodes [ 3 ] ] ; await garbageCollector . collectGarbage ( { runGC : true } ) ; assert ( mockLogger . matchEvents ( [ { eventName : inactiveObjectRevivedEvent , timeout : deleteTimeoutMs , id : nodes [ 3 ] } , ] ) , "inactiveObjectRevived event not generated as expected" ) ; } ) ; it ( "generates events once per node" , async ( ) => { const garbageCollector = createGarbageCollector ( ) ; defaultGCData . gcNodes [ nodes [ 2 ] ] = [ ] ; await garbageCollector . collectGarbage ( { runGC : true } ) ; clock . tick ( deleteTimeoutMs + 1 ) ; changeAllNodes ( garbageCollector ) ; assert ( mockLogger . matchEvents ( [ { eventName : inactiveObjectChangedEvent , timeout : deleteTimeoutMs , id : nodes [ 3 ] } , ] ) , "inactiveObjectChanged event not generated as expected" ) ; changeAllNodes ( garbageCollector ) ; validateNoInactiveEvents ( ) ; } ) ; /**
         * Here, the base snapshot contains nodes that are inactive. The test validates that we generate inactive events
         * for these nodes.
         */ it ( "generates events for nodes that are inactive on load" , async ( ) => { const gcSnapshotTree = getDummySnapshotTree ( ) ; const gcBlobId = "root" ; gcSnapshotTree . blobs [ `${ gcBlobPrefix }_${ gcBlobId }` ] = gcBlobId ; const baseSnapshot = getDummySnapshotTree ( ) ; baseSnapshot . trees [ gcTreeKey ] = gcSnapshotTree ; const gcState = { gcNodes : { } } ; const node3Data = { outboundRoutes : [ ] , unreferencedTimestampMs : Date . now ( ) - ( deleteTimeoutMs + 100 ) } ; gcState . gcNodes [ nodes [ 3 ] ] = node3Data ; const getNodeGCDetails = ( blobId ) => { if ( blobId === gcBlobId ) { return gcState ; } return { } ; } ; const garbageCollector = createGarbageCollector ( baseSnapshot , getNodeGCDetails ) ; defaultGCData . gcNodes [ nodes [ 2 ] ] = [ ] ; await garbageCollector . collectGarbage ( { runGC : true } ) ; garbageCollector . nodeChanged ( nodes [ 3 ] ) ; assert ( mockLogger . matchEvents ( [ { eventName : inactiveObjectChangedEvent , timeout : deleteTimeoutMs , id : nodes [ 3 ] } , ] ) , "inactiveObjectChanged event not generated as expected" ) ; defaultGCData . gcNodes [ nodes [ 2 ] ] = [ nodes [ 3 ] ] ; await garbageCollector . collectGarbage ( { runGC : true } ) ; assert ( mockLogger . matchEvents ( [ { eventName : inactiveObjectRevivedEvent , timeout : deleteTimeoutMs , id : nodes [ 3 ] } , ] ) , "inactiveObjectRevived event not generated as expected" ) ; } ) ; /**
         * Here, the base snapshot contains nodes that are inactive and the GC blob in snapshot is in old format. The
         * test validates that we generate inactive events for these nodes.
         */ it ( "generates events for nodes that are inactive on load - old snapshot format" , async ( ) => { const node3GCDetails = { gcData : { gcNodes : { "/" : [ ] } } , unrefTimestamp : Date . now ( ) - ( deleteTimeoutMs + 100 ) } ; const node3Snapshot = getDummySnapshotTree ( ) ; node3Snapshot . blobs [ gcBlobKey ] = "node3GCDetails" ; const baseSnapshot = getDummySnapshotTree ( ) ; baseSnapshot . trees [ nodes [ 3 ] . slice ( 1 ) ] = node3Snapshot ; const getNodeGCDetails = ( blobId ) => { if ( blobId === "node3GCDetails" ) { return node3GCDetails ; } return { } ; } ; const garbageCollector = createGarbageCollector ( baseSnapshot , getNodeGCDetails ) ; defaultGCData . gcNodes [ nodes [ 2 ] ] = [ ] ; await garbageCollector . collectGarbage ( { runGC : true } ) ; garbageCollector . nodeChanged ( nodes [ 3 ] ) ; assert ( mockLogger . matchEvents ( [ { eventName : inactiveObjectChangedEvent , timeout : deleteTimeoutMs , id : nodes [ 3 ] } , ] ) , "inactiveObjectChanged event not generated as expected" ) ; defaultGCData . gcNodes [ nodes [ 2 ] ] = [ nodes [ 3 ] ] ; await garbageCollector . collectGarbage ( { runGC : true } ) ; assert ( mockLogger . matchEvents ( [ { eventName : inactiveObjectRevivedEvent , timeout : deleteTimeoutMs , id : nodes [ 3 ] } , ] ) , "inactiveObjectRevived event not generated as expected" ) ; } ) ; /**
         * Here, the base snapshot contains nodes that are inactive and the GC data in snapshot is present in multiple
         * blobs. The test validates that we generate inactive events for these nodes.
         */ it ( `generates events for nodes that are inactive on load - multi blob GC data` , async ( ) => { const gcBlobMap = new Map ( ) ; const expiredTimestampMs = Date . now ( ) - ( deleteTimeoutMs + 100 ) ; const blob1Id = "blob1" ; const blob1GCState = { gcNodes : { } } ; blob1GCState . gcNodes [ nodes [ 1 ] ] = { outboundRoutes : [ ] , unreferencedTimestampMs : expiredTimestampMs } ; gcBlobMap . set ( blob1Id , blob1GCState ) ; const blob2Id = "blob2" ; const blob2GCState = { gcNodes : { } } ; blob2GCState . gcNodes [ nodes [ 2 ] ] = { outboundRoutes : [ ] , unreferencedTimestampMs : expiredTimestampMs } ; gcBlobMap . set ( blob2Id , blob2GCState ) ; const blob3Id = "blob3" ; const blob3GCState = { gcNodes : { } } ; blob3GCState . gcNodes [ nodes [ 3 ] ] = { outboundRoutes : [ ] , unreferencedTimestampMs : expiredTimestampMs } ; gcBlobMap . set ( blob3Id , blob3GCState ) ; const gcSnapshotTree = getDummySnapshotTree ( ) ; gcSnapshotTree . blobs [ `${ gcBlobPrefix }_${ blob1Id }` ] = blob1Id ; gcSnapshotTree . blobs [ `${ gcBlobPrefix }_${ blob2Id }` ] = blob2Id ; gcSnapshotTree . blobs [ `${ gcBlobPrefix }_${ blob3Id }` ] = blob3Id ; const baseSnapshot = getDummySnapshotTree ( ) ; baseSnapshot . trees [ gcTreeKey ] = gcSnapshotTree ; const getNodeGCDetails = ( blobId ) => { return gcBlobMap . get ( blobId ) ?? { } ; } ; const garbageCollector = createGarbageCollector ( baseSnapshot , getNodeGCDetails ) ; defaultGCData . gcNodes [ nodes [ 0 ] ] = [ ] ; defaultGCData . gcNodes [ nodes [ 1 ] ] = [ ] ; defaultGCData . gcNodes [ nodes [ 2 ] ] = [ ] ; await garbageCollector . collectGarbage ( { runGC : true } ) ; garbageCollector . nodeChanged ( nodes [ 1 ] ) ; garbageCollector . nodeChanged ( nodes [ 2 ] ) ; garbageCollector . nodeChanged ( nodes [ 3 ] ) ; assert ( mockLogger . matchEvents ( [ { eventName : inactiveObjectChangedEvent , timeout : deleteTimeoutMs , id : nodes [ 1 ] } , { eventName : inactiveObjectChangedEvent , timeout : deleteTimeoutMs , id : nodes [ 2 ] } , { eventName : inactiveObjectChangedEvent , timeout : deleteTimeoutMs , id : nodes [ 3 ] } , ] ) , "inactiveObjectChanged event not generated as expected" ) ; } ) ; } ) ; /**
     * These tests validate such scenarios where nodes transition from unreferenced -> referenced -> ureferenced state
     * by verifing that their unreferenced timestamps are updated correctly.
     *
     * In these tests, V = nodes and E = edges between nodes. Root nodes that are always referenced are marked as *.
     */ describe ( "References between summaries" , ( ) => { const unknownRouteEvent = "GarbageCollector:gcUnknownOutboundRoute" ; let garbageCollector ; const nodeA = "/A" ; const nodeB = "/B" ; const nodeC = "/C" ; const nodeD = "/D" ; async function getUnreferencedTimestamps ( ) { clock . tick ( 1 ) ; await garbageCollector . collectGarbage ( { runGC : true } ) ; const summaryTree = garbageCollector . summarize ( ) ?. summary ; assert ( summaryTree !== undefined , "Nothing to summarize after running GC" ) ; let rootGCState = { gcNodes : { } } ; for ( const key of Object . keys ( summaryTree . tree ) ) { if ( ! key . startsWith ( gcBlobPrefix ) ) { continue ; } const gcBlob = summaryTree . tree [ key ] ; assert ( gcBlob ?. type === SummaryType . Blob , `GC blob not available` ) ; const gcState = JSON . parse ( gcBlob . content as string ) as IGarbageCollectionState ; rootGCState = concatGarbageCollectionStates ( rootGCState , gcState ) ; } const nodeTimestamps = new Map ( ) ; for ( const [ nodeId , nodeData ] of Object . entries ( rootGCState . gcNodes ) ) { nodeTimestamps . set ( nodeId , nodeData . unreferencedTimestampMs ) ; } return nodeTimestamps ; } beforeEach ( ( ) => { defaultGCData . gcNodes = { } ; garbageCollector = createGarbageCollector ( ) ; } ) ; /**
         * Validates that we can detect references that were added and then removed.
         * 1. Summary 1 at t1. V = [A*, B]. E = []. B has unreferenced time t1.
         * 2. Reference from A to B added. E = [A -> B].
         * 3. Reference from A to B removed. E = [].
         * 4. Summary 2 at t2. V = [A*, B]. E = []. B has unreferenced time t2.
         * Validates that the unreferenced time for B is t2 which is > t1.
         */ it ( `Scenario 1 - Reference added and then removed` , async ( ) => { defaultGCData . gcNodes [ "/" ] = [ nodeA ] ; defaultGCData . gcNodes [ nodeA ] = [ ] ; defaultGCData . gcNodes [ nodeB ] = [ ] ; const timestamps1 = await getUnreferencedTimestamps ( ) ; assert ( timestamps1 . get ( nodeA ) === undefined , "A should be referenced" ) ; const nodeBTime1 = timestamps1 . get ( nodeB ) ; assert ( nodeBTime1 !== undefined , "B should have unreferenced timestamp" ) ; garbageCollector . addedOutboundReference ( nodeA , nodeB ) ; defaultGCData . gcNodes [ nodeA ] = [ nodeB ] ; defaultGCData . gcNodes [ nodeA ] = [ ] ; const timestamps2 = await getUnreferencedTimestamps ( ) ; assert ( timestamps2 . get ( nodeA ) === undefined , "A should be referenced" ) ; const nodeBTime2 = timestamps2 . get ( nodeB ) ; assert ( nodeBTime2 !== undefined && nodeBTime2 > nodeBTime1 , "B's timestamp should have updated" ) ; } ) ; /**
         * Validates that we can detect references that were added transitively and then removed.
         * 1. Summary 1 at t1. V = [A*, B, C]. E = [B -> C]. B and C have unreferenced time t2.
         * 2. Reference from A to B added. E = [A -> B, B -> C].
         * 3. Reference from B to C removed. E = [A -> B].
         * 4. Reference from A to B removed. E = [].
         * 5. Summary 2 at t2. V = [A*, B, C]. E = []. B and C have unreferenced time t2.
         * Validates that the unreferenced time for B and C is t2 which is > t1.
         */ it ( `Scenario 2 - Reference transitively added and removed` , async ( ) => { defaultGCData . gcNodes [ "/" ] = [ nodeA ] ; defaultGCData . gcNodes [ nodeA ] = [ ] ; defaultGCData . gcNodes [ nodeB ] = [ nodeC ] ; defaultGCData . gcNodes [ nodeC ] = [ ] ; const timestamps1 = await getUnreferencedTimestamps ( ) ; assert ( timestamps1 . get ( nodeA ) === undefined , "A should be referenced" ) ; const nodeBTime1 = timestamps1 . get ( nodeB ) ; const nodeCTime1 = timestamps1 . get ( nodeC ) ; assert ( nodeBTime1 !== undefined , "B should have unreferenced timestamp" ) ; assert ( nodeCTime1 !== undefined , "C should have unreferenced timestamp" ) ; garbageCollector . addedOutboundReference ( nodeA , nodeB ) ; defaultGCData . gcNodes [ nodeA ] = [ nodeB ] ; defaultGCData . gcNodes [ nodeB ] = [ ] ; defaultGCData . gcNodes [ nodeA ] = [ ] ; const timestamps2 = await getUnreferencedTimestamps ( ) ; assert ( timestamps2 . get ( nodeA ) === undefined , "A should be referenced" ) ; const nodeBTime2 = timestamps2 . get ( nodeB ) ; const nodeCTime2 = timestamps2 . get ( nodeC ) ; assert ( nodeBTime2 !== undefined && nodeBTime2 > nodeBTime1 , "B's timestamp should have updated" ) ; assert ( nodeCTime2 !== undefined && nodeCTime2 > nodeCTime1 , "C's timestamp should have updated" ) ; } ) ; /**
         * Validates that we can detect chain of references in which the first reference was added and then removed.
         * 1. Summary 1 at t1. V = [A*, B, C, D]. E = [B -> C, C -> D]. B, C and D have unreferenced time t2.
         * 2. Reference from A to B added. E = [A -> B, B -> C, C -> D].
         * 3. Reference from A to B removed. E = [B -> C, C -> D].
         * 4. Summary 2 at t2. V = [A*, B, C, D]. E = [B -> C, C -> D]. B, C and D have unreferenced time t2.
         * Validates that the unreferenced time for B, C and D is t2 which is > t1.
         */ it ( `Scenario 3 - Reference added through chain of references and removed` , async ( ) => { defaultGCData . gcNodes [ "/" ] = [ nodeA ] ; defaultGCData . gcNodes [ nodeA ] = [ ] ; defaultGCData . gcNodes [ nodeB ] = [ nodeC ] ; defaultGCData . gcNodes [ nodeC ] = [ nodeD ] ; defaultGCData . gcNodes [ nodeD ] = [ ] ; const timestamps1 = await getUnreferencedTimestamps ( ) ; assert ( timestamps1 . get ( nodeA ) === undefined , "A should be referenced" ) ; const nodeBTime1 = timestamps1 . get ( nodeB ) ; const nodeCTime1 = timestamps1 . get ( nodeC ) ; const nodeDTime1 = timestamps1 . get ( nodeD ) ; assert ( nodeBTime1 !== undefined , "B should have unreferenced timestamp" ) ; assert ( nodeCTime1 !== undefined , "C should have unreferenced timestamp" ) ; assert ( nodeDTime1 !== undefined , "D should have unreferenced timestamp" ) ; garbageCollector . addedOutboundReference ( nodeA , nodeB ) ; defaultGCData . gcNodes [ nodeA ] = [ nodeB ] ; defaultGCData . gcNodes [ nodeA ] = [ ] ; const timestamps2 = await getUnreferencedTimestamps ( ) ; assert ( timestamps2 . get ( nodeA ) === undefined , "A should be referenced" ) ; const nodeBTime2 = timestamps2 . get ( nodeB ) ; const nodeCTime2 = timestamps2 . get ( nodeC ) ; const nodeDTime2 = timestamps2 . get ( nodeD ) ; assert ( nodeBTime2 !== undefined && nodeBTime2 > nodeBTime1 , "B's timestamp should have updated" ) ; assert ( nodeCTime2 !== undefined && nodeCTime2 > nodeCTime1 , "C's timestamp should have updated" ) ; assert ( nodeDTime2 !== undefined && nodeDTime2 > nodeDTime1 , "D's timestamp should have updated" ) ; } ) ; /**
         * Validates that we can detect references that were added and removed via new nodes.
         * 1. Summary 1 at t1. V = [A*, C]. E = []. C has unreferenced time t1.
         * 2. Node B is created. E = [].
         * 3. Reference from A to B added. E = [A -> B].
         * 4. Reference from B to C added. E = [A -> B, B -> C].
         * 5. Reference from B to C removed. E = [A -> B].
         * 6. Summary 2 at t2. V = [A*, B, C]. E = [A -> B]. C has unreferenced time t2.
         * Validates that the unreferenced time for C is t2 which is > t1.
         */ it ( `Scenario 4 - Reference added via new nodes and removed` , async ( ) => { defaultGCData . gcNodes [ "/" ] = [ nodeA ] ; defaultGCData . gcNodes [ nodeA ] = [ ] ; defaultGCData . gcNodes [ nodeC ] = [ ] ; const timestamps1 = await getUnreferencedTimestamps ( ) ; assert ( timestamps1 . get ( nodeA ) === undefined , "A should be referenced" ) ; const nodeCTime1 = timestamps1 . get ( nodeC ) ; assert ( nodeCTime1 !== undefined , "C should have unreferenced timestamp" ) ; defaultGCData . gcNodes [ nodeB ] = [ ] ; garbageCollector . addedOutboundReference ( nodeA , nodeB ) ; defaultGCData . gcNodes [ nodeA ] = [ nodeB ] ; garbageCollector . addedOutboundReference ( nodeB , nodeC ) ; defaultGCData . gcNodes [ nodeB ] = [ nodeC ] ; defaultGCData . gcNodes [ nodeB ] = [ ] ; const timestamps2 = await getUnreferencedTimestamps ( ) ; assert ( timestamps2 . get ( nodeA ) === undefined , "A should be referenced" ) ; assert ( timestamps2 . get ( nodeB ) === undefined , "B should be referenced" ) ; const nodeCTime2 = timestamps2 . get ( nodeC ) ; assert ( nodeCTime2 !== undefined && nodeCTime2 > nodeCTime1 , "C's timestamp should have updated" ) ; } ) ; /**
         * Validates that references added by unreferences nodes do not show up as references.
         * 1. Summary 1 at t1. V = [A*, B, C]. E = []. B and C have unreferenced time t1.
         * 2. Reference from B to C. E = [B -> C].
         * 3. Summary 2 at t2. V = [A*, B, C]. E = [B -> C]. B and C have unreferenced time t1.
         * Validates that the unreferenced time for B and C is still t1.
         */ it ( `Scenario 5 - Reference added via unreferenced nodes` , async ( ) => { defaultGCData . gcNodes [ "/" ] = [ nodeA ] ; defaultGCData . gcNodes [ nodeA ] = [ ] ; defaultGCData . gcNodes [ nodeB ] = [ ] ; defaultGCData . gcNodes [ nodeC ] = [ ] ; const timestamps1 = await getUnreferencedTimestamps ( ) ; assert ( timestamps1 . get ( nodeA ) === undefined , "A should be referenced" ) ; const nodeBTime1 = timestamps1 . get ( nodeB ) ; const nodeCTime1 = timestamps1 . get ( nodeC ) ; assert ( nodeBTime1 !== undefined , "B should have unreferenced timestamp" ) ; assert ( nodeCTime1 !== undefined , "C should have unreferenced timestamp" ) ; garbageCollector . addedOutboundReference ( nodeB , nodeC ) ; defaultGCData . gcNodes [ nodeB ] = [ nodeC ] ; const timestamps2 = await getUnreferencedTimestamps ( ) ; assert ( timestamps2 . get ( nodeA ) === undefined , "A should be referenced" ) ; const nodeBTime2 = timestamps2 . get ( nodeB ) ; const nodeCTime2 = timestamps2 . get ( nodeC ) ; assert ( nodeBTime2 === nodeBTime1 , "B's timestamp should be unchanged" ) ; assert ( nodeCTime2 === nodeCTime1 , "C's timestamp should be unchanged" ) ; } ) ; /**
         * Validates that we can detect multiple references that were added and then removed by the same node.
         * 1. Summary 1 at t1. V = [A*, B, C]. E = []. B and C have unreferenced time t1.
         * 2. Reference from A to B added. E = [A -> B].
         * 3. Reference from A to C added. E = [A -> B, A -> C].
         * 4. Reference from A to B removed. E = [A -> C].
         * 5. Reference from A to C removed. E = [].
         * 6. Summary 2 at t2. V = [A*, B]. E = []. B and C have unreferenced time t2.
         * Validates that the unreferenced time for B and C is t2 which is > t1.
         */ it ( `Scenario 6 - Multiple references added and then removed by same node` , async ( ) => { defaultGCData . gcNodes [ "/" ] = [ nodeA ] ; defaultGCData . gcNodes [ nodeA ] = [ ] ; defaultGCData . gcNodes [ nodeB ] = [ ] ; defaultGCData . gcNodes [ nodeC ] = [ ] ; const timestamps1 = await getUnreferencedTimestamps ( ) ; assert ( timestamps1 . get ( nodeA ) === undefined , "A should be referenced" ) ; const nodeBTime1 = timestamps1 . get ( nodeB ) ; const nodeCTime1 = timestamps1 . get ( nodeC ) ; assert ( nodeBTime1 !== undefined , "B should have unreferenced timestamp" ) ; assert ( nodeCTime1 !== undefined , "C should have unreferenced timestamp" ) ; garbageCollector . addedOutboundReference ( nodeA , nodeB ) ; defaultGCData . gcNodes [ nodeA ] = [ nodeB ] ; garbageCollector . addedOutboundReference ( nodeA , nodeC ) ; defaultGCData . gcNodes [ nodeA ] = [ nodeB , nodeC ] ; defaultGCData . gcNodes [ nodeA ] = [ nodeC ] ; defaultGCData . gcNodes [ nodeA ] = [ ] ; const timestamps2 = await getUnreferencedTimestamps ( ) ; assert ( timestamps2 . get ( nodeA ) === undefined , "A should be referenced" ) ; const nodeBTime2 = timestamps2 . get ( nodeB ) ; const nodeCTime2 = timestamps2 . get ( nodeC ) ; assert ( nodeCTime2 !== undefined && nodeCTime2 > nodeCTime1 , "C's timestamp should have updated" ) ; assert ( nodeBTime2 !== undefined && nodeBTime2 > nodeBTime1 , "B's timestamp should have updated" ) ; } ) ; /**
         * Validates that we generate error on detecting reference during GC that was not notified explicitly.
         * 1. Summary 1 at t1. V = [A*]. E = [].
         * 2. Node B is created. E = [].
         * 3. Reference from A to B added without notifying GC. E = [A -> B].
         * 4. Summary 2 at t2. V = [A*, B]. E = [A -> B].
         * Validates that we log an error since B is detected as a referenced node but its reference was notified
         * to GC.
         */ it . skip ( `Scenario 7 - Reference added without notifying GC` , async ( ) => { defaultGCData . gcNodes [ "/" ] = [ nodeA ] ; defaultGCData . gcNodes [ nodeA ] = [ ] ; const timestamps1 = await getUnreferencedTimestamps ( ) ; assert ( timestamps1 . get ( nodeA ) === undefined , "A should be referenced" ) ; defaultGCData . gcNodes [ nodeB ] = [ ] ; defaultGCData . gcNodes [ nodeA ] = [ nodeB ] ; await getUnreferencedTimestamps ( ) ; mockLogger . matchEvents ( [ { eventName : unknownRouteEvent , route : nodeB } , ] ) ; } ) ; } ) ; } ) ;