@fluidframework/merge-tree
Advanced tools
Comparing version 2.10.0-307399 to 2.10.0
@@ -219,3 +219,3 @@ ## Alpha API Report File for "@fluidframework/merge-tree" | ||
// @alpha (undocumented) | ||
export type IMergeTreeDeltaOp = IMergeTreeInsertMsg | IMergeTreeRemoveMsg | IMergeTreeAnnotateMsg | IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg; | ||
export type IMergeTreeDeltaOp = IMergeTreeInsertMsg | IMergeTreeRemoveMsg | IMergeTreeAnnotateMsg | IMergeTreeAnnotateAdjustMsg | IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg; | ||
@@ -294,2 +294,3 @@ // @alpha (undocumented) | ||
catchUpBlobName?: string; | ||
mergeTreeEnableAnnotateAdjust?: boolean; | ||
mergeTreeEnableObliterate?: boolean; | ||
@@ -296,0 +297,0 @@ mergeTreeEnableObliterateReconnect?: boolean; |
# @fluidframework/merge-tree | ||
## 2.10.0 | ||
### Minor Changes | ||
- Unsupported merge-tree types and related exposed internals have been removed ([#22696](https://github.com/microsoft/FluidFramework/pull/22696)) [7a032533a6](https://github.com/microsoft/FluidFramework/commit/7a032533a6ee6a6f76fe154ef65dfa33f87e5a7b) | ||
As part of ongoing improvements, several internal types and related APIs have been removed. These types are unnecessary for any supported scenarios and could lead to errors if used. Since directly using these types would likely result in errors, these changes are not likely to impact any Fluid Framework consumers. | ||
Removed types: | ||
- IMergeTreeTextHelper | ||
- MergeNode | ||
- ObliterateInfo | ||
- PropertiesManager | ||
- PropertiesRollback | ||
- SegmentGroup | ||
- SegmentGroupCollection | ||
In addition to removing the above types, they are no longer exposed through the following interfaces and their implementations: `ISegment`, `ReferencePosition`, and `ISerializableInterval`. | ||
Removed functions: | ||
- addProperties | ||
- ack | ||
Removed properties: | ||
- propertyManager | ||
- segmentGroups | ||
The initial deprecations of the now changed or removed types were announced in Fluid Framework v2.2.0: | ||
[Fluid Framework v2.2.0](https://github.com/microsoft/FluidFramework/blob/main/RELEASE_NOTES/2.2.0.md) | ||
- SharedString DDS annotateAdjustRange ([#22751](https://github.com/microsoft/FluidFramework/pull/22751)) [d54b9dde14](https://github.com/microsoft/FluidFramework/commit/d54b9dde14e9e0e5eb7999db8ebf6da98fdfb526) | ||
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 }, | ||
}); | ||
``` | ||
- MergeTree `Client` Legacy API Removed ([#22697](https://github.com/microsoft/FluidFramework/pull/22697)) [2aa0b5e794](https://github.com/microsoft/FluidFramework/commit/2aa0b5e7941efe52386782595f96ff847c786fc3) | ||
The `Client` class in the merge-tree package has been removed. Types that directly or indirectly expose the merge-tree `Client` class have also been removed. | ||
The removed types were not meant to be used directly, and direct usage was not supported: | ||
- AttributionPolicy | ||
- IClientEvents | ||
- IMergeTreeAttributionOptions | ||
- SharedSegmentSequence | ||
- SharedStringClass | ||
Some classes that referenced the `Client` class have been transitioned to interfaces. Direct instantiation of these classes was not supported or necessary for any supported scenario, so the change to an interface should not impact usage. This applies to the following types: | ||
- SequenceInterval | ||
- SequenceEvent | ||
- SequenceDeltaEvent | ||
- SequenceMaintenanceEvent | ||
The initial deprecations of the now changed or removed types were announced in Fluid Framework v2.4.0: | ||
[Several MergeTree Client Legacy APIs are now deprecated](https://github.com/microsoft/FluidFramework/blob/main/RELEASE_NOTES/2.4.0.md#several-mergetree-client-legacy-apis-are-now-deprecated-22629) | ||
## 2.5.0 | ||
@@ -4,0 +83,0 @@ |
@@ -87,3 +87,3 @@ "use strict"; | ||
// Only attribute annotations which change the tracked property | ||
op.props[propName] !== undefined && | ||
(op.props?.[propName] !== undefined || op.adjust?.[propName] !== undefined) && | ||
(isLocal || (propertyDeltas !== undefined && propName in propertyDeltas)); | ||
@@ -90,0 +90,0 @@ if (shouldAttributeInsert || shouldAttributeAnnotate) { |
@@ -16,4 +16,4 @@ /*! | ||
import { CollaborationWindow, ISegment, ISegmentAction, Marker, SegmentGroup } from "./mergeTreeNodes.js"; | ||
import { IJSONSegment, IMergeTreeAnnotateMsg, IMergeTreeGroupMsg, IMergeTreeInsertMsg, IMergeTreeObliterateMsg, IMergeTreeOp, IMergeTreeRemoveMsg, IRelativePosition, ReferenceType, type IMergeTreeObliterateSidedMsg } from "./ops.js"; | ||
import { PropertySet } from "./properties.js"; | ||
import { IJSONSegment, IMergeTreeAnnotateMsg, IMergeTreeGroupMsg, IMergeTreeInsertMsg, IMergeTreeObliterateMsg, IMergeTreeOp, IMergeTreeRemoveMsg, IRelativePosition, ReferenceType, type AdjustParams, type IMergeTreeAnnotateAdjustMsg, type IMergeTreeObliterateSidedMsg } from "./ops.js"; | ||
import { PropertySet, type MapLike } from "./properties.js"; | ||
import { ReferencePosition } from "./referencePositions.js"; | ||
@@ -98,2 +98,6 @@ import { type InteriorSequencePlace } from "./sequencePlace.js"; | ||
/** | ||
* adjusts a value | ||
*/ | ||
annotateAdjustRangeLocal(start: number, end: number, adjust: MapLike<AdjustParams>): IMergeTreeAnnotateAdjustMsg; | ||
/** | ||
* Removes the range | ||
@@ -100,0 +104,0 @@ * |
@@ -112,2 +112,15 @@ "use strict"; | ||
/** | ||
* adjusts a value | ||
*/ | ||
annotateAdjustRangeLocal(start, end, adjust) { | ||
const annotateOp = (0, opBuilder_js_1.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 internal_4.UsageError(`min is greater than max for ${key}`); | ||
} | ||
} | ||
this.applyAnnotateRangeOp({ op: annotateOp }); | ||
return annotateOp; | ||
} | ||
/** | ||
* Removes the range | ||
@@ -304,3 +317,3 @@ * | ||
const range = this.getValidOpRange(op, clientArgs); | ||
this._mergeTree.annotateRange(range.start, range.end, op.props, clientArgs.referenceSequenceNumber, clientArgs.clientId, clientArgs.sequenceNumber, opArgs); | ||
this._mergeTree.annotateRange(range.start, range.end, op, clientArgs.referenceSequenceNumber, clientArgs.clientId, clientArgs.sequenceNumber, opArgs); | ||
} | ||
@@ -550,3 +563,4 @@ /** | ||
case ops_js_1.MergeTreeDeltaType.ANNOTATE: { | ||
(0, internal_1.assert)(segment.propertyManager?.hasPendingProperties(resetOp.props) === true, 0x036 /* "Segment has no pending properties" */); | ||
(0, internal_1.assert)(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 | ||
@@ -561,3 +575,6 @@ // unless the remove was local, in which case the annotate must have come | ||
segment.movedSeq === constants_js_1.UnassignedSequenceNumber))) { | ||
newOp = (0, opBuilder_js_1.createAnnotateRangeOp)(segmentPosition, segmentPosition + segment.cachedLength, resetOp.props); | ||
newOp = | ||
resetOp.props === undefined | ||
? (0, opBuilder_js_1.createAdjustRangeOp)(segmentPosition, segmentPosition + segment.cachedLength, resetOp.adjust) | ||
: (0, opBuilder_js_1.createAnnotateRangeOp)(segmentPosition, segmentPosition + segment.cachedLength, resetOp.props); | ||
} | ||
@@ -564,0 +581,0 @@ break; |
@@ -16,3 +16,3 @@ /*! | ||
import { ReferencePosition } from "./referencePositions.js"; | ||
import { PropertiesRollback } from "./segmentPropertiesManager.js"; | ||
import { PropertiesRollback, type PropsOrAdjust } from "./segmentPropertiesManager.js"; | ||
import { type InteriorSequencePlace } from "./sequencePlace.js"; | ||
@@ -79,2 +79,9 @@ /** | ||
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; | ||
} | ||
@@ -311,3 +318,3 @@ /** | ||
* @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 | ||
@@ -319,3 +326,3 @@ * @param clientId - The id of the client making the annotate | ||
*/ | ||
annotateRange(start: number, end: number, props: PropertySet, refSeq: number, clientId: number, seq: number, opArgs: IMergeTreeDeltaOpArgs, rollback?: PropertiesRollback): void; | ||
annotateRange(start: number, end: number, propsOrAdjust: PropsOrAdjust, refSeq: number, clientId: number, seq: number, opArgs: IMergeTreeDeltaOpArgs, rollback?: PropertiesRollback): void; | ||
private obliterateRangeSided; | ||
@@ -322,0 +329,0 @@ obliterateRange(start: number | InteriorSequencePlace, end: number | InteriorSequencePlace, refSeq: number, clientId: number, seq: number, opArgs: IMergeTreeDeltaOpArgs): void; |
@@ -6,4 +6,4 @@ /*! | ||
import { ISegment, Marker } from "./mergeTreeNodes.js"; | ||
import { IMergeTreeAnnotateMsg, IMergeTreeDeltaOp, IMergeTreeGroupMsg, IMergeTreeInsertMsg, IMergeTreeObliterateMsg, IMergeTreeRemoveMsg, type IMergeTreeObliterateSidedMsg } from "./ops.js"; | ||
import { PropertySet } from "./properties.js"; | ||
import { IMergeTreeAnnotateMsg, IMergeTreeDeltaOp, IMergeTreeGroupMsg, IMergeTreeInsertMsg, IMergeTreeObliterateMsg, IMergeTreeRemoveMsg, type AdjustParams, type IMergeTreeAnnotateAdjustMsg, type IMergeTreeObliterateSidedMsg } from "./ops.js"; | ||
import { PropertySet, type MapLike } from "./properties.js"; | ||
import { type SequencePlace } from "./sequencePlace.js"; | ||
@@ -30,2 +30,12 @@ /** | ||
/** | ||
* 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 declare function createAdjustRangeOp(start: number, end: number, adjust: MapLike<AdjustParams>): IMergeTreeAnnotateAdjustMsg; | ||
/** | ||
* Creates the op to remove a range | ||
@@ -32,0 +42,0 @@ * |
@@ -7,3 +7,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createGroupOp = exports.createInsertOp = exports.createInsertSegmentOp = exports.createObliterateRangeOpSided = exports.createObliterateRangeOp = exports.createRemoveRangeOp = exports.createAnnotateRangeOp = exports.createAnnotateMarkerOp = void 0; | ||
exports.createGroupOp = exports.createInsertOp = exports.createInsertSegmentOp = exports.createObliterateRangeOpSided = exports.createObliterateRangeOp = exports.createRemoveRangeOp = exports.createAdjustRangeOp = exports.createAnnotateRangeOp = exports.createAnnotateMarkerOp = void 0; | ||
const ops_js_1 = require("./ops.js"); | ||
@@ -51,2 +51,20 @@ const sequencePlace_js_1 = require("./sequencePlace.js"); | ||
/** | ||
* 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 | ||
*/ | ||
function createAdjustRangeOp(start, end, adjust) { | ||
return { | ||
pos1: start, | ||
pos2: end, | ||
adjust: { ...adjust }, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
}; | ||
} | ||
exports.createAdjustRangeOp = createAdjustRangeOp; | ||
/** | ||
* Creates the op to remove a range | ||
@@ -53,0 +71,0 @@ * |
@@ -245,3 +245,3 @@ /*! | ||
*/ | ||
export type IMergeTreeDeltaOp = IMergeTreeInsertMsg | IMergeTreeRemoveMsg | IMergeTreeAnnotateMsg | IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg; | ||
export type IMergeTreeDeltaOp = IMergeTreeInsertMsg | IMergeTreeRemoveMsg | IMergeTreeAnnotateMsg | IMergeTreeAnnotateAdjustMsg | IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg; | ||
/** | ||
@@ -248,0 +248,0 @@ * @legacy |
@@ -115,3 +115,3 @@ "use strict"; | ||
if (collaborating) { | ||
(0, internal_1.assert)(pending !== undefined, "Pending changes must exist for rollback when collaborating"); | ||
(0, internal_1.assert)(pending !== undefined, 0xa6f /* Pending changes must exist for rollback when collaborating */); | ||
pending.local.pop(); | ||
@@ -124,3 +124,3 @@ properties[key] = computePropertyValue(pending.msnConsensus, pending.remote.map((n) => n.data), pending.local.map((n) => n.data)); | ||
else { | ||
(0, internal_1.assert)(pending === undefined, "Pending changes must not exist when not collaborating"); | ||
(0, internal_1.assert)(pending === undefined, 0xa70 /* Pending changes must not exist when not collaborating */); | ||
properties[key] = computePropertyValue(previousValue, [value]); | ||
@@ -199,3 +199,3 @@ } | ||
const acked = change?.local?.shift(); | ||
(0, internal_1.assert)(change !== undefined && acked !== undefined, "must have local change to ack"); | ||
(0, internal_1.assert)(change !== undefined && acked !== undefined, 0xa71 /* must have local change to ack */); | ||
// we only track remotes if there are adjusts, as only adjusts make application anti-commutative | ||
@@ -202,0 +202,0 @@ // this will limit the impact of this change to only those using adjusts. Additionally, we only |
@@ -90,3 +90,3 @@ "use strict"; | ||
const collabWindow = this.mergeTree.collabWindow; | ||
this.seq = collabWindow.minSeq; | ||
const seq = (this.seq = collabWindow.minSeq); | ||
this.header = { | ||
@@ -101,16 +101,15 @@ segmentsTotalLength: this.mergeTree.getLength(this.mergeTree.collabWindow.minSeq, constants_js_1.NonCollabClient), | ||
if (segment.seq !== constants_js_1.UnassignedSequenceNumber && | ||
segment.seq <= this.seq && | ||
segment.seq <= seq && | ||
(segment.removedSeq === undefined || | ||
segment.removedSeq === constants_js_1.UnassignedSequenceNumber || | ||
segment.removedSeq > this.seq)) { | ||
segment.removedSeq > seq)) { | ||
originalSegments += 1; | ||
if (prev?.canAppend(segment) && (0, properties_js_1.matchProperties)(prev.properties, segment.properties)) { | ||
prev = prev.clone(); | ||
const properties = segment.propertyManager?.getAtSeq(segment.properties, seq) ?? segment.properties; | ||
if (prev?.canAppend(segment) && (0, properties_js_1.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); | ||
} | ||
@@ -121,5 +120,2 @@ } | ||
this.mergeTree.mapRange(extractSegment, this.seq, constants_js_1.NonCollabClient, undefined); | ||
if (prev) { | ||
segs.push(prev); | ||
} | ||
this.segments = []; | ||
@@ -126,0 +122,0 @@ let totalLength = 0; |
@@ -9,2 +9,4 @@ "use strict"; | ||
const node_assert_1 = require("node:assert"); | ||
const internal_1 = require("@fluidframework/core-interfaces/internal"); | ||
const internal_2 = require("@fluidframework/telemetry-utils/internal"); | ||
const constants_js_1 = require("../constants.js"); | ||
@@ -528,2 +530,128 @@ const mergeTreeNodeWalk_js_1 = require("../mergeTreeNodeWalk.js"); | ||
}); | ||
describe("annotateRangeAdjust", () => { | ||
it("validate local and remote adjust combine", () => { | ||
const clients = (0, testClientLogger_js_1.createClientsAtInitialState)({ | ||
initialState: "0123456789", | ||
options: { mergeTreeEnableAnnotateAdjust: true }, | ||
}, "A", "B"); | ||
let seq = 0; | ||
const logger = new testClientLogger_js_1.TestClientLogger(clients.all); | ||
const ops = []; | ||
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); | ||
} | ||
node_assert_1.strict.deepStrictEqual({ ...clients.A.getPropertiesAtPosition(2) }, { key: 2 }); | ||
node_assert_1.strict.deepStrictEqual({ ...clients.B.getPropertiesAtPosition(2) }, { key: 2 }); | ||
logger.validate({ baseText: "0123456789" }); | ||
}); | ||
it("validate local and remote adjust combine with min", () => { | ||
const clients = (0, testClientLogger_js_1.createClientsAtInitialState)({ | ||
initialState: "0123456789", | ||
options: { mergeTreeEnableAnnotateAdjust: true }, | ||
}, "A", "B"); | ||
let seq = 0; | ||
const logger = new testClientLogger_js_1.TestClientLogger(clients.all); | ||
const ops = []; | ||
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); | ||
} | ||
node_assert_1.strict.deepStrictEqual({ ...clients.A.getPropertiesAtPosition(2) }, { key: 0 }); | ||
node_assert_1.strict.deepStrictEqual({ ...clients.B.getPropertiesAtPosition(2) }, { key: 0 }); | ||
logger.validate({ baseText: "0123456789" }); | ||
}); | ||
it("validate local and remote adjust combine with max", () => { | ||
const clients = (0, testClientLogger_js_1.createClientsAtInitialState)({ | ||
initialState: "0123456789", | ||
options: { mergeTreeEnableAnnotateAdjust: true }, | ||
}, "A", "B"); | ||
let seq = 0; | ||
const logger = new testClientLogger_js_1.TestClientLogger(clients.all); | ||
const ops = []; | ||
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); | ||
} | ||
node_assert_1.strict.deepStrictEqual({ ...clients.A.getPropertiesAtPosition(2) }, { key: 1 }); | ||
node_assert_1.strict.deepStrictEqual({ ...clients.B.getPropertiesAtPosition(2) }, { key: 1 }); | ||
logger.validate({ baseText: "0123456789" }); | ||
}); | ||
it("validate local and remote adjust combine with min and max", () => { | ||
const clients = (0, testClientLogger_js_1.createClientsAtInitialState)({ | ||
initialState: "0123456789", | ||
options: { mergeTreeEnableAnnotateAdjust: true }, | ||
}, "A", "B"); | ||
let seq = 0; | ||
const logger = new testClientLogger_js_1.TestClientLogger(clients.all); | ||
const ops = []; | ||
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); | ||
} | ||
node_assert_1.strict.deepStrictEqual({ ...clients.A.getPropertiesAtPosition(2) }, { key: 0 }); | ||
node_assert_1.strict.deepStrictEqual({ ...clients.B.getPropertiesAtPosition(2) }, { key: 0 }); | ||
logger.validate({ baseText: "0123456789" }); | ||
}); | ||
it("validate min must be less than max", () => { | ||
const clients = (0, testClientLogger_js_1.createClientsAtInitialState)({ | ||
initialState: "0123456789", | ||
options: { mergeTreeEnableAnnotateAdjust: true }, | ||
}, "A"); | ||
try { | ||
clients.A.annotateAdjustRangeLocal(1, 3, { | ||
key: { | ||
delta: 1, | ||
max: 1, | ||
min: 2, | ||
}, | ||
}); | ||
node_assert_1.strict.fail("should fail"); | ||
} | ||
catch (error) { | ||
(0, node_assert_1.strict)((0, internal_2.isFluidError)(error)); | ||
node_assert_1.strict.equal(error.errorType, internal_1.FluidErrorTypes.usageError); | ||
} | ||
}); | ||
}); | ||
describe("obliterate", () => { | ||
@@ -530,0 +658,0 @@ // op types: 0) insert 1) remove 2) annotate |
@@ -122,3 +122,3 @@ "use strict"; | ||
} | ||
const clients = [new testClient_js_1.TestClient()]; | ||
const clients = [new testClient_js_1.TestClient({ mergeTreeEnableAnnotateAdjust: true })]; | ||
// This test is based on reconnectFarm, but we keep a second set of clients. For | ||
@@ -131,3 +131,3 @@ // these clients, we apply the generated ops as stashed ops, then regenerate | ||
c.startOrUpdateCollaboration(clientNames[i]); | ||
stashClients = [new testClient_js_1.TestClient()]; | ||
stashClients = [new testClient_js_1.TestClient({ mergeTreeEnableAnnotateAdjust: true })]; | ||
for (const [i, c] of stashClients.entries()) | ||
@@ -134,0 +134,0 @@ c.startOrUpdateCollaboration(clientNames[i]); |
@@ -77,2 +77,3 @@ "use strict"; | ||
mergeTreeEnableSidedObliterate: true, | ||
mergeTreeEnableAnnotateAdjust: true, | ||
}), | ||
@@ -79,0 +80,0 @@ ]; |
@@ -65,3 +65,3 @@ "use strict"; | ||
} | ||
const clients = [new testClient_js_1.TestClient()]; | ||
const clients = [new testClient_js_1.TestClient({ mergeTreeEnableAnnotateAdjust: true })]; | ||
for (const [i, c] of clients.entries()) | ||
@@ -68,0 +68,0 @@ c.startOrUpdateCollaboration(clientNames[i]); |
@@ -25,3 +25,3 @@ "use strict"; | ||
// A: readonly, B: rollback, C: rollback + edit, D: edit | ||
const clients = (0, testClientLogger_js_1.createClientsAtInitialState)({ initialState: "" }, "A", "B", "C", "D"); | ||
const clients = (0, testClientLogger_js_1.createClientsAtInitialState)({ initialState: "", options: { mergeTreeEnableAnnotateAdjust: true } }, "A", "B", "C", "D"); | ||
let seq = 0; | ||
@@ -28,0 +28,0 @@ for (let round = 0; round < defaultOptions.rounds; round++) { |
@@ -38,3 +38,3 @@ "use strict"; | ||
mergeTree.annotateRange(4, 6, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
@@ -49,3 +49,3 @@ node_assert_1.strict.deepStrictEqual(count, { | ||
mergeTree.annotateRange(3, 3, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, currentSequenceNumber, localClientId, ++currentSequenceNumber, undefined); | ||
@@ -69,3 +69,3 @@ node_assert_1.strict.deepStrictEqual(count, { | ||
mergeTree.annotateRange(3, 8, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
@@ -92,3 +92,3 @@ node_assert_1.strict.deepStrictEqual(count, { | ||
mergeTree.annotateRange(3, 8, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
@@ -115,3 +115,3 @@ node_assert_1.strict.deepStrictEqual(count, { | ||
mergeTree.annotateRange(3, 8, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
@@ -138,3 +138,3 @@ node_assert_1.strict.deepStrictEqual(count, { | ||
mergeTree.annotateRange(4, 6, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, remoteSequenceNumber, remoteClientId, ++remoteSequenceNumber, undefined); | ||
@@ -141,0 +141,0 @@ node_assert_1.strict.deepStrictEqual(count, { |
@@ -57,3 +57,3 @@ "use strict"; | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
props: { propertySource: "remote" }, | ||
}, currentSequenceNumber, remoteClientId, currentSequenceNumber + 1, undefined); | ||
@@ -66,3 +66,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "local", | ||
props: { propertySource: "local" }, | ||
}, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
@@ -82,3 +82,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
const props = { | ||
propertySource: "local", | ||
props: { propertySource: "local" }, | ||
}; | ||
@@ -95,3 +95,3 @@ beforeEach(() => { | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
secondProperty: "local", | ||
props: { secondProperty: "local" }, | ||
}, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
@@ -111,7 +111,11 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
const secondChangeProps = { | ||
secondChange: 1, | ||
props: { | ||
secondChange: 1, | ||
}, | ||
}; | ||
mergeTree.annotateRange(annotateStart, annotateEnd, secondChangeProps, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
const splitOnlyProps = { | ||
splitOnly: 1, | ||
props: { | ||
splitOnly: 1, | ||
}, | ||
}; | ||
@@ -135,3 +139,3 @@ mergeTree.annotateRange(splitPos, annotateEnd, splitOnlyProps, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -155,3 +159,3 @@ }, | ||
pos2: annotateEnd, | ||
props: secondChangeProps, | ||
...secondChangeProps, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -175,3 +179,3 @@ }, | ||
pos2: annotateEnd, | ||
props: splitOnlyProps, | ||
...splitOnlyProps, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -194,4 +198,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteProperty: 1, | ||
props: { propertySource: "remote", remoteProperty: 1 }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -209,3 +212,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -227,3 +230,3 @@ }, | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -236,4 +239,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteProperty: 1, | ||
props: { propertySource: "remote", remoteProperty: 1 }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -251,4 +253,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
const props2 = { | ||
propertySource: "local2", | ||
secondSource: 1, | ||
props: { propertySource: "local2", secondSource: 1 }, | ||
}; | ||
@@ -259,3 +260,5 @@ mergeTree.annotateRange(annotateStart, annotateEnd, props2, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
const props3 = { | ||
thirdSource: 1, | ||
props: { | ||
thirdSource: 1, | ||
}, | ||
}; | ||
@@ -270,3 +273,3 @@ mergeTree.annotateRange(annotateStart, annotateEnd, props3, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -285,3 +288,3 @@ }, | ||
pos2: annotateEnd, | ||
props: props2, | ||
...props2, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -300,3 +303,3 @@ }, | ||
pos2: annotateEnd, | ||
props: props3, | ||
...props3, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -314,3 +317,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
secondSource: "local2", | ||
props: { secondSource: "local2" }, | ||
}, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
@@ -321,3 +324,3 @@ mergeTree.ackPendingSegment({ | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -330,5 +333,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteOnly: 1, | ||
secondSource: "remote", | ||
props: { propertySource: "remote", remoteOnly: 1, secondSource: "remote" }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -345,4 +346,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteProperty: 1, | ||
props: { propertySource: "remote", remoteProperty: 1 }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -368,3 +368,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "local", | ||
props: { propertySource: "local" }, | ||
}, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
@@ -378,3 +378,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
const props = { | ||
propertySource: "local", | ||
props: { propertySource: "local" }, | ||
}; | ||
@@ -389,3 +389,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -404,3 +404,3 @@ }, | ||
const props = { | ||
propertySource: "local", | ||
props: { propertySource: "local" }, | ||
}; | ||
@@ -412,4 +412,3 @@ beforeEach(() => { | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "local2", | ||
secondProperty: "local", | ||
props: { propertySource: "local2", secondProperty: "local" }, | ||
}, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
@@ -423,4 +422,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteProperty: 1, | ||
props: { propertySource: "remote", remoteProperty: 1 }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -438,3 +436,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -447,4 +445,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteProperty: 1, | ||
props: { propertySource: "remote", remoteProperty: 1 }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -459,3 +456,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
secondSource: "local2", | ||
props: { secondSource: "local2" }, | ||
}, currentSequenceNumber, localClientId, constants_js_1.UnassignedSequenceNumber, undefined); | ||
@@ -466,3 +463,3 @@ mergeTree.ackPendingSegment({ | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: ops_js_1.MergeTreeDeltaType.ANNOTATE, | ||
@@ -475,5 +472,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteOnly: 1, | ||
secondSource: "remote", | ||
props: { propertySource: "remote", remoteOnly: 1, secondSource: "remote" }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -480,0 +475,0 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); |
@@ -65,3 +65,21 @@ "use strict"; | ||
exports.obliterateRangeSided = obliterateRangeSided; | ||
const annotateRange = (client, opStart, opEnd) => client.annotateRangeLocal(opStart, opEnd, { client: client.longClientId }); | ||
const annotateRange = (client, opStart, opEnd, random) => { | ||
// 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, | ||
}, | ||
}); | ||
} | ||
}; | ||
exports.annotateRange = annotateRange; | ||
@@ -68,0 +86,0 @@ const insertAtRefPos = (client, opStart, opEnd, random) => { |
@@ -34,3 +34,3 @@ "use strict"; | ||
initialState: "", | ||
options: {}, | ||
options: { mergeTreeEnableAnnotateAdjust: true }, | ||
}, "A", "B", "C"); | ||
@@ -37,0 +37,0 @@ let seq = 0; |
@@ -84,3 +84,3 @@ /*! | ||
// Only attribute annotations which change the tracked property | ||
op.props[propName] !== undefined && | ||
(op.props?.[propName] !== undefined || op.adjust?.[propName] !== undefined) && | ||
(isLocal || (propertyDeltas !== undefined && propName in propertyDeltas)); | ||
@@ -87,0 +87,0 @@ if (shouldAttributeInsert || shouldAttributeAnnotate) { |
@@ -16,4 +16,4 @@ /*! | ||
import { CollaborationWindow, ISegment, ISegmentAction, Marker, SegmentGroup } from "./mergeTreeNodes.js"; | ||
import { IJSONSegment, IMergeTreeAnnotateMsg, IMergeTreeGroupMsg, IMergeTreeInsertMsg, IMergeTreeObliterateMsg, IMergeTreeOp, IMergeTreeRemoveMsg, IRelativePosition, ReferenceType, type IMergeTreeObliterateSidedMsg } from "./ops.js"; | ||
import { PropertySet } from "./properties.js"; | ||
import { IJSONSegment, IMergeTreeAnnotateMsg, IMergeTreeGroupMsg, IMergeTreeInsertMsg, IMergeTreeObliterateMsg, IMergeTreeOp, IMergeTreeRemoveMsg, IRelativePosition, ReferenceType, type AdjustParams, type IMergeTreeAnnotateAdjustMsg, type IMergeTreeObliterateSidedMsg } from "./ops.js"; | ||
import { PropertySet, type MapLike } from "./properties.js"; | ||
import { ReferencePosition } from "./referencePositions.js"; | ||
@@ -98,2 +98,6 @@ import { type InteriorSequencePlace } from "./sequencePlace.js"; | ||
/** | ||
* adjusts a value | ||
*/ | ||
annotateAdjustRangeLocal(start: number, end: number, adjust: MapLike<AdjustParams>): IMergeTreeAnnotateAdjustMsg; | ||
/** | ||
* Removes the range | ||
@@ -100,0 +104,0 @@ * |
@@ -17,3 +17,3 @@ /*! | ||
import { compareStrings, toMoveInfo, } from "./mergeTreeNodes.js"; | ||
import { createAnnotateMarkerOp, createAnnotateRangeOp, | ||
import { createAdjustRangeOp, createAnnotateMarkerOp, createAnnotateRangeOp, | ||
// eslint-disable-next-line import/no-deprecated | ||
@@ -112,2 +112,15 @@ createGroupOp, createInsertSegmentOp, createObliterateRangeOp, createObliterateRangeOpSided, createRemoveRangeOp, } from "./opBuilder.js"; | ||
/** | ||
* adjusts a value | ||
*/ | ||
annotateAdjustRangeLocal(start, end, adjust) { | ||
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 | ||
@@ -304,3 +317,3 @@ * | ||
const range = this.getValidOpRange(op, clientArgs); | ||
this._mergeTree.annotateRange(range.start, range.end, op.props, clientArgs.referenceSequenceNumber, clientArgs.clientId, clientArgs.sequenceNumber, opArgs); | ||
this._mergeTree.annotateRange(range.start, range.end, op, clientArgs.referenceSequenceNumber, clientArgs.clientId, clientArgs.sequenceNumber, opArgs); | ||
} | ||
@@ -550,3 +563,4 @@ /** | ||
case MergeTreeDeltaType.ANNOTATE: { | ||
assert(segment.propertyManager?.hasPendingProperties(resetOp.props) === true, 0x036 /* "Segment has no pending properties" */); | ||
assert(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 | ||
@@ -561,3 +575,6 @@ // unless the remove was local, in which case the annotate must have come | ||
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); | ||
} | ||
@@ -564,0 +581,0 @@ break; |
@@ -16,3 +16,3 @@ /*! | ||
import { ReferencePosition } from "./referencePositions.js"; | ||
import { PropertiesRollback } from "./segmentPropertiesManager.js"; | ||
import { PropertiesRollback, type PropsOrAdjust } from "./segmentPropertiesManager.js"; | ||
import { type InteriorSequencePlace } from "./sequencePlace.js"; | ||
@@ -79,2 +79,9 @@ /** | ||
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; | ||
} | ||
@@ -311,3 +318,3 @@ /** | ||
* @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 | ||
@@ -319,3 +326,3 @@ * @param clientId - The id of the client making the annotate | ||
*/ | ||
annotateRange(start: number, end: number, props: PropertySet, refSeq: number, clientId: number, seq: number, opArgs: IMergeTreeDeltaOpArgs, rollback?: PropertiesRollback): void; | ||
annotateRange(start: number, end: number, propsOrAdjust: PropsOrAdjust, refSeq: number, clientId: number, seq: number, opArgs: IMergeTreeDeltaOpArgs, rollback?: PropertiesRollback): void; | ||
private obliterateRangeSided; | ||
@@ -322,0 +329,0 @@ obliterateRange(start: number | InteriorSequencePlace, end: number | InteriorSequencePlace, refSeq: number, clientId: number, seq: number, opArgs: IMergeTreeDeltaOpArgs): void; |
@@ -6,4 +6,4 @@ /*! | ||
import { ISegment, Marker } from "./mergeTreeNodes.js"; | ||
import { IMergeTreeAnnotateMsg, IMergeTreeDeltaOp, IMergeTreeGroupMsg, IMergeTreeInsertMsg, IMergeTreeObliterateMsg, IMergeTreeRemoveMsg, type IMergeTreeObliterateSidedMsg } from "./ops.js"; | ||
import { PropertySet } from "./properties.js"; | ||
import { IMergeTreeAnnotateMsg, IMergeTreeDeltaOp, IMergeTreeGroupMsg, IMergeTreeInsertMsg, IMergeTreeObliterateMsg, IMergeTreeRemoveMsg, type AdjustParams, type IMergeTreeAnnotateAdjustMsg, type IMergeTreeObliterateSidedMsg } from "./ops.js"; | ||
import { PropertySet, type MapLike } from "./properties.js"; | ||
import { type SequencePlace } from "./sequencePlace.js"; | ||
@@ -30,2 +30,12 @@ /** | ||
/** | ||
* 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 declare function createAdjustRangeOp(start: number, end: number, adjust: MapLike<AdjustParams>): IMergeTreeAnnotateAdjustMsg; | ||
/** | ||
* Creates the op to remove a range | ||
@@ -32,0 +42,0 @@ * |
@@ -45,2 +45,19 @@ /*! | ||
/** | ||
* 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, end, adjust) { | ||
return { | ||
pos1: start, | ||
pos2: end, | ||
adjust: { ...adjust }, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
}; | ||
} | ||
/** | ||
* Creates the op to remove a range | ||
@@ -47,0 +64,0 @@ * |
@@ -245,3 +245,3 @@ /*! | ||
*/ | ||
export type IMergeTreeDeltaOp = IMergeTreeInsertMsg | IMergeTreeRemoveMsg | IMergeTreeAnnotateMsg | IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg; | ||
export type IMergeTreeDeltaOp = IMergeTreeInsertMsg | IMergeTreeRemoveMsg | IMergeTreeAnnotateMsg | IMergeTreeAnnotateAdjustMsg | IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg; | ||
/** | ||
@@ -248,0 +248,0 @@ * @legacy |
@@ -111,3 +111,3 @@ /*! | ||
if (collaborating) { | ||
assert(pending !== undefined, "Pending changes must exist for rollback when collaborating"); | ||
assert(pending !== undefined, 0xa6f /* Pending changes must exist for rollback when collaborating */); | ||
pending.local.pop(); | ||
@@ -120,3 +120,3 @@ properties[key] = computePropertyValue(pending.msnConsensus, pending.remote.map((n) => n.data), pending.local.map((n) => n.data)); | ||
else { | ||
assert(pending === undefined, "Pending changes must not exist when not collaborating"); | ||
assert(pending === undefined, 0xa70 /* Pending changes must not exist when not collaborating */); | ||
properties[key] = computePropertyValue(previousValue, [value]); | ||
@@ -195,3 +195,3 @@ } | ||
const acked = change?.local?.shift(); | ||
assert(change !== undefined && acked !== undefined, "must have local change to ack"); | ||
assert(change !== undefined && acked !== undefined, 0xa71 /* must have local change to ack */); | ||
// we only track remotes if there are adjusts, as only adjusts make application anti-commutative | ||
@@ -198,0 +198,0 @@ // this will limit the impact of this change to only those using adjusts. Additionally, we only |
@@ -87,3 +87,3 @@ /*! | ||
const collabWindow = this.mergeTree.collabWindow; | ||
this.seq = collabWindow.minSeq; | ||
const seq = (this.seq = collabWindow.minSeq); | ||
this.header = { | ||
@@ -98,16 +98,15 @@ segmentsTotalLength: this.mergeTree.getLength(this.mergeTree.collabWindow.minSeq, NonCollabClient), | ||
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); | ||
} | ||
@@ -118,5 +117,2 @@ } | ||
this.mergeTree.mapRange(extractSegment, this.seq, NonCollabClient, undefined); | ||
if (prev) { | ||
segs.push(prev); | ||
} | ||
this.segments = []; | ||
@@ -123,0 +119,0 @@ let totalLength = 0; |
@@ -7,2 +7,4 @@ /*! | ||
import { strict as assert } from "node:assert"; | ||
import { FluidErrorTypes } from "@fluidframework/core-interfaces/internal"; | ||
import { isFluidError } from "@fluidframework/telemetry-utils/internal"; | ||
import { UnassignedSequenceNumber } from "../constants.js"; | ||
@@ -526,2 +528,128 @@ import { walkAllChildSegments } from "../mergeTreeNodeWalk.js"; | ||
}); | ||
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 = []; | ||
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 = []; | ||
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 = []; | ||
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 = []; | ||
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) { | ||
assert(isFluidError(error)); | ||
assert.equal(error.errorType, FluidErrorTypes.usageError); | ||
} | ||
}); | ||
}); | ||
describe("obliterate", () => { | ||
@@ -528,0 +656,0 @@ // op types: 0) insert 1) remove 2) annotate |
@@ -119,3 +119,3 @@ /*! | ||
} | ||
const clients = [new TestClient()]; | ||
const clients = [new TestClient({ mergeTreeEnableAnnotateAdjust: true })]; | ||
// This test is based on reconnectFarm, but we keep a second set of clients. For | ||
@@ -128,3 +128,3 @@ // these clients, we apply the generated ops as stashed ops, then regenerate | ||
c.startOrUpdateCollaboration(clientNames[i]); | ||
stashClients = [new TestClient()]; | ||
stashClients = [new TestClient({ mergeTreeEnableAnnotateAdjust: true })]; | ||
for (const [i, c] of stashClients.entries()) | ||
@@ -131,0 +131,0 @@ c.startOrUpdateCollaboration(clientNames[i]); |
@@ -74,2 +74,3 @@ /*! | ||
mergeTreeEnableSidedObliterate: true, | ||
mergeTreeEnableAnnotateAdjust: true, | ||
}), | ||
@@ -76,0 +77,0 @@ ]; |
@@ -62,3 +62,3 @@ /*! | ||
} | ||
const clients = [new TestClient()]; | ||
const clients = [new TestClient({ mergeTreeEnableAnnotateAdjust: true })]; | ||
for (const [i, c] of clients.entries()) | ||
@@ -65,0 +65,0 @@ c.startOrUpdateCollaboration(clientNames[i]); |
@@ -23,3 +23,3 @@ /*! | ||
// 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; | ||
@@ -26,0 +26,0 @@ for (let round = 0; round < defaultOptions.rounds; round++) { |
@@ -36,3 +36,3 @@ /*! | ||
mergeTree.annotateRange(4, 6, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
@@ -47,3 +47,3 @@ assert.deepStrictEqual(count, { | ||
mergeTree.annotateRange(3, 3, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, currentSequenceNumber, localClientId, ++currentSequenceNumber, undefined); | ||
@@ -67,3 +67,3 @@ assert.deepStrictEqual(count, { | ||
mergeTree.annotateRange(3, 8, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
@@ -90,3 +90,3 @@ assert.deepStrictEqual(count, { | ||
mergeTree.annotateRange(3, 8, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
@@ -113,3 +113,3 @@ assert.deepStrictEqual(count, { | ||
mergeTree.annotateRange(3, 8, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
@@ -136,3 +136,3 @@ assert.deepStrictEqual(count, { | ||
mergeTree.annotateRange(4, 6, { | ||
foo: "bar", | ||
props: { foo: "bar" }, | ||
}, remoteSequenceNumber, remoteClientId, ++remoteSequenceNumber, undefined); | ||
@@ -139,0 +139,0 @@ assert.deepStrictEqual(count, { |
@@ -55,3 +55,3 @@ /*! | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
props: { propertySource: "remote" }, | ||
}, currentSequenceNumber, remoteClientId, currentSequenceNumber + 1, undefined); | ||
@@ -64,3 +64,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "local", | ||
props: { propertySource: "local" }, | ||
}, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
@@ -80,3 +80,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
const props = { | ||
propertySource: "local", | ||
props: { propertySource: "local" }, | ||
}; | ||
@@ -93,3 +93,3 @@ beforeEach(() => { | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
secondProperty: "local", | ||
props: { secondProperty: "local" }, | ||
}, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
@@ -109,7 +109,11 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
const secondChangeProps = { | ||
secondChange: 1, | ||
props: { | ||
secondChange: 1, | ||
}, | ||
}; | ||
mergeTree.annotateRange(annotateStart, annotateEnd, secondChangeProps, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
const splitOnlyProps = { | ||
splitOnly: 1, | ||
props: { | ||
splitOnly: 1, | ||
}, | ||
}; | ||
@@ -133,3 +137,3 @@ mergeTree.annotateRange(splitPos, annotateEnd, splitOnlyProps, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -153,3 +157,3 @@ }, | ||
pos2: annotateEnd, | ||
props: secondChangeProps, | ||
...secondChangeProps, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -173,3 +177,3 @@ }, | ||
pos2: annotateEnd, | ||
props: splitOnlyProps, | ||
...splitOnlyProps, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -192,4 +196,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteProperty: 1, | ||
props: { propertySource: "remote", remoteProperty: 1 }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -207,3 +210,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -225,3 +228,3 @@ }, | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -234,4 +237,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteProperty: 1, | ||
props: { propertySource: "remote", remoteProperty: 1 }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -249,4 +251,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
const props2 = { | ||
propertySource: "local2", | ||
secondSource: 1, | ||
props: { propertySource: "local2", secondSource: 1 }, | ||
}; | ||
@@ -257,3 +258,5 @@ mergeTree.annotateRange(annotateStart, annotateEnd, props2, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
const props3 = { | ||
thirdSource: 1, | ||
props: { | ||
thirdSource: 1, | ||
}, | ||
}; | ||
@@ -268,3 +271,3 @@ mergeTree.annotateRange(annotateStart, annotateEnd, props3, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -283,3 +286,3 @@ }, | ||
pos2: annotateEnd, | ||
props: props2, | ||
...props2, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -298,3 +301,3 @@ }, | ||
pos2: annotateEnd, | ||
props: props3, | ||
...props3, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -312,3 +315,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
secondSource: "local2", | ||
props: { secondSource: "local2" }, | ||
}, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
@@ -319,3 +322,3 @@ mergeTree.ackPendingSegment({ | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -328,5 +331,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteOnly: 1, | ||
secondSource: "remote", | ||
props: { propertySource: "remote", remoteOnly: 1, secondSource: "remote" }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -343,4 +344,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteProperty: 1, | ||
props: { propertySource: "remote", remoteProperty: 1 }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -366,3 +366,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "local", | ||
props: { propertySource: "local" }, | ||
}, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
@@ -376,3 +376,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
const props = { | ||
propertySource: "local", | ||
props: { propertySource: "local" }, | ||
}; | ||
@@ -387,3 +387,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -402,3 +402,3 @@ }, | ||
const props = { | ||
propertySource: "local", | ||
props: { propertySource: "local" }, | ||
}; | ||
@@ -410,4 +410,3 @@ beforeEach(() => { | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "local2", | ||
secondProperty: "local", | ||
props: { propertySource: "local2", secondProperty: "local" }, | ||
}, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
@@ -421,4 +420,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteProperty: 1, | ||
props: { propertySource: "remote", remoteProperty: 1 }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -436,3 +434,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -445,4 +443,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteProperty: 1, | ||
props: { propertySource: "remote", remoteProperty: 1 }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -457,3 +454,3 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
secondSource: "local2", | ||
props: { secondSource: "local2" }, | ||
}, currentSequenceNumber, localClientId, UnassignedSequenceNumber, undefined); | ||
@@ -464,3 +461,3 @@ mergeTree.ackPendingSegment({ | ||
pos2: annotateEnd, | ||
props, | ||
...props, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
@@ -473,5 +470,3 @@ }, | ||
mergeTree.annotateRange(annotateStart, annotateEnd, { | ||
propertySource: "remote", | ||
remoteOnly: 1, | ||
secondSource: "remote", | ||
props: { propertySource: "remote", remoteOnly: 1, secondSource: "remote" }, | ||
}, currentSequenceNumber, remoteClientId, ++currentSequenceNumber, undefined); | ||
@@ -478,0 +473,0 @@ const segmentInfo = mergeTree.getContainingSegment(annotateStart, currentSequenceNumber, localClientId); |
@@ -36,3 +36,21 @@ /*! | ||
}; | ||
export const annotateRange = (client, opStart, opEnd) => client.annotateRangeLocal(opStart, opEnd, { client: client.longClientId }); | ||
export const annotateRange = (client, opStart, opEnd, random) => { | ||
// 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 = (client, opStart, opEnd, random) => { | ||
@@ -39,0 +57,0 @@ const segs = []; |
@@ -29,3 +29,3 @@ /*! | ||
initialState: "", | ||
options: {}, | ||
options: { mergeTreeEnableAnnotateAdjust: true }, | ||
}, "A", "B", "C"); | ||
@@ -32,0 +32,0 @@ let seq = 0; |
{ | ||
"name": "@fluidframework/merge-tree", | ||
"version": "2.10.0-307399", | ||
"version": "2.10.0", | ||
"description": "Merge tree", | ||
@@ -84,12 +84,12 @@ "homepage": "https://fluidframework.com", | ||
"dependencies": { | ||
"@fluid-internal/client-utils": "2.10.0-307399", | ||
"@fluidframework/container-definitions": "2.10.0-307399", | ||
"@fluidframework/core-interfaces": "2.10.0-307399", | ||
"@fluidframework/core-utils": "2.10.0-307399", | ||
"@fluidframework/datastore-definitions": "2.10.0-307399", | ||
"@fluidframework/driver-definitions": "2.10.0-307399", | ||
"@fluidframework/runtime-definitions": "2.10.0-307399", | ||
"@fluidframework/runtime-utils": "2.10.0-307399", | ||
"@fluidframework/shared-object-base": "2.10.0-307399", | ||
"@fluidframework/telemetry-utils": "2.10.0-307399" | ||
"@fluid-internal/client-utils": "~2.10.0", | ||
"@fluidframework/container-definitions": "~2.10.0", | ||
"@fluidframework/core-interfaces": "~2.10.0", | ||
"@fluidframework/core-utils": "~2.10.0", | ||
"@fluidframework/datastore-definitions": "~2.10.0", | ||
"@fluidframework/driver-definitions": "~2.10.0", | ||
"@fluidframework/runtime-definitions": "~2.10.0", | ||
"@fluidframework/runtime-utils": "~2.10.0", | ||
"@fluidframework/shared-object-base": "~2.10.0", | ||
"@fluidframework/telemetry-utils": "~2.10.0" | ||
}, | ||
@@ -99,5 +99,5 @@ "devDependencies": { | ||
"@biomejs/biome": "~1.9.3", | ||
"@fluid-internal/mocha-test-setup": "2.10.0-307399", | ||
"@fluid-private/stochastic-test-utils": "2.10.0-307399", | ||
"@fluid-private/test-pairwise-generator": "2.10.0-307399", | ||
"@fluid-internal/mocha-test-setup": "~2.10.0", | ||
"@fluid-private/stochastic-test-utils": "~2.10.0", | ||
"@fluid-private/test-pairwise-generator": "~2.10.0", | ||
"@fluid-tools/benchmark": "^0.50.0", | ||
@@ -109,3 +109,3 @@ "@fluid-tools/build-cli": "^0.51.0", | ||
"@fluidframework/merge-tree-previous": "npm:@fluidframework/merge-tree@2.5.0", | ||
"@fluidframework/test-runtime-utils": "2.10.0-307399", | ||
"@fluidframework/test-runtime-utils": "~2.10.0", | ||
"@microsoft/api-extractor": "7.47.8", | ||
@@ -202,2 +202,5 @@ "@types/diff": "^3.5.1", | ||
}, | ||
"Class_Client": { | ||
"forwardCompat": false | ||
}, | ||
"Class_PropertiesManager": { | ||
@@ -204,0 +207,0 @@ "forwardCompat": false, |
@@ -155,3 +155,3 @@ /*! | ||
// Only attribute annotations which change the tracked property | ||
op.props[propName] !== undefined && | ||
(op.props?.[propName] !== undefined || op.adjust?.[propName] !== undefined) && | ||
(isLocal || (propertyDeltas !== undefined && propName in propertyDeltas)); | ||
@@ -158,0 +158,0 @@ |
@@ -57,2 +57,3 @@ /*! | ||
import { | ||
createAdjustRangeOp, | ||
createAnnotateMarkerOp, | ||
@@ -81,5 +82,7 @@ createAnnotateRangeOp, | ||
ReferenceType, | ||
type AdjustParams, | ||
type IMergeTreeAnnotateAdjustMsg, | ||
type IMergeTreeObliterateSidedMsg, | ||
} from "./ops.js"; | ||
import { PropertySet } from "./properties.js"; | ||
import { PropertySet, type MapLike } from "./properties.js"; | ||
import { DetachedReferencePosition, ReferencePosition } from "./referencePositions.js"; | ||
@@ -251,2 +254,22 @@ import { Side, type InteriorSequencePlace } from "./sequencePlace.js"; | ||
/** | ||
* adjusts a value | ||
*/ | ||
public annotateAdjustRangeLocal( | ||
start: number, | ||
end: number, | ||
adjust: MapLike<AdjustParams>, | ||
): 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 | ||
@@ -570,3 +593,3 @@ * | ||
range.end, | ||
op.props, | ||
op, | ||
clientArgs.referenceSequenceNumber, | ||
@@ -693,2 +716,3 @@ clientArgs.clientId, | ||
| IMergeTreeAnnotateMsg | ||
| IMergeTreeAnnotateAdjustMsg | ||
| IMergeTreeInsertMsg | ||
@@ -775,7 +799,3 @@ | IMergeTreeRemoveMsg | ||
| undefined, | ||
): { | ||
clientId: number; | ||
referenceSequenceNumber: number; | ||
sequenceNumber: number; | ||
} { | ||
): IMergeTreeClientSequenceArgs { | ||
// If there this no sequenced message, then the op is local | ||
@@ -916,3 +936,4 @@ // and unacked, so use this clients sequenced args | ||
assert( | ||
segment.propertyManager?.hasPendingProperties(resetOp.props) === true, | ||
segment.propertyManager?.hasPendingProperties(resetOp.props ?? resetOp.adjust) === | ||
true, | ||
0x036 /* "Segment has no pending properties" */, | ||
@@ -931,7 +952,14 @@ ); | ||
) { | ||
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, | ||
); | ||
} | ||
@@ -938,0 +966,0 @@ break; |
@@ -17,5 +17,7 @@ /*! | ||
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"; | ||
@@ -71,2 +73,24 @@ | ||
/** | ||
* 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<AdjustParams>, | ||
): IMergeTreeAnnotateAdjustMsg { | ||
return { | ||
pos1: start, | ||
pos2: end, | ||
adjust: { ...adjust }, | ||
type: MergeTreeDeltaType.ANNOTATE, | ||
}; | ||
} | ||
/** | ||
* Creates the op to remove a range | ||
@@ -73,0 +97,0 @@ * |
@@ -268,2 +268,3 @@ /*! | ||
| IMergeTreeAnnotateMsg | ||
| IMergeTreeAnnotateAdjustMsg | ||
| IMergeTreeObliterateMsg | ||
@@ -270,0 +271,0 @@ | IMergeTreeObliterateSidedMsg; |
@@ -165,3 +165,3 @@ /*! | ||
pending !== undefined, | ||
"Pending changes must exist for rollback when collaborating", | ||
0xa6f /* Pending changes must exist for rollback when collaborating */, | ||
); | ||
@@ -178,3 +178,6 @@ pending.local.pop(); | ||
} else { | ||
assert(pending === undefined, "Pending changes must not exist when not collaborating"); | ||
assert( | ||
pending === undefined, | ||
0xa70 /* Pending changes must not exist when not collaborating */, | ||
); | ||
properties[key] = computePropertyValue(previousValue, [value]); | ||
@@ -263,3 +266,6 @@ } | ||
const acked = change?.local?.shift(); | ||
assert(change !== undefined && acked !== undefined, "must have local change to ack"); | ||
assert( | ||
change !== undefined && acked !== undefined, | ||
0xa71 /* must have local change to ack */, | ||
); | ||
// we only track remotes if there are adjusts, as only adjusts make application anti-commutative | ||
@@ -266,0 +272,0 @@ // this will limit the impact of this change to only those using adjusts. Additionally, we only |
@@ -21,3 +21,3 @@ /*! | ||
import { MergeTree } from "./mergeTree.js"; | ||
import { ISegment } from "./mergeTreeNodes.js"; | ||
import { ISegment, type ISegmentLeaf } from "./mergeTreeNodes.js"; | ||
import { matchProperties } from "./properties.js"; | ||
@@ -197,3 +197,3 @@ import { | ||
const collabWindow = this.mergeTree.collabWindow; | ||
this.seq = collabWindow.minSeq; | ||
const seq = (this.seq = collabWindow.minSeq); | ||
this.header = { | ||
@@ -210,5 +210,5 @@ segmentsTotalLength: this.mergeTree.getLength( | ||
const segs: ISegment[] = []; | ||
let prev: ISegment | undefined; | ||
let prev: ISegmentLeaf | undefined; | ||
const extractSegment = ( | ||
segment: ISegment, | ||
segment: ISegmentLeaf, | ||
pos: number, | ||
@@ -222,16 +222,16 @@ refSeq: number, | ||
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); | ||
} | ||
@@ -243,5 +243,2 @@ } | ||
this.mergeTree.mapRange(extractSegment, this.seq, NonCollabClient, undefined); | ||
if (prev) { | ||
segs.push(prev); | ||
} | ||
@@ -248,0 +245,0 @@ this.segments = []; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
7209151
67570
0
+ Added@fluid-internal/client-utils@2.10.0(transitive)
+ Added@fluidframework/container-definitions@2.10.0(transitive)
+ Added@fluidframework/container-runtime@2.10.0(transitive)
+ Added@fluidframework/container-runtime-definitions@2.10.0(transitive)
+ Added@fluidframework/core-interfaces@2.10.0(transitive)
+ Added@fluidframework/core-utils@2.10.0(transitive)
+ Added@fluidframework/datastore@2.10.0(transitive)
+ Added@fluidframework/datastore-definitions@2.10.0(transitive)
+ Added@fluidframework/driver-definitions@2.10.0(transitive)
+ Added@fluidframework/driver-utils@2.10.0(transitive)
+ Added@fluidframework/id-compressor@2.10.0(transitive)
+ Added@fluidframework/runtime-definitions@2.10.0(transitive)
+ Added@fluidframework/runtime-utils@2.10.0(transitive)
+ Added@fluidframework/shared-object-base@2.10.0(transitive)
+ Added@fluidframework/telemetry-utils@2.10.0(transitive)
- Removed@fluid-internal/client-utils@2.10.0-307399(transitive)
- Removed@fluidframework/container-definitions@2.10.0-307399(transitive)
- Removed@fluidframework/container-runtime@2.10.0-307399(transitive)
- Removed@fluidframework/container-runtime-definitions@2.10.0-307399(transitive)
- Removed@fluidframework/core-interfaces@2.10.0-307399(transitive)
- Removed@fluidframework/core-utils@2.10.0-307399(transitive)
- Removed@fluidframework/datastore@2.10.0-307399(transitive)
- Removed@fluidframework/datastore-definitions@2.10.0-307399(transitive)
- Removed@fluidframework/driver-definitions@2.10.0-307399(transitive)
- Removed@fluidframework/driver-utils@2.10.0-307399(transitive)
- Removed@fluidframework/id-compressor@2.10.0-307399(transitive)
- Removed@fluidframework/runtime-definitions@2.10.0-307399(transitive)
- Removed@fluidframework/runtime-utils@2.10.0-307399(transitive)
- Removed@fluidframework/shared-object-base@2.10.0-307399(transitive)
- Removed@fluidframework/telemetry-utils@2.10.0-307399(transitive)