import { ITelemetryLogger } from "@fluidframework/common-definitions" ; import { assert , Uint8ArrayToString , unreachableCase } from "@fluidframework/common-utils" ; import { ISummaryContext } from "@fluidframework/driver-definitions" ; import { getGitType } from "@fluidframework/protocol-base" ; import * as api from "@fluidframework/protocol-definitions" ; import { InstrumentedStorageTokenFetcher } from "@fluidframework/odsp-driver-definitions" ; import { loggerToMonitoringContext , MonitoringContext , PerformanceEvent } from "@fluidframework/telemetry-utils" ; import { IOdspSummaryPayload , IWriteSummaryResponse , IOdspSummaryTree , IOdspSummaryTreeBaseEntry , OdspSummaryTreeEntry , OdspSummaryTreeValue , } from "./contracts" ; import { EpochTracker } from "./epochTracker" ; import { getUrlAndHeadersWithAuth } from "./getUrlAndHeadersWithAuth" ; import { getWithRetryForTokenRefresh } from "./odspUtils" ; /**
 * This class manages a summary upload. When it receives a call to upload summary, it converts the summary tree into
 * a snapshot tree and then uploads that to the server.
 */ export class OdspSummaryUploadManager { private lastSummaryProposalHandle : string | undefined ; private readonly mc : MonitoringContext ; constructor ( private readonly snapshotUrl , private readonly getStorageToken , logger , private readonly epochTracker ) { this . mc = loggerToMonitoringContext ( logger ) ; } public async writeSummaryTree ( tree , context ) { if ( context . proposalHandle !== this . lastSummaryProposalHandle ) { this . mc . logger . sendTelemetryEvent ( { eventName : "LastSummaryProposedHandleMismatch" , ackedSummaryProposedHandle : context . proposalHandle , lastSummaryProposalHandle : this . lastSummaryProposalHandle } ) ; } const result = await this . writeSummaryTreeCore ( context . ackHandle , context . referenceSequenceNumber , tree ) ; const id = result ? result . id : undefined ; if ( ! result || ! id ) { throw new Error ( `Failed to write summary tree` ) ; } this . lastSummaryProposalHandle = id ; return id ; } private async writeSummaryTreeCore ( parentHandle , referenceSequenceNumber , tree ) { const { snapshotTree , blobs } = await this . convertSummaryToSnapshotTree ( parentHandle , tree , ".app" , "" ) ; const snapshot = { entries : snapshotTree . entries ! , message : "app" , sequenceNumber : referenceSequenceNumber , type : parentHandle === undefined ? "container" : "channel" } ; return getWithRetryForTokenRefresh ( async ( options ) => { const storageToken = await this . getStorageToken ( options , "WriteSummaryTree" ) ; const { url , headers } = getUrlAndHeadersWithAuth ( `${ this . snapshotUrl }/snapshot` , storageToken ) ; headers [ "Content-Type" ] = "application/json" ; if ( parentHandle ) { headers [ "If-Match" ] = `fluid:containerid=${ parentHandle }` ; } const postBody = JSON . stringify ( snapshot ) ; return PerformanceEvent . timedExecAsync ( this . mc . logger , { eventName : "uploadSummary" , attempt : options . refresh ? 2 : 1 , hasClaims : ! ! options . claims , hasTenantId : ! ! options . tenantId , headers : Object . keys ( headers ) . length !== 0 ? true : undefined , blobs , size : postBody . length } , async ( ) => { const response = await this . epochTracker . fetchAndParseAsJSON < IWriteSummaryResponse > ( url , { body : postBody , headers , method : "POST" } , "uploadSummary" ) ; return response . content ; } ) ; } ) ; } param parentHandle param tree param rootNodeName param path param markUnreferencedNodes private async convertSummaryToSnapshotTree ( parentHandle , tree , rootNodeName , path = "" , markUnreferencedNodes = this . mc . config . getBoolean ( "Fluid.Driver.Odsp.MarkUnreferencedNodes" ) ?? true ) { const snapshotTree = { type : "tree" , entries : [ ] as OdspSummaryTreeEntry [ ] } ; let blobs = 0 ; const keys = Object . keys ( tree . tree ) ; for ( const key of keys ) { const summaryObject = tree . tree [ key ] ; let id ; let value ; let unreferenced ; const currentPath = path === "" ? `${ rootNodeName }/${ key }` : `${ path }/${ key }` ; switch ( summaryObject . type ) { case api . SummaryType . Tree : { const result = await this . convertSummaryToSnapshotTree ( parentHandle , summaryObject , rootNodeName , currentPath ) ; value = result . snapshotTree ; unreferenced = markUnreferencedNodes ? summaryObject . unreferenced : undefined ; blobs += result . blobs ; break ; } case api . SummaryType . Blob : { if ( typeof summaryObject . content === "string" ) { value = { type : "blob" , content : summaryObject . content , encoding : "utf-8" } ; } else { value = { type : "blob" , content : Uint8ArrayToString ( summaryObject . content , "base64" ) , encoding : "base64" } ; } blobs ++ ; break ; } case api . SummaryType . Handle : { if ( ! parentHandle ) { throw Error ( "Parent summary does not exist to reference by handle." ) ; } let handlePath = summaryObject . handle ; if ( handlePath . length > 0 && ! handlePath . startsWith ( "/" ) ) { handlePath = `/${ handlePath }` ; } const pathKey = `${ rootNodeName }${ handlePath }` ; id = `${ parentHandle }/${ pathKey }` ; break ; } case api . SummaryType . Attachment : { id = summaryObject . id ; break ; } default : { unreachableCase ( summaryObject , `Unknown type: ${ ( summaryObject as any ) . type }` ) ; } } const baseEntry = { path : encodeURIComponent ( key ) , type : getGitType ( summaryObject ) } ; let entry ; if ( value ) { assert ( id === undefined , 0x0ad ) ; entry = { value , ... baseEntry , unreferenced } ; } else if ( id ) { entry = { ... baseEntry , id } ; } else { throw new Error ( `Invalid tree entry for ${ summaryObject . type }` ) ; } snapshotTree . entries ! . push ( entry ) ; } return { snapshotTree , blobs } ; } }