From 723252ae3d97e449c4eb14d2d66c67a0814be6e6 Mon Sep 17 00:00:00 2001 From: Zach Newton Date: Tue, 21 Oct 2025 13:30:42 -0700 Subject: [PATCH 1/3] Expose container.serialize --- .../fluid-static/src/fluidContainer.ts | 14 ++++++++++ .../odsp-client/src/odspClient.ts | 28 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/packages/framework/fluid-static/src/fluidContainer.ts b/packages/framework/fluid-static/src/fluidContainer.ts index d92a79230737..0bd8d25d3a38 100644 --- a/packages/framework/fluid-static/src/fluidContainer.ts +++ b/packages/framework/fluid-static/src/fluidContainer.ts @@ -265,6 +265,11 @@ export interface IFluidContainerInternal extends ContainerExtensionStore { * @remarks This method is used to expose uploadBlob to the IFluidContainer level. UploadBlob will upload data to server side (as of now, ODSP only). There is no downloadBlob provided as it is not needed(blob lifetime managed by server). */ uploadBlob(blob: ArrayBufferLike): Promise>; + + /** + * Serialize a detached container to a string representation. This can be saved for later rehydration. + */ + serialize(): string; } /** @@ -401,4 +406,13 @@ class FluidContainer public async uploadBlob(blob: ArrayBufferLike): Promise> { return this.rootDataObject.uploadBlob(blob); } + + public serialize(): string { + if (this.container.closed || this.container.attachState !== AttachState.Detached) { + throw new Error( + "Cannot serialize container. Container must be in detached state and not closed.", + ); + } + return this.container.serialize(); + } } diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index 725f72e7d079..9833685a0040 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -11,6 +11,7 @@ import type { import { createDetachedContainer, loadExistingContainer, + rehydrateDetachedContainer, type ILoaderProps, } from "@fluidframework/container-loader/internal"; import type { @@ -136,6 +137,33 @@ export class OdspClient { return { container: fluidContainer as IFluidContainer, services }; } + /** + * Create a new container from the serialized state of a detached container. + * + * @param serializedContainer - Serialized string representation of the container. + * @param containerSchema - The schema of the container to rehydrate. + */ + public async rehydrateContainer( + serializedContainer: string, + containerSchema: T, + ): Promise<{ + container: IFluidContainer; + services: IOdspContainerServices; + }> { + const loaderProps = this.getLoaderProps(containerSchema); + + const container = await rehydrateDetachedContainer({ + ...loaderProps, + serializedState: serializedContainer, + }); + + const fluidContainer = await this.createFluidContainer(container, this.connectionConfig); + + const services = await this.getContainerServices(container); + + return { container: fluidContainer as IFluidContainer, services }; + } + public async getContainer( id: string, containerSchema: T, From d0317918df7e0de3a2e1df75df1af74ea74e2475 Mon Sep 17 00:00:00 2001 From: Zach Newton Date: Mon, 3 Nov 2025 13:24:39 -0800 Subject: [PATCH 2/3] expose only via odsp fluid container --- .../odsp-client/src/interfaces.ts | 5 ++++ .../odsp-client/src/odspClient.ts | 28 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/service-clients/odsp-client/src/interfaces.ts b/packages/service-clients/odsp-client/src/interfaces.ts index 2096dd4333d7..8d97bbc2c4df 100644 --- a/packages/service-clients/odsp-client/src/interfaces.ts +++ b/packages/service-clients/odsp-client/src/interfaces.ts @@ -97,6 +97,11 @@ export interface IOdspFluidContainer< * @returns A promise which resolves when the attach is complete, with the string identifier of the container. */ attach(props?: ContainerAttachProps): Promise; + + /** + * Serialize the container to a string representation. This can be saved for later rehydration. + */ + serialize(): string; } /** diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index 61515fbb6e4b..1dd793343e14 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -25,6 +25,8 @@ import type { ContainerAttachProps, ContainerSchema } from "@fluidframework/flui import { createDOProviderContainerRuntimeFactory, createFluidContainer, + isInternalFluidContainer, + type IFluidContainerInternal, } from "@fluidframework/fluid-static/internal"; import { OdspDocumentServiceFactory, @@ -147,7 +149,7 @@ export class OdspClient { serializedContainer: string, containerSchema: T, ): Promise<{ - container: IFluidContainer; + container: IOdspFluidContainer; services: IOdspContainerServices; }> { const loaderProps = this.getLoaderProps(containerSchema); @@ -157,11 +159,18 @@ export class OdspClient { serializedState: serializedContainer, }); - const fluidContainer = await this.createFluidContainer(container, this.connectionConfig); + const fluidContainer = await this.createFluidContainer( + container, + this.connectionConfig, + ); + // Perform type guard to access internal APIs exposed by OdspFluidContainer. + if (!isInternalFluidContainer(fluidContainer)) { + throw new Error("Fluid container is not internal"); + } const services = await this.getContainerServices(container); - return { container: fluidContainer as IFluidContainer, services }; + return { container: fluidContainer, services }; } public async getContainer( @@ -183,6 +192,10 @@ export class OdspClient { const fluidContainer = await createFluidContainer({ container, }); + // Perform type guard to access internal APIs exposed by OdspFluidContainer. + if (!isInternalFluidContainer(fluidContainer)) { + throw new Error("Fluid container is not internal"); + } const services = await this.getContainerServices(container); return { container: fluidContainer, services }; } @@ -254,7 +267,14 @@ export class OdspClient { */ return resolvedUrl.itemId; }; - const fluidContainer = await createFluidContainer({ container }); + const fluidContainer = await createFluidContainer({ + container, + }); + // Perform type guard to access internal APIs exposed by OdspFluidContainer. + if (!isInternalFluidContainer(fluidContainer)) { + throw new Error("Fluid container is not internal"); + } + // Assign custom attach method fluidContainer.attach = attach; return fluidContainer; } From 38afd85937585df4e18127f0ff888cc046fbb635 Mon Sep 17 00:00:00 2001 From: Zach Newton Date: Mon, 3 Nov 2025 13:28:41 -0800 Subject: [PATCH 3/3] fix build issues --- .../odsp-client/api-report/odsp-client.alpha.api.md | 5 +++++ .../odsp-client/api-report/odsp-client.beta.api.md | 5 +++++ packages/service-clients/odsp-client/src/odspClient.ts | 1 - 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/service-clients/odsp-client/api-report/odsp-client.alpha.api.md b/packages/service-clients/odsp-client/api-report/odsp-client.alpha.api.md index 8a0462d5c31d..8877d38dfb8c 100644 --- a/packages/service-clients/odsp-client/api-report/odsp-client.alpha.api.md +++ b/packages/service-clients/odsp-client/api-report/odsp-client.alpha.api.md @@ -10,6 +10,7 @@ export type IOdspAudience = IServiceAudience; // @beta export interface IOdspFluidContainer extends IFluidContainer { attach(props?: ContainerAttachProps): Promise; + serialize(): string; } // @beta @@ -31,6 +32,10 @@ export class OdspClient { container: IOdspFluidContainer; services: OdspContainerServices; }>; + rehydrateContainer(serializedContainer: string, containerSchema: T): Promise<{ + container: IOdspFluidContainer; + services: OdspContainerServices; + }>; } // @beta (undocumented) diff --git a/packages/service-clients/odsp-client/api-report/odsp-client.beta.api.md b/packages/service-clients/odsp-client/api-report/odsp-client.beta.api.md index fc893e97c251..0c8de5104433 100644 --- a/packages/service-clients/odsp-client/api-report/odsp-client.beta.api.md +++ b/packages/service-clients/odsp-client/api-report/odsp-client.beta.api.md @@ -10,6 +10,7 @@ export type IOdspAudience = IServiceAudience; // @beta export interface IOdspFluidContainer extends IFluidContainer { attach(props?: ContainerAttachProps): Promise; + serialize(): string; } // @beta @@ -31,6 +32,10 @@ export class OdspClient { container: IOdspFluidContainer; services: OdspContainerServices; }>; + rehydrateContainer(serializedContainer: string, containerSchema: T): Promise<{ + container: IOdspFluidContainer; + services: OdspContainerServices; + }>; } // @beta (undocumented) diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index 1dd793343e14..1f6f8240b6f3 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -26,7 +26,6 @@ import { createDOProviderContainerRuntimeFactory, createFluidContainer, isInternalFluidContainer, - type IFluidContainerInternal, } from "@fluidframework/fluid-static/internal"; import { OdspDocumentServiceFactory,