import { ITelemetryProperties , ITelemetryBaseLogger , ITelemetryLogger } from "@fluidframework/common-definitions" ; import { IResolvedUrl , DriverErrorType } from "@fluidframework/driver-definitions" ; import { isOnline , OnlineStatus , RetryableError , NonRetryableError } from "@fluidframework/driver-utils" ; import { assert , performance } from "@fluidframework/common-utils" ; import { ISequencedDocumentMessage , ISnapshotTree } from "@fluidframework/protocol-definitions" ; import { ChildLogger , PerformanceEvent , wrapError } from "@fluidframework/telemetry-utils" ; import { fetchIncorrectResponse , throwOdspNetworkError , getSPOAndGraphRequestIdsFromResponse , } from "@fluidframework/odsp-doclib-utils" ; import { IOdspResolvedUrl , TokenFetchOptions , OdspErrorType , tokenFromResponse , isTokenFromCache , OdspResourceTokenFetchOptions , ShareLinkTypes , TokenFetcher , ICacheEntry , snapshotKey , InstrumentedStorageTokenFetcher , } from "@fluidframework/odsp-driver-definitions" ; import { fetch } from "./fetch" ; import { pkgVersion } from "./packageVersion" ; import { IOdspSnapshot } from "./contracts" ; export const getWithRetryForTokenRefreshRepeat = "getWithRetryForTokenRefreshRepeat" ; /** Parse the given url and return the origin (host name) */ export const getOrigin = ( url ) => new URL ( url ) . origin ; export interface ISnapshotContents { snapshotTree : ISnapshotTree ; blobs : Map < string , ArrayBuffer > ; ops : ISequencedDocumentMessage [ ] ; sequenceNumber : number | undefined ; } export interface IOdspResponse < T > { content : T ; headers : Map < string , string > ; commonSpoHeaders : ITelemetryProperties ; duration : number ; } export interface TokenFetchOptionsEx extends TokenFetchOptions { /** previous error we hit in getWithRetryForTokenRefresh */ previousError ? : any ; } function headersToMap ( headers ) { const newHeaders = new Map < string , string > ( ) ; for ( const [ key , value ] of headers . entries ( ) ) { newHeaders . set ( key , value ) ; } return newHeaders ; } /**
 * This API should be used with pretty much all network calls (fetch, webSocket connection) in order
 * to correctly handle expired tokens. It relies on callback fetching token, and be able to refetch
 * token on failure. Only specific cases get retry call with refresh = true, all other / unknown errors
 * simply propagate to caller
 */ export async function getWithRetryForTokenRefresh < T > ( get ) { return get ( { refresh : false } ) . catch ( async ( e ) => { const options = { refresh : true , previousError : e } ; switch ( e . errorType ) { case DriverErrorType . authorizationError : return get ( { ... options , claims : e . claims , tenantId : e . tenantId } ) ; case DriverErrorType . incorrectServerResponse : case OdspErrorType . fetchTokenError : return get ( options ) ; default : if ( e [ getWithRetryForTokenRefreshRepeat ] === true ) { return get ( options ) ; } throw e ; } } ) ; } export async function fetchHelper ( requestInfo , requestInit ) { const start = performance . now ( ) ; return fetch ( requestInfo , requestInit ) . then ( async ( fetchResponse ) => { const response = fetchResponse as any as Response ; if ( ! response ) { throw new NonRetryableError ( "odspFetchErrorNoResponse" , "No response from fetch call" , DriverErrorType . incorrectServerResponse ) ; } if ( ! response . ok || response . status < 200 || response . status >= 300 ) { throwOdspNetworkError ( `odspFetchError [${ response . status }]` , response . status , response , await response . text ( ) ) ; } const headers = headersToMap ( response . headers ) ; return { content : response , headers , commonSpoHeaders : getSPOAndGraphRequestIdsFromResponse ( headers ) , duration : performance . now ( ) - start } ; } , ( error ) => { let online = isOnline ( ) ; const errorText = `${ error }` ; if ( errorText === "TypeError: Failed to fetch" ) { online = OnlineStatus . Offline ; } if ( error . name === "AbortError" ) { throw new RetryableError ( "fetchAbort" , "Fetch Timeout (AbortError)" , OdspErrorType . fetchTimeout ) ; } if ( errorText . indexOf ( "ETIMEDOUT" ) !== - 1 ) { throw new RetryableError ( "fetchETimedout" , "Fetch Timeout (ETIMEDOUT)" , OdspErrorType . fetchTimeout ) ; } if ( online === OnlineStatus . Offline ) { throw new RetryableError ( "OdspFetchOffline" , `Offline: ${ errorText }` , DriverErrorType . offlineError ) ; } else { throw new RetryableError ( "OdspFetchError" , `Fetch error: ${ errorText }` , DriverErrorType . fetchFailure ) ; } } ) ; } param requestInfo param requestInit export async function fetchArray ( requestInfo , requestInit ) { const { content , headers , commonSpoHeaders , duration } = await fetchHelper ( requestInfo , requestInit ) ; const arrayBuffer = await content . arrayBuffer ( ) ; commonSpoHeaders . bodySize = arrayBuffer . byteLength ; return { headers , content : arrayBuffer , commonSpoHeaders , duration } ; } param requestInfo param requestInit export async function fetchAndParseAsJSONHelper < T > ( requestInfo , requestInit ) { const { content , headers , commonSpoHeaders , duration } = await fetchHelper ( requestInfo , requestInit ) ; let text ; try { text = await content . text ( ) ; } catch ( e ) { throwOdspNetworkError ( "errorWhileParsingFetchResponse" , fetchIncorrectResponse , content , text ) ; } commonSpoHeaders . bodySize = text . length ; const res = { headers , content : JSON . parse ( text ) , commonSpoHeaders , duration } ; return res ; } export interface INewFileInfo { siteUrl : string ; driveId : string ; filename : string ; filePath : string ; /**
     * application can request creation of a share link along with the creation of a new file
     * by passing in an optional param to specify the kind of sharing link
     * (at the time of adding this comment Sept/2021), odsp only supports csl
     */ createLinkType ? : ShareLinkTypes ; } export function getOdspResolvedUrl ( resolvedUrl ) { assert ( ( resolvedUrl as IOdspResolvedUrl ) . odspResolvedUrl === true , 0x1de ) ; return resolvedUrl as IOdspResolvedUrl ; } export const createOdspLogger = ( logger ? ) => ChildLogger . create ( logger , "OdspDriver" , { all : { driverVersion : pkgVersion } } ) ; export function evalBlobsAndTrees ( snapshot ) { let numTrees = 0 ; let numBlobs = 0 ; let encodedBlobsSize = 0 ; let decodedBlobsSize = 0 ; for ( const tree of snapshot . trees ) { for ( const treeEntry of tree . entries ) { if ( treeEntry . type === "blob" ) { numBlobs ++ ; } else if ( treeEntry . type === "tree" ) { numTrees ++ ; } } } if ( snapshot . blobs !== undefined ) { for ( const blob of snapshot . blobs ) { decodedBlobsSize += blob . size ; encodedBlobsSize += blob . content . length ; } } return { numTrees , numBlobs , encodedBlobsSize , decodedBlobsSize } ; } export function toInstrumentedOdspTokenFetcher ( logger , resolvedUrl , tokenFetcher , throwOnNullToken ) { return async ( options , name , alwaysRecordTokenFetchTelemetry = false ) => { return PerformanceEvent . timedExecAsync ( logger , { eventName : `${ name }_GetToken` , attempts : options . refresh ? 2 : 1 , hasClaims : ! ! options . claims , hasTenantId : ! ! options . tenantId } , async ( event ) => tokenFetcher ( { ... options , siteUrl : resolvedUrl . siteUrl , driveId : resolvedUrl . driveId , itemId : resolvedUrl . itemId } ) . then ( ( tokenResponse ) => { const token = tokenFromResponse ( tokenResponse ) ; if ( alwaysRecordTokenFetchTelemetry || event . duration >= 32 ) { event . end ( { fromCache : isTokenFromCache ( tokenResponse ) , isNull : token === null } ) ; } if ( token === null && throwOnNullToken ) { throw new NonRetryableError ( "storageTokenIsNull" , `Token is null for ${ name } call` , OdspErrorType . fetchTokenError , { method : name } ) ; } return token ; } , ( error ) => { const tokenError = wrapError ( error , ( errorMessage ) => new NonRetryableError ( "tokenFetcherFailed" , errorMessage , OdspErrorType . fetchTokenError , { method : name } ) ) ; throw tokenError ; } ) , { cancel : "generic" } ) ; } ; } export function createCacheSnapshotKey ( odspResolvedUrl ) { const cacheEntry = { type : snapshotKey , key : "" , file : { resolvedUrl : odspResolvedUrl , docId : odspResolvedUrl . hashedDocumentId } } ; return cacheEntry ; }