From 28882fa00a87a786d917711e977b7b3458c503ad Mon Sep 17 00:00:00 2001 From: Noah Encke Date: Thu, 12 Feb 2026 11:58:12 -0800 Subject: [PATCH 1/4] Add tree context API --- .changeset/green-sides-watch.md | 46 ++++++++++ .../dds/tree/api-report/tree.alpha.api.md | 32 ++++--- packages/dds/tree/src/index.ts | 2 + .../src/shared-tree/schematizingTreeView.ts | 4 + .../dds/tree/src/shared-tree/treeAlpha.ts | 54 ++++++++++++ .../dds/tree/src/simple-tree/api/index.ts | 2 + .../src/simple-tree/api/transactionTypes.ts | 31 +++---- packages/dds/tree/src/simple-tree/api/tree.ts | 86 ++++++++++++++++++- packages/dds/tree/src/simple-tree/index.ts | 2 + .../test/simple-tree/api/treeNodeApi.spec.ts | 43 ++++++++++ ...treeContext.spec.ts => treeBranch.spec.ts} | 0 .../api-report/fluid-framework.alpha.api.md | 32 ++++--- 12 files changed, 298 insertions(+), 36 deletions(-) create mode 100644 .changeset/green-sides-watch.md rename packages/dds/tree/src/test/simple-tree/{treeContext.spec.ts => treeBranch.spec.ts} (100%) diff --git a/.changeset/green-sides-watch.md b/.changeset/green-sides-watch.md new file mode 100644 index 000000000000..8d45ed5d511e --- /dev/null +++ b/.changeset/green-sides-watch.md @@ -0,0 +1,46 @@ +--- +"@fluidframework/tree": minor +"__section": tree +--- +Added `TreeAlpha.context(node)` to provide context-aware APIs for any SharedTree node, plus a new `TreeContextAlpha` surface for transactions and branch checks. + +This release introduces a node-scoped context that works for both hydrated and unhydrated nodes. The new `TreeContextAlpha` interface exposes `runTransaction` / `runTransactionAsync` and an `isBranch()` type guard. `TreeBranchAlpha` now extends `TreeContextAlpha`, so you can keep using branch APIs when available. + +### Migration +If you previously used `TreeAlpha.branch(node)` to discover a branch, switch to `TreeAlpha.context(node)` and check `isBranch()`: + +```ts +import { TreeAlpha } from "@fluidframework/tree/alpha"; + +const context = TreeAlpha.context(node); +if (context.isBranch()) { + // Same branch APIs as before + context.fork(); +} +``` + +`TreeAlpha.branch(node)` is now deprecated; prefer the context API above. + +### New transaction entry point +You can now run transactions from a node context, regardless of whether the node is hydrated: + +```ts +const context = TreeAlpha.context(node); + +// No return value +const result = context.runTransaction(() => { + node.count += 1; +}); + +// Return a value by wrapping it in `{ value }` +const resultWithValue = context.runTransaction(() => ({ value: node.count })); +``` + +For async work: + +```ts +const result = await context.runTransactionAsync(async () => { + await doWork(); + return { value: node.count }; +}); +``` diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index e755cd62e1ed..850005e7ce42 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -1453,13 +1453,11 @@ export namespace TableSchema { export function trackDirtyNodes(view: TreeViewAlpha, dirty: DirtyTreeMap): () => void; // @alpha -export type TransactionCallbackStatus = ({ +export type TransactionCallbackStatus = ((WithValue & { rollback?: false; - value: TSuccessValue; -} | { +}) | (WithValue & { rollback: true; - value: TFailureValue; -}) & { +})) & { preconditionsOnRevert?: readonly TransactionConstraintAlpha[]; }; @@ -1476,15 +1474,13 @@ export type TransactionResult = Omit, "value"> export type TransactionResultExt = TransactionResultSuccess | TransactionResultFailed; // @alpha -export interface TransactionResultFailed { +export interface TransactionResultFailed extends WithValue { success: false; - value: TFailureValue; } // @alpha -export interface TransactionResultSuccess { +export interface TransactionResultSuccess extends WithValue { success: true; - value: TSuccessValue; } // @public @sealed @@ -1498,9 +1494,11 @@ export const Tree: Tree; // @alpha @sealed export interface TreeAlpha { + // @deprecated branch(node: TreeNode): TreeBranchAlpha | undefined; child(node: TreeNode, key: string | number): TreeNode | TreeLeafValue | undefined; children(node: TreeNode): Iterable<[propertyKey: string | number, child: TreeNode | TreeLeafValue]>; + context(node: TreeNode): TreeContextAlpha; create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; exportCompressed(tree: TreeNode | TreeLeafValue, options: { idCompressor?: IIdCompressor; @@ -1573,7 +1571,7 @@ export interface TreeBranch extends IDisposable { } // @alpha @sealed -export interface TreeBranchAlpha extends TreeBranch { +export interface TreeBranchAlpha extends TreeBranch, TreeContextAlpha { applyChange(change: JsonCompatibleReadOnly): void; readonly events: Listenable_2; // (undocumented) @@ -1614,6 +1612,15 @@ export enum TreeCompressionStrategy { Uncompressed = 1 } +// @alpha +export interface TreeContextAlpha { + isBranch(): this is TreeBranchAlpha; + runTransaction(transaction: () => WithValue): TransactionResultExt; + runTransaction(transaction: () => void): TransactionResult; + runTransactionAsync(transaction: () => Promise>): Promise>; + runTransactionAsync(transaction: () => Promise): Promise; +} + // @beta @input export interface TreeEncodingOptions { readonly keys?: TKeyOptions; @@ -1911,6 +1918,11 @@ export interface WithType; } +// @alpha +export interface WithValue { + value: TValue; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index c6f25db635b4..02983187cf82 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -305,6 +305,8 @@ export { type SchemaCompatibilitySnapshotsOptions, type ArrayPlaceAnchor, createArrayInsertionAnchor, + type WithValue, + type TreeContextAlpha, } from "./simple-tree/index.js"; export { SharedTree, diff --git a/packages/dds/tree/src/shared-tree/schematizingTreeView.ts b/packages/dds/tree/src/shared-tree/schematizingTreeView.ts index 087afbcb7cae..fc8875496dfd 100644 --- a/packages/dds/tree/src/shared-tree/schematizingTreeView.ts +++ b/packages/dds/tree/src/shared-tree/schematizingTreeView.ts @@ -166,6 +166,10 @@ export class SchematizingSimpleTreeView< ); } + public isBranch(): this is TreeBranchAlpha { + return true; + } + public applyChange(change: JsonCompatibleReadOnly): void { this.checkout.applySerializedChange(change); } diff --git a/packages/dds/tree/src/shared-tree/treeAlpha.ts b/packages/dds/tree/src/shared-tree/treeAlpha.ts index 705a02da6d4c..26f574bfa35b 100644 --- a/packages/dds/tree/src/shared-tree/treeAlpha.ts +++ b/packages/dds/tree/src/shared-tree/treeAlpha.ts @@ -80,9 +80,13 @@ import { exportConcise, borrowCursorFromTreeNodeOrValue, contentSchemaSymbol, + type TreeContextAlpha, type TreeNodeSchema, getUnhydratedContext, type TreeBranchAlpha, + type TransactionResult, + type TransactionResultExt, + type WithValue, } from "../simple-tree/index.js"; import { brand, extractFromOpaque, type JsonCompatible } from "../util/index.js"; @@ -237,9 +241,17 @@ export interface TreeAlpha { * * This does not fork a new branch, but rather retrieves the _existing_ branch for the node. * To create a new branch, use e.g. {@link TreeBranch.fork | `myBranch.fork()`}. + * + * @deprecated To obtain a {@link TreeBranchAlpha | branch }, use `TreeAlpha.context(node)` to obtain a {@link TreeContextAlpha | context} and then check {@link TreeContextAlpha.isBranch | isBranch()}. */ branch(node: TreeNode): TreeBranchAlpha | undefined; + /** + * Retrieve the {@link TreeContextAlpha | context} for the given node. + * @param node - The node to query + */ + context(node: TreeNode): TreeContextAlpha; + /** * Construct tree content that is compatible with the field defined by the provided `schema`. * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. @@ -763,6 +775,10 @@ export const TreeAlpha: TreeAlpha = { return result; }, + context(node: TreeNode): TreeContextAlpha { + return this.branch(node) ?? UnhydratedTreeContext.instance; + }, + branch(node: TreeNode): TreeBranchAlpha | undefined { const kernel = getKernel(node); if (!kernel.isHydrated()) { @@ -1079,3 +1095,41 @@ function borrowFieldCursorFromTreeNodeOrValue( const mapTree = mapTreeFromCursor(cursor); return cursorForMapTreeField([mapTree]); } + +class UnhydratedTreeContext implements TreeContextAlpha { + public static instance = new UnhydratedTreeContext(); + private constructor() {} + + public isBranch(): this is TreeBranchAlpha { + return false; + } + + public runTransaction( + t: () => WithValue, + ): TransactionResultExt; + public runTransaction(t: () => void): TransactionResult; + public runTransaction( + t: () => WithValue | void, + ): TransactionResultExt | TransactionResult { + return UnhydratedTreeContext.wrapTransactionResult(t()); + } + + public runTransactionAsync( + t: () => Promise>, + ): Promise>; + public runTransactionAsync(t: () => Promise): Promise; + public async runTransactionAsync( + t: () => Promise | void>, + ): Promise | TransactionResult> { + return UnhydratedTreeContext.wrapTransactionResult(await t()); + } + + private static wrapTransactionResult( + value: WithValue | void, + ): TransactionResultExt | TransactionResult { + if (value?.value !== undefined) { + return { success: true, value: value.value }; + } + return { success: true }; + } +} diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index 2b3d24b3f502..662fd1972dde 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -22,6 +22,7 @@ export type { TreeBranch, TreeBranchAlpha, TreeBranchEvents, + TreeContextAlpha, ITreeAlpha, } from "./tree.js"; export { asTreeViewAlpha } from "./tree.js"; @@ -164,6 +165,7 @@ export { type TransactionResultExt, type TransactionResultSuccess, type TransactionResultFailed, + type WithValue, rollback, } from "./transactionTypes.js"; diff --git a/packages/dds/tree/src/simple-tree/api/transactionTypes.ts b/packages/dds/tree/src/simple-tree/api/transactionTypes.ts index 0a51e28aeaf1..1dfed2df7806 100644 --- a/packages/dds/tree/src/simple-tree/api/transactionTypes.ts +++ b/packages/dds/tree/src/simple-tree/api/transactionTypes.ts @@ -48,23 +48,28 @@ export interface NoChangeConstraint { readonly type: "noChange"; } +/** + * An interface representing a value associated with a transaction. + * @alpha + */ +export interface WithValue { + /** The user defined value. */ + value: TValue; +} + /** * The status of the transaction callback in the {@link RunTransaction | RunTransaction} API. * @alpha */ export type TransactionCallbackStatus = ( - | { + | (WithValue & { /** Indicates that the transaction callback ran successfully. */ rollback?: false; - /** The user defined value when the transaction ran successfully. */ - value: TSuccessValue; - } - | { + }) + | (WithValue & { /** Indicates that the transaction callback failed and the transaction should be rolled back. */ rollback: true; - /** The user defined value when the transaction failed. */ - value: TFailureValue; - } + }) ) & { /** * An optional list of {@link TransactionConstraintAlpha | constraints} that will be checked when the commit corresponding @@ -78,7 +83,7 @@ export type TransactionCallbackStatus = ( /** * The status of a the transaction callback in the {@link RunTransaction | RunTransaction} API where the transaction doesn't - * need to return a value. This is the same as {@link TransactionCallbackStatus} but with the `value` field omitted. This + * need to return a value. This is the same as {@link TransactionCallbackStatus} but with the `value` field omitted. * @alpha */ export type VoidTransactionCallbackStatus = Omit< @@ -90,22 +95,18 @@ export type VoidTransactionCallbackStatus = Omit< * The result of the {@link RunTransaction | RunTransaction} API when it was successful. * @alpha */ -export interface TransactionResultSuccess { +export interface TransactionResultSuccess extends WithValue { /** Indicates that the transaction was successful. */ success: true; - /** The user defined value when the transaction was successful. */ - value: TSuccessValue; } /** * The result of the {@link RunTransaction | RunTransaction} API when it failed. * @alpha */ -export interface TransactionResultFailed { +export interface TransactionResultFailed extends WithValue { /** Indicates that the transaction failed. */ success: false; - /** The user defined value when the transaction failed. */ - value: TFailureValue; } /** diff --git a/packages/dds/tree/src/simple-tree/api/tree.ts b/packages/dds/tree/src/simple-tree/api/tree.ts index f5cc7fc3f8c4..04fd12e29190 100644 --- a/packages/dds/tree/src/simple-tree/api/tree.ts +++ b/packages/dds/tree/src/simple-tree/api/tree.ts @@ -11,6 +11,9 @@ import type { RevertibleAlphaFactory, RevertibleFactory, } from "../../core/index.js"; +// This is referenced by doc comments. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { TreeStatus } from "../../feature-libraries/index.js"; import type { // This is referenced by doc comments. // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports @@ -35,6 +38,7 @@ import type { TransactionResult, TransactionResultExt, VoidTransactionCallbackStatus, + WithValue, } from "./transactionTypes.js"; import type { VerboseTree } from "./verboseTree.js"; @@ -175,6 +179,86 @@ export interface TreeBranch extends IDisposable { dispose(error?: Error): void; } +/** + * Context for a tree node which provides additional context-aware APIs. + * @alpha + */ +export interface TreeContextAlpha { + /** + * Run a synchronous transaction which applies one or more edits to the tree as a single atomic unit. + * @param transaction - The function to run as the body of the transaction. + * It may return a {@link WithValue | value }, which will be returned by the `runTransaction` call. + * @returns A result object of {@link TransactionResultExt | TransactionResultExt} type. It includes the following: + * + * - A "success" flag indicating whether the transaction was successful or not. + * - The success or failure value as returned by the transaction function. + * + * @remarks + * If `runTransaction` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether the returned context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * + * If `runTransaction` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransaction` does nothing additional). + * The transaction will always succeed. + */ + runTransaction( + transaction: () => WithValue, + ): TransactionResultExt; + /** + * Run a synchronous transaction which applies one or more edits to the tree as a single atomic unit. + * @param transaction - The function to run as the body of the transaction. + * @remarks + * If `runTransaction` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether the returned context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * + * If `runTransaction` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransaction` does nothing additional). + * The transaction will always succeed. + */ + runTransaction(transaction: () => void): TransactionResult; + /** + * Run an asynchronous transaction which applies one or more edits to the tree as a single atomic unit. + * @param transaction - The function to run as the body of the transaction. + * It may return a {@link WithValue | value }, which will be returned by the `runTransactionAsync` call. + * @returns A promise that resolves to a result object of {@link TransactionResultExt | TransactionResultExt} type. It includes the following: + * + * - A "success" flag indicating whether the transaction was successful or not. + * - The success or failure value as returned by the transaction function. + * + * @remarks + * If `runTransactionAsync` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether the returned context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * + * If `runTransactionAsync` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransactionAsync` does nothing additional). + * The transaction will always succeed. + */ + runTransactionAsync( + transaction: () => Promise>, + ): Promise>; + /** + * Run an asynchronous transaction which applies one or more edits to the tree as a single atomic unit. + * @param transaction - The function to run as the body of the transaction. + * @remarks + * If `runTransactionAsync` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether the returned context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * + * If `runTransactionAsync` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransactionAsync` does nothing additional). + * The transaction will always succeed. + */ + runTransactionAsync(transaction: () => Promise): Promise; + + /** + * True if this context is associated with a {@link TreeBranchAlpha | branch} and false if it is associated with an {@link TreeStatus.New | unhydrated } node. + * @remarks If this returns true, the context can be safely inferred or cast to {@link TreeBranchAlpha} to access additional branch-specific APIs. + * @example + * ```typescript + * const context = tree.context(someNode); + * if (context.isBranch()) { + * context.fork(); // `fork` is a method on TreeBranchAlpha, so this is only accessible if `context` is a branch context. + * } + * ``` + */ + isBranch(): this is TreeBranchAlpha; +} + /** * {@link TreeBranch} with alpha-level APIs. * @remarks @@ -183,7 +267,7 @@ export interface TreeBranch extends IDisposable { * A branch does not necessarily know the schema of its SharedTree - to convert a branch to a {@link TreeViewAlpha | view with a schema}, use {@link TreeBranchAlpha.hasRootSchema | hasRootSchema()}. * @sealed @alpha */ -export interface TreeBranchAlpha extends TreeBranch { +export interface TreeBranchAlpha extends TreeBranch, TreeContextAlpha { /** * Events for the branch */ diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index 78ca27b13cf0..e039c180f67d 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -201,6 +201,8 @@ export { type SnapshotFileSystem, type SchemaCompatibilitySnapshotsOptions, createCustomizedFluidFrameworkScopedFactory, + type TreeContextAlpha, + type WithValue, } from "./api/index.js"; export type { SimpleTreeSchema, diff --git a/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts b/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts index 9458badff95c..b3893da01596 100644 --- a/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts @@ -3796,6 +3796,49 @@ describe("treeNodeApi", () => { assert.ok(Tree.is(grandParent.parent?.child, Son)); }); }); + + describe("context", () => { + const sf = new SchemaFactory(undefined); + class Obj extends sf.object("Test", { n: sf.number }) {} + + it("for hydrated nodes is the branch", () => { + const obj = hydrate(Obj, { n: 3 }); + const branch = TreeAlpha.context(obj); + assert(branch.isBranch()); + // Compile check: `isBranch()` should downcast the context to a branch + branch.hasRootSchema(Obj); // This is a method on branches but not on context + }); + + it("for unhydrated nodes is not a branch", () => { + const obj = new Obj({ n: 3 }); + const context = TreeAlpha.context(obj); + assert.ok(!context.isBranch()); + }); + + it("has synchronous transaction APIs for both hydrated and unhydrated nodes", () => { + const hydratedObj = hydrate(Obj, { n: 3 }); + const unhydratedObj = new Obj({ n: 3 }); + for (const obj of [hydratedObj, unhydratedObj]) { + const context = TreeAlpha.context(obj); + context.runTransaction(() => (obj.n = 4)); // Transaction with no return value + const value = context.runTransaction(() => ({ value: obj.n })); // Transaction with return value + assert.equal(obj.n, value); + } + }); + + it("has async transaction APIs for both hydrated and unhydrated nodes", async () => { + const hydratedObj = hydrate(Obj, { n: 3 }); + const unhydratedObj = new Obj({ n: 3 }); + for (const obj of [hydratedObj, unhydratedObj]) { + const context = TreeAlpha.context(obj); + await context.runTransactionAsync(async () => { + obj.n = 4; // Transaction with no return value + }); + const value = await context.runTransactionAsync(async () => ({ value: obj.n })); // Transaction with return value + assert.equal(obj.n, value); + } + }); + }); }); function checkoutWithInitialTree( diff --git a/packages/dds/tree/src/test/simple-tree/treeContext.spec.ts b/packages/dds/tree/src/test/simple-tree/treeBranch.spec.ts similarity index 100% rename from packages/dds/tree/src/test/simple-tree/treeContext.spec.ts rename to packages/dds/tree/src/test/simple-tree/treeBranch.spec.ts diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md index f0bbb0888e39..66647de26333 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md @@ -1842,13 +1842,11 @@ export type TelemetryBaseEventPropertyType = string | number | boolean | undefin export function trackDirtyNodes(view: TreeViewAlpha, dirty: DirtyTreeMap): () => void; // @alpha -export type TransactionCallbackStatus = ({ +export type TransactionCallbackStatus = ((WithValue & { rollback?: false; - value: TSuccessValue; -} | { +}) | (WithValue & { rollback: true; - value: TFailureValue; -}) & { +})) & { preconditionsOnRevert?: readonly TransactionConstraintAlpha[]; }; @@ -1865,15 +1863,13 @@ export type TransactionResult = Omit, "value"> export type TransactionResultExt = TransactionResultSuccess | TransactionResultFailed; // @alpha -export interface TransactionResultFailed { +export interface TransactionResultFailed extends WithValue { success: false; - value: TFailureValue; } // @alpha -export interface TransactionResultSuccess { +export interface TransactionResultSuccess extends WithValue { success: true; - value: TSuccessValue; } // @public @@ -1890,9 +1886,11 @@ export const Tree: Tree; // @alpha @sealed export interface TreeAlpha { + // @deprecated branch(node: TreeNode): TreeBranchAlpha | undefined; child(node: TreeNode, key: string | number): TreeNode | TreeLeafValue | undefined; children(node: TreeNode): Iterable<[propertyKey: string | number, child: TreeNode | TreeLeafValue]>; + context(node: TreeNode): TreeContextAlpha; create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; exportCompressed(tree: TreeNode | TreeLeafValue, options: { idCompressor?: IIdCompressor_2; @@ -1965,7 +1963,7 @@ export interface TreeBranch extends IDisposable { } // @alpha @sealed -export interface TreeBranchAlpha extends TreeBranch { +export interface TreeBranchAlpha extends TreeBranch, TreeContextAlpha { applyChange(change: JsonCompatibleReadOnly): void; readonly events: Listenable; // (undocumented) @@ -2006,6 +2004,15 @@ export enum TreeCompressionStrategy { Uncompressed = 1 } +// @alpha +export interface TreeContextAlpha { + isBranch(): this is TreeBranchAlpha; + runTransaction(transaction: () => WithValue): TransactionResultExt; + runTransaction(transaction: () => void): TransactionResult; + runTransactionAsync(transaction: () => Promise>): Promise>; + runTransactionAsync(transaction: () => Promise): Promise; +} + // @beta @input export interface TreeEncodingOptions { readonly keys?: TKeyOptions; @@ -2303,4 +2310,9 @@ export interface WithType; } +// @alpha +export interface WithValue { + value: TValue; +} + ``` From 7c9f73cc3686474e1a50399817214e637ce2b297 Mon Sep 17 00:00:00 2001 From: Noah Encke Date: Thu, 12 Feb 2026 15:09:34 -0800 Subject: [PATCH 2/4] Cleanup docs and fix tests --- .changeset/green-sides-watch.md | 9 ++++-- packages/dds/tree/src/simple-tree/api/tree.ts | 31 ++++++++++--------- .../test/simple-tree/api/treeNodeApi.spec.ts | 6 ++-- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/.changeset/green-sides-watch.md b/.changeset/green-sides-watch.md index 8d45ed5d511e..c05afb6f2144 100644 --- a/.changeset/green-sides-watch.md +++ b/.changeset/green-sides-watch.md @@ -4,7 +4,9 @@ --- Added `TreeAlpha.context(node)` to provide context-aware APIs for any SharedTree node, plus a new `TreeContextAlpha` surface for transactions and branch checks. -This release introduces a node-scoped context that works for both hydrated and unhydrated nodes. The new `TreeContextAlpha` interface exposes `runTransaction` / `runTransactionAsync` and an `isBranch()` type guard. `TreeBranchAlpha` now extends `TreeContextAlpha`, so you can keep using branch APIs when available. +This release introduces a node-scoped context that works for nodes inserted into the tree as well as new, uninserted nodes. +The new `TreeContextAlpha` interface exposes `runTransaction` / `runTransactionAsync` and an `isBranch()` type guard. +`TreeBranchAlpha` now extends `TreeContextAlpha`, so you can keep using branch APIs when available. ### Migration If you previously used `TreeAlpha.branch(node)` to discover a branch, switch to `TreeAlpha.context(node)` and check `isBranch()`: @@ -19,7 +21,8 @@ if (context.isBranch()) { } ``` -`TreeAlpha.branch(node)` is now deprecated; prefer the context API above. +`TreeAlpha.branch(node)` is now deprecated. +Prefer the context API above. ### New transaction entry point You can now run transactions from a node context, regardless of whether the node is hydrated: @@ -36,7 +39,7 @@ const result = context.runTransaction(() => { const resultWithValue = context.runTransaction(() => ({ value: node.count })); ``` -For async work: +For asynchronous work: ```ts const result = await context.runTransactionAsync(async () => { diff --git a/packages/dds/tree/src/simple-tree/api/tree.ts b/packages/dds/tree/src/simple-tree/api/tree.ts index 04fd12e29190..6bc90c2ee2bb 100644 --- a/packages/dds/tree/src/simple-tree/api/tree.ts +++ b/packages/dds/tree/src/simple-tree/api/tree.ts @@ -188,14 +188,15 @@ export interface TreeContextAlpha { * Run a synchronous transaction which applies one or more edits to the tree as a single atomic unit. * @param transaction - The function to run as the body of the transaction. * It may return a {@link WithValue | value }, which will be returned by the `runTransaction` call. - * @returns A result object of {@link TransactionResultExt | TransactionResultExt} type. It includes the following: + * @returns A {@link TransactionResultExt | result object}. + * It includes the following: * * - A "success" flag indicating whether the transaction was successful or not. * - The success or failure value as returned by the transaction function. * * @remarks * If `runTransaction` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. - * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether the returned context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. * * If `runTransaction` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransaction` does nothing additional). * The transaction will always succeed. @@ -208,7 +209,7 @@ export interface TreeContextAlpha { * @param transaction - The function to run as the body of the transaction. * @remarks * If `runTransaction` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. - * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether the returned context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. * * If `runTransaction` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransaction` does nothing additional). * The transaction will always succeed. @@ -218,14 +219,15 @@ export interface TreeContextAlpha { * Run an asynchronous transaction which applies one or more edits to the tree as a single atomic unit. * @param transaction - The function to run as the body of the transaction. * It may return a {@link WithValue | value }, which will be returned by the `runTransactionAsync` call. - * @returns A promise that resolves to a result object of {@link TransactionResultExt | TransactionResultExt} type. It includes the following: + * @returns A promise that resolves to a {@link TransactionResultExt | result object}. + * It includes the following: * * - A "success" flag indicating whether the transaction was successful or not. * - The success or failure value as returned by the transaction function. * * @remarks * If `runTransactionAsync` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. - * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether the returned context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. * * If `runTransactionAsync` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransactionAsync` does nothing additional). * The transaction will always succeed. @@ -238,7 +240,7 @@ export interface TreeContextAlpha { * @param transaction - The function to run as the body of the transaction. * @remarks * If `runTransactionAsync` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. - * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether the returned context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. * * If `runTransactionAsync` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransactionAsync` does nothing additional). * The transaction will always succeed. @@ -297,12 +299,12 @@ export interface TreeBranchAlpha extends TreeBranch, TreeContextAlpha { /** * Run a synchronous transaction which applies one or more edits to the tree as a single atomic unit. * @param transaction - The function to run as the body of the transaction. - * It should return a status object of {@link TransactionCallbackStatus | TransactionCallbackStatus } type. + * It should return a {@link TransactionCallbackStatus | status object }. * It includes a "rollback" property which may be returned as true at any point during the transaction. This will * abort the transaction and discard any changes it made so far. * "rollback" can be set to false or left undefined to indicate that the body of the transaction has successfully run. * @param params - The optional parameters for the transaction. It includes the constraints that will be checked before the transaction begins. - * @returns A result object of {@link TransactionResultExt | TransactionResultExt} type. It includes the following: + * @returns A {@link TransactionResultExt | result object}. It includes the following: * * - A "success" flag indicating whether the transaction was successful or not. * - The success or failure value as returned by the transaction function. @@ -337,12 +339,13 @@ export interface TreeBranchAlpha extends TreeBranch, TreeContextAlpha { * @param transaction - The function to run as the body of the transaction. It may return the following: * * - Nothing to indicate that the body of the transaction has successfully run. - * - A status object of {@link VoidTransactionCallbackStatus | VoidTransactionCallbackStatus } type. It includes a "rollback" property which + * - A {@link VoidTransactionCallbackStatus | status object }. + * It includes a "rollback" property which * may be returned as true at any point during the transaction. This will abort the transaction and discard any changes it made so * far. "rollback" can be set to false or left undefined to indicate that the body of the transaction has successfully run. * * @param params - The optional parameters for the transaction. It includes the constraints that will be checked before the transaction begins. - * @returns A result object of {@link TransactionResult | TransactionResult} type. It includes a "success" flag indicating whether the + * @returns A {@link TransactionResult | result object}. It includes a "success" flag indicating whether the * transaction was successful or not. * * @remarks @@ -373,12 +376,12 @@ export interface TreeBranchAlpha extends TreeBranch, TreeContextAlpha { /** * Run an asynchronous transaction which applies one or more edits to the tree as a single atomic unit. * @param transaction - The function to run as the body of the transaction. - * It should return a promise that resolves to a status object of {@link TransactionCallbackStatus | TransactionCallbackStatus } type. + * It should return a promise that resolves to a {@link TransactionCallbackStatus | status object }. * It includes a "rollback" property which may be returned as true at any point during the transaction. This will * abort the transaction and discard any changes it made so far. * "rollback" can be set to false or left undefined to indicate that the body of the transaction has successfully run. * @param params - The optional parameters for the transaction. It includes the constraints that will be checked before the transaction begins. - * @returns A promise that resolves to a result object of {@link TransactionResultExt | TransactionResultExt} type. It includes the following: + * @returns A promise that resolves to a {@link TransactionResultExt | result object}. It includes the following: * * - A "success" flag indicating whether the transaction was successful or not. * - The success or failure value as returned by the transaction function. @@ -416,12 +419,12 @@ export interface TreeBranchAlpha extends TreeBranch, TreeContextAlpha { * @param transaction - The function to run as the body of the transaction. It must return a promise that can resolve to any of the following: * * - Nothing to indicate that the body of the transaction has successfully run. - * - A status object of {@link VoidTransactionCallbackStatus | VoidTransactionCallbackStatus } type. It includes a "rollback" property which + * - A {@link VoidTransactionCallbackStatus | status object }. It includes a "rollback" property which * may be returned as true at any point during the transaction. This will abort the transaction and discard any changes it made so * far. "rollback" can be set to false or left undefined to indicate that the body of the transaction has successfully run. * * @param params - The optional parameters for the transaction. It includes the constraints that will be checked before the transaction begins. - * @returns A promise that resolves to a result object of {@link TransactionResult | TransactionResult} type. It includes a "success" flag indicating whether the + * @returns A promise that resolves to a {@link TransactionResult | result object}. It includes a "success" flag indicating whether the * transaction was successful or not. The promise will reject if the constraints are not met or something unexpected happens. * * @remarks diff --git a/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts b/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts index b3893da01596..1432706b1aca 100644 --- a/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts @@ -3822,7 +3822,8 @@ describe("treeNodeApi", () => { const context = TreeAlpha.context(obj); context.runTransaction(() => (obj.n = 4)); // Transaction with no return value const value = context.runTransaction(() => ({ value: obj.n })); // Transaction with return value - assert.equal(obj.n, value); + assert.ok(value.success); + assert.equal(obj.n, value.value); } }); @@ -3835,7 +3836,8 @@ describe("treeNodeApi", () => { obj.n = 4; // Transaction with no return value }); const value = await context.runTransactionAsync(async () => ({ value: obj.n })); // Transaction with return value - assert.equal(obj.n, value); + assert.ok(value.success); + assert.equal(obj.n, value.value); } }); }); From f89fff833c78201250c4832d4430cf53adcbc1d5 Mon Sep 17 00:00:00 2001 From: Noah Encke Date: Thu, 12 Feb 2026 15:27:50 -0800 Subject: [PATCH 3/4] Use tsdoc selector for runTRansaction doc --- packages/dds/tree/src/simple-tree/api/tree.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/dds/tree/src/simple-tree/api/tree.ts b/packages/dds/tree/src/simple-tree/api/tree.ts index 6bc90c2ee2bb..6703e3245a83 100644 --- a/packages/dds/tree/src/simple-tree/api/tree.ts +++ b/packages/dds/tree/src/simple-tree/api/tree.ts @@ -196,7 +196,7 @@ export interface TreeContextAlpha { * * @remarks * If `runTransaction` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. - * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha.(runTransaction:1) | access to more transaction capabilities} if so. * * If `runTransaction` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransaction` does nothing additional). * The transaction will always succeed. @@ -209,7 +209,7 @@ export interface TreeContextAlpha { * @param transaction - The function to run as the body of the transaction. * @remarks * If `runTransaction` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. - * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha.(runTransaction:2) | access to more transaction capabilities} if so. * * If `runTransaction` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransaction` does nothing additional). * The transaction will always succeed. @@ -227,7 +227,7 @@ export interface TreeContextAlpha { * * @remarks * If `runTransactionAsync` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. - * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha.(runTransactionAsync:1) | access to more transaction capabilities} if so. * * If `runTransactionAsync` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransactionAsync` does nothing additional). * The transaction will always succeed. @@ -240,7 +240,7 @@ export interface TreeContextAlpha { * @param transaction - The function to run as the body of the transaction. * @remarks * If `runTransactionAsync` is invoked on the context of a {@link TreeStatus.InDocument | node in the document }, the transaction will be applied to the {@link TreeBranchAlpha | branch associated with that node}. - * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha | access to more transaction capabilities} if so. + * Use {@link TreeContextAlpha.isBranch | isBranch() } to check whether this context is associated with a branch and gain {@link TreeBranchAlpha.(runTransactionAsync:2) | access to more transaction capabilities} if so. * * If `runTransactionAsync` is invoked on the context of an {@link TreeStatus.New | unhydrated } node, it is equivalent to running the `transaction` delegate directly (i.e. `runTransactionAsync` does nothing additional). * The transaction will always succeed. From 9fbb362f45cb961da23bafb62021224e5e481ad4 Mon Sep 17 00:00:00 2001 From: Noah Encke Date: Thu, 12 Feb 2026 15:44:49 -0800 Subject: [PATCH 4/4] Update changeset --- .changeset/green-sides-watch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/green-sides-watch.md b/.changeset/green-sides-watch.md index c05afb6f2144..8d349f177627 100644 --- a/.changeset/green-sides-watch.md +++ b/.changeset/green-sides-watch.md @@ -4,7 +4,7 @@ --- Added `TreeAlpha.context(node)` to provide context-aware APIs for any SharedTree node, plus a new `TreeContextAlpha` surface for transactions and branch checks. -This release introduces a node-scoped context that works for nodes inserted into the tree as well as new, uninserted nodes. +This release introduces a node-scoped context that works for nodes inserted into the tree as well as new nodes that are not yet inserted. The new `TreeContextAlpha` interface exposes `runTransaction` / `runTransactionAsync` and an `isBranch()` type guard. `TreeBranchAlpha` now extends `TreeContextAlpha`, so you can keep using branch APIs when available.