Skip to content

fix(tree): walk allowed types #24820

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 41 commits into from
Jun 20, 2025
Merged
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
aa6c1f1
stuff
jenn-le Jun 2, 2025
4faed42
exposing tree node schema api issue
jenn-le Jun 5, 2025
1a34af1
Merge remote-tracking branch 'upstream/main' into fix-walk-allowed-types
jenn-le Jun 10, 2025
e831ff7
write tests and get them working
jenn-le Jun 10, 2025
8792f09
use non alpha
jenn-le Jun 10, 2025
16d2396
clean up and add tests
jenn-le Jun 11, 2025
9986975
Update packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts
jenn-le Jun 11, 2025
58201d3
Update packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts
jenn-le Jun 11, 2025
426ef3d
cleanup
jenn-le Jun 11, 2025
f732d2f
clean up tests
jenn-le Jun 11, 2025
630b151
get rid of readonly sets of annotated allowed schemas
jenn-le Jun 11, 2025
18e5318
cleanup
jenn-le Jun 11, 2025
4c5a5b4
build
jenn-le Jun 12, 2025
3d12712
visit all nodes
jenn-le Jun 14, 2025
616dba6
rename and changeset
jenn-le Jun 14, 2025
593e70b
downcast helper
jenn-le Jun 14, 2025
4addf0d
dedupe annotated types
jenn-le Jun 16, 2025
59e0919
refactor
jenn-le Jun 16, 2025
1cb7ca2
Merge remote-tracking branch 'upstream/main' into fix-walk-allowed-types
jenn-le Jun 16, 2025
9a7102d
remove sealed
jenn-le Jun 18, 2025
79065bb
fix configuration
jenn-le Jun 18, 2025
2ac5480
fix
jenn-le Jun 18, 2025
058687e
remove internal
jenn-le Jun 18, 2025
508e6b4
Update packages/dds/tree/src/simple-tree/api/configuration.ts
jenn-le Jun 18, 2025
68f1350
Update .changeset/six-steaks-clean.md
jenn-le Jun 18, 2025
12b28df
Update packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts
jenn-le Jun 18, 2025
3e27e94
Merge remote-tracking branch 'upstream/main' into fix-walk-allowed-types
jenn-le Jun 18, 2025
ed00bd7
Merge branch 'fix-walk-allowed-types' of https://github.com/jenn-le/F…
jenn-le Jun 18, 2025
1e122cb
use annotatedallowedtypes
jenn-le Jun 19, 2025
5b6b110
Update packages/dds/tree/src/simple-tree/api/configuration.ts
jenn-le Jun 19, 2025
d6cddef
Update packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts
jenn-le Jun 19, 2025
55420e8
Update packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts
jenn-le Jun 19, 2025
29fb81f
fix typo
jenn-le Jun 19, 2025
dd106d7
fix error
jenn-le Jun 19, 2025
6fd8393
Update packages/dds/tree/src/simple-tree/core/walkSchema.ts
jenn-le Jun 19, 2025
b4e7de5
don't pass annotations to walkNodeSchema
jenn-le Jun 19, 2025
2519edf
build
jenn-le Jun 19, 2025
5a2e184
Merge branch 'fix-walk-allowed-types' of https://github.com/jenn-le/F…
jenn-le Jun 19, 2025
2ab61e9
fixes
jenn-le Jun 19, 2025
4d07d9e
cleanup
jenn-le Jun 19, 2025
8100000
fix tests
jenn-le Jun 19, 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
9 changes: 9 additions & 0 deletions .changeset/six-steaks-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"fluid-framework": minor
"@fluidframework/tree": minor
"__section": tree
---
Rename and change type of `annotatedAllowedTypeSet` on `FieldSchemaAlpha` to more closely align with `allowedTypesSet`

Check warning on line 6 in .changeset/six-steaks-clean.md

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Adverbs] Remove 'closely' if it's not important to the meaning of the statement. Raw Output: {"message": "[Microsoft.Adverbs] Remove 'closely' if it's not important to the meaning of the statement.", "location": {"path": ".changeset/six-steaks-clean.md", "range": {"start": {"line": 6, "column": 83}}}, "severity": "WARNING"}

This changes the `annotatedAllowedTypeSet` property on [`FieldSchemaAlpha`](https://fluidframework.com/docs/api/fluid-framework/fieldschemaalpha-class).
It is now called `annotatedAllowedTypesNormalized` and stores evaluated schemas along with their annotations in a list of objects rather than as a mapping from the schemas to their annotations. This makes the API easier to use and better aligns with the current public APIs.
12 changes: 9 additions & 3 deletions packages/dds/tree/api-report/tree.alpha.api.md
Original file line number Diff line number Diff line change
@@ -32,9 +32,9 @@ export interface AllowedTypesMetadata {
export function allowUnused<T>(t?: T): void;

// @alpha
export interface AnnotatedAllowedType<T extends TreeNodeSchema = TreeNodeSchema> {
export interface AnnotatedAllowedType<T = LazyItem<TreeNodeSchema>> {
readonly metadata: AllowedTypeMetadata;
readonly type: LazyItem<T>;
readonly type: T;
}

// @alpha
@@ -214,7 +214,7 @@ export class FieldSchemaAlpha<Kind extends FieldKind = FieldKind, Types extends
readonly allowedTypesMetadata: AllowedTypesMetadata;
// (undocumented)
readonly annotatedAllowedTypes: ImplicitAnnotatedAllowedTypes;
get annotatedAllowedTypeSet(): ReadonlyMap<TreeNodeSchema, AllowedTypeMetadata>;
get annotatedAllowedTypesNormalized(): NormalizedAnnotatedAllowedTypes;
get persistedMetadata(): JsonCompatibleReadOnlyObject | undefined;
}

@@ -618,6 +618,12 @@ export interface NodeSchemaOptionsAlpha<out TCustomMetadata = unknown> extends N
// @alpha
export const noopValidator: JsonValidator;

// @alpha
export interface NormalizedAnnotatedAllowedTypes {
readonly metadata: AllowedTypesMetadata;
readonly types: readonly AnnotatedAllowedType<TreeNodeSchema>[];
}

// @public @system
export type ObjectFromSchemaRecord<T extends RestrictiveStringRecord<ImplicitFieldSchema>> = RestrictiveStringRecord<ImplicitFieldSchema> extends T ? {} : {
-readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField<T[Property]> : unknown;
1 change: 1 addition & 0 deletions packages/dds/tree/src/index.ts
Original file line number Diff line number Diff line change
@@ -158,6 +158,7 @@ export {
type ImplicitAnnotatedFieldSchema,
type AnnotatedAllowedType,
type AnnotatedAllowedTypes,
type NormalizedAnnotatedAllowedTypes,
type AllowedTypeMetadata,
type AllowedTypesMetadata,
type InsertableObjectFromAnnotatedSchemaRecord,
7 changes: 5 additions & 2 deletions packages/dds/tree/src/shared-tree/schematizingTreeView.ts
Original file line number Diff line number Diff line change
@@ -22,7 +22,6 @@ import {
TreeStatus,
} from "../feature-libraries/index.js";
import {
type FieldSchema,
type ImplicitFieldSchema,
type SchemaCompatibilityStatus,
type TreeView,
@@ -53,6 +52,7 @@ import {
areImplicitFieldSchemaEqual,
createUnknownOptionalFieldPolicy,
prepareForInsertionContextless,
type FieldSchema,
} from "../simple-tree/index.js";
import {
type Breakable,
@@ -353,7 +353,10 @@ export class SchematizingSimpleTreeView<
);
this.checkout.forest.anchors.slots.set(
SimpleContextSlot,
new HydratedContext(this.rootFieldSchema.allowedTypeSet, view.context),
new HydratedContext(
normalizeFieldSchema(this.rootFieldSchema).annotatedAllowedTypesNormalized,
view.context,
),
);

const unregister = this.checkout.storedSchema.events.on("afterSchemaChange", () => {
10 changes: 8 additions & 2 deletions packages/dds/tree/src/simple-tree/api/configuration.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,9 @@ import { UsageError } from "@fluidframework/telemetry-utils/internal";
import {
type FieldSchemaAlpha,
type ImplicitFieldSchema,
evaluateLazySchema,
FieldKind,
isAnnotatedAllowedType,
markSchemaMostDerived,
normalizeFieldSchema,
} from "../schemaTypes.js";
@@ -202,8 +204,12 @@ export class TreeViewConfiguration<
debugAssert(() => !definitions.has(schema.identifier));
definitions.set(schema.identifier, schema as SimpleNodeSchema & TreeNodeSchema);
},
allowedTypes(types): void {
checkUnion(types, config.preventAmbiguity, ambiguityErrors);
allowedTypes({ types }): void {
checkUnion(
types.map((t) => evaluateLazySchema(isAnnotatedAllowedType(t) ? t.type : t)),
config.preventAmbiguity,
ambiguityErrors,
);
},
});

6 changes: 3 additions & 3 deletions packages/dds/tree/src/simple-tree/core/context.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import type {
} from "../../feature-libraries/index.js";
import { brand } from "../../util/index.js";

import type { TreeNodeSchema } from "./treeNodeSchema.js";
import type { NormalizedAnnotatedAllowedTypes, TreeNodeSchema } from "./treeNodeSchema.js";
import { walkAllowedTypes } from "./walkSchema.js";

/**
@@ -52,7 +52,7 @@ export class Context {
* Since this walks the schema, it must not be invoked during schema declaration or schema forward references could fail to be resolved.
*/
public constructor(
rootSchema: Iterable<TreeNodeSchema>,
rootSchema: NormalizedAnnotatedAllowedTypes,
public readonly flexContext: FlexTreeContext,
) {
const schema: Map<TreeNodeSchemaIdentifier, TreeNodeSchema> = new Map();
@@ -71,7 +71,7 @@ export class Context {
*/
export class HydratedContext extends Context {
public constructor(
rootSchema: Iterable<TreeNodeSchema>,
rootSchema: NormalizedAnnotatedAllowedTypes,
public override readonly flexContext: FlexTreeHydratedContext,
) {
super(rootSchema, flexContext);
3 changes: 3 additions & 0 deletions packages/dds/tree/src/simple-tree/core/index.ts
Original file line number Diff line number Diff line change
@@ -31,6 +31,9 @@ export {
type TreeNodeSchemaNonClass,
type TreeNodeSchemaCore,
type TreeNodeSchemaBoth,
type AnnotatedAllowedType,
type NormalizedAnnotatedAllowedTypes,
isAnnotatedAllowedTypes,
} from "./treeNodeSchema.js";
export { walkAllowedTypes, type SchemaVisitor } from "./walkSchema.js";
export { Context, HydratedContext, SimpleContextSlot } from "./context.js";
114 changes: 113 additions & 1 deletion packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,15 @@
* Licensed under the MIT License.
*/

import type { TreeLeafValue } from "../schemaTypes.js";
import { assert } from "@fluidframework/core-utils/internal";
import type { LazyItem } from "../flexList.js";
import type {
AllowedTypeMetadata,
AllowedTypesMetadata,
AnnotatedAllowedTypes,
ImplicitAnnotatedAllowedTypes,
TreeLeafValue,
} from "../schemaTypes.js";
import type { SimpleNodeSchemaBase } from "../simpleSchema.js";

import type { TreeNode } from "./treeNode.js";
@@ -62,6 +70,48 @@ export type TreeNodeSchema<
TCustomMetadata
>;

/**
* Stores annotations for an individual allowed type.
* @alpha
*/
export interface AnnotatedAllowedType<T = LazyItem<TreeNodeSchema>> {
/**
* Annotations for the allowed type.
*/
readonly metadata: AllowedTypeMetadata;
/**
* The allowed type the annotations apply to in a particular schema.
*/
readonly type: T;
}

/**
* Stores annotations for a set of evaluated annotated allowed types.
* @alpha
*/
export interface NormalizedAnnotatedAllowedTypes {
/**
* Annotations that apply to a set of allowed types.
*/
readonly metadata: AllowedTypesMetadata;
/**
* All the evaluated allowed types that the annotations apply to. The types themselves are also individually annotated.
*/
readonly types: readonly AnnotatedAllowedType<TreeNodeSchema>[];
}

/**
* Checks if the input is an {@link AnnotatedAllowedTypes}.
*/
export function isAnnotatedAllowedTypes(
allowedTypes: ImplicitAnnotatedAllowedTypes,
): allowedTypes is AnnotatedAllowedTypes {
return (
// Class based schema, and lazy schema references report type "function": filtering them out with typeof makes narrowing based on members mostly safe
typeof allowedTypes === "object" && "metadata" in allowedTypes && "types" in allowedTypes
);
}

/**
* Schema which is not a class.
* @remarks
@@ -231,8 +281,13 @@ export type TreeNodeSchemaBoth<

/**
* Data common to all tree node schema.
*
* @remarks
* Implementation detail of {@link TreeNodeSchema} which should be accessed instead of referring to this type directly.
*
* @privateRemarks
* All implementations must implement {@link TreeNodeSchemaCorePrivate} as well.
*
* @sealed @public
*/
export interface TreeNodeSchemaCore<
@@ -316,6 +371,63 @@ export interface TreeNodeSchemaCore<
createFromInsertable(data: TInsertable): Unhydrated<TreeNode | TreeLeafValue>;
}

/**
* {@link TreeNodeSchemaCore} extended with some non-exported APIs.
*/
export interface TreeNodeSchemaCorePrivate<
Name extends string = string,
Kind extends NodeKind = NodeKind,
TInsertable = never,
ImplicitlyConstructable extends boolean = boolean,
Info = unknown,
TCustomMetadata = unknown,
> extends TreeNodeSchemaCore<
Name,
Kind,
ImplicitlyConstructable,
Info,
TInsertable,
TCustomMetadata
> {
/**
* All possible annotated allowed types that a field under a node with this schema could have.
* @remarks
* In this case "field" includes anything that is a field in the internal (flex-tree) abstraction layer.
* This includes the content field for arrays, and all the fields for map nodes.
* If this node does not have fields (and thus is a leaf), the array will be empty.
*
* This set cannot be used before the schema in it have been defined:
* more specifically, when using lazy schema references (for example to make foreword references to schema which have not yet been defined),
* users must wait until after the schema are defined to access this array.
*
* @privateRemarks
* If this is stabilized, it will live alongside the childTypes property on {@link TreeNodeSchemaCore}.
* @system
*/
readonly childAnnotatedAllowedTypes: readonly NormalizedAnnotatedAllowedTypes[];
}

/**
* Downcasts a {@link TreeNodeSchemaCore} to {@link TreeNodeSchemaCorePrivate} if it is one.
*
* @remarks
* This function should only be used internally. The result should not be exposed publicly
* in any exported types or API return values.
*/
export function asTreeNodeSchemaCorePrivate(
schema: TreeNodeSchemaCore<string, NodeKind, boolean>,
): TreeNodeSchemaCorePrivate {
assert(
"childAnnotatedAllowedTypes" in schema,
"All implementations of TreeNodeSchemaCore must also implement TreeNodeSchemaCorePrivate",
);
assert(
Array.isArray((schema as TreeNodeSchemaCorePrivate).childAnnotatedAllowedTypes),
"All implementations of TreeNodeSchemaCore must also implement TreeNodeSchemaCorePrivate",
);
return schema as TreeNodeSchemaCorePrivate;
}

/**
* Kind of tree node.
* @remarks
23 changes: 16 additions & 7 deletions packages/dds/tree/src/simple-tree/core/walkSchema.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,11 @@
* Licensed under the MIT License.
*/

import type { TreeNodeSchema } from "./treeNodeSchema.js";
import {
asTreeNodeSchemaCorePrivate,
type NormalizedAnnotatedAllowedTypes,
type TreeNodeSchema,
} from "./treeNodeSchema.js";

/**
* Traverses all {@link TreeNodeSchema} schema reachable from `schema`, applying the visitor pattern.
@@ -16,9 +20,14 @@ export function walkNodeSchema(
if (visitedSet.has(schema)) {
return;
}

visitedSet.add(schema);

walkAllowedTypes(schema.childTypes, visitor, visitedSet);
const annotatedAllowedTypes = asTreeNodeSchemaCorePrivate(schema).childAnnotatedAllowedTypes;

for (const fieldAllowedTypes of annotatedAllowedTypes) {
walkAllowedTypes(fieldAllowedTypes, visitor, visitedSet);
}

// This visit is done at the end so the traversal order is most inner types first.
// This was picked since when fixing errors,
@@ -31,14 +40,14 @@ export function walkNodeSchema(
* Traverses all {@link TreeNodeSchema} schema reachable from `allowedTypes`, applying the visitor pattern.
*/
export function walkAllowedTypes(
allowedTypes: Iterable<TreeNodeSchema>,
annotatedAllowedTypes: NormalizedAnnotatedAllowedTypes,
visitor: SchemaVisitor,
visitedSet: Set<TreeNodeSchema> = new Set(),
): void {
for (const childType of allowedTypes) {
walkNodeSchema(childType, visitor, visitedSet);
for (const { type } of annotatedAllowedTypes.types) {
walkNodeSchema(type, visitor, visitedSet);
}
visitor.allowedTypes?.(allowedTypes);
visitor.allowedTypes?.(annotatedAllowedTypes);
}

/**
@@ -55,5 +64,5 @@ export interface SchemaVisitor {
*
* This includes every field, but also the allowed types array for maps and arrays and the root if starting at {@link walkAllowedTypes}.
*/
allowedTypes?: (allowedTypes: Iterable<TreeNodeSchema>) => void;
allowedTypes?: (allowedTypes: NormalizedAnnotatedAllowedTypes) => void;
}
2 changes: 1 addition & 1 deletion packages/dds/tree/src/simple-tree/createContext.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,6 @@ export function getUnhydratedContext(schema: ImplicitFieldSchema): Context {
const normalized = normalizeFieldSchema(schema);

const flexContext = new UnhydratedContext(defaultSchemaPolicy, toStoredSchema(schema));
return new Context(normalized.allowedTypeSet, flexContext);
return new Context(normalized.annotatedAllowedTypesNormalized, flexContext);
});
}
5 changes: 3 additions & 2 deletions packages/dds/tree/src/simple-tree/index.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,8 @@ export {
typeSchemaSymbol,
type WithType,
type TreeNodeSchema,
type AnnotatedAllowedType,
type NormalizedAnnotatedAllowedTypes,
NodeKind,
type TreeNodeSchemaClass,
type TreeNodeSchemaNonClass,
@@ -138,8 +140,6 @@ export type {
export {
type NodeFromSchema,
isTreeNodeSchemaClass,
type AnnotatedAllowedType,
type AnnotatedAllowedTypes,
type ImplicitFieldSchema,
type ImplicitAnnotatedFieldSchema,
type TreeFieldFromImplicitField,
@@ -158,6 +158,7 @@ export {
type AllowedTypes,
type AllowedTypeMetadata,
type AllowedTypesMetadata,
type AnnotatedAllowedTypes,
FieldKind,
FieldSchema,
type FieldSchemaAlpha,
8 changes: 7 additions & 1 deletion packages/dds/tree/src/simple-tree/leafNodeSchema.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,12 @@ import {
valueSchemaAllows,
} from "../feature-libraries/index.js";

import { NodeKind, type TreeNodeSchema, type TreeNodeSchemaNonClass } from "./core/index.js";
import {
NodeKind,
type NormalizedAnnotatedAllowedTypes,
type TreeNodeSchema,
type TreeNodeSchemaNonClass,
} from "./core/index.js";
import type { NodeSchemaMetadata, TreeLeafValue } from "./schemaTypes.js";
import type { SimpleLeafNodeSchema } from "./simpleSchema.js";
import type { JsonCompatibleReadOnlyObject } from "../util/index.js";
@@ -34,6 +39,7 @@ export class LeafNodeSchema<Name extends string, const T extends ValueSchema>
public readonly info: T;
public readonly implicitlyConstructable = true as const;
public readonly childTypes: ReadonlySet<TreeNodeSchema> = new Set();
public readonly childAnnotatedAllowedTypes: readonly NormalizedAnnotatedAllowedTypes[] = [];

public create(data: TreeValue<T> | FlexTreeNode): TreeValue<T> {
if (isFlexTreeNode(data)) {
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.