Skip to content

Commit 532fec0

Browse files
committed
merge: 'main' into core/JsonString
2 parents 099397c + 21d45d5 commit 532fec0

File tree

166 files changed

+4017
-936
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

166 files changed

+4017
-936
lines changed

.changeset/slick-pillows-cheer.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
"fluid-framework": minor
3+
"@fluidframework/tree": minor
4+
"@fluid-experimental/tree-react-api": minor
5+
"__section": tree
6+
---
7+
Added APIs for tracking observations of SharedTree content for automatic invalidation
8+
9+
`TreeAlpha.trackObservations` and `TreeAlpha.trackObservationsOnce` have been added.
10+
These provide a way to run some operation which reads content from [TreeNodes](https://fluidframework.com/docs/api/tree/treenode-class), then run a call back when anything observed by that operation changes.
11+
12+
This functionality has also been exposed in the form of React hooks and React higher order components via the `@fluid-experimental/tree-react-api` package.
13+
It is now possible to use these utilities to implement React applications which pass TreeNodes in their props and get all necessary invalidation from tree changes handled automatically.
14+
The recommended pattern for doing this is to use `treeDataObject` or `TreeViewComponent` at the root, then `withTreeObservations` or `withMemoizedTreeObservations` for any sub-components which read from TreeNodes.
15+
Alternatively more localized changes can be made by using `PropNode` to type erase TreeNodes passed in props, then use one of the `usePropTreeNode` or `usePropTreeRecord` hooks to read from them.
16+
17+
These APIs work with both hydrated and [un-hydrated](https://fluidframework.com/docs/api/tree/unhydrated-typealias) TreeNodes.
18+
19+
### React Support
20+
21+
Here is a simple example of a React components which has an invalidation bug due to reading a mutable field from a TreeNode that was provided in a prop:
22+
23+
```typescript
24+
const builder = new SchemaFactory("example");
25+
class Item extends builder.object("Item", { text: SchemaFactory.string }) {}
26+
const ItemComponentBug = ({ item }: { item: Item }): JSX.Element => (
27+
<span>{item.text}</span> // Reading `text`, a mutable value from a React prop, causes an invalidation bug.
28+
);
29+
```
30+
31+
This bug can now easily be fixed using `withTreeObservations` or ``withMemoizedTreeObservations`:
32+
33+
```typescript
34+
const ItemComponent = withTreeObservations(
35+
({ item }: { item: Item }): JSX.Element => <span>{item.text}</span>,
36+
);
37+
```
38+
39+
For components which take in TreeNodes, but merely forward them and do not read their properties, they can use `PropTreeNode` as shown:
40+
41+
```typescript
42+
const ItemParentComponent = ({ item }: { item: PropTreeNode<Item> }): JSX.Element => (
43+
<ItemComponent item={item} />
44+
);
45+
```
46+
47+
If such a component reads from the TreeNode, it gets a compile error instead of an invalidation bug.
48+
In this case the invalidation bug would be that if `item.text` is modified, the component would not re-render.
49+
50+
```typescript
51+
const InvalidItemParentComponent = ({
52+
item,
53+
}: { item: PropTreeNode<Item> }): JSX.Element => (
54+
// @ts-expect-error PropTreeNode turns this invalidation bug into a compile error
55+
<span>{item.text}</span>
56+
);
57+
```
58+
59+
To provide access to TreeNode content in only part of a component the `usePropTreeNode` or `usePropTreeRecord` hooks can be used.
60+
61+
62+
### TreeAlpha.trackObservationsOnce Examples
63+
64+
Here is a rather minimal example of how `TreeAlpha.trackObservationsOnce` can be used:
65+
66+
```typescript
67+
cachedFoo ??= TreeAlpha.trackObservationsOnce(
68+
() => {
69+
cachedFoo = undefined;
70+
},
71+
() => nodeA.someChild.bar + nodeB.someChild.baz,
72+
).result;
73+
```
74+
75+
That is equivalent to doing the following:
76+
77+
```typescript
78+
if (cachedFoo === undefined) {
79+
cachedFoo = nodeA.someChild.bar + nodeB.someChild.baz;
80+
const invalidate = (): void => {
81+
cachedFoo = undefined;
82+
for (const u of unsubscribe) {
83+
u();
84+
}
85+
};
86+
const unsubscribe: (() => void)[] = [
87+
TreeBeta.on(nodeA, "nodeChanged", (data) => {
88+
if (data.changedProperties.has("someChild")) {
89+
invalidate();
90+
}
91+
}),
92+
TreeBeta.on(nodeB, "nodeChanged", (data) => {
93+
if (data.changedProperties.has("someChild")) {
94+
invalidate();
95+
}
96+
}),
97+
TreeBeta.on(nodeA.someChild, "nodeChanged", (data) => {
98+
if (data.changedProperties.has("bar")) {
99+
invalidate();
100+
}
101+
}),
102+
TreeBeta.on(nodeB.someChild, "nodeChanged", (data) => {
103+
if (data.changedProperties.has("baz")) {
104+
invalidate();
105+
}
106+
}),
107+
];
108+
}
109+
```
110+
111+
Here is more complete example showing how to use `TreeAlpha.trackObservationsOnce` invalidate a property derived from its tree fields.
112+
113+
```typescript
114+
const factory = new SchemaFactory("com.example");
115+
class Vector extends factory.object("Vector", {
116+
x: SchemaFactory.number,
117+
y: SchemaFactory.number,
118+
}) {
119+
#length: number | undefined = undefined;
120+
public length(): number {
121+
if (this.#length === undefined) {
122+
const result = TreeAlpha.trackObservationsOnce(
123+
() => {
124+
this.#length = undefined;
125+
},
126+
() => Math.hypot(this.x, this.y),
127+
);
128+
this.#length = result.result;
129+
}
130+
return this.#length;
131+
}
132+
}
133+
const vec = new Vector({ x: 3, y: 4 });
134+
assert.equal(vec.length(), 5);
135+
vec.x = 0;
136+
assert.equal(vec.length(), 4);
137+
```

.changeset/stupid-ties-raise.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@fluidframework/driver-definitions": minor
3+
"@fluidframework/driver-web-cache": minor
4+
"@fluidframework/odsp-driver": minor
5+
"@fluidframework/odsp-driver-definitions": minor
6+
"__section": deprecation
7+
---
8+
Move IPersistedCache types to driver-definitions
9+
10+
In an effort to decouple the driver web cache from the odsp driver a number of types have been moved from `@fluidframework/odsp-driver-definitions` to `@fluidframework/driver-definitions`. The moved types have been deprecated in `@fluidframework/odsp-driver-definitions`, and any usages should be moved to `@fluidframework/driver-definitions`.
11+
12+
The moved types are:
13+
- `IEntry`
14+
- `IFileEntry`
15+
- `ICacheEntry`
16+
- `IPersistedCache`

docs/docs/data-structures/tree/schema-definition.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ It also has three methods for specifying internal nodes; `object()`, `array()`,
1111
Use the members of the class to build a schema.
1212
See [defining a schema](../../start/tree-start.mdx#defining-a-schema) for an example.
1313

14+
:::note
15+
Once you have documents using your schema, see [Schema Evolution](./schema-evolution/index.mdx) for guidance on safely updating your schema over time.
16+
:::
17+
1418
## Object Schema
1519

1620
Use the `object()` method to create a schema for a note. Note the following about this code:
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
---
2+
title: Schema Evolution
3+
sidebar_position: 8
4+
---
5+
6+
# Schema Evolution
7+
8+
As your application grows and evolves, you may need to modify the structure of your [SharedTree](../index.mdx) data.
9+
SharedTree makes it possible to update your [schemas](../schema-definition.mdx) in ways that allow an app to load or upgrade documents created or edited by earlier versions of the app (see [understanding compatibility](./index.mdx#understanding-compatibility)).
10+
11+
:::note
12+
For detailed information about which types of changes are safe versus breaking, see [types of schema changes](./types-of-changes).
13+
:::
14+
15+
## Stored vs View Schema
16+
17+
When updating your schema, it's important to understand the difference between the two types of schemas:
18+
19+
- **View Schema**: The schema defined in your application code using [`SchemaFactory`](../../../api/fluid-framework/schemafactory-class)
20+
- **Stored Schema**: The schema persisted with each document. It describes what type of tree can be stored in the document.
21+
22+
When a document is first created, its stored schema is set using the given view schema.
23+
Afterwards, it is immutable until explicitly [upgraded](./index.mdx#schema-upgrade-process).
24+
25+
### Stored Schema Invariants
26+
27+
The stored schema follows additional constraints that don't apply to view schemas:
28+
29+
**1. Complete Description**
30+
The stored schema completely describes all possible data in the tree.
31+
Every node and field in the document is known to the stored schema.
32+
33+
**2. Additive Only**
34+
The stored schema can grow over time but cannot shrink.
35+
All nodes types present in the stored schema will exist forever, even if they stop being used by the view schema.
36+
37+
**3. Field Key Stability**
38+
Field keys on object nodes are permanent once introduced.
39+
Therefore, a field key on an object node in the stored schema can never be changed/reused/repurposed.
40+
41+
### Why These Constraints?
42+
43+
It's assumed that a Fluid application developer does not have absolute control over all user documents.
44+
Documents can persist for an unbounded length of time before they are re-opened by an application.
45+
Therefore, a Fluid application should be prepared to encounter any version of a document, even a very old one, and handle it appropriately.
46+
47+
## Schema Compatibility
48+
49+
When you open a document, SharedTree compares your application's view schema with the document's stored schema to determine compatibility:
50+
51+
- **Compatible**: Document can be opened and used normally
52+
- **Upgradeable**: Document can be upgraded to match your view schema using [`view.upgradeSchema()`](../../../api/fluid-framework/treeview-interface#upgradeschema-methodsignature)
53+
- **Incompatible**: Document cannot be opened with or upgraded to match your current view schema
54+
55+
They must be compatible or upgradeable in order for the application to work - otherwise, it cannot read or write data from/to the document.
56+
SharedTree will throw an assert if the application attempts to access the document's data with an incompatible view schema.
57+
58+
Initially, a document's stored schema is created to match the view schema at the time of creation, and therefore the view schema is compatible with the stored schema.
59+
However, later on the application might change the view schema.
60+
Such a change might be backwards compatible, forwards compatible, or neither or both.
61+
62+
### Understanding Compatibility
63+
64+
#### Backwards Compatibility
65+
66+
A change to the view schema is **backwards compatible** if the *new view schema* is compatible with the *old stored schema*.
67+
In practice, this means that the new version of the application will be able to [upgrade](./index.mdx#schema-upgrade-process) documents to the new schema (note that the process of upgrading might affect forwards compatibility - see below).
68+
69+
If a change is made that is not backwards compatible, then existing documents will "break", that is, they can no longer be loaded by the application.
70+
71+
#### Forwards Compatibility
72+
73+
A change to the view schema is **forwards compatible** if an *old view schema* is compatible with the *new stored schema* that results from the upgrade.
74+
In practice, the older version of the application must be able to "view" the new document after it has been upgraded.
75+
76+
If a change is made that is backwards compatible, but not forwards compatible, then old versions of the application will "break" when trying to load newer/upgraded documents.
77+
To prevent this, the application can employ a [staged rollout](./index.mdx#staged-rollouts).
78+
79+
### Schema Change Examples
80+
81+
#### 😎 Chill
82+
| Backwards Compatible | Forwards Compatible | Rollout Process |
83+
|-----------------------|----------------------|-----------------|
84+
||| Normal |
85+
86+
**Examples:**
87+
- Adding an optional field when compatibility flag is enabled
88+
89+
#### 🌶️ Spicy
90+
| Backwards Compatible | Forwards Compatible | Rollout Process |
91+
|-----------------------|----------------------|-----------------|
92+
||| ⚠️ Staged |
93+
94+
**Examples:**
95+
- Adding a new allowed type (TODO link to how to doc)
96+
97+
#### 👮 Go to Jail
98+
| Backwards Compatible | Forwards Compatible | Rollout Process |
99+
|-----------------------|----------------------|-----------------|
100+
||| 🚫 Forbidden |
101+
102+
**Examples:**
103+
- Adding/removing a required field
104+
- Removing an allowed type from a field
105+
- Renaming a node identifier
106+
107+
108+
### Checking Compatibility
109+
110+
Use [`TreeView.compatibility`](../../../api/fluid-framework/treeview-interface#compatibility-propertysignature) to check if your schema changes are compatible with existing documents.
111+
112+
Generally, the most important properties of the `compatibility` object to monitor are [`canView`](../../../api/fluid-framework/schemacompatibilitystatus-interface#canview-propertysignature) and [`canUpgrade`](../../../api/fluid-framework/schemacompatibilitystatus-interface#canupgrade-propertysignature).
113+
`canView` indicates that the view schema is [compatible](./index.mdx#schema-compatibility) with the stored schema while `canUpgrade` indicates that it is valid to [upgrade the schema](./index.mdx#schema-upgrade-process).
114+
115+
### Monitoring Remote Schema Changes
116+
117+
In collaborative applications, other clients may upgrade the document's schema while you're working.
118+
Listen for the `schemaChanged` event to handle these remote upgrades:
119+
120+
```typescript
121+
// Monitor for remote schema changes
122+
view.events.on("schemaChanged", () => {
123+
// Check view.compatibility here and handle accordingly
124+
});
125+
```
126+
127+
## Schema Upgrade Process
128+
129+
When you make [backwards compatible](./index.mdx#understanding-compatibility) changes to your schema, you can upgrade existing documents to use the new schema using [`view.upgradeSchema()`](../../../api/fluid-framework/treeview-interface#upgradeschema-methodsignature):
130+
131+
```typescript
132+
import { SchemaFactory, TreeViewConfiguration } from "@fluidframework/tree";
133+
134+
const factory = new SchemaFactory("WhiteboardApp");
135+
136+
// Updated schema with new optional field
137+
class Note extends factory.object("Note", {
138+
id: factory.string,
139+
x: factory.number,
140+
y: factory.number,
141+
color: factory.optional(factory.string), // New optional field
142+
}) {}
143+
144+
// Create tree view with updated schema
145+
const config = new TreeViewConfiguration({ schema: Note });
146+
const view = tree.viewWith(config);
147+
148+
// Check compatibility and upgrade if needed
149+
if (!view.compatibility.canView) {
150+
if (view.compatibility.canUpgrade) {
151+
view.upgradeSchema(); // Upgrade document to new schema
152+
console.log("Document upgraded successfully");
153+
} else {
154+
throw new Error("Schema is incompatible and cannot be upgraded");
155+
}
156+
}
157+
158+
// Now you can use the upgraded document
159+
view.root.color = "#FF0000";
160+
```
161+
162+
For detailed information about which types of changes are safe versus breaking, see [Types of Schema Changes](./types-of-changes).
163+
164+
### Staged Rollouts
165+
166+
When deploying schema changes in production, use staged rollouts to ensure compatibility between clients running different application versions.
167+
Specifically, staged rollouts allow you to handle changes that are [backwards compatible](./index.mdx#backwards-compatibility) but not [forwards compatible](./index.mdx#forwards-compatibility).
168+
169+
**Why Staged Rollouts Are Necessary**
170+
171+
In collaborative applications, multiple users with different client versions may work on the same document simultaneously.
172+
If a client upgrades a document to a new schema that older clients cannot read, those users will be locked out until they update their application.
173+
174+
**Two-Phase Deployment Strategy**
175+
176+
**Phase 1: Deploy Schema Reading Support**
177+
- Deploy application versions that can read the new schema without upgrading documents
178+
- Wait until all (or "tolerably most") users have updated to the new application version. This is called **client saturation**.
179+
- Monitor deployment metrics to ensure coverage
180+
181+
**Phase 2: Enable Document Upgrades**
182+
- Deploy code that automatically upgrades documents to the new schema
183+
- All clients can now collaborate since they support reading the upgraded schema
184+
- Monitor for any compatibility issues
185+
186+
Changes that are both [backwards compatible](./index.mdx#backwards-compatibility) and [forwards compatible](./index.mdx#forwards-compatibility) are capable of cross-version collaboration without requiring a staged rollout.
187+
In that case, new clients can read and write to the same document as old clients without either one being fundamentally incompatible.
188+
However, staged rollouts are not easily avoidable.
189+
Even seemingly simple schema changes (for example, adding a new type of node to a field) may not be forwards-compatible, cannot support cross-version collaboration and therefore require a staged rollout.
190+
191+
**Best Practices:**
192+
- Allow appropriate amount of time between release cycles to ensure client version adoption
193+
- Monitor client version distribution before enabling upgrades
194+
- Test compatibility scenarios with mixed client versions
195+
196+
## See Also
197+
198+
- [Types of Schema Changes](./types-of-changes) - Detailed guide on safe vs breaking changes
199+
- [Schema Definition](../schema-definition.mdx) - How to define schemas
200+
- [Node Types](../node-types.mdx) - Understanding different node types

0 commit comments

Comments
 (0)