Skip to content
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d535c62
docs: TODO plus clean up comment list formatting
Josmithr Aug 20, 2025
c36a3db
docs: Add note
Josmithr Aug 20, 2025
bca00a7
refactor: Hoist transaction APIs from `TreeViewAlpha` to `TreeBranch`
Josmithr Aug 20, 2025
8967eba
Remove redundant extends clause
Josmithr Aug 20, 2025
d4a3e09
docs: Update API report
Josmithr Aug 21, 2025
8d99ddd
docs: Tag `RunTransactionParams` as `@input`
Josmithr Aug 21, 2025
55a69d9
docs: Add changeset
Josmithr Aug 21, 2025
53e0f0b
Merge branch 'main' into tree/move-transaction-api
Josmithr Aug 21, 2025
1292e55
Merge branch 'main' into tree/deferred-events-during-transactions
Josmithr Aug 21, 2025
c0f54ed
Merge branch 'tree/move-transaction-api' into tree/deferred-events-du…
Josmithr Aug 21, 2025
0dc1740
WIP: Scaffolding
Josmithr Aug 21, 2025
4a07c85
refactor: Rename param
Josmithr Aug 21, 2025
ac1cbbd
refactor: Remove unneeded use of `any`
Josmithr Aug 21, 2025
270401c
WIP: Implement event pausing
Josmithr Aug 22, 2025
d694730
WIP: opt into event deferral in table schema transactions
Josmithr Aug 22, 2025
8d3526c
docs: TODO
Josmithr Aug 25, 2025
c29a0c8
Merge branch 'main' into tree/deferred-events-during-transactions
Josmithr Aug 25, 2025
9c83f13
test: Add unit tests
Josmithr Aug 25, 2025
bdfd345
docs: Fix typo
Josmithr Aug 25, 2025
8c93b42
test: Remove `.only`
Josmithr Aug 25, 2025
1ead4e4
refactor: Unify infra
Josmithr Aug 25, 2025
b5a61c4
style: Formatting
Josmithr Aug 25, 2025
724bb05
WIP: support event deferral in TreeNodeKernel
Josmithr Aug 26, 2025
6329538
refactor: Use TreeNodeKernal-level deferral
Josmithr Aug 26, 2025
1d7116d
revert: transaction changes
Josmithr Aug 26, 2025
c4e10a4
test(tree): Run TableSchema tests in both hydrated and unhydrated modes
Josmithr Aug 27, 2025
cdf9853
refactor: Cleanup disables
Josmithr Aug 27, 2025
2d874a4
Merge branch 'tree/table-test-hydration' into tree/deferred-events-du…
Josmithr Aug 27, 2025
3949ff8
Merge branch 'main' into tree/deferred-events-during-transactions
Josmithr Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/dds/tree/api-report/tree.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ export interface RunTransaction {

// @alpha @input
export interface RunTransactionParams {
readonly deferTreeEvents?: boolean;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: revert this

readonly preconditions?: readonly TransactionConstraint[];
}

Expand Down
83 changes: 56 additions & 27 deletions packages/dds/tree/src/core/tree/anchorSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,24 @@ export class AnchorSet implements AnchorLocator {
}
}

// #region TODO: document all of this

private eventsPaused: boolean = false;
private bufferedEvents: BufferedEvent[] = [];

public pauseEvents(): void {
this.eventsPaused = true;
this.bufferedEvents = [];
}

public resumeAndFlushEvents(): void {
this.eventsPaused = false;
flushBufferedEvents(this.bufferedEvents);
this.bufferedEvents = [];
}

// #endregion

/**
* Provides a visitor that can be used to mutate this {@link AnchorSet}.
*
Expand Down Expand Up @@ -768,33 +786,10 @@ export class AnchorSet implements AnchorLocator {
}
this.anchorSet.activeVisitor = undefined;

// Aggregate changedFields by node.
const eventsByNode: Map<PathNode, Set<FieldKey>> = new Map();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for reviewers: extracted into a static function below so it can be reused. This functionality doesn't consume any object state, so no functional changes were required.

for (const { node, event, changedField } of this.bufferedEvents) {
if (event === "childrenChangedAfterBatch") {
const keys = getOrCreate(eventsByNode, node, () => new Set());
keys.add(
changedField ??
fail(0xb57 /* childrenChangedAfterBatch events should have a changedField */),
);
}
}

const alreadyEmitted = new Map<PathNode, (keyof AnchorEvents)[]>();
for (const { node, event } of this.bufferedEvents) {
const emittedEvents = getOrAddEmptyToMap(alreadyEmitted, node);
if (emittedEvents.includes(event)) {
continue;
}
emittedEvents.push(event);
if (event === "childrenChangedAfterBatch") {
const changedFields =
eventsByNode.get(node) ??
fail(0xaeb /* childrenChangedAfterBatch events should have changedFields */);
node.events.emit(event, { changedFields });
} else {
node.events.emit(event);
}
if (this.anchorSet.eventsPaused) {
this.anchorSet.bufferedEvents.push(...this.bufferedEvents);
} else {
flushBufferedEvents(this.bufferedEvents);
}
},
notifyChildrenChanging(): void {
Expand Down Expand Up @@ -945,6 +940,40 @@ export class AnchorSet implements AnchorLocator {
}
}

/**
* Flushes and emits any buffered events.
*/
function flushBufferedEvents(bufferedEvents: readonly BufferedEvent[]): void {
// Aggregate changedFields by node.
const eventsByNode: Map<PathNode, Set<FieldKey>> = new Map();
for (const { node, event, changedField } of bufferedEvents) {
if (event === "childrenChangedAfterBatch") {
const keys = getOrCreate(eventsByNode, node, () => new Set());
keys.add(
changedField ??
fail(0xb57 /* childrenChangedAfterBatch events should have a changedField */),
);
}
}

const alreadyEmitted = new Map<PathNode, (keyof AnchorEvents)[]>();
for (const { node, event } of bufferedEvents) {
const emittedEvents = getOrAddEmptyToMap(alreadyEmitted, node);
if (emittedEvents.includes(event)) {
continue;
}
emittedEvents.push(event);
if (event === "childrenChangedAfterBatch") {
const changedFields =
eventsByNode.get(node) ??
fail(0xaeb /* childrenChangedAfterBatch events should have changedFields */);
node.events.emit(event, { changedFields });
} else {
node.events.emit(event);
}
}
}

/**
* Indicates the status of a `NodePath`.
*/
Expand Down
18 changes: 11 additions & 7 deletions packages/dds/tree/src/shared-tree/schematizingTreeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,16 +229,10 @@ export class SchematizingSimpleTreeView<
return this.flexTreeContext;
}

/**
* {@inheritDoc @fluidframework/shared-tree#TreeViewAlpha.runTransaction}
*/
public runTransaction<TSuccessValue, TFailureValue>(
transaction: () => TransactionCallbackStatus<TSuccessValue, TFailureValue>,
params?: RunTransactionParams,
): TransactionResultExt<TSuccessValue, TFailureValue>;
/**
* {@inheritDoc @fluidframework/shared-tree#TreeViewAlpha.runTransaction}
*/
public runTransaction(
transaction: () => VoidTransactionCallbackStatus | void,
params?: RunTransactionParams,
Expand All @@ -250,6 +244,8 @@ export class SchematizingSimpleTreeView<
| void,
params?: RunTransactionParams,
): TransactionResultExt<TSuccessValue, TFailureValue> | TransactionResult {
const { preconditions, deferTreeEvents } = params ?? {};

const addConstraints = (
constraintsOnRevert: boolean,
constraints: readonly TransactionConstraint[] = [],
Expand All @@ -259,8 +255,12 @@ export class SchematizingSimpleTreeView<

this.checkout.transaction.start();

if (deferTreeEvents === true) {
this.checkout.forest.anchors.pauseEvents();
}

// Validate preconditions before running the transaction callback.
addConstraints(false /* constraintsOnRevert */, params?.preconditions);
addConstraints(false /* constraintsOnRevert */, preconditions);
const transactionCallbackStatus = transaction();
const rollback = transactionCallbackStatus?.rollback;
const value = (
Expand All @@ -280,6 +280,10 @@ export class SchematizingSimpleTreeView<
transactionCallbackStatus?.preconditionsOnRevert,
);

if (deferTreeEvents === true) {
this.checkout.forest.anchors.resumeAndFlushEvents();
}

this.checkout.transaction.commit();
return value !== undefined
? { success: true, value: value as TSuccessValue }
Expand Down
7 changes: 7 additions & 0 deletions packages/dds/tree/src/simple-tree/api/transactionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,11 @@ export interface RunTransactionParams {
* this client and ignored by all other clients.
*/
readonly preconditions?: readonly TransactionConstraint[];

/**
* Set to defer all tree event notifications until completion of the transaction.
* @remarks TODO: note benefits and note potential pitfalls
* @defaultValue `false`
*/
readonly deferTreeEvents?: boolean;
}
12 changes: 7 additions & 5 deletions packages/dds/tree/src/simple-tree/api/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,17 @@ export interface TreeBranch extends IDisposable {

/**
* Run a 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 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 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.
* - The success of failure value as returned by the transaction function.
*
* @remarks
* This API will throw an error if the constraints are not met or something unexpected happens.
Expand Down Expand Up @@ -205,16 +206,17 @@ export interface TreeBranch extends IDisposable {
): TransactionResultExt<TSuccessValue, TFailureValue>;
/**
* Run a 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 the following:
*
* @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
* 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
* transaction was successful or not.
*
* @returns A result object of {@link TransactionResult | TransactionResult} type.
* It includes a "success" flag indicating whether the transaction was successful or not.
*
* @remarks
* This API will throw an error if the constraints are not met or something unexpected happens.
Expand Down
4 changes: 1 addition & 3 deletions packages/dds/tree/src/simple-tree/core/treeNodeKernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,7 @@ export class TreeNodeKernel {
for (const eventName of kernelEvents) {
if (events.hasListeners(eventName)) {
this.#hydrationState.offAnchorNode.add(
// Argument is forwarded between matching events, so the type should be correct.
Copy link
Contributor Author

@Josmithr Josmithr Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for reviewers: this explicit typing as any was not needed, so it was removed.

// eslint-disable-next-line @typescript-eslint/no-explicit-any
anchorNode.events.on(eventName, (arg: any) => events.emit(eventName, arg)),
anchorNode.events.on(eventName, (arg) => events.emit(eventName, arg)),
);
}
}
Expand Down
Loading
Loading