diff --git a/.changeset/green-sides-watch.md b/.changeset/green-sides-watch.md new file mode 100644 index 000000000000..8d349f177627 --- /dev/null +++ b/.changeset/green-sides-watch.md @@ -0,0 +1,49 @@ +--- +"@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 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. + +### 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 asynchronous 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..6703e3245a83 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,88 @@ 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 {@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 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. + */ + 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 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. + */ + 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 {@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 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. + */ + 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 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. + */ + 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 +269,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 */ @@ -213,12 +299,12 @@ export interface TreeBranchAlpha extends TreeBranch { /** * 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. @@ -253,12 +339,13 @@ export interface TreeBranchAlpha extends TreeBranch { * @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 @@ -289,12 +376,12 @@ export interface TreeBranchAlpha extends TreeBranch { /** * 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. @@ -332,12 +419,12 @@ export interface TreeBranchAlpha extends TreeBranch { * @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/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..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 @@ -3796,6 +3796,51 @@ 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.ok(value.success); + assert.equal(obj.n, value.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.ok(value.success); + assert.equal(obj.n, value.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; +} + ```