Skip to content

Commit 2427fe3

Browse files
authored
Refactor TreeNodeKernel to make undemanded node case explicit (microsoft#23076)
## Description This moves `innerNode` into `#hydrationState`, which makes it clearer what the possible states and transitions are. A side effect of this is that the `innerNode` property is now `undefined` in the case where a node has been hydrated by its corresponding flex node has not yet been generated. Previously, the `innerNode` remained an `UnhydratedFlexTreeNode` even after hydration, which while not incorrect was still confusing to see in the debugger. This PR also does some related cleanups. However, it does not attempt to clean up TreeNodeKernel entirely - there is still plenty more possible cleanup, but this PR would like to avoid trying to do everything at once.
1 parent d48c3b8 commit 2427fe3

File tree

1 file changed

+62
-64
lines changed

1 file changed

+62
-64
lines changed

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

Lines changed: 62 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
assertFlexTreeEntityNotFreed,
1818
ContextSlot,
1919
flexTreeSlot,
20-
isFlexTreeNode,
2120
isFreedSymbol,
2221
LazyEntity,
2322
TreeStatus,
@@ -71,10 +70,15 @@ export function tryGetTreeNodeSchema(value: unknown): undefined | TreeNodeSchema
7170
}
7271

7372
/** The {@link HydrationState} of a {@link TreeNodeKernel} before the kernel is hydrated */
74-
type UnhydratedState = Off;
73+
interface UnhydratedState {
74+
off: Off;
75+
innerNode: UnhydratedFlexTreeNode;
76+
}
7577

7678
/** The {@link HydrationState} of a {@link TreeNodeKernel} after the kernel is hydrated */
7779
interface HydratedState {
80+
/** The flex node for this kernel (lazy - undefined if it has not yet been demanded) */
81+
innerNode?: FlexTreeNode;
7882
/** The {@link AnchorNode} that this node is associated with. */
7983
anchorNode: AnchorNode;
8084
/** All {@link Off | event deregistration functions} that should be run when the kernel is disposed. */
@@ -86,14 +90,13 @@ type HydrationState = UnhydratedState | HydratedState;
8690

8791
/** True if and only if the given {@link HydrationState} is post-hydration */
8892
function isHydrated(state: HydrationState): state is HydratedState {
89-
return typeof state === "object";
93+
return (state as Partial<HydratedState>).anchorNode !== undefined;
9094
}
9195

9296
/**
9397
* Contains state and an internal API for managing {@link TreeNode}s.
9498
* @remarks All {@link TreeNode}s have an associated kernel object.
9599
* The kernel has the same lifetime as the node and spans both its unhydrated and hydrated states.
96-
* When hydration occurs, the kernel is notified via the {@link TreeNodeKernel.hydrate | hydrate} method.
97100
*/
98101
export class TreeNodeKernel {
99102
private disposed = false;
@@ -110,7 +113,7 @@ export class TreeNodeKernel {
110113
*/
111114
public generationNumber: number = 0;
112115

113-
#hydrationState: HydrationState = () => {};
116+
#hydrationState: HydrationState;
114117

115118
/**
116119
* Events registered before hydration.
@@ -133,7 +136,7 @@ export class TreeNodeKernel {
133136
public constructor(
134137
public readonly node: TreeNode,
135138
public readonly schema: TreeNodeSchema,
136-
private innerNode: InnerNode,
139+
innerNode: InnerNode,
137140
private readonly initialContext: Context,
138141
) {
139142
assert(!treeNodeToKernel.has(node), 0xa1a /* only one kernel per node can be made */);
@@ -144,9 +147,9 @@ export class TreeNodeKernel {
144147
mapTreeNodeToProxy.set(innerNode, node);
145148
// Register for change events from the unhydrated flex node.
146149
// These will be fired if the unhydrated node is edited, and will also be forwarded later to the hydrated node.
147-
this.#hydrationState = innerNode.events.on(
148-
"childrenChangedAfterBatch",
149-
({ changedFields }) => {
150+
this.#hydrationState = {
151+
innerNode,
152+
off: innerNode.events.on("childrenChangedAfterBatch", ({ changedFields }) => {
150153
this.#unhydratedEvents.value.emit("childrenChangedAfterBatch", {
151154
changedFields,
152155
});
@@ -161,15 +164,16 @@ export class TreeNodeKernel {
161164
// This cast is safe because the parent (if it exists) of an unhydrated flex node is always another unhydrated flex node.
162165
n = n.parentField.parent.parent as UnhydratedFlexTreeNode | undefined;
163166
}
164-
},
165-
);
167+
}),
168+
};
166169
} else {
167170
// Hydrated case
171+
const { anchorNode } = innerNode;
168172
assert(
169-
!innerNode.anchorNode.slots.has(proxySlot),
173+
!anchorNode.slots.has(proxySlot),
170174
0x7f5 /* Cannot associate an flex node with multiple simple-tree nodes */,
171175
);
172-
this.hydrate(innerNode.anchorNode);
176+
this.#hydrationState = this.createHydratedState(anchorNode);
173177
}
174178
}
175179

@@ -191,27 +195,11 @@ export class TreeNodeKernel {
191195
* Happens at most once for any given node.
192196
* Cleans up mappings to {@link UnhydratedFlexTreeNode} - it is assumed that they are no longer needed once the proxy has an anchor node.
193197
*/
194-
public hydrate(anchorNode: AnchorNode): void {
198+
private hydrate(anchorNode: AnchorNode): void {
195199
assert(!this.disposed, 0xa2a /* cannot hydrate a disposed node */);
196200
assert(!isHydrated(this.#hydrationState), 0xa2b /* hydration should only happen once */);
197-
198-
// If the this node is raw and thus has a MapTreeNode, forget it:
199-
if (this.innerNode instanceof UnhydratedFlexTreeNode) {
200-
mapTreeNodeToProxy.delete(this.innerNode);
201-
}
202-
203-
// However, it's fine for an anchor node to rotate through different proxies when the content at that place in the tree is replaced.
204-
anchorNode.slots.set(proxySlot, this.node);
205-
this.#hydrationState = {
206-
anchorNode,
207-
offAnchorNode: new Set([
208-
anchorNode.events.on("afterDestroy", () => this.dispose()),
209-
// TODO: this should be triggered on change even for unhydrated nodes.
210-
anchorNode.events.on("childrenChanging", () => {
211-
this.generationNumber += 1;
212-
}),
213-
]),
214-
};
201+
mapTreeNodeToProxy.delete(this.#hydrationState.innerNode);
202+
this.#hydrationState = this.createHydratedState(anchorNode);
215203

216204
// If needed, register forwarding emitters for events from before hydration
217205
if (this.#unhydratedEvents.evaluated) {
@@ -228,6 +216,20 @@ export class TreeNodeKernel {
228216
}
229217
}
230218

219+
private createHydratedState(anchorNode: AnchorNode): HydratedState {
220+
anchorNode.slots.set(proxySlot, this.node);
221+
return {
222+
anchorNode,
223+
offAnchorNode: new Set([
224+
anchorNode.events.on("afterDestroy", () => this.dispose()),
225+
// TODO: this should be triggered on change even for unhydrated nodes.
226+
anchorNode.events.on("childrenChanging", () => {
227+
this.generationNumber += 1;
228+
}),
229+
]),
230+
};
231+
}
232+
231233
public getStatus(): TreeStatus {
232234
if (this.disposed) {
233235
return TreeStatus.Deleted;
@@ -282,35 +284,36 @@ export class TreeNodeKernel {
282284
* Note that for "marinated" nodes, this FlexTreeNode exists and returns it: it does not return the MapTreeNode which is the current InnerNode.
283285
*/
284286
public getOrCreateInnerNode(allowFreed = false): InnerNode {
285-
if (!(this.innerNode instanceof UnhydratedFlexTreeNode)) {
286-
// Cooked case
287-
return this.innerNode;
287+
if (!isHydrated(this.#hydrationState)) {
288+
return this.#hydrationState.innerNode; // Unhydrated case
288289
}
289290

290-
if (!isHydrated(this.#hydrationState)) {
291-
return this.innerNode;
291+
if (this.#hydrationState.innerNode !== undefined) {
292+
return this.#hydrationState.innerNode; // Cooked case
292293
}
293294

294295
// Marinated case -> cooked
295296
const anchorNode = this.#hydrationState.anchorNode;
296297
// The proxy is bound to an anchor node, but it may or may not have an actual flex node yet
297298
const flexNode = anchorNode.slots.get(flexTreeSlot);
298299
if (flexNode !== undefined) {
299-
this.innerNode = flexNode;
300-
return flexNode; // If it does have a flex node, return it...
301-
} // ...otherwise, the flex node must be created
302-
const context = anchorNode.anchorSet.slots.get(ContextSlot) ?? fail("missing context");
303-
const cursor = context.checkout.forest.allocateCursor("getFlexNode");
304-
context.checkout.forest.moveCursorToPath(anchorNode, cursor);
305-
const newFlexNode = makeTree(context, cursor);
306-
cursor.free();
307-
this.innerNode = newFlexNode;
308-
// Calling this is a performance improvement, however, do this only after demand to avoid momentarily having no anchors to anchorNode
309-
anchorForgetters?.get(this.node)?.();
310-
if (!allowFreed) {
311-
assertFlexTreeEntityNotFreed(newFlexNode);
300+
// If the flex node already exists, use it...
301+
this.#hydrationState.innerNode = flexNode;
302+
} else {
303+
// ...otherwise, the flex node must be created
304+
const context = anchorNode.anchorSet.slots.get(ContextSlot) ?? fail("missing context");
305+
const cursor = context.checkout.forest.allocateCursor("getFlexNode");
306+
context.checkout.forest.moveCursorToPath(anchorNode, cursor);
307+
this.#hydrationState.innerNode = makeTree(context, cursor);
308+
cursor.free();
309+
// Calling this is a performance improvement, however, do this only after demand to avoid momentarily having no anchors to anchorNode
310+
anchorForgetters?.get(this.node)?.();
311+
if (!allowFreed) {
312+
assertFlexTreeEntityNotFreed(this.#hydrationState.innerNode);
313+
}
312314
}
313-
return newFlexNode;
315+
316+
return this.#hydrationState.innerNode;
314317
}
315318

316319
/**
@@ -344,24 +347,19 @@ export class TreeNodeKernel {
344347
/**
345348
* Retrieves the InnerNode associated with the given target via {@link setInnerNode}, if any.
346349
* @remarks
347-
* If `target` is a unhydrated node, returns its MapTreeNode.
350+
* If `target` is an unhydrated node, returns its UnhydratedFlexTreeNode.
348351
* If `target` is a cooked node (or marinated but a FlexTreeNode exists) returns the FlexTreeNode.
349-
* If the target is not a node, or a marinated node with no FlexTreeNode for its anchor, returns undefined.
352+
* If the target is a marinated node with no FlexTreeNode for its anchor, returns undefined.
350353
*/
351354
public tryGetInnerNode(): InnerNode | undefined {
352-
if (isFlexTreeNode(this.innerNode)) {
353-
// Cooked case
354-
return this.innerNode;
355-
}
356-
357-
if (!isHydrated(this.#hydrationState)) {
358-
return this.innerNode;
355+
if (isHydrated(this.#hydrationState)) {
356+
return (
357+
this.#hydrationState.innerNode ??
358+
this.#hydrationState.anchorNode.slots.get(flexTreeSlot)
359+
);
359360
}
360361

361-
// Marinated case -> cooked
362-
const anchorNode = this.#hydrationState.anchorNode;
363-
// The proxy is bound to an anchor node, but it may or may not have an actual flex node yet
364-
return anchorNode.slots.get(flexTreeSlot);
362+
return this.#hydrationState.innerNode;
365363
}
366364
}
367365

0 commit comments

Comments
 (0)