Skip to content

Commit 991e432

Browse files
committed
Merge branch 'main' of https://github.com/microsoft/FluidFramework into RemoveJsonValidatorExport
2 parents 8b9098f + 91f05df commit 991e432

File tree

142 files changed

+3930
-968
lines changed

Some content is hidden

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

142 files changed

+3930
-968
lines changed

.changeset/fifty-foxes-repair.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@fluid-experimental/tree-react-api": minor
3+
"@fluidframework/react": minor
4+
"__section": breaking
5+
---
6+
The exports of @fluid-experimental/tree-react-api have been moved to the new @fluidframework/react package and placed under its /alpha exports
7+
8+
`@fluid-experimental/tree-react-api` has been adjusted to align with Fluid Framework's [API Support Levels](https://fluidframework.com/docs/build/releases-and-apitags/#api-support-levels).
9+
It has been renamed to `@fluidframework/react` and all existing APIs are now available under `@fluidframework/react/alpha`.
10+
11+
Since this package was under `@fluid-experimental`, previously it implicitly made no guarantees.
12+
Now all the APIs are `@alpha`, which also amounts to making no guarantees but makes it possible to promote APIs to `@beta` in the future to offer some stability.
13+
14+
To accommodate this change, all users of this package will need to adjust:
15+
- Package dependencies from `"@fluid-experimental/tree-react-api"` to `"@fluidframework/react"`.
16+
- Imports from `"@fluid-experimental/tree-react-api"` to `"@fluidframework/react/alpha"`.

.changeset/slick-pillows-cheer.md

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

PACKAGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ The dependencies between layers are enforced by the layer-check command._
105105

106106
| Packages | Layer Dependencies |
107107
| --- | --- |
108-
| - [@fluid-experimental/data-objects](/experimental/framework/data-objects)</br>- [@fluidframework/fluid-static](/packages/framework/fluid-static)</br>- [@fluid-experimental/property-changeset](/experimental/PropertyDDS/packages/property-changeset)</br>- [@fluid-experimental/property-common](/experimental/PropertyDDS/packages/property-common)</br>- [@fluid-internal/platform-dependent](/experimental/PropertyDDS/packages/property-common/platform-dependent) (private)</br>- [@fluid-experimental/property-dds](/experimental/PropertyDDS/packages/property-dds)</br>- [@fluid-experimental/property-properties](/experimental/PropertyDDS/packages/property-properties)</br>- [@fluid-experimental/last-edited](/experimental/framework/last-edited)</br>- [@fluid-experimental/tree-react-api](/experimental/framework/tree-react-api)</br>- [@fluidframework/agent-scheduler](/packages/framework/agent-scheduler)</br>- [@fluidframework/ai-collab](/packages/framework/ai-collab)</br>- [@fluidframework/aqueduct](/packages/framework/aqueduct)</br>- [@fluid-experimental/attributor](/packages/framework/attributor)</br>- [@fluidframework/app-insights-logger](/packages/framework/client-logger/app-insights-logger)</br>- [@fluidframework/fluid-telemetry](/packages/framework/client-logger/fluid-telemetry)</br>- [@fluid-experimental/data-object-base](/packages/framework/data-object-base)</br>- [@fluid-experimental/dds-interceptions](/packages/framework/dds-interceptions)</br>- [@fluid-experimental/oldest-client-observer](/packages/framework/oldest-client-observer)</br>- [@fluidframework/presence](/packages/framework/presence)</br>- [@fluidframework/request-handler](/packages/framework/request-handler)</br>- [@fluidframework/synthesize](/packages/framework/synthesize)</br>- [@fluidframework/tree-agent](/packages/framework/tree-agent)</br>- [@fluidframework/undo-redo](/packages/framework/undo-redo) | - [Core-Interfaces](#Core-Interfaces)</br>- [Driver-Definitions](#Driver-Definitions)</br>- [Container-Definitions](#Container-Definitions)</br>- [Core-Utils](#Core-Utils)</br>- [Client-Utils](#Client-Utils)</br>- [Telemetry-Utils](#Telemetry-Utils)</br>- [Loader](#Loader)</br>- [Runtime](#Runtime)</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp; |
108+
| - [@fluid-experimental/data-objects](/experimental/framework/data-objects)</br>- [@fluidframework/fluid-static](/packages/framework/fluid-static)</br>- [@fluid-experimental/property-changeset](/experimental/PropertyDDS/packages/property-changeset)</br>- [@fluid-experimental/property-common](/experimental/PropertyDDS/packages/property-common)</br>- [@fluid-internal/platform-dependent](/experimental/PropertyDDS/packages/property-common/platform-dependent) (private)</br>- [@fluid-experimental/property-dds](/experimental/PropertyDDS/packages/property-dds)</br>- [@fluid-experimental/property-properties](/experimental/PropertyDDS/packages/property-properties)</br>- [@fluid-experimental/last-edited](/experimental/framework/last-edited)</br>- [@fluidframework/agent-scheduler](/packages/framework/agent-scheduler)</br>- [@fluidframework/ai-collab](/packages/framework/ai-collab)</br>- [@fluidframework/aqueduct](/packages/framework/aqueduct)</br>- [@fluid-experimental/attributor](/packages/framework/attributor)</br>- [@fluidframework/app-insights-logger](/packages/framework/client-logger/app-insights-logger)</br>- [@fluidframework/fluid-telemetry](/packages/framework/client-logger/fluid-telemetry)</br>- [@fluid-experimental/data-object-base](/packages/framework/data-object-base)</br>- [@fluid-experimental/dds-interceptions](/packages/framework/dds-interceptions)</br>- [@fluid-experimental/oldest-client-observer](/packages/framework/oldest-client-observer)</br>- [@fluidframework/presence](/packages/framework/presence)</br>- [@fluidframework/react](/packages/framework/react)</br>- [@fluidframework/request-handler](/packages/framework/request-handler)</br>- [@fluidframework/synthesize](/packages/framework/synthesize)</br>- [@fluidframework/tree-agent](/packages/framework/tree-agent)</br>- [@fluidframework/undo-redo](/packages/framework/undo-redo) | - [Core-Interfaces](#Core-Interfaces)</br>- [Driver-Definitions](#Driver-Definitions)</br>- [Container-Definitions](#Container-Definitions)</br>- [Core-Utils](#Core-Utils)</br>- [Client-Utils](#Client-Utils)</br>- [Telemetry-Utils](#Telemetry-Utils)</br>- [Loader](#Loader)</br>- [Runtime](#Runtime)</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp;</br>&nbsp; |
109109

110110
### Build
111111

build-tools/packages/build-cli/src/library/layerGraph.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,6 @@ But some packages in layer A depend on packages in layer B, and likewise some in
571571

572572
this.padArraysToSameLength(packagesInCell, layersInCell, "&nbsp;");
573573
lines.push(`| Packages | Layer Dependencies |`, `| --- | --- |`);
574-
// eslint-disable-next-line unicorn/no-array-push-push
575574
lines.push(
576575
`| ${packagesInCell.join("</br>")} | ${layersInCell.join("</br>")} |${newline}`,
577576
);

examples/data-objects/inventory-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@
4040
},
4141
"dependencies": {
4242
"@fluid-example/example-utils": "workspace:~",
43-
"@fluid-experimental/tree-react-api": "workspace:~",
4443
"@fluidframework/aqueduct": "workspace:~",
4544
"@fluidframework/core-interfaces": "workspace:~",
4645
"@fluidframework/datastore-definitions": "workspace:~",
46+
"@fluidframework/react": "workspace:~",
4747
"@fluidframework/tree": "workspace:~",
4848
"fluid-framework": "workspace:~",
4949
"react": "^18.3.1"

examples/data-objects/inventory-app/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { ContainerViewRuntimeFactory } from "@fluid-example/example-utils";
7-
import type { IReactTreeDataObject } from "@fluid-experimental/tree-react-api";
7+
import type { IReactTreeDataObject } from "@fluidframework/react/alpha";
88
import * as React from "react";
99

1010
import { InventoryListFactory } from "./inventoryList.js";

examples/data-objects/inventory-app/src/inventoryList.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import { treeDataObjectInternal } from "@fluid-experimental/tree-react-api/internal";
6+
// eslint-disable-next-line import/no-internal-modules
7+
import { treeDataObjectInternal } from "@fluidframework/react/internal";
78

89
import { Inventory, treeConfiguration } from "./schema.js";
910

examples/data-objects/inventory-app/src/schema.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,34 @@ import { SchemaFactory, TreeViewConfiguration } from "@fluidframework/tree";
77

88
const builder = new SchemaFactory("com.contoso.app.inventory");
99

10+
// Below is a simple schema for this example app.
11+
// This schema has some unnecessary complexity to allow demonstrating more features, but also cuts some corners to keep the example simple.
12+
// This should not be considered a model of best practices for schema design: instead it is just an example of what is possible.
13+
14+
/**
15+
* A part in the inventory.
16+
*/
1017
export class Part extends builder.object("Part", {
18+
/**
19+
* The name of the part.
20+
* @privateRemarks
21+
* This comment shows off that comments are supported on fields, how to use them,
22+
* and that they work well with intellisense.
23+
*/
1124
name: builder.string,
1225
quantity: builder.number,
1326
}) {}
27+
28+
export class PartList extends builder.array("PartList", Part) {}
29+
30+
/**
31+
* The root of the inventory tree.
32+
* @remarks
33+
* This wrapper around {@link PartList} is not actually required: the root could be a PartList directly.
34+
* In a real app, this extra layer of indirection might be useful to allow future schema evolution.
35+
*/
1436
export class Inventory extends builder.object("Inventory", {
15-
parts: builder.array(Part),
37+
parts: PartList,
1638
}) {}
1739

1840
export const treeConfiguration = new TreeViewConfiguration({ schema: Inventory });

0 commit comments

Comments
 (0)