diff --git a/examples/apps/tree-comparison/src/model/legacyTreeInventoryList.ts b/examples/apps/tree-comparison/src/model/legacyTreeInventoryList.ts index 8eab78619d1d..9ce2926792bf 100644 --- a/examples/apps/tree-comparison/src/model/legacyTreeInventoryList.ts +++ b/examples/apps/tree-comparison/src/model/legacyTreeInventoryList.ts @@ -3,10 +3,17 @@ * Licensed under the MIT License. */ -import type { BuildNode, TraitLabel, TreeView, TreeViewNode } from "@fluid-experimental/tree"; +import type { + BuildNode, + ISharedTree, + TraitLabel, + TreeView, + TreeViewNode, +} from "@fluid-experimental/tree"; import { Change, EagerCheckout, + // eslint-disable-next-line import-x/no-deprecated SharedTree as LegacySharedTree, StablePlace, StableRange, @@ -69,10 +76,10 @@ export class LegacyTreeInventoryItem } export class LegacyTreeInventoryList extends DataObject implements IInventoryList { - private _tree: LegacySharedTree | undefined; + private _tree: ISharedTree | undefined; private readonly _inventoryItems = new Map(); - private get tree(): LegacySharedTree { + private get tree(): ISharedTree { if (this._tree === undefined) { throw new Error("Not initialized properly"); } @@ -120,8 +127,9 @@ export class LegacyTreeInventoryList extends DataObject implements IInventoryLis protected async initializingFirstTime(): Promise { const legacySharedTree = this.runtime.createChannel( undefined, + // eslint-disable-next-line import-x/no-deprecated LegacySharedTree.getFactory().type, - ) as LegacySharedTree; + ) as ISharedTree; const inventoryNode: BuildNode = { definition: "inventory", @@ -185,12 +193,11 @@ export class LegacyTreeInventoryList extends DataObject implements IInventoryLis */ protected async hasInitialized(): Promise { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._tree = await this.root - .get>(legacySharedTreeKey)! - .get(); + this._tree = await this.root.get>(legacySharedTreeKey)!.get(); // We must use a checkout in order to get "viewChange" events - it doesn't change any of the rest of our usage though. - const checkout = new EagerCheckout(this._tree); + // eslint-disable-next-line import-x/no-deprecated + const checkout = new EagerCheckout(this._tree as LegacySharedTree); // This event handler fires for any change to the tree, so it needs to handle all possibilities (change, add, remove). checkout.on("viewChange", (before: TreeView, after: TreeView) => { const { changed, added, removed } = before.delta(after); @@ -301,6 +308,7 @@ export class LegacyTreeInventoryList extends DataObject implements IInventoryLis export const LegacyTreeInventoryListFactory = new DataObjectFactory( "legacy-tree-inventory-list", LegacyTreeInventoryList, + // eslint-disable-next-line import-x/no-deprecated [LegacySharedTree.getFactory()], {}, ); diff --git a/examples/benchmarks/bubblebench/experimental-tree/src/main.tsx b/examples/benchmarks/bubblebench/experimental-tree/src/main.tsx index 12b448c2de49..833918e1e78f 100644 --- a/examples/benchmarks/bubblebench/experimental-tree/src/main.tsx +++ b/examples/benchmarks/bubblebench/experimental-tree/src/main.tsx @@ -4,6 +4,8 @@ */ import type { IClient } from "@fluid-example/bubblebench-common"; +import type { ISharedTree } from "@fluid-experimental/tree"; +// eslint-disable-next-line import-x/no-deprecated import { SharedTree, WriteFormat } from "@fluid-experimental/tree"; import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct/legacy"; import type { IFluidHandle } from "@fluidframework/core-interfaces"; @@ -20,10 +22,11 @@ interface IApp { */ export class Bubblebench extends DataObject { public static readonly Name = "@fluid-example/bubblebench-sharedtree"; - private maybeTree?: SharedTree = undefined; + private maybeTree?: ISharedTree = undefined; private maybeAppState?: AppState = undefined; protected async initializingFirstTime(): Promise { + // eslint-disable-next-line import-x/no-deprecated const tree = (this.maybeTree = SharedTree.create(this.runtime)); const p = TreeObjectProxy(tree, tree.currentView.root, tree.applyEdit.bind(tree)); @@ -34,7 +37,7 @@ export class Bubblebench extends DataObject { protected async initializingFromExisting(): Promise { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.maybeTree = await this.root.get>("tree")!.get(); + this.maybeTree = await this.root.get>("tree")!.get(); } protected async hasInitialized(): Promise { @@ -64,7 +67,7 @@ export class Bubblebench extends DataObject { } } - private get tree(): SharedTree { + private get tree(): ISharedTree { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.maybeTree!; } @@ -83,5 +86,6 @@ export class Bubblebench extends DataObject { export const BubblebenchInstantiationFactory = new DataObjectFactory({ type: Bubblebench.Name, ctor: Bubblebench, + // eslint-disable-next-line import-x/no-deprecated sharedObjects: [SharedTree.getFactory(WriteFormat.v0_1_1)], }); diff --git a/examples/benchmarks/bubblebench/experimental-tree/src/proxy/tree.ts b/examples/benchmarks/bubblebench/experimental-tree/src/proxy/tree.ts index 22eaae887fec..dc9ae6179d3f 100644 --- a/examples/benchmarks/bubblebench/experimental-tree/src/proxy/tree.ts +++ b/examples/benchmarks/bubblebench/experimental-tree/src/proxy/tree.ts @@ -7,7 +7,7 @@ import { Change, type ChangeNode, type NodeId, - type SharedTree, + type ISharedTree, StablePlace, StableRange, type TraitLabel, @@ -17,7 +17,7 @@ import type { Serializable } from "@fluidframework/datastore-definitions/legacy" import { NodeKind, fromJson } from "./treeutils.js"; function getChild( - tree: SharedTree, + tree: ISharedTree, nodeId: NodeId, update: (...change: Change[]) => void, ): unknown { @@ -38,7 +38,7 @@ function getChild( // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types export const TreeObjectProxy = ( - tree: SharedTree, + tree: ISharedTree, nodeId: NodeId, update: (...change: Change[]) => void, ): T => @@ -83,7 +83,7 @@ export const TreeObjectProxy = ( export class TreeArrayProxy { constructor( - private readonly tree: SharedTree, + private readonly tree: ISharedTree, private readonly nodeId: NodeId, private readonly update: (...change: Change[]) => void, ) { diff --git a/examples/benchmarks/bubblebench/experimental-tree/src/state.ts b/examples/benchmarks/bubblebench/experimental-tree/src/state.ts index 5a88d0f405f4..7964a49dbc5a 100644 --- a/examples/benchmarks/bubblebench/experimental-tree/src/state.ts +++ b/examples/benchmarks/bubblebench/experimental-tree/src/state.ts @@ -11,7 +11,7 @@ import { type SimpleClient, type IBubble, } from "@fluid-example/bubblebench-common"; -import type { Change, SharedTree } from "@fluid-experimental/tree"; +import type { Change, ISharedTree } from "@fluid-experimental/tree"; import { type TreeArrayProxy, TreeObjectProxy, fromJson } from "./proxy/index.js"; @@ -30,7 +30,7 @@ export class AppState implements IAppState { private readonly deferredChanges: Change[] = []; constructor( - private readonly tree: SharedTree, + private readonly tree: ISharedTree, private _width: number, private _height: number, numBubbles: number, diff --git a/examples/utils/bundle-size-tests/src/experimentalSharedTree.ts b/examples/utils/bundle-size-tests/src/experimentalSharedTree.ts index 45990d3fb3d0..7bcad2a03e85 100644 --- a/examples/utils/bundle-size-tests/src/experimentalSharedTree.ts +++ b/examples/utils/bundle-size-tests/src/experimentalSharedTree.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +/* eslint-disable import-x/no-deprecated -- This bundle-size test intentionally uses the deprecated SharedTree value. */ import { SharedTree } from "@fluid-experimental/tree"; export function apisToBundle(): typeof SharedTree { diff --git a/examples/version-migration/tree-shim/src/model/inventoryList.ts b/examples/version-migration/tree-shim/src/model/inventoryList.ts index 0c12bbfd922e..eb473864fcbc 100644 --- a/examples/version-migration/tree-shim/src/model/inventoryList.ts +++ b/examples/version-migration/tree-shim/src/model/inventoryList.ts @@ -3,12 +3,14 @@ * Licensed under the MIT License. */ +import type { ISharedTree } from "@fluid-experimental/tree"; import { - SharedTree as LegacySharedTree, MigrationShim, MigrationShimFactory, + SharedTreeFactory as LegacySharedTreeFactory, SharedTreeShim, SharedTreeShimFactory, + WriteFormat, } from "@fluid-experimental/tree"; import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct/legacy"; import { IFluidHandle } from "@fluidframework/core-interfaces"; @@ -36,7 +38,7 @@ const DEBUG_migrateSlowly = false; const newTreeFactory = SharedTree.getFactory(); -function migrate(legacyTree: LegacySharedTree, newTree: ITree): void { +function migrate(legacyTree: ISharedTree, newTree: ITree): void { // Revert local edits - otherwise we will be eventually inconsistent const edits = legacyTree.edits; const localEdits = [...edits.getLocalEdits()].reverse(); @@ -61,7 +63,7 @@ function migrate(legacyTree: LegacySharedTree, newTree: ITree): void { NewTreeInventoryListController.initializeTree(newTree, initialTree); } -const legacyTreeFactory = LegacySharedTree.getFactory(); +const legacyTreeFactory = new LegacySharedTreeFactory(WriteFormat.v0_1_1); const migrationShimFactory = new MigrationShimFactory( legacyTreeFactory, newTreeFactory, @@ -116,7 +118,7 @@ export class InventoryList extends DataObject implements IInventoryList, IMigrat undefined, migrationShimFactory.type, ) as MigrationShim; - const legacySharedTree = migrationShim.currentTree as LegacySharedTree; + const legacySharedTree = migrationShim.currentTree as ISharedTree; LegacyTreeInventoryListController.initializeTree(legacySharedTree); @@ -151,7 +153,7 @@ export class InventoryList extends DataObject implements IInventoryList, IMigrat .get>(treeKey)! .get(); if (this.shim.attributes.type === legacyTreeFactory.type) { - const tree = this.shim.currentTree as LegacySharedTree; + const tree = this.shim.currentTree as ISharedTree; this._model = new LegacyTreeInventoryListController(tree); const migrationShim = this.shim as MigrationShim; migrationShim.on("migrated", () => { diff --git a/examples/version-migration/tree-shim/src/model/legacyTreeInventoryListController.ts b/examples/version-migration/tree-shim/src/model/legacyTreeInventoryListController.ts index ac7756df5626..bc35c57bdcdf 100644 --- a/examples/version-migration/tree-shim/src/model/legacyTreeInventoryListController.ts +++ b/examples/version-migration/tree-shim/src/model/legacyTreeInventoryListController.ts @@ -4,10 +4,12 @@ */ import { EventEmitter } from "@fluid-example/example-utils"; +import type { ISharedTree } from "@fluid-experimental/tree"; import { BuildNode, Change, EagerCheckout, + // eslint-disable-next-line import-x/no-deprecated SharedTree as LegacySharedTree, StablePlace, StableRange, @@ -69,7 +71,7 @@ export class LegacyTreeInventoryItem } export class LegacyTreeInventoryListController extends EventEmitter implements IInventoryList { - public static initializeTree(tree: LegacySharedTree): void { + public static initializeTree(tree: ISharedTree): void { const inventoryNode: BuildNode = { definition: "inventory", traits: { @@ -126,10 +128,11 @@ export class LegacyTreeInventoryListController extends EventEmitter implements I private readonly _inventoryItems = new Map(); - public constructor(private readonly _tree: LegacySharedTree) { + public constructor(private readonly _tree: ISharedTree) { super(); // We must use a checkout in order to get "viewChange" events - it doesn't change any of the rest of our usage though. - const checkout = new EagerCheckout(this._tree); + // eslint-disable-next-line import-x/no-deprecated + const checkout = new EagerCheckout(this._tree as LegacySharedTree); // This event handler fires for any change to the tree, so it needs to handle all possibilities (change, add, remove). checkout.on("viewChange", (before: TreeView, after: TreeView) => { const { changed, added, removed } = before.delta(after); diff --git a/experimental/dds/tree/src/ISharedTree.ts b/experimental/dds/tree/src/ISharedTree.ts new file mode 100644 index 000000000000..cf365a21818c --- /dev/null +++ b/experimental/dds/tree/src/ISharedTree.ts @@ -0,0 +1,138 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { ITelemetryBaseProperties } from '@fluidframework/core-interfaces'; +import type { IFluidDataStoreRuntime } from '@fluidframework/datastore-definitions/internal'; +import type { ISharedObject, IFluidSerializer } from '@fluidframework/shared-object-base/internal'; +import type { ITelemetryLoggerExt } from '@fluidframework/telemetry-utils/internal'; + +import type { Change } from './ChangeTypes.js'; +import type { OrderedEditSet } from './EditLog.js'; +import type { AttributionId, EditId, NodeId, StableNodeId } from './Identifiers.js'; +import type { LogViewer } from './LogViewer.js'; +import type { NodeIdContext } from './NodeIdUtilities.js'; +import type { RevisionView } from './RevisionView.js'; +import type { ISharedTreeEvents } from './SharedTree.js'; +import type { + ChangeInternal, + Edit, + InternalizedChange, + SharedTreeSummaryBase, + WriteFormat, +} from './persisted-types/index.js'; + +/** + * A {@link https://github.com/microsoft/FluidFramework/blob/main/experimental/dds/tree/README.md | distributed tree}. + * @alpha + */ +export interface ISharedTree extends ISharedObject, NodeIdContext { + /** + * The UUID used for attribution of nodes created by this SharedTree. + */ + readonly attributionId: AttributionId; + + /** + * Viewer for trees defined by the edit log. This allows access to views of the tree at different revisions. + */ + readonly logViewer: LogViewer; + + /** + * Logger for SharedTree events. + */ + readonly logger: ITelemetryLoggerExt; + + /** + * @returns the current view of the tree. + */ + readonly currentView: RevisionView; + + /** + * @returns the edit history of the tree. + */ + readonly edits: OrderedEditSet; + + /** + * The write format version currently used by this `SharedTree`. + */ + getWriteFormat(): WriteFormat; + + /** + * Applies a set of changes to this tree. + */ + applyEdit(...changes: readonly Change[]): Edit; + applyEdit(changes: readonly Change[]): Edit; + + /** + * Applies a set of internal changes to this tree. + * This is exposed for internal use only. + */ + applyEditInternal(editOrChanges: Edit | readonly ChangeInternal[]): Edit; + + /** + * Converts a public Change type to an internal representation. + * This is exposed for internal use only. + */ + internalizeChange(change: Change): ChangeInternal; + + /** + * Merges `edits` from `other` into this SharedTree. + */ + mergeEditsFrom( + other: ISharedTree, + edits: Iterable>, + stableIdRemapper?: (id: StableNodeId) => StableNodeId + ): EditId[]; + + /** + * Reverts a previous edit by applying a new edit containing the inverse of the original edit's changes. + * @param editId - the edit to revert + * @returns the id of the new edit, or undefined if the original edit could not be inverted given the current tree state. + */ + revert(editId: EditId): EditId | undefined; + + /** + * Revert the given changes. + * @param changes - the changes to revert + * @param before - the revision view before the changes were originally applied + * @returns the inverse of `changes` or undefined if the changes could not be inverted for the given tree state. + */ + revertChanges(changes: readonly InternalizedChange[], before: RevisionView): ChangeInternal[] | undefined; + + /** + * Returns the attribution ID associated with the SharedTree that generated the given node ID. + */ + attributeNodeId(id: NodeId): AttributionId; + + /** + * Compares this shared tree to another for equality. + */ + equals(sharedTree: ISharedTree): boolean; + + /** + * Gets the runtime associated with this SharedTree. + */ + getRuntime(): IFluidDataStoreRuntime; + + /** + * Saves this SharedTree into a deserialized summary. + */ + saveSummary(): SharedTreeSummaryBase; + + /** + * Initialize shared tree with a deserialized summary. + */ + loadSummary(summary: SharedTreeSummaryBase): void; + + /** + * Saves this SharedTree into a serialized summary. This is used for testing. + */ + saveSerializedSummary(options?: { serializer?: IFluidSerializer }): string; + + /** + * Initialize shared tree with a serialized summary. This is used for testing. + * @returns Statistics about the loaded summary. + */ + loadSerializedSummary(blobData: string): ITelemetryBaseProperties; +} diff --git a/experimental/dds/tree/src/SharedTree.ts b/experimental/dds/tree/src/SharedTree.ts index 90b303fc3438..e30d0bd80eca 100644 --- a/experimental/dds/tree/src/SharedTree.ts +++ b/experimental/dds/tree/src/SharedTree.ts @@ -384,6 +384,8 @@ const stashedSessionId = '8477b8d5-cf6c-4673-8345-8f076a8f9bc6' as SessionId; /** * A {@link https://github.com/microsoft/FluidFramework/blob/main/experimental/dds/tree/README.md | distributed tree}. + * @deprecated Direct usage of the SharedTree class is deprecated. Use the {@link ISharedTree} interface + * to reference tree instances instead. The class will be removed from the public API surface in a future release. * @alpha */ export class SharedTree extends SharedObject implements NodeIdContext { diff --git a/experimental/dds/tree/src/index.ts b/experimental/dds/tree/src/index.ts index 748c42eb1b42..16f2724db50b 100644 --- a/experimental/dds/tree/src/index.ts +++ b/experimental/dds/tree/src/index.ts @@ -144,6 +144,7 @@ export { ISharedTreeEvents, StashedLocalOpMetadata, } from './SharedTree.js'; +export type { ISharedTree } from './ISharedTree.js'; export { StringInterner } from './StringInterner.js'; export { SharedTreeAttributes, SharedTreeFactoryType } from './publicContracts.js';