import * as moniker from "moniker" ; import { v4 as uuid } from "uuid" ; import { ContainerRuntimeFactoryWithDefaultDataStore } from "@fluidframework/aqueduct" ; import { assert , BaseTelemetryNullLogger , Deferred } from "@fluidframework/common-utils" ; import { AttachState , IFluidModule , IFluidCodeResolver , IResolvedFluidCodeDetails , isFluidBrowserPackage , IProvideRuntimeFactory , IContainer , } from "@fluidframework/container-definitions" ; import { Loader } from "@fluidframework/container-loader" ; import { prefetchLatestSnapshot } from "@fluidframework/odsp-driver" ; import { HostStoragePolicy , IPersistedCache } from "@fluidframework/odsp-driver-definitions" ; import { IUser } from "@fluidframework/protocol-definitions" ; import { HTMLViewAdapter } from "@fluidframework/view-adapters" ; import { IFluidMountableView } from "@fluidframework/view-interfaces" ; import { extractPackageIdentifierDetails , resolveFluidPackageEnvironment , WebCodeLoader , } from "@fluidframework/web-code-loader" ; import { IFluidPackage , IFluidCodeDetails , FluidObject } from "@fluidframework/core-interfaces" ; import { IDocumentServiceFactory , IResolvedUrl } from "@fluidframework/driver-definitions" ; import { LocalDocumentServiceFactory , LocalResolver } from "@fluidframework/local-driver" ; import { RequestParser , createDataStoreFactory } from "@fluidframework/runtime-utils" ; import { ensureFluidResolvedUrl } from "@fluidframework/driver-utils" ; import { IProvideFluidDataStoreFactory } from "@fluidframework/runtime-definitions" ; import { MultiUrlResolver } from "./multiResolver" ; import { deltaConns , getDocumentServiceFactory } from "./multiDocumentServiceFactory" ; import { OdspPersistentCache } from "./odspPersistantCache" ; export interface IDevServerUser extends IUser { name : string ; } export interface IBaseRouteOptions { port : number ; npm ? : string ; } export interface ILocalRouteOptions extends IBaseRouteOptions { mode : "local" ; single ? : boolean ; } export interface IDockerRouteOptions extends IBaseRouteOptions { mode : "docker" ; tenantId ? : string ; tenantSecret ? : string ; bearerSecret ? : string ; enableWholeSummaryUpload ? : boolean ; } export interface IRouterliciousRouteOptions extends IBaseRouteOptions { mode : "r11s" ; fluidHost ? : string ; tenantId ? : string ; tenantSecret ? : string ; bearerSecret ? : string ; enableWholeSummaryUpload ? : boolean ; } export interface ITinyliciousRouteOptions extends IBaseRouteOptions { mode : "tinylicious" ; bearerSecret ? : string ; tinyliciousPort ? : number ; } export interface IOdspRouteOptions extends IBaseRouteOptions { mode : "spo" | "spo-df" ; server ? : string ; odspAccessToken ? : string ; pushAccessToken ? : string ; forceReauth ? : boolean ; driveId ? : string ; } export type RouteOptions = ILocalRouteOptions | IDockerRouteOptions | IRouterliciousRouteOptions | ITinyliciousRouteOptions | IOdspRouteOptions ; function wrapWithRuntimeFactoryIfNeeded ( packageJson , fluidModule ) { const fluidExport = fluidModule . fluidExport ; if ( fluidExport . IRuntimeFactory === undefined ) { const dataStoreFactory = fluidExport . IFluidDataStoreFactory ; const defaultFactory = createDataStoreFactory ( packageJson . name , dataStoreFactory ) ; const runtimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore ( defaultFactory , new Map ( [ [ defaultFactory . type , Promise . resolve ( defaultFactory ) ] , ] ) ) ; return { fluidExport : { IRuntimeFactory : runtimeFactory , IFluidDataStoreFactory : dataStoreFactory } } ; } return fluidModule ; } function makeSideBySideDiv ( divId ) { const div = document . createElement ( "div" ) ; div . style . flexGrow = "1" ; div . style . width = "50%" ; div . style . border = "1px solid lightgray" ; div . style . boxSizing = "border-box" ; div . style . position = "relative" ; div . id = divId ; return div ; } class WebpackCodeResolver implements IFluidCodeResolver { constructor ( private readonly options ) { } async resolveCodeDetails ( details ) { const baseUrl = details . config . cdn ?? `http://localhost:${ this . options . port }` ; let pkg = details . package ; if ( typeof pkg === "string" ) { const resp = await fetch ( `${ baseUrl }/package.json` ) ; pkg = await resp . json ( ) as IFluidPackage ; } if ( ! isFluidBrowserPackage ( pkg ) ) { throw new Error ( "Not a Fluid package" ) ; } const browser = resolveFluidPackageEnvironment ( pkg . fluid . browser , baseUrl ) ; const parse = extractPackageIdentifierDetails ( pkg ) ; return { ... details , resolvedPackage : { ... pkg , fluid : { ... pkg . fluid , browser } } , resolvedPackageCacheId : parse . fullId } ; } } /**
 * Create a loader with WebCodeLoader and return it.
 */ async function createWebLoader ( documentId , fluidModule , options , urlResolver , codeDetails , testOrderer = false , odspPersistantCache ? ) { const odspHostStoragePolicy = { } ; if ( window . location . hash === "#binarySnapshot" ) { assert ( options . mode === "spo-df" || options . mode === "spo" , 0x240 ) ; odspHostStoragePolicy . fetchBinarySnapshotFormat = true ; } let documentServiceFactory = getDocumentServiceFactory ( documentId , options , odspPersistantCache , odspHostStoragePolicy ) ; if ( testOrderer ) { const resolvedUrl = await urlResolver . resolve ( await urlResolver . createRequestForCreateNew ( documentId ) ) ; const innerDocumentService = await documentServiceFactory . createDocumentService ( resolvedUrl ) ; documentServiceFactory = new LocalDocumentServiceFactory ( deltaConns . get ( documentId ) , undefined , innerDocumentService ) ; } const codeLoader = new WebCodeLoader ( new WebpackCodeResolver ( options ) ) ; await codeLoader . seedModule ( codeDetails , wrapWithRuntimeFactoryIfNeeded ( codeDetails . package as IFluidPackage , fluidModule ) ) ; return new Loader ( { urlResolver : testOrderer ? new MultiUrlResolver ( documentId , window . location . origin , options , true ) : urlResolver , documentServiceFactory , codeLoader } ) ; } const containers = [ ] ; export function isSynchronized ( ) { if ( containers . length === 0 ) { return true ; } const seqNum = containers [ 0 ] . deltaManager . lastSequenceNumber ; return containers . every ( ( c ) => ! c . isDirty && c . deltaManager . lastSequenceNumber === seqNum ) ; } export async function start ( id , packageJson , fluidModule , options , div ) { let documentId = id ; let url = window . location . href ; /**
     * For new documents, the `url` is of the format - http://localhost:8080/new or http://localhost:8080/manualAttach.
     * So, we create a new `id` and use that as the `documentId`.
     * We will also replace the url in the browser with a new url of format - http://localhost:8080/doc/<documentId>.
     */ const autoAttach = id === "new" || id === "testorderer" ; const manualAttach = id === "manualAttach" ; const testOrderer = id === "testorderer" ; if ( autoAttach || manualAttach ) { documentId = moniker . choose ( ) ; url = url . replace ( id , `doc/${ documentId }` ) ; } const codeDetails = { package : packageJson , config : { } } ; const urlResolver = new MultiUrlResolver ( documentId , window . location . origin , options ) ; const odspPersistantCache = new OdspPersistentCache ( ) ; const loader1 = await createWebLoader ( documentId , fluidModule , options , urlResolver , codeDetails , testOrderer , odspPersistantCache ) ; let container1 ; if ( autoAttach || manualAttach ) { container1 = await loader1 . createDetachedContainer ( codeDetails ) ; containers . push ( container1 ) ; } else { const documentUrl = `${ window . location . origin }/${ documentId }` ; if ( window . location . hash === "#prefetch" ) { assert ( options . mode === "spo-df" || options . mode === "spo" , 0x1ea ) ; const prefetched = await prefetchLatestSnapshot ( await urlResolver . resolve ( { url : documentUrl } ) , async ( ) => options . odspAccessToken , odspPersistantCache , new BaseTelemetryNullLogger ( ) , undefined ) ; assert ( prefetched , 0x1eb ) ; } container1 = await loader1 . resolve ( { url : documentUrl } ) ; containers . push ( container1 ) ; } let leftDiv = div ; let rightDiv ; if ( ( options . mode === "local" && ! options . single ) || testOrderer ) { div . style . display = "flex" ; leftDiv = makeSideBySideDiv ( "sbs-left" ) ; rightDiv = makeSideBySideDiv ( "sbs-right" ) ; div . append ( leftDiv , rightDiv ) ; } const reqParser = RequestParser . create ( { url } ) ; const fluidObjectUrl = `/${ reqParser . createSubRequest ( 4 ) . url }` ; await getFluidObjectAndRender ( container1 , fluidObjectUrl , leftDiv ) ; container1 . on ( "contextChanged" , ( ) => { getFluidObjectAndRender ( container1 , fluidObjectUrl , leftDiv ) . catch ( ( ) => { } ) ; } ) ; if ( container1 . attachState === AttachState . Detached ) { container1 = await attachContainer ( loader1 , container1 , fluidObjectUrl , urlResolver , documentId , url , leftDiv , rightDiv , manualAttach , testOrderer , ! options . mode . startsWith ( "spo" ) ) ; } if ( rightDiv ) { const loader2 = await createWebLoader ( documentId , fluidModule , options , urlResolver , codeDetails , testOrderer ) ; const requestUrl2 = await urlResolver . getAbsoluteUrl ( container1 . resolvedUrl , "" ) ; const container2 = await loader2 . resolve ( { url : requestUrl2 } ) ; containers . push ( container2 ) ; await getFluidObjectAndRender ( container2 , fluidObjectUrl , rightDiv ) ; container2 . on ( "contextChanged" , ( ) => { getFluidObjectAndRender ( container2 , fluidObjectUrl , rightDiv ) . catch ( ( ) => { } ) ; } ) ; } } async function getFluidObjectAndRender ( container , url , div ) { const response = await container . request ( { headers : { mountableView : true } , url } ) ; if ( response . status !== 200 || ! ( response . mimeType === "fluid/object" ) ) { return false ; } const fluidObject = response . value ; if ( fluidObject === undefined ) { return ; } const mountableView = fluidObject . IFluidMountableView ; if ( mountableView !== undefined ) { mountableView . mount ( div ) ; return ; } console . warn ( `Container returned a non-IFluidMountableView.  This can cause errors when mounting Fluid objects ` + `with React hooks across bundle boundaries.  URL: ${ url }` ) ; const view = new HTMLViewAdapter ( fluidObject ) ; view . render ( div , { display : "block" } ) ; } /**
 * Attached a detached container.
 * In case of manual attach (when manualAttach is true), it creates a button and attaches the container when the button
 * is clicked. Otherwise, it attaches the container right away.
 */ async function attachContainer ( loader , container , fluidObjectUrl , urlResolver , documentId , url , leftDiv , rightDiv , manualAttach , testOrderer , shouldUseContainerId ) { const replaceUrl = ( resolvedUrl ) => { let [ docUrl , title ] = [ url , documentId ] ; if ( shouldUseContainerId ) { ensureFluidResolvedUrl ( resolvedUrl ) ; docUrl = url . replace ( documentId , resolvedUrl . id ) ; title = resolvedUrl . id ; } window . history . replaceState ( { } , "" , docUrl ) ; document . title = title ; } ; let currentContainer = container ; let currentLeftDiv = leftDiv ; const attached = new Deferred < void > ( ) ; const attachUrl = testOrderer ? new LocalResolver ( ) . createCreateNewRequest ( documentId ) : await urlResolver . createRequestForCreateNew ( documentId ) ; if ( manualAttach ) { const attachDiv = document . createElement ( "div" ) ; const attachButton = document . createElement ( "button" ) ; attachButton . innerText = "Attach Container" ; const serializeButton = document . createElement ( "button" ) ; serializeButton . innerText = "Serialize" ; const rehydrateButton = document . createElement ( "button" ) ; rehydrateButton . innerText = "Rehydrate Container" ; rehydrateButton . hidden = true ; const summaryList = document . createElement ( "select" ) ; summaryList . hidden = true ; attachDiv . append ( attachButton ) ; attachDiv . append ( serializeButton ) ; attachDiv . append ( summaryList ) ; document . body . prepend ( attachDiv ) ; let summaryNum = 1 ; serializeButton . onclick = ( ) => { summaryList . hidden = false ; rehydrateButton . hidden = false ; attachDiv . append ( rehydrateButton ) ; const summary = currentContainer . serialize ( ) ; const listItem = document . createElement ( "option" ) ; listItem . innerText = `Summary_${ summaryNum }` ; summaryNum += 1 ; listItem . value = summary ; summaryList . appendChild ( listItem ) ; rehydrateButton . onclick = async ( ) => { const snapshot = summaryList . value ; currentContainer = await loader . rehydrateDetachedContainerFromSnapshot ( snapshot ) ; let newLeftDiv ; if ( rightDiv !== undefined ) { newLeftDiv = makeSideBySideDiv ( uuid ( ) ) ; } else { newLeftDiv = document . createElement ( "div" ) ; } currentLeftDiv . replaceWith ( newLeftDiv ) ; currentLeftDiv = newLeftDiv ; await getFluidObjectAndRender ( currentContainer , fluidObjectUrl , newLeftDiv ) ; currentContainer . on ( "contextChanged" , ( ) => { getFluidObjectAndRender ( currentContainer , fluidObjectUrl , newLeftDiv ) . catch ( ( ) => { } ) ; } ) ; } ; } ; attachButton . onclick = ( ) => { currentContainer . attach ( attachUrl ) . then ( ( ) => { attachDiv . remove ( ) ; replaceUrl ( currentContainer . resolvedUrl ) ; if ( rightDiv ) { rightDiv . innerText = "" ; } attached . resolve ( ) ; } , ( error ) => { console . error ( error ) ; } ) ; } ; if ( rightDiv ) { rightDiv . innerText = "Waiting for container attach" ; } } else { await currentContainer . attach ( attachUrl ) ; replaceUrl ( currentContainer . resolvedUrl ) ; attached . resolve ( ) ; } await attached . promise ; return currentContainer ; }