Skip to content

Commit b286e27

Browse files
Remove TreeNode "marinated" state (#25648)
## Description Simply TreeNode state space by removing the marinated state.
1 parent 6d27e86 commit b286e27

28 files changed

+318
-323
lines changed

packages/dds/tree/src/feature-libraries/flex-tree/flexTreeTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export interface FlexTreeOptionalField extends FlexTreeField {
398398
export type FlexTreeTypedField<Kind extends FlexFieldKind> =
399399
Kind extends typeof FieldKinds.sequence
400400
? FlexTreeSequenceField
401-
: Kind extends typeof FieldKinds.required
401+
: Kind extends typeof FieldKinds.required | typeof FieldKinds.identifier
402402
? FlexTreeRequiredField
403403
: Kind extends typeof FieldKinds.optional
404404
? FlexTreeOptionalField

packages/dds/tree/src/shared-tree/schematizingTreeView.ts

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@ import type {
1212
import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
1313
import { UsageError } from "@fluidframework/telemetry-utils/internal";
1414

15-
import { anchorSlot } from "../core/index.js";
15+
import { anchorSlot, rootFieldKey } from "../core/index.js";
1616
import {
1717
type NodeIdentifierManager,
1818
defaultSchemaPolicy,
1919
cursorForMapTreeField,
2020
TreeStatus,
2121
Context,
22+
type FlexTreeOptionalField,
23+
type FlexTreeUnknownUnboxed,
24+
FieldKinds,
25+
type FlexTreeRequiredField,
2226
} from "../feature-libraries/index.js";
2327
import {
2428
type ImplicitFieldSchema,
@@ -38,7 +42,7 @@ import {
3842
type UnsafeUnknownSchema,
3943
type TreeBranch,
4044
type TreeBranchEvents,
41-
getOrCreateInnerNode,
45+
getInnerNode,
4246
getKernel,
4347
type VoidTransactionCallbackStatus,
4448
type TransactionCallbackStatus,
@@ -117,6 +121,11 @@ export class SchematizingSimpleTreeView<
117121
*/
118122
private midUpgrade = false;
119123

124+
/**
125+
* Hydration work deferred until Context has been created.
126+
*/
127+
private pendingHydration?: () => void;
128+
120129
private readonly rootFieldSchema: FieldSchema;
121130
public readonly breaker: Breakable;
122131

@@ -174,6 +183,13 @@ export class SchematizingSimpleTreeView<
174183

175184
this.runSchemaEdit(() => {
176185
const schema = toInitialSchema(this.config.schema);
186+
// This has to be the contextless version, since when "initialize" is called (right after this),
187+
// it will do a schema change which would dispose of the current context (see inside `update`).
188+
// Thus using the current context (if any) would hydrate nodes then
189+
// immediately dispose them instead of having them actually be useable after initialize.
190+
// For this to work,
191+
// the hydration must be deferred until after the content is inserted into the tree and the final schema change is done (for required roots),
192+
// but before any user event could could run.
177193
const mapTree = prepareForInsertionContextless(
178194
content as InsertableContent | undefined,
179195
this.rootFieldSchema,
@@ -183,6 +199,19 @@ export class SchematizingSimpleTreeView<
183199
},
184200
this,
185201
schema.rootFieldSchema,
202+
(batches, doHydration) => {
203+
assert(this.pendingHydration === undefined, "pendingHydration already set");
204+
this.pendingHydration = () => {
205+
assert(batches.length <= 1, "initialize should at most one hydration batch");
206+
for (const batch of batches) {
207+
doHydration(batch, {
208+
parent: undefined,
209+
parentField: rootFieldKey,
210+
parentIndex: 0,
211+
});
212+
}
213+
};
214+
},
186215
);
187216

188217
this.checkout.transaction.start();
@@ -350,7 +379,10 @@ export class SchematizingSimpleTreeView<
350379
// TODO: provide a better event: this.view.flexTree.on(????) and/or integrate with with the normal event code paths.
351380

352381
// Track what the root was before to be able to detect changes.
353-
let lastRoot: ReadableField<TRootSchema> = this.root;
382+
// This uses the flex tree root to avoid demanding the simple-tree TreeNode when it might not be hydrated yet.
383+
let lastRoot: FlexTreeUnknownUnboxed | undefined = (
384+
this.flexTreeContext.root as FlexTreeOptionalField
385+
).content;
354386

355387
this.flexTreeViewUnregisterCallbacks.add(
356388
this.checkout.events.on("afterBatch", () => {
@@ -359,8 +391,8 @@ export class SchematizingSimpleTreeView<
359391
// - The rootChanged event will already be raised at the end of the current upgrade
360392
// - It doesn't matter that `lastRoot` isn't updated in this case, because `update` will be called again before the upgrade
361393
// completes (at which point this callback and the `lastRoot` captured here will be out of scope anyway)
362-
if (!this.midUpgrade && lastRoot !== this.root) {
363-
lastRoot = this.root;
394+
if (!this.midUpgrade && lastRoot !== this.flexRoot.content) {
395+
lastRoot = this.flexRoot.content;
364396
this.events.emit("rootChanged");
365397
}
366398
}),
@@ -374,6 +406,10 @@ export class SchematizingSimpleTreeView<
374406
);
375407

376408
if (!this.midUpgrade) {
409+
assert(
410+
this.pendingHydration === undefined,
411+
"no nodes should be pending hydration when triggering events that could access nodes",
412+
);
377413
this.events.emit("schemaChanged");
378414
this.events.emit("rootChanged");
379415
}
@@ -386,6 +422,9 @@ export class SchematizingSimpleTreeView<
386422
} finally {
387423
this.midUpgrade = false;
388424
}
425+
// Ensure hydration is flushed before events run which could access nodes.
426+
this.pendingHydration?.();
427+
this.pendingHydration = undefined;
389428
this.events.emit("schemaChanged");
390429
this.events.emit("rootChanged");
391430
}
@@ -426,15 +465,25 @@ export class SchematizingSimpleTreeView<
426465
}
427466
}
428467

429-
public get root(): ReadableField<TRootSchema> {
468+
private get flexRoot(): FlexTreeOptionalField | FlexTreeRequiredField {
430469
this.breaker.use();
431470
if (!this.compatibility.canView) {
432471
throw new UsageError(
433472
"Document is out of schema. Check TreeView.compatibility before accessing TreeView.root.",
434473
);
435474
}
436475
const view = this.getFlexTreeContext();
437-
return tryGetTreeNodeForField(view.root) as ReadableField<TRootSchema>;
476+
assert(
477+
view.root.is(FieldKinds.optional) ||
478+
view.root.is(FieldKinds.required) ||
479+
view.root.is(FieldKinds.identifier),
480+
"unexpected root field kind",
481+
);
482+
return view.root;
483+
}
484+
485+
public get root(): ReadableField<TRootSchema> {
486+
return tryGetTreeNodeForField(this.flexRoot) as ReadableField<TRootSchema>;
438487
}
439488

440489
public set root(newRoot: InsertableField<TRootSchema>) {
@@ -499,7 +548,7 @@ export function addConstraintsToTransaction(
499548
for (const constraint of constraints) {
500549
switch (constraint.type) {
501550
case "nodeInDocument": {
502-
const node = getOrCreateInnerNode(constraint.node);
551+
const node = getInnerNode(constraint.node);
503552
const nodeStatus = getKernel(constraint.node).getStatus();
504553
if (nodeStatus !== TreeStatus.InDocument) {
505554
const revertText = constraintsOnRevert ? " on revert" : "";

packages/dds/tree/src/shared-tree/tree.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
type TreeNode,
1212
type TreeNodeApi,
1313
type TreeView,
14-
getOrCreateInnerNode,
14+
getInnerNode,
1515
treeNodeApi,
1616
rollback,
1717
type TransactionConstraint,
@@ -447,7 +447,7 @@ export function runTransaction<
447447
} else {
448448
const node = treeOrNode as TNode;
449449
const t = transaction as (node: TNode) => TResult | typeof rollback;
450-
const context = getOrCreateInnerNode(node).context;
450+
const context = getInnerNode(node).context;
451451
if (context.isHydrated() === false) {
452452
throw new UsageError(
453453
"Transactions cannot be run on Unhydrated nodes. Transactions apply to a TreeView and Unhydrated nodes are not part of a TreeView.",

packages/dds/tree/src/shared-tree/treeAlpha.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import {
4444
unhydratedFlexTreeFromInsertable,
4545
getOrCreateNodeFromInnerNode,
4646
getOrCreateNodeFromInnerUnboxedNode,
47-
getOrCreateInnerNode,
47+
getInnerNode,
4848
NodeKind,
4949
tryGetTreeNodeForField,
5050
isObjectNodeSchema,
@@ -881,7 +881,7 @@ export const TreeAlpha: TreeAlpha = {
881881
node: TreeNode,
882882
propertyKey: string | number,
883883
): TreeNode | TreeLeafValue | undefined => {
884-
const flexNode = getOrCreateInnerNode(node);
884+
const flexNode = getInnerNode(node);
885885
debugAssert(
886886
() => !flexNode.context.isDisposed() || "The provided tree node has been disposed.",
887887
);
@@ -951,7 +951,7 @@ export const TreeAlpha: TreeAlpha = {
951951
},
952952

953953
children(node: TreeNode): [propertyKey: string | number, child: TreeNode | TreeLeafValue][] {
954-
const flexNode = getOrCreateInnerNode(node);
954+
const flexNode = getInnerNode(node);
955955
debugAssert(
956956
() => !flexNode.context.isDisposed() || "The provided tree node has been disposed.",
957957
);

packages/dds/tree/src/simple-tree/api/treeBeta.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export function borrowCursorFromTreeNodeOrValue(
238238
return cursorFromVerbose(node, {});
239239
}
240240
const kernel = getKernel(node);
241-
const cursor = kernel.getOrCreateInnerNode().borrowCursor();
241+
const cursor = kernel.getInnerNode().borrowCursor();
242242
return cursor;
243243
}
244244

@@ -339,7 +339,7 @@ export const TreeBeta: TreeBeta = {
339339
}
340340

341341
const kernel = getKernel(node);
342-
const cursor = kernel.getOrCreateInnerNode().borrowCursor();
342+
const cursor = kernel.getInnerNode().borrowCursor();
343343

344344
// To handle when the node transitively contains unknown optional fields,
345345
// derive the context from the source node's stored schema which has stored schema for any such fields and their contents.

packages/dds/tree/src/simple-tree/api/treeNodeApi.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
tryGetTreeNodeSchema,
2929
getOrCreateNodeFromInnerNode,
3030
typeSchemaSymbol,
31-
getOrCreateInnerNode,
31+
getInnerNode,
3232
type TreeLeafValue,
3333
type ImplicitAllowedTypes,
3434
type TreeNodeFromImplicitAllowedTypes,
@@ -141,7 +141,7 @@ export interface TreeNodeApi {
141141
*/
142142
export const treeNodeApi: TreeNodeApi = {
143143
parent(node: TreeNode): TreeNode | undefined {
144-
const editNode = getOrCreateInnerNode(node).parentField.parent.parent;
144+
const editNode = getInnerNode(node).parentField.parent.parent;
145145
if (editNode === undefined) {
146146
return undefined;
147147
}
@@ -306,7 +306,7 @@ export function getIdentifierFromNode(
306306
return undefined;
307307
}
308308

309-
const flexNode = getOrCreateInnerNode(node);
309+
const flexNode = getInnerNode(node);
310310
const identifierFieldKeys = schema.identifierFieldKeys;
311311

312312
switch (identifierFieldKeys.length) {
@@ -361,7 +361,7 @@ export function getIdentifierFromNode(
361361
export function getStoredKey(node: TreeNode): string | number {
362362
// Note: the flex domain strictly works with "stored keys", and knows nothing about the developer-facing
363363
// "property keys".
364-
const parentField = getOrCreateInnerNode(node).parentField;
364+
const parentField = getInnerNode(node).parentField;
365365
if (parentField.parent.schema === FieldKinds.sequence.identifier) {
366366
// The parent of `node` is an array node
367367
assert(

packages/dds/tree/src/simple-tree/core/TreeNodeBinding.md

Lines changed: 14 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ Here is an example:
88
function addPoint(curve: Curve, x: number, y: number): Point {
99
const point = new Point({ x: 3, y: 3 });
1010
curve.points.insertAtEnd(point);
11-
// After insertion, `point` can be queried:
12-
assert(point.x === 3);
13-
// In fact, `point` is the same simple-tree node object that you would get from reading it off of its new parent in the tree:
11+
// `point` is the same simple-tree node object that you would get from reading it off of its new parent in the tree:
1412
assert(point === curve.points[curve.points.length - 1]);
1513
// So, to read the content that was just inserted, the original object can be used and there is no need to read via the parent:
1614
return point;
@@ -22,83 +20,29 @@ function addPoint(curve: Curve, x: number, y: number): Point {
2220

2321
This feature is supported by doing some bookkeeping to ensure that the simple-tree objects,
2422
flex tree nodes and anchor nodes in the tree get associated and disassociated at the right times.
25-
There are three states that a node simple-tree node can be in: "raw", "marinated" and "cooked".
23+
There are three states that a node simple-tree node can be in: "Unhydrated", "hydrating" and "Hydrated".
2624

27-
Note from the public API perspective, `Unhydrated` nodes are "raw", and hydrated nodes are either "marinated" or "cooked".
25+
### Unhydrated Nodes
2826

29-
### Raw Proxies
30-
31-
A newly created simple-tree node, a.k.a. a **raw** simple-tree node. A raw simple-tree node is produced by invoking the schema-provided constructor for a node:
32-
33-
```ts
34-
const rawPoint = new Point({ x: 3, y: 3 });
35-
```
36-
37-
Such a simple-tree node will be raw until it is inserted into the tree and becomes "marinated" (see below).
38-
As raw nodes can be read or mutated.
39-
40-
### Marinated Proxies
41-
42-
Proxies become **marinated** as soon as they are inserted into the tree.
43-
44-
This happens whether proxies are inserted directly, as the root of the content subtree...
27+
A newly created simple-tree node, a.k.a. an **unhydrated** simple-tree node. An unhydrated simple-tree node is produced by invoking the schema-provided constructor for a node:
4528

4629
```ts
47-
// Upon insertion, `rawPoint` transitions from "raw" to the next state, "marinated"
48-
app.graph.curves[0].insertAtEnd(rawPoint);
30+
const unhydratedPoint = new Point({ x: 3, y: 3 });
4931
```
5032

51-
...or proxies are inserted indirectly, making up some arbitrary portion of the content subtree.
33+
Such a simple-tree node will be unhydrated until it is inserted into the tree and becomes "hydrated" (see below).
34+
Unhydrated nodes can be read or mutated just like hydrated ones.
5235

53-
```ts
54-
// Upon insertion, `rawPoint` transitions from "raw" to the next state, "marinated"
55-
app.graph = {
56-
curves: [
57-
[new Point({ x: 10, y: 10 }), new Point({ x: 20, y: 20 })],
58-
[new Point({ x: 2, y: 2 }), rawPoint, new Point({ x: 4, y: 4 })],
59-
],
60-
};
61-
```
36+
### Hydrating Nodes
6237

63-
A marinated simple-tree node, by definition, is bound bi-directionally to an `AnchorNode`.
64-
When insertion occurs, an `AnchorNode` will be created (if not already present) for the location of the new content.
65-
The simple-tree node for that content will then be mapped to the `AnchorNode` (via its kernel) and the `AnchorNode` will be mapped to the simple-tree node.
66-
Note that the `AnchorNode` may not yet have a `FlexTreeNode` associated with it - that happens when the node becomes "cooked" (see below).
38+
Between insertion edit and the change callback which updates the node to "Hydrated" (see [prepareForInsertion.ts](../prepareForInsertion.ts)),
39+
the node is in a poorly defined "hydrating" state and should not be interacted with.
6740

68-
### Cooked Proxies
41+
### Hydrated Nodes
6942

70-
A simple-tree node is fully cooked when it finally associates itself with a `FlexTreeNode`.
71-
This happens lazily on demand, if/when a marinated simple-tree node is read or mutated (by the local client).
43+
A simple-tree node is fully hydrated when it is associated with a `HydratedFlexTreeNode`.
7244

7345
```ts
74-
const point = new Point({ x: 3, y: 3 }); // `point` is raw
75-
curves.points.insertAtEnd(point); // `point` becomes marinated
76-
const x = point.x; // `point` becomes cooked in order to support the read of `x`
46+
const point = new Point({ x: 3, y: 3 }); // `point` is unhydrated
47+
curves.points.insertAtEnd(point); // `point` becomes hydrated
7748
```
78-
79-
This laziness prevents the simple-tree node tree from generating unnecessary `FlexTreeNodes`.
80-
81-
Cooking a marinated simple-tree node works as follows:
82-
83-
1. Get the `AnchorNode` associated with the marinated simple-tree node.
84-
2. Get or create a `FlexTreeNode` for the anchor.
85-
3. This will cause the `FlexTreeNode` to be generated which corresponds to the simple-tree node.
86-
87-
### Mappings
88-
89-
```mermaid
90-
graph LR;
91-
AnchorNode(AnchorNode)<-->|Cooked and sometimes when Marinated|FlexTreeNode(FlexTreeNode)
92-
AnchorNode<--->|Marinated or Cooked|TreeNode(TreeNode)-.->|Raw|RawTreeNode(RawTreeNode)
93-
linkStyle 0 stroke:#0f0
94-
linkStyle 1 stroke:#00f
95-
linkStyle 2 stroke:#f00
96-
```
97-
98-
Note that it is possible for the `Cooked` mappings between an `AnchorNode` and a `FlexTreeNode` to exist regardless of whether there is also a simple-tree node yet created for that node.
99-
In that case, when that simple-tree node is created it will immediately be given its `Marinated` mappings and therefore already be cooked.
100-
101-
`RawTreeNode`s, which implement the `FlexTreeNode` interface (or at least, as of 2024-03-21, pretend to), are substitutes for the true `FlexTreeNode`s that don't yet exist for a raw node.
102-
The `Raw` mapping is removed when a simple-tree node is marinated, and the `RawTreeNode` is forgotten.
103-
104-
See [proxyBinding.ts](./proxyBinding.ts) for more details.

packages/dds/tree/src/simple-tree/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export {
1010
tryGetTreeNodeSchema,
1111
type InnerNode,
1212
tryDisposeTreeNode,
13-
getOrCreateInnerNode,
13+
getInnerNode,
1414
treeNodeFromAnchor,
1515
getSimpleNodeSchemaFromInnerNode,
1616
SimpleContextSlot,

0 commit comments

Comments
 (0)