diff --git a/.changeset/zesty-otters-collab.md b/.changeset/zesty-otters-collab.md new file mode 100644 index 000000000000..cbdf66428df3 --- /dev/null +++ b/.changeset/zesty-otters-collab.md @@ -0,0 +1,29 @@ +--- +"@fluidframework/fluid-static": minor +"fluid-framework": minor +"__section": legacy +--- +`createTreeContainerRuntimeFactory` no longer accepts `minVersionForCollabOverride` + +The `minVersionForCollabOverride` property on the `props` argument of `createTreeContainerRuntimeFactory` has been removed. + +Pass a [`MinimumVersionForCollab`](https://fluidframework.com/docs/api/runtime-definitions/minimumversionforcollab-typealias) semver string (for example `"2.10.0"`) directly via the `compatibilityMode` property instead. `compatibilityMode` now accepts either a `MinimumVersionForCollab` semver string or the existing (deprecated) `CompatibilityMode` values `"1"` / `"2"`. + +Before: + +```ts +createTreeContainerRuntimeFactory({ + schema, + compatibilityMode: "2", + minVersionForCollabOverride: "2.10.0", +}); +``` + +After: + +```ts +createTreeContainerRuntimeFactory({ + schema, + compatibilityMode: "2.10.0", +}); +``` diff --git a/CrossClientCompatibility.md b/CrossClientCompatibility.md index 6b1593f7518f..1aa782195d62 100644 --- a/CrossClientCompatibility.md +++ b/CrossClientCompatibility.md @@ -108,25 +108,21 @@ compat internally (see #### Configuring Cross-Client Compatibility (Declarative Model) -If you are using a service client (i.e. `AzureClient` or `OdspClient`), cross-client compatibility is configured via the `CompatibilityMode` parameter. This is a required argument when creating or loading a container: +If you are using a service client (i.e. `AzureClient` or `OdspClient`), cross-client compatibility is +configured by passing a `minVersionForCollab` semver string when creating or loading a container: ```typescript // Creating a new container -const { container } = await azureClient.createContainer(schema, compatibilityMode); +const { container } = await azureClient.createContainer(schema, "2.0.0"); // Loading an existing container -const { container } = await azureClient.getContainer(id, schema, compatibilityMode); +const { container } = await azureClient.getContainer(id, schema, "2.0.0"); ``` -The client will map `CompatibilityMode` to a `minVersionForCollab` value (see [utils.ts](./packages/framework/fluid-static/src/utils.ts) for details) and automatically configure runtime options via [compatibilityConfiguration.ts](./packages/framework/fluid-static/src/compatibilityConfiguration.ts). This means you do not need to manage individual runtime options or version strings directly. - -Below is the mapping of `CompatibilityMode` values to `minVersionForCollab` at the time of writing. For the most up-to-date mapping, please refer to `compatibilityModeToMinVersionForCollab` in [utils.ts](./packages/framework/fluid-static/src/utils.ts). - - -| Mode | Meaning | Mapped `minVersionForCollab` | -| --- | --- | --- | -| `"1"` | Supports collaboration with 1.x clients. Uses a conservative set of runtime options. | `"1.0.0"` | -| `"2"` | Supports collaboration with 2.x clients only. Enables newer features (e.g., runtime ID compressor for SharedTree support). | `"2.0.0"` | +This sets the minimum Fluid version allowed to collaborate on the document and automatically configures +runtime options to be compatible with that version (see +[compatibilityConfiguration.ts](./packages/framework/fluid-static/src/compatibilityConfiguration.ts)). +You do not need to manage individual runtime options directly. #### Configuring Cross-Client Compatibility (Encapsulated Model) @@ -165,9 +161,9 @@ version your users are [saturated](#terminology) on. This will ensure: We recommend following the below pattern to ensure cross-client compatibility. Keeping your compatibility configuration up-to-date on an ongoing basis ensures you are always within a safe compatibility window. 1. Observe the distribution of Fluid versions across your application's clients. See [Observing Client Version Distribution](./FluidCompatibilityConsiderations.md#observing-client-version-distribution) for how to do this using telemetry. -2. Update your compatibility configuration to match the oldest deployed version that your clients are [saturated](#terminology) on: - - **Declarative model**: Set `CompatibilityMode` to the value corresponding to that saturated version. - - **Encapsulated model**: Set `minVersionForCollab` to the specific saturated version (e.g., `"2.10.0"`). +2. Update your compatibility configuration to match the oldest deployed version that your clients are + [saturated](#terminology) on. In both the declarative and encapsulated models, set `minVersionForCollab` + to that saturated version (e.g., `"2.10.0"`). 3. Verify that the configured compatibility checkpoint is within the supported compatibility window of the Fluid Framework version you want to upgrade to. If it is, bump your Fluid Framework dependencies and update your lock file (so a newer version isn't picked up implicitly); no further action is required. If not, wait for further saturation and return to step 1. 4. Monitor telemetry for warnings/errors to ensure safe rollout (see [Errors and Warnings to Monitor](#errors-and-warnings-to-monitor) below). At this point any clients running a version older than the configured `minVersionForCollab` may be blocked from accessing the document. diff --git a/examples/apps/presence-tracker/src/app.ts b/examples/apps/presence-tracker/src/app.ts index c6fa5e0f049e..ded034335a92 100644 --- a/examples/apps/presence-tracker/src/app.ts +++ b/examples/apps/presence-tracker/src/app.ts @@ -44,7 +44,7 @@ async function start(): Promise { if (createNew) { // The client will create a new detached container using the schema // A detached container will enable the app to modify the container before attaching it to the client - ({ container } = await client.createContainer(containerSchema, "2")); + ({ container } = await client.createContainer(containerSchema, "2.0.0")); // If the app is in a `createNew` state, and the container is detached, we attach the container. // This uploads the container to the service and connects to the collaboration session. @@ -56,7 +56,7 @@ async function start(): Promise { id = location.hash.slice(1); // Use the unique container ID to fetch the container created earlier. It will already be connected to the // collaboration session. - ({ container } = await client.getContainer(id, containerSchema, "2")); + ({ container } = await client.getContainer(id, containerSchema, "2.0.0")); } const presence = getPresence(container); diff --git a/examples/benchmarks/tablebench/src/azure.ts b/examples/benchmarks/tablebench/src/azure.ts index adc1d830cca5..1900e8083eea 100644 --- a/examples/benchmarks/tablebench/src/azure.ts +++ b/examples/benchmarks/tablebench/src/azure.ts @@ -42,7 +42,7 @@ export async function initFluid(): Promise<{ view: TreeView }> { const { tree } = container.initialObjects; view = tree.viewWith(config); } else { - ({ container } = await client.createContainer(containerSchema, "2")); + ({ container } = await client.createContainer(containerSchema, "2.0.0")); const { tree } = container.initialObjects; view = tree.viewWith(config); view.initialize(generateTable(10000)); diff --git a/examples/client-logger/app-insights-logger/src/components/ClientUtilities.ts b/examples/client-logger/app-insights-logger/src/components/ClientUtilities.ts index 096c1655af78..28d74a0eae75 100644 --- a/examples/client-logger/app-insights-logger/src/components/ClientUtilities.ts +++ b/examples/client-logger/app-insights-logger/src/components/ClientUtilities.ts @@ -75,7 +75,7 @@ export async function createFluidContainer( console.log("Creating new container..."); let createContainerResult: ContainerLoadResult; try { - createContainerResult = await client.createContainer(containerSchema, "2"); + createContainerResult = await client.createContainer(containerSchema, "2.0.0"); } catch (error) { console.error(`Encountered error creating Fluid container: "${error}".`); throw error; @@ -126,7 +126,7 @@ export async function loadExistingFluidContainer( console.log("Loading existing container..."); let loadContainerResult: ContainerLoadResult; try { - loadContainerResult = await client.getContainer(containerId, containerSchema, "2"); + loadContainerResult = await client.getContainer(containerId, containerSchema, "2.0.0"); } catch (error) { console.error(`Encountered error loading Fluid container: "${error}".`); throw error; diff --git a/examples/data-objects/text-editor/src/app.tsx b/examples/data-objects/text-editor/src/app.tsx index 40a251587de6..5189ceac7154 100644 --- a/examples/data-objects/text-editor/src/app.tsx +++ b/examples/data-objects/text-editor/src/app.tsx @@ -97,7 +97,7 @@ async function createAndAttachNewContainer(client: AzureClient): Promise<{ containerId: string; treeView: TreeViewAlpha; }> { - const { container } = await client.createContainer(containerSchema, "2"); + const { container } = await client.createContainer(containerSchema, "2.0.0"); const treeView = asAlpha( container.initialObjects.tree.viewWith(treeConfig), @@ -127,7 +127,7 @@ async function loadExistingContainer( container: IFluidContainer; treeView: TreeViewAlpha; }> { - const { container } = await client.getContainer(containerId, containerSchema, "2"); + const { container } = await client.getContainer(containerId, containerSchema, "2.0.0"); const treeView = asAlpha(container.initialObjects.tree.viewWith(treeConfig)); return { container, diff --git a/examples/service-clients/azure-client/external-controller/src/app.ts b/examples/service-clients/azure-client/external-controller/src/app.ts index 66c9077935f9..e1c0863af1d4 100644 --- a/examples/service-clients/azure-client/external-controller/src/app.ts +++ b/examples/service-clients/azure-client/external-controller/src/app.ts @@ -44,7 +44,10 @@ async function start(): Promise { if (createNew) { // The client will create a new detached container using the schema // A detached container will enable the app to modify the container before attaching it to the client - ({ container, services } = await client.createContainer(diceRollerContainerSchema, "2")); + ({ container, services } = await client.createContainer( + diceRollerContainerSchema, + "2.0.0", + )); // const map1 = container.initialObjects.map1 as ISharedMap; // map1.set("diceValue", 1); // const map2 = container.initialObjects.map1 as ISharedMap; @@ -63,7 +66,11 @@ async function start(): Promise { id = location.hash.slice(1); // Use the unique container ID to fetch the container created earlier. It will already be connected to the // collaboration session. - ({ container, services } = await client.getContainer(id, diceRollerContainerSchema, "2")); + ({ container, services } = await client.getContainer( + id, + diceRollerContainerSchema, + "2.0.0", + )); appModel = loadAppFromExistingContainer(container); } diff --git a/examples/service-clients/azure-client/external-controller/tests/index.ts b/examples/service-clients/azure-client/external-controller/tests/index.ts index c24a7d0ab54f..3ecef0ee0cf4 100644 --- a/examples/service-clients/azure-client/external-controller/tests/index.ts +++ b/examples/service-clients/azure-client/external-controller/tests/index.ts @@ -104,7 +104,7 @@ async function createContainerAndRenderInElement( containerId, createDOProviderContainerRuntimeFactory({ schema: diceRollerContainerSchema, - compatibilityMode: "2", + compatibilityMode: "2.0.0", }), createNewFlag, ); diff --git a/examples/service-clients/azure-client/todo-list/src/app.tsx b/examples/service-clients/azure-client/todo-list/src/app.tsx index ef77714e7f70..62a5a628175f 100644 --- a/examples/service-clients/azure-client/todo-list/src/app.tsx +++ b/examples/service-clients/azure-client/todo-list/src/app.tsx @@ -57,7 +57,7 @@ async function start(): Promise { if (createNew) { // The client will create a new detached container using the schema // A detached container will enable the app to modify the container before attaching it to the client - ({ container } = await client.createContainer(todoListContainerSchema, "2")); + ({ container } = await client.createContainer(todoListContainerSchema, "2.0.0")); // Initialize our models so they are ready for use with our controllers appModel = await initializeAppForNewContainer(container); @@ -71,7 +71,7 @@ async function start(): Promise { containerId = location.hash.slice(1); // Use the unique container ID to fetch the container created earlier. It will already be connected to the // collaboration session. - ({ container } = await client.getContainer(containerId, todoListContainerSchema, "2")); + ({ container } = await client.getContainer(containerId, todoListContainerSchema, "2.0.0")); appModel = loadAppFromExistingContainer(container); } diff --git a/examples/service-clients/azure-client/todo-list/test/index.tsx b/examples/service-clients/azure-client/todo-list/test/index.tsx index c0a9622d8c8b..118247462b8f 100644 --- a/examples/service-clients/azure-client/todo-list/test/index.tsx +++ b/examples/service-clients/azure-client/todo-list/test/index.tsx @@ -105,7 +105,7 @@ async function createContainerAndRenderInElement( containerId, createTreeContainerRuntimeFactory({ schema: todoListContainerSchema, - compatibilityMode: "2", + compatibilityMode: "2.0.0", }), createNewFlag, ); diff --git a/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts b/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts index 0c02f0924993..ffe2cef657c1 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts +++ b/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts @@ -23,7 +23,7 @@ export async function loadFluidData( services: OdspContainerServices; container: IFluidContainer; }> { - const { container, services } = await client.getContainer(itemId, schema); + const { container, services } = await client.getContainer(itemId, schema, "2.0.0"); return { services, container }; } @@ -36,7 +36,7 @@ export async function createFluidData( }> { // The client will create a new detached container using the schema // A detached container will enable the app to modify the container before attaching it to the client - const { container, services } = await client.createContainer(schema); + const { container, services } = await client.createContainer(schema, "2.0.0"); return { services, container }; } diff --git a/packages/framework/fluid-static/api-report/fluid-static.alpha.api.md b/packages/framework/fluid-static/api-report/fluid-static.alpha.api.md index 56b3c969c8cb..b43e5f026aa4 100644 --- a/packages/framework/fluid-static/api-report/fluid-static.alpha.api.md +++ b/packages/framework/fluid-static/api-report/fluid-static.alpha.api.md @@ -4,7 +4,7 @@ ```ts -// @public +// @public @deprecated export type CompatibilityMode = "1" | "2"; // @public diff --git a/packages/framework/fluid-static/api-report/fluid-static.beta.api.md b/packages/framework/fluid-static/api-report/fluid-static.beta.api.md index 3b329602039e..a3076610f0d6 100644 --- a/packages/framework/fluid-static/api-report/fluid-static.beta.api.md +++ b/packages/framework/fluid-static/api-report/fluid-static.beta.api.md @@ -4,7 +4,7 @@ ```ts -// @public +// @public @deprecated export type CompatibilityMode = "1" | "2"; // @public diff --git a/packages/framework/fluid-static/api-report/fluid-static.legacy.beta.api.md b/packages/framework/fluid-static/api-report/fluid-static.legacy.beta.api.md index ad89f685e26a..fb115e985111 100644 --- a/packages/framework/fluid-static/api-report/fluid-static.legacy.beta.api.md +++ b/packages/framework/fluid-static/api-report/fluid-static.legacy.beta.api.md @@ -4,7 +4,7 @@ ```ts -// @public +// @public @deprecated export type CompatibilityMode = "1" | "2"; // @public @@ -19,10 +19,9 @@ export interface ContainerSchema { // @beta @legacy export function createTreeContainerRuntimeFactory(props: { readonly schema: TreeContainerSchema; - readonly compatibilityMode: CompatibilityMode; + readonly compatibilityMode: MinimumVersionForCollab | CompatibilityMode; readonly rootDataStoreRegistry?: IFluidDataStoreRegistry; readonly runtimeOptionOverrides?: Partial; - readonly minVersionForCollabOverride?: MinimumVersionForCollab; }): IRuntimeFactory; // @public diff --git a/packages/framework/fluid-static/api-report/fluid-static.legacy.public.api.md b/packages/framework/fluid-static/api-report/fluid-static.legacy.public.api.md index e6bcd3fc736f..0467bd8f6778 100644 --- a/packages/framework/fluid-static/api-report/fluid-static.legacy.public.api.md +++ b/packages/framework/fluid-static/api-report/fluid-static.legacy.public.api.md @@ -4,7 +4,7 @@ ```ts -// @public +// @public @deprecated export type CompatibilityMode = "1" | "2"; // @public diff --git a/packages/framework/fluid-static/api-report/fluid-static.public.api.md b/packages/framework/fluid-static/api-report/fluid-static.public.api.md index e6bcd3fc736f..0467bd8f6778 100644 --- a/packages/framework/fluid-static/api-report/fluid-static.public.api.md +++ b/packages/framework/fluid-static/api-report/fluid-static.public.api.md @@ -4,7 +4,7 @@ ```ts -// @public +// @public @deprecated export type CompatibilityMode = "1" | "2"; // @public diff --git a/packages/framework/fluid-static/package.json b/packages/framework/fluid-static/package.json index 9131e257e8ca..29088cf5989a 100644 --- a/packages/framework/fluid-static/package.json +++ b/packages/framework/fluid-static/package.json @@ -142,7 +142,8 @@ "@fluidframework/runtime-utils": "workspace:~", "@fluidframework/shared-object-base": "workspace:~", "@fluidframework/telemetry-utils": "workspace:~", - "@fluidframework/tree": "workspace:~" + "@fluidframework/tree": "workspace:~", + "semver-ts": "^1.0.3" }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.2", diff --git a/packages/framework/fluid-static/src/compatibilityConfiguration.ts b/packages/framework/fluid-static/src/compatibilityConfiguration.ts index 39b554b2399e..fff5a3c6d909 100644 --- a/packages/framework/fluid-static/src/compatibilityConfiguration.ts +++ b/packages/framework/fluid-static/src/compatibilityConfiguration.ts @@ -4,19 +4,19 @@ */ import type { IContainerRuntimeOptionsInternal } from "@fluidframework/container-runtime/internal"; - -import type { CompatibilityMode } from "./types.js"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions/internal"; +import { gte } from "semver-ts"; /** - * The CompatibilityMode selected determines the set of runtime options to use. In "1" mode we support - * full interop with true 1.x clients, while in "2" mode we only support interop with 2.x clients. + * The `minVersionForCollab` determines the set of runtime options to use. + * For a 1.x `minVersionForCollab` we support full interop with true 1.x clients. + * For a 2.x `minVersionForCollab` we only support interop with 2.x clients. * - * @privateRemarks In general, we can use the `compatibilityMode` property of `LoadContainerRuntimeParams` to apply - * the proper configurations. However, there are some options that we need to explicity set that differ - * from the default values (i.e. `enableRuntimeIdCompressor` below). + * @privateRemarks The purpose of this map is to use a different set of defaults + * than what the runtime normally uses based on a given `minVersionForCollab` (e.g. `enableRuntimeIdCompressor` below).) */ -export const compatibilityModeRuntimeOptions: Record< - CompatibilityMode, +const minVersionForCollabToDefaultRuntimeOptions: Record< + "1" | "2", IContainerRuntimeOptionsInternal > = { "1": {}, @@ -27,3 +27,20 @@ export const compatibilityModeRuntimeOptions: Record< enableRuntimeIdCompressor: "on", }, }; + +/** + * Returns the fluid-static-specific runtime option overrides for the given `minVersionForCollab`. + * + * @remarks + * The bulk of runtime defaults for a given `minVersionForCollab` are selected by container-runtime + * (via `getMinVersionForCollabDefaults`). This function only contributes the additional overrides + * that fluid-static needs to layer on top of those defaults. + * @internal + */ +export function defaultRuntimeOptionsForMinVersion( + minVersionForCollab: MinimumVersionForCollab, +): IContainerRuntimeOptionsInternal { + return minVersionForCollabToDefaultRuntimeOptions[ + gte(minVersionForCollab, "2.0.0") ? "2" : "1" + ]; +} diff --git a/packages/framework/fluid-static/src/index.ts b/packages/framework/fluid-static/src/index.ts index bef5b736a6ff..725829a0cedd 100644 --- a/packages/framework/fluid-static/src/index.ts +++ b/packages/framework/fluid-static/src/index.ts @@ -33,4 +33,7 @@ export type { Myself, TreeContainerSchema, } from "./types.js"; -export { isTreeContainerSchema } from "./utils.js"; +export { + isTreeContainerSchema, + resolveCompatibilityModeToMinVersionForCollab, +} from "./utils.js"; diff --git a/packages/framework/fluid-static/src/rootDataObject.ts b/packages/framework/fluid-static/src/rootDataObject.ts index 53818d6a4324..c7792d748d40 100644 --- a/packages/framework/fluid-static/src/rootDataObject.ts +++ b/packages/framework/fluid-static/src/rootDataObject.ts @@ -31,8 +31,9 @@ import type { } from "@fluidframework/runtime-definitions/internal"; import type { SharedObjectKind } from "@fluidframework/shared-object-base/internal"; -import { compatibilityModeRuntimeOptions } from "./compatibilityConfiguration.js"; +import { defaultRuntimeOptionsForMinVersion } from "./compatibilityConfiguration.js"; import type { + // eslint-disable-next-line import-x/no-deprecated CompatibilityMode, ContainerSchema, IRootDataObject, @@ -42,13 +43,13 @@ import type { LoadableObjectRecord, } from "./types.js"; import { - compatibilityModeToMinVersionForCollab, createDataObject, createSharedObject, isDataObjectKind, isSharedObjectKind, makeFluidObject, parseDataObjectsFromSharedObjects, + resolveCompatibilityModeToMinVersionForCollab, } from "./utils.js"; /** @@ -196,9 +197,13 @@ export function createDOProviderContainerRuntimeFactory(props: { */ schema: ContainerSchema; /** - * See {@link CompatibilityMode} and compatibilityModeRuntimeOptions for more details. + * Minimum Fluid Framework version required for collaboration. Accepts a + * {@link @fluidframework/runtime-definitions#MinimumVersionForCollab} semver string; + * the legacy {@link CompatibilityMode} values `"1"` and `"2"` are **deprecated** + * equivalents of `"1.0.0"` and `"2.0.0"`. */ - compatibilityMode: CompatibilityMode; + // eslint-disable-next-line import-x/no-deprecated + compatibilityMode: MinimumVersionForCollab | CompatibilityMode; /** * Optional registry of data stores to pass to the DataObject factory. * If not provided, one will be created based on the schema. @@ -209,31 +214,20 @@ export function createDOProviderContainerRuntimeFactory(props: { * If not provided, only the default options for the given compatibilityMode will be used. */ runtimeOptionOverrides?: Partial; - /** - * Optional override for minimum version for collab. - * If not provided, the default for the given compatibilityMode will be used. - * @remarks - * This is useful when runtime options are overridden and change the minimum version for collab. - */ - minVersionForCollabOverride?: MinimumVersionForCollab; }): IRuntimeFactory { - const { - compatibilityMode, - minVersionForCollabOverride, - rootDataStoreRegistry, - runtimeOptionOverrides, - schema, - } = props; + const { rootDataStoreRegistry, runtimeOptionOverrides, schema } = props; + const minVersionForCollab = resolveCompatibilityModeToMinVersionForCollab( + props.compatibilityMode, + ); const [registryEntries, sharedObjects] = parseDataObjectsFromSharedObjects(schema); const registry = rootDataStoreRegistry ?? new FluidDataStoreRegistry(registryEntries); return new DOProviderContainerRuntimeFactory( schema, - compatibilityMode, new RootDataObjectFactory(sharedObjects, registry), { runtimeOptions: runtimeOptionOverrides, - minVersionForCollab: minVersionForCollabOverride, + minVersionForCollab, }, ); } @@ -263,31 +257,28 @@ class DOProviderContainerRuntimeFactory extends BaseContainerRuntimeFactory { * since it can take care of constructing the root data object factory based on the schema. * * @param schema - The schema for the container - * @param compatibilityMode - Compatibility mode * @param rootDataObjectFactory - A factory that can construct the root data object. + * @param config - Resolved minimum version for collab (required) and optional runtime option overrides. */ public constructor( schema: ContainerSchema, - compatibilityMode: CompatibilityMode, rootDataObjectFactory: DataObjectFactory< RootDataObject, { InitialState: RootDataObjectProps } >, - overrides?: Partial<{ - runtimeOptions: Partial; + config: { minVersionForCollab: MinimumVersionForCollab; - }>, + runtimeOptions?: Partial; + }, ) { super({ registryEntries: [rootDataObjectFactory.registryEntry], runtimeOptions: { - ...compatibilityModeRuntimeOptions[compatibilityMode], - ...overrides?.runtimeOptions, + ...defaultRuntimeOptionsForMinVersion(config.minVersionForCollab), + ...config.runtimeOptions, }, provideEntryPoint, - minVersionForCollab: - overrides?.minVersionForCollab ?? - compatibilityModeToMinVersionForCollab[compatibilityMode], + minVersionForCollab: config.minVersionForCollab, }); this.rootDataObjectFactory = rootDataObjectFactory; this.initialObjects = schema.initialObjects; diff --git a/packages/framework/fluid-static/src/treeRootDataObject.ts b/packages/framework/fluid-static/src/treeRootDataObject.ts index 0a7cb85bb01f..de33eb09b3ad 100644 --- a/packages/framework/fluid-static/src/treeRootDataObject.ts +++ b/packages/framework/fluid-static/src/treeRootDataObject.ts @@ -31,8 +31,9 @@ import type { } from "@fluidframework/runtime-definitions/internal"; import type { SharedObjectKind } from "@fluidframework/shared-object-base/internal"; -import { compatibilityModeRuntimeOptions } from "./compatibilityConfiguration.js"; +import { defaultRuntimeOptionsForMinVersion } from "./compatibilityConfiguration.js"; import type { + // eslint-disable-next-line import-x/no-deprecated CompatibilityMode, IRootDataObject, IStaticEntryPoint, @@ -41,13 +42,13 @@ import type { TreeContainerSchema, } from "./types.js"; import { - compatibilityModeToMinVersionForCollab, createDataObject, createSharedObject, isDataObjectKind, isSharedObjectKind, makeFluidObject, parseDataObjectsFromSharedObjects, + resolveCompatibilityModeToMinVersionForCollab, } from "./utils.js"; /** @@ -135,23 +136,20 @@ class TreeContainerRuntimeFactory extends BaseContainerRuntimeFactory { readonly #treeRootDataObjectFactory: TreeDataObjectFactory; public constructor( - compatibilityMode: CompatibilityMode, treeRootDataObjectFactory: TreeDataObjectFactory, - overrides?: Partial<{ - runtimeOptions: Partial; + config: { minVersionForCollab: MinimumVersionForCollab; - }>, + runtimeOptions?: Partial; + }, ) { super({ registryEntries: [treeRootDataObjectFactory.registryEntry], runtimeOptions: { - ...compatibilityModeRuntimeOptions[compatibilityMode], - ...overrides?.runtimeOptions, + ...defaultRuntimeOptionsForMinVersion(config.minVersionForCollab), + ...config.runtimeOptions, }, provideEntryPoint, - minVersionForCollab: - overrides?.minVersionForCollab ?? - compatibilityModeToMinVersionForCollab[compatibilityMode], + minVersionForCollab: config.minVersionForCollab, }); this.#treeRootDataObjectFactory = treeRootDataObjectFactory; } @@ -211,9 +209,13 @@ export function createTreeContainerRuntimeFactory(props: { readonly schema: TreeContainerSchema; /** - * See {@link CompatibilityMode} and compatibilityModeRuntimeOptions for more details. + * Minimum Fluid Framework version required for collaboration. Accepts a + * {@link @fluidframework/runtime-definitions#MinimumVersionForCollab} semver string; + * the legacy {@link CompatibilityMode} values `"1"` and `"2"` are **deprecated** + * equivalents of `"1.0.0"` and `"2.0.0"`. */ - readonly compatibilityMode: CompatibilityMode; + // eslint-disable-next-line import-x/no-deprecated + readonly compatibilityMode: MinimumVersionForCollab | CompatibilityMode; /** * Optional registry of data stores to pass to the DataObject factory. * If not provided, one will be created based on the schema. @@ -224,31 +226,20 @@ export function createTreeContainerRuntimeFactory(props: { * If not provided, only the default options for the given compatibilityMode will be used. */ readonly runtimeOptionOverrides?: Partial; - /** - * Optional override for minimum version for collab. - * If not provided, the default for the given compatibilityMode will be used. - * @remarks - * This is useful when runtime options are overridden and change the minimum version for collab. - */ - readonly minVersionForCollabOverride?: MinimumVersionForCollab; }): IRuntimeFactory { - const { - compatibilityMode, - minVersionForCollabOverride, - rootDataStoreRegistry, - runtimeOptionOverrides, - schema, - } = props; + const { rootDataStoreRegistry, runtimeOptionOverrides, schema } = props; + const minVersionForCollab = resolveCompatibilityModeToMinVersionForCollab( + props.compatibilityMode, + ); const [registryEntries, sharedObjects] = parseDataObjectsFromSharedObjects(schema); const registry = rootDataStoreRegistry ?? new FluidDataStoreRegistry(registryEntries); return new TreeContainerRuntimeFactory( - compatibilityMode, new TreeRootDataObjectFactory(sharedObjects, registry), { runtimeOptions: runtimeOptionOverrides, - minVersionForCollab: minVersionForCollabOverride, + minVersionForCollab, }, ); } diff --git a/packages/framework/fluid-static/src/types.ts b/packages/framework/fluid-static/src/types.ts index af0b2c442845..91de09c155bc 100644 --- a/packages/framework/fluid-static/src/types.ts +++ b/packages/framework/fluid-static/src/types.ts @@ -22,6 +22,12 @@ import type { ITree } from "@fluidframework/tree"; * In "1" mode we support full interop between 2.x clients and 1.x clients, * while in "2" mode we only support interop between 2.x clients. * + * @deprecated Specify the minimum Fluid Framework version directly via the + * `minVersionForCollab` parameter, which accepts a + * {@link @fluidframework/runtime-definitions#MinimumVersionForCollab} semver string. The + * legacy mode "1" is equivalent to `minVersionForCollab: "1.0.0"`; mode "2" is + * equivalent to `"2.0.0"`. + * * @public */ export type CompatibilityMode = "1" | "2"; diff --git a/packages/framework/fluid-static/src/utils.ts b/packages/framework/fluid-static/src/utils.ts index 41fcb4b8bf33..b57b3a915558 100644 --- a/packages/framework/fluid-static/src/utils.ts +++ b/packages/framework/fluid-static/src/utils.ts @@ -20,6 +20,7 @@ import { UsageError } from "@fluidframework/telemetry-utils/internal"; import { SharedTreeFactoryType } from "@fluidframework/tree/internal"; import type { + // eslint-disable-next-line import-x/no-deprecated CompatibilityMode, ContainerSchema, LoadableObjectKind, @@ -147,12 +148,24 @@ export function makeFluidObject< } /** - * Maps CompatibilityMode to a semver valid string that can be passed to the container runtime. + * Resolves the `compatibilityMode` input — either a `MinimumVersionForCollab` + * semver string or a legacy `CompatibilityMode` value — into a precise + * `MinimumVersionForCollab`. + * + * TODO: This can be removed when the deprecated CompatibilityMode is removed - AB#73679 + * + * @internal */ -export const compatibilityModeToMinVersionForCollab = { - "1": "1.0.0", - "2": "2.0.0", -} as const satisfies Record; +export function resolveCompatibilityModeToMinVersionForCollab( + // eslint-disable-next-line import-x/no-deprecated + compatibilityMode: MinimumVersionForCollab | CompatibilityMode, +): MinimumVersionForCollab { + return compatibilityMode === "1" + ? "1.0.0" + : compatibilityMode === "2" + ? "2.0.0" + : compatibilityMode; +} /** * Determines if the provided schema is a valid tree-based container schema. diff --git a/packages/runtime/runtime-definitions/api-report/runtime-definitions.beta.api.md b/packages/runtime/runtime-definitions/api-report/runtime-definitions.beta.api.md index d20a3e70d70d..56d71f69b5b5 100644 --- a/packages/runtime/runtime-definitions/api-report/runtime-definitions.beta.api.md +++ b/packages/runtime/runtime-definitions/api-report/runtime-definitions.beta.api.md @@ -4,7 +4,7 @@ ```ts -// @beta @input +// @public @input export type MinimumVersionForCollab = `${1 | 2}.${bigint}.${bigint}` | `${1 | 2}.${bigint}.${bigint}-${string}`; // (No @packageDocumentation comment for this package) diff --git a/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.alpha.api.md b/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.alpha.api.md index 9f18e360b28c..756b235f8efc 100644 --- a/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.alpha.api.md +++ b/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.alpha.api.md @@ -398,7 +398,7 @@ export interface LocalAttributionKey { type: "local"; } -// @beta @input +// @public @input export type MinimumVersionForCollab = `${1 | 2}.${bigint}.${bigint}` | `${1 | 2}.${bigint}.${bigint}-${string}`; // @beta @legacy diff --git a/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.beta.api.md b/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.beta.api.md index 75f077ccfa54..aeee28a6ab04 100644 --- a/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.beta.api.md +++ b/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.beta.api.md @@ -391,7 +391,7 @@ export interface LocalAttributionKey { type: "local"; } -// @beta @input +// @public @input export type MinimumVersionForCollab = `${1 | 2}.${bigint}.${bigint}` | `${1 | 2}.${bigint}.${bigint}-${string}`; // @beta @legacy diff --git a/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.public.api.md b/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.public.api.md index 26348215bc12..8850043fa4ea 100644 --- a/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.public.api.md +++ b/packages/runtime/runtime-definitions/api-report/runtime-definitions.legacy.public.api.md @@ -4,6 +4,9 @@ ```ts +// @public @input +export type MinimumVersionForCollab = `${1 | 2}.${bigint}.${bigint}` | `${1 | 2}.${bigint}.${bigint}-${string}`; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/runtime/runtime-definitions/api-report/runtime-definitions.public.api.md b/packages/runtime/runtime-definitions/api-report/runtime-definitions.public.api.md index 26348215bc12..8850043fa4ea 100644 --- a/packages/runtime/runtime-definitions/api-report/runtime-definitions.public.api.md +++ b/packages/runtime/runtime-definitions/api-report/runtime-definitions.public.api.md @@ -4,6 +4,9 @@ ```ts +// @public @input +export type MinimumVersionForCollab = `${1 | 2}.${bigint}.${bigint}` | `${1 | 2}.${bigint}.${bigint}-${string}`; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/runtime/runtime-definitions/src/compatibilityDefinitions.ts b/packages/runtime/runtime-definitions/src/compatibilityDefinitions.ts index 38cd8014dd78..f1726f7ca0d2 100644 --- a/packages/runtime/runtime-definitions/src/compatibilityDefinitions.ts +++ b/packages/runtime/runtime-definitions/src/compatibilityDefinitions.ts @@ -36,7 +36,7 @@ * ``` * * @input - * @beta + * @public */ export type MinimumVersionForCollab = | `${1 | 2}.${bigint}.${bigint}` diff --git a/packages/service-clients/azure-client/api-report/azure-client.beta.api.md b/packages/service-clients/azure-client/api-report/azure-client.beta.api.md index 4ab351c4ddee..85b633ce69b8 100644 --- a/packages/service-clients/azure-client/api-report/azure-client.beta.api.md +++ b/packages/service-clients/azure-client/api-report/azure-client.beta.api.md @@ -7,15 +7,29 @@ // @public export class AzureClient { constructor(properties: AzureClientProps); + createContainer(containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; + }>; + // @deprecated createContainer(containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: AzureContainerServices; }>; + getContainer(id: string, containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; + }>; + // @deprecated getContainer(id: string, containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: AzureContainerServices; }>; getContainerVersions(id: string, options?: AzureGetVersionsOptions): Promise; + viewContainerVersion(id: string, containerSchema: TContainerSchema, version: AzureContainerVersion, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + }>; + // @deprecated viewContainerVersion(id: string, containerSchema: TContainerSchema, version: AzureContainerVersion, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; }>; @@ -73,7 +87,7 @@ export interface AzureRemoteConnectionConfig extends AzureConnectionConfig { type: "remote"; } -// @public +// @public @deprecated export type CompatibilityMode = "1" | "2"; // @public diff --git a/packages/service-clients/azure-client/api-report/azure-client.legacy.beta.api.md b/packages/service-clients/azure-client/api-report/azure-client.legacy.beta.api.md index 4ab351c4ddee..85b633ce69b8 100644 --- a/packages/service-clients/azure-client/api-report/azure-client.legacy.beta.api.md +++ b/packages/service-clients/azure-client/api-report/azure-client.legacy.beta.api.md @@ -7,15 +7,29 @@ // @public export class AzureClient { constructor(properties: AzureClientProps); + createContainer(containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; + }>; + // @deprecated createContainer(containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: AzureContainerServices; }>; + getContainer(id: string, containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; + }>; + // @deprecated getContainer(id: string, containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: AzureContainerServices; }>; getContainerVersions(id: string, options?: AzureGetVersionsOptions): Promise; + viewContainerVersion(id: string, containerSchema: TContainerSchema, version: AzureContainerVersion, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + }>; + // @deprecated viewContainerVersion(id: string, containerSchema: TContainerSchema, version: AzureContainerVersion, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; }>; @@ -73,7 +87,7 @@ export interface AzureRemoteConnectionConfig extends AzureConnectionConfig { type: "remote"; } -// @public +// @public @deprecated export type CompatibilityMode = "1" | "2"; // @public diff --git a/packages/service-clients/azure-client/api-report/azure-client.legacy.public.api.md b/packages/service-clients/azure-client/api-report/azure-client.legacy.public.api.md index 57c9ead9db3b..086c53aadfe6 100644 --- a/packages/service-clients/azure-client/api-report/azure-client.legacy.public.api.md +++ b/packages/service-clients/azure-client/api-report/azure-client.legacy.public.api.md @@ -7,15 +7,29 @@ // @public export class AzureClient { constructor(properties: AzureClientProps); + createContainer(containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; + }>; + // @deprecated createContainer(containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: AzureContainerServices; }>; + getContainer(id: string, containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; + }>; + // @deprecated getContainer(id: string, containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: AzureContainerServices; }>; getContainerVersions(id: string, options?: AzureGetVersionsOptions): Promise; + viewContainerVersion(id: string, containerSchema: TContainerSchema, version: AzureContainerVersion, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + }>; + // @deprecated viewContainerVersion(id: string, containerSchema: TContainerSchema, version: AzureContainerVersion, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; }>; @@ -73,7 +87,7 @@ export interface AzureRemoteConnectionConfig extends AzureConnectionConfig { type: "remote"; } -// @public +// @public @deprecated export type CompatibilityMode = "1" | "2"; // @public diff --git a/packages/service-clients/azure-client/api-report/azure-client.public.api.md b/packages/service-clients/azure-client/api-report/azure-client.public.api.md index 57c9ead9db3b..086c53aadfe6 100644 --- a/packages/service-clients/azure-client/api-report/azure-client.public.api.md +++ b/packages/service-clients/azure-client/api-report/azure-client.public.api.md @@ -7,15 +7,29 @@ // @public export class AzureClient { constructor(properties: AzureClientProps); + createContainer(containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; + }>; + // @deprecated createContainer(containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: AzureContainerServices; }>; + getContainer(id: string, containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; + }>; + // @deprecated getContainer(id: string, containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: AzureContainerServices; }>; getContainerVersions(id: string, options?: AzureGetVersionsOptions): Promise; + viewContainerVersion(id: string, containerSchema: TContainerSchema, version: AzureContainerVersion, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + }>; + // @deprecated viewContainerVersion(id: string, containerSchema: TContainerSchema, version: AzureContainerVersion, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; }>; @@ -73,7 +87,7 @@ export interface AzureRemoteConnectionConfig extends AzureConnectionConfig { type: "remote"; } -// @public +// @public @deprecated export type CompatibilityMode = "1" | "2"; // @public diff --git a/packages/service-clients/azure-client/package.json b/packages/service-clients/azure-client/package.json index 5fb82b7d3e45..1e45cee7304e 100644 --- a/packages/service-clients/azure-client/package.json +++ b/packages/service-clients/azure-client/package.json @@ -98,6 +98,7 @@ "@fluidframework/driver-utils": "workspace:~", "@fluidframework/fluid-static": "workspace:~", "@fluidframework/routerlicious-driver": "workspace:~", + "@fluidframework/runtime-definitions": "workspace:~", "@fluidframework/telemetry-utils": "workspace:~" }, "devDependencies": { diff --git a/packages/service-clients/azure-client/src/AzureClient.ts b/packages/service-clients/azure-client/src/AzureClient.ts index 7a392714ae7c..20599bb03b47 100644 --- a/packages/service-clients/azure-client/src/AzureClient.ts +++ b/packages/service-clients/azure-client/src/AzureClient.ts @@ -29,14 +29,17 @@ import { applyStorageCompression } from "@fluidframework/driver-utils/internal"; import type { ContainerSchema, IFluidContainer, + // eslint-disable-next-line import-x/no-deprecated CompatibilityMode, } from "@fluidframework/fluid-static"; import { createDOProviderContainerRuntimeFactory, createFluidContainer, createServiceAudience, + resolveCompatibilityModeToMinVersionForCollab, } from "@fluidframework/fluid-static/internal"; import { RouterliciousDocumentServiceFactory } from "@fluidframework/routerlicious-driver/internal"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions"; import { wrapConfigProviderWithDefaults } from "@fluidframework/telemetry-utils/internal"; import { createAzureAudienceMember } from "./AzureAudience.js"; @@ -100,7 +103,7 @@ export class AzureClient { compatibilityMode, }: { schema: ContainerSchema; - compatibilityMode: CompatibilityMode; + compatibilityMode: MinimumVersionForCollab; }) => IRuntimeFactory; /** @@ -138,15 +141,42 @@ export class AzureClient { * @typeparam TContainerSchema - Used to infer the the type of 'initialObjects' in the returned container. * (normally not explicitly specified.) * @param containerSchema - Container schema for the new container. - * @param compatibilityMode - Compatibility mode the container should run in. + * @param minVersionForCollab - Minimum Fluid Framework version required for collaboration, as a + * `MinimumVersionForCollab` semver string (e.g. `"1.0.0"`, `"2.0.0"`). * @returns New detached container instance along with associated services. */ public async createContainer( containerSchema: TContainerSchema, + minVersionForCollab: MinimumVersionForCollab, + ): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; + }>; + /** + * Creates a new detached container instance in the Azure Fluid Relay. + * @typeparam TContainerSchema - Used to infer the the type of 'initialObjects' in the returned container. + * (normally not explicitly specified.) + * @param containerSchema - Container schema for the new container. + * @param compatibilityMode - Legacy {@link @fluidframework/fluid-static#CompatibilityMode} value. + * @returns New detached container instance along with associated services. + * @deprecated Pass a `MinimumVersionForCollab` semver string (e.g. `"2.0.0"`) instead. The legacy + * values `"1"` and `"2"` correspond to `"1.0.0"` and `"2.0.0"` respectively. + */ + public async createContainer( + containerSchema: TContainerSchema, + // eslint-disable-next-line import-x/no-deprecated compatibilityMode: CompatibilityMode, ): Promise<{ container: IFluidContainer; services: AzureContainerServices; + }>; + public async createContainer( + containerSchema: TContainerSchema, + // eslint-disable-next-line import-x/no-deprecated + compatibilityMode: MinimumVersionForCollab | CompatibilityMode, + ): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; }> { const loaderProps = this.getLoaderProps(containerSchema, compatibilityMode); @@ -172,16 +202,46 @@ export class AzureClient { * (normally not explicitly specified.) * @param id - Unique ID of the container in Azure Fluid Relay. * @param containerSchema - Container schema used to access data objects in the container. - * @param compatibilityMode - Compatibility mode the container should run in. + * @param minVersionForCollab - Minimum framework version required for collaboration, as a + * `MinimumVersionForCollab` semver string (e.g. `"1.0.0"`, `"2.0.0"`). + * @returns Existing container instance along with associated services. + */ + public async getContainer( + id: string, + containerSchema: TContainerSchema, + minVersionForCollab: MinimumVersionForCollab, + ): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; + }>; + /** + * Accesses the existing container given its unique ID in the Azure Fluid Relay. + * @typeparam TContainerSchema - Used to infer the the type of 'initialObjects' in the returned container. + * (normally not explicitly specified.) + * @param id - Unique ID of the container in Azure Fluid Relay. + * @param containerSchema - Container schema used to access data objects in the container. + * @param compatibilityMode - Legacy {@link @fluidframework/fluid-static#CompatibilityMode} value. * @returns Existing container instance along with associated services. + * @deprecated Pass a `MinimumVersionForCollab` semver string (e.g. `"2.0.0"`) instead. The legacy + * values `"1"` and `"2"` correspond to `"1.0.0"` and `"2.0.0"` respectively. */ public async getContainer( id: string, containerSchema: TContainerSchema, + // eslint-disable-next-line import-x/no-deprecated compatibilityMode: CompatibilityMode, ): Promise<{ container: IFluidContainer; services: AzureContainerServices; + }>; + public async getContainer( + id: string, + containerSchema: TContainerSchema, + // eslint-disable-next-line import-x/no-deprecated + compatibilityMode: MinimumVersionForCollab | CompatibilityMode, + ): Promise<{ + container: IFluidContainer; + services: AzureContainerServices; }> { const loaderProps = this.getLoaderProps(containerSchema, compatibilityMode); const url = new URL(this.connectionConfig.endpoint); @@ -210,16 +270,47 @@ export class AzureClient { * @param id - Unique ID of the source container in Azure Fluid Relay. * @param containerSchema - Container schema used to access data objects in the container. * @param version - Unique version of the source container in Azure Fluid Relay. - * @param compatibilityMode - Compatibility mode the container should run in. + * @param minVersionForCollab - Minimum framework version required for collaboration, as a + * `MinimumVersionForCollab` semver string (e.g. `"1.0.0"`, `"2.0.0"`). + * @returns Loaded container instance at the specified version. + */ + public async viewContainerVersion( + id: string, + containerSchema: TContainerSchema, + version: AzureContainerVersion, + minVersionForCollab: MinimumVersionForCollab, + ): Promise<{ + container: IFluidContainer; + }>; + /** + * Load a specific version of a container for viewing only. + * @typeparam TContainerSchema - Used to infer the the type of 'initialObjects' in the returned container. + * (normally not explicitly specified.) + * @param id - Unique ID of the source container in Azure Fluid Relay. + * @param containerSchema - Container schema used to access data objects in the container. + * @param version - Unique version of the source container in Azure Fluid Relay. + * @param compatibilityMode - Legacy {@link @fluidframework/fluid-static#CompatibilityMode} value. * @returns Loaded container instance at the specified version. + * @deprecated Pass a `MinimumVersionForCollab` semver string (e.g. `"2.0.0"`) instead. The legacy + * values `"1"` and `"2"` correspond to `"1.0.0"` and `"2.0.0"` respectively. */ public async viewContainerVersion( id: string, containerSchema: TContainerSchema, version: AzureContainerVersion, + // eslint-disable-next-line import-x/no-deprecated compatibilityMode: CompatibilityMode, ): Promise<{ container: IFluidContainer; + }>; + public async viewContainerVersion( + id: string, + containerSchema: TContainerSchema, + version: AzureContainerVersion, + // eslint-disable-next-line import-x/no-deprecated + compatibilityMode: MinimumVersionForCollab | CompatibilityMode, + ): Promise<{ + container: IFluidContainer; }> { const loaderProps = this.getLoaderProps(containerSchema, compatibilityMode); const url = new URL(this.connectionConfig.endpoint); @@ -286,17 +377,16 @@ export class AzureClient { private getLoaderProps( schema: ContainerSchema, - compatibilityMode: CompatibilityMode, + // eslint-disable-next-line import-x/no-deprecated + compatibilityMode: MinimumVersionForCollab | CompatibilityMode, ): ILoaderProps { + const factoryArguments = { + schema, + compatibilityMode: resolveCompatibilityModeToMinVersionForCollab(compatibilityMode), + }; const runtimeFactory = this.createContainerRuntimeFactory - ? this.createContainerRuntimeFactory({ - schema, - compatibilityMode, - }) - : createDOProviderContainerRuntimeFactory({ - schema, - compatibilityMode, - }); + ? this.createContainerRuntimeFactory(factoryArguments) + : createDOProviderContainerRuntimeFactory(factoryArguments); const load = async (): Promise => { return { diff --git a/packages/service-clients/azure-client/src/interfaces.ts b/packages/service-clients/azure-client/src/interfaces.ts index cbc26d467fa2..ce80d79f6476 100644 --- a/packages/service-clients/azure-client/src/interfaces.ts +++ b/packages/service-clients/azure-client/src/interfaces.ts @@ -10,13 +10,9 @@ import type { } from "@fluidframework/core-interfaces"; import type { IUser } from "@fluidframework/driver-definitions"; import type { ICompressionStorageConfig } from "@fluidframework/driver-utils"; -import type { - CompatibilityMode, - ContainerSchema, - IMember, - IServiceAudience, -} from "@fluidframework/fluid-static"; +import type { ContainerSchema, IMember, IServiceAudience } from "@fluidframework/fluid-static"; import type { ITokenProvider } from "@fluidframework/routerlicious-driver"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions"; /** * Props for initializing a new AzureClient instance @@ -53,7 +49,7 @@ export interface AzureClientPropsInternal extends AzureClientProps { compatibilityMode, }: { schema: ContainerSchema; - compatibilityMode: CompatibilityMode; + compatibilityMode: MinimumVersionForCollab; }) => IRuntimeFactory; } diff --git a/packages/service-clients/azure-client/src/test/AzureClient.spec.ts b/packages/service-clients/azure-client/src/test/AzureClient.spec.ts index 20875f56d72d..c3845fdfad4e 100644 --- a/packages/service-clients/azure-client/src/test/AzureClient.spec.ts +++ b/packages/service-clients/azure-client/src/test/AzureClient.spec.ts @@ -76,7 +76,7 @@ const connectionModeOf = (container: IFluidContainer): ConnectionMode => { return getContainerConnectionMode(container.container); }; -for (const compatibilityMode of ["1", "2"] as const) { +for (const compatibilityMode of ["1.0.0", "2.0.0"] as const) { describe(`AzureClient (compatibilityMode: ${compatibilityMode})`, function () { const connectTimeoutMs = 1000; let client: AzureClient; @@ -363,7 +363,7 @@ for (const compatibilityMode of ["1", "2"] as const) { it("preserves 'SharedTree' type", async function () { // SharedTree is not supported in compatibilityMode "1", because it requires idCompressor to be enabled. - if (compatibilityMode === "1") { + if (compatibilityMode === "1.0.0") { this.skip(); } const { container } = await client.createContainer( @@ -438,7 +438,7 @@ for (const compatibilityMode of ["1", "2"] as const) { } as const satisfies ContainerRuntimeOptionsInternal; const expectedRuntimeOptions = - compatibilityMode === "1" ? expectedRuntimeOptions1 : expectedRuntimeOptions2; + compatibilityMode === "1.0.0" ? expectedRuntimeOptions1 : expectedRuntimeOptions2; assert(isInternalFluidContainer(container_defaultConfig)); const actualRuntimeOptions = getRuntimeOptions( getContainerRuntime(container_defaultConfig.container), diff --git a/packages/service-clients/end-to-end-tests/azure-client/package.json b/packages/service-clients/end-to-end-tests/azure-client/package.json index cbc50f4c07f5..b143b5e1066e 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/package.json +++ b/packages/service-clients/end-to-end-tests/azure-client/package.json @@ -15,6 +15,7 @@ "scripts": { "build": "fluid-build . --task build", "build:compile": "fluid-build . --task compile", + "build:genver": "gen-version", "build:test": "tsc --project ./src/test/tsconfig.json", "check:biome": "biome check .", "check:format": "npm run check:biome", diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/packageVersion.ts b/packages/service-clients/end-to-end-tests/azure-client/src/packageVersion.ts new file mode 100644 index 000000000000..9e2ead378643 --- /dev/null +++ b/packages/service-clients/end-to-end-tests/azure-client/src/packageVersion.ts @@ -0,0 +1,9 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + * + * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY + */ + +export const pkgName = "@fluidframework/azure-end-to-end-tests"; +export const pkgVersion = "2.101.0"; diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/AzureClientFactory.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/AzureClientFactory.ts index a29919a66985..6b6c4846ea69 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/AzureClientFactory.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/AzureClientFactory.ts @@ -20,7 +20,12 @@ import { import type { IRuntimeFactory } from "@fluidframework/container-definitions/legacy"; import type { IConfigProviderBase } from "@fluidframework/core-interfaces"; import { ScopeType } from "@fluidframework/driver-definitions/legacy"; -import type { CompatibilityMode, ContainerSchema } from "@fluidframework/fluid-static"; +import type { + // eslint-disable-next-line import-x/no-deprecated + CompatibilityMode, + ContainerSchema, +} from "@fluidframework/fluid-static"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions"; import { type MockLogger, createChildLogger, @@ -59,7 +64,8 @@ export function createAzureClient( compatibilityMode, }: { schema: ContainerSchema; - compatibilityMode: CompatibilityMode; + // eslint-disable-next-line import-x/no-deprecated + compatibilityMode: MinimumVersionForCollab | CompatibilityMode; }) => IRuntimeFactory, ): AzureClient { const args = process.argv.slice(2); diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/audience.spec.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/audience.spec.ts index 821832d4a872..957c31abed59 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/audience.spec.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/audience.spec.ts @@ -19,7 +19,7 @@ import { ScopeType, } from "./AzureClientFactory.js"; import * as ephemeralSummaryTrees from "./ephemeralSummaryTrees.js"; -import { configProvider, waitForMember, getTestMatrix } from "./utils.js"; +import { configProvider, waitForMember, getTestMatrix, pkgVersion } from "./utils.js"; const testMatrix = getTestMatrix(); for (const testOpts of testMatrix) { @@ -54,9 +54,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container, services } = await client.getContainer(containerId, schema, "2")); + ({ container, services } = await client.getContainer(containerId, schema, pkgVersion)); } else { - ({ container, services } = await client.createContainer(schema, "2")); + ({ container, services } = await client.createContainer(schema, pkgVersion)); containerId = await container.attach(); } @@ -99,9 +99,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container, services } = await client.getContainer(containerId, schema, "2")); + ({ container, services } = await client.getContainer(containerId, schema, pkgVersion)); } else { - ({ container, services } = await client.createContainer(schema, "2")); + ({ container, services } = await client.createContainer(schema, pkgVersion)); containerId = await container.attach(); } @@ -131,7 +131,11 @@ for (const testOpts of testMatrix) { "Fluid.Container.ForceWriteConnection": true, }), ); - const { services: servicesGet } = await client2.getContainer(containerId, schema, "2"); + const { services: servicesGet } = await client2.getContainer( + containerId, + schema, + pkgVersion, + ); /* This is a workaround for a known bug, we should have one member (self) upon container connection */ const partner = await waitForMember(servicesGet.audience, "test-user-id-2"); @@ -163,9 +167,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container } = await client.getContainer(containerId, schema, "2")); + ({ container } = await client.getContainer(containerId, schema, pkgVersion)); } else { - ({ container } = await client.createContainer(schema, "2")); + ({ container } = await client.createContainer(schema, pkgVersion)); containerId = await container.attach(); } @@ -184,7 +188,11 @@ for (const testOpts of testMatrix) { "Fluid.Container.ForceWriteConnection": true, }), ); - const { services: servicesGet } = await client2.getContainer(containerId, schema, "2"); + const { services: servicesGet } = await client2.getContainer( + containerId, + schema, + pkgVersion, + ); /* This is a workaround for a known bug, we should have one member (self) upon container connection */ const partner = await waitForMember(servicesGet.audience, "test-user-id-2"); @@ -222,9 +230,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container, services } = await client.getContainer(containerId, schema, "2")); + ({ container, services } = await client.getContainer(containerId, schema, pkgVersion)); } else { - ({ container, services } = await client.createContainer(schema, "2")); + ({ container, services } = await client.createContainer(schema, pkgVersion)); containerId = await container.attach(); } @@ -254,7 +262,7 @@ for (const testOpts of testMatrix) { [ScopeType.DocRead], ); const { container: partnerContainer, services: partnerServices } = - await partnerClient.getContainer(containerId, schema, "2"); + await partnerClient.getContainer(containerId, schema, pkgVersion); if (partnerContainer.connectionState !== ConnectionState.Connected) { await timeoutPromise( @@ -331,9 +339,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container } = await client.getContainer(containerId, schema, "2")); + ({ container } = await client.getContainer(containerId, schema, pkgVersion)); } else { - ({ container } = await client.createContainer(schema, "2")); + ({ container } = await client.createContainer(schema, pkgVersion)); containerId = await container.attach(); } @@ -352,7 +360,7 @@ for (const testOpts of testMatrix) { [ScopeType.DocRead], ); const { container: partnerContainer, services: partnerServices } = - await partnerClient.getContainer(containerId, schema, "2"); + await partnerClient.getContainer(containerId, schema, pkgVersion); if (partnerContainer.connectionState !== ConnectionState.Connected) { await timeoutPromise( @@ -398,7 +406,7 @@ for (const testOpts of testMatrix) { [ScopeType.DocRead], ); const { container: partnerContainer2, services: partnerServices2 } = - await partnerClient2.getContainer(containerId, schema, "2"); + await partnerClient2.getContainer(containerId, schema, pkgVersion); if (partnerContainer2.connectionState !== ConnectionState.Connected) { await timeoutPromise( diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/containerCreate.spec.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/containerCreate.spec.ts index 9207a3c9accf..14bdb3d6a9bb 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/containerCreate.spec.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/containerCreate.spec.ts @@ -36,7 +36,7 @@ import { getContainerIdFromPayloadResponse, } from "./AzureClientFactory.js"; import * as ephemeralSummaryTrees from "./ephemeralSummaryTrees.js"; -import { getTestMatrix, mapWait } from "./utils.js"; +import { getTestMatrix, mapWait, pkgVersion } from "./utils.js"; const configProvider = (settings: Record): IConfigProviderBase => ({ getRawConfig: (name: string): ConfigTypes => settings[name], @@ -73,7 +73,7 @@ for (const testOpts of testMatrix) { if (isEphemeral) { this.skip(); } - const { container } = await client.createContainer(schema, "2"); + const { container } = await client.createContainer(schema, pkgVersion); assert.strictEqual( container.attachState, AttachState.Detached, @@ -101,9 +101,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container } = await client.getContainer(containerId, schema, "2")); + ({ container } = await client.getContainer(containerId, schema, pkgVersion)); } else { - ({ container } = await client.createContainer(schema, "2")); + ({ container } = await client.createContainer(schema, pkgVersion)); containerId = await container.attach(); } @@ -138,9 +138,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container } = await client.getContainer(containerId, schema, "2")); + ({ container } = await client.getContainer(containerId, schema, pkgVersion)); } else { - ({ container } = await client.createContainer(schema, "2")); + ({ container } = await client.createContainer(schema, pkgVersion)); containerId = await container.attach(); } @@ -181,7 +181,7 @@ for (const testOpts of testMatrix) { ); containerId = getContainerIdFromPayloadResponse(containerResponse); } else { - ({ container: newContainer } = await client.createContainer(schema, "2")); + ({ container: newContainer } = await client.createContainer(schema, pkgVersion)); containerId = await newContainer.attach(); if (newContainer.connectionState !== ConnectionState.Connected) { @@ -192,7 +192,7 @@ for (const testOpts of testMatrix) { } } - const resources = client.getContainer(containerId, schema, "2"); + const resources = client.getContainer(containerId, schema, pkgVersion); await assert.doesNotReject( resources, () => true, @@ -210,7 +210,7 @@ for (const testOpts of testMatrix) { it("cannot load improperly created container (cannot load a non-existent container)", async () => { const consoleErrorFn = console.error; console.error = (): void => {}; - const containerAndServicesP = client.getContainer("containerConfig", schema, "2"); + const containerAndServicesP = client.getContainer("containerConfig", schema, pkgVersion); const errorFn = (error: Error): boolean => { assert.notStrictEqual(error.message, undefined, "Azure Client error is undefined"); @@ -266,7 +266,7 @@ for (const testOpts of testMatrix) { if (isEphemeral) { this.skip(); } - await client.createContainer(schema, "2"); + await client.createContainer(schema, pkgVersion); const event = mockLogger.events.find((e) => e.eventName.endsWith("ContainerLoadStats")); assert(event !== undefined, "ContainerLoadStats event should exist"); const featureGates = event.featureGates as string; @@ -310,7 +310,7 @@ for (const testOpts of testMatrix) { * Expected behavior: an error should not be thrown nor should a rejected promise * be returned. */ - for (const compatibilityMode of ["1", "2"] as const) { + for (const compatibilityMode of ["1.0.0", "2.0.0"] as const) { it(`Current AzureClient (mode: "${compatibilityMode}") can get container made by legacy AzureClient`, async () => { const { container: containerLegacy } = await clientLegacy.createContainer(schemaLegacy); @@ -348,7 +348,7 @@ for (const testOpts of testMatrix) { // Await the value being saved, especially important if we dispose the legacy container. await valueSetP; - if (compatibilityMode === "2") { + if (compatibilityMode === "2.0.0") { // We don't support interop between legacy containers and "2" mode, dispose the legacy // container to avoid this case. containerLegacy.dispose(); @@ -429,7 +429,7 @@ for (const testOpts of testMatrix) { const { container: containerCurrent } = await clientCurrent1.createContainer( schemaCurrent, // Note: Only containers created in compatibility mode "1" may be loaded by legacy client. - "1", + "1.0.0", ); const containerId = await containerCurrent.attach(); @@ -470,7 +470,7 @@ for (const testOpts of testMatrix) { it(`Current AzureClient (mode: "2") can get container made by current AzureClient (mode: "1")`, async () => { const { container: containerCurrent1 } = await clientCurrent1.createContainer( schemaCurrent, - "1", + "1.0.0", ); const containerId = await containerCurrent1.attach(); @@ -486,7 +486,7 @@ for (const testOpts of testMatrix) { containerCurrent1.initialObjects.map1.set("key", "value"); - const resources = clientCurrent2.getContainer(containerId, schemaCurrent, "2"); + const resources = clientCurrent2.getContainer(containerId, schemaCurrent, "2.0.0"); await assert.doesNotReject(resources, () => true, "container could not be loaded"); const { container: containerCurrent2 } = await resources; @@ -512,7 +512,7 @@ for (const testOpts of testMatrix) { it(`Current AzureClient (mode: "1") can get container made by current AzureClient (mode: "2")`, async () => { const { container: containerCurrent2 } = await clientCurrent2.createContainer( schemaCurrent, - "2", + "2.0.0", ); const containerId = await containerCurrent2.attach(); @@ -528,7 +528,7 @@ for (const testOpts of testMatrix) { containerCurrent2.initialObjects.map1.set("key", "value"); - const resources = clientCurrent1.getContainer(containerId, schemaCurrent, "1"); + const resources = clientCurrent1.getContainer(containerId, schemaCurrent, "1.0.0"); await assert.doesNotReject(resources, () => true, "container could not be loaded"); const { container: containerCurrent1 } = await resources; @@ -549,7 +549,7 @@ for (const testOpts of testMatrix) { it("op grouping disabled as expected for 1.x clients", async () => { const { container: container1 } = await clientCurrent1.createContainer( schemaCurrent, - "1", + "1.0.0", ); const containerId = await container1.attach(); @@ -605,7 +605,7 @@ for (const testOpts of testMatrix) { } }); - for (const compatibilityMode of ["1", "2"] as const) { + for (const compatibilityMode of ["1.0.0", "2.0.0"] as const) { it(`op grouping works as expected (compatibilityMode: ${compatibilityMode})`, async () => { const { container: container1 } = await clientCurrent1.createContainer( schemaCurrent, @@ -660,7 +660,7 @@ for (const testOpts of testMatrix) { } } - if (compatibilityMode === "1") { + if (compatibilityMode === "1.0.0") { assert.strictEqual( groupedBatchCount, 0, @@ -706,7 +706,7 @@ describe("Container create in tree-only mode", () => { tree: SharedTree, }, }; - const { container } = await client.createContainer(schema, "2"); + const { container } = await client.createContainer(schema, pkgVersion); assert(SharedTree.is(container.initialObjects.tree)); }); @@ -721,7 +721,7 @@ describe("Container create in tree-only mode", () => { }; await assert.rejects( - client.createContainer(schema, "2"), + client.createContainer(schema, pkgVersion), (error: unknown) => { assert(error instanceof UsageError); assert.strictEqual(error.message, invalidSchemaErrorMessage); diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/ddsTests.spec.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/ddsTests.spec.ts index c8dfdc2650d9..135fda0acb7b 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/ddsTests.spec.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/ddsTests.spec.ts @@ -19,7 +19,7 @@ import { } from "./AzureClientFactory.js"; import { CounterTestDataObject, TestDataObject } from "./TestDataObject.js"; import * as ephemeralSummaryTrees from "./ephemeralSummaryTrees.js"; -import { getTestMatrix, mapWait } from "./utils.js"; +import { getTestMatrix, mapWait, pkgVersion } from "./utils.js"; const testMatrix = getTestMatrix(); for (const testOpts of testMatrix) { @@ -54,9 +54,13 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container: newContainer } = await client.getContainer(containerId, schema, "2")); + ({ container: newContainer } = await client.getContainer( + containerId, + schema, + pkgVersion, + )); } else { - ({ container: newContainer } = await client.createContainer(schema, "2")); + ({ container: newContainer } = await client.createContainer(schema, pkgVersion)); containerId = await newContainer.attach(); } @@ -67,7 +71,7 @@ for (const testOpts of testMatrix) { }); } - const resources = client.getContainer(containerId, schema, "2"); + const resources = client.getContainer(containerId, schema, pkgVersion); await assert.doesNotReject( resources, () => true, @@ -97,9 +101,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container } = await client.getContainer(containerId, schema, "2")); + ({ container } = await client.getContainer(containerId, schema, pkgVersion)); } else { - ({ container } = await client.createContainer(schema, "2")); + ({ container } = await client.createContainer(schema, pkgVersion)); containerId = await container.attach(); } @@ -115,7 +119,11 @@ for (const testOpts of testMatrix) { map1Create.set("new-key", "new-value"); const valueCreate: string | undefined = map1Create.get("new-key"); - const { container: containerGet } = await client.getContainer(containerId, schema, "2"); + const { container: containerGet } = await client.getContainer( + containerId, + schema, + pkgVersion, + ); const map1Get = containerGet.initialObjects.map1; const valueGet: string | undefined = await mapWait(map1Get, "new-key"); assert.strictEqual(valueGet, valueCreate, "container can't change initial objects"); @@ -142,9 +150,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container } = await client.getContainer(containerId, doSchema, "2")); + ({ container } = await client.getContainer(containerId, doSchema, pkgVersion)); } else { - ({ container } = await client.createContainer(doSchema, "2")); + ({ container } = await client.createContainer(doSchema, pkgVersion)); containerId = await container.attach(); } @@ -168,7 +176,7 @@ for (const testOpts of testMatrix) { const { container: containerGet } = await client.getContainer( containerId, doSchema, - "2", + pkgVersion, ); const initialObjectsGet = containerGet.initialObjects; assert( @@ -205,9 +213,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container } = await client.getContainer(containerId, doSchema, "2")); + ({ container } = await client.getContainer(containerId, doSchema, pkgVersion)); } else { - ({ container } = await client.createContainer(doSchema, "2")); + ({ container } = await client.createContainer(doSchema, pkgVersion)); containerId = await container.attach(); } @@ -235,7 +243,7 @@ for (const testOpts of testMatrix) { const { container: containerGet } = await client.getContainer( containerId, doSchema, - "2", + pkgVersion, ); const initialObjectsGet = containerGet.initialObjects; assert( @@ -274,7 +282,7 @@ for (const testOpts of testMatrix) { ); containerId = getContainerIdFromPayloadResponse(containerResponse); } else { - ({ container } = await client.createContainer(doSchema, "2")); + ({ container } = await client.createContainer(doSchema, pkgVersion)); const initialObjectsCreate = container.initialObjects; const mdo2 = initialObjectsCreate.mdo2 as CounterTestDataObject; @@ -297,7 +305,7 @@ for (const testOpts of testMatrix) { const { container: containerGet } = await client.getContainer( containerId, doSchema, - "2", + pkgVersion, ); const initialObjectsGet = containerGet.initialObjects; const mdo2get = initialObjectsGet.mdo2 as CounterTestDataObject; @@ -333,9 +341,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container } = await client.getContainer(containerId, dynamicSchema, "2")); + ({ container } = await client.getContainer(containerId, dynamicSchema, pkgVersion)); } else { - ({ container } = await client.createContainer(dynamicSchema, "2")); + ({ container } = await client.createContainer(dynamicSchema, pkgVersion)); containerId = await container.attach(); } diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.tool.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.tool.ts index d8574fa2329a..a18fbb2a49e8 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.tool.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.tool.ts @@ -31,6 +31,7 @@ import { timeoutPromise } from "@fluidframework/test-utils/internal"; import { createAzureTokenProvider } from "../AzureTokenFactory.js"; import { TestDataObject } from "../TestDataObject.js"; +import { pkgVersion } from "../utils.js"; import type { MessageFromChild as MessageToParent, @@ -145,10 +146,14 @@ const getOrCreateContainer = async (params: { }); let services: AzureContainerServices; if (containerId === undefined) { - ({ container, services } = await client.createContainer(containerSchema, "2")); + ({ container, services } = await client.createContainer(containerSchema, pkgVersion)); containerId = await container.attach(); } else { - ({ container, services } = await client.getContainer(containerId, containerSchema, "2")); + ({ container, services } = await client.getContainer( + containerId, + containerSchema, + pkgVersion, + )); } container.on("disconnected", onDisconnected); diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/signals.spec.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/signals.spec.ts index 098699380195..13082c50887c 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/signals.spec.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/signals.spec.ts @@ -19,7 +19,7 @@ import { } from "./AzureClientFactory.js"; import { SignalerTestDataObject } from "./TestDataObject.js"; import * as ephemeralSummaryTrees from "./ephemeralSummaryTrees.js"; -import { configProvider, getTestMatrix } from "./utils.js"; +import { configProvider, getTestMatrix, pkgVersion } from "./utils.js"; interface UserIdAndName { readonly id: string; @@ -108,14 +108,18 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container, services } = await client.getContainer(containerId, schema, "2")); + ({ container, services } = await client.getContainer( + containerId, + schema, + pkgVersion, + )); } else { - ({ container, services } = await client.createContainer(schema, "2")); + ({ container, services } = await client.createContainer(schema, pkgVersion)); containerId = await container.attach(); } } else { containerId = id; - ({ container, services } = await client.getContainer(containerId, schema, "2")); + ({ container, services } = await client.getContainer(containerId, schema, pkgVersion)); } if (container.connectionState !== ConnectionState.Connected) { diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/tree.spec.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/tree.spec.ts index 10a3a2042f9f..a689d8c5a779 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/tree.spec.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/tree.spec.ts @@ -20,7 +20,7 @@ import { getContainerIdFromPayloadResponse, } from "./AzureClientFactory.js"; import * as ephemeralSummaryTrees from "./ephemeralSummaryTrees.js"; -import { getTestMatrix } from "./utils.js"; +import { getTestMatrix, pkgVersion } from "./utils.js"; const sf = new SchemaFactory("d302b84c-75f6-4ecd-9663-524f467013e3"); @@ -87,7 +87,7 @@ for (const testOpts of testMatrix) { let treeData: TreeView; if (summaryTree === undefined) { - const { container } = await client.createContainer(schema, "2"); + const { container } = await client.createContainer(schema, pkgVersion); treeData = container.initialObjects.tree1.viewWith(treeConfiguration); treeData.initialize(new StringArray([])); containerId = await container.attach(); @@ -100,7 +100,7 @@ for (const testOpts of testMatrix) { ); containerId = getContainerIdFromPayloadResponse(containerResponse); - const { container } = await client.getContainer(containerId, schema, "2"); + const { container } = await client.getContainer(containerId, schema, pkgVersion); treeData = container.initialObjects.tree1.viewWith(treeConfiguration); await waitForConnection(container); } @@ -137,7 +137,7 @@ for (const testOpts of testMatrix) { treeData.root.insertNew("test string 1"); - const resources = client.getContainer(containerId, schema, "2"); + const resources = client.getContainer(containerId, schema, pkgVersion); await assert.doesNotReject( resources, () => true, @@ -172,7 +172,7 @@ for (const testOpts of testMatrix) { }) {} it("can read and edit data", async () => { - const { container } = await client.createContainer(schema, "2"); + const { container } = await client.createContainer(schema, pkgVersion); await container.attach(); const view = container.initialObjects.tree1.viewWith( new TreeViewConfiguration({ schema: User, enableSchemaValidation: true }), @@ -213,7 +213,7 @@ for (const testOpts of testMatrix) { }); it("can handle undo/redo and transactions", async () => { - const { container } = await client.createContainer(schema, "2"); + const { container } = await client.createContainer(schema, pkgVersion); await container.attach(); const view = asAlpha( container.initialObjects.tree1.viewWith( @@ -262,7 +262,7 @@ for (const testOpts of testMatrix) { it("can use identifiers and the static Tree APIs", async () => { class Widget extends sf.object("Widget", { id: sf.identifier }) {} - const { container } = await client.createContainer(schema, "2"); + const { container } = await client.createContainer(schema, pkgVersion); await container.attach(); const view = container.initialObjects.tree1.viewWith( new TreeViewConfiguration({ @@ -297,7 +297,7 @@ for (const testOpts of testMatrix) { }) {} allowUnused>(); - const { container } = await client.createContainer(schema, "2"); + const { container } = await client.createContainer(schema, pkgVersion); await container.attach(); const view = container.initialObjects.tree1.viewWith( new TreeViewConfiguration({ schema: Doll, enableSchemaValidation: true }), diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/tsconfig.json b/packages/service-clients/end-to-end-tests/azure-client/src/test/tsconfig.json index 3f0b3018a758..51e495c6da03 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/tsconfig.json +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/tsconfig.json @@ -1,11 +1,11 @@ { "extends": "../../../../../../common/build/build-common/tsconfig.test.node16.json", "compilerOptions": { - "rootDir": "./", - "outDir": "../../lib/test", + "rootDir": "../", + "outDir": "../../lib", "types": ["mocha", "node"], "noUncheckedIndexedAccess": false, "exactOptionalPropertyTypes": false, }, - "include": ["./**/*"], + "include": ["./**/*", "../packageVersion.ts"], } diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/utils.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/utils.ts index 4b43de4ec4a0..9461e20e0cbe 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/utils.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/utils.ts @@ -8,6 +8,8 @@ import type { ConfigTypes, IConfigProviderBase } from "@fluidframework/core-inte import type { IMember } from "@fluidframework/fluid-static"; import type { ISharedMap, IValueChanged } from "@fluidframework/map/legacy"; +export { pkgVersion } from "../packageVersion.js"; + export const waitForMember = async ( audience: IAzureAudience, id: string, diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/viewContainerVersion.spec.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/viewContainerVersion.spec.ts index 6c02cdc15440..2929a5b8d2c9 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/viewContainerVersion.spec.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/viewContainerVersion.spec.ts @@ -17,7 +17,7 @@ import { getContainerIdFromPayloadResponse, } from "./AzureClientFactory.js"; import * as ephemeralSummaryTrees from "./ephemeralSummaryTrees.js"; -import { getTestMatrix } from "./utils.js"; +import { getTestMatrix, pkgVersion } from "./utils.js"; const testMatrix = getTestMatrix(); for (const testOpts of testMatrix) { @@ -63,9 +63,9 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container } = await client.getContainer(containerId, schema, "2")); + ({ container } = await client.getContainer(containerId, schema, pkgVersion)); } else { - ({ container } = await client.createContainer(schema, "2")); + ({ container } = await client.createContainer(schema, pkgVersion)); containerId = await container.attach(); } @@ -126,7 +126,7 @@ for (const testOpts of testMatrix) { ); containerId = getContainerIdFromPayloadResponse(containerResponse); } else { - ({ container } = await client.createContainer(schema, "2")); + ({ container } = await client.createContainer(schema, pkgVersion)); containerId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { @@ -143,7 +143,7 @@ for (const testOpts of testMatrix) { containerId, schema, versions[0], - "2", + pkgVersion, ); await assert.doesNotReject(viewContainerVersionAttempt); const { container: containerView } = await viewContainerVersionAttempt; @@ -169,10 +169,10 @@ for (const testOpts of testMatrix) { "test-user-name-1", ); containerId = getContainerIdFromPayloadResponse(containerResponse); - ({ container } = await client.getContainer(containerId, schema, "2")); + ({ container } = await client.getContainer(containerId, schema, pkgVersion)); map1 = container.initialObjects.map1 as SharedMap; } else { - ({ container } = await client.createContainer(schema, "2")); + ({ container } = await client.createContainer(schema, pkgVersion)); map1 = container.initialObjects.map1 as SharedMap; map1.set(testKey, expectedValue); @@ -197,7 +197,7 @@ for (const testOpts of testMatrix) { containerId, schema, versions[versions.length - 1], - "2", + pkgVersion, ); await assert.doesNotReject(viewContainerVersionAttempt); const { container: containerView } = await viewContainerVersionAttempt; @@ -216,7 +216,7 @@ for (const testOpts of testMatrix) { { id: "whatever", }, - "2", + pkgVersion, ); const errorFn = (error: Error): boolean => { assert.notStrictEqual(error.message, undefined, "Azure Client error is undefined"); diff --git a/packages/service-clients/end-to-end-tests/odsp-client/package.json b/packages/service-clients/end-to-end-tests/odsp-client/package.json index bf0542a4e9de..de666ecea342 100644 --- a/packages/service-clients/end-to-end-tests/odsp-client/package.json +++ b/packages/service-clients/end-to-end-tests/odsp-client/package.json @@ -15,6 +15,7 @@ "scripts": { "build": "fluid-build . --task build", "build:compile": "fluid-build . --task compile", + "build:genver": "gen-version", "build:test": "tsc --project ./src/test/tsconfig.json", "check:biome": "biome check .", "check:format": "npm run check:biome", diff --git a/packages/service-clients/end-to-end-tests/odsp-client/src/packageVersion.ts b/packages/service-clients/end-to-end-tests/odsp-client/src/packageVersion.ts new file mode 100644 index 000000000000..64181dcb5702 --- /dev/null +++ b/packages/service-clients/end-to-end-tests/odsp-client/src/packageVersion.ts @@ -0,0 +1,9 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + * + * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY + */ + +export const pkgName = "@fluid-experimental/odsp-end-to-end-tests"; +export const pkgVersion = "2.102.0"; diff --git a/packages/service-clients/end-to-end-tests/odsp-client/src/test/containerCreate.spec.ts b/packages/service-clients/end-to-end-tests/odsp-client/src/test/containerCreate.spec.ts index 24cf43a4f46a..dad5e2eff2d0 100644 --- a/packages/service-clients/end-to-end-tests/odsp-client/src/test/containerCreate.spec.ts +++ b/packages/service-clients/end-to-end-tests/odsp-client/src/test/containerCreate.spec.ts @@ -12,6 +12,8 @@ import { SharedMap } from "@fluidframework/map/internal"; import type { OdspClient } from "@fluidframework/odsp-client/internal"; import { timeoutPromise } from "@fluidframework/test-utils/internal"; +import { pkgVersion } from "../packageVersion.js"; + import { createOdspClient, getCredentials } from "./OdspClientFactory.js"; describe("Container create scenarios", () => { @@ -42,7 +44,7 @@ describe("Container create scenarios", () => { * be returned. */ it("Created container is detached", async () => { - const { container } = await client.createContainer(schema); + const { container } = await client.createContainer(schema, pkgVersion); assert.strictEqual( container.attachState, AttachState.Detached, @@ -61,7 +63,7 @@ describe("Container create scenarios", () => { * be returned. */ it("can attach a container", async () => { - const { container } = await client.createContainer(schema); + const { container } = await client.createContainer(schema, pkgVersion); const itemId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { @@ -86,7 +88,7 @@ describe("Container create scenarios", () => { * be returned. */ it("cannot attach a container twice", async () => { - const { container } = await client.createContainer(schema); + const { container } = await client.createContainer(schema, pkgVersion); const itemId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { @@ -112,7 +114,7 @@ describe("Container create scenarios", () => { * be returned. */ it("can retrieve existing ODSP container successfully", async () => { - const { container: newContainer } = await client.createContainer(schema); + const { container: newContainer } = await client.createContainer(schema, pkgVersion); const itemId = await newContainer.attach(); if (newContainer.connectionState !== ConnectionState.Connected) { @@ -122,7 +124,7 @@ describe("Container create scenarios", () => { }); } - const resources = client.getContainer(itemId, schema); + const resources = client.getContainer(itemId, schema, pkgVersion); await assert.doesNotReject( resources, () => true, @@ -136,7 +138,7 @@ describe("Container create scenarios", () => { * Expected behavior: an error should be thrown when trying to get a non-existent container. */ it("cannot load improperly created container (cannot load a non-existent container)", async () => { - const containerAndServicesP = client.getContainer("containerConfig", schema); + const containerAndServicesP = client.getContainer("containerConfig", schema, pkgVersion); const errorFn = (error: Error): boolean => { assert.notStrictEqual(error.message, undefined, "Odsp Client error is undefined"); diff --git a/packages/service-clients/end-to-end-tests/odsp-client/src/test/tsconfig.json b/packages/service-clients/end-to-end-tests/odsp-client/src/test/tsconfig.json index 3c20a3790638..46f4c2894afe 100644 --- a/packages/service-clients/end-to-end-tests/odsp-client/src/test/tsconfig.json +++ b/packages/service-clients/end-to-end-tests/odsp-client/src/test/tsconfig.json @@ -1,10 +1,10 @@ { "extends": "../../../../../../common/build/build-common/tsconfig.test.node16.json", "compilerOptions": { - "rootDir": "./", - "outDir": "../../lib/test", + "rootDir": "../", + "outDir": "../../lib", "types": ["mocha", "node"], "exactOptionalPropertyTypes": false, }, - "include": ["./**/*"], + "include": ["./**/*", "../packageVersion.ts"], } diff --git a/packages/service-clients/odsp-client/README.md b/packages/service-clients/odsp-client/README.md index 42e187eb7476..8a7f3073e4fb 100644 --- a/packages/service-clients/odsp-client/README.md +++ b/packages/service-clients/odsp-client/README.md @@ -104,7 +104,7 @@ const containerSchema = { ], }; const odspClient = new OdspClient(clientProps); -const { container, services } = await odspClient.createContainer(containerSchema); +const { container, services } = await odspClient.createContainer(containerSchema, "2.0.0"); const itemId = await container.attach(); ``` @@ -117,7 +117,7 @@ Using the `OdspClient` class the developer can create and get Fluid containers. import { OdspClient } from "@fluidframework/odsp-client"; const odspClient = new OdspClient(props); -const { container, services } = await odspClient.getContainer("_unique-itemId_", schema); +const { container, services } = await odspClient.getContainer("_unique-itemId_", schema, "2.0.0"); ``` ## Using initial objects @@ -137,7 +137,7 @@ const schema = { }; // Fetch back the container that had been created earlier with the same itemId and schema -const { container, services } = await OdspClient.getContainer("_unique-itemId_", schema); +const { container, services } = await OdspClient.getContainer("_unique-itemId_", schema, "2.0.0"); // Get our list of initial objects that we had defined in the schema. initialObjects here will have the same signature const initialObjects = container.initialObjects; 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 0c9ed9b7c689..5ef196e7c7b9 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 @@ -28,12 +28,20 @@ export interface IOdspTokenProvider { // @beta @sealed export class OdspClient { constructor(properties: OdspClientProps); - // (undocumented) + createContainer(containerSchema: T, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IOdspFluidContainer; + services: OdspContainerServices; + }>; + // @deprecated createContainer(containerSchema: T): Promise<{ container: IOdspFluidContainer; services: OdspContainerServices; }>; - // (undocumented) + getContainer(id: string, containerSchema: T, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IOdspFluidContainer; + services: OdspContainerServices; + }>; + // @deprecated getContainer(id: string, containerSchema: T): Promise<{ container: IOdspFluidContainer; services: OdspContainerServices; 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 5ff2a629caca..05f9098d6dab 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 @@ -28,12 +28,20 @@ export interface IOdspTokenProvider { // @beta @sealed export class OdspClient { constructor(properties: OdspClientProps); - // (undocumented) + createContainer(containerSchema: T, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IOdspFluidContainer; + services: OdspContainerServices; + }>; + // @deprecated createContainer(containerSchema: T): Promise<{ container: IOdspFluidContainer; services: OdspContainerServices; }>; - // (undocumented) + getContainer(id: string, containerSchema: T, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IOdspFluidContainer; + services: OdspContainerServices; + }>; + // @deprecated getContainer(id: string, containerSchema: T): Promise<{ container: IOdspFluidContainer; services: OdspContainerServices; diff --git a/packages/service-clients/odsp-client/package.json b/packages/service-clients/odsp-client/package.json index 844f2be04a08..4174cc664f77 100644 --- a/packages/service-clients/odsp-client/package.json +++ b/packages/service-clients/odsp-client/package.json @@ -115,6 +115,7 @@ "@fluidframework/odsp-doclib-utils": "workspace:~", "@fluidframework/odsp-driver": "workspace:~", "@fluidframework/odsp-driver-definitions": "workspace:~", + "@fluidframework/runtime-definitions": "workspace:~", "@fluidframework/telemetry-utils": "workspace:~", "uuid": "^11.1.0" }, diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index db8690c54095..d661ab93309f 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -34,6 +34,7 @@ import { isOdspResolvedUrl, } from "@fluidframework/odsp-driver/internal"; import type { OdspResourceTokenFetchOptions } from "@fluidframework/odsp-driver-definitions/internal"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions"; import { wrapConfigProviderWithDefaults } from "@fluidframework/telemetry-utils/internal"; import { v4 as uuid } from "uuid"; @@ -111,13 +112,41 @@ export class OdspClient { this.configProvider = wrapConfigProvider(properties.configProvider); } + /** + * Creates a new detached container instance backed by ODSP. + * @param containerSchema - Container schema for the new container. + * @param minVersionForCollab - Minimum Fluid Framework version required for collaboration, as a + * `MinimumVersionForCollab` semver string (e.g. `"2.0.0"`). + * @returns New detached container instance along with associated services. + */ public async createContainer( containerSchema: T, + minVersionForCollab: MinimumVersionForCollab, + ): Promise<{ + container: IOdspFluidContainer; + services: IOdspContainerServices; + }>; + /** + * Creates a new detached container instance backed by ODSP. + * @param containerSchema - Container schema for the new container. + * @returns New detached container instance along with associated services. + * @deprecated Pass a `MinimumVersionForCollab` semver string (e.g. `"2.0.0"`) as a second argument. + * The previous behavior was equivalent to passing `"2.0.0"`. + */ + public async createContainer( + containerSchema: T, + ): Promise<{ + container: IOdspFluidContainer; + services: IOdspContainerServices; + }>; + public async createContainer( + containerSchema: T, + minVersionForCollab: MinimumVersionForCollab = "2.0.0", ): Promise<{ container: IOdspFluidContainer; services: IOdspContainerServices; }> { - const loaderProps = this.getLoaderProps(containerSchema); + const loaderProps = this.getLoaderProps(containerSchema, minVersionForCollab); const container = await createDetachedContainer({ ...loaderProps, @@ -137,14 +166,46 @@ export class OdspClient { return { container: fluidContainer, services }; } + /** + * Accesses an existing container by its unique ID in ODSP. + * @param id - Unique ID of the container in ODSP. + * @param containerSchema - Container schema used to access data objects in the container. + * @param minVersionForCollab - Minimum Fluid Framework version required for collaboration, as a + * `MinimumVersionForCollab` semver string (e.g. `"2.0.0"`). + * @returns Existing container instance along with associated services. + */ + public async getContainer( + id: string, + containerSchema: T, + minVersionForCollab: MinimumVersionForCollab, + ): Promise<{ + container: IOdspFluidContainer; + services: IOdspContainerServices; + }>; + /** + * Accesses an existing container by its unique ID in ODSP. + * @param id - Unique ID of the container in ODSP. + * @param containerSchema - Container schema used to access data objects in the container. + * @returns Existing container instance along with associated services. + * @deprecated Pass a `MinimumVersionForCollab` semver string (e.g. `"2.0.0"`) as a third argument. + * The previous behavior was equivalent to passing `"2.0.0"`. + */ + public async getContainer( + id: string, + containerSchema: T, + ): Promise<{ + container: IOdspFluidContainer; + services: IOdspContainerServices; + }>; public async getContainer( id: string, containerSchema: T, + minVersionForCollab: MinimumVersionForCollab = "2.0.0", ): Promise<{ container: IOdspFluidContainer; services: IOdspContainerServices; }> { - const loaderProps = this.getLoaderProps(containerSchema); + const loaderProps = this.getLoaderProps(containerSchema, minVersionForCollab); const url = createOdspUrl({ siteUrl: this.connectionConfig.siteUrl, driveId: this.connectionConfig.driveId, @@ -163,10 +224,13 @@ export class OdspClient { return { container: fluidContainer, services }; } - private getLoaderProps(schema: ContainerSchema): ILoaderProps { + private getLoaderProps( + schema: ContainerSchema, + minVersionForCollab: MinimumVersionForCollab, + ): ILoaderProps { const runtimeFactory = createDOProviderContainerRuntimeFactory({ schema, - compatibilityMode: "2", + compatibilityMode: minVersionForCollab, }); const load = async (): Promise => { return { diff --git a/packages/service-clients/odsp-client/src/test/odspClient.spec.ts b/packages/service-clients/odsp-client/src/test/odspClient.spec.ts index 2d613b44ea1b..58454355b300 100644 --- a/packages/service-clients/odsp-client/src/test/odspClient.spec.ts +++ b/packages/service-clients/odsp-client/src/test/odspClient.spec.ts @@ -75,7 +75,7 @@ describe("OdspClient", () => { * be returned. */ it("can create new ODSP container successfully", async () => { - const resourcesP = client.createContainer(schema); + const resourcesP = client.createContainer(schema, "2.0.0"); await assert.doesNotReject(resourcesP, () => true, "container cannot be created in ODSP"); }); @@ -88,7 +88,7 @@ describe("OdspClient", () => { * be returned. */ it("created container is detached", async () => { - const { container } = await client.createContainer(schema); + const { container } = await client.createContainer(schema, "2.0.0"); assert.strictEqual( container.attachState, AttachState.Detached, @@ -97,7 +97,10 @@ describe("OdspClient", () => { }); it("GC is disabled by default", async () => { - const { container: container_defaultConfig } = await client.createContainer(schema); + const { container: container_defaultConfig } = await client.createContainer( + schema, + "2.0.0", + ); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { sweepEnabled, throwOnTombstoneLoad } = // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access diff --git a/packages/service-clients/tinylicious-client/api-report/tinylicious-client.alpha.api.md b/packages/service-clients/tinylicious-client/api-report/tinylicious-client.alpha.api.md index 52ea8da37ab4..05fbf2875875 100644 --- a/packages/service-clients/tinylicious-client/api-report/tinylicious-client.alpha.api.md +++ b/packages/service-clients/tinylicious-client/api-report/tinylicious-client.alpha.api.md @@ -12,10 +12,20 @@ export type ITinyliciousAudience = IServiceAudience; // @public @sealed export class TinyliciousClient { constructor(properties?: TinyliciousClientProps); + createContainer(containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: TinyliciousContainerServices; + }>; + // @deprecated createContainer(containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: TinyliciousContainerServices; }>; + getContainer(id: string, containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: TinyliciousContainerServices; + }>; + // @deprecated getContainer(id: string, containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: TinyliciousContainerServices; diff --git a/packages/service-clients/tinylicious-client/api-report/tinylicious-client.beta.api.md b/packages/service-clients/tinylicious-client/api-report/tinylicious-client.beta.api.md index d6facf41cd8a..0c44007d5449 100644 --- a/packages/service-clients/tinylicious-client/api-report/tinylicious-client.beta.api.md +++ b/packages/service-clients/tinylicious-client/api-report/tinylicious-client.beta.api.md @@ -12,10 +12,20 @@ export type ITinyliciousAudience = IServiceAudience; // @public @sealed export class TinyliciousClient { constructor(properties?: TinyliciousClientProps); + createContainer(containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: TinyliciousContainerServices; + }>; + // @deprecated createContainer(containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: TinyliciousContainerServices; }>; + getContainer(id: string, containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: TinyliciousContainerServices; + }>; + // @deprecated getContainer(id: string, containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: TinyliciousContainerServices; diff --git a/packages/service-clients/tinylicious-client/api-report/tinylicious-client.public.api.md b/packages/service-clients/tinylicious-client/api-report/tinylicious-client.public.api.md index a9336956c0da..359643136b38 100644 --- a/packages/service-clients/tinylicious-client/api-report/tinylicious-client.public.api.md +++ b/packages/service-clients/tinylicious-client/api-report/tinylicious-client.public.api.md @@ -12,10 +12,20 @@ export type ITinyliciousAudience = IServiceAudience; // @public @sealed export class TinyliciousClient { constructor(properties?: TinyliciousClientProps); + createContainer(containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: TinyliciousContainerServices; + }>; + // @deprecated createContainer(containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: TinyliciousContainerServices; }>; + getContainer(id: string, containerSchema: TContainerSchema, minVersionForCollab: MinimumVersionForCollab): Promise<{ + container: IFluidContainer; + services: TinyliciousContainerServices; + }>; + // @deprecated getContainer(id: string, containerSchema: TContainerSchema, compatibilityMode: CompatibilityMode): Promise<{ container: IFluidContainer; services: TinyliciousContainerServices; diff --git a/packages/service-clients/tinylicious-client/package.json b/packages/service-clients/tinylicious-client/package.json index a1e3071e8804..0a2b3d2e363d 100644 --- a/packages/service-clients/tinylicious-client/package.json +++ b/packages/service-clients/tinylicious-client/package.json @@ -93,6 +93,7 @@ "@fluidframework/fluid-static": "workspace:~", "@fluidframework/map": "workspace:~", "@fluidframework/routerlicious-driver": "workspace:~", + "@fluidframework/runtime-definitions": "workspace:~", "@fluidframework/runtime-utils": "workspace:~", "@fluidframework/telemetry-utils": "workspace:~", "@fluidframework/tinylicious-driver": "workspace:~" diff --git a/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts b/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts index 8e30c30194fc..c070d172b252 100644 --- a/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts +++ b/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts @@ -22,14 +22,17 @@ import type { import type { ContainerSchema, IFluidContainer, + // eslint-disable-next-line import-x/no-deprecated CompatibilityMode, } from "@fluidframework/fluid-static"; import { createDOProviderContainerRuntimeFactory, createFluidContainer, createServiceAudience, + resolveCompatibilityModeToMinVersionForCollab, } from "@fluidframework/fluid-static/internal"; import { RouterliciousDocumentServiceFactory } from "@fluidframework/routerlicious-driver/internal"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions"; import { wrapConfigProviderWithDefaults } from "@fluidframework/telemetry-utils/internal"; import { InsecureTinyliciousTokenProvider, @@ -72,17 +75,45 @@ export class TinyliciousClient { /** * Creates a new detached container instance in Tinylicious server. * @param containerSchema - Container schema for the new container. - * @param compatibilityMode - Compatibility mode the container should run in. + * @param minVersionForCollab - Minimum framework version required for collaboration, as a + * `MinimumVersionForCollab` semver string (e.g. `"1.0.0"`, `"2.0.0"`). * @returns New detached container instance along with associated services. */ public async createContainer( containerSchema: TContainerSchema, + minVersionForCollab: MinimumVersionForCollab, + ): Promise<{ + container: IFluidContainer; + services: TinyliciousContainerServices; + }>; + /** + * Creates a new detached container instance in Tinylicious server. + * @param containerSchema - Container schema for the new container. + * @param compatibilityMode - Legacy {@link @fluidframework/fluid-static#CompatibilityMode} value. + * @returns New detached container instance along with associated services. + * @deprecated Pass a `MinimumVersionForCollab` semver string (e.g. `"2.0.0"`) instead. The legacy + * values `"1"` and `"2"` correspond to `"1.0.0"` and `"2.0.0"` respectively. + */ + public async createContainer( + containerSchema: TContainerSchema, + // eslint-disable-next-line import-x/no-deprecated compatibilityMode: CompatibilityMode, ): Promise<{ container: IFluidContainer; services: TinyliciousContainerServices; + }>; + public async createContainer( + containerSchema: TContainerSchema, + // eslint-disable-next-line import-x/no-deprecated + compatibilityMode: MinimumVersionForCollab | CompatibilityMode, + ): Promise<{ + container: IFluidContainer; + services: TinyliciousContainerServices; }> { - const loaderProps = this.getLoaderProps(containerSchema, compatibilityMode); + const loaderProps = this.getLoaderProps( + containerSchema, + resolveCompatibilityModeToMinVersionForCollab(compatibilityMode), + ); // We're not actually using the code proposal (our code loader always loads the same module // regardless of the proposal), but the Container will only give us a NullRuntime if there's @@ -123,18 +154,49 @@ export class TinyliciousClient { * Accesses the existing container given its unique ID in the tinylicious server. * @param id - Unique ID of the container. * @param containerSchema - Container schema used to access data objects in the container. - * @param compatibilityMode - Compatibility mode the container should run in. + * @param minVersionForCollab - Minimum Fluid Framework version required for collaboration, as a + * `MinimumVersionForCollab` semver string (e.g. `"1.0.0"`, `"2.0.0"`). + * @returns Existing container instance along with associated services. + */ + public async getContainer( + id: string, + containerSchema: TContainerSchema, + minVersionForCollab: MinimumVersionForCollab, + ): Promise<{ + container: IFluidContainer; + services: TinyliciousContainerServices; + }>; + /** + * Accesses the existing container given its unique ID in the tinylicious server. + * @param id - Unique ID of the container. + * @param containerSchema - Container schema used to access data objects in the container. + * @param compatibilityMode - Legacy {@link @fluidframework/fluid-static#CompatibilityMode} value. * @returns Existing container instance along with associated services. + * @deprecated Pass a `MinimumVersionForCollab` semver string (e.g. `"2.0.0"`) instead. The legacy + * values `"1"` and `"2"` correspond to `"1.0.0"` and `"2.0.0"` respectively. */ public async getContainer( id: string, containerSchema: TContainerSchema, + // eslint-disable-next-line import-x/no-deprecated compatibilityMode: CompatibilityMode, ): Promise<{ container: IFluidContainer; services: TinyliciousContainerServices; + }>; + public async getContainer( + id: string, + containerSchema: TContainerSchema, + // eslint-disable-next-line import-x/no-deprecated + compatibilityMode: MinimumVersionForCollab | CompatibilityMode, + ): Promise<{ + container: IFluidContainer; + services: TinyliciousContainerServices; }> { - const loaderProps = this.getLoaderProps(containerSchema, compatibilityMode); + const loaderProps = this.getLoaderProps( + containerSchema, + resolveCompatibilityModeToMinVersionForCollab(compatibilityMode), + ); const container = await loadExistingContainer({ ...loaderProps, request: { url: id } }); const fluidContainer = await createFluidContainer({ container, @@ -155,11 +217,11 @@ export class TinyliciousClient { private getLoaderProps( schema: ContainerSchema, - compatibilityMode: CompatibilityMode, + minVersionForCollab: MinimumVersionForCollab, ): ILoaderProps { const containerRuntimeFactory = createDOProviderContainerRuntimeFactory({ schema, - compatibilityMode, + compatibilityMode: minVersionForCollab, }); const load = async (): Promise => { return { diff --git a/packages/service-clients/tinylicious-client/src/interfaces.ts b/packages/service-clients/tinylicious-client/src/interfaces.ts index ed82d94deee9..718371f170a7 100644 --- a/packages/service-clients/tinylicious-client/src/interfaces.ts +++ b/packages/service-clients/tinylicious-client/src/interfaces.ts @@ -64,7 +64,7 @@ export interface TinyliciousConnectionConfig { * Any functionality regarding how the data is handled within the FluidContainer itself (e.g., which data objects or * DDSes to use) will not be included here but rather on the FluidContainer class itself. * - * Returned by {@link TinyliciousClient.createContainer} and {@link TinyliciousClient.getContainer} alongside the FluidContainer. + * Returned by `TinyliciousClient.createContainer` and `TinyliciousClient.getContainer` alongside the FluidContainer. * * @sealed * @public diff --git a/packages/service-clients/tinylicious-client/src/test/TinyliciousClient.spec.ts b/packages/service-clients/tinylicious-client/src/test/TinyliciousClient.spec.ts index e90f44a20564..26ed4173d9c0 100644 --- a/packages/service-clients/tinylicious-client/src/test/TinyliciousClient.spec.ts +++ b/packages/service-clients/tinylicious-client/src/test/TinyliciousClient.spec.ts @@ -50,7 +50,7 @@ const waitForDataCorruption = async (container: IFluidContainer): Promise }), ); -for (const compatibilityMode of ["1", "2"] as const) { +for (const compatibilityMode of ["1.0.0", "2.0.0"] as const) { describe(`TinyliciousClient (compatibilityMode: ${compatibilityMode})`, function () { let tinyliciousClient: TinyliciousClient; const schema = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 409ca0e27e27..1c31fc5cc311 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11466,6 +11466,9 @@ importers: '@fluidframework/tree': specifier: workspace:~ version: link:../../dds/tree + semver-ts: + specifier: ^1.0.3 + version: 1.0.3 devDependencies: '@arethetypeswrong/cli': specifier: ^0.18.2 @@ -13732,6 +13735,9 @@ importers: '@fluidframework/routerlicious-driver': specifier: workspace:~ version: link:../../drivers/routerlicious-driver + '@fluidframework/runtime-definitions': + specifier: workspace:~ + version: link:../../runtime/runtime-definitions '@fluidframework/telemetry-utils': specifier: workspace:~ version: link:../../utils/telemetry-utils @@ -14098,6 +14104,9 @@ importers: '@fluidframework/odsp-driver-definitions': specifier: workspace:~ version: link:../../drivers/odsp-driver-definitions + '@fluidframework/runtime-definitions': + specifier: workspace:~ + version: link:../../runtime/runtime-definitions '@fluidframework/telemetry-utils': specifier: workspace:~ version: link:../../utils/telemetry-utils @@ -14195,6 +14204,9 @@ importers: '@fluidframework/routerlicious-driver': specifier: workspace:~ version: link:../../drivers/routerlicious-driver + '@fluidframework/runtime-definitions': + specifier: workspace:~ + version: link:../../runtime/runtime-definitions '@fluidframework/runtime-utils': specifier: workspace:~ version: link:../../runtime/runtime-utils