Skip to content

feat(tree): Add schema-agnostic tree traversal APIs #24723

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

Merged
merged 96 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
f176d1b
WIP: Add API
Josmithr May 22, 2025
1e83e40
refactor: Move new API to alpha scope
Josmithr May 22, 2025
98d8370
WIP: Placeholder implementations
Josmithr May 22, 2025
678c3d1
docs: Fix typo
Josmithr May 22, 2025
c4db625
Merge branch 'main' into tree/generic-traversal
Josmithr May 27, 2025
1f889d5
feat: [WIP] Child accessors
Josmithr May 27, 2025
a660773
test: Add test stubs for `child`
Josmithr May 27, 2025
31711ec
test: Add `children` test stubs
Josmithr May 27, 2025
3a91603
fix: Handle missing property key
Josmithr May 27, 2025
4f746f8
test: Object node tests
Josmithr May 27, 2025
3a65e66
test: Map tests
Josmithr May 27, 2025
3f71684
fix: Remove unneeded early out
Josmithr May 27, 2025
8e6cf78
feat: Array support
Josmithr May 27, 2025
4a9c7fa
Merge branch 'main' into tree/generic-traversal
Josmithr May 28, 2025
adde8a2
fix: Handling of array nodes in `children`
Josmithr May 28, 2025
205da98
fix: `child`
Josmithr May 28, 2025
90f1421
test: `children` tests
Josmithr May 28, 2025
25edf40
docs: Update API reports
Josmithr May 28, 2025
ab3b51c
test: Simplify object tests
Josmithr May 28, 2025
a83bf23
test: Simplify tests
Josmithr May 28, 2025
b4dc251
test: More tests
Josmithr May 28, 2025
5f2a1fa
test: Shadowed property test
Josmithr May 28, 2025
efe6128
test: Subclass property test
Josmithr May 28, 2025
6b81132
test: More object children tests
Josmithr May 28, 2025
ab5e998
test: More children tests
Josmithr May 28, 2025
cc3f3c4
test: Broken disposal tests
Josmithr May 28, 2025
5df2ff8
refactor: Remove unused function
Josmithr May 29, 2025
1db1c8a
refactor: Move function
Josmithr May 29, 2025
9028ff1
docs: TODO
Josmithr May 29, 2025
3c945fe
refactor: Rename helper function
Josmithr May 29, 2025
32c338a
refactor: Simplify array handling
Josmithr May 29, 2025
0bb00a6
refactor: Simplify non-array case
Josmithr May 29, 2025
036281d
refactor: Simplify sequence access
Josmithr May 29, 2025
e7b991a
refactor: Don't use stored key access in array case
Josmithr May 29, 2025
4fb6a55
refactor: Avoid boxed access
Josmithr May 29, 2025
d46f898
refactor: Inline stored key mapping
Josmithr May 29, 2025
93749da
docs: Comments
Josmithr May 29, 2025
08bb642
test: Update tests
Josmithr May 29, 2025
971068b
improvement: Cache inner node on construction
Josmithr May 30, 2025
9d78702
remove: `allowDeleted`
Josmithr May 30, 2025
7727527
fix: Throw UsageError when accessing disposed node
Josmithr May 30, 2025
46edc57
improvement: Fix capitalization
Josmithr May 30, 2025
35d620c
improvement: Fix error message casing
Josmithr May 30, 2025
42dd810
test: Simplify tests
Josmithr May 30, 2025
c336dad
docs: Document usage requirements
Josmithr May 30, 2025
855821f
docs: More explicit policy documentation around the use of deleted nodes
Josmithr May 30, 2025
fcfad7b
docs: Simplify `@throws` blocks
Josmithr May 30, 2025
3fda363
test: Update test to account for error message update
Josmithr May 30, 2025
cb84ba9
Merge branch 'tree/fix-disposal-handling' into tree/generic-traversal
Josmithr May 30, 2025
379309b
test: Fix test
Josmithr May 30, 2025
ebc416c
test: Fix tests
Josmithr May 30, 2025
f704420
Merge branch 'main' into tree/generic-traversal
Josmithr May 30, 2025
5f7abb0
Merge branch 'main' into tree/generic-traversal
Josmithr Jun 3, 2025
6939f06
docs: Update API docs
Josmithr Jun 3, 2025
78b2726
docs: Add links
Josmithr Jun 3, 2025
1dffc26
docs: More updates
Josmithr Jun 3, 2025
515c9d2
docs: More updates
Josmithr Jun 3, 2025
51b560d
test: Add recursive schema tests
Josmithr Jun 3, 2025
bbf311f
test: Remove `.only`
Josmithr Jun 3, 2025
a6c47aa
test: Add parent of child tests
Josmithr Jun 3, 2025
43e35cf
test: Recursive children tests
Josmithr Jun 3, 2025
9c9fa80
docs: Add comment
Josmithr Jun 3, 2025
08ebd14
docs: Add changeset
Josmithr Jun 3, 2025
87a1053
docs: Fix links
Josmithr Jun 3, 2025
3ec4fcf
docs: Add links
Josmithr Jun 3, 2025
97135f9
refactor: Use switch
Josmithr Jun 3, 2025
b5fe9e3
refactor: Iterable instead of array
Josmithr Jun 3, 2025
84c1eed
docs: Update comments
Josmithr Jun 3, 2025
3100139
docs: Update comment
Josmithr Jun 3, 2025
72c6a88
Merge branch 'main' into tree/generic-traversal
Josmithr Jun 9, 2025
37a0207
Merge branch 'main' into tree/generic-traversal
Josmithr Jun 9, 2025
9f550dc
test: Add unhydrated node with ID fields test
Josmithr Jun 9, 2025
304b7a6
docs: Update example
Josmithr Jun 9, 2025
d2bc8c4
Merge branch 'main' into tree/generic-traversal
Josmithr Jun 9, 2025
345e662
Merge branch 'main' into tree/generic-traversal
Josmithr Jun 10, 2025
191f9d1
improvement: More consistent debug assert messaging
Josmithr Jun 10, 2025
7861fc5
test: Fix test
Josmithr Jun 10, 2025
3779201
Merge branch 'main' into tree/generic-traversal
Josmithr Jun 10, 2025
6acc1c9
refactor: Remove generator pattern
Josmithr Jun 10, 2025
84dd650
refactor: Named tuple members
Josmithr Jun 10, 2025
a0fe526
refactor: Simplify field access
Josmithr Jun 10, 2025
cefe00e
refactor: Simplify object field iteration
Josmithr Jun 10, 2025
9f76176
test: Stored key tests
Josmithr Jun 11, 2025
bbe7246
fix: Object handling
Josmithr Jun 11, 2025
35bf511
remove: Assert
Josmithr Jun 11, 2025
31ae6fd
test: Update test names
Josmithr Jun 11, 2025
2b8a98e
test: Fix unknown optional field tests
Josmithr Jun 12, 2025
18675ed
Merge branch 'main' into tree/generic-traversal
Josmithr Jun 12, 2025
466d6bc
fix: Consistent Map access policy
Josmithr Jun 12, 2025
3bae5c1
refactor: Move utility
Josmithr Jun 13, 2025
5f5d4fd
refactor: Use helper
Josmithr Jun 13, 2025
14cdf4d
Merge branch 'main' into tree/generic-traversal
Josmithr Jun 13, 2025
4b3886c
Merge branch 'main' into tree/generic-traversal
Josmithr Jun 17, 2025
74d60ef
Merge branch 'main' into tree/generic-traversal
Josmithr Jun 17, 2025
c4dfa55
fix: Add missing import
Josmithr Jun 17, 2025
914ca40
docs: Add array examples to changeset
Josmithr Jun 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .changeset/seven-toes-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
"@fluidframework/tree": minor
"fluid-framework": minor
"__section": tree
---
Add TreeAlpha.child and TreeAlpha.children APIs for generic tree traversal

#### TreeAlpha.child

Check warning on line 8 in .changeset/seven-toes-smell.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Headings] 'TreeAlpha.child' should use sentence-style capitalization. Raw Output: {"message": "[Microsoft.Headings] 'TreeAlpha.child' should use sentence-style capitalization.", "location": {"path": ".changeset/seven-toes-smell.md", "range": {"start": {"line": 8, "column": 6}}}, "severity": "INFO"}

Access a child node or value of a `TreeNode` by its property key.

```typescript
class MyObject extends schemaFactory.object("MyObject", {
foo: schemaFactory.string;
bar: schemaFactory.optional(schemaFactory.string);
}) {}

const myObject = new MyObject({
foo: "Hello world!"
});

const foo = TreeAlpha.child(myObject, "foo"); // "Hello world!"
const bar = TreeAlpha.child(myObject, "bar"); // undefined
const baz = TreeAlpha.child(myObject, "baz"); // undefined
```

```typescript
class MyArray extends schemaFactory.array("MyArray", schemaFactory.string) {}

const myArray = new MyArray("Hello", "World");

const child0 = TreeAlpha.child(myArray, 0); // "Hello"
const child1 = TreeAlpha.child(myArray, 1); // "World
const child2 = TreeAlpha.child(myArray, 2); // undefined
```

#### TreeAlpha.children

Check warning on line 37 in .changeset/seven-toes-smell.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Headings] 'TreeAlpha.children' should use sentence-style capitalization. Raw Output: {"message": "[Microsoft.Headings] 'TreeAlpha.children' should use sentence-style capitalization.", "location": {"path": ".changeset/seven-toes-smell.md", "range": {"start": {"line": 37, "column": 6}}}, "severity": "INFO"}

Get all child nodes / values of a `TreeNode`, keyed by their property keys.

```typescript
class MyObject extends schemaFactory.object("MyObject", {
foo: schemaFactory.string;
bar: schemaFactory.optional(schemaFactory.string);
baz: schemaFactory.optional(schemaFactory.number);
}) {}

const myObject = new MyObject({
foo: "Hello world!",
baz: 42,
});

const children = TreeAlpha.children(myObject); // [["foo", "Hello world!"], ["baz", 42]]
```

```typescript
class MyArray extends schemaFactory.array("MyArray", schemaFactory.string) {}

const myArray = new MyArray("Hello", "World");

const children = TreeAlpha.children(myObject); // [[0, "Hello"], [1, "World"]]
```
2 changes: 2 additions & 0 deletions packages/dds/tree/api-report/tree.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,8 @@ export const Tree: Tree;
// @alpha @sealed @system
export interface TreeAlpha {
branch(node: TreeNode): TreeBranch | undefined;
child(node: TreeNode, key: string | number): TreeNode | TreeLeafValue | undefined;
children(node: TreeNode): Iterable<[propertyKey: string | number, child: TreeNode | TreeLeafValue]>;
create<const TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema>(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField<TSchema>): Unhydrated<TSchema extends ImplicitFieldSchema ? TreeFieldFromImplicitField<TSchema> : TreeNode | TreeLeafValue | undefined>;
exportCompressed(tree: TreeNode | TreeLeafValue, options: {
idCompressor?: IIdCompressor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export interface FlexTreeNode extends FlexTreeEntity, MapTreeNodeViewGeneric<Fle
* @remarks
* All fields implicitly exist, so `getBoxed` can be called with any key and will always return a field.
* Even if the field is empty, it will still be returned, and can be edited to insert content if allowed by the field kind.
* See {@link FlexTreeNode.tryGetField} for a variant that does not allocate afield in the empty case.
* See {@link FlexTreeNode.tryGetField} for a variant that does not allocate a field in the empty case.
*/
getBoxed(key: FieldKey): FlexTreeField;

Expand Down
4 changes: 2 additions & 2 deletions packages/dds/tree/src/shared-tree/schematizingTreeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
type SchemaCompatibilityStatus,
type TreeView,
type TreeViewEvents,
getTreeNodeForField,
tryGetTreeNodeForField,
setField,
normalizeFieldSchema,
SchemaCompatibilityTester,
Expand Down Expand Up @@ -428,7 +428,7 @@ export class SchematizingSimpleTreeView<
);
}
const view = this.getView();
return getTreeNodeForField(view.flexTree) as ReadableField<TRootSchema>;
return tryGetTreeNodeForField(view.flexTree) as ReadableField<TRootSchema>;
}

public set root(newRoot: InsertableField<TRootSchema>) {
Expand Down
188 changes: 185 additions & 3 deletions packages/dds/tree/src/shared-tree/treeAlpha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
* Licensed under the MIT License.
*/

import { assert, fail } from "@fluidframework/core-utils/internal";
import {
assert,
debugAssert,
fail,
unreachableCase,
} from "@fluidframework/core-utils/internal";
import { createIdCompressor } from "@fluidframework/id-compressor/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import type { IFluidHandle } from "@fluidframework/core-interfaces";
import type { IIdCompressor } from "@fluidframework/id-compressor";

import {
asIndex,
getKernel,
type TreeNode,
type Unhydrated,
Expand Down Expand Up @@ -40,15 +46,20 @@ import {
getIdentifierFromNode,
unhydratedFlexTreeFromInsertable,
getOrCreateNodeFromInnerNode,
getOrCreateNodeFromInnerUnboxedNode,
getOrCreateInnerNode,
NodeKind,
tryGetTreeNodeForField,
isObjectNodeSchema,
} from "../simple-tree/index.js";
import { extractFromOpaque, type JsonCompatible } from "../util/index.js";
import { brand, extractFromOpaque, type JsonCompatible } from "../util/index.js";
import {
FluidClientVersion,
noopValidator,
type ICodecOptions,
type CodecWriteOptions,
} from "../codec/index.js";
import type { ITreeCursorSynchronous } from "../core/index.js";
import { EmptyKey, type ITreeCursorSynchronous } from "../core/index.js";
import {
cursorForMapTreeField,
defaultSchemaPolicy,
Expand All @@ -60,6 +71,7 @@ import {
type FieldBatchEncodingContext,
fluidVersionToFieldBatchCodecWriteVersion,
type LocalNodeIdentifier,
type FlexTreeSequenceField,
} from "../feature-libraries/index.js";
import { independentInitializedView, type ViewContent } from "./independentView.js";
import { SchematizingSimpleTreeView, ViewSlot } from "./schematizingTreeView.js";
Expand Down Expand Up @@ -343,6 +355,48 @@ export interface TreeAlpha {
* Otherwise, this returns the key of the field that it is under (a `string`).
*/
key2(node: TreeNode): string | number | undefined;

/**
* Gets the child of the given node with the given property key if a child exists under that key.
*
* @remarks {@link SchemaFactoryObjectOptions.allowUnknownOptionalFields | Unknown optional fields} of Object nodes will not be returned by this method.
*
* @param node - The parent node whose child is being requested.
* @param key - The property key under the node under which the child is being requested.
* For Object nodes, this is the developer-facing "property key", not the "{@link SimpleObjectFieldSchema.storedKey | stored keys}".
*
* @returns The child node or leaf value under the given key, or `undefined` if no such child exists.
*
* @see {@link (TreeAlpha:interface).key2}
* @see {@link (TreeNodeApi:interface).parent}
*/
child(node: TreeNode, key: string | number): TreeNode | TreeLeafValue | undefined;

/**
* Gets the children of the provided node, paired with their property keys under the node.
*
* @remarks
* No guarantees are made regarding the order of the children in the returned array.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curiosity: when accessing the children of an array node, I imagine we probably do return them in order? No need to make it part of the contract though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice, that appears to be true, yes.

*
* Optional properties of Object nodes with no value are not included in the result.
*
* {@link SchemaFactoryObjectOptions.allowUnknownOptionalFields | Unknown optional fields} of Object nodes are not included in the result.
*
* @param node - The node whose children are being requested.
*
* @returns
* An array of pairs of the form `[propertyKey, child]`.
*
* For Array nodes, the `propertyKey` is the index of the child in the array.
*
* For Object nodes, the returned `propertyKey`s are the developer-facing "property keys", not the "{@link SimpleObjectFieldSchema.storedKey | stored keys}".
*
* @see {@link (TreeAlpha:interface).key2}
* @see {@link (TreeNodeApi:interface).parent}
*/
children(
node: TreeNode,
): Iterable<[propertyKey: string | number, child: TreeNode | TreeLeafValue]>;
}

/**
Expand Down Expand Up @@ -492,6 +546,134 @@ export const TreeAlpha: TreeAlpha = {
const parentSchema = treeNodeApi.schema(parent);
return getPropertyKeyFromStoredKey(parentSchema, storedKey);
},

child: (
node: TreeNode,
propertyKey: string | number,
): TreeNode | TreeLeafValue | undefined => {
const flexNode = getOrCreateInnerNode(node);
debugAssert(
() => !flexNode.context.isDisposed() || "The provided tree node has been disposed.",
);

const schema = treeNodeApi.schema(node);

switch (schema.kind) {
case NodeKind.Array: {
const sequence = flexNode.tryGetField(EmptyKey) as FlexTreeSequenceField | undefined;

// Empty sequence - cannot have children.
if (sequence === undefined) {
return undefined;
}

const index =
typeof propertyKey === "number"
? propertyKey
: asIndex(propertyKey, Number.POSITIVE_INFINITY);

// If the key is not a valid index, then there is no corresponding child.
if (index === undefined) {
return undefined;
}
Comment on lines +570 to +578
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me that we should be tolerating strings containing numbers to be used as keys into Arays here.

From reading our APi docs and types, I would not expect this to be supported.

Suggested change
const index =
typeof propertyKey === "number"
? propertyKey
: asIndex(propertyKey, Number.POSITIVE_INFINITY);
// If the key is not a valid index, then there is no corresponding child.
if (index === undefined) {
return undefined;
}
// If the key is not a valid index, then there is no corresponding child.
if (typeof propertyKey !== "number") {
return undefined;
}

Copy link
Contributor Author

@Josmithr Josmithr Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CraigMacomber I can remove this support, but I'm wondering how consistent we've been in this regard. ArrayNode's get explicitly does support this. Seems odd to me that we wouldn't support it here.

Copy link
Contributor Author

@Josmithr Josmithr Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I don't think that's what we want. POJO arrays allow both (implicitly numbers), and POJO objects allow both (implicitly strings), so it seems like we should follow suit.

https://www.typescriptlang.org/play/?#code/MYewdgzgLgBAhgJwXAngLhmArgWwEYCmCA2gLowC8MxAjADQBMdAzKQNwBQHokIANgQB0fEAHMAFImQpiABlIBKNjAD0KmDW7gI-ISIlTUxAESzji5Wo1ctkWADMQISjADeHGJ5iyMxgBIEfCLGdBwAvpy2OgLCYuKOIHIWqur+gcFRurESCSZmyVZpQSDGNkA

Maps are an exception here. And maybe we want to only allow string keys for those. But since we strictly support string keys in our maps, matching the policy for Objects may make more sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we just preserve current behavior? I think the current behavior is that numbers are allowed for arrays, but not for objects and maps.

Copy link
Contributor Author

@Josmithr Josmithr Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that number keys are allowed when accessing Object node properties (though not Map node properties). E.g.

class TestObject extends schema.object("TestObject", {
	"0": schema.string,
}) {}
const test = new TestObject({
	0: "Hello world!"
});

const zero = test[0]; // "Hello world!"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, what I have is consistent with our standard property access for Arrays and Objects. And this is consistent with POJO. So, I'm pretty strongly convinced that deviating from this would be a bad idea.

With the exception of Map nodes. I think there is room for longer term discussion about maybe allowing use of numeric keys with our maps, since they strictly support string keys. But for now, that isn't supported, so it makes the most sense to be consistent with that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is literally impossible to implement arrays without supporting string keys, since JavaScript does not support numeric keys: they are stringified. The property key received by a proxy can never be a number: it is always a string, even when the user provides a number.

Our arrays support string keys exactly the same as JavaScript arrays, meaning we use typescript to tell you to pass in a number, which JS converts to a string behind the scenes. We do not support strings in places where not supporting strings is possible, for example in "at".

Our code for converting string indexes back to numbers exists to make indexing with numbers work: the fact indexing with strings works is an unfortunate side effect of this very questionable language.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example ["x"]["0"] gives "x" just like ["x"][0] does, and the proxy can't tell those two lookups apart as both search for a member under the key "0". Note that this also gives "x": {"0": "x"}[0] : this conversion from number to string is a JS property thing, and has nothing to do with arrays.

Copy link
Contributor Author

@Josmithr Josmithr Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my main point can be summed up by the following:

Array nodes can be indexed using string-formatted integer keys, so I don't think it makes sense for them not to work via child.

class MyArray extends schemaFactory.array("my-array", schemaFactory.string) {}
const myArray = new MyArray(["Hello world"]);
myArray["0"]; // "Hello world"
Tree.child(myArray, "0"); // Should match
  • Similarly, for object nodes, we support indexing them with numeric keys, so the behavior of child should match.
class MyObject extends schema.object("MyObject", {
	"1": SchemaFactory.optional(schema.string),
}) {}
const myObject = new MyObject({ 1: "Hello world" });

myObject[1]; // "Hello world"
TreeAlpha.child(myObject, 1); // Should match

Maps are a different story, and child already rejects number keys for them. But I think we need the above support for Arrays and Objects (and soon, Records).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, @Josmithr and I talked offline and I think the idea of consistency with POJO is compelling. I strongly disagree with the idea of possibly wanting maps to support something similar, as I think a very important property of maps is that keys that compare equal should look up the same value, and keys that do not should not. If we break that contract, I think this would be a massive bug pit, and I don't see any offsetting reason to do so. Setting that aside, I'll vote in favor of consistency with POJO :)


const childFlexTree = sequence.at(index);

// No child at the given index.
if (childFlexTree === undefined) {
return undefined;
}

return getOrCreateNodeFromInnerUnboxedNode(childFlexTree);
}
case NodeKind.Map:
if (typeof propertyKey !== "string") {
// Map nodes only support string keys.
return undefined;
}
// Fall through
case NodeKind.Object: {
let storedKey: string | number = propertyKey;
if (isObjectNodeSchema(schema)) {
const fieldSchema = schema.fields.get(String(propertyKey));
if (fieldSchema === undefined) {
return undefined;
}

storedKey = fieldSchema.storedKey;
}

const field = flexNode.tryGetField(brand(String(storedKey)));
if (field !== undefined) {
return tryGetTreeNodeForField(field);
}

return undefined;
}
case NodeKind.Leaf: {
fail("Leaf schema associated with non-leaf tree node.");
}
default: {
unreachableCase(schema.kind);
}
}
},

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

const schema = treeNodeApi.schema(node);

const result: [string | number, TreeNode | TreeLeafValue][] = [];
switch (schema.kind) {
case NodeKind.Array: {
const sequence = flexNode.tryGetField(EmptyKey) as FlexTreeSequenceField | undefined;
if (sequence === undefined) {
break;
}

for (let index = 0; index < sequence.length; index++) {
const childFlexTree = sequence.at(index);
assert(childFlexTree !== undefined, "Sequence child was undefined.");
const childTree = getOrCreateNodeFromInnerUnboxedNode(childFlexTree);
result.push([index, childTree]);
}
break;
}
case NodeKind.Map: {
for (const [key, flexField] of flexNode.fields) {
const childTreeNode = tryGetTreeNodeForField(flexField);
if (childTreeNode !== undefined) {
result.push([key, childTreeNode]);
}
}
break;
}
case NodeKind.Object: {
assert(isObjectNodeSchema(schema), "Expected object schema.");
for (const [propertyKey, fieldSchema] of schema.fields) {
const storedKey = fieldSchema.storedKey;
const flexField = flexNode.tryGetField(brand(String(storedKey)));
if (flexField !== undefined) {
const childTreeNode = tryGetTreeNodeForField(flexField);
assert(childTreeNode !== undefined, "Expected child tree node for field.");
result.push([propertyKey, childTreeNode]);
}
}
break;
}
case NodeKind.Leaf: {
fail("Leaf schema associated with non-leaf tree node.");
}
default: {
unreachableCase(schema.kind);
}
}
return result;
},
};

function exportConcise(
Expand Down
6 changes: 3 additions & 3 deletions packages/dds/tree/src/simple-tree/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ export {
singletonSchema,
} from "./schemaCreationUtilities.js";
export {
getIdentifierFromNode,
getPropertyKeyFromStoredKey,
getStoredKey,
treeNodeApi,
type TreeNodeApi,
tryGetSchema,
getStoredKey,
getPropertyKeyFromStoredKey,
getIdentifierFromNode,
} from "./treeNodeApi.js";
export { createFromCursor } from "./create.js";
export {
Expand Down
7 changes: 5 additions & 2 deletions packages/dds/tree/src/simple-tree/api/treeNodeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
} from "../core/index.js";
import type { TreeChangeEvents } from "./treeChangeEvents.js";
import { isObjectNodeSchema } from "../node-kinds/index.js";
import { getTreeNodeForField } from "../getTreeNodeForField.js";
import { tryGetTreeNodeForField } from "../getTreeNodeForField.js";

/**
* Provides various functions for analyzing {@link TreeNode}s.
Expand Down Expand Up @@ -83,6 +83,9 @@ export interface TreeNodeApi {
* Return the node under which this node resides in the tree (or undefined if this is a root node of the tree).
*
* @throws A {@link @fluidframework/telemetry-utils#UsageError} if the node has been {@link TreeStatus.Deleted | deleted}.
*
* @see {@link (TreeAlpha:interface).child}
* @see {@link (TreeAlpha:interface).children}
*/
parent(node: TreeNode): TreeNode | undefined;

Expand Down Expand Up @@ -316,7 +319,7 @@ export function getIdentifierFromNode(
const key = identifierFieldKeys[0] ?? oob();
const identifierField = flexNode.tryGetField(key);
assert(identifierField !== undefined, 0xbb5 /* missing identifier field */);
const identifierValue = getTreeNodeForField(identifierField);
const identifierValue = tryGetTreeNodeForField(identifierField);
assert(typeof identifierValue === "string", 0xbb6 /* identifier not a string */);

const context = flexNode.context;
Expand Down
Loading
Loading