From 1147ba33c18a996b7a5349e23a2624b42c97d22f Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Mon, 21 Apr 2025 13:17:47 -0700 Subject: [PATCH 01/22] Runtime: Expose readonly directly off the datastore runtime --- .../src/channelCollection.ts | 25 +++ .../container-runtime/src/containerRuntime.ts | 11 +- .../container-runtime/src/dataStoreContext.ts | 71 ++++++- .../src/deltaManagerProxies.ts | 13 +- .../src/runtimeLayerCompatState.ts | 7 +- .../src/test/createChildDataStoreSync.spec.ts | 6 +- .../src/test/dataStoreContext.spec.ts | 4 + .../src/test/dataStoreCreation.spec.ts | 6 +- .../src/test/dataStoreCreationHelper.ts | 6 +- .../summary/summarizerClientElection.spec.ts | 2 +- .../datastore-definitions.legacy.alpha.api.md | 4 + .../src/dataStoreRuntime.ts | 3 + .../api-report/datastore.legacy.alpha.api.md | 4 + packages/runtime/datastore/package.json | 6 +- .../runtime/datastore/src/dataStoreRuntime.ts | 45 ++++- .../validateDatastorePrevious.generated.ts | 1 + .../runtime-definitions.legacy.alpha.api.md | 3 + .../src/dataStoreContext.ts | 6 + .../runtime/runtime-definitions/src/index.ts | 5 +- .../src/runtimeLayerCompatFeatureNames.ts | 6 + .../test-runtime-utils.legacy.alpha.api.md | 8 +- .../runtime/test-runtime-utils/package.json | 9 +- .../test-runtime-utils/src/mockDeltas.ts | 2 +- .../runtime/test-runtime-utils/src/mocks.ts | 5 + .../src/mocksDataStoreContext.ts | 5 +- ...idateTestRuntimeUtilsPrevious.generated.ts | 2 + .../src/test/readonly.spec.ts | 184 ++++++++++++++++++ 27 files changed, 428 insertions(+), 21 deletions(-) create mode 100644 packages/test/local-server-tests/src/test/readonly.spec.ts diff --git a/packages/runtime/container-runtime/src/channelCollection.ts b/packages/runtime/container-runtime/src/channelCollection.ts index 01255683c30c..917193f82275 100644 --- a/packages/runtime/container-runtime/src/channelCollection.ts +++ b/packages/runtime/container-runtime/src/channelCollection.ts @@ -149,6 +149,9 @@ export function wrapContext(context: IFluidParentContext): IFluidParentContext { get attachState() { return context.attachState; }, + get readonly() { + return context.readonly; + }, containerRuntime: context.containerRuntime, scope: context.scope, gcThrowOnTombstoneUsage: context.gcThrowOnTombstoneUsage, @@ -1116,6 +1119,28 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable { } } + public setReadOnlyState(readonly: boolean): void { + for (const [fluidDataStoreId, context] of this.contexts) { + try { + context.setReadOnlyState(readonly); + } catch (error) { + this.mc.logger.sendErrorEvent( + { + eventName: "SetReadOnlyStateError", + ...tagCodeArtifacts({ + fluidDataStoreId, + }), + details: { + runtimeReadonly: this.parentContext.readonly, + readonly, + }, + }, + error, + ); + } + } + } + public setAttachState(attachState: AttachState.Attaching | AttachState.Attached): void { for (const [, context] of this.contexts) { // Fire only for bounded stores. diff --git a/packages/runtime/container-runtime/src/containerRuntime.ts b/packages/runtime/container-runtime/src/containerRuntime.ts index 412895d71695..3dc6f6ad8fe2 100644 --- a/packages/runtime/container-runtime/src/containerRuntime.ts +++ b/packages/runtime/container-runtime/src/containerRuntime.ts @@ -255,7 +255,6 @@ import { idCompressorBlobName, metadataBlobName, rootHasIsolatedChannels, - summarizerClientType, wrapSummaryInChannelsTree, formCreateSummarizerFn, summarizerRequestUrl, @@ -267,6 +266,7 @@ import { ISummaryConfiguration, DefaultSummaryConfiguration, isSummariesDisabled, + summarizerClientType, } from "./summary/index.js"; import { Throttler, formExponentialFn } from "./throttler.js"; @@ -1119,6 +1119,10 @@ export class ContainerRuntime return this._getAttachState(); } + public get readonly(): boolean { + return this.deltaManager.readOnlyInfo.readonly === true; + } + /** * Current session schema - defines what options are on & off. * It's overlap of document schema (controlled by summary & ops) and options controlling this session. @@ -1736,6 +1740,7 @@ export class ContainerRuntime new Map(dataStoreAliasMap), async (runtime: ChannelCollection) => provideEntryPoint, ); + this._deltaManager.on("readonly", (readonly) => this.setReadOnlyState(readonly)); this.blobManager = new BlobManager({ routeContext: this.handleContext, @@ -2577,6 +2582,10 @@ export class ContainerRuntime return this._loadIdCompressor; } + public setReadOnlyState(readonly: boolean): void { + this.channelCollection.setReadOnlyState(readonly); + } + public setConnectionState(connected: boolean, clientId?: string): void { // Validate we have consistent state const currentClientId = this._audience.getSelf()?.clientId; diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 0d92b7a379ed..31ae8b867371 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -5,11 +5,17 @@ import { TypedEventEmitter, type ILayerCompatDetails } from "@fluid-internal/client-utils"; import { AttachState, IAudience } from "@fluidframework/container-definitions"; -import { IDeltaManager } from "@fluidframework/container-definitions/internal"; +import { + IDeltaManager, + isIDeltaManagerFull, + type IDeltaManagerFull, + type ReadOnlyInfo, +} from "@fluidframework/container-definitions/internal"; import { FluidObject, IDisposable, ITelemetryBaseProperties, + type IErrorBase, type IEvent, } from "@fluidframework/core-interfaces"; import { @@ -75,6 +81,7 @@ import { tagCodeArtifacts, } from "@fluidframework/telemetry-utils/internal"; +import { BaseDeltaManagerProxy } from "./deltaManagerProxies.js"; import { runtimeCompatDetailsForDataStore, validateDatastoreCompatibility, @@ -188,6 +195,50 @@ export interface IFluidDataStoreContextEvents extends IEvent { (event: "attaching" | "attached", listener: () => void); } +/** + * Eventually we should remove the delta manger from being exposed to Datastore runtimes via the context. However to remove that exposure we need to add new + * features, and those features themselves need forward and back compat. This proxy is here to enable that back compat. Each feature this proxy is used to + * support should be listed below, and as layer compat support goes away for those feature, we should also remove them from this proxy, with the eventual goal + * of completely remove this proxy. + * + * - Everything regarding readonly is to support older datastore runtimes which do not have the setReadonly function, so must get their readonly state via the delta manager. + * + */ +class ContextDeltaManagerProxy extends BaseDeltaManagerProxy { + constructor(base: IDeltaManagerFull) { + super(base, { + onReadonly: (readonly, reason): void => this.setReadonly(readonly, reason), + }); + this._readonly = base.readOnlyInfo.readonly; + } + + public get readOnlyInfo(): ReadOnlyInfo { + if (this._readonly === this.deltaManager.readOnlyInfo.readonly) { + return this.deltaManager.readOnlyInfo; + } else { + return this._readonly === true + ? { + readonly: true, + forced: false, + permissions: undefined, + storageOnly: false, + } + : { readonly: this._readonly }; + } + } + + private _readonly: boolean | undefined; + public setReadonly( + readonly: boolean, + readonlyConnectionReason?: { reason: string; error?: IErrorBase }, + ): void { + this._readonly = readonly; + if (this._readonly !== readonly) { + this.emit("readonly", readonly, readonlyConnectionReason); + } + } +} + /** * Represents the context for the store. This context is passed to the store runtime. * @internal @@ -217,8 +268,13 @@ export abstract class FluidDataStoreContext return this.parentContext.baseLogger; } + private readonly _deltaManager: ContextDeltaManagerProxy; public get deltaManager(): IDeltaManager { - return this.parentContext.deltaManager; + return this._deltaManager; + } + + public get readonly(): boolean | undefined { + return this.parentContext.readonly; } public get connected(): boolean { @@ -420,6 +476,10 @@ export abstract class FluidDataStoreContext // By default, a data store can log maximum 10 local changes telemetry in summarizer. this.localChangesTelemetryCount = this.mc.config.getNumber("Fluid.Telemetry.LocalChangesTelemetryCount") ?? 10; + + assert(isIDeltaManagerFull(this.parentContext.deltaManager), "Invalid delta manager"); + + this._deltaManager = new ContextDeltaManagerProxy(this.parentContext.deltaManager); } public dispose(): void { @@ -615,6 +675,13 @@ export abstract class FluidDataStoreContext this.channel!.setConnectionState(connected, clientId); } + public setReadOnlyState(readonly: boolean): void { + this.verifyNotClosed("setReadOnlyState", false /* checkTombstone */); + + this.channel?.setReadOnlyState?.(readonly); + this._deltaManager.setReadonly(readonly); + } + /** * Process messages for this data store. The messages here are contiguous messages for this data store in a batch. * @param messageCollection - The collection of messages to process. diff --git a/packages/runtime/container-runtime/src/deltaManagerProxies.ts b/packages/runtime/container-runtime/src/deltaManagerProxies.ts index 6e3efceb80d1..3dbf5e741721 100644 --- a/packages/runtime/container-runtime/src/deltaManagerProxies.ts +++ b/packages/runtime/container-runtime/src/deltaManagerProxies.ts @@ -99,7 +99,15 @@ export abstract class BaseDeltaManagerProxy return this.deltaManager.readOnlyInfo; } - constructor(protected readonly deltaManager: IDeltaManagerFull) { + constructor( + protected readonly deltaManager: IDeltaManagerFull, + overrides?: { + onReadonly?: ( + readonly: boolean, + readonlyConnectionReason?: { reason: string; error?: IErrorBase }, + ) => void; + }, + ) { super(); // We are expecting this class to have many listeners, so we suppress noisy "MaxListenersExceededWarning" logging. @@ -111,6 +119,9 @@ export abstract class BaseDeltaManagerProxy this.deltaManager.on("pong", this.onPong); this.deltaManager.on("connect", this.onConnect); this.deltaManager.on("disconnect", this.onDisconnect); + if (overrides?.onReadonly !== undefined) { + this.onReadonly = overrides.onReadonly; + } this.deltaManager.on("readonly", this.onReadonly); } diff --git a/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts b/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts index cb307e45d97b..d5c8a3dc33a9 100644 --- a/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts +++ b/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts @@ -9,7 +9,10 @@ import { type ILayerCompatSupportRequirements, } from "@fluid-internal/client-utils"; import type { ICriticalContainerError } from "@fluidframework/container-definitions"; -import { encodeHandlesInContainerRuntime } from "@fluidframework/runtime-definitions/internal"; +import { + encodeHandlesInContainerRuntime, + setReadonly, +} from "@fluidframework/runtime-definitions/internal"; import { UsageError } from "@fluidframework/telemetry-utils/internal"; import { pkgVersion } from "./packageVersion.js"; @@ -66,7 +69,7 @@ export const runtimeCompatDetailsForDataStore: ILayerCompatDetails = { /** * The features supported by the Runtime layer across the Runtime / DataStore boundary. */ - supportedFeatures: new Set([encodeHandlesInContainerRuntime]), + supportedFeatures: new Set([encodeHandlesInContainerRuntime, setReadonly]), }; /** diff --git a/packages/runtime/container-runtime/src/test/createChildDataStoreSync.spec.ts b/packages/runtime/container-runtime/src/test/createChildDataStoreSync.spec.ts index af092942961e..c3658ece9745 100644 --- a/packages/runtime/container-runtime/src/test/createChildDataStoreSync.spec.ts +++ b/packages/runtime/container-runtime/src/test/createChildDataStoreSync.spec.ts @@ -18,7 +18,10 @@ import { type ISummarizerNodeWithGC, } from "@fluidframework/runtime-definitions/internal"; import { isFluidError } from "@fluidframework/telemetry-utils/internal"; -import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils/internal"; +import { + MockDeltaManager, + MockFluidDataStoreRuntime, +} from "@fluidframework/test-runtime-utils/internal"; import { FluidDataStoreContext, @@ -89,6 +92,7 @@ describe("createChildDataStore", () => { }); }, } satisfies Partial as unknown as IContainerRuntimeBase, + deltaManager: new MockDeltaManager(), } satisfies Partial as unknown as IFluidParentContext; const context = new testContext( diff --git a/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts b/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts index 91e03300db7f..17b69b52f69e 100644 --- a/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts +++ b/packages/runtime/container-runtime/src/test/dataStoreContext.spec.ts @@ -47,6 +47,7 @@ import { isFluidError, } from "@fluidframework/telemetry-utils/internal"; import { + MockDeltaManager, MockFluidDataStoreRuntime, validateAssertionError, } from "@fluidframework/test-runtime-utils/internal"; @@ -245,6 +246,7 @@ describe("Data Store Context Tests", () => { parentContext = { IFluidDataStoreRegistry: registryWithSubRegistries, clientDetails: {} as unknown as IFluidParentContext["clientDetails"], + deltaManager: new MockDeltaManager(), } satisfies Partial as unknown as IFluidParentContext; localDataStoreContext = new LocalFluidDataStoreContext({ id: dataStoreId, @@ -541,6 +543,7 @@ describe("Data Store Context Tests", () => { IFluidDataStoreRegistry: registry, clientDetails: {} as unknown as IFluidParentContext["clientDetails"], containerRuntime: parentContext as unknown as IContainerRuntimeBase, + deltaManager: new MockDeltaManager(), } satisfies Partial as unknown as IFluidParentContext; }); @@ -1036,6 +1039,7 @@ describe("Data Store Context Tests", () => { IFluidDataStoreRegistry: registry, baseLogger: createChildLogger(), clientDetails: {} as unknown as IFluidParentContext["clientDetails"], + deltaManager: new MockDeltaManager(), } satisfies Partial as unknown as IFluidParentContext; }); diff --git a/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts b/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts index ab8d374b1871..f5e031f6d843 100644 --- a/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts +++ b/packages/runtime/container-runtime/src/test/dataStoreCreation.spec.ts @@ -19,7 +19,10 @@ import { SummarizeInternalFn, } from "@fluidframework/runtime-definitions/internal"; import { createChildLogger } from "@fluidframework/telemetry-utils/internal"; -import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils/internal"; +import { + MockDeltaManager, + MockFluidDataStoreRuntime, +} from "@fluidframework/test-runtime-utils/internal"; import { LocalFluidDataStoreContext } from "../dataStoreContext.js"; import { createRootSummarizerNodeWithGC } from "../summary/index.js"; @@ -111,6 +114,7 @@ describe("Data Store Creation Tests", () => { IFluidDataStoreRegistry: globalRegistry, baseLogger: createChildLogger(), clientDetails: {} as unknown as IFluidParentContext["clientDetails"], + deltaManager: new MockDeltaManager(), } satisfies Partial as unknown as IFluidParentContext; const summarizerNode = createRootSummarizerNodeWithGC( createChildLogger(), diff --git a/packages/runtime/container-runtime/src/test/dataStoreCreationHelper.ts b/packages/runtime/container-runtime/src/test/dataStoreCreationHelper.ts index 6d28b0ab8f42..bc793343a43b 100644 --- a/packages/runtime/container-runtime/src/test/dataStoreCreationHelper.ts +++ b/packages/runtime/container-runtime/src/test/dataStoreCreationHelper.ts @@ -18,7 +18,10 @@ import { type SummarizeInternalFn, } from "@fluidframework/runtime-definitions/internal"; import { createChildLogger } from "@fluidframework/telemetry-utils/internal"; -import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils/internal"; +import { + MockDeltaManager, + MockFluidDataStoreRuntime, +} from "@fluidframework/test-runtime-utils/internal"; import { LocalFluidDataStoreContext, @@ -58,6 +61,7 @@ export function createParentContext( baseLogger: logger, clientDetails, submitMessage: () => {}, + deltaManager: new MockDeltaManager(), } satisfies Partial as unknown as IFluidParentContext; } diff --git a/packages/runtime/container-runtime/src/test/summary/summarizerClientElection.spec.ts b/packages/runtime/container-runtime/src/test/summary/summarizerClientElection.spec.ts index 106b0c01551f..6b404c141844 100644 --- a/packages/runtime/container-runtime/src/test/summary/summarizerClientElection.spec.ts +++ b/packages/runtime/container-runtime/src/test/summary/summarizerClientElection.spec.ts @@ -24,8 +24,8 @@ import { OrderedClientCollection, OrderedClientElection, SummarizerClientElection, - SummaryManager, summarizerClientType, + SummaryManager, } from "../../summary/index.js"; import { TestQuorumClients } from "./testQuorumClients.js"; 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 248e1cf99128..eb3a9383614c 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 @@ -98,6 +98,8 @@ export interface IFluidDataStoreRuntime extends IEventProvider; // (undocumented) + readonly readonly: boolean; + // (undocumented) readonly rootRoutingContext: IFluidHandleContext; submitSignal: (type: string, content: unknown, targetClientId?: string) => void; uploadBlob(blob: ArrayBufferLike, signal?: AbortSignal): Promise>; @@ -120,6 +122,8 @@ export interface IFluidDataStoreRuntimeEvents extends IEvent { (event: "signal", listener: (message: IInboundSignalMessage, local: boolean) => void): any; // (undocumented) (event: "connected", listener: (clientId: string) => void): any; + // (undocumented) + (event: "readonly", listener: (isReadOnly: boolean) => void): any; } // @alpha (undocumented) diff --git a/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts b/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts index 59d9c6315a8c..771b6fb0e837 100644 --- a/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts @@ -34,6 +34,7 @@ export interface IFluidDataStoreRuntimeEvents extends IEvent { (event: "op", listener: (message: ISequencedDocumentMessage) => void); (event: "signal", listener: (message: IInboundSignalMessage, local: boolean) => void); (event: "connected", listener: (clientId: string) => void); + (event: "readonly", listener: (isReadOnly: boolean) => void); } /** @@ -69,6 +70,8 @@ export interface IFluidDataStoreRuntime readonly connected: boolean; + readonly readonly: boolean; + readonly logger: ITelemetryBaseLogger; /** diff --git a/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md b/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md index 0ccee30275df..ef49cba4f2f0 100644 --- a/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md +++ b/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md @@ -75,6 +75,8 @@ export class FluidDataStoreRuntime extends TypedEventEmitter; // (undocumented) resolveHandle(request: IRequest): Promise; @@ -89,6 +91,8 @@ export class FluidDataStoreRuntime extends TypedEventEmitter; diff --git a/packages/runtime/datastore/package.json b/packages/runtime/datastore/package.json index 94fc59092489..487d78fd629c 100644 --- a/packages/runtime/datastore/package.json +++ b/packages/runtime/datastore/package.json @@ -158,7 +158,11 @@ "typescript": "~5.4.5" }, "typeValidation": { - "broken": {}, + "broken": { + "Class_FluidDataStoreRuntime": { + "forwardCompat": false + } + }, "entrypoint": "legacy" } } diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index 290c5dabc384..d1da869454c0 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -56,6 +56,7 @@ import { IInboundSignalMessage, type IRuntimeMessageCollection, type IRuntimeMessagesContent, + setReadonly, } from "@fluidframework/runtime-definitions/internal"; import { GCDataBuilder, @@ -122,6 +123,21 @@ export interface ISharedObjectRegistry { get(name: string): IChannelFactory | undefined; } +type RequireProps, K extends keyof T> = Omit & + Required>; + +interface IFluidDataStoreContextFeaturesToTypes { + [setReadonly]: RequireProps; +} + +function contextSupportsFeature( + obj: IFluidDataStoreContext, + feature: K, +): obj is IFluidDataStoreContextFeaturesToTypes[K] { + const { ILayerCompatDetails } = obj as FluidObject; + return ILayerCompatDetails?.supportedFeatures.has(feature) ?? false; +} + /** * Base data store class * @legacy @@ -140,6 +156,10 @@ export class FluidDataStoreRuntime return this.dataStoreContext.connected; } + public get readonly(): boolean { + return this._readonly; + } + public get clientId(): string | undefined { return this.dataStoreContext.clientId; } @@ -254,11 +274,17 @@ export class FluidDataStoreRuntime ); // Validate that the Runtime is compatible with this DataStore. - const maybeRuntimeCompatDetails = dataStoreContext as FluidObject; - validateRuntimeCompatibility( - maybeRuntimeCompatDetails.ILayerCompatDetails, - this.dispose.bind(this), - ); + const { ILayerCompatDetails } = dataStoreContext as FluidObject; + validateRuntimeCompatibility(ILayerCompatDetails, this.dispose.bind(this)); + + if (contextSupportsFeature(dataStoreContext, "setReadonly")) { + this._readonly = dataStoreContext.readonly; + } else { + this._readonly = this.dataStoreContext.deltaManager.readOnlyInfo.readonly === true; + this.dataStoreContext.deltaManager.on("readonly", (readonly) => + this.setReadOnlyState(readonly), + ); + } this.mc = createChildMonitoringContext({ logger: dataStoreContext.baseLogger, @@ -640,6 +666,15 @@ export class FluidDataStoreRuntime raiseConnectedEvent(this.logger, this, connected, clientId); } + private _readonly: boolean; + public setReadOnlyState(readonly: boolean) { + this.verifyNotClosed(); + if (readonly !== this._readonly) { + this._readonly = readonly; + this.emit("readonly", readonly); + } + } + public getQuorum(): IQuorumClients { return this.quorum; } diff --git a/packages/runtime/datastore/src/test/types/validateDatastorePrevious.generated.ts b/packages/runtime/datastore/src/test/types/validateDatastorePrevious.generated.ts index bab8d7182452..5fe3440cb5cc 100644 --- a/packages/runtime/datastore/src/test/types/validateDatastorePrevious.generated.ts +++ b/packages/runtime/datastore/src/test/types/validateDatastorePrevious.generated.ts @@ -22,6 +22,7 @@ declare type MakeUnusedImportErrorsGoAway = TypeOnly | MinimalType | Fu * typeValidation.broken: * "Class_FluidDataStoreRuntime": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Class_FluidDataStoreRuntime = requireAssignableTo, TypeOnly> /* 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 d497b1a40dff..071100faf4b4 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 @@ -138,6 +138,7 @@ export interface IFluidDataStoreChannel extends IDisposable { // (undocumented) setAttachState(attachState: AttachState.Attaching | AttachState.Attached): void; setConnectionState(connected: boolean, clientId?: string): any; + setReadOnlyState?(readonly: boolean): any; summarize(fullTree?: boolean, trackState?: boolean, telemetryContext?: ITelemetryContext): Promise; updateUsedRoutes(usedRoutes: string[]): void; } @@ -218,6 +219,8 @@ export interface IFluidParentContext extends IProvideFluidHandleContext, Partial makeLocallyVisible(): void; // (undocumented) readonly options: Record; + // (undocumented) + readonly readonly?: boolean; readonly scope: FluidObject; setChannelDirty(address: string): void; // (undocumented) diff --git a/packages/runtime/runtime-definitions/src/dataStoreContext.ts b/packages/runtime/runtime-definitions/src/dataStoreContext.ts index 5530e9d02f94..f1414bb40621 100644 --- a/packages/runtime/runtime-definitions/src/dataStoreContext.ts +++ b/packages/runtime/runtime-definitions/src/dataStoreContext.ts @@ -370,6 +370,11 @@ export interface IFluidDataStoreChannel extends IDisposable { */ setConnectionState(connected: boolean, clientId?: string); + /** + * Notifies this object about changes in the readonly state + */ + setReadOnlyState?(readonly: boolean); + /** * Ask the DDS to resubmit a message. This could be because we reconnected and this message was not acked. * @param type - The type of the original message. @@ -440,6 +445,7 @@ export interface IFluidParentContext readonly options: Record; readonly clientId: string | undefined; readonly connected: boolean; + readonly readonly?: boolean; readonly deltaManager: IDeltaManager; readonly storage: IDocumentStorageService; readonly baseLogger: ITelemetryBaseLogger; diff --git a/packages/runtime/runtime-definitions/src/index.ts b/packages/runtime/runtime-definitions/src/index.ts index 14178798eaa6..667e88a320d0 100644 --- a/packages/runtime/runtime-definitions/src/index.ts +++ b/packages/runtime/runtime-definitions/src/index.ts @@ -53,7 +53,10 @@ export type { IRuntimeMessagesContent, ISequencedMessageEnvelope, } from "./protocol.js"; -export { encodeHandlesInContainerRuntime } from "./runtimeLayerCompatFeatureNames.js"; +export { + encodeHandlesInContainerRuntime, + setReadonly, +} from "./runtimeLayerCompatFeatureNames.js"; export type { CreateChildSummarizerNodeParam, IExperimentalIncrementalSummaryContext, diff --git a/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts b/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts index cef12c82fa41..b98b89dedd54 100644 --- a/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts +++ b/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts @@ -10,3 +10,9 @@ * @internal */ export const encodeHandlesInContainerRuntime = "encodeHandlesInContainerRuntime" as const; + +/** + * + * @internal + */ +export const setReadonly = "setReadonly" as const; 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 d2e38472f9b5..3ec7ee807a2e 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 @@ -196,7 +196,7 @@ export class MockDeltaConnection implements IDeltaConnection { // @alpha export class MockDeltaManager extends TypedEventEmitter implements IDeltaManager { - constructor(getClientId?: (() => string) | undefined); + constructor(getClientId?: (() => string | undefined) | undefined); // (undocumented) readonly active: boolean; // (undocumented) @@ -354,6 +354,8 @@ export class MockFluidDataStoreContext implements IFluidDataStoreContext { // (undocumented) packagePath: readonly string[]; // (undocumented) + readonly: boolean; + // (undocumented) scope: FluidObject; // (undocumented) setChannelDirty(address: string): void; @@ -468,6 +470,8 @@ export class MockFluidDataStoreRuntime extends EventEmitter implements IFluidDat // (undocumented) quorum: MockQuorumClients; // (undocumented) + readonly: boolean; + // (undocumented) request(request: IRequest): Promise; // (undocumented) requestDataStore(request: IRequest): Promise; @@ -486,6 +490,8 @@ export class MockFluidDataStoreRuntime extends EventEmitter implements IFluidDat // (undocumented) setConnectionState(connected: boolean, clientId?: string): void; // (undocumented) + setReadOnlyState(readonly: boolean): void; + // (undocumented) submitMessage(type: MessageType, content: any): null; // (undocumented) submitSignal(type: string, content: any): null; diff --git a/packages/runtime/test-runtime-utils/package.json b/packages/runtime/test-runtime-utils/package.json index 7818e18b458e..acc81b0fe654 100644 --- a/packages/runtime/test-runtime-utils/package.json +++ b/packages/runtime/test-runtime-utils/package.json @@ -153,7 +153,14 @@ "typescript": "~5.4.5" }, "typeValidation": { - "broken": {}, + "broken": { + "Class_MockFluidDataStoreContext": { + "forwardCompat": false + }, + "Class_MockFluidDataStoreRuntime": { + "forwardCompat": false + } + }, "entrypoint": "legacy" } } diff --git a/packages/runtime/test-runtime-utils/src/mockDeltas.ts b/packages/runtime/test-runtime-utils/src/mockDeltas.ts index fef2ff45878a..2bcd544c519a 100644 --- a/packages/runtime/test-runtime-utils/src/mockDeltas.ts +++ b/packages/runtime/test-runtime-utils/src/mockDeltas.ts @@ -186,7 +186,7 @@ export class MockDeltaManager this.emit("op", message); } - constructor(private readonly getClientId?: () => string) { + constructor(private readonly getClientId?: () => string | undefined) { super(); this._inbound = new MockDeltaQueue(); diff --git a/packages/runtime/test-runtime-utils/src/mocks.ts b/packages/runtime/test-runtime-utils/src/mocks.ts index 15636ce00a6a..33e2a15a3a05 100644 --- a/packages/runtime/test-runtime-utils/src/mocks.ts +++ b/packages/runtime/test-runtime-utils/src/mocks.ts @@ -841,6 +841,7 @@ export class MockFluidDataStoreRuntime this.registry = new Map(registry.map((factory) => [factory.type, factory])); } } + public readonly: boolean = false; public readonly entryPoint: IFluidHandleInternal; @@ -1039,6 +1040,10 @@ export class MockFluidDataStoreRuntime return; } + public setReadOnlyState(readonly: boolean) { + this.readonly = readonly; + } + public async resolveHandle(request: IRequest): Promise { if (request.url !== undefined) { return { diff --git a/packages/runtime/test-runtime-utils/src/mocksDataStoreContext.ts b/packages/runtime/test-runtime-utils/src/mocksDataStoreContext.ts index 33126e144086..c05e4996a68e 100644 --- a/packages/runtime/test-runtime-utils/src/mocksDataStoreContext.ts +++ b/packages/runtime/test-runtime-utils/src/mocksDataStoreContext.ts @@ -33,6 +33,8 @@ import { } from "@fluidframework/telemetry-utils/internal"; import { v4 as uuid } from "uuid"; +import { MockDeltaManager } from "./mockDeltas.js"; + /** * @legacy * @alpha @@ -45,9 +47,10 @@ export class MockFluidDataStoreContext implements IFluidDataStoreContext { public clientId: string | undefined = uuid(); public clientDetails: IClientDetails; public connected: boolean = true; + public readonly: boolean = false; public baseSnapshot: ISnapshotTree | undefined; public deltaManager: IDeltaManager = - undefined as any; + new MockDeltaManager(() => this.clientId); public containerRuntime: IContainerRuntimeBase = undefined as any; public storage: IDocumentStorageService = undefined as any; public IFluidDataStoreRegistry: IFluidDataStoreRegistry = undefined as any; diff --git a/packages/runtime/test-runtime-utils/src/test/types/validateTestRuntimeUtilsPrevious.generated.ts b/packages/runtime/test-runtime-utils/src/test/types/validateTestRuntimeUtilsPrevious.generated.ts index 60347db4f053..687153b795f6 100644 --- a/packages/runtime/test-runtime-utils/src/test/types/validateTestRuntimeUtilsPrevious.generated.ts +++ b/packages/runtime/test-runtime-utils/src/test/types/validateTestRuntimeUtilsPrevious.generated.ts @@ -166,6 +166,7 @@ declare type current_as_old_for_Class_MockDeltaQueue = requireAssignableTo, TypeOnly> /* @@ -184,6 +185,7 @@ declare type current_as_old_for_Class_MockFluidDataStoreContext = requireAssigna * typeValidation.broken: * "Class_MockFluidDataStoreRuntime": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Class_MockFluidDataStoreRuntime = requireAssignableTo, TypeOnly> /* diff --git a/packages/test/local-server-tests/src/test/readonly.spec.ts b/packages/test/local-server-tests/src/test/readonly.spec.ts new file mode 100644 index 000000000000..d826c7d57838 --- /dev/null +++ b/packages/test/local-server-tests/src/test/readonly.spec.ts @@ -0,0 +1,184 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct/internal"; +import { type IRuntimeFactory } from "@fluidframework/container-definitions/internal"; +import { + createDetachedContainer, + loadExistingContainer, + type ILoadExistingContainerProps, +} from "@fluidframework/container-loader/internal"; +import { loadContainerRuntime } from "@fluidframework/container-runtime/internal"; +import { type FluidObject } from "@fluidframework/core-interfaces/internal"; +import { assert } from "@fluidframework/core-utils/internal"; +import { LocalDeltaConnectionServer } from "@fluidframework/server-local-server"; + +import { createLoader } from "../utils.js"; + +class DefaultDataObject extends DataObject { + get DefaultDataObject() { + return this; + } + get readonly() { + return this.runtime.readonly; + } + + protected async hasInitialized(): Promise { + this.runtime.on("readonly", () => this.readonlyEventCount++); + } + + public readonlyEventCount: number = 0; +} +const defaultDataObjectFactory = new DataObjectFactory( + "DefaultDataObject", + DefaultDataObject, + undefined, + {}, +); + +// a simple container runtime factory with a single datastore aliased as default. +// the default datastore is also returned as the entrypoint +const runtimeFactory: IRuntimeFactory = { + get IRuntimeFactory() { + return this; + }, + instantiateRuntime: async (context, existing) => { + return loadContainerRuntime({ + context, + existing, + registryEntries: [ + [ + defaultDataObjectFactory.type, + // the parent is still async in the container registry + // this allows things like code splitting for dynamic loading + Promise.resolve(defaultDataObjectFactory), + ], + ], + provideEntryPoint: async (rt) => { + const maybeRoot = await rt.getAliasedDataStoreEntryPoint("default"); + if (maybeRoot === undefined) { + const ds = await rt.createDataStore(defaultDataObjectFactory.type); + await ds.trySetAlias("default"); + } + const root = await rt.getAliasedDataStoreEntryPoint("default"); + assert(root !== undefined, "default must exist"); + return root.get(); + }, + }); + }, +}; + +async function createContainerAndGetLoadProps(): Promise { + const deltaConnectionServer = LocalDeltaConnectionServer.create(); + + const { loaderProps, codeDetails, urlResolver } = createLoader({ + deltaConnectionServer, + runtimeFactory, + }); + + const container = await createDetachedContainer({ ...loaderProps, codeDetails }); + await container.getEntryPoint(); + + await container.attach(urlResolver.createCreateNewRequest("test")); + const url = await container.getAbsoluteUrl(""); + assert(url !== undefined, "container must have url"); + container.dispose(); + return { ...loaderProps, request: { url } }; +} + +describe("readonly", () => { + it("Readonly is correct across container create", async () => { + const deltaConnectionServer = LocalDeltaConnectionServer.create(); + + const { loaderProps, codeDetails, urlResolver } = createLoader({ + deltaConnectionServer, + runtimeFactory, + }); + + const container = await createDetachedContainer({ ...loaderProps, codeDetails }); + + const entrypoint: FluidObject = await container.getEntryPoint(); + + assert( + entrypoint.DefaultDataObject !== undefined, + "container entrypoint must be DefaultDataObject", + ); + + assert(entrypoint.DefaultDataObject.readonly === false, "shouldn't be readonly"); + assert( + entrypoint.DefaultDataObject.readonlyEventCount === 0, + "shouldn't be any readonly events", + ); + + await container.attach(urlResolver.createCreateNewRequest("test")); + + assert(entrypoint.DefaultDataObject.readonly === false, "shouldn't be readonly"); + assert( + entrypoint.DefaultDataObject.readonlyEventCount === 0, + "shouldn't be any readonly events", + ); + }); + + it("Readonly is correct after container load", async () => { + const loadedContainer = await loadExistingContainer( + await createContainerAndGetLoadProps(), + ); + + const entrypoint: FluidObject = await loadedContainer.getEntryPoint(); + + assert( + entrypoint.DefaultDataObject !== undefined, + "container entrypoint must be DefaultDataObject", + ); + + assert(entrypoint.DefaultDataObject.readonly === false, "shouldn't be readonly"); + assert( + entrypoint.DefaultDataObject.readonlyEventCount === 0, + "shouldn't be any readonly events", + ); + }); + + it("Readonly is correct after datastore load and forceReadonly", async () => { + const loadedContainer = await loadExistingContainer( + await createContainerAndGetLoadProps(), + ); + + const entrypoint: FluidObject = await loadedContainer.getEntryPoint(); + + assert( + entrypoint.DefaultDataObject !== undefined, + "container entrypoint must be DefaultDataObject", + ); + + loadedContainer.forceReadonly?.(true); + + assert(entrypoint.DefaultDataObject.readonly === true, "should be readonly"); + assert( + entrypoint.DefaultDataObject.readonlyEventCount === 1, + "should be any readonly events", + ); + }); + + it("Readonly is correct after forceReadonly before datastore load", async () => { + const loadedContainer = await loadExistingContainer( + await createContainerAndGetLoadProps(), + ); + + loadedContainer.forceReadonly?.(true); + + const entrypoint: FluidObject = await loadedContainer.getEntryPoint(); + + assert( + entrypoint.DefaultDataObject !== undefined, + "container entrypoint must be DefaultDataObject", + ); + + assert(entrypoint.DefaultDataObject.readonly === true, "should be readonly"); + assert( + entrypoint.DefaultDataObject.readonlyEventCount === 0, + "shouldn't be any readonly events", + ); + }); +}); From a47722caf53d647f529b83cff7cefb5475345a8d Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Mon, 21 Apr 2025 15:07:28 -0700 Subject: [PATCH 02/22] update type tests --- packages/framework/aqueduct/package.json | 6 +++++- .../src/test/types/validateAqueductPrevious.generated.ts | 1 + packages/test/test-utils/package.json | 9 ++++++++- .../test/types/validateTestUtilsPrevious.generated.ts | 2 ++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/framework/aqueduct/package.json b/packages/framework/aqueduct/package.json index 4110b0784c7a..6ff79e74bd90 100644 --- a/packages/framework/aqueduct/package.json +++ b/packages/framework/aqueduct/package.json @@ -154,7 +154,11 @@ "typescript": "~5.4.5" }, "typeValidation": { - "broken": {}, + "broken": { + "Interface_IDataObjectProps": { + "forwardCompat": false + } + }, "entrypoint": "legacy" } } diff --git a/packages/framework/aqueduct/src/test/types/validateAqueductPrevious.generated.ts b/packages/framework/aqueduct/src/test/types/validateAqueductPrevious.generated.ts index b7fd21423392..7491dd0f6fd5 100644 --- a/packages/framework/aqueduct/src/test/types/validateAqueductPrevious.generated.ts +++ b/packages/framework/aqueduct/src/test/types/validateAqueductPrevious.generated.ts @@ -238,6 +238,7 @@ declare type current_as_old_for_Interface_DataObjectTypes = requireAssignableTo< * typeValidation.broken: * "Interface_IDataObjectProps": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Interface_IDataObjectProps = requireAssignableTo, TypeOnly> /* diff --git a/packages/test/test-utils/package.json b/packages/test/test-utils/package.json index 34c3aef7be8f..8396d8b2d4fb 100644 --- a/packages/test/test-utils/package.json +++ b/packages/test/test-utils/package.json @@ -163,7 +163,14 @@ "typescript": "~5.4.5" }, "typeValidation": { - "broken": {}, + "broken": { + "Interface_IProvideTestFluidObject": { + "forwardCompat": false + }, + "Interface_ITestFluidObject": { + "forwardCompat": false + } + }, "entrypoint": "legacy" } } diff --git a/packages/test/test-utils/src/test/types/validateTestUtilsPrevious.generated.ts b/packages/test/test-utils/src/test/types/validateTestUtilsPrevious.generated.ts index 23bb8b2365d0..cf9cc8cc4a1e 100644 --- a/packages/test/test-utils/src/test/types/validateTestUtilsPrevious.generated.ts +++ b/packages/test/test-utils/src/test/types/validateTestUtilsPrevious.generated.ts @@ -76,6 +76,7 @@ declare type current_as_old_for_Interface_IOpProcessingController = requireAssig * typeValidation.broken: * "Interface_IProvideTestFluidObject": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Interface_IProvideTestFluidObject = requireAssignableTo, TypeOnly> /* @@ -94,6 +95,7 @@ declare type current_as_old_for_Interface_IProvideTestFluidObject = requireAssig * typeValidation.broken: * "Interface_ITestFluidObject": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Interface_ITestFluidObject = requireAssignableTo, TypeOnly> /* From 3f54418a0749c2854ba762bd26728da58abc9352 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Mon, 21 Apr 2025 16:15:15 -0700 Subject: [PATCH 03/22] revert whitespace --- .../src/test/summary/summarizerClientElection.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/container-runtime/src/test/summary/summarizerClientElection.spec.ts b/packages/runtime/container-runtime/src/test/summary/summarizerClientElection.spec.ts index 6b404c141844..106b0c01551f 100644 --- a/packages/runtime/container-runtime/src/test/summary/summarizerClientElection.spec.ts +++ b/packages/runtime/container-runtime/src/test/summary/summarizerClientElection.spec.ts @@ -24,8 +24,8 @@ import { OrderedClientCollection, OrderedClientElection, SummarizerClientElection, - summarizerClientType, SummaryManager, + summarizerClientType, } from "../../summary/index.js"; import { TestQuorumClients } from "./testQuorumClients.js"; From 6f22decefed5113933f901ecaca643aa73841090 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Mon, 21 Apr 2025 17:25:53 -0700 Subject: [PATCH 04/22] update tests --- .../src/test/readonly.spec.ts | 121 ++++++++++++------ 1 file changed, 83 insertions(+), 38 deletions(-) diff --git a/packages/test/local-server-tests/src/test/readonly.spec.ts b/packages/test/local-server-tests/src/test/readonly.spec.ts index d826c7d57838..4d38dd41a397 100644 --- a/packages/test/local-server-tests/src/test/readonly.spec.ts +++ b/packages/test/local-server-tests/src/test/readonly.spec.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. */ -import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct/internal"; import { type IRuntimeFactory } from "@fluidframework/container-definitions/internal"; import { createDetachedContainer, @@ -13,31 +12,77 @@ import { import { loadContainerRuntime } from "@fluidframework/container-runtime/internal"; import { type FluidObject } from "@fluidframework/core-interfaces/internal"; import { assert } from "@fluidframework/core-utils/internal"; +import { FluidDataStoreRuntime } from "@fluidframework/datastore/internal"; +import type { + IChannelFactory, + IFluidDataStoreRuntime, +} from "@fluidframework/datastore-definitions/internal"; +import { SharedMap, ISharedMap } from "@fluidframework/map/internal"; +import type { IFluidDataStoreFactory } from "@fluidframework/runtime-definitions/internal"; import { LocalDeltaConnectionServer } from "@fluidframework/server-local-server"; import { createLoader } from "../utils.js"; -class DefaultDataObject extends DataObject { - get DefaultDataObject() { +const mapFactory = SharedMap.getFactory(); +const sharedObjectRegistry = new Map([[mapFactory.type, mapFactory]]); + +class DefaultDataStore { + public static create(runtime: IFluidDataStoreRuntime) { + const root = SharedMap.create(runtime, "root"); + root.bindToContext(); + return new DefaultDataStore(runtime, root); + } + + public static async load(runtime: IFluidDataStoreRuntime) { + const root = (await runtime.getChannel("root")) as unknown as ISharedMap; + return new DefaultDataStore(runtime, root); + } + + public readonlyEventCount = 0; + + private constructor( + private readonly runtime: IFluidDataStoreRuntime, + sharedMap: SharedMap, + ) { + this.runtime.on("readonly", () => this.readonlyEventCount++); + } + + get DefaultDataStore() { return this; } get readonly() { return this.runtime.readonly; } - protected async hasInitialized(): Promise { - this.runtime.on("readonly", () => this.readonlyEventCount++); + get handle() { + return this.runtime.entryPoint; } - - public readonlyEventCount: number = 0; } -const defaultDataObjectFactory = new DataObjectFactory( - "DefaultDataObject", - DefaultDataObject, - undefined, - {}, -); +class DefaultDataStoreFactory implements IFluidDataStoreFactory { + static readonly instance = new DefaultDataStoreFactory(); + private constructor() {} + + get IFluidDataStoreFactory() { + return this; + } + + public readonly type = "DefaultDataStore"; + + async instantiateDataStore(context, existing) { + const runtime: FluidDataStoreRuntime = new FluidDataStoreRuntime( + context, + sharedObjectRegistry, + existing, + async () => dataStore, + ); + const dataStore = existing + ? DefaultDataStore.load(runtime) + : DefaultDataStore.create(runtime); + + return runtime; + } +} // a simple container runtime factory with a single datastore aliased as default. // the default datastore is also returned as the entrypoint const runtimeFactory: IRuntimeFactory = { @@ -50,16 +95,16 @@ const runtimeFactory: IRuntimeFactory = { existing, registryEntries: [ [ - defaultDataObjectFactory.type, + DefaultDataStoreFactory.instance.type, // the parent is still async in the container registry // this allows things like code splitting for dynamic loading - Promise.resolve(defaultDataObjectFactory), + Promise.resolve(DefaultDataStoreFactory.instance), ], ], provideEntryPoint: async (rt) => { const maybeRoot = await rt.getAliasedDataStoreEntryPoint("default"); if (maybeRoot === undefined) { - const ds = await rt.createDataStore(defaultDataObjectFactory.type); + const ds = await rt.createDataStore(DefaultDataStoreFactory.instance.type); await ds.trySetAlias("default"); } const root = await rt.getAliasedDataStoreEntryPoint("default"); @@ -99,24 +144,24 @@ describe("readonly", () => { const container = await createDetachedContainer({ ...loaderProps, codeDetails }); - const entrypoint: FluidObject = await container.getEntryPoint(); + const entrypoint: FluidObject = await container.getEntryPoint(); assert( - entrypoint.DefaultDataObject !== undefined, - "container entrypoint must be DefaultDataObject", + entrypoint.DefaultDataStore !== undefined, + "container entrypoint must be DefaultDataStore", ); - assert(entrypoint.DefaultDataObject.readonly === false, "shouldn't be readonly"); + assert(entrypoint.DefaultDataStore.readonly === false, "shouldn't be readonly"); assert( - entrypoint.DefaultDataObject.readonlyEventCount === 0, + entrypoint.DefaultDataStore.readonlyEventCount === 0, "shouldn't be any readonly events", ); await container.attach(urlResolver.createCreateNewRequest("test")); - assert(entrypoint.DefaultDataObject.readonly === false, "shouldn't be readonly"); + assert(entrypoint.DefaultDataStore.readonly === false, "shouldn't be readonly"); assert( - entrypoint.DefaultDataObject.readonlyEventCount === 0, + entrypoint.DefaultDataStore.readonlyEventCount === 0, "shouldn't be any readonly events", ); }); @@ -126,16 +171,16 @@ describe("readonly", () => { await createContainerAndGetLoadProps(), ); - const entrypoint: FluidObject = await loadedContainer.getEntryPoint(); + const entrypoint: FluidObject = await loadedContainer.getEntryPoint(); assert( - entrypoint.DefaultDataObject !== undefined, - "container entrypoint must be DefaultDataObject", + entrypoint.DefaultDataStore !== undefined, + "container entrypoint must be DefaultDataStore", ); - assert(entrypoint.DefaultDataObject.readonly === false, "shouldn't be readonly"); + assert(entrypoint.DefaultDataStore.readonly === false, "shouldn't be readonly"); assert( - entrypoint.DefaultDataObject.readonlyEventCount === 0, + entrypoint.DefaultDataStore.readonlyEventCount === 0, "shouldn't be any readonly events", ); }); @@ -145,18 +190,18 @@ describe("readonly", () => { await createContainerAndGetLoadProps(), ); - const entrypoint: FluidObject = await loadedContainer.getEntryPoint(); + const entrypoint: FluidObject = await loadedContainer.getEntryPoint(); assert( - entrypoint.DefaultDataObject !== undefined, - "container entrypoint must be DefaultDataObject", + entrypoint.DefaultDataStore !== undefined, + "container entrypoint must be DefaultDataStore", ); loadedContainer.forceReadonly?.(true); - assert(entrypoint.DefaultDataObject.readonly === true, "should be readonly"); + assert(entrypoint.DefaultDataStore.readonly === true, "should be readonly"); assert( - entrypoint.DefaultDataObject.readonlyEventCount === 1, + entrypoint.DefaultDataStore.readonlyEventCount === 1, "should be any readonly events", ); }); @@ -168,16 +213,16 @@ describe("readonly", () => { loadedContainer.forceReadonly?.(true); - const entrypoint: FluidObject = await loadedContainer.getEntryPoint(); + const entrypoint: FluidObject = await loadedContainer.getEntryPoint(); assert( - entrypoint.DefaultDataObject !== undefined, - "container entrypoint must be DefaultDataObject", + entrypoint.DefaultDataStore !== undefined, + "container entrypoint must be DefaultDataStore", ); - assert(entrypoint.DefaultDataObject.readonly === true, "should be readonly"); + assert(entrypoint.DefaultDataStore.readonly === true, "should be readonly"); assert( - entrypoint.DefaultDataObject.readonlyEventCount === 0, + entrypoint.DefaultDataStore.readonlyEventCount === 0, "shouldn't be any readonly events", ); }); From 780ab0edcb25e401df5be0862d619a5671a0f8b2 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Mon, 21 Apr 2025 17:38:17 -0700 Subject: [PATCH 05/22] fixes --- packages/runtime/container-runtime/src/dataStoreContext.ts | 2 +- packages/runtime/runtime-definitions/src/dataStoreContext.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 31ae8b867371..bbecd370da06 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -232,8 +232,8 @@ class ContextDeltaManagerProxy extends BaseDeltaManagerProxy { readonly: boolean, readonlyConnectionReason?: { reason: string; error?: IErrorBase }, ): void { - this._readonly = readonly; if (this._readonly !== readonly) { + this._readonly = readonly; this.emit("readonly", readonly, readonlyConnectionReason); } } diff --git a/packages/runtime/runtime-definitions/src/dataStoreContext.ts b/packages/runtime/runtime-definitions/src/dataStoreContext.ts index f1414bb40621..9a08a1ae5094 100644 --- a/packages/runtime/runtime-definitions/src/dataStoreContext.ts +++ b/packages/runtime/runtime-definitions/src/dataStoreContext.ts @@ -373,7 +373,7 @@ export interface IFluidDataStoreChannel extends IDisposable { /** * Notifies this object about changes in the readonly state */ - setReadOnlyState?(readonly: boolean); + setReadOnlyState?(readonly: boolean): void; /** * Ask the DDS to resubmit a message. This could be because we reconnected and this message was not acked. From b11bf645452c52e5c32dc77a23ac5103d07cb303 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Mon, 21 Apr 2025 18:07:53 -0700 Subject: [PATCH 06/22] regen docs --- .../api-report/runtime-definitions.legacy.alpha.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 071100faf4b4..a975cafa951e 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 @@ -138,7 +138,7 @@ export interface IFluidDataStoreChannel extends IDisposable { // (undocumented) setAttachState(attachState: AttachState.Attaching | AttachState.Attached): void; setConnectionState(connected: boolean, clientId?: string): any; - setReadOnlyState?(readonly: boolean): any; + setReadOnlyState?(readonly: boolean): void; summarize(fullTree?: boolean, trackState?: boolean, telemetryContext?: ITelemetryContext): Promise; updateUsedRoutes(usedRoutes: string[]): void; } From 413dce50db8cff608cdf69230f300cf43a694ba2 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Tue, 22 Apr 2025 09:58:43 -0700 Subject: [PATCH 07/22] update feature name --- .../container-runtime/src/runtimeLayerCompatState.ts | 4 ++-- packages/runtime/datastore/src/dataStoreRuntime.ts | 6 +++--- packages/runtime/runtime-definitions/src/index.ts | 2 +- .../src/runtimeLayerCompatFeatureNames.ts | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts b/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts index d5c8a3dc33a9..f2ac4af74982 100644 --- a/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts +++ b/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts @@ -11,7 +11,7 @@ import { import type { ICriticalContainerError } from "@fluidframework/container-definitions"; import { encodeHandlesInContainerRuntime, - setReadonly, + setReadOnlyState, } from "@fluidframework/runtime-definitions/internal"; import { UsageError } from "@fluidframework/telemetry-utils/internal"; @@ -69,7 +69,7 @@ export const runtimeCompatDetailsForDataStore: ILayerCompatDetails = { /** * The features supported by the Runtime layer across the Runtime / DataStore boundary. */ - supportedFeatures: new Set([encodeHandlesInContainerRuntime, setReadonly]), + supportedFeatures: new Set([encodeHandlesInContainerRuntime, setReadOnlyState]), }; /** diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index d1da869454c0..c2456366129d 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -56,7 +56,7 @@ import { IInboundSignalMessage, type IRuntimeMessageCollection, type IRuntimeMessagesContent, - setReadonly, + setReadOnlyState, } from "@fluidframework/runtime-definitions/internal"; import { GCDataBuilder, @@ -127,7 +127,7 @@ type RequireProps, K extends keyof T> = Omit>; interface IFluidDataStoreContextFeaturesToTypes { - [setReadonly]: RequireProps; + [setReadOnlyState]: RequireProps; } function contextSupportsFeature( @@ -277,7 +277,7 @@ export class FluidDataStoreRuntime const { ILayerCompatDetails } = dataStoreContext as FluidObject; validateRuntimeCompatibility(ILayerCompatDetails, this.dispose.bind(this)); - if (contextSupportsFeature(dataStoreContext, "setReadonly")) { + if (contextSupportsFeature(dataStoreContext, setReadOnlyState)) { this._readonly = dataStoreContext.readonly; } else { this._readonly = this.dataStoreContext.deltaManager.readOnlyInfo.readonly === true; diff --git a/packages/runtime/runtime-definitions/src/index.ts b/packages/runtime/runtime-definitions/src/index.ts index 667e88a320d0..8fdaca855897 100644 --- a/packages/runtime/runtime-definitions/src/index.ts +++ b/packages/runtime/runtime-definitions/src/index.ts @@ -55,7 +55,7 @@ export type { } from "./protocol.js"; export { encodeHandlesInContainerRuntime, - setReadonly, + setReadOnlyState, } from "./runtimeLayerCompatFeatureNames.js"; export type { CreateChildSummarizerNodeParam, diff --git a/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts b/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts index b98b89dedd54..00182088b967 100644 --- a/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts +++ b/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts @@ -12,7 +12,8 @@ export const encodeHandlesInContainerRuntime = "encodeHandlesInContainerRuntime" as const; /** - * + * This feature indicates that the datastore context will call setReadOnlyState on the + * datastore runtime. * @internal */ -export const setReadonly = "setReadonly" as const; +export const setReadOnlyState = "setReadOnlyState" as const; From 5691bb58da7b0944287d50e83b9ec6e746d160b3 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Tue, 22 Apr 2025 13:31:05 -0700 Subject: [PATCH 08/22] Update packages/runtime/container-runtime/src/dataStoreContext.ts Co-authored-by: Mark Fields --- packages/runtime/container-runtime/src/dataStoreContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index bbecd370da06..853136d95688 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -223,7 +223,7 @@ class ContextDeltaManagerProxy extends BaseDeltaManagerProxy { permissions: undefined, storageOnly: false, } - : { readonly: this._readonly }; + : { readonly: false }; } } From 0a7fe4d7f8fe1b327418545237ab2211d5eae0da Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Tue, 22 Apr 2025 14:36:45 -0700 Subject: [PATCH 09/22] Update packages/runtime/container-runtime/src/dataStoreContext.ts Co-authored-by: Kian Thompson <102998837+kian-thompson@users.noreply.github.com> --- packages/runtime/container-runtime/src/dataStoreContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 853136d95688..dfd9af611746 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -201,7 +201,7 @@ export interface IFluidDataStoreContextEvents extends IEvent { * support should be listed below, and as layer compat support goes away for those feature, we should also remove them from this proxy, with the eventual goal * of completely remove this proxy. * - * - Everything regarding readonly is to support older datastore runtimes which do not have the setReadonly function, so must get their readonly state via the delta manager. + * - Everything regarding readonly is to support older datastore runtimes which do not have the setReadonly function, so they must get their readonly state via the delta manager. * */ class ContextDeltaManagerProxy extends BaseDeltaManagerProxy { From 1f12146e134f8a5caa9591317fe3129add58cc27 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Tue, 22 Apr 2025 14:36:58 -0700 Subject: [PATCH 10/22] Update packages/runtime/container-runtime/src/dataStoreContext.ts Co-authored-by: Kian Thompson <102998837+kian-thompson@users.noreply.github.com> --- packages/runtime/container-runtime/src/dataStoreContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index dfd9af611746..9137aeb86fb8 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -199,7 +199,7 @@ export interface IFluidDataStoreContextEvents extends IEvent { * Eventually we should remove the delta manger from being exposed to Datastore runtimes via the context. However to remove that exposure we need to add new * features, and those features themselves need forward and back compat. This proxy is here to enable that back compat. Each feature this proxy is used to * support should be listed below, and as layer compat support goes away for those feature, we should also remove them from this proxy, with the eventual goal - * of completely remove this proxy. + * of completely removing this proxy. * * - Everything regarding readonly is to support older datastore runtimes which do not have the setReadonly function, so they must get their readonly state via the delta manager. * From c1550329823f677b1d463983f126704e7cfcb3b6 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Tue, 22 Apr 2025 14:37:14 -0700 Subject: [PATCH 11/22] Update packages/runtime/container-runtime/src/dataStoreContext.ts Co-authored-by: Matt Rakow --- packages/runtime/container-runtime/src/dataStoreContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 9137aeb86fb8..d61766674076 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -268,7 +268,7 @@ export abstract class FluidDataStoreContext return this.parentContext.baseLogger; } - private readonly _deltaManager: ContextDeltaManagerProxy; + private readonly _contextDeltaManagerProxy: ContextDeltaManagerProxy; public get deltaManager(): IDeltaManager { return this._deltaManager; } From 57a735195e9a6e136f9bbef5683a54a2a235d738 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Tue, 22 Apr 2025 14:38:27 -0700 Subject: [PATCH 12/22] small fixes --- packages/runtime/datastore/src/dataStoreRuntime.ts | 4 ++-- .../api-report/runtime-definitions.legacy.alpha.api.md | 1 - packages/runtime/runtime-definitions/src/dataStoreContext.ts | 4 ++++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index c2456366129d..5b41817fa597 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -123,11 +123,11 @@ export interface ISharedObjectRegistry { get(name: string): IChannelFactory | undefined; } -type RequireProps, K extends keyof T> = Omit & +type PickRequired, K extends keyof T> = Omit & Required>; interface IFluidDataStoreContextFeaturesToTypes { - [setReadOnlyState]: RequireProps; + [setReadOnlyState]: PickRequired; } function contextSupportsFeature( 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 a975cafa951e..49c273fb8c7e 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 @@ -219,7 +219,6 @@ export interface IFluidParentContext extends IProvideFluidHandleContext, Partial makeLocallyVisible(): void; // (undocumented) readonly options: Record; - // (undocumented) readonly readonly?: boolean; readonly scope: FluidObject; setChannelDirty(address: string): void; diff --git a/packages/runtime/runtime-definitions/src/dataStoreContext.ts b/packages/runtime/runtime-definitions/src/dataStoreContext.ts index 9a08a1ae5094..77e029a55df0 100644 --- a/packages/runtime/runtime-definitions/src/dataStoreContext.ts +++ b/packages/runtime/runtime-definitions/src/dataStoreContext.ts @@ -445,6 +445,10 @@ export interface IFluidParentContext readonly options: Record; readonly clientId: string | undefined; readonly connected: boolean; + /** + * Indicates if the parent context is readonly. If readonly is true, the consumer of + * the context should also consider themselves readonly. + */ readonly readonly?: boolean; readonly deltaManager: IDeltaManager; readonly storage: IDocumentStorageService; From e465ca47b384497176e41157a165b7e7f74377ca Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Tue, 22 Apr 2025 16:10:13 -0700 Subject: [PATCH 13/22] add some comments --- .../src/channelCollection.ts | 3 ++ .../container-runtime/src/dataStoreContext.ts | 34 +++++++++++-------- .../api-report/datastore.legacy.alpha.api.md | 1 - .../runtime/datastore/src/dataStoreRuntime.ts | 5 +++ 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/runtime/container-runtime/src/channelCollection.ts b/packages/runtime/container-runtime/src/channelCollection.ts index 917193f82275..d1bbc7277eb3 100644 --- a/packages/runtime/container-runtime/src/channelCollection.ts +++ b/packages/runtime/container-runtime/src/channelCollection.ts @@ -1119,6 +1119,9 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable { } } + /** + * Enumerates the contexts and calls setReadOnlyState on them. + */ public setReadOnlyState(readonly: boolean): void { for (const [fluidDataStoreId, context] of this.contexts) { try { diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index d61766674076..994224364ef1 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -205,37 +205,38 @@ export interface IFluidDataStoreContextEvents extends IEvent { * */ class ContextDeltaManagerProxy extends BaseDeltaManagerProxy { - constructor(base: IDeltaManagerFull) { + constructor( + base: IDeltaManagerFull, + private readonly isReadOnly: () => boolean | undefined, + ) { super(base, { - onReadonly: (readonly, reason): void => this.setReadonly(readonly, reason), + onReadonly: (): void => { + /* readonly is controlled from the context which calls setReadonly */ + }, }); - this._readonly = base.readOnlyInfo.readonly; } public get readOnlyInfo(): ReadOnlyInfo { - if (this._readonly === this.deltaManager.readOnlyInfo.readonly) { + const readonly = this.isReadOnly(); + if (readonly === this.deltaManager.readOnlyInfo.readonly) { return this.deltaManager.readOnlyInfo; } else { - return this._readonly === true + return readonly === true ? { - readonly: true, + readonly, forced: false, permissions: undefined, storageOnly: false, } - : { readonly: false }; + : { readonly }; } } - private _readonly: boolean | undefined; public setReadonly( readonly: boolean, readonlyConnectionReason?: { reason: string; error?: IErrorBase }, ): void { - if (this._readonly !== readonly) { - this._readonly = readonly; - this.emit("readonly", readonly, readonlyConnectionReason); - } + this.emit("readonly", readonly, readonlyConnectionReason); } } @@ -270,7 +271,7 @@ export abstract class FluidDataStoreContext private readonly _contextDeltaManagerProxy: ContextDeltaManagerProxy; public get deltaManager(): IDeltaManager { - return this._deltaManager; + return this._contextDeltaManagerProxy; } public get readonly(): boolean | undefined { @@ -479,7 +480,10 @@ export abstract class FluidDataStoreContext assert(isIDeltaManagerFull(this.parentContext.deltaManager), "Invalid delta manager"); - this._deltaManager = new ContextDeltaManagerProxy(this.parentContext.deltaManager); + this._contextDeltaManagerProxy = new ContextDeltaManagerProxy( + this.parentContext.deltaManager, + () => this.readonly, + ); } public dispose(): void { @@ -679,7 +683,7 @@ export abstract class FluidDataStoreContext this.verifyNotClosed("setReadOnlyState", false /* checkTombstone */); this.channel?.setReadOnlyState?.(readonly); - this._deltaManager.setReadonly(readonly); + this._contextDeltaManagerProxy.setReadonly(readonly); } /** diff --git a/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md b/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md index ef49cba4f2f0..2d5ac0469fe3 100644 --- a/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md +++ b/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md @@ -90,7 +90,6 @@ export class FluidDataStoreRuntime extends TypedEventEmitter Date: Tue, 22 Apr 2025 16:19:38 -0700 Subject: [PATCH 14/22] more comments --- packages/runtime/container-runtime/src/dataStoreContext.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 994224364ef1..b5189ace4ff0 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -232,6 +232,13 @@ class ContextDeltaManagerProxy extends BaseDeltaManagerProxy { } } + /** + * Called by the owning datastore context to configure the readonly + * state of the delta manger that is project down to the datastore + * runtime. This state may not align with that of the true delta + * manager if the context wishes to control the read only state + * differently than the delta manager itself. + */ public setReadonly( readonly: boolean, readonlyConnectionReason?: { reason: string; error?: IErrorBase }, From c5ba247947401f86621e2f7fe626395993e5f72c Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Wed, 23 Apr 2025 09:39:37 -0700 Subject: [PATCH 15/22] PR feedback --- packages/runtime/container-runtime/src/channelCollection.ts | 2 +- packages/runtime/datastore/src/dataStoreRuntime.ts | 2 +- packages/runtime/test-runtime-utils/src/mocks.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/runtime/container-runtime/src/channelCollection.ts b/packages/runtime/container-runtime/src/channelCollection.ts index d1bbc7277eb3..8939e2f5480a 100644 --- a/packages/runtime/container-runtime/src/channelCollection.ts +++ b/packages/runtime/container-runtime/src/channelCollection.ts @@ -149,7 +149,7 @@ export function wrapContext(context: IFluidParentContext): IFluidParentContext { get attachState() { return context.attachState; }, - get readonly() { + get readonly(): boolean | undefined { return context.readonly; }, containerRuntime: context.containerRuntime, diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index 16515cf648ed..fdb90e08a346 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -692,7 +692,7 @@ export class FluidDataStoreRuntime * readonly state of this object. It should not be invoked by * any other callers. */ - public setReadOnlyState(readonly: boolean) { + public setReadOnlyState(readonly: boolean): void { this.verifyNotClosed(); if (readonly !== this._readonly) { this._readonly = readonly; diff --git a/packages/runtime/test-runtime-utils/src/mocks.ts b/packages/runtime/test-runtime-utils/src/mocks.ts index 33e2a15a3a05..502beb2210e2 100644 --- a/packages/runtime/test-runtime-utils/src/mocks.ts +++ b/packages/runtime/test-runtime-utils/src/mocks.ts @@ -1040,7 +1040,7 @@ export class MockFluidDataStoreRuntime return; } - public setReadOnlyState(readonly: boolean) { + public setReadOnlyState(readonly: boolean): void { this.readonly = readonly; } From 988ab66a9197516c4fb6dfa52a4883609a8c8645 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Wed, 23 Apr 2025 09:55:41 -0700 Subject: [PATCH 16/22] update name to notifyReadOnlyState --- .../runtime/container-runtime/src/channelCollection.ts | 6 +++--- .../runtime/container-runtime/src/containerRuntime.ts | 6 +++--- .../runtime/container-runtime/src/dataStoreContext.ts | 6 +++--- .../container-runtime/src/runtimeLayerCompatState.ts | 4 ++-- .../datastore/api-report/datastore.legacy.alpha.api.md | 2 +- packages/runtime/datastore/src/dataStoreRuntime.ts | 10 +++++----- .../api-report/runtime-definitions.legacy.alpha.api.md | 2 +- .../runtime-definitions/src/dataStoreContext.ts | 2 +- packages/runtime/runtime-definitions/src/index.ts | 2 +- .../src/runtimeLayerCompatFeatureNames.ts | 4 ++-- .../api-report/test-runtime-utils.legacy.alpha.api.md | 4 ++-- packages/runtime/test-runtime-utils/src/mocks.ts | 2 +- 12 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/runtime/container-runtime/src/channelCollection.ts b/packages/runtime/container-runtime/src/channelCollection.ts index 8939e2f5480a..2330cdbd9faa 100644 --- a/packages/runtime/container-runtime/src/channelCollection.ts +++ b/packages/runtime/container-runtime/src/channelCollection.ts @@ -1120,12 +1120,12 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable { } /** - * Enumerates the contexts and calls setReadOnlyState on them. + * Enumerates the contexts and calls notifyReadOnlyState on them. */ - public setReadOnlyState(readonly: boolean): void { + public notifyReadOnlyState(readonly: boolean): void { for (const [fluidDataStoreId, context] of this.contexts) { try { - context.setReadOnlyState(readonly); + context.notifyReadOnlyState(readonly); } catch (error) { this.mc.logger.sendErrorEvent( { diff --git a/packages/runtime/container-runtime/src/containerRuntime.ts b/packages/runtime/container-runtime/src/containerRuntime.ts index ab27d89080ab..19fce5e884db 100644 --- a/packages/runtime/container-runtime/src/containerRuntime.ts +++ b/packages/runtime/container-runtime/src/containerRuntime.ts @@ -1723,7 +1723,7 @@ export class ContainerRuntime new Map(dataStoreAliasMap), async (runtime: ChannelCollection) => provideEntryPoint, ); - this._deltaManager.on("readonly", (readonly) => this.setReadOnlyState(readonly)); + this._deltaManager.on("readonly", (readonly) => this.notifyReadOnlyState(readonly)); this.blobManager = new BlobManager({ routeContext: this.handleContext, @@ -2565,8 +2565,8 @@ export class ContainerRuntime return this._loadIdCompressor; } - public setReadOnlyState(readonly: boolean): void { - this.channelCollection.setReadOnlyState(readonly); + public notifyReadOnlyState(readonly: boolean): void { + this.channelCollection.notifyReadOnlyState(readonly); } public setConnectionState(connected: boolean, clientId?: string): void { diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index b5189ace4ff0..907c689e3efa 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -686,10 +686,10 @@ export abstract class FluidDataStoreContext this.channel!.setConnectionState(connected, clientId); } - public setReadOnlyState(readonly: boolean): void { - this.verifyNotClosed("setReadOnlyState", false /* checkTombstone */); + public notifyReadOnlyState(readonly: boolean): void { + this.verifyNotClosed("notifyReadOnlyState", false /* checkTombstone */); - this.channel?.setReadOnlyState?.(readonly); + this.channel?.notifyReadOnlyState?.(readonly); this._contextDeltaManagerProxy.setReadonly(readonly); } diff --git a/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts b/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts index f2ac4af74982..806e38791ea6 100644 --- a/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts +++ b/packages/runtime/container-runtime/src/runtimeLayerCompatState.ts @@ -11,7 +11,7 @@ import { import type { ICriticalContainerError } from "@fluidframework/container-definitions"; import { encodeHandlesInContainerRuntime, - setReadOnlyState, + notifiesReadOnlyState, } from "@fluidframework/runtime-definitions/internal"; import { UsageError } from "@fluidframework/telemetry-utils/internal"; @@ -69,7 +69,7 @@ export const runtimeCompatDetailsForDataStore: ILayerCompatDetails = { /** * The features supported by the Runtime layer across the Runtime / DataStore boundary. */ - supportedFeatures: new Set([encodeHandlesInContainerRuntime, setReadOnlyState]), + supportedFeatures: new Set([encodeHandlesInContainerRuntime, notifiesReadOnlyState]), }; /** diff --git a/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md b/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md index 2d5ac0469fe3..d7a56a927c75 100644 --- a/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md +++ b/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md @@ -67,6 +67,7 @@ export class FluidDataStoreRuntime extends TypedEventEmitter, K extends keyof T> = Omit; + [notifiesReadOnlyState]: PickRequired; } function contextSupportsFeature( @@ -290,12 +290,12 @@ export class FluidDataStoreRuntime dataStoreContext as FluidObject; validateRuntimeCompatibility(runtimeCompatDetails, this.dispose.bind(this)); - if (contextSupportsFeature(dataStoreContext, setReadOnlyState)) { + if (contextSupportsFeature(dataStoreContext, notifiesReadOnlyState)) { this._readonly = dataStoreContext.readonly; } else { this._readonly = this.dataStoreContext.deltaManager.readOnlyInfo.readonly === true; this.dataStoreContext.deltaManager.on("readonly", (readonly) => - this.setReadOnlyState(readonly), + this.notifyReadOnlyState(readonly), ); } @@ -692,7 +692,7 @@ export class FluidDataStoreRuntime * readonly state of this object. It should not be invoked by * any other callers. */ - public setReadOnlyState(readonly: boolean): void { + public notifyReadOnlyState(readonly: boolean): void { this.verifyNotClosed(); if (readonly !== this._readonly) { this._readonly = readonly; 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 49c273fb8c7e..c0c9f42823dd 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 @@ -129,6 +129,7 @@ export interface IFluidDataStoreChannel extends IDisposable { getAttachSummary(telemetryContext?: ITelemetryContext): ISummaryTreeWithStats; getGCData(fullGC?: boolean): Promise; makeVisibleAndAttachGraph(): void; + notifyReadOnlyState?(readonly: boolean): void; processMessages(messageCollection: IRuntimeMessageCollection): void; processSignal(message: IInboundSignalMessage, local: boolean): void; // (undocumented) @@ -138,7 +139,6 @@ export interface IFluidDataStoreChannel extends IDisposable { // (undocumented) setAttachState(attachState: AttachState.Attaching | AttachState.Attached): void; setConnectionState(connected: boolean, clientId?: string): any; - setReadOnlyState?(readonly: boolean): void; summarize(fullTree?: boolean, trackState?: boolean, telemetryContext?: ITelemetryContext): Promise; updateUsedRoutes(usedRoutes: string[]): void; } diff --git a/packages/runtime/runtime-definitions/src/dataStoreContext.ts b/packages/runtime/runtime-definitions/src/dataStoreContext.ts index 77e029a55df0..f49f388e33cb 100644 --- a/packages/runtime/runtime-definitions/src/dataStoreContext.ts +++ b/packages/runtime/runtime-definitions/src/dataStoreContext.ts @@ -373,7 +373,7 @@ export interface IFluidDataStoreChannel extends IDisposable { /** * Notifies this object about changes in the readonly state */ - setReadOnlyState?(readonly: boolean): void; + notifyReadOnlyState?(readonly: boolean): void; /** * Ask the DDS to resubmit a message. This could be because we reconnected and this message was not acked. diff --git a/packages/runtime/runtime-definitions/src/index.ts b/packages/runtime/runtime-definitions/src/index.ts index 8fdaca855897..e4f17c9846e9 100644 --- a/packages/runtime/runtime-definitions/src/index.ts +++ b/packages/runtime/runtime-definitions/src/index.ts @@ -55,7 +55,7 @@ export type { } from "./protocol.js"; export { encodeHandlesInContainerRuntime, - setReadOnlyState, + notifiesReadOnlyState, } from "./runtimeLayerCompatFeatureNames.js"; export type { CreateChildSummarizerNodeParam, diff --git a/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts b/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts index 2dfdc3691c1c..03c7a226bb01 100644 --- a/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts +++ b/packages/runtime/runtime-definitions/src/runtimeLayerCompatFeatureNames.ts @@ -12,8 +12,8 @@ export const encodeHandlesInContainerRuntime = "encodeHandlesInContainerRuntime" as const; /** - * This feature indicates that the datastore context will call setReadOnlyState on the + * This feature indicates that the datastore context will call notifyReadOnlyState on the * datastore runtime. * @internal */ -export const setReadOnlyState = "setReadOnlyState" as const; +export const notifiesReadOnlyState = "notifiesReadOnlyState" as const; 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 e5e5c4578d59..f96a7df1880c 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 @@ -458,6 +458,8 @@ export class MockFluidDataStoreRuntime extends EventEmitter implements IFluidDat // (undocumented) makeVisibleAndAttachGraph(): void; // (undocumented) + notifyReadOnlyState(readonly: boolean): void; + // (undocumented) get objectsRoutingContext(): IFluidHandleContext; // (undocumented) options: Record; @@ -490,8 +492,6 @@ export class MockFluidDataStoreRuntime extends EventEmitter implements IFluidDat // (undocumented) setConnectionState(connected: boolean, clientId?: string): void; // (undocumented) - setReadOnlyState(readonly: boolean): void; - // (undocumented) submitMessage(type: MessageType, content: any): null; // (undocumented) submitSignal(type: string, content: any): null; diff --git a/packages/runtime/test-runtime-utils/src/mocks.ts b/packages/runtime/test-runtime-utils/src/mocks.ts index 502beb2210e2..6a4cb1575a58 100644 --- a/packages/runtime/test-runtime-utils/src/mocks.ts +++ b/packages/runtime/test-runtime-utils/src/mocks.ts @@ -1040,7 +1040,7 @@ export class MockFluidDataStoreRuntime return; } - public setReadOnlyState(readonly: boolean): void { + public notifyReadOnlyState(readonly: boolean): void { this.readonly = readonly; } From c71914dd4e68c1dadf92d137171052ae7f334744 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Wed, 23 Apr 2025 11:43:54 -0700 Subject: [PATCH 17/22] fix delta manager proxy disposal --- .../runtime/container-runtime/src/containerRuntime.ts | 9 ++++++--- .../runtime/container-runtime/src/dataStoreContext.ts | 1 + .../runtime/container-runtime/src/deltaManagerProxies.ts | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/runtime/container-runtime/src/containerRuntime.ts b/packages/runtime/container-runtime/src/containerRuntime.ts index 19fce5e884db..eafdda2defcf 100644 --- a/packages/runtime/container-runtime/src/containerRuntime.ts +++ b/packages/runtime/container-runtime/src/containerRuntime.ts @@ -165,6 +165,7 @@ import { ContainerFluidHandleContext } from "./containerHandleContext.js"; import { channelToDataStore } from "./dataStore.js"; import { FluidDataStoreRegistry } from "./dataStoreRegistry.js"; import { + BaseDeltaManagerProxy, DeltaManagerPendingOpsProxy, DeltaManagerSummarizerProxy, } from "./deltaManagerProxies.js"; @@ -1723,7 +1724,7 @@ export class ContainerRuntime new Map(dataStoreAliasMap), async (runtime: ChannelCollection) => provideEntryPoint, ); - this._deltaManager.on("readonly", (readonly) => this.notifyReadOnlyState(readonly)); + this._deltaManager.on("readonly", this.notifyReadOnlyState); this.blobManager = new BlobManager({ routeContext: this.handleContext, @@ -2113,6 +2114,9 @@ export class ContainerRuntime this.pendingStateManager.dispose(); this.inboundBatchAggregator.dispose(); this.deltaScheduler.dispose(); + if (this._deltaManager instanceof BaseDeltaManagerProxy) { + this._deltaManager.dispose(); + } this.emit("dispose"); this.removeAllListeners(); } @@ -2565,9 +2569,8 @@ export class ContainerRuntime return this._loadIdCompressor; } - public notifyReadOnlyState(readonly: boolean): void { + private readonly notifyReadOnlyState = (readonly: boolean): void => this.channelCollection.notifyReadOnlyState(readonly); - } public setConnectionState(connected: boolean, clientId?: string): void { // Validate we have consistent state diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 907c689e3efa..e15281bb5bdb 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -508,6 +508,7 @@ export abstract class FluidDataStoreContext }) .catch((error) => {}); } + this._contextDeltaManagerProxy.dispose(); } /** diff --git a/packages/runtime/container-runtime/src/deltaManagerProxies.ts b/packages/runtime/container-runtime/src/deltaManagerProxies.ts index ff8d83e1ba6a..f649cce76fcd 100644 --- a/packages/runtime/container-runtime/src/deltaManagerProxies.ts +++ b/packages/runtime/container-runtime/src/deltaManagerProxies.ts @@ -211,6 +211,7 @@ export abstract class BaseDeltaManagerProxy this.deltaManager.off("connect", this.eventHandlers.onConnect); this.deltaManager.off("disconnect", this.eventHandlers.onDisconnect); this.deltaManager.off("readonly", this.eventHandlers.onReadonly); + this.removeAllListeners(); } public submitSignal(content: string, targetClientId?: string): void { From 58522b64312b07dde02f747da4b12653e73078f1 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Wed, 23 Apr 2025 11:52:59 -0700 Subject: [PATCH 18/22] remove tri-state --- packages/runtime/container-runtime/src/dataStoreContext.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index e15281bb5bdb..f9e81c17fde1 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -207,7 +207,7 @@ export interface IFluidDataStoreContextEvents extends IEvent { class ContextDeltaManagerProxy extends BaseDeltaManagerProxy { constructor( base: IDeltaManagerFull, - private readonly isReadOnly: () => boolean | undefined, + private readonly isReadOnly: () => boolean, ) { super(base, { onReadonly: (): void => { @@ -281,8 +281,8 @@ export abstract class FluidDataStoreContext return this._contextDeltaManagerProxy; } - public get readonly(): boolean | undefined { - return this.parentContext.readonly; + public get readonly(): boolean { + return this.parentContext.readonly ?? false; } public get connected(): boolean { From c27695d8a59c07ac84407c1485e791a90bc77742 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Wed, 23 Apr 2025 13:26:28 -0700 Subject: [PATCH 19/22] move readonly to method --- .../runtime/container-runtime/src/channelCollection.ts | 7 +++---- .../runtime/container-runtime/src/containerRuntime.ts | 4 +--- .../runtime/container-runtime/src/dataStoreContext.ts | 6 ++---- .../api-report/datastore-definitions.legacy.alpha.api.md | 4 ++-- .../runtime/datastore-definitions/src/dataStoreRuntime.ts | 2 +- .../datastore/api-report/datastore.legacy.alpha.api.md | 4 ++-- packages/runtime/datastore/src/dataStoreRuntime.ts | 8 +++----- .../api-report/runtime-definitions.legacy.alpha.api.md | 2 +- .../runtime/runtime-definitions/src/dataStoreContext.ts | 2 +- .../api-report/test-runtime-utils.legacy.alpha.api.md | 4 ++-- packages/runtime/test-runtime-utils/src/mocks.ts | 3 ++- 11 files changed, 20 insertions(+), 26 deletions(-) diff --git a/packages/runtime/container-runtime/src/channelCollection.ts b/packages/runtime/container-runtime/src/channelCollection.ts index 2330cdbd9faa..e52ebd6a0466 100644 --- a/packages/runtime/container-runtime/src/channelCollection.ts +++ b/packages/runtime/container-runtime/src/channelCollection.ts @@ -124,6 +124,7 @@ interface FluidDataStoreMessage { * Creates a shallow wrapper of {@link IFluidParentContext}. The wrapper can then have its methods overwritten as needed */ export function wrapContext(context: IFluidParentContext): IFluidParentContext { + const isReadOnly = context.isReadOnly; return { get IFluidDataStoreRegistry() { return context.IFluidDataStoreRegistry; @@ -149,9 +150,7 @@ export function wrapContext(context: IFluidParentContext): IFluidParentContext { get attachState() { return context.attachState; }, - get readonly(): boolean | undefined { - return context.readonly; - }, + isReadOnly: isReadOnly === undefined ? undefined : () => isReadOnly(), containerRuntime: context.containerRuntime, scope: context.scope, gcThrowOnTombstoneUsage: context.gcThrowOnTombstoneUsage, @@ -1134,7 +1133,7 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable { fluidDataStoreId, }), details: { - runtimeReadonly: this.parentContext.readonly, + runtimeReadonly: this.parentContext.isReadOnly?.(), readonly, }, }, diff --git a/packages/runtime/container-runtime/src/containerRuntime.ts b/packages/runtime/container-runtime/src/containerRuntime.ts index eafdda2defcf..dda8c0b06c64 100644 --- a/packages/runtime/container-runtime/src/containerRuntime.ts +++ b/packages/runtime/container-runtime/src/containerRuntime.ts @@ -1106,9 +1106,7 @@ export class ContainerRuntime return this._getAttachState(); } - public get readonly(): boolean { - return this.deltaManager.readOnlyInfo.readonly === true; - } + public readonly isReadOnly = (): boolean => this.deltaManager.readOnlyInfo.readonly === true; /** * Current session schema - defines what options are on & off. diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index f9e81c17fde1..2a6d41a06b19 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -281,9 +281,7 @@ export abstract class FluidDataStoreContext return this._contextDeltaManagerProxy; } - public get readonly(): boolean { - return this.parentContext.readonly ?? false; - } + public isReadOnly = (): boolean => this.parentContext.isReadOnly?.() ?? false; public get connected(): boolean { return this.parentContext.connected; @@ -489,7 +487,7 @@ export abstract class FluidDataStoreContext this._contextDeltaManagerProxy = new ContextDeltaManagerProxy( this.parentContext.deltaManager, - () => this.readonly, + () => this.isReadOnly(), ); } 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 eb3a9383614c..e065bc090c17 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 @@ -92,14 +92,14 @@ export interface IFluidDataStoreRuntime extends IEventProvider boolean; + // (undocumented) readonly logger: ITelemetryBaseLogger; // (undocumented) readonly objectsRoutingContext: IFluidHandleContext; // (undocumented) readonly options: Record; // (undocumented) - readonly readonly: boolean; - // (undocumented) readonly rootRoutingContext: IFluidHandleContext; submitSignal: (type: string, content: unknown, targetClientId?: string) => void; uploadBlob(blob: ArrayBufferLike, signal?: AbortSignal): Promise>; diff --git a/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts b/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts index 9d67e53ce6e7..b94d34713bac 100644 --- a/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts @@ -70,7 +70,7 @@ export interface IFluidDataStoreRuntime readonly connected: boolean; - readonly readonly: boolean; + readonly isReadOnly: () => boolean; readonly logger: ITelemetryBaseLogger; diff --git a/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md b/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md index d7a56a927c75..fa42d27c836b 100644 --- a/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md +++ b/packages/runtime/datastore/api-report/datastore.legacy.alpha.api.md @@ -65,6 +65,8 @@ export class FluidDataStoreRuntime extends TypedEventEmitter boolean; + // (undocumented) get logger(): ITelemetryLoggerExt; makeVisibleAndAttachGraph(): void; notifyReadOnlyState(readonly: boolean): void; @@ -76,8 +78,6 @@ export class FluidDataStoreRuntime extends TypedEventEmitter; // (undocumented) resolveHandle(request: IRequest): Promise; diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index 13e888423f60..ab57fbd13414 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -110,7 +110,7 @@ type PickRequired, K extends keyof T> = Omit; + [notifiesReadOnlyState]: PickRequired; } function contextSupportsFeature( @@ -159,9 +159,7 @@ export class FluidDataStoreRuntime return this.dataStoreContext.connected; } - public get readonly(): boolean { - return this._readonly; - } + public readonly isReadOnly = (): boolean => this._readonly; public get clientId(): string | undefined { return this.dataStoreContext.clientId; @@ -291,7 +289,7 @@ export class FluidDataStoreRuntime validateRuntimeCompatibility(runtimeCompatDetails, this.dispose.bind(this)); if (contextSupportsFeature(dataStoreContext, notifiesReadOnlyState)) { - this._readonly = dataStoreContext.readonly; + this._readonly = dataStoreContext.isReadOnly(); } else { this._readonly = this.dataStoreContext.deltaManager.readOnlyInfo.readonly === true; this.dataStoreContext.deltaManager.on("readonly", (readonly) => 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 c0c9f42823dd..95cd8e251a8e 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 @@ -215,11 +215,11 @@ export interface IFluidParentContext extends IProvideFluidHandleContext, Partial getQuorum(): IQuorumClients; // (undocumented) readonly idCompressor?: IIdCompressor; + readonly isReadOnly?: () => boolean; readonly loadingGroupId?: string; makeLocallyVisible(): void; // (undocumented) readonly options: Record; - readonly readonly?: boolean; readonly scope: FluidObject; setChannelDirty(address: string): void; // (undocumented) diff --git a/packages/runtime/runtime-definitions/src/dataStoreContext.ts b/packages/runtime/runtime-definitions/src/dataStoreContext.ts index f49f388e33cb..c9ec052a8d4e 100644 --- a/packages/runtime/runtime-definitions/src/dataStoreContext.ts +++ b/packages/runtime/runtime-definitions/src/dataStoreContext.ts @@ -449,7 +449,7 @@ export interface IFluidParentContext * Indicates if the parent context is readonly. If readonly is true, the consumer of * the context should also consider themselves readonly. */ - readonly readonly?: boolean; + readonly isReadOnly?: () => boolean; readonly deltaManager: IDeltaManager; readonly storage: IDocumentStorageService; readonly baseLogger: ITelemetryBaseLogger; 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 f96a7df1880c..08ce7f7ecb0c 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 @@ -449,6 +449,8 @@ export class MockFluidDataStoreRuntime extends EventEmitter implements IFluidDat // (undocumented) get isAttached(): boolean; // (undocumented) + readonly isReadOnly: () => boolean; + // (undocumented) readonly loader: ILoader; // @deprecated (undocumented) get local(): boolean; @@ -472,8 +474,6 @@ export class MockFluidDataStoreRuntime extends EventEmitter implements IFluidDat // (undocumented) quorum: MockQuorumClients; // (undocumented) - readonly: boolean; - // (undocumented) request(request: IRequest): Promise; // (undocumented) requestDataStore(request: IRequest): Promise; diff --git a/packages/runtime/test-runtime-utils/src/mocks.ts b/packages/runtime/test-runtime-utils/src/mocks.ts index 6a4cb1575a58..3db4d1daa3a1 100644 --- a/packages/runtime/test-runtime-utils/src/mocks.ts +++ b/packages/runtime/test-runtime-utils/src/mocks.ts @@ -841,7 +841,8 @@ export class MockFluidDataStoreRuntime this.registry = new Map(registry.map((factory) => [factory.type, factory])); } } - public readonly: boolean = false; + private readonly: boolean = false; + public readonly isReadOnly = () => this.readonly; public readonly entryPoint: IFluidHandleInternal; From e045be52991326f714e2dd1b83691e5e43a26f89 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Wed, 23 Apr 2025 14:38:05 -0700 Subject: [PATCH 20/22] fix test --- .../runtime-definitions/src/dataStoreContext.ts | 2 +- .../local-server-tests/src/test/readonly.spec.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/runtime/runtime-definitions/src/dataStoreContext.ts b/packages/runtime/runtime-definitions/src/dataStoreContext.ts index c9ec052a8d4e..99bc07713aa0 100644 --- a/packages/runtime/runtime-definitions/src/dataStoreContext.ts +++ b/packages/runtime/runtime-definitions/src/dataStoreContext.ts @@ -446,7 +446,7 @@ export interface IFluidParentContext readonly clientId: string | undefined; readonly connected: boolean; /** - * Indicates if the parent context is readonly. If readonly is true, the consumer of + * Indicates if the parent context is readonly. If isReadOnly is true, the consumer of * the context should also consider themselves readonly. */ readonly isReadOnly?: () => boolean; diff --git a/packages/test/local-server-tests/src/test/readonly.spec.ts b/packages/test/local-server-tests/src/test/readonly.spec.ts index 4d38dd41a397..2dd499337f72 100644 --- a/packages/test/local-server-tests/src/test/readonly.spec.ts +++ b/packages/test/local-server-tests/src/test/readonly.spec.ts @@ -50,8 +50,8 @@ class DefaultDataStore { get DefaultDataStore() { return this; } - get readonly() { - return this.runtime.readonly; + isReadOnly() { + return this.runtime.isReadOnly(); } get handle() { @@ -151,7 +151,7 @@ describe("readonly", () => { "container entrypoint must be DefaultDataStore", ); - assert(entrypoint.DefaultDataStore.readonly === false, "shouldn't be readonly"); + assert(entrypoint.DefaultDataStore.isReadOnly() === false, "shouldn't be readonly"); assert( entrypoint.DefaultDataStore.readonlyEventCount === 0, "shouldn't be any readonly events", @@ -159,7 +159,7 @@ describe("readonly", () => { await container.attach(urlResolver.createCreateNewRequest("test")); - assert(entrypoint.DefaultDataStore.readonly === false, "shouldn't be readonly"); + assert(entrypoint.DefaultDataStore.isReadOnly() === false, "shouldn't be readonly"); assert( entrypoint.DefaultDataStore.readonlyEventCount === 0, "shouldn't be any readonly events", @@ -178,7 +178,7 @@ describe("readonly", () => { "container entrypoint must be DefaultDataStore", ); - assert(entrypoint.DefaultDataStore.readonly === false, "shouldn't be readonly"); + assert(entrypoint.DefaultDataStore.isReadOnly() === false, "shouldn't be readonly"); assert( entrypoint.DefaultDataStore.readonlyEventCount === 0, "shouldn't be any readonly events", @@ -199,7 +199,7 @@ describe("readonly", () => { loadedContainer.forceReadonly?.(true); - assert(entrypoint.DefaultDataStore.readonly === true, "should be readonly"); + assert(entrypoint.DefaultDataStore.isReadOnly() === true, "should be readonly"); assert( entrypoint.DefaultDataStore.readonlyEventCount === 1, "should be any readonly events", @@ -220,7 +220,7 @@ describe("readonly", () => { "container entrypoint must be DefaultDataStore", ); - assert(entrypoint.DefaultDataStore.readonly === true, "should be readonly"); + assert(entrypoint.DefaultDataStore.isReadOnly() === true, "should be readonly"); assert( entrypoint.DefaultDataStore.readonlyEventCount === 0, "shouldn't be any readonly events", From 880b14e0c4411955977dca4213fcea5cbefcd214 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Wed, 23 Apr 2025 15:00:39 -0700 Subject: [PATCH 21/22] Update packages/runtime/datastore-definitions/src/dataStoreRuntime.ts --- .../runtime/datastore-definitions/src/dataStoreRuntime.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts b/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts index b94d34713bac..8d5184bbd972 100644 --- a/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts @@ -34,6 +34,10 @@ export interface IFluidDataStoreRuntimeEvents extends IEvent { (event: "op", listener: (message: ISequencedDocumentMessage) => void); (event: "signal", listener: (message: IInboundSignalMessage, local: boolean) => void); (event: "connected", listener: (clientId: string) => void); + /* + * The readonly event is fired when the readonly state of the datastore runtime changes. + * The isReadOnly will express the new readonly state. + */ (event: "readonly", listener: (isReadOnly: boolean) => void); } From 7fb62fe9bfa41be5efb7adbaf280b492614eaa63 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Wed, 23 Apr 2025 15:09:51 -0700 Subject: [PATCH 22/22] update comments --- .../api-report/datastore-definitions.legacy.alpha.api.md | 1 - .../runtime/datastore-definitions/src/dataStoreRuntime.ts | 8 ++++++-- packages/runtime/datastore/src/dataStoreRuntime.ts | 3 +++ 3 files changed, 9 insertions(+), 3 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 e065bc090c17..956235c6c2c2 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 @@ -91,7 +91,6 @@ export interface IFluidDataStoreRuntime extends IEventProvider boolean; // (undocumented) readonly logger: ITelemetryBaseLogger; diff --git a/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts b/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts index 8d5184bbd972..5c5517d85d02 100644 --- a/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore-definitions/src/dataStoreRuntime.ts @@ -34,9 +34,9 @@ export interface IFluidDataStoreRuntimeEvents extends IEvent { (event: "op", listener: (message: ISequencedDocumentMessage) => void); (event: "signal", listener: (message: IInboundSignalMessage, local: boolean) => void); (event: "connected", listener: (clientId: string) => void); - /* + /* * The readonly event is fired when the readonly state of the datastore runtime changes. - * The isReadOnly will express the new readonly state. + * The isReadOnly param will express the new readonly state. */ (event: "readonly", listener: (isReadOnly: boolean) => void); } @@ -74,6 +74,10 @@ export interface IFluidDataStoreRuntime readonly connected: boolean; + /** + * Get the current readonly state. + * @returns true if read-only, otherwise false + */ readonly isReadOnly: () => boolean; readonly logger: ITelemetryBaseLogger; diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index ab57fbd13414..84ac912f6188 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -159,6 +159,9 @@ export class FluidDataStoreRuntime return this.dataStoreContext.connected; } + /** + * {@inheritDoc @fluidframework/datastore-definitions#IFluidDataStoreRuntime.isReadOnly} + */ public readonly isReadOnly = (): boolean => this._readonly; public get clientId(): string | undefined {