Add Clone to SharedTree Revertible (#23044)
#### Description [13864](https://dev.azure.com/fluidframework/internal/_workitems/edit/13864/) This PR adds forkable revertible feature to the `Revertible` object of SharedTree. - Removed `DisposableRevertible` and replaced by `RevertibleAlpha`. - Added `clone()` method to the new interface. - Uses `TreeBranch` (which is subset of `TreeCheckout`) to access data necessary for revert operation. --------- Co-authored-by: Noah Encke <78610362+noencke@users.noreply.github.com> Co-authored-by: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> Co-authored-by: Jenn <jennle@microsoft.com>
This commit is contained in:
Родитель
d3bf90ca08
Коммит
5abfa015af
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
"fluid-framework": minor
|
||||||
|
"@fluidframework/tree": minor
|
||||||
|
---
|
||||||
|
---
|
||||||
|
"section": tree
|
||||||
|
---
|
||||||
|
|
||||||
|
Enables Revertible objects to be cloned using `RevertibleAlpha.clone()`.
|
||||||
|
|
||||||
|
Replaced `DisposableRevertible` with `RevertibleAlpha`. The new `RevertibleAlpha` interface extends `Revertible` and includes a `clone(branch: TreeBranch)` method to facilitate cloning a Revertible to a specified target branch. The source branch where the `RevertibleAlpha` was created must share revision logs with the target branch where the `RevertibleAlpha` is being cloned. If this condition is not met, the operation will throw an error.
|
|
@ -539,6 +539,14 @@ export interface Revertible {
|
||||||
readonly status: RevertibleStatus;
|
readonly status: RevertibleStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @alpha @sealed
|
||||||
|
export interface RevertibleAlpha extends Revertible {
|
||||||
|
clone: (branch: TreeBranch) => RevertibleAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @alpha @sealed
|
||||||
|
export type RevertibleAlphaFactory = (onRevertibleDisposed?: (revertible: RevertibleAlpha) => void) => RevertibleAlpha;
|
||||||
|
|
||||||
// @public @sealed
|
// @public @sealed
|
||||||
export type RevertibleFactory = (onRevertibleDisposed?: (revertible: Revertible) => void) => Revertible;
|
export type RevertibleFactory = (onRevertibleDisposed?: (revertible: Revertible) => void) => Revertible;
|
||||||
|
|
||||||
|
@ -723,8 +731,8 @@ export interface TreeBranch extends IDisposable {
|
||||||
|
|
||||||
// @alpha @sealed
|
// @alpha @sealed
|
||||||
export interface TreeBranchEvents {
|
export interface TreeBranchEvents {
|
||||||
changed(data: CommitMetadata, getRevertible?: RevertibleFactory): void;
|
changed(data: CommitMetadata, getRevertible?: RevertibleAlphaFactory): void;
|
||||||
commitApplied(data: CommitMetadata, getRevertible?: RevertibleFactory): void;
|
commitApplied(data: CommitMetadata, getRevertible?: RevertibleAlphaFactory): void;
|
||||||
schemaChanged(): void;
|
schemaChanged(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -206,4 +206,10 @@ export {
|
||||||
AllowedUpdateType,
|
AllowedUpdateType,
|
||||||
} from "./schema-view/index.js";
|
} from "./schema-view/index.js";
|
||||||
|
|
||||||
export { type Revertible, RevertibleStatus, type RevertibleFactory } from "./revertible.js";
|
export {
|
||||||
|
type Revertible,
|
||||||
|
RevertibleStatus,
|
||||||
|
type RevertibleFactory,
|
||||||
|
type RevertibleAlphaFactory,
|
||||||
|
type RevertibleAlpha,
|
||||||
|
} from "./revertible.js";
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
* Licensed under the MIT License.
|
* Licensed under the MIT License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { TreeBranch } from "../simple-tree/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows reversion of a change made to SharedTree.
|
* Allows reversion of a change made to SharedTree.
|
||||||
*
|
*
|
||||||
|
@ -36,6 +38,23 @@ export interface Revertible {
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Revertible} with features that are not yet stable.
|
||||||
|
*
|
||||||
|
* @sealed @alpha
|
||||||
|
*/
|
||||||
|
export interface RevertibleAlpha extends Revertible {
|
||||||
|
/**
|
||||||
|
* Clones the {@link Revertible} to a target branch.
|
||||||
|
*
|
||||||
|
* @param branch - A target branch to apply the revertible to.
|
||||||
|
* The target branch must contain the same commit that this revertible is meant to revert, otherwise will throw an error.
|
||||||
|
* @returns A cloned revertible is independent of the original, meaning disposing of one will not affect the other,
|
||||||
|
* provided they do not belong to the same branch. Both revertibles can be reverted independently.
|
||||||
|
*/
|
||||||
|
clone: (branch: TreeBranch) => RevertibleAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The status of a {@link Revertible}.
|
* The status of a {@link Revertible}.
|
||||||
*
|
*
|
||||||
|
@ -50,7 +69,7 @@ export enum RevertibleStatus {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory for creating a {@link Revertible}.
|
* Factory for creating a {@link Revertible}.
|
||||||
* Will error if invoked outside the scope of the `changed` event that provides it, or if invoked multiple times.
|
* @throws error if invoked outside the scope of the `changed` event that provides it, or if invoked multiple times.
|
||||||
*
|
*
|
||||||
* @param onRevertibleDisposed - A callback that will be invoked when the `Revertible` generated by this factory is disposed.
|
* @param onRevertibleDisposed - A callback that will be invoked when the `Revertible` generated by this factory is disposed.
|
||||||
* This happens when the `Revertible` is disposed manually, or when the `TreeView` that the `Revertible` belongs to is disposed,
|
* This happens when the `Revertible` is disposed manually, or when the `TreeView` that the `Revertible` belongs to is disposed,
|
||||||
|
@ -62,3 +81,18 @@ export enum RevertibleStatus {
|
||||||
export type RevertibleFactory = (
|
export type RevertibleFactory = (
|
||||||
onRevertibleDisposed?: (revertible: Revertible) => void,
|
onRevertibleDisposed?: (revertible: Revertible) => void,
|
||||||
) => Revertible;
|
) => Revertible;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for creating a {@link RevertibleAlpha}.
|
||||||
|
* @throws error if invoked outside the scope of the `changed` event that provides it, or if invoked multiple times.
|
||||||
|
*
|
||||||
|
* @param onRevertibleDisposed - A callback that will be invoked when the {@link RevertibleAlpha | Revertible} generated by this factory is disposed.
|
||||||
|
* This happens when the `Revertible` is disposed manually, or when the `TreeView` that the `Revertible` belongs to is disposed,
|
||||||
|
* whichever happens first.
|
||||||
|
* This is typically used to clean up any resources associated with the `Revertible` in the host application.
|
||||||
|
*
|
||||||
|
* @sealed @alpha
|
||||||
|
*/
|
||||||
|
export type RevertibleAlphaFactory = (
|
||||||
|
onRevertibleDisposed?: (revertible: RevertibleAlpha) => void,
|
||||||
|
) => RevertibleAlpha;
|
||||||
|
|
|
@ -29,6 +29,8 @@ export {
|
||||||
MapNodeStoredSchema,
|
MapNodeStoredSchema,
|
||||||
LeafNodeStoredSchema,
|
LeafNodeStoredSchema,
|
||||||
type RevertibleFactory,
|
type RevertibleFactory,
|
||||||
|
type RevertibleAlphaFactory,
|
||||||
|
type RevertibleAlpha,
|
||||||
} from "./core/index.js";
|
} from "./core/index.js";
|
||||||
export { type Brand } from "./util/index.js";
|
export { type Brand } from "./util/index.js";
|
||||||
|
|
||||||
|
|
|
@ -409,7 +409,7 @@ export class SchematizingSimpleTreeView<
|
||||||
* @remarks Currently, all contexts are also {@link SchematizingSimpleTreeView}s.
|
* @remarks Currently, all contexts are also {@link SchematizingSimpleTreeView}s.
|
||||||
* Other checkout implementations (e.g. not associated with a view) may be supported in the future.
|
* Other checkout implementations (e.g. not associated with a view) may be supported in the future.
|
||||||
*/
|
*/
|
||||||
function getCheckout(context: TreeBranch): TreeCheckout {
|
export function getCheckout(context: TreeBranch): TreeCheckout {
|
||||||
if (context instanceof SchematizingSimpleTreeView) {
|
if (context instanceof SchematizingSimpleTreeView) {
|
||||||
return context.checkout;
|
return context.checkout;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import {
|
||||||
type IEditableForest,
|
type IEditableForest,
|
||||||
type IForestSubscription,
|
type IForestSubscription,
|
||||||
type JsonableTree,
|
type JsonableTree,
|
||||||
type Revertible,
|
|
||||||
RevertibleStatus,
|
RevertibleStatus,
|
||||||
type RevisionTag,
|
type RevisionTag,
|
||||||
type RevisionTagCodec,
|
type RevisionTagCodec,
|
||||||
|
@ -37,7 +36,8 @@ import {
|
||||||
rootFieldKey,
|
rootFieldKey,
|
||||||
tagChange,
|
tagChange,
|
||||||
visitDelta,
|
visitDelta,
|
||||||
type RevertibleFactory,
|
type RevertibleAlphaFactory,
|
||||||
|
type RevertibleAlpha,
|
||||||
} from "../core/index.js";
|
} from "../core/index.js";
|
||||||
import {
|
import {
|
||||||
type HasListeners,
|
type HasListeners,
|
||||||
|
@ -80,8 +80,9 @@ import type {
|
||||||
TreeViewConfiguration,
|
TreeViewConfiguration,
|
||||||
UnsafeUnknownSchema,
|
UnsafeUnknownSchema,
|
||||||
ViewableTree,
|
ViewableTree,
|
||||||
|
TreeBranch,
|
||||||
} from "../simple-tree/index.js";
|
} from "../simple-tree/index.js";
|
||||||
import { SchematizingSimpleTreeView } from "./schematizingTreeView.js";
|
import { getCheckout, SchematizingSimpleTreeView } from "./schematizingTreeView.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events for {@link ITreeCheckout}.
|
* Events for {@link ITreeCheckout}.
|
||||||
|
@ -104,7 +105,7 @@ export interface CheckoutEvents {
|
||||||
* @param getRevertible - a function provided that allows users to get a revertible for the change. If not provided,
|
* @param getRevertible - a function provided that allows users to get a revertible for the change. If not provided,
|
||||||
* this change is not revertible.
|
* this change is not revertible.
|
||||||
*/
|
*/
|
||||||
changed(data: CommitMetadata, getRevertible?: RevertibleFactory): void;
|
changed(data: CommitMetadata, getRevertible?: RevertibleAlphaFactory): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -409,7 +410,7 @@ export class TreeCheckout implements ITreeCheckoutFork {
|
||||||
/**
|
/**
|
||||||
* Set of revertibles maintained for automatic disposal
|
* Set of revertibles maintained for automatic disposal
|
||||||
*/
|
*/
|
||||||
private readonly revertibles = new Set<DisposableRevertible>();
|
private readonly revertibles = new Set<RevertibleAlpha>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Each branch's head commit corresponds to a revertible commit.
|
* Each branch's head commit corresponds to a revertible commit.
|
||||||
|
@ -542,7 +543,7 @@ export class TreeCheckout implements ITreeCheckoutFork {
|
||||||
|
|
||||||
const getRevertible = hasSchemaChange(change)
|
const getRevertible = hasSchemaChange(change)
|
||||||
? undefined
|
? undefined
|
||||||
: (onRevertibleDisposed?: (revertible: Revertible) => void) => {
|
: (onRevertibleDisposed?: (revertible: RevertibleAlpha) => void) => {
|
||||||
if (!withinEventContext) {
|
if (!withinEventContext) {
|
||||||
throw new UsageError(
|
throw new UsageError(
|
||||||
"Cannot get a revertible outside of the context of a changed event.",
|
"Cannot get a revertible outside of the context of a changed event.",
|
||||||
|
@ -553,42 +554,12 @@ export class TreeCheckout implements ITreeCheckoutFork {
|
||||||
"Cannot generate the same revertible more than once. Note that this can happen when multiple changed event listeners are registered.",
|
"Cannot generate the same revertible more than once. Note that this can happen when multiple changed event listeners are registered.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const revertibleCommits = this.revertibleCommitBranches;
|
const revertible = this.createRevertible(
|
||||||
const revertible: DisposableRevertible = {
|
revision,
|
||||||
get status(): RevertibleStatus {
|
kind,
|
||||||
const revertibleCommit = revertibleCommits.get(revision);
|
this,
|
||||||
return revertibleCommit === undefined
|
onRevertibleDisposed,
|
||||||
? RevertibleStatus.Disposed
|
);
|
||||||
: RevertibleStatus.Valid;
|
|
||||||
},
|
|
||||||
revert: (release: boolean = true) => {
|
|
||||||
if (revertible.status === RevertibleStatus.Disposed) {
|
|
||||||
throw new UsageError(
|
|
||||||
"Unable to revert a revertible that has been disposed.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const revertMetrics = this.revertRevertible(revision, kind);
|
|
||||||
this.logger?.sendTelemetryEvent({
|
|
||||||
eventName: TreeCheckout.revertTelemetryEventName,
|
|
||||||
...revertMetrics,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (release) {
|
|
||||||
revertible.dispose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dispose: () => {
|
|
||||||
if (revertible.status === RevertibleStatus.Disposed) {
|
|
||||||
throw new UsageError(
|
|
||||||
"Unable to dispose a revertible that has already been disposed.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.disposeRevertible(revertible, revision);
|
|
||||||
onRevertibleDisposed?.(revertible);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.revertibleCommitBranches.set(revision, _branch.fork(commit));
|
this.revertibleCommitBranches.set(revision, _branch.fork(commit));
|
||||||
this.revertibles.add(revertible);
|
this.revertibles.add(revertible);
|
||||||
return revertible;
|
return revertible;
|
||||||
|
@ -643,6 +614,76 @@ export class TreeCheckout implements ITreeCheckoutFork {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link RevertibleAlpha} object that can undo a specific change in the tree's history.
|
||||||
|
* Revision must exist in the given {@link TreeCheckout}'s branch.
|
||||||
|
*
|
||||||
|
* @param revision - The revision tag identifying the change to be made revertible.
|
||||||
|
* @param kind - The {@link CommitKind} that produced this revertible (e.g., Default, Undo, Redo).
|
||||||
|
* @param checkout - The {@link TreeCheckout} instance this revertible belongs to.
|
||||||
|
* @param onRevertibleDisposed - Callback function that will be called when the revertible is disposed.
|
||||||
|
* @returns - {@link RevertibleAlpha}
|
||||||
|
*/
|
||||||
|
private createRevertible(
|
||||||
|
revision: RevisionTag,
|
||||||
|
kind: CommitKind,
|
||||||
|
checkout: TreeCheckout,
|
||||||
|
onRevertibleDisposed: ((revertible: RevertibleAlpha) => void) | undefined,
|
||||||
|
): RevertibleAlpha {
|
||||||
|
const commitBranches = checkout.revertibleCommitBranches;
|
||||||
|
|
||||||
|
const revertible: RevertibleAlpha = {
|
||||||
|
get status(): RevertibleStatus {
|
||||||
|
const revertibleCommit = commitBranches.get(revision);
|
||||||
|
return revertibleCommit === undefined
|
||||||
|
? RevertibleStatus.Disposed
|
||||||
|
: RevertibleStatus.Valid;
|
||||||
|
},
|
||||||
|
revert: (release: boolean = true) => {
|
||||||
|
if (revertible.status === RevertibleStatus.Disposed) {
|
||||||
|
throw new UsageError("Unable to revert a revertible that has been disposed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const revertMetrics = checkout.revertRevertible(revision, kind);
|
||||||
|
checkout.logger?.sendTelemetryEvent({
|
||||||
|
eventName: TreeCheckout.revertTelemetryEventName,
|
||||||
|
...revertMetrics,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (release) {
|
||||||
|
revertible.dispose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clone: (forkedBranch: TreeBranch) => {
|
||||||
|
if (forkedBranch === undefined) {
|
||||||
|
return this.createRevertible(revision, kind, checkout, onRevertibleDisposed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:#23442: When a revertible is cloned for a forked branch, optimize to create a fork of a revertible branch once per revision NOT once per revision per checkout.
|
||||||
|
const forkedCheckout = getCheckout(forkedBranch);
|
||||||
|
const revertibleBranch = this.revertibleCommitBranches.get(revision);
|
||||||
|
assert(
|
||||||
|
revertibleBranch !== undefined,
|
||||||
|
"change to revert does not exist on the given forked branch",
|
||||||
|
);
|
||||||
|
forkedCheckout.revertibleCommitBranches.set(revision, revertibleBranch.fork());
|
||||||
|
|
||||||
|
return this.createRevertible(revision, kind, forkedCheckout, onRevertibleDisposed);
|
||||||
|
},
|
||||||
|
dispose: () => {
|
||||||
|
if (revertible.status === RevertibleStatus.Disposed) {
|
||||||
|
throw new UsageError(
|
||||||
|
"Unable to dispose a revertible that has already been disposed.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
checkout.disposeRevertible(revertible, revision);
|
||||||
|
onRevertibleDisposed?.(revertible);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return revertible;
|
||||||
|
}
|
||||||
|
|
||||||
// For the new TreeViewAlpha API
|
// For the new TreeViewAlpha API
|
||||||
public viewWith<TRoot extends ImplicitFieldSchema | UnsafeUnknownSchema>(
|
public viewWith<TRoot extends ImplicitFieldSchema | UnsafeUnknownSchema>(
|
||||||
config: TreeViewConfiguration<ReadSchema<TRoot>>,
|
config: TreeViewConfiguration<ReadSchema<TRoot>>,
|
||||||
|
@ -809,7 +850,7 @@ export class TreeCheckout implements ITreeCheckoutFork {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private disposeRevertible(revertible: DisposableRevertible, revision: RevisionTag): void {
|
private disposeRevertible(revertible: RevertibleAlpha, revision: RevisionTag): void {
|
||||||
this.revertibleCommitBranches.get(revision)?.dispose();
|
this.revertibleCommitBranches.get(revision)?.dispose();
|
||||||
this.revertibleCommitBranches.delete(revision);
|
this.revertibleCommitBranches.delete(revision);
|
||||||
this.revertibles.delete(revertible);
|
this.revertibles.delete(revertible);
|
||||||
|
@ -922,7 +963,3 @@ export function runSynchronous(
|
||||||
? view.transaction.abort()
|
? view.transaction.abort()
|
||||||
: view.transaction.commit();
|
: view.transaction.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DisposableRevertible extends Revertible {
|
|
||||||
dispose: () => void;
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,7 +6,11 @@
|
||||||
import type { IFluidLoadable, IDisposable } from "@fluidframework/core-interfaces";
|
import type { IFluidLoadable, IDisposable } from "@fluidframework/core-interfaces";
|
||||||
import { UsageError } from "@fluidframework/telemetry-utils/internal";
|
import { UsageError } from "@fluidframework/telemetry-utils/internal";
|
||||||
|
|
||||||
import type { CommitMetadata, RevertibleFactory } from "../../core/index.js";
|
import type {
|
||||||
|
CommitMetadata,
|
||||||
|
RevertibleAlphaFactory,
|
||||||
|
RevertibleFactory,
|
||||||
|
} from "../../core/index.js";
|
||||||
import type { Listenable } from "../../events/index.js";
|
import type { Listenable } from "../../events/index.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -629,7 +633,7 @@ export interface TreeBranchEvents {
|
||||||
* @param getRevertible - a function that allows users to get a revertible for the change. If not provided,
|
* @param getRevertible - a function that allows users to get a revertible for the change. If not provided,
|
||||||
* this change is not revertible.
|
* this change is not revertible.
|
||||||
*/
|
*/
|
||||||
changed(data: CommitMetadata, getRevertible?: RevertibleFactory): void;
|
changed(data: CommitMetadata, getRevertible?: RevertibleAlphaFactory): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired when:
|
* Fired when:
|
||||||
|
@ -644,7 +648,7 @@ export interface TreeBranchEvents {
|
||||||
* @param getRevertible - a function provided that allows users to get a revertible for the commit that was applied. If not provided,
|
* @param getRevertible - a function provided that allows users to get a revertible for the commit that was applied. If not provided,
|
||||||
* this commit is not revertible.
|
* this commit is not revertible.
|
||||||
*/
|
*/
|
||||||
commitApplied(data: CommitMetadata, getRevertible?: RevertibleFactory): void;
|
commitApplied(data: CommitMetadata, getRevertible?: RevertibleAlphaFactory): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,13 +6,19 @@
|
||||||
import {
|
import {
|
||||||
type FieldUpPath,
|
type FieldUpPath,
|
||||||
type Revertible,
|
type Revertible,
|
||||||
|
RevertibleStatus,
|
||||||
type UpPath,
|
type UpPath,
|
||||||
rootFieldKey,
|
rootFieldKey,
|
||||||
} from "../../core/index.js";
|
} from "../../core/index.js";
|
||||||
import { singleJsonCursor } from "../json/index.js";
|
import { singleJsonCursor } from "../json/index.js";
|
||||||
import { SharedTreeFactory, type ITreeCheckout } from "../../shared-tree/index.js";
|
import { SharedTreeFactory, type ITreeCheckout } from "../../shared-tree/index.js";
|
||||||
import { type JsonCompatible, brand } from "../../util/index.js";
|
import { type JsonCompatible, brand } from "../../util/index.js";
|
||||||
import { createTestUndoRedoStacks, expectJsonTree, moveWithin } from "../utils.js";
|
import {
|
||||||
|
createTestUndoRedoStacks,
|
||||||
|
expectJsonTree,
|
||||||
|
moveWithin,
|
||||||
|
TestTreeProviderLite,
|
||||||
|
} from "../utils.js";
|
||||||
import { insert, jsonSequenceRootSchema, remove } from "../sequenceRootUtils.js";
|
import { insert, jsonSequenceRootSchema, remove } from "../sequenceRootUtils.js";
|
||||||
import { createIdCompressor } from "@fluidframework/id-compressor/internal";
|
import { createIdCompressor } from "@fluidframework/id-compressor/internal";
|
||||||
import {
|
import {
|
||||||
|
@ -21,7 +27,11 @@ import {
|
||||||
MockStorage,
|
MockStorage,
|
||||||
} from "@fluidframework/test-runtime-utils/internal";
|
} from "@fluidframework/test-runtime-utils/internal";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { SchemaFactory, TreeViewConfiguration } from "../../simple-tree/index.js";
|
import {
|
||||||
|
asTreeViewAlpha,
|
||||||
|
SchemaFactory,
|
||||||
|
TreeViewConfiguration,
|
||||||
|
} from "../../simple-tree/index.js";
|
||||||
// eslint-disable-next-line import/no-internal-modules
|
// eslint-disable-next-line import/no-internal-modules
|
||||||
import { initialize } from "../../shared-tree/schematizeTree.js";
|
import { initialize } from "../../shared-tree/schematizeTree.js";
|
||||||
|
|
||||||
|
@ -158,6 +168,44 @@ const testCases: {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema definitions for forkable revertible test suites.
|
||||||
|
* TODO: Should be removed once #24414 is implemented.
|
||||||
|
*/
|
||||||
|
function createInitializedView() {
|
||||||
|
const factory = new SchemaFactory("shared-tree-test");
|
||||||
|
class ChildNodeSchema extends factory.object("child-item", {
|
||||||
|
propertyOne: factory.optional(factory.number),
|
||||||
|
propertyTwo: factory.object("propertyTwo-item", {
|
||||||
|
itemOne: factory.string,
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
|
class RootNodeSchema extends factory.object("root-item", {
|
||||||
|
child: factory.optional(ChildNodeSchema),
|
||||||
|
}) {}
|
||||||
|
const provider = new TestTreeProviderLite();
|
||||||
|
const view = asTreeViewAlpha(
|
||||||
|
provider.trees[0].viewWith(
|
||||||
|
new TreeViewConfiguration({
|
||||||
|
schema: RootNodeSchema,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
view.initialize(
|
||||||
|
new RootNodeSchema({
|
||||||
|
child: {
|
||||||
|
propertyOne: 128,
|
||||||
|
propertyTwo: {
|
||||||
|
itemOne: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
describe("Undo and redo", () => {
|
describe("Undo and redo", () => {
|
||||||
for (const attached of [true, false]) {
|
for (const attached of [true, false]) {
|
||||||
const attachStr = attached ? "attached" : "detached";
|
const attachStr = attached ? "attached" : "detached";
|
||||||
|
@ -453,6 +501,178 @@ describe("Undo and redo", () => {
|
||||||
revertible.revert();
|
revertible.revert();
|
||||||
assert.equal(view.root.foo, 1);
|
assert.equal(view.root.foo, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO:#24414: Enable forkable revertibles tests to run on attached/detached mode.
|
||||||
|
it("reverts original & forked revertibles after making change to the original view", () => {
|
||||||
|
const originalView = createInitializedView();
|
||||||
|
const { undoStack } = createTestUndoRedoStacks(originalView.events);
|
||||||
|
|
||||||
|
assert(originalView.root.child !== undefined);
|
||||||
|
originalView.root.child.propertyOne = 256; // 128 -> 256
|
||||||
|
|
||||||
|
const forkedView = originalView.fork();
|
||||||
|
|
||||||
|
const propertyOneUndo = undoStack.pop();
|
||||||
|
const clonedPropertyOneUndo = propertyOneUndo?.clone(forkedView);
|
||||||
|
|
||||||
|
propertyOneUndo?.revert();
|
||||||
|
|
||||||
|
assert.equal(originalView.root.child?.propertyOne, 128);
|
||||||
|
assert.equal(forkedView.root.child?.propertyOne, 256);
|
||||||
|
assert.equal(propertyOneUndo?.status, RevertibleStatus.Disposed);
|
||||||
|
assert.equal(clonedPropertyOneUndo?.status, RevertibleStatus.Valid);
|
||||||
|
|
||||||
|
clonedPropertyOneUndo?.revert();
|
||||||
|
|
||||||
|
assert.equal(forkedView.root.child?.propertyOne, 128);
|
||||||
|
assert.equal(clonedPropertyOneUndo?.status, RevertibleStatus.Disposed);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO:#24414: Enable forkable revertibles tests to run on attached/detached mode.
|
||||||
|
it("reverts original & forked revertibles after making separate changes to the original & forked view", () => {
|
||||||
|
const originalView = createInitializedView();
|
||||||
|
const { undoStack: undoStack1 } = createTestUndoRedoStacks(originalView.events);
|
||||||
|
|
||||||
|
assert(originalView.root.child !== undefined);
|
||||||
|
originalView.root.child.propertyOne = 256; // 128 -> 256
|
||||||
|
originalView.root.child.propertyTwo.itemOne = "newItem";
|
||||||
|
|
||||||
|
const forkedView = originalView.fork();
|
||||||
|
const { undoStack: undoStack2 } = createTestUndoRedoStacks(forkedView.events);
|
||||||
|
|
||||||
|
assert(forkedView.root.child !== undefined);
|
||||||
|
forkedView.root.child.propertyOne = 512; // 256 -> 512
|
||||||
|
|
||||||
|
undoStack2.pop()?.revert();
|
||||||
|
assert.equal(forkedView.root.child?.propertyOne, 256);
|
||||||
|
|
||||||
|
const undoOriginalPropertyTwo = undoStack1.pop();
|
||||||
|
const clonedUndoOriginalPropertyTwo = undoOriginalPropertyTwo?.clone(forkedView);
|
||||||
|
|
||||||
|
const undoOriginalPropertyOne = undoStack1.pop();
|
||||||
|
const clonedUndoOriginalPropertyOne = undoOriginalPropertyOne?.clone(forkedView);
|
||||||
|
|
||||||
|
undoOriginalPropertyOne?.revert();
|
||||||
|
undoOriginalPropertyTwo?.revert();
|
||||||
|
|
||||||
|
assert.equal(originalView.root.child?.propertyOne, 128);
|
||||||
|
assert.equal(originalView.root.child?.propertyTwo.itemOne, "");
|
||||||
|
assert.equal(forkedView.root.child?.propertyOne, 256);
|
||||||
|
assert.equal(forkedView.root.child?.propertyTwo.itemOne, "newItem");
|
||||||
|
|
||||||
|
clonedUndoOriginalPropertyOne?.revert();
|
||||||
|
clonedUndoOriginalPropertyTwo?.revert();
|
||||||
|
|
||||||
|
assert.equal(forkedView.root.child?.propertyOne, 128);
|
||||||
|
assert.equal(forkedView.root.child?.propertyTwo.itemOne, "");
|
||||||
|
|
||||||
|
assert.equal(undoOriginalPropertyOne?.status, RevertibleStatus.Disposed);
|
||||||
|
assert.equal(undoOriginalPropertyTwo?.status, RevertibleStatus.Disposed);
|
||||||
|
assert.equal(clonedUndoOriginalPropertyOne?.status, RevertibleStatus.Disposed);
|
||||||
|
assert.equal(clonedUndoOriginalPropertyTwo?.status, RevertibleStatus.Disposed);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO:#24414: Enable forkable revertibles tests to run on attached/detached mode.
|
||||||
|
it("reverts cloned revertible on original view", () => {
|
||||||
|
const view = createInitializedView();
|
||||||
|
const { undoStack } = createTestUndoRedoStacks(view.events);
|
||||||
|
|
||||||
|
assert(view.root.child !== undefined);
|
||||||
|
view.root.child.propertyOne = 256; // 128 -> 256
|
||||||
|
view.root.child.propertyTwo.itemOne = "newItem";
|
||||||
|
|
||||||
|
const undoOriginalPropertyTwo = undoStack.pop();
|
||||||
|
const undoOriginalPropertyOne = undoStack.pop();
|
||||||
|
|
||||||
|
const clonedUndoOriginalPropertyTwo = undoOriginalPropertyTwo?.clone(view);
|
||||||
|
const clonedUndoOriginalPropertyOne = undoOriginalPropertyOne?.clone(view);
|
||||||
|
|
||||||
|
clonedUndoOriginalPropertyTwo?.revert();
|
||||||
|
clonedUndoOriginalPropertyOne?.revert();
|
||||||
|
|
||||||
|
assert.equal(view.root.child?.propertyOne, 128);
|
||||||
|
assert.equal(view.root.child?.propertyTwo.itemOne, "");
|
||||||
|
assert.equal(undoOriginalPropertyOne?.status, RevertibleStatus.Disposed);
|
||||||
|
assert.equal(undoOriginalPropertyTwo?.status, RevertibleStatus.Disposed);
|
||||||
|
assert.equal(clonedUndoOriginalPropertyOne?.status, RevertibleStatus.Disposed);
|
||||||
|
assert.equal(clonedUndoOriginalPropertyTwo?.status, RevertibleStatus.Disposed);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO:#24414: Enable forkable revertibles tests to run on attached/detached mode.
|
||||||
|
it("reverts cloned revertible prior to original revertible", () => {
|
||||||
|
const originalView = createInitializedView();
|
||||||
|
const { undoStack } = createTestUndoRedoStacks(originalView.events);
|
||||||
|
|
||||||
|
assert(originalView.root.child !== undefined);
|
||||||
|
originalView.root.child.propertyOne = 256; // 128 -> 256
|
||||||
|
originalView.root.child.propertyTwo.itemOne = "newItem";
|
||||||
|
|
||||||
|
const forkedView = originalView.fork();
|
||||||
|
|
||||||
|
const undoOriginalPropertyTwo = undoStack.pop();
|
||||||
|
const undoOriginalPropertyOne = undoStack.pop();
|
||||||
|
|
||||||
|
const clonedUndoOriginalPropertyTwo = undoOriginalPropertyTwo?.clone(forkedView);
|
||||||
|
const clonedUndoOriginalPropertyOne = undoOriginalPropertyOne?.clone(forkedView);
|
||||||
|
|
||||||
|
clonedUndoOriginalPropertyTwo?.revert();
|
||||||
|
clonedUndoOriginalPropertyOne?.revert();
|
||||||
|
|
||||||
|
assert.equal(originalView.root.child?.propertyOne, 256);
|
||||||
|
assert.equal(originalView.root.child?.propertyTwo.itemOne, "newItem");
|
||||||
|
assert.equal(forkedView.root.child?.propertyOne, 128);
|
||||||
|
assert.equal(forkedView.root.child?.propertyTwo.itemOne, "");
|
||||||
|
assert.equal(undoOriginalPropertyOne?.status, RevertibleStatus.Valid);
|
||||||
|
assert.equal(undoOriginalPropertyTwo?.status, RevertibleStatus.Valid);
|
||||||
|
assert.equal(clonedUndoOriginalPropertyOne?.status, RevertibleStatus.Disposed);
|
||||||
|
assert.equal(clonedUndoOriginalPropertyTwo?.status, RevertibleStatus.Disposed);
|
||||||
|
|
||||||
|
undoOriginalPropertyTwo?.revert();
|
||||||
|
undoOriginalPropertyOne?.revert();
|
||||||
|
|
||||||
|
assert.equal(originalView.root.child?.propertyOne, 128);
|
||||||
|
assert.equal(originalView.root.child?.propertyTwo.itemOne, "");
|
||||||
|
assert.equal(undoOriginalPropertyOne?.status, RevertibleStatus.Disposed);
|
||||||
|
assert.equal(undoOriginalPropertyTwo?.status, RevertibleStatus.Disposed);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO:#24414: Enable forkable revertibles tests to run on attached/detached mode.
|
||||||
|
it("clone revertible fails if trees are different", () => {
|
||||||
|
const viewA = createInitializedView();
|
||||||
|
const viewB = createInitializedView();
|
||||||
|
|
||||||
|
const { undoStack } = createTestUndoRedoStacks(viewA.events);
|
||||||
|
|
||||||
|
assert(viewA.root.child !== undefined);
|
||||||
|
viewA.root.child.propertyOne = 256; // 128 -> 256
|
||||||
|
|
||||||
|
const undoOriginalPropertyOne = undoStack.pop();
|
||||||
|
|
||||||
|
assert.throws(() => undoOriginalPropertyOne?.clone(viewB).revert(), "Error: 0x576");
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO:#24414: Enable forkable revertibles tests to run on attached/detached mode.
|
||||||
|
it("cloned revertible fails if already applied", () => {
|
||||||
|
const view = createInitializedView();
|
||||||
|
const { undoStack } = createTestUndoRedoStacks(view.events);
|
||||||
|
|
||||||
|
assert(view.root.child !== undefined);
|
||||||
|
view.root.child.propertyOne = 256; // 128 -> 256
|
||||||
|
|
||||||
|
const undoOriginalPropertyOne = undoStack.pop();
|
||||||
|
const clonedUndoOriginalPropertyOne = undoOriginalPropertyOne?.clone(view);
|
||||||
|
|
||||||
|
undoOriginalPropertyOne?.revert();
|
||||||
|
|
||||||
|
assert.equal(view.root.child?.propertyOne, 128);
|
||||||
|
assert.equal(undoOriginalPropertyOne?.status, RevertibleStatus.Disposed);
|
||||||
|
assert.equal(clonedUndoOriginalPropertyOne?.status, RevertibleStatus.Disposed);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => clonedUndoOriginalPropertyOne?.revert(),
|
||||||
|
"Error: Unable to revert a revertible that has been disposed.",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -62,7 +62,6 @@ import {
|
||||||
type IEditableForest,
|
type IEditableForest,
|
||||||
type IForestSubscription,
|
type IForestSubscription,
|
||||||
type JsonableTree,
|
type JsonableTree,
|
||||||
type Revertible,
|
|
||||||
type RevisionInfo,
|
type RevisionInfo,
|
||||||
type RevisionMetadataSource,
|
type RevisionMetadataSource,
|
||||||
type RevisionTag,
|
type RevisionTag,
|
||||||
|
@ -86,7 +85,8 @@ import {
|
||||||
type TreeStoredSchemaSubscription,
|
type TreeStoredSchemaSubscription,
|
||||||
type ITreeCursorSynchronous,
|
type ITreeCursorSynchronous,
|
||||||
CursorLocationType,
|
CursorLocationType,
|
||||||
type RevertibleFactory,
|
type RevertibleAlpha,
|
||||||
|
type RevertibleAlphaFactory,
|
||||||
} from "../core/index.js";
|
} from "../core/index.js";
|
||||||
import type { HasListeners, IEmitter, Listenable } from "../events/index.js";
|
import type { HasListeners, IEmitter, Listenable } from "../events/index.js";
|
||||||
import { typeboxValidator } from "../external-utilities/index.js";
|
import { typeboxValidator } from "../external-utilities/index.js";
|
||||||
|
@ -450,7 +450,7 @@ export function spyOnMethod(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns `true` iff the given delta has a visible impact on the document tree.
|
* Determines whether or not the given delta has a visible impact on the document tree.
|
||||||
*/
|
*/
|
||||||
export function isDeltaVisible(delta: DeltaFieldChanges): boolean {
|
export function isDeltaVisible(delta: DeltaFieldChanges): boolean {
|
||||||
for (const mark of delta.local ?? []) {
|
for (const mark of delta.local ?? []) {
|
||||||
|
@ -1049,14 +1049,14 @@ export function rootFromDeltaFieldMap(
|
||||||
export function createTestUndoRedoStacks(
|
export function createTestUndoRedoStacks(
|
||||||
events: Listenable<TreeBranchEvents | CheckoutEvents>,
|
events: Listenable<TreeBranchEvents | CheckoutEvents>,
|
||||||
): {
|
): {
|
||||||
undoStack: Revertible[];
|
undoStack: RevertibleAlpha[];
|
||||||
redoStack: Revertible[];
|
redoStack: RevertibleAlpha[];
|
||||||
unsubscribe: () => void;
|
unsubscribe: () => void;
|
||||||
} {
|
} {
|
||||||
const undoStack: Revertible[] = [];
|
const undoStack: RevertibleAlpha[] = [];
|
||||||
const redoStack: Revertible[] = [];
|
const redoStack: RevertibleAlpha[] = [];
|
||||||
|
|
||||||
function onDispose(disposed: Revertible): void {
|
function onDispose(disposed: RevertibleAlpha): void {
|
||||||
const redoIndex = redoStack.indexOf(disposed);
|
const redoIndex = redoStack.indexOf(disposed);
|
||||||
if (redoIndex !== -1) {
|
if (redoIndex !== -1) {
|
||||||
redoStack.splice(redoIndex, 1);
|
redoStack.splice(redoIndex, 1);
|
||||||
|
@ -1068,7 +1068,7 @@ export function createTestUndoRedoStacks(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNewCommit(commit: CommitMetadata, getRevertible?: RevertibleFactory): void {
|
function onNewCommit(commit: CommitMetadata, getRevertible?: RevertibleAlphaFactory): void {
|
||||||
if (getRevertible !== undefined) {
|
if (getRevertible !== undefined) {
|
||||||
const revertible = getRevertible(onDispose);
|
const revertible = getRevertible(onDispose);
|
||||||
if (commit.kind === CommitKind.Undo) {
|
if (commit.kind === CommitKind.Undo) {
|
||||||
|
|
|
@ -892,6 +892,14 @@ export interface Revertible {
|
||||||
readonly status: RevertibleStatus;
|
readonly status: RevertibleStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @alpha @sealed
|
||||||
|
export interface RevertibleAlpha extends Revertible {
|
||||||
|
clone: (branch: TreeBranch) => RevertibleAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @alpha @sealed
|
||||||
|
export type RevertibleAlphaFactory = (onRevertibleDisposed?: (revertible: RevertibleAlpha) => void) => RevertibleAlpha;
|
||||||
|
|
||||||
// @public @sealed
|
// @public @sealed
|
||||||
export type RevertibleFactory = (onRevertibleDisposed?: (revertible: Revertible) => void) => Revertible;
|
export type RevertibleFactory = (onRevertibleDisposed?: (revertible: Revertible) => void) => Revertible;
|
||||||
|
|
||||||
|
@ -1098,8 +1106,8 @@ export interface TreeBranch extends IDisposable {
|
||||||
|
|
||||||
// @alpha @sealed
|
// @alpha @sealed
|
||||||
export interface TreeBranchEvents {
|
export interface TreeBranchEvents {
|
||||||
changed(data: CommitMetadata, getRevertible?: RevertibleFactory): void;
|
changed(data: CommitMetadata, getRevertible?: RevertibleAlphaFactory): void;
|
||||||
commitApplied(data: CommitMetadata, getRevertible?: RevertibleFactory): void;
|
commitApplied(data: CommitMetadata, getRevertible?: RevertibleAlphaFactory): void;
|
||||||
schemaChanged(): void;
|
schemaChanged(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Загрузка…
Ссылка в новой задаче