Skip to content

Commit

Permalink
Point Value FieldKind to Optional FieldKind implementation (#16130)
Browse files Browse the repository at this point in the history
## Description
This points the Value FieldKind implementation to the Optional FieldKind
implementation so that we only have to maintain one implementation
rather than two.

## Reviewer Guidance

The review process is outlined on [this wiki
page](https://github.com/microsoft/FluidFramework/wiki/PR-Guidelines#guidelines).
  • Loading branch information
justus-camp authored Jun 27, 2023
1 parent ef1d720 commit 3aeeec0
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 269 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,9 @@ export class DefaultEditBuilder implements ChangeFamilyEditor, IDefaultEditBuild
public valueField(field: FieldUpPath): ValueFieldEditBuilder {
return {
set: (newContent: ITreeCursor): void => {
const id = this.modularBuilder.generateId();
const change: FieldChangeset = brand(
valueFieldKind.changeHandler.editor.set(newContent),
valueFieldKind.changeHandler.editor.set(newContent, id),
);
this.modularBuilder.submitChange(field, valueFieldKind.identifier, change);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,20 @@ import { TAnySchema, Type } from "@sinclair/typebox";
import { ICodecFamily, IJsonCodec, makeCodecFamily, makeValueCodec, unitCodec } from "../codec";
import { JsonCompatibleReadOnly, Mutable } from "../util";
import { jsonableTreeFromCursor, singleTextCursor } from "./treeTextCursor";
import type {
NodeUpdate,
OptionalChangeset,
OptionalFieldChange,
ValueChangeset,
} from "./defaultFieldChangeTypes";
import type { NodeUpdate, OptionalChangeset, OptionalFieldChange } from "./defaultFieldChangeTypes";
import type { NodeChangeset } from "./modular-schema";
import {
EncodedValueChangeset,
EncodedOptionalChangeset,
EncodedNodeUpdate,
} from "./defaultFieldChangeFormat";
import { EncodedOptionalChangeset, EncodedNodeUpdate } from "./defaultFieldChangeFormat";

export const noChangeCodecFamily: ICodecFamily<0> = makeCodecFamily([[0, unitCodec]]);

export const counterCodecFamily: ICodecFamily<number> = makeCodecFamily([
[0, makeValueCodec(Type.Number())],
]);

export const makeValueFieldCodecFamily = (childCodec: IJsonCodec<NodeChangeset>) =>
makeCodecFamily([[0, makeValueFieldCodec(childCodec)]]);

export const makeOptionalFieldCodecFamily = (
childCodec: IJsonCodec<NodeChangeset>,
): ICodecFamily<OptionalChangeset> => makeCodecFamily([[0, makeOptionalFieldCodec(childCodec)]]);

function makeValueFieldCodec(
childCodec: IJsonCodec<NodeChangeset>,
): IJsonCodec<ValueChangeset, EncodedValueChangeset<TAnySchema>> {
const nodeUpdateCodec = makeNodeUpdateCodec(childCodec);
return {
encode: (change: ValueChangeset) => {
const encoded: EncodedValueChangeset<TAnySchema> = {};
if (change.value !== undefined) {
encoded.value = nodeUpdateCodec.encode(change.value);
}

if (change.changes !== undefined) {
encoded.changes = childCodec.encode(change.changes);
}

return encoded;
},

decode: (encoded: EncodedValueChangeset<TAnySchema>) => {
const decoded: ValueChangeset = {};
if (encoded.value !== undefined) {
decoded.value = nodeUpdateCodec.decode(encoded.value);
}

if (encoded.changes !== undefined) {
decoded.changes = childCodec.decode(encoded.changes);
}

return decoded;
},
encodedSchema: EncodedValueChangeset(childCodec.encodedSchema ?? Type.Any()),
};
}

function makeOptionalFieldCodec(
childCodec: IJsonCodec<NodeChangeset>,
): IJsonCodec<OptionalChangeset, EncodedOptionalChangeset<TAnySchema>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ class Wrapper<T extends TSchema> {
public encodedNodeUpdate(e: T) {
return EncodedNodeUpdate<T>(e);
}
public encodedValueChangeset(e: T) {
return EncodedValueChangeset<T>(e);
}
public encodedOptionalFieldChange(e: T) {
return EncodedOptionalFieldChange<T>(e);
}
Expand All @@ -49,16 +46,6 @@ export type EncodedNodeUpdate<Schema extends TSchema> = Static<
ReturnType<Wrapper<Schema>["encodedNodeUpdate"]>
>;

export const EncodedValueChangeset = <Schema extends TSchema>(tNodeChange: Schema) =>
Type.Object({
value: Type.Optional(EncodedNodeUpdate(tNodeChange)),
changes: Type.Optional(tNodeChange),
});

export type EncodedValueChangeset<Schema extends TSchema> = Static<
ReturnType<Wrapper<Schema>["encodedValueChangeset"]>
>;

export const EncodedOptionalFieldChange = <Schema extends TSchema>(tNodeChange: Schema) =>
Type.Object({
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ export type NodeUpdate =
changes?: NodeChangeset;
};

export interface ValueChangeset {
value?: NodeUpdate;
changes?: NodeChangeset;
}

export interface OptionalFieldChange {
/**
* Uniquely identifies, in the scope of the changeset, the change made to the field.
Expand Down
230 changes: 63 additions & 167 deletions experimental/dds/tree2/src/feature-libraries/defaultFieldKinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,9 @@ import { populateChildModifications } from "./deltaUtils";
import {
counterCodecFamily,
makeOptionalFieldCodecFamily,
makeValueFieldCodecFamily,
noChangeCodecFamily,
} from "./defaultFieldChangeCodecs";
import {
ValueChangeset,
NodeUpdate,
OptionalChangeset,
OptionalFieldChange,
} from "./defaultFieldChangeTypes";
import { OptionalChangeset, OptionalFieldChange } from "./defaultFieldChangeTypes";

/**
* @alpha
Expand Down Expand Up @@ -219,137 +213,13 @@ export const counter: BrandedFieldKind<
new Set(),
);

const valueRebaser: FieldChangeRebaser<ValueChangeset> = {
compose: (
changes: TaggedChange<ValueChangeset>[],
composeChildren: NodeChangeComposer,
): ValueChangeset => {
if (changes.length === 0) {
return {};
}
let newValue: NodeUpdate | undefined;
const childChanges: TaggedChange<NodeChangeset>[] = [];
for (const { change, revision } of changes) {
if (change.value !== undefined) {
newValue = change.value;

// The previous changes applied to a different value, so we discard them.
// TODO: Consider if we should represent muted changes
childChanges.length = 0;
}

if (change.changes !== undefined) {
childChanges.push(tagChange(change.changes, revision));
}
}

const composed: ValueChangeset = {};
if (newValue !== undefined) {
composed.value = newValue;
}

if (childChanges.length > 0) {
composed.changes = composeChildren(childChanges);
}

return composed;
},

amendCompose: () => fail("Not implemented"),

invert: (
{ revision, change }: TaggedChange<ValueChangeset>,
invertChild: NodeChangeInverter,
reviver: NodeReviver,
): ValueChangeset => {
const inverse: ValueChangeset = {};
if (change.changes !== undefined) {
inverse.changes = invertChild(change.changes, 0);
}
if (change.value !== undefined) {
assert(revision !== undefined, 0x591 /* Unable to revert to undefined revision */);
inverse.value = {
revert: reviver(revision, 0, 1)[0],
// KLUDGE: Value field changes don't have local IDs so we use an arbitrary value.
// TODO: Either delete the value field implementation or give its changes some local IDs.
changeId: { revision, localId: brand(42) },
};
}
return inverse;
},

amendInvert: () => fail("Not implemnted"),

rebase: (
change: ValueChangeset,
over: TaggedChange<ValueChangeset>,
rebaseChild: NodeChangeRebaser,
): ValueChangeset => {
if (change.changes === undefined || over.change.changes === undefined) {
return change;
}
return { ...change, changes: rebaseChild(change.changes, over.change.changes) };
},

amendRebase: (
change: ValueChangeset,
over: TaggedChange<ValueChangeset>,
rebaseChild: NodeChangeRebaser,
) => ({ ...change, changes: rebaseChild(change.changes, over.change.changes) }),
};

export interface ValueFieldEditor extends FieldEditor<ValueChangeset> {
export interface ValueFieldEditor extends FieldEditor<OptionalChangeset> {
/**
* Creates a change which replaces the current value of the field with `newValue`.
*/
set(newValue: ITreeCursor): ValueChangeset;
set(newValue: ITreeCursor, id: ChangesetLocalId): OptionalChangeset;
}

const valueFieldEditor: ValueFieldEditor = {
buildChildChange: (index, change) => {
assert(index === 0, 0x3b6 /* Value fields only support a single child node */);
return { changes: change };
},

set: (newValue: ITreeCursor) => ({ value: { set: jsonableTreeFromCursor(newValue) } }),
};

const valueChangeHandler: FieldChangeHandler<ValueChangeset, ValueFieldEditor> = {
rebaser: valueRebaser,
codecsFactory: makeValueFieldCodecFamily,
editor: valueFieldEditor,

intoDelta: (change: ValueChangeset, deltaFromChild: ToDelta) => {
if (change.value !== undefined) {
const newValue: ITreeCursorSynchronous =
"revert" in change.value ? change.value.revert : singleTextCursor(change.value.set);
const insertDelta = deltaFromInsertAndChange(newValue, change.changes, deltaFromChild);
return [{ type: Delta.MarkType.Delete, count: 1 }, ...insertDelta];
}

return change.changes === undefined ? [] : [deltaFromChild(change.changes)];
},

isEmpty: (change: ValueChangeset) => change.changes === undefined && change.value === undefined,
};

/**
* Exactly one item.
*/
export const value: BrandedFieldKind<"Value", Multiplicity.Value, ValueFieldEditor> =
brandedFieldKind(
"Value",
Multiplicity.Value,
valueChangeHandler,
(types, other) =>
(other.kind.identifier === sequence.identifier ||
other.kind.identifier === value.identifier ||
other.kind.identifier === optional.identifier ||
other.kind.identifier === nodeKey.identifier) &&
allowsTreeSchemaIdentifierSuperset(types, other.types),
new Set(),
);

const optionalChangeRebaser: FieldChangeRebaser<OptionalChangeset> = {
compose: (
changes: TaggedChange<OptionalChangeset>[],
Expand Down Expand Up @@ -658,6 +528,35 @@ function deltaForDelete(
return [deleteDelta];
}

function optionalFieldIntoDelta(change: OptionalChangeset, deltaFromChild: ToDelta) {
if (change.fieldChange === undefined) {
if (change.deletedBy === undefined && change.childChange !== undefined) {
return [deltaFromChild(change.childChange)];
}
return [];
}

const deleteDelta = deltaForDelete(
!change.fieldChange.wasEmpty,
change.deletedBy === undefined ? change.childChange : undefined,
deltaFromChild,
);

const update = change.fieldChange?.newContent;
let content: ITreeCursorSynchronous | undefined;
if (update === undefined) {
content = undefined;
} else if ("set" in update) {
content = singleTextCursor(update.set);
} else {
content = update.revert;
}

const insertDelta = deltaFromInsertAndChange(content, update?.changes, deltaFromChild);

return [...deleteDelta, ...insertDelta];
}

/**
* 0 or 1 items.
*/
Expand All @@ -670,47 +569,44 @@ export const optional: BrandedFieldKind<"Optional", Multiplicity.Optional, Optio
codecsFactory: makeOptionalFieldCodecFamily,
editor: optionalFieldEditor,

intoDelta: (change: OptionalChangeset, deltaFromChild: ToDelta) => {
if (change.fieldChange === undefined) {
if (change.deletedBy === undefined && change.childChange !== undefined) {
return [deltaFromChild(change.childChange)];
}
return [];
}

const deleteDelta = deltaForDelete(
!change.fieldChange.wasEmpty,
change.deletedBy === undefined ? change.childChange : undefined,
deltaFromChild,
);

const update = change.fieldChange?.newContent;
let content: ITreeCursorSynchronous | undefined;
if (update === undefined) {
content = undefined;
} else if ("set" in update) {
content = singleTextCursor(update.set);
} else {
content = update.revert;
}

const insertDelta = deltaFromInsertAndChange(
content,
update?.changes,
deltaFromChild,
);

return [...deleteDelta, ...insertDelta];
},

intoDelta: (change: OptionalChangeset, deltaFromChild: ToDelta) =>
optionalFieldIntoDelta(change, deltaFromChild),
isEmpty: (change: OptionalChangeset) =>
change.childChange === undefined && change.fieldChange === undefined,
},
(types, other) =>
(other.kind.identifier === sequence.identifier ||
other.kind.identifier === optional.identifier) &&
allowsTreeSchemaIdentifierSuperset(types, other.types),
new Set([value.identifier]),
new Set([]),
);

const valueFieldEditor: ValueFieldEditor = {
...optionalFieldEditor,
set: (newContent: ITreeCursor, id: ChangesetLocalId): OptionalChangeset =>
optionalFieldEditor.set(newContent, false, id),
};

const valueChangeHandler: FieldChangeHandler<OptionalChangeset, ValueFieldEditor> = {
...optional.changeHandler,
editor: valueFieldEditor,
};

/**
* Exactly one item.
*/
export const value: BrandedFieldKind<"Value", Multiplicity.Value, ValueFieldEditor> =
brandedFieldKind(
"Value",
Multiplicity.Value,
valueChangeHandler,
(types, other) =>
(other.kind.identifier === sequence.identifier ||
other.kind.identifier === value.identifier ||
other.kind.identifier === optional.identifier ||
other.kind.identifier === nodeKey.identifier) &&
allowsTreeSchemaIdentifierSuperset(types, other.types),
new Set(),
);

/**
Expand Down
Loading

0 comments on commit 3aeeec0

Please sign in to comment.