Skip to content

Conversation

@yann-achard-MS
Copy link
Contributor

@yann-achard-MS yann-achard-MS commented Dec 4, 2025

Description

Aborting a transaction used to put the tree in a state that would trigger an assert when sending some undo/redo edits to peers.
This would prevent some undo/redo edits from being sent and would put the tree in a broken state that prevented any further edits.
This issue could not have caused document corruption, so reopening the document was a possible remedy.

After this PR, aborting a transaction no longer puts the tree in such a state, so it is safe to perform undo/redo edits after that.

This bug was due to the fact that TreeCheckout overwrote its DetachedFieldIndex member with a new instance when performing a transaction rollback but had no way of informing the SharedTreeReadonlyChangeEnricher that it should now be reading from the new instance during change enrichment.

This PR fixes the bug by ensuring that a given TreeCheckout instance always uses the same DetachedFieldIndex instance. This requires introducing a way of snapshotting and (on transaction rollback) restoring the DetachedFieldIndex state that does not require replacing the instance.

This PR also reduces the DetachedFieldIndex API surface made available to SharedTreeReadonlyChangeEnricher by introducing the ReadOnlyDetachedFieldIndex interface. This is not required as part of this change and could be done in a separate PR.

Breaking Changes

None

Reviewer Guidance

Let me know if you think it's worth spinning up a separate PR for the introduction of ReadOnlyDetachedFieldIndex.

@yann-achard-MS yann-achard-MS marked this pull request as ready for review December 4, 2025 00:57
@yann-achard-MS yann-achard-MS requested review from a team as code owners December 4, 2025 00:57
Copilot AI review requested due to automatic review settings December 5, 2025 18:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a bug where aborting a transaction would cause subsequent undo/redo operations to fail with an assertion error. The fix introduces a snapshot/restore mechanism for DetachedFieldIndex instead of replacing the instance, ensuring that the SharedTreeReadonlyChangeEnricher continues to reference the correct index after transaction rollbacks.

Key changes:

  • Introduced ReadOnlyDetachedFieldIndex interface to reduce the API surface exposed to SharedTreeReadonlyChangeEnricher
  • Added createSnapshot() method and DetachedFieldIndexSnapshot interface to enable snapshotting and restoring index state
  • Modified TreeCheckout to use snapshot/restore instead of instance replacement during transaction rollbacks

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/dds/tree/src/core/tree/detachedFieldIndex.ts Adds ReadOnlyDetachedFieldIndex interface and implements snapshot/restore mechanism
packages/dds/tree/src/shared-tree/treeCheckout.ts Changes from replacing _removedRoots instance to using snapshot/restore; exposes readonly getter
packages/dds/tree/src/shared-tree/sharedTreeChangeEnricher.ts Updates to use ReadOnlyDetachedFieldIndex and IForestSubscription with "borrowed" naming convention
packages/dds/tree/src/test/core/tree/detachedFieldIndex.spec.ts Adds comprehensive tests for snapshot/restore functionality
packages/dds/tree/src/test/shared-tree-core/sharedTreeCore.spec.ts Enhances tests to verify undo/redo operations work correctly after transaction aborts
packages/dds/tree/src/test/shared-tree/sharedTreeChangeEnricher.spec.ts Updates property names to use "borrowed" prefix for consistency
packages/dds/tree/src/core/tree/index.ts Exports new ReadOnlyDetachedFieldIndex and DetachedFieldIndexSnapshot types
packages/dds/tree/src/core/index.ts Re-exports new types
.changeset/spicy-grapes-leave.md Documents the bug fix

createSnapshot(): DetachedFieldIndexSnapshot;
}

export interface DetachedFieldIndexSnapshot {
Copy link
Contributor

@noencke noencke Dec 5, 2025

Choose a reason for hiding this comment

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

I find this type (and the associated API that creates it) a little bit confusing, since usually we use the word snapshot to mean "the actual snapshotted data" but this is not that, it's actually a (tiny) tool that lets you revert some index (that it is implicitly bound to) to some state.

What do you think of renaming createSnapshot to checkpoint and have it return a simple () => void directly, then just document what it does in the @return doc?

Then the code looks like:

const restore = index.checkpoint();
// ...
restore()

Copy link
Contributor

@Josmithr Josmithr left a comment

Choose a reason for hiding this comment

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

Left a couple nitpicks, otherwise docs look great!

@yann-achard-MS yann-achard-MS enabled auto-merge (squash) December 5, 2025 22:33
@yann-achard-MS yann-achard-MS merged commit 93ec6c7 into microsoft:main Dec 5, 2025
29 checks passed
@yann-achard-MS yann-achard-MS deleted the fix-enrichment-bug branch December 8, 2025 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants