Skip to content

Conversation

@daesunp
Copy link
Contributor

@daesunp daesunp commented Nov 17, 2025

Description

SharedTree eventing doc for potential future work in this area

@github-actions github-actions bot added area: dds Issues related to distributed data structures area: dds: tree base: main PRs targeted against main branch labels Nov 17, 2025
@github-actions
Copy link
Contributor

🔗 No broken links found! ✅

Your attention to detail is admirable.

linkcheck output


> fluid-framework-docs-site@0.0.0 ci:check-links /home/runner/work/FluidFramework/FluidFramework/docs
> start-server-and-test "npm run serve -- --no-open" 3000 check-links

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-docs-site@0.0.0 serve
> docusaurus serve --no-open

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

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

Crawling...

Stats:
  250224 links
    1778 destination URLs
    2015 URLs ignored
       0 warnings
       0 errors



Currently there is no single API that covers root field level mutations or “whole document” changes. Instead, our current way to do this are the following.

### Root field level mutations**
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd just delete this. It's not a particularly special case.


```ts
treeView.events.on(“rootNodeChanged”, listener, {
resubscribeOnRootReplace: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think resubscribeOnRootReplace makes sense here. These events are not subscribing to the root node. They are subscribing to the root field, which is parented by the view. The root node can come and go, but the view does not change. So if you register rootNodeChanged or rootTreeChanged, those events will never go away until you deregister them. If you replace the root node, they still don't go away. There's no replacement that could make them go away, so the flag doesn't do anything.

resubscribeOnReplace makes sense on all the other events - Tree.on(...) because they are associated with nodes. If the node that the event is registered on gets replaced, then a flag like this could help avoid the annoying "I gotta watch the parent too" logic. It's an interesting idea, however:

  1. It could probably be written as a helper/wrapper rather than a flag, which might be preferable.
  2. It's unclear what happens if the node is removed - not replaced - but then another node comes back in the same place later. Is that still considered a replace?
  3. Suppose we add event parameters that are specific to the node type (as is proposed elsewhere in this document). For example, array nodes provide information about the indexes that moved/changed, or map nodes provide the keys that were added/removed/changed. Then, what happens if you subscribe to an array node with resubscribeOnReplace but then the array node gets replaced with a map node? Would we fire the event but without supplying the index information? Would we skip firing the event? Is it effectively deregistered, or does it come back if the map node is then later replaced with an array node?

```ts
treeView.events.on(“rootNodeChanged”, listener, {
resubscribeOnRootReplace: boolean;
includeSchemaChanges?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're adding a new event to simplify things, I think we should lean into the "simple". We probably just don't want this here at all. To handle schema changes, the user should use a different event (probably schemaChanged).


#### Option A: Previous Tree State Payload

Provide the previous tree state in the event payload. As this feature is something that is requested by a few select customers, it may not be very widely used. If this is the case, simply providing a “node snapshot” of the previous tree state, and letting the users decide what to do would be the easiest way.
Copy link
Contributor

Choose a reason for hiding this comment

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

Have you researched what the cost would be to provide a "node snapshot"? Even for just the shallow "nodeChanged" event, you'd need to snapshot the entire subtree under the registered node (so the user can walk down it). You could even argue that you need to snapshot the entire tree, because the user might walk up with Tree.parent, though, we could probably reasonably restrict that if we needed to.

  1. What's the development cost of this - how hard is it to keep around a snapshot?
  2. What's the performance cost of this? I suspect it's cheaper if using a ChunkedForest but potentially expensive if using an ObjectForest, though I can't say for sure.

Yann might be a good resource to answer these questions.

Copy link
Contributor

@yann-achard-MS yann-achard-MS Nov 20, 2025

Choose a reason for hiding this comment

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

I expect we'd have to clone the forest (which is indeed cheap on ChunkedForest but potentially expensive if using an ObjectForest) and keep an anchor to the node in the cloned forest.
This is probably not hard to implement (Craig would know best).

It does seems like a questionable API choice since comparing states is a bit error prone (especially when considering the maintenance burden of the code doing the comparison), and it is lossy (e.g., not clear what was moved where or what was changed).
I'm not saying I couldn't be convinced, but I would want to learn more about the app scenario to see if we can offer something better.

Provide the previous tree state in the event payload. As this feature is something that is requested by a few select customers, it may not be very widely used. If this is the case, simply providing a “node snapshot” of the previous tree state, and letting the users decide what to do would be the easiest way.

We may consider the following to represent the previous tree state:
- JsonableTree representation of node/subtree
Copy link
Contributor

Choose a reason for hiding this comment

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

My first thought is that the previous tree state would be actual nodes. You'd be allowed to use them for the lifetime of the event. If you wanted to dump them to a JSON format you of course could. Giving the user a JSON format directly is not going to be convenient for the typical use case, where the user will want to read data out of the previous tree. Users probably want to do things like:

(prev, cur) => {
  if (prev.foo.bar !== cur.foo.bar) {
    // Update my bar cache
  }
}

It would be cumbersome to make them do:

(prev, cur) => {
  const prev2 = TreeAlpha.importVerbose(MySchema, prev); // Verbose, or whatever format it is
  if (prev2.foo.bar !== cur.foo.bar) {
    // Update my bar cache
  }
}


#### Option B: Flat List

Provide a list of UpPaths of the nodes changed. If we feel like the size of the list can become an issue for large subtrees, we can also have configuration settings to bound the number of nodes changed to “first n edits”.
Copy link
Contributor

Choose a reason for hiding this comment

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

If bound to the "first n edits", then you miss out on the changes from the remaining n edits - that wouldn't provide a good experience, would it? Or are you imagining you could page through the edits? You could have it be a generator rather than an array.


---

#### Option C: Aggregate Summary
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting idea. I suspect it would only be useful in niche scenarios.


---

#### Option A: Sparse Tree
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious if this is a good place to weave in something like trackDirtyNodes. The API being that you can lookup a given node and see how it changed. We could combine that with this API potentially. Have a map of Nodes to Status, where the status has information about how it changed, as well as its path. I guess maybe a place to start is - what query does the user want to do? Do they want to take a node and see how it changed? Or do they want to take a path and see what changed at that path? Or do they just want to explore/discover paths that changed? Or all of those? Might be good to talk to Nick about scenarios that he would have found useful over the years of writing his apps.

Copy link
Contributor

@yann-achard-MS yann-achard-MS Nov 20, 2025

Choose a reason for hiding this comment

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

One scenario that came up several times in Designer was answering the question "which fields had their subtrees edited?", though from the point of view of the app it might be nicer to just have an "on field edited" event that can be used to listen to either shallow or shallow & deep changes.


However, for nodes without a parent node (detached trees / root node), one potential solution could be to design some sort of object that contains different properties for the different scenarios.

For instance, although root nodes and detached nodes do not have a parent node, it is still technically parented by the view/branch. For these cases, we could have the object contain the view/branch (and maybe the detachedRootId/rootFieldKey) to identify what exactly we are trying to subscribe to. We can return a parent-like object for eventing purposes, to cover all different scenarios.
Copy link
Contributor

Choose a reason for hiding this comment

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

What are the "different scenarios"? I don't really understand what this API is for or how it is useful. Can you provide motivating example scenarios? Let's see the API in action to demonstrate the why and the how.


### Proposal

Provide a feature to batch event reactions using the browser’s requestAnimationFrame API. The events will all still get emitted at the same rate but would be flushed once per frame based on the priority of the event type. This minimizes per-frame computation and may provide a smoother UI experience.
Copy link
Contributor

Choose a reason for hiding this comment

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

This whole scheme is somewhat complicated. What's the actual API proposal? What should SharedTree do? Some of this sounds quite prescriptive and not something that all apps will want. We should start by building primitives and see what apps build on top of them.

For example, we start with something as simple as:

// Only fires when flush is called
const { flush, off } = Tree.on(node, "nodeChangedBatched", () => {})

Does that meet the needs - can they build what they need on top of that?

JSON Patch can also be a good solution which provides a standardized, well documented format for the delta. However, JSON Patch isn’t completely lossless and can lose some of our edit semantics.

Some of the information that can be lost in JSON Patch are:
- multi-count moves
Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I can tell, moves are possible in JSON Patch, so multi-count moves would require multiple patches. That's different from the information being lost.


JSON Patch can also be a good solution which provides a standardized, well documented format for the delta. However, JSON Patch isn’t completely lossless and can lose some of our edit semantics.

Some of the information that can be lost in JSON Patch are:
Copy link
Contributor

Choose a reason for hiding this comment

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

One downside of JSON Patch is that you need multiple patches in order to represent multiple insertions/deletions/moves. That may be unacceptable for things like text editing.

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 base: main PRs targeted against main branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants