Skip to content

Commit

Permalink
Implement moves between unhydrated array nodes (microsoft#22228)
Browse files Browse the repository at this point in the history
## Description

This updates the ArrayNode tests to run all move tests suites against
both hydrated and unhydrated nodes.
  • Loading branch information
noencke authored Aug 16, 2024
1 parent 4d3bc87 commit 74b4277
Show file tree
Hide file tree
Showing 6 changed files with 554 additions and 507 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

export {
type MapTreeNode,
type MapTreeSequenceField,
isMapTreeNode,
isMapTreeSequenceField,
getOrCreateMapTreeNode,
tryGetMapTreeNode,
} from "./mapTreeNode.js";
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,35 @@ export function isMapTreeNode(flexNode: FlexTreeNode): flexNode is MapTreeNode {
return flexNode instanceof EagerMapTreeNode;
}

/**
* Checks if the given {@link FlexTreeField} is a {@link MapTreeSequenceField}.
*/
export function isMapTreeSequenceField<T extends FlexAllowedTypes>(
field: FlexTreeSequenceField<T> | FlexTreeField,
): field is MapTreeSequenceField<T> {
return field instanceof EagerMapTreeSequenceField;
}

/**
* An unhydrated {@link FlexTreeSequenceField}, which has additional editing capabilities.
* @remarks When doing a removal edit, a {@link MapTreeSequenceField}'s `editor` returns ownership of the removed {@link ExclusiveMapTree}s to the caller.
*/
export interface MapTreeSequenceField<T extends FlexAllowedTypes>
extends FlexTreeSequenceField<T> {
readonly editor: MapTreeSequenceFieldEditBuilder;
}

interface MapTreeSequenceFieldEditBuilder
extends SequenceFieldEditBuilder<ExclusiveMapTree[]> {
/**
* Issues a change which removes `count` elements starting at the given `index`.
* @param index - The index of the first removed element.
* @param count - The number of elements to remove.
* @returns the MapTrees that were removed
*/
remove(index: number, count: number): ExclusiveMapTree[];
}

/** A node's parent field and its index in that field */
interface LocationInField {
readonly parent: MapTreeField;
Expand Down Expand Up @@ -440,7 +469,7 @@ class EagerMapTreeOptionalField<T extends FlexAllowedTypes>
implements FlexTreeOptionalField<T>
{
public readonly editor = {
set: (newContent: ExclusiveMapTree | undefined) => {
set: (newContent: ExclusiveMapTree | undefined): void => {
// If the new content is a MapTreeNode, it needs to have its parent pointer updated
if (newContent !== undefined) {
nodeCache.get(newContent)?.adoptBy(this, 0);
Expand Down Expand Up @@ -487,8 +516,8 @@ class EagerMapTreeSequenceField<T extends FlexAllowedTypes>
extends EagerMapTreeField<T>
implements FlexTreeSequenceField<T>
{
public readonly editor: SequenceFieldEditBuilder<ExclusiveMapTree[]> = {
insert: (index, newContent) => {
public readonly editor: MapTreeSequenceFieldEditBuilder = {
insert: (index, newContent): void => {
for (let i = 0; i < newContent.length; i++) {
const c = newContent[i];
assert(c !== undefined, "Unexpected sparse array content");
Expand All @@ -504,15 +533,17 @@ class EagerMapTreeSequenceField<T extends FlexAllowedTypes>
}
});
},
remove: (index, count) => {
remove: (index, count): ExclusiveMapTree[] => {
for (let i = index; i < index + count; i++) {
const c = this.mapTrees[i];
assert(c !== undefined, "Unexpected sparse array");
nodeCache.get(c)?.adoptBy(undefined);
}
let removed: ExclusiveMapTree[] | undefined;
this.edit((mapTrees) => {
mapTrees.splice(index, count);
removed = mapTrees.splice(index, count);
});
return removed ?? fail("Expected removed to be set by edit");
},
};

Expand Down
2 changes: 2 additions & 0 deletions packages/dds/tree/src/feature-libraries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,9 @@ export { makeMitigatedChangeFamily } from "./mitigatedChangeFamily.js";

export {
type MapTreeNode,
type MapTreeSequenceField,
isMapTreeNode,
isMapTreeSequenceField,
getOrCreateMapTreeNode,
tryGetMapTreeNode,
} from "./flex-map-tree/index.js";
59 changes: 38 additions & 21 deletions packages/dds/tree/src/simple-tree/arrayNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License.
*/

import { oob } from "@fluidframework/core-utils/internal";
import { EmptyKey, type ExclusiveMapTree } from "../core/index.js";
import {
type FlexAllowedTypes,
Expand All @@ -13,6 +14,7 @@ import {
getOrCreateMapTreeNode,
getSchemaAndPolicy,
isFlexTreeNode,
isMapTreeSequenceField,
} from "../feature-libraries/index.js";
import {
type InsertableContent,
Expand Down Expand Up @@ -833,40 +835,55 @@ abstract class CustomArrayNodeBase<const T extends ImplicitAllowedTypes>
source?: TreeArrayNode,
): void {
const destinationField = getSequenceField(this);
if (destinationField.context === undefined) {
throw new UsageError(
`Cannot move elements into an array before the array is inserted into the tree`,
);
}
const sourceField = source !== undefined ? getSequenceField(source) : destinationField;

validateIndex(destinationIndex, destinationField, "moveRangeToIndex", true);
validateIndexRange(sourceStart, sourceEnd, source ?? destinationField, "moveRangeToIndex");
const sourceField = source !== undefined ? getSequenceField(source) : destinationField;
if (sourceField.context === undefined) {
throw new UsageError(
`Cannot move elements from an array before the array is inserted into the tree`,
);
}

// TODO: determine support for move across different sequence types
if (destinationField.schema.types !== undefined && sourceField !== destinationField) {
for (let i = sourceStart; i < sourceEnd; i++) {
const sourceNode = sourceField.boxedAt(i) ?? fail("impossible out of bounds index");
const sourceNode = sourceField.boxedAt(i) ?? oob();
if (!destinationField.schema.types.has(sourceNode.schema.name)) {
throw new UsageError("Type in source sequence is not allowed in destination.");
}
}
}

const movedCount = sourceEnd - sourceStart;
const sourceFieldPath = sourceField.getFieldPath();
if (destinationField.context === undefined) {
if (!isMapTreeSequenceField(sourceField)) {
throw new UsageError(
"Cannot move elements from an inserted array to an array that has not yet been inserted",
);
}

const destinationFieldPath = destinationField.getFieldPath();
destinationField.context.checkout.editor.move(
sourceFieldPath,
sourceStart,
movedCount,
destinationFieldPath,
destinationIndex,
);
if (sourceField !== destinationField || destinationIndex < sourceStart) {
destinationField.editor.insert(
destinationIndex,
sourceField.editor.remove(sourceStart, movedCount),
);
} else if (destinationIndex > sourceStart + movedCount) {
destinationField.editor.insert(
destinationIndex - movedCount,
sourceField.editor.remove(sourceStart, movedCount),
);
}
} else {
if (sourceField.context === undefined) {
throw new UsageError(
"Cannot move elements from an array that has not yet been inserted to an inserted array",
);
}

destinationField.context.checkout.editor.move(
sourceField.getFieldPath(),
sourceStart,
movedCount,
destinationField.getFieldPath(),
destinationIndex,
);
}
}

public values(): IterableIterator<TreeNodeFromImplicitAllowedTypes<T>> {
Expand Down
Loading

0 comments on commit 74b4277

Please sign in to comment.