Skip to content
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

procedural data generation for identifiers #15800

Merged
merged 10 commits into from
Jun 13, 2023
20 changes: 18 additions & 2 deletions api-report/tree2.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,10 @@ export interface CursorAdapter<TNode> {
}

// @alpha
export function cursorForTypedTreeData<T extends TreeSchema>(schemaData: SchemaDataAndPolicy, schema: T, data: TypedNode<T, ApiMode.Simple>): ITreeCursorSynchronous;
export function cursorForTypedTreeData<T extends TreeSchema>(context: TreeDataContext, schema: T, data: TypedNode<T, ApiMode.Simple>): ITreeCursorSynchronous;

// @alpha
export function cursorFromContextualData(schemaData: SchemaDataAndPolicy, typeSet: TreeTypeSet, data: ContextuallyTypedNodeData): ITreeCursorSynchronous;
export function cursorFromContextualData(context: TreeDataContext, typeSet: TreeTypeSet, data: ContextuallyTypedNodeData): ITreeCursorSynchronous;

// @alpha (undocumented)
export const enum CursorLocationType {
Expand Down Expand Up @@ -384,6 +384,7 @@ export interface EditableTree extends Iterable<EditableField>, ContextuallyTyped
// @alpha
export interface EditableTreeContext extends ISubscribable<ForestEvents> {
clear(): void;
fieldSource?(key: FieldKey, schema: FieldStoredSchema): undefined | FieldGenerator;
free(): void;
prepareForEdit(): void;
get root(): EditableField;
Expand Down Expand Up @@ -507,6 +508,9 @@ export interface FieldEditor<TChangeset> {
buildChildChange(childIndex: number, change: NodeChangeset): TChangeset;
}

// @alpha
export type FieldGenerator = () => MapTree[];

// @alpha
export type FieldKey = LocalFieldKey | GlobalFieldKeySymbol;

Expand Down Expand Up @@ -1109,6 +1113,12 @@ interface LocalFields {
interface MakeNominal {
}

// @alpha
export interface MapTree extends NodeData {
// (undocumented)
fields: Map<FieldKey, MapTree[]>;
}

// @alpha
type Mark<TTree = ProtoNode> = Skip | Modify<TTree> | Delete<TTree> | MoveOut<TTree> | MoveIn | Insert<TTree>;

Expand Down Expand Up @@ -1663,6 +1673,12 @@ export interface TreeAdapter {
readonly output: TreeSchemaIdentifier;
}

// @alpha
export interface TreeDataContext {
fieldSource?(key: FieldKey, schema: FieldStoredSchema): undefined | FieldGenerator;
readonly schema: SchemaDataAndPolicy;
}

// @alpha (undocumented)
export interface TreeLocation {
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ export class AppState implements IAppState {
clientsSequence.insertNodes(
clientsSequence.length,
cursorFromContextualData(
clientsSequence.context.schema,
{
schema: clientsSequence.context.schema,
fieldSource: () => undefined,
},
rootAppStateSchema.types,
this.createInitialClientNode(numBubbles),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export class ClientWrapper implements IClient {
// TODO: better API
bubbles.insertNodes(
bubbles.length,
cursorFromContextualData(bubbles.context.schema, bubbles.fieldSchema.types, bubble),
cursorFromContextualData(
{ schema: bubbles.context.schema, fieldSource: () => undefined },
bubbles.fieldSchema.types,
bubble,
),
);
}

Expand Down
116 changes: 94 additions & 22 deletions experimental/dds/tree2/src/feature-libraries/contextuallyTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
MapTree,
symbolIsFieldKey,
ITreeCursorSynchronous,
symbolFromKey,
} from "../core";
// TODO:
// This module currently is assuming use of defaultFieldKinds.
Expand Down Expand Up @@ -145,27 +146,27 @@ export function getFieldKind(fieldSchema: FieldStoredSchema): FieldKind {
* @returns all allowed child types for `typeSet`.
*/
export function getAllowedTypes(
schema: SchemaDataAndPolicy,
schemaData: SchemaDataAndPolicy,
typeSet: TreeTypeSet,
): ReadonlySet<TreeSchemaIdentifier> {
// TODO: Performance: avoid the `undefined` case being frequent, possibly with caching in the caller of `getPossibleChildTypes`.
return typeSet ?? new Set(schema.treeSchema.keys());
return typeSet ?? new Set(schemaData.treeSchema.keys());
}

/**
* @returns all types, for which the data is schema-compatible.
*/
export function getPossibleTypes(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
typeSet: TreeTypeSet,
data: ContextuallyTypedNodeData,
) {
// All types allowed by schema
const allowedTypes = getAllowedTypes(schemaData, typeSet);
const allowedTypes = getAllowedTypes(context.schema, typeSet);

const possibleTypes: TreeSchemaIdentifier[] = [];
for (const allowed of allowedTypes) {
if (shallowCompatibilityTest(schemaData, allowed, data)) {
if (shallowCompatibilityTest(context.schema, allowed, data)) {
possibleTypes.push(allowed);
}
}
Expand Down Expand Up @@ -234,6 +235,41 @@ export type ContextuallyTypedNodeData =
*/
export type ContextuallyTypedFieldData = ContextuallyTypedNodeData | undefined;

/**
* Information needed to interpret a subtree described by {@link ContextuallyTypedNodeData} and {@link ContextuallyTypedFieldData}.
* @alpha
* TODO:
* Currently being exposed at the package level which also requires us to export MapTree at the package level.
* Refactor the FieldGenerator to use JsonableTree instead of MapTree, and convert them internally.
*/
export interface TreeDataContext {
/**
* Schema for the document which the tree will be used in.
*/
readonly schema: SchemaDataAndPolicy;

/**
* Procedural data generator for fields.
* Fields which provide generators here can be omitted in the input contextually typed data.
*
* @remarks
* TODO:
* For implementers of this which are not pure (like identifier generation),
* order of invocation should be made consistent and documented.
* This will be important for identifier elision optimizations in tree encoding for session based identifier generation.
*/
fieldSource?(key: FieldKey, schema: FieldStoredSchema): undefined | FieldGenerator;
}

/**
* Generates field content for a MapTree on demand.
* @alpha
* TODO:
* Currently being exposed at the package level which also requires us to export MapTree at the package level.
* Refactor the FieldGenerator to use JsonableTree instead of MapTree, and convert them internally.
*/
export type FieldGenerator = () => MapTree[];

/**
* Checks the type of a `ContextuallyTypedNodeData`.
*/
Expand Down Expand Up @@ -344,11 +380,11 @@ function shallowCompatibilityTest(
* @alpha
*/
export function cursorFromContextualData(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
typeSet: TreeTypeSet,
data: ContextuallyTypedNodeData,
): ITreeCursorSynchronous {
const mapTree = applyTypesFromContext(schemaData, typeSet, data);
const mapTree = applyTypesFromContext(context, typeSet, data);
return singleMapTreeCursor(mapTree);
}

Expand All @@ -357,12 +393,12 @@ export function cursorFromContextualData(
* @alpha
*/
export function cursorForTypedTreeData<T extends TreeSchema>(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
schema: T,
data: TypedNode<T, ApiMode.Simple>,
): ITreeCursorSynchronous {
return cursorFromContextualData(
schemaData,
context,
new Set([schema.name]),
data as ContextuallyTypedNodeData,
);
Expand All @@ -378,7 +414,7 @@ export function cursorForTypedData<T extends AllowedTypes>(
data: AllowedTypesToTypedTrees<ApiMode.Simple, T>,
): ITreeCursorSynchronous {
return cursorFromContextualData(
schemaData,
{ schema: schemaData },
allowedTypesToTypeSet(schema),
data as unknown as ContextuallyTypedNodeData,
);
Expand All @@ -391,11 +427,11 @@ export function cursorForTypedData<T extends AllowedTypes>(
* TODO: migrate APIs which take arrays of cursors to take cursors in fields mode.
*/
export function cursorsFromContextualData(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
field: FieldStoredSchema,
data: ContextuallyTypedNodeData | undefined,
): ITreeCursorSynchronous[] {
const mapTrees = applyFieldTypesFromContext(schemaData, field, data);
const mapTrees = applyFieldTypesFromContext(context, field, data);
return mapTrees.map(singleMapTreeCursor);
}

Expand All @@ -408,7 +444,11 @@ export function cursorsForTypedFieldData<T extends FieldSchema>(
schema: T,
data: TypedField<T, ApiMode.Simple>,
): ITreeCursorSynchronous {
return cursorFromContextualData(schemaData, schema.types, data as ContextuallyTypedNodeData);
return cursorFromContextualData(
{ schema: schemaData },
schema.types,
data as ContextuallyTypedNodeData,
);
}

/**
Expand All @@ -422,11 +462,11 @@ export function cursorsForTypedFieldData<T extends FieldSchema>(
* This should not be reexported from the parent module.
*/
export function applyTypesFromContext(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
typeSet: TreeTypeSet,
data: ContextuallyTypedNodeData,
): MapTree {
const possibleTypes: TreeSchemaIdentifier[] = getPossibleTypes(schemaData, typeSet, data);
const possibleTypes: TreeSchemaIdentifier[] = getPossibleTypes(context, typeSet, data);

assert(
possibleTypes.length !== 0,
Expand All @@ -438,7 +478,8 @@ export function applyTypesFromContext(
);

const type = possibleTypes[0];
const schema = lookupTreeSchema(schemaData, type);
const schema = lookupTreeSchema(context.schema, type);

if (isPrimitiveValue(data)) {
// This check avoids returning an out of schema node
// in the case where schema permits the value, but has required fields.
Expand All @@ -457,7 +498,7 @@ export function applyTypesFromContext(
primary !== undefined,
0x4d6 /* array data reported comparable with the schema without a primary field */,
);
const children = applyFieldTypesFromContext(schemaData, primary.schema, data);
const children = applyFieldTypesFromContext(context, primary.schema, data);
return {
value: undefined,
type,
Expand All @@ -467,13 +508,27 @@ export function applyTypesFromContext(
const fields: Map<FieldKey, MapTree[]> = new Map();
for (const key of fieldKeysFromData(data)) {
assert(!fields.has(key), 0x6b3 /* Keys should not be duplicated */);
const childSchema = getFieldSchema(key, schemaData, schema);
const children = applyFieldTypesFromContext(schemaData, childSchema, data[key]);
const childSchema = getFieldSchema(key, context.schema, schema);
const children = applyFieldTypesFromContext(context, childSchema, data[key]);

if (children.length > 0) {
fields.set(key, children);
}
}

for (const key of schema.globalFields.keys()) {
const currentKey = symbolFromKey(key);
if (data[currentKey] === undefined) {
setFieldForKey(currentKey, context, schema, fields);
}
}

for (const key of schema.localFields.keys()) {
if (data[key] === undefined) {
setFieldForKey(key, context, schema, fields);
}
}

const value = data[valueSymbol];
assert(
allowsValue(schema.value, value),
Expand All @@ -483,6 +538,23 @@ export function applyTypesFromContext(
}
}

function setFieldForKey(
key: FieldKey,
context: TreeDataContext,
schema: TreeStoredSchema,
fields: Map<FieldKey, MapTree[]>,
): void {
const requiredFieldSchema = getFieldSchema(key, context.schema, schema);
const multiplicity = getFieldKind(requiredFieldSchema).multiplicity;
if (multiplicity === Multiplicity.Value && context.fieldSource !== undefined) {
const fieldGenerator = context.fieldSource(key, requiredFieldSchema);
if (fieldGenerator !== undefined) {
const children = fieldGenerator();
fields.set(key, children);
}
}
}

function fieldKeysFromData(data: ContextuallyTypedNodeDataObject): FieldKey[] {
const keys: (string | symbol)[] = Reflect.ownKeys(data).filter(
(key) => typeof key === "string" || symbolIsFieldKey(key),
Expand All @@ -501,7 +573,7 @@ function fieldKeysFromData(data: ContextuallyTypedNodeDataObject): FieldKey[] {
* This should not be reexported from the parent module.
*/
export function applyFieldTypesFromContext(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
field: FieldStoredSchema,
data: ContextuallyTypedFieldData,
): MapTree[] {
Expand All @@ -516,13 +588,13 @@ export function applyFieldTypesFromContext(
if (multiplicity === Multiplicity.Sequence) {
assert(isArrayLike(data), 0x4d9 /* expected array for a sequence field */);
const children = Array.from(data, (child) =>
applyTypesFromContext(schemaData, field.types, child),
applyTypesFromContext(context, field.types, child),
);
return children;
}
assert(
multiplicity === Multiplicity.Value || multiplicity === Multiplicity.Optional,
0x4da /* single value provided for an unsupported field */,
);
return [applyTypesFromContext(schemaData, field.types, data)];
return [applyTypesFromContext(context, field.types, data)];
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export class FieldProxyTarget extends ProxyTarget<FieldAnchor> implements Editab
}
}

return cursorsFromContextualData(this.context.schema, this.fieldSchema, content);
return cursorsFromContextualData(this.context, this.fieldSchema, content);
}

public get [proxyTargetSymbol](): FieldProxyTarget {
Expand Down Expand Up @@ -498,7 +498,7 @@ const fieldProxyHandler: AdaptingProxyHandler<FieldProxyTarget, EditableField> =
);

const cursor = cursorFromContextualData(
target.context.schema,
target.context,
target.fieldSchema.types,
value as ContextuallyTypedNodeData,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ import {
Anchor,
SchemaDataAndPolicy,
ForestEvents,
FieldStoredSchema,
FieldKey,
} from "../../core";
import { ISubscribable } from "../../events";
import { DefaultEditBuilder } from "../defaultChangeFamily";
import { FieldGenerator } from "../contextuallyTyped";
import { EditableField, NewFieldContent, UnwrappedEditableField } from "./editableTreeTypes";
import { makeField, unwrappedField } from "./editableField";
import { ProxyTarget } from "./ProxyTarget";
Expand Down Expand Up @@ -101,6 +104,11 @@ export interface EditableTreeContext extends ISubscribable<ForestEvents> {
* to create new trees starting from the root.
*/
clear(): void;

/**
* FieldSource used to get a FieldGenerator to populate required fields during procedural contextual data generation.
*/
fieldSource?(key: FieldKey, schema: FieldStoredSchema): undefined | FieldGenerator;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions experimental/dds/tree2/src/feature-libraries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export {
cursorForTypedData,
cursorForTypedTreeData,
cursorsForTypedFieldData,
FieldGenerator,
TreeDataContext,
} from "./contextuallyTyped";

export { ForestSummarizer } from "./forestSummarizer";
Expand Down
Loading