From cdc304a5142fee67467770dec1d1ca6b7a195249 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Mon, 3 Mar 2025 10:40:31 -0800 Subject: [PATCH 1/2] Switch to handle based api --- .../container-runtime/src/containerRuntime.ts | 56 ++++++++++++++----- .../src/dataStoreRuntime.ts | 2 + .../runtime/datastore/src/dataStoreRuntime.ts | 4 ++ .../src/dataStoreContext.ts | 14 +++-- .../runtime/runtime-definitions/src/index.ts | 2 +- .../runtime/test-runtime-utils/src/mocks.ts | 1 + .../src/test/stagingMode.spec.ts | 15 +++-- 7 files changed, 70 insertions(+), 24 deletions(-) diff --git a/packages/runtime/container-runtime/src/containerRuntime.ts b/packages/runtime/container-runtime/src/containerRuntime.ts index 84c8e18816f5..70d6ee1dd0a8 100644 --- a/packages/runtime/container-runtime/src/containerRuntime.ts +++ b/packages/runtime/container-runtime/src/containerRuntime.ts @@ -37,6 +37,7 @@ import { IResponse, ITelemetryBaseLogger, } from "@fluidframework/core-interfaces"; +import type { ErasedType } from "@fluidframework/core-interfaces"; import { type IErrorBase, IFluidHandleContext, @@ -50,6 +51,7 @@ import { LazyPromise, PromiseCache, delay, + unreachableCase, } from "@fluidframework/core-utils/internal"; import { IClientDetails, @@ -98,7 +100,6 @@ import { IInboundSignalMessage, type IRuntimeMessagesContent, type ISummarizerNodeWithGC, - type StageControls, } from "@fluidframework/runtime-definitions/internal"; import { GCDataBuilder, @@ -3479,25 +3480,52 @@ export class ContainerRuntime return result; } - enterStagingMode = (): StageControls => { - const checkpoint = this.outbox.getBatchCheckpoints(true); - const branchInfo = { - discardChanges: () => { + private stagingModeHandle?: { checkpoint: ReturnType }; + public get inStagingMode(): boolean { + return this.stagingModeHandle !== undefined; + } + enterStagingMode = (): ErasedType<"StagingModeHandle"> => { + if (this.stagingModeHandle !== undefined) { + throw new Error("Already in staging mode"); + } + this.stagingModeHandle = { + checkpoint: this.outbox.getBatchCheckpoints(true), + }; + return this.stagingModeHandle as unknown as ErasedType<"StagingModeHandle">; + }; + + exitStagingMode = ( + handle: ErasedType<"StagingModeHandle">, + arg: { type: "accept" } | { type: "reject" }, + ): void => { + if (this.stagingModeHandle === undefined) { + throw new Error("Must be in staging mode"); + } + if ((this.stagingModeHandle as unknown) !== handle) { + throw new Error("Invalid StagingModeHandle"); + } + const { checkpoint } = this.stagingModeHandle; + this.stagingModeHandle = undefined; + const { type } = arg; + switch (type) { + case "accept": { + checkpoint.unblockFlush(); + this.outbox.flush(); + return; + } + case "reject": { assert( checkpoint.blobAttachBatch.isEmpty() && checkpoint.idAllocationBatch.isEmpty(), "other batches must be empty", ); - checkpoint.mainBatch.rollback(); checkpoint.unblockFlush(); - }, - commitChanges: () => { - checkpoint.unblockFlush(); - this.outbox.flush(); - }, - }; - - return branchInfo; + return; + } + default: { + unreachableCase(type); + } + } }; /** diff --git a/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts b/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts index 59d9c6315a8c..7af5bd996228 100644 --- a/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts @@ -76,6 +76,8 @@ export interface IFluidDataStoreRuntime */ readonly attachState: AttachState; + get inStagingMode(): boolean; + readonly idCompressor: IIdCompressor | undefined; /** diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index 1f031d022cbd..898e070741af 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -135,6 +135,10 @@ export class FluidDataStoreRuntime return this.dataStoreContext.connected; } + get inStagingMode(): boolean { + return this.dataStoreContext.containerRuntime.inStagingMode; + } + public get clientId(): string | undefined { return this.dataStoreContext.clientId; } diff --git a/packages/runtime/runtime-definitions/src/dataStoreContext.ts b/packages/runtime/runtime-definitions/src/dataStoreContext.ts index ee52d58722a0..41e6e61d595a 100644 --- a/packages/runtime/runtime-definitions/src/dataStoreContext.ts +++ b/packages/runtime/runtime-definitions/src/dataStoreContext.ts @@ -16,6 +16,7 @@ import type { ITelemetryBaseLogger, } from "@fluidframework/core-interfaces"; import type { + ErasedType, IFluidHandleInternal, IProvideFluidHandleContext, } from "@fluidframework/core-interfaces/internal"; @@ -193,10 +194,7 @@ export interface IDataStore { * @alpha * @sealed */ -export interface StageControls { - readonly commitChanges: () => void; - readonly discardChanges: () => void; -} +export type StagingModeHandle = ErasedType<"StagingModeHandle">; /** * A reduced set of functionality of IContainerRuntime that a data store context/data store runtime will need @@ -218,7 +216,13 @@ export interface IContainerRuntimeBase extends IEventProvider void): void; - readonly enterStagingMode: () => StageControls; + get inStagingMode(): boolean; + readonly enterStagingMode: () => StagingModeHandle; + + readonly exitStagingMode: ( + handle: StagingModeHandle, + arg: { type: "accept" } | { type: "reject" }, + ) => void; /** * Submits a container runtime level signal to be sent to other clients. * @param type - Type of the signal. diff --git a/packages/runtime/runtime-definitions/src/index.ts b/packages/runtime/runtime-definitions/src/index.ts index 19acb59ebedc..952dcfd17732 100644 --- a/packages/runtime/runtime-definitions/src/index.ts +++ b/packages/runtime/runtime-definitions/src/index.ts @@ -21,7 +21,7 @@ export type { IFluidParentContext, IFluidDataStoreContextDetached, IPendingMessagesState, - StageControls, + StagingModeHandle, } from "./dataStoreContext.js"; export { FlushMode, FlushModeExperimental, VisibilityState } from "./dataStoreContext.js"; export type { IProvideFluidDataStoreFactory } from "./dataStoreFactory.js"; diff --git a/packages/runtime/test-runtime-utils/src/mocks.ts b/packages/runtime/test-runtime-utils/src/mocks.ts index 77e41b20471b..c9c6a6a527ec 100644 --- a/packages/runtime/test-runtime-utils/src/mocks.ts +++ b/packages/runtime/test-runtime-utils/src/mocks.ts @@ -855,6 +855,7 @@ export class MockFluidDataStoreRuntime public get objectsRoutingContext(): IFluidHandleContext { return this; } + public readonly inStagingMode = false; public readonly documentId: string = undefined as any; public readonly id: string; diff --git a/packages/test/local-server-tests/src/test/stagingMode.spec.ts b/packages/test/local-server-tests/src/test/stagingMode.spec.ts index cbd2a6b1e68c..9f666adfa8a3 100644 --- a/packages/test/local-server-tests/src/test/stagingMode.spec.ts +++ b/packages/test/local-server-tests/src/test/stagingMode.spec.ts @@ -16,6 +16,7 @@ import { } from "@fluidframework/container-loader/internal"; import { loadContainerRuntime } from "@fluidframework/container-runtime/internal"; import { type FluidObject } from "@fluidframework/core-interfaces/internal"; +import type { StagingModeHandle } from "@fluidframework/runtime-definitions/internal"; import { LocalDeltaConnectionServer, type ILocalDeltaConnectionServer, @@ -53,6 +54,12 @@ class RootDataObject extends DataObject { public enterStagingMode() { return this.context.containerRuntime.enterStagingMode(); } + public exitStagingMode(handle: StagingModeHandle, accept: boolean) { + return this.context.containerRuntime.exitStagingMode( + handle, + accept ? { type: "accept" } : { type: "reject" }, + ); + } } /** @@ -175,7 +182,7 @@ describe("Scenario Test", () => { const deltaConnectionServer = LocalDeltaConnectionServer.create(); const clients = await createClients(deltaConnectionServer); - const branchData = clients.original.dataObject.enterStagingMode(); + const stagingModeHandle = clients.original.dataObject.enterStagingMode(); assert.deepStrictEqual( clients.original.dataObject.state, clients.loaded.dataObject.state, @@ -210,7 +217,7 @@ describe("Scenario Test", () => { "Expected mainline change to reach branch", ); - branchData.commitChanges(); + clients.original.dataObject.exitStagingMode(stagingModeHandle, true); await waitForSave(clients); @@ -225,7 +232,7 @@ describe("Scenario Test", () => { const deltaConnectionServer = LocalDeltaConnectionServer.create(); const clients = await createClients(deltaConnectionServer); - const branchData = clients.original.dataObject.enterStagingMode(); + const stagingModeHandle = clients.original.dataObject.enterStagingMode(); assert.deepStrictEqual( clients.original.dataObject.state, clients.loaded.dataObject.state, @@ -260,7 +267,7 @@ describe("Scenario Test", () => { "states should match after save", ); - branchData.discardChanges(); + clients.original.dataObject.exitStagingMode(stagingModeHandle, false); await waitForSave(clients); From fa56816138d896eec64efd41635378333b0e37a8 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Tue, 4 Mar 2025 12:27:05 -0800 Subject: [PATCH 2/2] generate docs --- .../datastore-definitions.legacy.alpha.api.md | 2 ++ .../api-report/datastore.legacy.alpha.api.md | 2 ++ .../runtime-definitions.legacy.alpha.api.md | 17 ++++++++++------- .../test-runtime-utils.legacy.alpha.api.md | 2 ++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/runtime/datastore-definitions/api-report/datastore-definitions.legacy.alpha.api.md b/packages/runtime/datastore-definitions/api-report/datastore-definitions.legacy.alpha.api.md index c7f4d5ef2efd..1ddd187c8202 100644 --- a/packages/runtime/datastore-definitions/api-report/datastore-definitions.legacy.alpha.api.md +++ b/packages/runtime/datastore-definitions/api-report/datastore-definitions.legacy.alpha.api.md @@ -94,6 +94,8 @@ export interface IFluidDataStoreRuntime extends IEventProvider StageControls; + readonly enterStagingMode: () => StagingModeHandle; + // (undocumented) + readonly exitStagingMode: (handle: StagingModeHandle, arg: { + type: "accept"; + } | { + type: "reject"; + }) => void; generateDocumentUniqueId(): number | string; getAbsoluteUrl(relativeUrl: string): Promise; getAliasedDataStoreEntryPoint(alias: string): Promise | undefined>; @@ -86,6 +92,8 @@ export interface IContainerRuntimeBase extends IEventProvider; + // (undocumented) + get inStagingMode(): boolean; orderSequentially(callback: () => void): void; submitSignal: (type: string, content: unknown, targetClientId?: string) => void; // (undocumented) @@ -397,12 +405,7 @@ export interface OpAttributionKey { } // @alpha @sealed (undocumented) -export interface StageControls { - // (undocumented) - readonly commitChanges: () => void; - // (undocumented) - readonly discardChanges: () => void; -} +export type StagingModeHandle = ErasedType<"StagingModeHandle">; // @alpha (undocumented) export type SummarizeInternalFn = (fullTree: boolean, trackState: boolean, telemetryContext?: ITelemetryContext, incrementalSummaryContext?: IExperimentalIncrementalSummaryContext) => Promise; diff --git a/packages/runtime/test-runtime-utils/api-report/test-runtime-utils.legacy.alpha.api.md b/packages/runtime/test-runtime-utils/api-report/test-runtime-utils.legacy.alpha.api.md index b82b7eea2cff..7dda0a95c8d6 100644 --- a/packages/runtime/test-runtime-utils/api-report/test-runtime-utils.legacy.alpha.api.md +++ b/packages/runtime/test-runtime-utils/api-report/test-runtime-utils.legacy.alpha.api.md @@ -442,6 +442,8 @@ export class MockFluidDataStoreRuntime extends EventEmitter implements IFluidDat // (undocumented) get IFluidHandleContext(): IFluidHandleContext; // (undocumented) + readonly inStagingMode = false; + // (undocumented) get isAttached(): boolean; // (undocumented) readonly loader: ILoader;