Skip to content

Commit 9a4d61b

Browse files
authored
Add schema-derived types for editable tree 2 proxies (#17736)
## Description This creates adaptations of the editable-tree-2 schema-derived types which: 1. Produce the "proxy" API objects 2. "Unbox" unconditionally It also renames "List" to "SharedTreeList".
1 parent 9e44f5c commit 9a4d61b

File tree

7 files changed

+310
-27
lines changed

7 files changed

+310
-27
lines changed

experimental/dds/tree2/src/feature-libraries/editable-tree-2/index.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,18 @@ export {
2626
TreeStatus,
2727
} from "./editableTreeTypes";
2828

29-
export { getProxyForField, List } from "./proxies";
29+
export {
30+
getProxyForField,
31+
SharedTreeList,
32+
ObjectFields,
33+
ProxyField,
34+
ProxyFieldInner,
35+
ProxyNode,
36+
ProxyNodeUnion,
37+
SharedTreeMap,
38+
SharedTreeObject,
39+
is,
40+
} from "./proxies";
3041
export { createRawStruct, rawStructErrorMessage, nodeContent } from "./rawStruct";
3142

3243
export {

experimental/dds/tree2/src/feature-libraries/editable-tree-2/proxies/index.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,14 @@
33
* Licensed under the MIT License.
44
*/
55

6-
export { getProxyForField } from "./proxies";
7-
export { List } from "./types";
6+
export { getProxyForField, is } from "./proxies";
7+
export {
8+
SharedTreeList,
9+
ObjectFields,
10+
ProxyField,
11+
ProxyFieldInner,
12+
ProxyNode,
13+
ProxyNodeUnion,
14+
SharedTreeMap,
15+
SharedTreeObject,
16+
} from "./types";

experimental/dds/tree2/src/feature-libraries/editable-tree-2/proxies/proxies.ts

+39-14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
FieldNodeSchema,
1010
FieldSchema,
1111
StructSchema,
12+
TreeSchema,
1213
schemaIsFieldNode,
1314
schemaIsLeaf,
1415
schemaIsMap,
@@ -18,7 +19,7 @@ import { FieldKinds } from "../../default-field-kinds";
1819
import { FieldNode, TreeNode, TypedField, TypedNodeUnion } from "../editableTreeTypes";
1920
import { LazySequence } from "../lazyField";
2021
import { FieldKey } from "../../../core";
21-
import { List } from "./types";
22+
import { ProxyField, ProxyNode, SharedTreeList, SharedTreeObject } from "./types";
2223

2324
/** Symbol used to store a private/internal reference to the underlying editable tree node. */
2425
const treeNodeSym = Symbol("TreeNode");
@@ -38,19 +39,32 @@ export function setTreeNode(target: any, treeNode: TreeNode) {
3839
});
3940
}
4041

42+
/**
43+
* Checks if the given object is a {@link SharedTreeObject}
44+
*/
45+
export function is<TSchema extends StructSchema>(
46+
x: unknown,
47+
schema: TSchema,
48+
): x is SharedTreeObject<TSchema> {
49+
// TODO: Do this a better way. Perhaps, should `treeNodeSym` be attached to object proxies via `setTreeNode`?
50+
return (x as any)[treeNodeSym].schema === schema;
51+
}
52+
4153
// TODO: Implement lifetime. The proxy that should be cached on their respective nodes and reused.
4254
// Object identity is tied to the proxy instance (not the target object)
4355

4456
/** Retrieve the associated proxy for the given field. */
45-
export function getProxyForField<T extends FieldSchema>(field: TypedField<T>) {
57+
export function getProxyForField<TSchema extends FieldSchema>(
58+
field: TypedField<TSchema>,
59+
): ProxyField<TSchema> {
4660
switch (field.schema.kind) {
4761
case FieldKinds.required: {
4862
const asValue = field as TypedField<FieldSchema<typeof FieldKinds.required>>;
4963

5064
// TODO: Ideally, we would return leaves without first boxing them. However, this is not
5165
// as simple as calling '.content' since this skips the node and returns the FieldNode's
5266
// inner field.
53-
return getProxyForNode(asValue.boxedContent);
67+
return getProxyForNode(asValue.boxedContent) as ProxyField<TSchema>;
5468
}
5569
case FieldKinds.optional: {
5670
fail(`"not implemented"`);
@@ -63,28 +77,30 @@ export function getProxyForField<T extends FieldSchema>(field: TypedField<T>) {
6377
}
6478
}
6579

66-
export function getProxyForNode(treeNode: TreeNode) {
80+
export function getProxyForNode<TSchema extends TreeSchema>(
81+
treeNode: TreeNode,
82+
): ProxyNode<TSchema> {
6783
const schema = treeNode.schema;
6884

6985
if (schemaIsMap(schema)) {
7086
fail("Map not implemented");
7187
}
7288
if (schemaIsLeaf(schema)) {
73-
return treeNode.value;
89+
return treeNode.value as ProxyNode<TSchema>;
7490
}
7591
if (schemaIsFieldNode(schema)) {
76-
return createListProxy(treeNode);
92+
return createListProxy(treeNode) as ProxyNode<TSchema>;
7793
}
7894
if (schemaIsStruct(schema)) {
79-
return createObjectProxy(treeNode, schema);
95+
return createObjectProxy(treeNode, schema) as ProxyNode<TSchema>;
8096
}
8197
fail("unrecognized node kind");
8298
}
8399

84-
export function createObjectProxy<TTypes extends AllowedTypes>(
100+
export function createObjectProxy<TSchema extends StructSchema, TTypes extends AllowedTypes>(
85101
content: TypedNodeUnion<TTypes>,
86-
schema: StructSchema,
87-
) {
102+
schema: TSchema,
103+
): SharedTreeObject<TSchema> {
88104
// To satisfy 'deepEquals' level scrutiny, the target of the proxy must be an object with the same
89105
// 'null prototype' you would get from on object literal '{}' or 'Object.create(null)'. This is
90106
// because 'deepEquals' uses 'Object.getPrototypeOf' as a way to quickly reject objects with different
@@ -97,7 +113,14 @@ export function createObjectProxy<TTypes extends AllowedTypes>(
97113
{
98114
get(target, key): unknown {
99115
const field = content.tryGetField(key as FieldKey);
100-
return field === undefined ? undefined : getProxyForField(field);
116+
if (field !== undefined) {
117+
return getProxyForField(field);
118+
}
119+
// TODO: Do this a better way.
120+
if (key === treeNodeSym) {
121+
return { schema };
122+
}
123+
return undefined;
101124
},
102125
set(target, key, value) {
103126
// TODO: Implement set
@@ -126,7 +149,7 @@ export function createObjectProxy<TTypes extends AllowedTypes>(
126149
return p;
127150
},
128151
},
129-
) as unknown;
152+
) as SharedTreeObject<TSchema>;
130153
}
131154

132155
const getField = <TTypes extends AllowedTypes>(target: object) => {
@@ -257,7 +280,9 @@ function asIndex(key: string | symbol, length: number) {
257280
}
258281
}
259282

260-
export function createListProxy<TTypes extends AllowedTypes>(treeNode: TreeNode): List<TTypes> {
283+
export function createListProxy<TTypes extends AllowedTypes>(
284+
treeNode: TreeNode,
285+
): SharedTreeList<TTypes> {
261286
// Create a 'dispatch' object that this Proxy forwards to instead of the proxy target.
262287
// Own properties on the dispatch object are surfaced as own properties of the proxy.
263288
// (e.g., 'length', which is defined below).
@@ -279,7 +304,7 @@ export function createListProxy<TTypes extends AllowedTypes>(treeNode: TreeNode)
279304
// To satisfy 'deepEquals' level scrutiny, the target of the proxy must be an array literal in order
280305
// to pass 'Object.getPrototypeOf'. It also satisfies 'Array.isArray' and 'Object.prototype.toString'
281306
// requirements without use of Array[Symbol.species], which is potentially on a path ot deprecation.
282-
return new Proxy<List<TTypes>>([] as any, {
307+
return new Proxy<SharedTreeList<TTypes>>([] as any, {
283308
get: (target, key) => {
284309
const field = getField(dispatch);
285310
const maybeIndex = asIndex(key, field.length);

experimental/dds/tree2/src/feature-libraries/editable-tree-2/proxies/types.ts

+116-3
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,32 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import { AllowedTypes } from "../../typed-schema";
6+
import { SchemaAware } from "../..";
7+
import { RestrictiveReadonlyRecord } from "../../../util";
8+
import { FieldKinds } from "../../default-field-kinds";
9+
import { FieldKind } from "../../modular-schema";
10+
import {
11+
AllowedTypes,
12+
Any,
13+
FieldNodeSchema,
14+
FieldSchema,
15+
InternalTypedSchemaTypes,
16+
LeafSchema,
17+
MapSchema,
18+
StructSchema,
19+
TreeSchema,
20+
} from "../../typed-schema";
721
import {
8-
UnboxNodeUnion,
922
CheckTypesOverlap,
1023
FlexibleNodeContent,
1124
Sequence,
25+
NodeKeyField,
26+
AssignableFieldKinds,
1227
} from "../editableTreeTypes";
1328

1429
/** Implements 'readonly T[]' and the list mutation APIs. */
15-
export interface List<TTypes extends AllowedTypes> extends ReadonlyArray<UnboxNodeUnion<TTypes>> {
30+
export interface SharedTreeList<TTypes extends AllowedTypes>
31+
extends ReadonlyArray<ProxyNodeUnion<TTypes>> {
1632
/**
1733
* Inserts new item(s) at a specified location.
1834
* @param index - The index at which to insert `value`.
@@ -131,3 +147,100 @@ export interface List<TTypes extends AllowedTypes> extends ReadonlyArray<UnboxNo
131147
// source: Sequence<CheckTypesOverlap<TTypesSource, TTypes>>,
132148
// ): void;
133149
}
150+
151+
/**
152+
* An object which supports property-based access to fields.
153+
* @alpha
154+
*/
155+
export type SharedTreeObject<TSchema extends StructSchema> = ObjectFields<
156+
TSchema["structFieldsObject"]
157+
>;
158+
159+
/**
160+
* Helper for generating the properties of a {@link SharedTreeObject}.
161+
* @alpha
162+
*/
163+
export type ObjectFields<TFields extends RestrictiveReadonlyRecord<string, FieldSchema>> = {
164+
// Add getter only (make property readonly) when the field is **not** of a kind that has a logical set operation.
165+
// If we could map to getters and setters separately, we would preferably do that, but we can't.
166+
// See https://github.com/microsoft/TypeScript/issues/43826 for more details on this limitation.
167+
readonly [key in keyof TFields as TFields[key]["kind"] extends AssignableFieldKinds
168+
? never
169+
: key]: ProxyField<TFields[key]>;
170+
} & {
171+
// Add setter (make property writable) when the field is of a kind that has a logical set operation.
172+
// If we could map to getters and setters separately, we would preferably do that, but we can't.
173+
// See https://github.com/microsoft/TypeScript/issues/43826 for more details on this limitation.
174+
-readonly [key in keyof TFields as TFields[key]["kind"] extends AssignableFieldKinds
175+
? key
176+
: never]: ProxyField<TFields[key]>;
177+
};
178+
179+
/**
180+
* A map of string keys to tree objects.
181+
* @alpha
182+
*/
183+
export type SharedTreeMap<TSchema extends MapSchema> = Map<string, ProxyNode<TSchema>>;
184+
185+
/**
186+
* Given a field's schema, return the corresponding object in the proxy-based API.
187+
* @alpha
188+
*/
189+
export type ProxyField<
190+
TSchema extends FieldSchema,
191+
// If "notEmpty", then optional fields will unbox to their content (not their content | undefined)
192+
Emptiness extends "maybeEmpty" | "notEmpty" = "maybeEmpty",
193+
> = ProxyFieldInner<TSchema["kind"], TSchema["allowedTypes"], Emptiness>;
194+
195+
/**
196+
* Helper for implementing {@link InternalEditableTreeTypes#ProxyField}.
197+
* @alpha
198+
*/
199+
export type ProxyFieldInner<
200+
Kind extends FieldKind,
201+
TTypes extends AllowedTypes,
202+
Emptiness extends "maybeEmpty" | "notEmpty",
203+
> = Kind extends typeof FieldKinds.sequence
204+
? SharedTreeList<TTypes>
205+
: Kind extends typeof FieldKinds.required
206+
? ProxyNodeUnion<TTypes>
207+
: Kind extends typeof FieldKinds.optional
208+
? ProxyNodeUnion<TTypes> | (Emptiness extends "notEmpty" ? never : undefined)
209+
: // Since struct already provides a short-hand accessor for the local field key, and the field provides a nicer general API than the node under it in this case, do not unbox nodeKey fields.
210+
Kind extends typeof FieldKinds.nodeKey
211+
? NodeKeyField
212+
: // TODO: forbidden
213+
unknown;
214+
215+
/**
216+
* Given multiple node schema types, return the corresponding object type union in the proxy-based API.
217+
* @alpha
218+
*/
219+
export type ProxyNodeUnion<TTypes extends AllowedTypes> = TTypes extends readonly [Any]
220+
? unknown
221+
: {
222+
// TODO: Is the the best way to write this type function? Can it be simplified?
223+
// This first maps the tuple of AllowedTypes to a tuple of node API types.
224+
// Then, it uses [number] to index arbitrarily into that tuple, effectively converting the type tuple into a type union.
225+
[Index in keyof TTypes]: TTypes[Index] extends InternalTypedSchemaTypes.LazyItem<
226+
infer InnerType
227+
>
228+
? InnerType extends TreeSchema
229+
? ProxyNode<InnerType>
230+
: never
231+
: never;
232+
}[number];
233+
234+
/**
235+
* Given a node's schema, return the corresponding object in the proxy-based API.
236+
* @alpha
237+
*/
238+
export type ProxyNode<TSchema extends TreeSchema> = TSchema extends LeafSchema
239+
? SchemaAware.InternalTypes.TypedValue<TSchema["leafValue"]>
240+
: TSchema extends MapSchema
241+
? SharedTreeMap<TSchema>
242+
: TSchema extends FieldNodeSchema
243+
? ProxyField<TSchema["structFieldsObject"][""]>
244+
: TSchema extends StructSchema
245+
? SharedTreeObject<TSchema>
246+
: unknown;

experimental/dds/tree2/src/feature-libraries/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ export {
254254
CheckTypesOverlap,
255255
TreeStatus,
256256
getProxyForField,
257+
ObjectFields,
258+
ProxyField,
259+
ProxyFieldInner,
260+
ProxyNode,
261+
ProxyNodeUnion,
262+
SharedTreeList,
263+
SharedTreeMap,
264+
SharedTreeObject,
265+
is,
257266
} from "./editable-tree-2";
258267

259268
// Split into separate import and export for compatibility with API-Extractor.

experimental/dds/tree2/src/test/feature-libraries/editable-tree-2/list.spec.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
*/
55

66
import { strict as assert } from "assert";
7-
import { SchemaBuilder } from "../../../feature-libraries";
7+
import { ProxyField, SchemaBuilder, SharedTreeList } from "../../../feature-libraries";
88
import { leaf } from "../../../domains";
9-
// eslint-disable-next-line import/no-internal-modules
10-
import { TypedNode, List } from "../../../feature-libraries/editable-tree-2";
119
import { createTreeView } from "./utils";
1210

1311
const builder = new SchemaBuilder({ scope: "test", libraries: [leaf.library] });
@@ -23,9 +21,8 @@ const root = builder.struct("root", {
2321
numbers: numberList,
2422
});
2523

26-
type Root = TypedNode<typeof root>;
27-
2824
const schema = builder.toDocumentSchema(root);
25+
type Root = ProxyField<(typeof schema)["rootFieldSchema"]>;
2926

3027
describe("List", () => {
3128
/** Similar to JSON stringify, but preserves 'undefined' and leaves numbers as-is. */
@@ -81,7 +78,7 @@ describe("List", () => {
8178

8279
// TODO: Combine createList helpers once we unbox unions.
8380
/** Helper that creates a new List<number> proxy */
84-
function createNumberList(items: readonly number[]): List<[typeof leaf.number]> {
81+
function createNumberList(items: readonly number[]): SharedTreeList<[typeof leaf.number]> {
8582
const list = createTree().numbers;
8683
list.insertAtStart(items);
8784
assert.deepEqual(list, items);
@@ -90,7 +87,7 @@ describe("List", () => {
9087

9188
// TODO: Combine createList helpers once we unbox unions.
9289
/** Helper that creates a new List<string> proxy */
93-
function createStringList(items: readonly string[]): List<[typeof leaf.string]> {
90+
function createStringList(items: readonly string[]): SharedTreeList<[typeof leaf.string]> {
9491
const list = createTree().strings;
9592
list.insertAtStart(items);
9693
assert.deepEqual(list, items);

0 commit comments

Comments
 (0)