Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions .changeset/tricky-shirts-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
"@fluidframework/tree": minor
"__section": feature
---
Added `Tree.ensureSchema`

This helper function allows content to be tagged with a schema type before being inserted into the tree.
This allows content that would otherwise be ambiguous to be well-defined, without having to wrap it in a node constructor.

Example:

```typescript
const sf = new SchemaFactory("example");
class Dog extends sf.object("Dog", { name: sf.string() }) {}
class Cat extends sf.object("Cat", { name: sf.string() }) {}
class Root extends sf.object("Root", { pet: [Dog, Cat] }) {}
// ...
const pet = { name: "Max" };
view.root.pet = pet; // Error: `pet` is ambiguous - is it a Dog or a Cat?
view.root.pet = new Dog(pet); // This works, but has the overhead of creating a Dog node before the insertion actually happens.
TreeAlpha.ensureSchema(Dog, pet); // Instead, this tags the `pet` object as a Dog...
view.root.pet = pet; // So now there is no error for a normal insertion - it's a Dog.
```

This function works by leveraging the new `schemaSymbol`, which is also available for use.
See its documentation for more information.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
// @public
export type ConfigTypes = string | number | boolean | number[] | string[] | boolean[] | undefined;

// @beta @sealed @system
export abstract class ErasedBaseType<out Name = unknown> {
protected constructor();
protected abstract brand(dummy: never): Name;
}

// @public @sealed
export abstract class ErasedType<out Name = unknown> {
static [Symbol.hasInstance](value: never): value is never;
Expand Down
4 changes: 2 additions & 2 deletions packages/common/core-interfaces/src/erasedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,10 @@ export abstract class ErasedType<out Name = unknown> {
*
* This class should only be a `type` package export, preventing users from extending it directly.
*
* Since {@link ErasedTypeImplementation} is exported as `@internal`, this restricts implementations of the sealed interfaces to users of `@internal` APIs, which should be anything withing this release group.
* Since {@link ErasedTypeImplementation} is exported as `@internal`, this restricts implementations of the sealed interfaces to users of `@internal` APIs, which should be anything within this release group.
* Any finer grained restrictions can be done as documentation, but not type enforced.
* @sealed
* @alpha
* @beta
* @system
*/
export abstract class ErasedBaseType<out Name = unknown> {
Expand Down
24 changes: 14 additions & 10 deletions packages/dds/tree/api-report/tree.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function adaptEnum<TScope extends string, const TEnum extends Record<stri
}, Record<string, never>, true, Record<string, never>, undefined>; }[keyof TEnum]>;
};

// @alpha @input
// @beta @input
export interface AllowedTypeMetadata {
readonly custom?: unknown;
readonly stagedSchemaUpgrade?: SchemaUpgrade;
Expand All @@ -24,13 +24,13 @@ export interface AllowedTypeMetadata {
// @public @system
export type AllowedTypes = readonly LazyItem<TreeNodeSchema>[];

// @alpha @sealed
// @beta @sealed
export type AllowedTypesFull<T extends readonly AnnotatedAllowedType[] = readonly AnnotatedAllowedType[]> = AnnotatedAllowedTypes<T> & UnannotateAllowedTypesList<T>;

// @alpha @sealed
// @beta @sealed
export type AllowedTypesFullEvaluated = AllowedTypesFull<readonly AnnotatedAllowedType<TreeNodeSchema>[]>;

// @alpha @sealed
// @beta @sealed @system
export type AllowedTypesFullFromMixed<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = UnannotateAllowedTypesList<T> & AnnotatedAllowedTypes<AnnotateAllowedTypesList<T>>;

// @alpha @sealed @system
Expand All @@ -39,15 +39,15 @@ export type AllowedTypesFullFromMixedUnsafe<T extends readonly Unenforced<Annota
// @alpha @sealed @system
export type AllowedTypesFullUnsafe<T extends readonly AnnotatedAllowedTypeUnsafe[] = readonly AnnotatedAllowedTypeUnsafe[]> = AnnotatedAllowedTypes<T> & UnannotateAllowedTypesListUnsafe<T>;

// @alpha @input
// @beta @input
export interface AllowedTypesMetadata {
readonly custom?: unknown;
}

// @alpha
export function allowUnused<T>(t?: T): void;

// @alpha @system
// @beta @system
export type AnnotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
[I in keyof T]: T[I] extends AnnotatedAllowedType<unknown> ? T[I] : AnnotatedAllowedType<T[I]>;
};
Expand All @@ -57,13 +57,13 @@ export type AnnotateAllowedTypesListUnsafe<T extends readonly Unenforced<Annotat
[I in keyof T]: T[I] extends AnnotatedAllowedTypeUnsafe ? T[I] : AnnotatedAllowedTypeUnsafe<T[I]>;
};

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

// @alpha @sealed
// @beta @sealed
export interface AnnotatedAllowedTypes<T = readonly AnnotatedAllowedType[]> extends ErasedBaseType<"tree.AnnotatedAllowedTypes"> {
evaluate(): AllowedTypesFullEvaluated;
evaluateIdentifiers(): ReadonlySet<string>;
Expand Down Expand Up @@ -965,7 +965,10 @@ export interface SchemaStaticsAlpha {
readonly typesRecursive: <const T extends readonly Unenforced<AnnotatedAllowedType | LazyItem<TreeNodeSchema>>[]>(t: T, metadata?: AllowedTypesMetadata) => AllowedTypesFullFromMixedUnsafe<T>;
}

// @alpha @sealed
// @alpha
export const schemaSymbol: unique symbol;

// @beta @sealed
export class SchemaUpgrade {
// (undocumented)
protected _typeCheck: MakeNominal;
Expand Down Expand Up @@ -1348,6 +1351,7 @@ export interface TreeAlpha {
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>;
ensureSchema<TSchema extends TreeNodeSchema, TContent extends InsertableField<TSchema>>(schema: TSchema, content: TContent): TContent;
exportCompressed(tree: TreeNode | TreeLeafValue, options: {
idCompressor?: IIdCompressor;
} & Pick<CodecWriteOptions, "minVersionForCollab">): JsonCompatible<IFluidHandle>;
Expand Down Expand Up @@ -1638,7 +1642,7 @@ const typeNameSymbol: unique symbol;
// @public @system
export const typeSchemaSymbol: unique symbol;

// @alpha @system
// @beta @system
export type UnannotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
[I in keyof T]: T[I] extends AnnotatedAllowedType<infer X> ? X : T[I];
};
Expand Down
51 changes: 51 additions & 0 deletions packages/dds/tree/api-report/tree.beta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,49 @@ export function adaptEnum<TScope extends string, const TEnum extends Record<stri
}, Record<string, never>, true, Record<string, never>, undefined>; }[keyof TEnum]>;
};

// @beta @input
export interface AllowedTypeMetadata {
readonly custom?: unknown;
readonly stagedSchemaUpgrade?: SchemaUpgrade;
}

// @public @system
export type AllowedTypes = readonly LazyItem<TreeNodeSchema>[];

// @beta @sealed
export type AllowedTypesFull<T extends readonly AnnotatedAllowedType[] = readonly AnnotatedAllowedType[]> = AnnotatedAllowedTypes<T> & UnannotateAllowedTypesList<T>;

// @beta @sealed
export type AllowedTypesFullEvaluated = AllowedTypesFull<readonly AnnotatedAllowedType<TreeNodeSchema>[]>;

// @beta @sealed @system
export type AllowedTypesFullFromMixed<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = UnannotateAllowedTypesList<T> & AnnotatedAllowedTypes<AnnotateAllowedTypesList<T>>;

// @beta @input
export interface AllowedTypesMetadata {
readonly custom?: unknown;
}

// @beta @system
export type AnnotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
[I in keyof T]: T[I] extends AnnotatedAllowedType<unknown> ? T[I] : AnnotatedAllowedType<T[I]>;
};

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

// @beta @sealed
export interface AnnotatedAllowedTypes<T = readonly AnnotatedAllowedType[]> extends ErasedBaseType<"tree.AnnotatedAllowedTypes"> {
evaluate(): AllowedTypesFullEvaluated;
evaluateIdentifiers(): ReadonlySet<string>;
evaluateSet(): ReadonlySet<TreeNodeSchema>;
readonly metadata: AllowedTypesMetadata;
readonly types: T;
}

// @public @system
type ApplyKind<T, Kind extends FieldKind> = {
[FieldKind.Required]: T;
Expand Down Expand Up @@ -474,6 +514,12 @@ export interface SchemaStatics {
readonly string: LeafSchema<"string", string>;
}

// @beta @sealed
export class SchemaUpgrade {
// (undocumented)
protected _typeCheck: MakeNominal;
}

// @public @system
type ScopedSchemaName<TScope extends string | undefined, TName extends number | string> = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`;

Expand Down Expand Up @@ -773,6 +819,11 @@ const typeNameSymbol: unique symbol;
// @public @system
export const typeSchemaSymbol: unique symbol;

// @beta @system
export type UnannotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
[I in keyof T]: T[I] extends AnnotatedAllowedType<infer X> ? X : T[I];
};

// @public
export type Unenforced<_DesiredExtendsConstraint> = unknown;

Expand Down
51 changes: 51 additions & 0 deletions packages/dds/tree/api-report/tree.legacy.beta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,49 @@ export function adaptEnum<TScope extends string, const TEnum extends Record<stri
}, Record<string, never>, true, Record<string, never>, undefined>; }[keyof TEnum]>;
};

// @beta @input
export interface AllowedTypeMetadata {
readonly custom?: unknown;
readonly stagedSchemaUpgrade?: SchemaUpgrade;
}

// @public @system
export type AllowedTypes = readonly LazyItem<TreeNodeSchema>[];

// @beta @sealed
export type AllowedTypesFull<T extends readonly AnnotatedAllowedType[] = readonly AnnotatedAllowedType[]> = AnnotatedAllowedTypes<T> & UnannotateAllowedTypesList<T>;

// @beta @sealed
export type AllowedTypesFullEvaluated = AllowedTypesFull<readonly AnnotatedAllowedType<TreeNodeSchema>[]>;

// @beta @sealed @system
export type AllowedTypesFullFromMixed<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = UnannotateAllowedTypesList<T> & AnnotatedAllowedTypes<AnnotateAllowedTypesList<T>>;

// @beta @input
export interface AllowedTypesMetadata {
readonly custom?: unknown;
}

// @beta @system
export type AnnotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
[I in keyof T]: T[I] extends AnnotatedAllowedType<unknown> ? T[I] : AnnotatedAllowedType<T[I]>;
};

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

// @beta @sealed
export interface AnnotatedAllowedTypes<T = readonly AnnotatedAllowedType[]> extends ErasedBaseType<"tree.AnnotatedAllowedTypes"> {
evaluate(): AllowedTypesFullEvaluated;
evaluateIdentifiers(): ReadonlySet<string>;
evaluateSet(): ReadonlySet<TreeNodeSchema>;
readonly metadata: AllowedTypesMetadata;
readonly types: T;
}

// @public @system
type ApplyKind<T, Kind extends FieldKind> = {
[FieldKind.Required]: T;
Expand Down Expand Up @@ -477,6 +517,12 @@ export interface SchemaStatics {
readonly string: LeafSchema<"string", string>;
}

// @beta @sealed
export class SchemaUpgrade {
// (undocumented)
protected _typeCheck: MakeNominal;
}

// @public @system
type ScopedSchemaName<TScope extends string | undefined, TName extends number | string> = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`;

Expand Down Expand Up @@ -785,6 +831,11 @@ const typeNameSymbol: unique symbol;
// @public @system
export const typeSchemaSymbol: unique symbol;

// @beta @system
export type UnannotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
[I in keyof T]: T[I] extends AnnotatedAllowedType<infer X> ? X : T[I];
};

// @public
export type Unenforced<_DesiredExtendsConstraint> = unknown;

Expand Down
2 changes: 1 addition & 1 deletion packages/dds/tree/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@
"concurrently": "^8.2.1",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"dependency-cruiser": "^14.1.0",
"dependency-cruiser": "^17.1.0",
"diff": "^3.5.0",
"easy-table": "^1.1.1",
"eslint": "~8.57.1",
Expand Down
1 change: 1 addition & 0 deletions packages/dds/tree/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export {
type WithType,
type NodeChangedData,
type SchemaUpgrade,
schemaSymbol,
// Types not really intended for public use, but used in links.
// Can not be moved to internalTypes since doing so causes app code to throw errors like:
// Error: src/simple-tree/objectNode.ts:72:1 - (ae-unresolved-link) The @link reference could not be resolved: The package "@fluidframework/tree" does not have an export "TreeNodeApi"
Expand Down
42 changes: 42 additions & 0 deletions packages/dds/tree/src/shared-tree/treeAlpha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import {
importConcise,
exportConcise,
borrowCursorFromTreeNodeOrValue,
schemaSymbol,
type TreeNodeSchema,
} from "../simple-tree/index.js";
import { brand, extractFromOpaque, type JsonCompatible } from "../util/index.js";
import {
Expand Down Expand Up @@ -519,6 +521,31 @@ export interface TreeAlpha {
onInvalidation: () => void,
trackDuring: () => TResult,
): ObservationResults<TResult>;

/**
* Ensures that the provided content will be interpreted as the given schema when inserting into the tree.
* @returns `content`, for convenience.
* @remarks
* If applicable, this will tag the given content with a {@link schemaSymbol | special property} that indicates its intended schema.
* The `content` will be interpreted as the given `schema` when later inserted into the tree.
* This is particularly useful when the content's schema cannot be inferred from its structure alone because it is compatible with multiple schemas.
* @example
* ```typescript
* const sf = new SchemaFactory("example");
* class Dog extends sf.object("Dog", { name: sf.string() }) {}
* class Cat extends sf.object("Cat", { name: sf.string() }) {}
* class Root extends sf.object("Root", { pet: [Dog, Cat] }) {}
* // ...
* const pet = { name: "Max" };
* view.root.pet = pet; // Error: ambiguous schema - is it a Dog or a Cat?
* TreeAlpha.ensureSchema(Dog, pet); // Tags `pet` as a Dog.
* view.root.pet = pet; // No error - it's a Dog.
* ```
*/
ensureSchema<TSchema extends TreeNodeSchema, TContent extends InsertableField<TSchema>>(
schema: TSchema,
content: TContent,
): TContent;
}

/**
Expand Down Expand Up @@ -1003,6 +1030,21 @@ export const TreeAlpha: TreeAlpha = {
}
return result;
},

ensureSchema<TSchema extends TreeNodeSchema, TNode extends InsertableField<TSchema>>(
schema: TSchema,
node: TNode,
): TNode {
if (typeof node === "object" && node !== null) {
Reflect.defineProperty(node, schemaSymbol, {
configurable: false,
enumerable: false,
writable: true,
value: schema.identifier,
});
}
return node;
},
};

/**
Expand Down
Loading