Skip to content

arrayNode anchor tracking using deltas#27697

Open
daesunp wants to merge 4 commits into
microsoft:mainfrom
daesunp:array-anchors-delta
Open

arrayNode anchor tracking using deltas#27697
daesunp wants to merge 4 commits into
microsoft:mainfrom
daesunp:array-anchors-delta

Conversation

@daesunp

@daesunp daesunp commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Description

Adds anchor tracking to arrayNodes using its deltas. This can be used in our collab text apis utilities to leverage this

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Hi! Thank you for opening this PR. Want me to review it?

Based on the diff (262 lines, 10 files), I've queued these reviewers:

  • Correctness — logic errors, race conditions, lifecycle issues
  • Security — vulnerabilities, secret exposure, injection
  • API Compatibility — breaking changes, release tags, type design
  • Performance — algorithmic regressions, memory leaks
  • Testing — coverage gaps, hollow tests

How this works

  • Adjust the reviewer set by ticking/unticking boxes above. Reviewer toggles alone don't trigger anything.

  • Tick Start review below to dispatch the review fleet.

  • After review finishes, tick Start review again to request another run — it auto-resets after each dispatch.

  • This comment updates as new commits land; your reviewer selections are preserved.

  • Start review

Comment thread packages/dds/tree/src/simple-tree/node-kinds/array/arrayNode.ts
@daesunp
daesunp marked this pull request as ready for review July 16, 2026 18:10
@daesunp
daesunp requested a review from a team as a code owner July 16, 2026 18:10
Copilot AI review requested due to automatic review settings July 16, 2026 18:10
@daesunp
daesunp requested a review from a team as a code owner July 16, 2026 18:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Comment thread packages/common/core-utils/src/test/math.spec.ts Outdated
Comment thread packages/common/core-utils/src/math.ts Outdated
Comment on lines +1493 to +1495
* After {@link ArrayPlaceAnchor.dispose} has been called the anchor stops updating and this returns the last
* tracked value, which may no longer be within the array's bounds if the array has since shrunk. Do not use the
* index of a disposed anchor as an insertion index without first re-validating it against the current length.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there any reason to support users calling index after the anchor has been disposed? We could just throw an exception in this case, or leverage our breakable infrastructure to accomplish this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated with usage error when anchor is disposed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did you take a look at our breakable infra? I think this is the sort of use case it was built for.

Comment thread packages/dds/tree/src/simple-tree/node-kinds/array/arrayNode.ts Outdated
Comment thread packages/dds/tree/src/simple-tree/node-kinds/array/arrayNode.ts Outdated
Comment thread packages/dds/tree/src/simple-tree/node-kinds/array/arrayNode.ts Outdated

@Josmithr Josmithr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I left a few comments. Overall direction looks good!

Question: can we update our selection-tracking utilities in the react package to leverage this? Seems like we should be able to remove some code over there.

@github-actions github-actions Bot added area: framework Framework is a tag for issues involving the developer framework. Eg Aqueduct area: tools area: dds Issues related to distributed data structures area: repo Repo related work area: website public api change Changes to a public API area: dds: tree changeset-present base: main PRs targeted against main branch labels Jul 21, 2026
@daesunp

daesunp commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

I made some unpushed changes that updated our selection tracking utilities, and it cleans up nicely :) Did you want me to include those changes in this PR, or in a follow up?

@github-actions

Copy link
Copy Markdown
Contributor

🔗 No broken links found! ✅

Your attention to detail is admirable.

linkcheck output

1: starting server using command "npm run serve -- --no-open"
and when url "[ 'http://127.0.0.1:3000' ]" is responding with HTTP status code 200
running tests using command "npm run check-links"


> fluid-framework-website@0.0.0 serve
> docusaurus serve --no-open

[SUCCESS] Serving "build" directory at: http://localhost:3000/

> fluid-framework-website@0.0.0 check-links
> linkcheck http://localhost:3000 --skip-file skipped-urls.txt

Crawling...

Stats:
  306030 links
    1955 destination URLs
    2209 URLs ignored
       0 warnings
       0 errors


@Josmithr

Copy link
Copy Markdown
Contributor

I made some unpushed changes that updated our selection tracking utilities, and it cleans up nicely :) Did you want me to include those changes in this PR, or in a follow up?

Either way is fine by me. Whatever is easiest.

---
Array insertion anchors now track their index from change deltas

The `@alpha` `ArrayPlaceAnchor` returned by `createArrayInsertionAnchor` now maintains its `index` incrementally from the array node's change delta instead of re-deriving it from the child that happened to sit at the anchor point when it was created. Inserts and removes before the anchor shift it, while edits after it leave it in place.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: let's link to the API docs where appropriate here, since these APIs already exist.

* Stop tracking this anchor and release any resources it holds.
* @remarks
* Call this when the anchor is no longer needed (for example when a tracked cursor position is discarded).
* Reading {@link ArrayPlaceAnchor.index} after disposal throws. Calling `dispose` more than once has no effect.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To make this a bit more future-proof, it might be better to say something like "Interacting with an anchor (including reading its properties) after it has been disposed is invalid and will throw."

node: TreeArrayNode,
currentIndex: number,
): ArrayPlaceAnchor {
// Validate the caller-provided index rather than silently correcting it: an out-of-range or non-integer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: I don't think we need to note the alternative option of silently correcting. This is another example of (subtly) referencing previous implementation details in a way that is more likely to be confusing than helpful, in my opinion.

@github-actions

Copy link
Copy Markdown
Contributor

Bundle size comparison

Base commit: c12943d4ef6cd943241773cd98542ef3003415ac
Head commit: 06ace76122dd872b2d21436b448ac651016b996c

Notable changes

  • 🔴 fluidFramework.js: parsed 394142 → 395566 (+1424), gzip 111926 → 112337 (+411)
  • 🔴 sharedTree.js: parsed 383529 → 384946 (+1417), gzip 109311 → 109715 (+404)
Per-bundle deltas

@fluid-example/bundle-size-tests

  • azureClient.js: parsed 619911 → 620271 (+360), gzip 164958 → 165076 (+118)
  • odspClient.js: parsed 592637 → 592997 (+360), gzip 159070 → 159181 (+111)
  • aqueduct.js: parsed 526400 → 526739 (+339), gzip 140839 → 140931 (+92)
  • 🔴 fluidFramework.js: parsed 394142 → 395566 (+1424), gzip 111926 → 112337 (+411)
  • 🔴 sharedTree.js: parsed 383529 → 384946 (+1417), gzip 109311 → 109715 (+404)
  • containerRuntime.js: parsed 304750 → 305068 (+318), gzip 83332 → 83411 (+79)
  • sharedString.js: parsed 175984 → 175991 (+7), gzip 49445 → 49453 (+8)
  • experimentalSharedTree.js: parsed 160798 → 160798 (0), gzip 45804 → 45804 (0)
  • matrix.js: parsed 159845 → 159852 (+7), gzip 45411 → 45418 (+7)
  • loader.js: parsed 145256 → 145270 (+14), gzip 39063 → 39076 (+13)
  • odspDriver.js: parsed 104329 → 104350 (+21), gzip 32625 → 32635 (+10)
  • directory.js: parsed 66616 → 66623 (+7), gzip 18532 → 18540 (+8)
  • 748.js: parsed 58793 → 58793 (0), gzip 17827 → 17827 (0)
  • map.js: parsed 46709 → 46716 (+7), gzip 14310 → 14318 (+8)
  • odspPrefetchSnapshot.js: parsed 45642 → 45656 (+14), gzip 15277 → 15285 (+8)
  • 985.js: parsed 44491 → 44491 (0), gzip 13726 → 13726 (0)
  • summarizerDelayLoadedModule.js: parsed 30749 → 30749 (0), gzip 7753 → 7753 (0)
  • socketModule.js: parsed 26476 → 26483 (+7), gzip 7887 → 7895 (+8)
  • createNewModule.js: parsed 12480 → 12480 (0), gzip 4786 → 4786 (0)
  • summaryModule.js: parsed 3797 → 3797 (0), gzip 1860 → 1860 (0)
  • connectionState.js: parsed 724 → 724 (0), gzip 429 → 429 (0)
  • sharedTreeAttributes.js: parsed 666 → 673 (+7), gzip 432 → 442 (+10)
  • debugAssert.js: parsed 429 → 429 (0), gzip 299 → 299 (0)
  • FluidFramework-HashFallback.js: parsed 422 → 422 (0), gzip 316 → 316 (0)

* carry both `detach` and `attach`, which is handled as a removal followed by an insertion. The per-mark behavior is
* commented inline below.
*
* This mirrors the cursor-tracking accounting used for text selections.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

And presumably the latter will go away (and just leverage this code) in a future PR? If so, might be best to just remove this line (or be sure to remember to remove it in the follow-up PR).

});

// This case sticks to the end of the array, which is not ideal, and will need to be fixed with a more sophisticated anchor implementation.
// With delta-based tracking the anchor stays in the gap the removed item occupied rather than

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment appears to be referencing the old (unwanted) behavior. Let's just remove it.

// It's good to test that this still gives a valid index and does not crash, but ideally this would anchor to the range between items rather than jumping to the end.
assert.equal(anchor.index, 2);
// The item originally at the anchor point is gone; the anchor rests between the surviving
// neighbors (now [1, 3]) instead of jumping to the end.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's remove the "instead of jumping to the end" - this is a reference to previous behavior, which is more likely to be confusing than helpful.

anchor.dispose();
});

it("collapses to the start when a removed range straddles the anchor", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: it isn't immediately clear to me what "straddles the anchor" means. Might be worth updating the wording here to be a bit more explicit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: dds: tree area: dds Issues related to distributed data structures area: framework Framework is a tag for issues involving the developer framework. Eg Aqueduct area: repo Repo related work area: tools area: website base: main PRs targeted against main branch changeset-present public api change Changes to a public API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants