-
Notifications
You must be signed in to change notification settings - Fork 549
Fix handling of deleted TreeNodes #24345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
969c359
cd88269
ad06564
7d6b81d
7df1166
0a43e31
850d79b
92fa6a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,8 @@ | |
* Licensed under the MIT License. | ||
*/ | ||
|
||
import { assert, Lazy, fail } from "@fluidframework/core-utils/internal"; | ||
import { assert, Lazy, fail, debugAssert } from "@fluidframework/core-utils/internal"; | ||
import { UsageError } from "@fluidframework/telemetry-utils/internal"; | ||
import { createEmitter } from "@fluid-internal/client-utils"; | ||
import type { Listenable, Off } from "@fluidframework/core-interfaces"; | ||
import type { InternalTreeNode, TreeNode, Unhydrated } from "./types.js"; | ||
|
@@ -72,17 +73,17 @@ export function tryGetTreeNodeSchema(value: unknown): undefined | TreeNodeSchema | |
/** The {@link HydrationState} of a {@link TreeNodeKernel} before the kernel is hydrated */ | ||
interface UnhydratedState { | ||
off: Off; | ||
innerNode: UnhydratedFlexTreeNode; | ||
readonly innerNode: UnhydratedFlexTreeNode; | ||
} | ||
|
||
/** The {@link HydrationState} of a {@link TreeNodeKernel} after the kernel is hydrated */ | ||
interface HydratedState { | ||
/** The flex node for this kernel (lazy - undefined if it has not yet been demanded) */ | ||
innerNode?: FlexTreeNode; | ||
/** The {@link AnchorNode} that this node is associated with. */ | ||
anchorNode: AnchorNode; | ||
readonly anchorNode: AnchorNode; | ||
/** All {@link Off | event deregistration functions} that should be run when the kernel is disposed. */ | ||
offAnchorNode: Set<Off>; | ||
readonly offAnchorNode: Set<Off>; | ||
} | ||
|
||
/** State within a {@link TreeNodeKernel} that is related to the hydration process */ | ||
|
@@ -287,35 +288,46 @@ export class TreeNodeKernel { | |
* | ||
* For hydrated nodes it returns a FlexTreeNode backed by the forest. | ||
* Note that for "marinated" nodes, this FlexTreeNode exists and returns it: it does not return the MapTreeNode which is the current InnerNode. | ||
* | ||
* If `allowDeleted` is false, this will throw a UsageError if the node is deleted. | ||
*/ | ||
public getOrCreateInnerNode(allowFreed = false): InnerNode { | ||
public getOrCreateInnerNode(allowDeleted = false): InnerNode { | ||
if (!isHydrated(this.#hydrationState)) { | ||
debugAssert( | ||
() => | ||
this.#hydrationState.innerNode?.context.isDisposed() === false || | ||
"Unhydrated node should never be disposed", | ||
); | ||
return this.#hydrationState.innerNode; // Unhydrated case | ||
} | ||
|
||
if (this.#hydrationState.innerNode !== undefined) { | ||
return this.#hydrationState.innerNode; // Cooked case | ||
if (this.#hydrationState.innerNode === undefined) { | ||
// Marinated case -> cooked | ||
const anchorNode = this.#hydrationState.anchorNode; | ||
// The proxy is bound to an anchor node, but it may or may not have an actual flex node yet | ||
const flexNode = anchorNode.slots.get(flexTreeSlot); | ||
if (flexNode !== undefined) { | ||
// If the flex node already exists, use it... | ||
this.#hydrationState.innerNode = flexNode; | ||
} else { | ||
// ...otherwise, the flex node must be created | ||
const context = | ||
anchorNode.anchorSet.slots.get(ContextSlot) ?? fail(0xb41 /* missing context */); | ||
const cursor = context.checkout.forest.allocateCursor("getFlexNode"); | ||
context.checkout.forest.moveCursorToPath(anchorNode, cursor); | ||
this.#hydrationState.innerNode = makeTree(context, cursor); | ||
cursor.free(); | ||
// Calling this is a performance improvement, however, do this only after demand to avoid momentarily having no anchors to anchorNode | ||
anchorForgetters?.get(this.node)?.(); | ||
if (!allowDeleted) { | ||
assertFlexTreeEntityNotFreed(this.#hydrationState.innerNode); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: unrelated to this PR, but should this function be renamed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in the flex tree abstraction, we have isFreed which this is referring to, so I think its fine. Probably would be nice to better along the terminology in the two layers though. |
||
} | ||
} | ||
} | ||
|
||
// Marinated case -> cooked | ||
const anchorNode = this.#hydrationState.anchorNode; | ||
// The proxy is bound to an anchor node, but it may or may not have an actual flex node yet | ||
const flexNode = anchorNode.slots.get(flexTreeSlot); | ||
if (flexNode !== undefined) { | ||
// If the flex node already exists, use it... | ||
this.#hydrationState.innerNode = flexNode; | ||
} else { | ||
// ...otherwise, the flex node must be created | ||
const context = | ||
anchorNode.anchorSet.slots.get(ContextSlot) ?? fail(0xb41 /* missing context */); | ||
const cursor = context.checkout.forest.allocateCursor("getFlexNode"); | ||
context.checkout.forest.moveCursorToPath(anchorNode, cursor); | ||
this.#hydrationState.innerNode = makeTree(context, cursor); | ||
cursor.free(); | ||
// Calling this is a performance improvement, however, do this only after demand to avoid momentarily having no anchors to anchorNode | ||
anchorForgetters?.get(this.node)?.(); | ||
if (!allowFreed) { | ||
assertFlexTreeEntityNotFreed(this.#hydrationState.innerNode); | ||
if (!allowDeleted) { | ||
if (this.#hydrationState.innerNode.context.isDisposed()) { | ||
throw new UsageError("Cannot access a Deleted node."); | ||
} | ||
} | ||
|
||
|
@@ -417,11 +429,15 @@ export const unhydratedFlexTreeNodeToTreeNode = | |
*/ | ||
export const proxySlot = anchorSlot<TreeNode>(); | ||
|
||
/** | ||
* Dispose a TreeNode (if any) for an existing anchor without disposing the anchor. | ||
*/ | ||
export function tryDisposeTreeNode(anchorNode: AnchorNode): void { | ||
const treeNode = anchorNode.slots.get(proxySlot); | ||
if (treeNode !== undefined) { | ||
const kernel = getKernel(treeNode); | ||
kernel.dispose(); | ||
anchorNode.slots.delete(proxySlot); | ||
} | ||
} | ||
|
||
|
@@ -454,10 +470,12 @@ export function getSimpleContextFromInnerNode(innerNode: InnerNode): Context { | |
* | ||
* For hydrated nodes it returns a FlexTreeNode backed by the forest. | ||
* Note that for "marinated" nodes, this FlexTreeNode exists and returns it: it does not return the MapTreeNode which is the current InnerNode. | ||
* | ||
* If `allowDeleted` is false, this will throw a UsageError if the node is deleted. | ||
*/ | ||
export function getOrCreateInnerNode(treeNode: TreeNode, allowFreed = false): InnerNode { | ||
export function getOrCreateInnerNode(treeNode: TreeNode, allowDeleted = false): InnerNode { | ||
const kernel = getKernel(treeNode); | ||
return kernel.getOrCreateInnerNode(allowFreed); | ||
return kernel.getOrCreateInnerNode(allowDeleted); | ||
} | ||
|
||
/** | ||
|
Uh oh!
There was an error while loading. Please reload this page.