diff --git a/.changeset/ninety-dragons-fold.md b/.changeset/ninety-dragons-fold.md new file mode 100644 index 00000000000..1ec53cb3303 --- /dev/null +++ b/.changeset/ninety-dragons-fold.md @@ -0,0 +1,30 @@ +--- +"fluid-framework": minor +"@fluidframework/merge-tree": minor +"@fluidframework/sequence": minor +"@fluidframework/undo-redo": minor +--- +--- +"section": feature +--- + +### SharedString DDS annotateAdjustRange + +This update introduces a new feature to the `SharedString` DDS, allowing for the adjustment of properties over a specified range. The `annotateAdjustRange` method enables users to apply adjustments to properties within a given range, providing more flexibility and control over property modifications. + +An adjustment is a modification applied to a property value within a specified range. Adjustments can be used to increment or decrement property values dynamically. They are particularly useful in scenarios where property values need to be updated based on user interactions or other events. For example, in a rich text editor, adjustments can be used for modifying indentation levels or font sizes, where multiple users could apply differing numerical adjustments. + +### Key Features and Use Cases: +- **Adjustments with Constraints**: Adjustments can include optional minimum and maximum constraints to ensure the final value falls within specified bounds. This is particularly useful for maintaining consistent formatting in rich text editors. +- **Consistent Property Changes**: The feature ensures that property changes are consistent, managing both local and remote changes effectively. This is essential for collaborative rich text editing where multiple users may be making adjustments simultaneously. +- **Rich Text Formatting**: Adjustments can be used to modify text properties such as font size, indentation, or other formatting attributes dynamically based on user actions. + +### Configuration and Compatibility Requirements: +This feature is only available when the configuration `Fluid.Sequence.mergeTreeEnableAnnotateAdjust` is set to `true`. Additionally, all collaborating clients must have this feature enabled to use it. If any client does not have this feature enabled, it will lead to the client exiting collaboration. A future major version of Fluid will enable this feature by default. + +### Usage Example: +```typescript +sharedString.annotateAdjustRange(start, end, { + key: { value: 5, min: 0, max: 10 } +}); +``` diff --git a/packages/dds/merge-tree/api-report/merge-tree.legacy.alpha.api.md b/packages/dds/merge-tree/api-report/merge-tree.legacy.alpha.api.md index 1af7c4bd8a1..dfcebd7202e 100644 --- a/packages/dds/merge-tree/api-report/merge-tree.legacy.alpha.api.md +++ b/packages/dds/merge-tree/api-report/merge-tree.legacy.alpha.api.md @@ -216,7 +216,7 @@ export interface IMergeTreeDeltaCallbackArgs { return annotateOp; } + /** + * adjusts a value + */ + public annotateAdjustRangeLocal( + start: number, + end: number, + adjust: MapLike, + ): IMergeTreeAnnotateAdjustMsg { + const annotateOp = createAdjustRangeOp(start, end, adjust); + + for (const [key, value] of Object.entries(adjust)) { + if (value.min !== undefined && value.max !== undefined && value.min > value.max) { + throw new UsageError(`min is greater than max for ${key}`); + } + } + + this.applyAnnotateRangeOp({ op: annotateOp }); + return annotateOp; + } + /** * Removes the range * @@ -564,7 +587,7 @@ export class Client extends TypedEventEmitter { this._mergeTree.annotateRange( range.start, range.end, - op.props, + op, clientArgs.referenceSequenceNumber, clientArgs.clientId, clientArgs.sequenceNumber, @@ -686,6 +709,7 @@ export class Client extends TypedEventEmitter { private getValidOpRange( op: | IMergeTreeAnnotateMsg + | IMergeTreeAnnotateAdjustMsg | IMergeTreeInsertMsg | IMergeTreeRemoveMsg // eslint-disable-next-line import/no-deprecated @@ -767,11 +791,7 @@ export class Client extends TypedEventEmitter { | ISequencedDocumentMessage | Pick | undefined, - ): { - clientId: number; - referenceSequenceNumber: number; - sequenceNumber: number; - } { + ): IMergeTreeClientSequenceArgs { // If there this no sequenced message, then the op is local // and unacked, so use this clients sequenced args // @@ -907,7 +927,8 @@ export class Client extends TypedEventEmitter { switch (resetOp.type) { case MergeTreeDeltaType.ANNOTATE: { assert( - segment.propertyManager?.hasPendingProperties(resetOp.props) === true, + segment.propertyManager?.hasPendingProperties(resetOp.props ?? resetOp.adjust) === + true, 0x036 /* "Segment has no pending properties" */, ); // if the segment has been removed or obliterated, there's no need to send the annotate op @@ -921,11 +942,18 @@ export class Client extends TypedEventEmitter { (segment.localMovedSeq !== undefined && segment.movedSeq === UnassignedSequenceNumber)) ) { - newOp = createAnnotateRangeOp( - segmentPosition, - segmentPosition + segment.cachedLength, - resetOp.props, - ); + newOp = + resetOp.props === undefined + ? createAdjustRangeOp( + segmentPosition, + segmentPosition + segment.cachedLength, + resetOp.adjust, + ) + : createAnnotateRangeOp( + segmentPosition, + segmentPosition + segment.cachedLength, + resetOp.props, + ); } break; } diff --git a/packages/dds/merge-tree/src/mergeTree.ts b/packages/dds/merge-tree/src/mergeTree.ts index 60f8aa20daa..7caa808ed4e 100644 --- a/packages/dds/merge-tree/src/mergeTree.ts +++ b/packages/dds/merge-tree/src/mergeTree.ts @@ -94,6 +94,7 @@ import { copyPropertiesAndManager, PropertiesManager, PropertiesRollback, + type PropsOrAdjust, } from "./segmentPropertiesManager.js"; import { Side, type InteriorSequencePlace } from "./sequencePlace.js"; import { SortedSegmentSet } from "./sortedSegmentSet.js"; @@ -277,6 +278,14 @@ export interface IMergeTreeOptions { * @defaultValue `false` */ mergeTreeEnableSidedObliterate?: boolean; + + /** + * Enables support for annotate adjust operations, which allow for specifying + * a summand which is summed with the current value to compute the new value. + * + * @defaultValue `false` + */ + mergeTreeEnableAnnotateAdjust?: boolean; } /** @@ -1895,7 +1904,7 @@ export class MergeTree { * Annotate a range with properties * @param start - The inclusive start position of the range to annotate * @param end - The exclusive end position of the range to annotate - * @param props - The properties to annotate the range with + * @param propsOrAdjust - The properties or adjustments to annotate the range with * @param refSeq - The reference sequence number to use to apply the annotate * @param clientId - The id of the client making the annotate * @param seq - The sequence number of the annotate operation @@ -1905,15 +1914,18 @@ export class MergeTree { public annotateRange( start: number, end: number, - props: PropertySet, + propsOrAdjust: PropsOrAdjust, refSeq: number, clientId: number, seq: number, opArgs: IMergeTreeDeltaOpArgs, - // eslint-disable-next-line import/no-deprecated rollback: PropertiesRollback = PropertiesRollback.None, ): void { + if (propsOrAdjust.adjust !== undefined) { + errorIfOptionNotTrue(this.options, "mergeTreeEnableAnnotateAdjust"); + } + this.ensureIntervalBoundary(start, refSeq, clientId); this.ensureIntervalBoundary(end, refSeq, clientId); const deltaSegments: IMergeTreeSegmentDelta[] = []; @@ -1921,17 +1933,18 @@ export class MergeTree { seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined; // eslint-disable-next-line import/no-deprecated let segmentGroup: SegmentGroup | undefined; + const opObj = propsOrAdjust.props ?? propsOrAdjust.adjust; const annotateSegment = (segment: ISegmentLeaf): boolean => { assert( !Marker.is(segment) || - !(reservedMarkerIdKey in props) || - props.markerId === segment.properties?.markerId, + !(reservedMarkerIdKey in opObj) || + opObj.markerId === segment.properties?.markerId, 0x5ad /* Cannot change the markerId of an existing marker */, ); const propertyManager = (segment.propertyManager ??= new PropertiesManager()); const propertyDeltas = propertyManager.handleProperties( - { props }, + propsOrAdjust, segment, seq, this.collabWindow.minSeq, @@ -2409,12 +2422,11 @@ export class MergeTree { this.annotateRange( start, start + segment.cachedLength, - props, + { props }, UniversalSequenceNumber, this.collabWindow.clientId, UniversalSequenceNumber, { op: annotateOp }, - PropertiesRollback.Rollback, ); i++; diff --git a/packages/dds/merge-tree/src/opBuilder.ts b/packages/dds/merge-tree/src/opBuilder.ts index 3cf18f16f7d..7a80bc99ea0 100644 --- a/packages/dds/merge-tree/src/opBuilder.ts +++ b/packages/dds/merge-tree/src/opBuilder.ts @@ -14,9 +14,11 @@ import { IMergeTreeObliterateMsg, IMergeTreeRemoveMsg, MergeTreeDeltaType, + type AdjustParams, + type IMergeTreeAnnotateAdjustMsg, type IMergeTreeObliterateSidedMsg, } from "./ops.js"; -import { PropertySet } from "./properties.js"; +import { PropertySet, type MapLike } from "./properties.js"; import { normalizePlace, Side, type SequencePlace } from "./sequencePlace.js"; /** @@ -66,6 +68,28 @@ export function createAnnotateRangeOp( }; } +/** + * Creates the op for annotating the range with the provided properties + * @param start - The inclusive start position of the range to annotate + * @param end - The exclusive end position of the range to annotate + * @param props - The properties to annotate the range with + * @returns The annotate op + * + * @internal + */ +export function createAdjustRangeOp( + start: number, + end: number, + adjust: MapLike, +): IMergeTreeAnnotateAdjustMsg { + return { + pos1: start, + pos2: end, + adjust: { ...adjust }, + type: MergeTreeDeltaType.ANNOTATE, + }; +} + /** * Creates the op to remove a range * diff --git a/packages/dds/merge-tree/src/ops.ts b/packages/dds/merge-tree/src/ops.ts index 3281d56e496..53477988c97 100644 --- a/packages/dds/merge-tree/src/ops.ts +++ b/packages/dds/merge-tree/src/ops.ts @@ -265,6 +265,7 @@ export type IMergeTreeDeltaOp = | IMergeTreeInsertMsg | IMergeTreeRemoveMsg | IMergeTreeAnnotateMsg + | IMergeTreeAnnotateAdjustMsg | IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg; diff --git a/packages/dds/merge-tree/src/snapshotlegacy.ts b/packages/dds/merge-tree/src/snapshotlegacy.ts index a75962bf2a4..b726f0cee4f 100644 --- a/packages/dds/merge-tree/src/snapshotlegacy.ts +++ b/packages/dds/merge-tree/src/snapshotlegacy.ts @@ -18,7 +18,7 @@ import { import { NonCollabClient, UnassignedSequenceNumber } from "./constants.js"; import { MergeTree } from "./mergeTree.js"; -import { ISegment } from "./mergeTreeNodes.js"; +import { ISegment, type ISegmentLeaf } from "./mergeTreeNodes.js"; import { matchProperties } from "./properties.js"; import { JsonSegmentSpecs, @@ -193,7 +193,7 @@ export class SnapshotLegacy { extractSync(): ISegment[] { const collabWindow = this.mergeTree.collabWindow; - this.seq = collabWindow.minSeq; + const seq = (this.seq = collabWindow.minSeq); this.header = { segmentsTotalLength: this.mergeTree.getLength( this.mergeTree.collabWindow.minSeq, @@ -205,9 +205,9 @@ export class SnapshotLegacy { let originalSegments = 0; const segs: ISegment[] = []; - let prev: ISegment | undefined; + let prev: ISegmentLeaf | undefined; const extractSegment = ( - segment: ISegment, + segment: ISegmentLeaf, pos: number, refSeq: number, clientId: number, @@ -216,29 +216,26 @@ export class SnapshotLegacy { ): boolean => { if ( segment.seq !== UnassignedSequenceNumber && - segment.seq! <= this.seq! && + segment.seq! <= seq && (segment.removedSeq === undefined || segment.removedSeq === UnassignedSequenceNumber || - segment.removedSeq > this.seq!) + segment.removedSeq > seq) ) { originalSegments += 1; - if (prev?.canAppend(segment) && matchProperties(prev.properties, segment.properties)) { - prev = prev.clone(); + const properties = + segment.propertyManager?.getAtSeq(segment.properties, seq) ?? segment.properties; + if (prev?.canAppend(segment) && matchProperties(prev.properties, properties)) { prev.append(segment.clone()); } else { - if (prev) { - segs.push(prev); - } - prev = segment; + prev = segment.clone(); + prev.properties = properties; + segs.push(prev); } } return true; }; this.mergeTree.mapRange(extractSegment, this.seq, NonCollabClient, undefined); - if (prev) { - segs.push(prev); - } this.segments = []; let totalLength: number = 0; diff --git a/packages/dds/merge-tree/src/test/client.applyMsg.spec.ts b/packages/dds/merge-tree/src/test/client.applyMsg.spec.ts index 587b44b0af2..106a88b4ad7 100644 --- a/packages/dds/merge-tree/src/test/client.applyMsg.spec.ts +++ b/packages/dds/merge-tree/src/test/client.applyMsg.spec.ts @@ -7,7 +7,9 @@ import { strict as assert } from "node:assert"; +import { FluidErrorTypes } from "@fluidframework/core-interfaces/internal"; import { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal"; +import { isFluidError } from "@fluidframework/telemetry-utils/internal"; import { UnassignedSequenceNumber } from "../constants.js"; import { walkAllChildSegments } from "../mergeTreeNodeWalk.js"; @@ -692,6 +694,200 @@ describe("client.applyMsg", () => { logger.validate({ baseText: "DDDDDDcbD" }); }); + describe("annotateRangeAdjust", () => { + it("validate local and remote adjust combine", () => { + const clients = createClientsAtInitialState( + { + initialState: "0123456789", + options: { mergeTreeEnableAnnotateAdjust: true }, + }, + "A", + "B", + ); + let seq = 0; + const logger = new TestClientLogger(clients.all); + const ops: ISequencedDocumentMessage[] = []; + + ops.push( + clients.A.makeOpMessage( + clients.A.annotateAdjustRangeLocal(1, 3, { + key: { + delta: 1, + }, + }), + seq++, + ), + clients.B.makeOpMessage( + clients.B.annotateAdjustRangeLocal(1, 3, { + key: { + delta: 1, + }, + }), + seq++, + ), + ); + + for (const op of ops.splice(0)) + for (const c of clients.all) { + c.applyMsg(op); + } + assert.deepStrictEqual({ ...clients.A.getPropertiesAtPosition(2) }, { key: 2 }); + assert.deepStrictEqual({ ...clients.B.getPropertiesAtPosition(2) }, { key: 2 }); + logger.validate({ baseText: "0123456789" }); + }); + + it("validate local and remote adjust combine with min", () => { + const clients = createClientsAtInitialState( + { + initialState: "0123456789", + options: { mergeTreeEnableAnnotateAdjust: true }, + }, + "A", + "B", + ); + let seq = 0; + const logger = new TestClientLogger(clients.all); + const ops: ISequencedDocumentMessage[] = []; + + ops.push( + clients.A.makeOpMessage( + clients.A.annotateAdjustRangeLocal(1, 3, { + key: { + delta: -1, + }, + }), + seq++, + ), + clients.B.makeOpMessage( + clients.B.annotateAdjustRangeLocal(1, 3, { + key: { + delta: 1, + min: 0, + }, + }), + seq++, + ), + ); + + for (const op of ops.splice(0)) + for (const c of clients.all) { + c.applyMsg(op); + } + assert.deepStrictEqual({ ...clients.A.getPropertiesAtPosition(2) }, { key: 0 }); + assert.deepStrictEqual({ ...clients.B.getPropertiesAtPosition(2) }, { key: 0 }); + logger.validate({ baseText: "0123456789" }); + }); + + it("validate local and remote adjust combine with max", () => { + const clients = createClientsAtInitialState( + { + initialState: "0123456789", + options: { mergeTreeEnableAnnotateAdjust: true }, + }, + "A", + "B", + ); + let seq = 0; + const logger = new TestClientLogger(clients.all); + const ops: ISequencedDocumentMessage[] = []; + + ops.push( + clients.A.makeOpMessage( + clients.A.annotateAdjustRangeLocal(1, 3, { + key: { + delta: 1, + }, + }), + seq++, + ), + clients.B.makeOpMessage( + clients.B.annotateAdjustRangeLocal(1, 3, { + key: { + delta: 1, + max: 1, + }, + }), + seq++, + ), + ); + + for (const op of ops.splice(0)) + for (const c of clients.all) { + c.applyMsg(op); + } + assert.deepStrictEqual({ ...clients.A.getPropertiesAtPosition(2) }, { key: 1 }); + assert.deepStrictEqual({ ...clients.B.getPropertiesAtPosition(2) }, { key: 1 }); + logger.validate({ baseText: "0123456789" }); + }); + + it("validate local and remote adjust combine with min and max", () => { + const clients = createClientsAtInitialState( + { + initialState: "0123456789", + options: { mergeTreeEnableAnnotateAdjust: true }, + }, + "A", + "B", + ); + let seq = 0; + const logger = new TestClientLogger(clients.all); + const ops: ISequencedDocumentMessage[] = []; + + ops.push( + clients.A.makeOpMessage( + clients.A.annotateAdjustRangeLocal(1, 3, { + key: { + delta: 1, + }, + }), + seq++, + ), + clients.B.makeOpMessage( + clients.B.annotateAdjustRangeLocal(1, 3, { + key: { + delta: 0, + max: 0, + min: 0, + }, + }), + seq++, + ), + ); + + for (const op of ops.splice(0)) + for (const c of clients.all) { + c.applyMsg(op); + } + assert.deepStrictEqual({ ...clients.A.getPropertiesAtPosition(2) }, { key: 0 }); + assert.deepStrictEqual({ ...clients.B.getPropertiesAtPosition(2) }, { key: 0 }); + logger.validate({ baseText: "0123456789" }); + }); + + it("validate min must be less than max", () => { + const clients = createClientsAtInitialState( + { + initialState: "0123456789", + options: { mergeTreeEnableAnnotateAdjust: true }, + }, + "A", + ); + + try { + clients.A.annotateAdjustRangeLocal(1, 3, { + key: { + delta: 1, + max: 1, + min: 2, + }, + }); + assert.fail("should fail"); + } catch (error: unknown) { + assert(isFluidError(error)); + assert.equal(error.errorType, FluidErrorTypes.usageError); + } + }); + }); + describe("obliterate", () => { // op types: 0) insert 1) remove 2) annotate // Clients: 3 Ops: 3 Round: 86 diff --git a/packages/dds/merge-tree/src/test/client.applyStashedOpFarm.spec.ts b/packages/dds/merge-tree/src/test/client.applyStashedOpFarm.spec.ts index 7f3bb8b1b3b..ae79e603472 100644 --- a/packages/dds/merge-tree/src/test/client.applyStashedOpFarm.spec.ts +++ b/packages/dds/merge-tree/src/test/client.applyStashedOpFarm.spec.ts @@ -167,7 +167,7 @@ function runApplyStashedOpFarmTests( testOpts.resultsFilePostfix += extraSeed; } - const clients: TestClient[] = [new TestClient()]; + const clients: TestClient[] = [new TestClient({ mergeTreeEnableAnnotateAdjust: true })]; // This test is based on reconnectFarm, but we keep a second set of clients. For // these clients, we apply the generated ops as stashed ops, then regenerate // them to simulate resubmit(), then apply them. In the end, they should arrive @@ -175,7 +175,7 @@ function runApplyStashedOpFarmTests( let stashClients: TestClient[] = []; for (const [i, c] of clients.entries()) c.startOrUpdateCollaboration(clientNames[i]); - stashClients = [new TestClient()]; + stashClients = [new TestClient({ mergeTreeEnableAnnotateAdjust: true })]; for (const [i, c] of stashClients.entries()) c.startOrUpdateCollaboration(clientNames[i]); diff --git a/packages/dds/merge-tree/src/test/client.conflictFarm.spec.ts b/packages/dds/merge-tree/src/test/client.conflictFarm.spec.ts index c0139111aef..a5f66921bd1 100644 --- a/packages/dds/merge-tree/src/test/client.conflictFarm.spec.ts +++ b/packages/dds/merge-tree/src/test/client.conflictFarm.spec.ts @@ -97,6 +97,7 @@ function runConflictFarmTests(opts: IConflictFarmConfig, extraSeed?: number): vo new TestClient({ mergeTreeEnableObliterate: true, mergeTreeEnableSidedObliterate: true, + mergeTreeEnableAnnotateAdjust: true, }), ]; for (const [i, c] of clients.entries()) c.startOrUpdateCollaboration(clientNames[i]); diff --git a/packages/dds/merge-tree/src/test/client.reconnectFarm.spec.ts b/packages/dds/merge-tree/src/test/client.reconnectFarm.spec.ts index 0c1598b91eb..f0e7b4178dc 100644 --- a/packages/dds/merge-tree/src/test/client.reconnectFarm.spec.ts +++ b/packages/dds/merge-tree/src/test/client.reconnectFarm.spec.ts @@ -96,7 +96,7 @@ function runReconnectFarmTests(opts: IReconnectFarmConfig, extraSeed?: number): testOpts.resultsFilePostfix += extraSeed; } - const clients: TestClient[] = [new TestClient()]; + const clients: TestClient[] = [new TestClient({ mergeTreeEnableAnnotateAdjust: true })]; for (const [i, c] of clients.entries()) c.startOrUpdateCollaboration(clientNames[i]); let seq = 0; diff --git a/packages/dds/merge-tree/src/test/client.rollbackFarm.spec.ts b/packages/dds/merge-tree/src/test/client.rollbackFarm.spec.ts index d7b33110f45..5639ea296a6 100644 --- a/packages/dds/merge-tree/src/test/client.rollbackFarm.spec.ts +++ b/packages/dds/merge-tree/src/test/client.rollbackFarm.spec.ts @@ -35,7 +35,13 @@ describe("MergeTree.Client", () => { const random = makeRandom(0xdeadbeef, 0xfeedbed, minLength, opsPerRollback); // A: readonly, B: rollback, C: rollback + edit, D: edit - const clients = createClientsAtInitialState({ initialState: "" }, "A", "B", "C", "D"); + const clients = createClientsAtInitialState( + { initialState: "", options: { mergeTreeEnableAnnotateAdjust: true } }, + "A", + "B", + "C", + "D", + ); let seq = 0; for (let round = 0; round < defaultOptions.rounds; round++) { diff --git a/packages/dds/merge-tree/src/test/mergeTree.annotate.deltaCallback.spec.ts b/packages/dds/merge-tree/src/test/mergeTree.annotate.deltaCallback.spec.ts index e0bf7334add..f5e182bb3f6 100644 --- a/packages/dds/merge-tree/src/test/mergeTree.annotate.deltaCallback.spec.ts +++ b/packages/dds/merge-tree/src/test/mergeTree.annotate.deltaCallback.spec.ts @@ -49,7 +49,7 @@ describe("MergeTree", () => { 4, 6, { - foo: "bar", + props: { foo: "bar" }, }, currentSequenceNumber, localClientId, @@ -69,7 +69,7 @@ describe("MergeTree", () => { 3, 3, { - foo: "bar", + props: { foo: "bar" }, }, currentSequenceNumber, localClientId, @@ -100,7 +100,7 @@ describe("MergeTree", () => { 3, 8, { - foo: "bar", + props: { foo: "bar" }, }, currentSequenceNumber, localClientId, @@ -135,7 +135,7 @@ describe("MergeTree", () => { 3, 8, { - foo: "bar", + props: { foo: "bar" }, }, currentSequenceNumber, localClientId, @@ -170,7 +170,7 @@ describe("MergeTree", () => { 3, 8, { - foo: "bar", + props: { foo: "bar" }, }, currentSequenceNumber, localClientId, @@ -205,7 +205,7 @@ describe("MergeTree", () => { 4, 6, { - foo: "bar", + props: { foo: "bar" }, }, remoteSequenceNumber, remoteClientId, diff --git a/packages/dds/merge-tree/src/test/mergeTree.annotate.spec.ts b/packages/dds/merge-tree/src/test/mergeTree.annotate.spec.ts index ca7104f4bae..121f9be3db0 100644 --- a/packages/dds/merge-tree/src/test/mergeTree.annotate.spec.ts +++ b/packages/dds/merge-tree/src/test/mergeTree.annotate.spec.ts @@ -15,6 +15,7 @@ import { import { MergeTree } from "../mergeTree.js"; import { Marker, type ISegmentLeaf } from "../mergeTreeNodes.js"; import { MergeTreeDeltaType, ReferenceType } from "../ops.js"; +import type { PropsOrAdjust } from "../segmentPropertiesManager.js"; import { TextSegment } from "../textSegment.js"; import { insertSegments } from "./testUtils.js"; @@ -78,7 +79,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - propertySource: "remote", + props: { propertySource: "remote" }, }, currentSequenceNumber, remoteClientId, @@ -100,7 +101,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - propertySource: "local", + props: { propertySource: "local" }, }, currentSequenceNumber, localClientId, @@ -126,8 +127,8 @@ describe("MergeTree", () => { ); }); describe("local first", () => { - const props = { - propertySource: "local", + const props: PropsOrAdjust = { + props: { propertySource: "local" }, }; beforeEach(() => { mergeTree.annotateRange( @@ -156,7 +157,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - secondProperty: "local", + props: { secondProperty: "local" }, }, currentSequenceNumber, localClientId, @@ -186,8 +187,10 @@ describe("MergeTree", () => { }); it("unsequenced local after unsequenced local split", () => { - const secondChangeProps = { - secondChange: 1, + const secondChangeProps: PropsOrAdjust = { + props: { + secondChange: 1, + }, }; mergeTree.annotateRange( annotateStart, @@ -199,8 +202,10 @@ describe("MergeTree", () => { undefined as never, ); - const splitOnlyProps = { - splitOnly: 1, + const splitOnlyProps: PropsOrAdjust = { + props: { + splitOnly: 1, + }, }; mergeTree.annotateRange( @@ -241,7 +246,7 @@ describe("MergeTree", () => { op: { pos1: annotateStart, pos2: annotateEnd, - props, + ...props, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -263,7 +268,7 @@ describe("MergeTree", () => { op: { pos1: annotateStart, pos2: annotateEnd, - props: secondChangeProps, + ...secondChangeProps, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -285,7 +290,7 @@ describe("MergeTree", () => { op: { pos1: splitPos, pos2: annotateEnd, - props: splitOnlyProps, + ...splitOnlyProps, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -309,8 +314,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - propertySource: "remote", - remoteProperty: 1, + props: { propertySource: "remote", remoteProperty: 1 }, }, currentSequenceNumber, remoteClientId, @@ -335,7 +339,7 @@ describe("MergeTree", () => { op: { pos1: annotateStart, pos2: annotateEnd, - props, + ...props, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -358,7 +362,7 @@ describe("MergeTree", () => { op: { pos1: annotateStart, pos2: annotateEnd, - props, + ...props, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -370,8 +374,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - propertySource: "remote", - remoteProperty: 1, + props: { propertySource: "remote", remoteProperty: 1 }, }, currentSequenceNumber, remoteClientId, @@ -401,9 +404,8 @@ describe("MergeTree", () => { assert.equal(segment.properties?.propertySource, "local"); - const props2 = { - propertySource: "local2", - secondSource: 1, + const props2: PropsOrAdjust = { + props: { propertySource: "local2", secondSource: 1 }, }; mergeTree.annotateRange( annotateStart, @@ -418,8 +420,10 @@ describe("MergeTree", () => { assert.equal(segment.properties?.propertySource, "local2"); assert.equal(segment.properties?.secondSource, 1); - const props3 = { - thirdSource: 1, + const props3: PropsOrAdjust = { + props: { + thirdSource: 1, + }, }; mergeTree.annotateRange( annotateStart, @@ -439,7 +443,7 @@ describe("MergeTree", () => { op: { pos1: annotateStart, pos2: annotateEnd, - props, + ...props, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -455,7 +459,7 @@ describe("MergeTree", () => { op: { pos1: annotateStart, pos2: annotateEnd, - props: props2, + ...props2, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -471,7 +475,7 @@ describe("MergeTree", () => { op: { pos1: annotateStart, pos2: annotateEnd, - props: props3, + ...props3, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -489,7 +493,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - secondSource: "local2", + props: { secondSource: "local2" }, }, currentSequenceNumber, localClientId, @@ -501,7 +505,7 @@ describe("MergeTree", () => { op: { pos1: annotateStart, pos2: annotateEnd, - props, + ...props, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -513,9 +517,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - propertySource: "remote", - remoteOnly: 1, - secondSource: "remote", + props: { propertySource: "remote", remoteOnly: 1, secondSource: "remote" }, }, currentSequenceNumber, remoteClientId, @@ -541,8 +543,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - propertySource: "remote", - remoteProperty: 1, + props: { propertySource: "remote", remoteProperty: 1 }, }, currentSequenceNumber, remoteClientId, @@ -587,7 +588,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - propertySource: "local", + props: { propertySource: "local" }, }, currentSequenceNumber, localClientId, @@ -606,8 +607,8 @@ describe("MergeTree", () => { }); it("remote before sequenced local", () => { - const props = { - propertySource: "local", + const props: PropsOrAdjust = { + props: { propertySource: "local" }, }; const segmentInfo = mergeTree.getContainingSegment( @@ -633,7 +634,7 @@ describe("MergeTree", () => { op: { pos1: annotateStart, pos2: annotateEnd, - props, + ...props, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -647,8 +648,8 @@ describe("MergeTree", () => { }); }); describe("local with rewrite first", () => { - const props = { - propertySource: "local", + const props: PropsOrAdjust = { + props: { propertySource: "local" }, }; beforeEach(() => { mergeTree.annotateRange( @@ -667,8 +668,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - propertySource: "local2", - secondProperty: "local", + props: { propertySource: "local2", secondProperty: "local" }, }, currentSequenceNumber, localClientId, @@ -691,8 +691,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - propertySource: "remote", - remoteProperty: 1, + props: { propertySource: "remote", remoteProperty: 1 }, }, currentSequenceNumber, remoteClientId, @@ -717,7 +716,7 @@ describe("MergeTree", () => { op: { pos1: annotateStart, pos2: annotateEnd, - props, + ...props, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -729,8 +728,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - propertySource: "remote", - remoteProperty: 1, + props: { propertySource: "remote", remoteProperty: 1 }, }, currentSequenceNumber, remoteClientId, @@ -755,7 +753,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - secondSource: "local2", + props: { secondSource: "local2" }, }, currentSequenceNumber, localClientId, @@ -767,7 +765,7 @@ describe("MergeTree", () => { op: { pos1: annotateStart, pos2: annotateEnd, - props, + ...props, type: MergeTreeDeltaType.ANNOTATE, }, sequencedMessage: { @@ -779,9 +777,7 @@ describe("MergeTree", () => { annotateStart, annotateEnd, { - propertySource: "remote", - remoteOnly: 1, - secondSource: "remote", + props: { propertySource: "remote", remoteOnly: 1, secondSource: "remote" }, }, currentSequenceNumber, remoteClientId, diff --git a/packages/dds/merge-tree/src/test/mergeTreeOperationRunner.ts b/packages/dds/merge-tree/src/test/mergeTreeOperationRunner.ts index 1717a0ae8de..4aa313d0a11 100644 --- a/packages/dds/merge-tree/src/test/mergeTreeOperationRunner.ts +++ b/packages/dds/merge-tree/src/test/mergeTreeOperationRunner.ts @@ -70,7 +70,25 @@ export const annotateRange: TestOperation = ( client: TestClient, opStart: number, opEnd: number, -) => client.annotateRangeLocal(opStart, opEnd, { client: client.longClientId }); + random: IRandom, +) => { + // eslint-disable-next-line unicorn/prefer-ternary + if (random.bool()) { + return client.annotateRangeLocal(opStart, opEnd, { + [random.integer(1, 5)]: client.longClientId, + }); + } else { + const max = random.pick([undefined, random.integer(-10, 100)]); + const min = random.pick([undefined, random.integer(-100, 10)]); + return client.annotateAdjustRangeLocal(opStart, opEnd, { + [random.integer(0, 2).toString()]: { + delta: random.integer(-5, 5), + min: (min ?? max ?? 0) > (max ?? 0) ? undefined : min, + max, + }, + }); + } +}; export const insertAtRefPos: TestOperation = ( client: TestClient, diff --git a/packages/dds/merge-tree/src/test/revertibleFarm.spec.ts b/packages/dds/merge-tree/src/test/revertibleFarm.spec.ts index fd4897352eb..726990eea84 100644 --- a/packages/dds/merge-tree/src/test/revertibleFarm.spec.ts +++ b/packages/dds/merge-tree/src/test/revertibleFarm.spec.ts @@ -56,7 +56,7 @@ describe("MergeTree.Client", () => { const clients = createClientsAtInitialState( { initialState: "", - options: {}, + options: { mergeTreeEnableAnnotateAdjust: true }, }, "A", "B", diff --git a/packages/dds/merge-tree/src/test/types/validateMergeTreePrevious.generated.ts b/packages/dds/merge-tree/src/test/types/validateMergeTreePrevious.generated.ts index ca7d15561c3..43838494d65 100644 --- a/packages/dds/merge-tree/src/test/types/validateMergeTreePrevious.generated.ts +++ b/packages/dds/merge-tree/src/test/types/validateMergeTreePrevious.generated.ts @@ -41,6 +41,7 @@ declare type current_as_old_for_Class_BaseSegment = requireAssignableTo, TypeOnly> /* diff --git a/packages/dds/sequence/api-report/sequence.legacy.alpha.api.md b/packages/dds/sequence/api-report/sequence.legacy.alpha.api.md index 67026ebe5db..9e81d7e578b 100644 --- a/packages/dds/sequence/api-report/sequence.legacy.alpha.api.md +++ b/packages/dds/sequence/api-report/sequence.legacy.alpha.api.md @@ -211,6 +211,7 @@ export interface ISharedIntervalCollection extends ISharedObject, ISharedIntervalCollection, MergeTreeRevertibleDriver { + annotateAdjustRange(start: number, end: number, adjust: MapLike): void; annotateRange(start: number, end: number, props: PropertySet): void; createLocalReferencePosition(segment: T, offset: number, refType: ReferenceType, properties: PropertySet | undefined, slidingPreference?: SlidingPreference, canSlideToEndpoint?: boolean): LocalReferencePosition; getContainingSegment(pos: number): { diff --git a/packages/dds/sequence/package.json b/packages/dds/sequence/package.json index 466bf1341f1..eed3a1b5f58 100644 --- a/packages/dds/sequence/package.json +++ b/packages/dds/sequence/package.json @@ -261,6 +261,21 @@ }, "TypeAlias_SharedStringSegment": { "backCompat": false + }, + "Class_SharedSegmentSequence": { + "forwardCompat": false + }, + "Class_SharedStringClass": { + "forwardCompat": false + }, + "Interface_ISharedSegmentSequence": { + "forwardCompat": false + }, + "Interface_ISharedString": { + "forwardCompat": false + }, + "TypeAlias_SharedString": { + "forwardCompat": false } }, "entrypoint": "legacy" diff --git a/packages/dds/sequence/src/intervalCollectionMapInterfaces.ts b/packages/dds/sequence/src/intervalCollectionMapInterfaces.ts index 55a5e094425..8cc530d31ec 100644 --- a/packages/dds/sequence/src/intervalCollectionMapInterfaces.ts +++ b/packages/dds/sequence/src/intervalCollectionMapInterfaces.ts @@ -70,6 +70,7 @@ export interface SequenceOptions | "mergeTreeReferencesCanSlideToEndpoint" | "mergeTreeEnableObliterate" | "mergeTreeEnableSidedObliterate" + | "mergeTreeEnableAnnotateAdjust" > { /** * Enable the ability to use interval APIs that rely on positions before and diff --git a/packages/dds/sequence/src/sequence.ts b/packages/dds/sequence/src/sequence.ts index c97517f9c5e..186f992d8b7 100644 --- a/packages/dds/sequence/src/sequence.ts +++ b/packages/dds/sequence/src/sequence.ts @@ -46,7 +46,9 @@ import { createObliterateRangeOp, createRemoveRangeOp, matchProperties, + type AdjustParams, type InteriorSequencePlace, + type MapLike, } from "@fluidframework/merge-tree/internal"; import { ISummaryTreeWithStats, @@ -300,6 +302,21 @@ export interface ISharedSegmentSequence */ annotateRange(start: number, end: number, props: PropertySet): void; + /** + * Annotates a specified range within the sequence by applying the provided adjustments. + * + * @param start - The inclusive start position of the range to annotate. This is a zero-based index. + * @param end - The exclusive end position of the range to annotate. This is a zero-based index. + * @param adjust - A map-like object specifying the properties to adjust. Each key-value pair represents a property and its corresponding adjustment to be applied over the range. + * An adjustment is defined by an object containing a `delta` to be added to the current property value, and optional `min` and `max` constraints to limit the adjusted value. + * + * @remarks + * The range is defined by the start and end positions, where the start position is inclusive and the end position is exclusive. + * The properties provided in the adjust parameter will be applied to the specified range. Each adjustment modifies the current value of the property by adding the specified `value`. + * If the current value is not a number, the `delta` will be summed with 0 to compute the new value. The optional `min` and `max` constraints are applied after the adjustment to ensure the final value falls within the specified bounds. + */ + annotateAdjustRange(start: number, end: number, adjust: MapLike): void; + /** * @param start - The inclusive start of the range to remove * @param end - The exclusive end of the range to remove @@ -521,6 +538,7 @@ export abstract class SharedSegmentSequence mergeTreeEnableSidedObliterate: (c, n) => c.getBoolean(n), intervalStickinessEnabled: (c, n) => c.getBoolean(n), mergeTreeReferencesCanSlideToEndpoint: (c, n) => c.getBoolean(n), + mergeTreeEnableAnnotateAdjust: (c, n) => c.getBoolean(n), }, dataStoreRuntime.options, ); @@ -605,6 +623,10 @@ export abstract class SharedSegmentSequence this.guardReentrancy(() => this.client.annotateRangeLocal(start, end, props)); } + public annotateAdjustRange(start: number, end: number, adjust: MapLike): void { + this.guardReentrancy(() => this.client.annotateAdjustRangeLocal(start, end, adjust)); + } + public getPropertiesAtPosition(pos: number): PropertySet | undefined { return this.client.getPropertiesAtPosition(pos); } diff --git a/packages/dds/sequence/src/test/fuzz/fuzzUtils.ts b/packages/dds/sequence/src/test/fuzz/fuzzUtils.ts index 9ade09c3ba0..a6fb264f810 100644 --- a/packages/dds/sequence/src/test/fuzz/fuzzUtils.ts +++ b/packages/dds/sequence/src/test/fuzz/fuzzUtils.ts @@ -24,9 +24,9 @@ import { type Serializable, IChannelServices, } from "@fluidframework/datastore-definitions/internal"; -import { PropertySet, Side } from "@fluidframework/merge-tree/internal"; +import { PropertySet, Side, type AdjustParams } from "@fluidframework/merge-tree/internal"; -import type { SequenceInterval, SharedStringClass } from "../../index.js"; +import type { MapLike, SequenceInterval, SharedStringClass } from "../../index.js"; import { type IIntervalCollection } from "../../intervalCollection.js"; import { SharedStringRevertible, revertSharedStringRevertibles } from "../../revertibles.js"; import { SharedStringFactory } from "../../sequenceFactory.js"; @@ -68,6 +68,11 @@ export interface AnnotateRange extends RangeSpec { props: { key: string; value?: Serializable }[]; } +export interface AnnotateAdjustRange extends RangeSpec { + type: "annotateAdjustRange"; + adjust: { key: string; value: AdjustParams }[]; +} + export interface ObliterateRange extends RangeSpec { type: "obliterateRange"; } @@ -115,7 +120,12 @@ export interface RevertibleWeights { export type IntervalOperation = AddInterval | ChangeInterval | DeleteInterval; export type OperationWithRevert = IntervalOperation | RevertSharedStringRevertibles; -export type TextOperation = AddText | RemoveRange | AnnotateRange | ObliterateRange; +export type TextOperation = + | AddText + | RemoveRange + | AnnotateRange + | AnnotateAdjustRange + | ObliterateRange; export type ClientOperation = IntervalOperation | TextOperation; @@ -244,6 +254,13 @@ export function makeReducer( } client.channel.annotateRange(start, end, propertySet); }, + annotateAdjustRange: async ({ client }, { start, end, adjust }) => { + const adjustRange: MapLike = {}; + for (const { key, value } of adjust) { + adjustRange[key] = value; + } + client.channel.annotateAdjustRange(start, end, adjustRange); + }, obliterateRange: async ({ client }, { start, end }) => { client.channel.obliterateRange(start, end); }, @@ -338,6 +355,23 @@ export function createSharedStringGeneratorOperations( }; } + async function annotateAdjustRange(state: ClientOpState): Promise { + const { random } = state; + const key = random.pick(options.propertyNamePool); + const max = random.pick([undefined, random.integer(-10, 100)]); + const min = random.pick([undefined, random.integer(-100, 10)]); + const value: AdjustParams = { + delta: random.integer(-5, 5), + max, + min: (min ?? max ?? 0) > (max ?? 0) ? undefined : min, + }; + return { + type: "annotateAdjustRange", + ...exclusiveRange(state), + adjust: [{ key, value }], + }; + } + async function removeRange(state: ClientOpState): Promise { return { type: "removeRange", ...exclusiveRange(state) }; } @@ -360,6 +394,7 @@ export function createSharedStringGeneratorOperations( addText, obliterateRange, annotateRange, + annotateAdjustRange, removeRange, removeRangeLeaveChar, lengthSatisfies, @@ -368,6 +403,11 @@ export function createSharedStringGeneratorOperations( }; } +function setSharedStringRuntimeOptions(runtime: IFluidDataStoreRuntime) { + runtime.options.intervalStickinessEnabled = true; + runtime.options.mergeTreeEnableObliterate = true; + runtime.options.mergeTreeEnableAnnotateAdjust = true; +} export class SharedStringFuzzFactory extends SharedStringFactory { public async load( runtime: IFluidDataStoreRuntime, @@ -375,14 +415,12 @@ export class SharedStringFuzzFactory extends SharedStringFactory { services: IChannelServices, attributes: IChannelAttributes, ): Promise { - runtime.options.intervalStickinessEnabled = true; - runtime.options.mergeTreeEnableObliterate = true; + setSharedStringRuntimeOptions(runtime); return super.load(runtime, id, services, attributes); } public create(document: IFluidDataStoreRuntime, id: string): SharedStringClass { - document.options.intervalStickinessEnabled = true; - document.options.mergeTreeEnableObliterate = true; + setSharedStringRuntimeOptions(document); return super.create(document, id); } } diff --git a/packages/dds/sequence/src/test/fuzz/sharedString.fuzz.spec.ts b/packages/dds/sequence/src/test/fuzz/sharedString.fuzz.spec.ts index 51dc505f90d..a8c88c11e94 100644 --- a/packages/dds/sequence/src/test/fuzz/sharedString.fuzz.spec.ts +++ b/packages/dds/sequence/src/test/fuzz/sharedString.fuzz.spec.ts @@ -30,6 +30,7 @@ export function makeSharedStringOperationGenerator( addText, removeRange, annotateRange, + annotateAdjustRange, removeRangeLeaveChar, lengthSatisfies, hasNonzeroLength, @@ -50,6 +51,7 @@ export function makeSharedStringOperationGenerator( : hasNonzeroLength, ], [annotateRange, usableWeights.annotateRange, hasNonzeroLength], + [annotateAdjustRange, usableWeights.annotateRange, hasNonzeroLength], ]); } diff --git a/packages/dds/sequence/src/test/types/validateSequencePrevious.generated.ts b/packages/dds/sequence/src/test/types/validateSequencePrevious.generated.ts index 7550c4c6135..51d27b21252 100644 --- a/packages/dds/sequence/src/test/types/validateSequencePrevious.generated.ts +++ b/packages/dds/sequence/src/test/types/validateSequencePrevious.generated.ts @@ -136,6 +136,7 @@ declare type current_as_old_for_Class_SequenceMaintenanceEvent = requireAssignab * typeValidation.broken: * "Class_SharedSegmentSequence": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Class_SharedSegmentSequence = requireAssignableTo>, TypeOnly>> /* @@ -154,6 +155,7 @@ declare type current_as_old_for_Class_SharedSegmentSequence = requireAssignableT * typeValidation.broken: * "Class_SharedStringClass": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Class_SharedStringClass = requireAssignableTo, TypeOnly> /* @@ -635,6 +637,7 @@ declare type current_as_old_for_Interface_ISharedIntervalCollection = requireAss * typeValidation.broken: * "Interface_ISharedSegmentSequence": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Interface_ISharedSegmentSequence = requireAssignableTo>, TypeOnly>> /* @@ -671,6 +674,7 @@ declare type current_as_old_for_Interface_ISharedSegmentSequenceEvents = require * typeValidation.broken: * "Interface_ISharedString": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Interface_ISharedString = requireAssignableTo, TypeOnly> /* @@ -862,6 +866,7 @@ declare type current_as_old_for_TypeAlias_SequencePlace = requireAssignableTo, TypeOnly> /* diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md index f4fdf9a7bbe..3f29429f7b6 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md @@ -756,6 +756,7 @@ export interface ISharedObjectEvents extends IErrorEvent { // @alpha (undocumented) export interface ISharedSegmentSequence extends ISharedObject, ISharedIntervalCollection, MergeTreeRevertibleDriver { + annotateAdjustRange(start: number, end: number, adjust: MapLike): void; annotateRange(start: number, end: number, props: PropertySet): void; createLocalReferencePosition(segment: T, offset: number, refType: ReferenceType, properties: PropertySet | undefined, slidingPreference?: SlidingPreference, canSlideToEndpoint?: boolean): LocalReferencePosition; getContainingSegment(pos: number): { diff --git a/packages/framework/undo-redo/package.json b/packages/framework/undo-redo/package.json index 16139ffca37..83f7352a0dd 100644 --- a/packages/framework/undo-redo/package.json +++ b/packages/framework/undo-redo/package.json @@ -132,7 +132,11 @@ "typescript": "~5.4.5" }, "typeValidation": { - "broken": {}, + "broken": { + "Class_SharedSegmentSequenceRevertible": { + "forwardCompat": false + } + }, "entrypoint": "internal" } } diff --git a/packages/framework/undo-redo/src/test/types/validateUndoRedoPrevious.generated.ts b/packages/framework/undo-redo/src/test/types/validateUndoRedoPrevious.generated.ts index a9a47e38df3..a9a3c247a4b 100644 --- a/packages/framework/undo-redo/src/test/types/validateUndoRedoPrevious.generated.ts +++ b/packages/framework/undo-redo/src/test/types/validateUndoRedoPrevious.generated.ts @@ -58,6 +58,7 @@ declare type current_as_old_for_Class_SharedMapUndoRedoHandler = requireAssignab * typeValidation.broken: * "Class_SharedSegmentSequenceRevertible": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Class_SharedSegmentSequenceRevertible = requireAssignableTo, TypeOnly> /*