import { ITelemetryLogger } from "@fluidframework/common-definitions" ; import { assert , LazyPromise } from "@fluidframework/common-utils" ; import { cloneGCData } from "@fluidframework/garbage-collector" ; import { ISnapshotTree } from "@fluidframework/protocol-definitions" ; import { CreateChildSummarizerNodeParam , gcBlobKey , IGarbageCollectionData , IGarbageCollectionDetailsBase , IGarbageCollectionSummaryDetails , ISummarizeInternalResult , ISummarizeResult , ISummarizerNodeConfigWithGC , ISummarizerNodeWithGC , } from "@fluidframework/runtime-definitions" ; import { ReadAndParseBlob } from "../utils" ; import { SummarizerNode } from "./summarizerNode" ; import { EscapedPath , ICreateChildDetails , IInitialSummary , ISummarizerNodeRootContract , SummaryNode , } from "./summarizerNodeUtils" ; export interface IRootSummarizerNodeWithGC extends ISummarizerNodeWithGC , ISummarizerNodeRootContract { } class SummaryNodeWithGC extends SummaryNode { constructor ( public readonly serializedUsedRoutes , summary ) { super ( summary ) ; } } /**
 * Extends the functionality of SummarizerNode to manage this node's garbage collection data:
 * - Adds a new API `getGCData` to return GC data of this node.
 * - Caches the result of `getGCData` to be used if nothing changes between summaries.
 * - Manages the used routes of this node. These are used to identify if this node is referenced in the document
 *   and to determine if the node's used state changed since last summary.
 * - Adds trackState param to summarize. If trackState is false, it bypasses the SummarizerNode and calls
 *   directly into summarizeInternal method.
 */ export class SummarizerNodeWithGC extends SummarizerNode implements IRootSummarizerNodeWithGC { private wipSerializedUsedRoutes : string | undefined ; private referenceUsedRoutes : string [ ] | undefined ; private readonly baseGCDetailsP : LazyPromise < IGarbageCollectionDetailsBase > ; private baseGCDetailsLoaded : boolean = false ; private gcData : IGarbageCollectionData | undefined ; private usedRoutes : string [ ] = [ "" ] ; private unreferencedTimestampMs : number | undefined ; private readonly gcDisabled : boolean ; /**
     * Do not call constructor directly.
     * Use createRootSummarizerNodeWithGC to create root node, or createChild to create child nodes.
     */ public constructor ( logger , private readonly summarizeFn , config , changeSequenceNumber , /** Undefined means created without summary */ latestSummary ? , initialSummary ? , wipSummaryLogger ? , private readonly getGCDataFn ? , getBaseGCDetailsFn ? ) { super ( logger , async ( fullTree ) => summarizeFn ( fullTree , true ) , config , changeSequenceNumber , latestSummary , initialSummary , wipSummaryLogger ) ; this . gcDisabled = config . gcDisabled === true ; this . baseGCDetailsP = new LazyPromise ( async ( ) => { return ( await getBaseGCDetailsFn ?. ( ) ) ?? { usedRoutes : [ ] } ; } ) ; } deprecated public getGCSummaryDetails ( ) { return this . getBaseGCDetails ( ) ; } public getBaseGCDetails ( ) { return { gcData : this . gcData , usedRoutes : this . usedRoutes , unrefTimestamp : this . unreferencedTimestampMs } ; } /**
     * Loads state from this node's initial GC summary details. This contains the following data from the last summary
     * seen by the server for this client:
     * - usedRoutes: This is used to figure out if the used state of this node changed since last summary.
     * - gcData: The garbage collection data of this node that is required for running GC.
     */ private async loadBaseGCDetails ( ) { const baseGCDetails = await this . baseGCDetailsP ; if ( this . baseGCDetailsLoaded ) { return ; } this . baseGCDetailsLoaded = true ; if ( baseGCDetails . gcData !== undefined ) { this . gcData = cloneGCData ( baseGCDetails . gcData ) ; } this . referenceUsedRoutes = baseGCDetails . usedRoutes ; this . unreferencedTimestampMs = baseGCDetails . unrefTimestamp ; } public async summarize ( fullTree , trackState = true ) { if ( ! this . gcDisabled && this . isTrackingInProgress ( ) ) { assert ( this . wipSerializedUsedRoutes !== undefined , 0x1b1 ) ; } return trackState ? super . summarize ( fullTree ) : this . summarizeFn ( fullTree , trackState ) ; } param fullGC public async getGCData ( fullGC = false ) { assert ( ! this . gcDisabled , 0x1b2 ) ; assert ( this . getGCDataFn !== undefined , 0x1b3 ) ; await this . loadBaseGCDetails ( ) ; if ( ! fullGC && ! this . hasDataChanged ( ) && this . gcData !== undefined ) { return cloneGCData ( this . gcData ) ; } const gcData = await this . getGCDataFn ( fullGC ) ; this . gcData = cloneGCData ( gcData ) ; return gcData ; } /**
     * Called during the start of a summary. Updates the work-in-progress used routes.
     */ public startSummary ( referenceSequenceNumber , summaryLogger ) { if ( ! this . gcDisabled ) { assert ( this . wipSerializedUsedRoutes === undefined , 0x1b4 ) ; } super . startSummary ( referenceSequenceNumber , summaryLogger ) ; } /**
     * Called after summary has been uploaded to the server. Add the work-in-progress state to the pending
     * summary queue. We track this until we get an ack from the server for this summary.
     */ protected completeSummaryCore ( proposalHandle , parentPath , parentSkipRecursion ) { let wipSerializedUsedRoutes ; if ( ! this . gcDisabled ) { wipSerializedUsedRoutes = this . wipSerializedUsedRoutes ; assert ( wipSerializedUsedRoutes !== undefined , 0x1b5 ) ; } super . completeSummaryCore ( proposalHandle , parentPath , parentSkipRecursion ) ; if ( ! this . gcDisabled ) { const summaryNode = this . pendingSummaries . get ( proposalHandle ) ; if ( summaryNode !== undefined ) { const summaryNodeWithGC = new SummaryNodeWithGC ( wipSerializedUsedRoutes ! , summaryNode ) ; this . pendingSummaries . set ( proposalHandle , summaryNodeWithGC ) ; } } } /**
     * Clears the work-in-progress state.
     */ public clearSummary ( ) { this . wipSerializedUsedRoutes = undefined ; super . clearSummary ( ) ; } /**
     * Called when we get an ack from the server for a summary we sent. Update the reference state of this node
     * from the state in the pending summary queue.
     */ protected refreshLatestSummaryFromPending ( proposalHandle , referenceSequenceNumber ) { if ( ! this . gcDisabled ) { const summaryNode = this . pendingSummaries . get ( proposalHandle ) as SummaryNodeWithGC ; if ( summaryNode !== undefined ) { this . referenceUsedRoutes = JSON . parse ( summaryNode . serializedUsedRoutes ) ; } } return super . refreshLatestSummaryFromPending ( proposalHandle , referenceSequenceNumber ) ; } /**
     * Called when we need to upload the reference state from the given summary. Read the GC blob and get the state
     * to upload from it.
     */ protected async refreshLatestSummaryFromSnapshot ( referenceSequenceNumber , snapshotTree , basePath , localPath , correlatedSummaryLogger , readAndParseBlob ) { if ( ! this . gcDisabled ) { const gcDetailsBlob = snapshotTree . blobs [ gcBlobKey ] ; if ( gcDetailsBlob !== undefined ) { const gcDetails = await readAndParseBlob < IGarbageCollectionDetailsBase > ( gcDetailsBlob ) ; if ( this . referenceSequenceNumber >= referenceSequenceNumber ) { return ; } this . referenceUsedRoutes = gcDetails . usedRoutes ; } } return super . refreshLatestSummaryFromSnapshot ( referenceSequenceNumber , snapshotTree , basePath , localPath , correlatedSummaryLogger , readAndParseBlob ) ; } /**
     * Override the createChild method to return an instance of SummarizerNodeWithGC.
     */ public createChild ( /** Summarize function */ summarizeInternalFn , /** Initial id or path part of this node */ id , /**
     * Information needed to create the node.
     * If it is from a base summary, it will assert that a summary has been seen.
     * Attach information if it is created from an attach op.
     */ createParam , config = { } , getGCDataFn ? , getBaseGCDetailsFn ? ) { assert ( ! this . children . has ( id ) , 0x1b6 ) ; const createDetails = this . getCreateDetailsForChild ( id , createParam ) ; const child = new SummarizerNodeWithGC ( this . defaultLogger , summarizeInternalFn , { ... config , gcDisabled : config . gcDisabled ?? this . gcDisabled } , createDetails . changeSequenceNumber , createDetails . latestSummary , createDetails . initialSummary , this . wipSummaryLogger , getGCDataFn , getBaseGCDetailsFn ) ; this . maybeUpdateChildState ( child ) ; this . children . set ( id , child ) ; return child ; } /**
     * Deletes the child node with the given id.
     */ public deleteChild ( id ) { this . children . delete ( id ) ; } /**
     * Override the getChild method to return an instance of SummarizerNodeWithGC.
     */ public getChild ( id ) { return this . children . get ( id ) as SummarizerNodeWithGC ; } public isReferenced ( ) { return this . usedRoutes . includes ( "" ) || this . usedRoutes . includes ( "/" ) ; } public updateUsedRoutes ( usedRoutes , gcTimestamp ? ) { this . usedRoutes = usedRoutes . sort ( ) ; if ( ! this . gcDisabled && this . isTrackingInProgress ( ) ) { this . wipSerializedUsedRoutes = JSON . stringify ( this . usedRoutes ) ; } if ( this . isReferenced ( ) ) { this . unreferencedTimestampMs = undefined ; return ; } if ( this . unreferencedTimestampMs === undefined ) { this . unreferencedTimestampMs = gcTimestamp ; } } /**
     * Override the hasChanged method. If this node data or its used state changed, the node is considered changed.
     */ protected hasChanged ( ) { return this . hasDataChanged ( ) || this . hasUsedStateChanged ( ) ; } /**
     * This tells whether the data in this node has changed or not.
     */ private hasDataChanged ( ) { return super . hasChanged ( ) ; } /**
     * This tells whether the used state of this node has changed since last successful summary. If the used routes
     * of this node changed, its used state is considered changed. Basically, if this node or any of its child nodes
     * was previously used and became unused (or vice versa), its used state has changed.
     */ private hasUsedStateChanged ( ) { if ( this . gcDisabled ) { return false ; } return this . referenceUsedRoutes === undefined || JSON . stringify ( this . usedRoutes ) !== JSON . stringify ( this . referenceUsedRoutes ) ; } } param logger param summarizeInternalFn param changeSequenceNumber param referenceSequenceNumber param config param getGCDataFn param baseGCDetailsP export const createRootSummarizerNodeWithGC = ( logger , summarizeInternalFn , changeSequenceNumber , referenceSequenceNumber , config = { } , getGCDataFn ? , getBaseGCDetailsFn ? ) : IRootSummarizerNodeWithGC => new SummarizerNodeWithGC ( logger , summarizeInternalFn , config , changeSequenceNumber , referenceSequenceNumber === undefined ? undefined : SummaryNode . createForRoot ( referenceSequenceNumber ) , undefined , undefined , getGCDataFn , getBaseGCDetailsFn ) ;