@fluidframework/sequence 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/dist/defaultMap.d.ts +2 -6
- package/dist/defaultMap.d.ts.map +1 -1
- package/dist/defaultMap.js +27 -37
- package/dist/defaultMap.js.map +1 -1
- package/dist/defaultMapInterfaces.d.ts +24 -3
- package/dist/defaultMapInterfaces.d.ts.map +1 -1
- package/dist/defaultMapInterfaces.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/intervalCollection.d.ts +72 -8
- package/dist/intervalCollection.d.ts.map +1 -1
- package/dist/intervalCollection.js +325 -155
- package/dist/intervalCollection.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/sequence.d.ts +4 -5
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +11 -15
- package/dist/sequence.js.map +1 -1
- package/dist/sharedIntervalCollection.d.ts.map +1 -1
- package/dist/sharedIntervalCollection.js +1 -1
- package/dist/sharedIntervalCollection.js.map +1 -1
- package/dist/sharedSequence.js.map +1 -1
- package/dist/sparsematrix.js +2 -2
- package/dist/sparsematrix.js.map +1 -1
- package/lib/defaultMap.d.ts +2 -6
- package/lib/defaultMap.d.ts.map +1 -1
- package/lib/defaultMap.js +27 -37
- package/lib/defaultMap.js.map +1 -1
- package/lib/defaultMapInterfaces.d.ts +24 -3
- package/lib/defaultMapInterfaces.d.ts.map +1 -1
- package/lib/defaultMapInterfaces.js.map +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/intervalCollection.d.ts +72 -8
- package/lib/intervalCollection.d.ts.map +1 -1
- package/lib/intervalCollection.js +325 -155
- package/lib/intervalCollection.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/sequence.d.ts +4 -5
- package/lib/sequence.d.ts.map +1 -1
- package/lib/sequence.js +11 -15
- package/lib/sequence.js.map +1 -1
- package/lib/sharedIntervalCollection.d.ts.map +1 -1
- package/lib/sharedIntervalCollection.js +1 -1
- package/lib/sharedIntervalCollection.js.map +1 -1
- package/lib/sharedSequence.js.map +1 -1
- package/lib/sparsematrix.js +2 -2
- package/lib/sparsematrix.js.map +1 -1
- package/package.json +20 -44
- package/src/defaultMap.ts +39 -41
- package/src/defaultMapInterfaces.ts +28 -3
- package/src/index.ts +3 -0
- package/src/intervalCollection.ts +447 -181
- package/src/packageVersion.ts +1 -1
- package/src/sequence.ts +17 -21
- package/src/sharedIntervalCollection.ts +3 -2
- package/src/sharedSequence.ts +1 -1
- package/src/sparsematrix.ts +2 -2
|
@@ -30,8 +30,16 @@ import {
|
|
|
30
30
|
UnassignedSequenceNumber,
|
|
31
31
|
} from "@fluidframework/merge-tree";
|
|
32
32
|
import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
|
|
33
|
+
import { LoggingError } from "@fluidframework/telemetry-utils";
|
|
33
34
|
import { v4 as uuid } from "uuid";
|
|
34
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
IMapMessageLocalMetadata,
|
|
37
|
+
IValueFactory,
|
|
38
|
+
IValueOpEmitter,
|
|
39
|
+
IValueOperation,
|
|
40
|
+
IValueType,
|
|
41
|
+
IValueTypeOperationValue,
|
|
42
|
+
} from "./defaultMapInterfaces";
|
|
35
43
|
|
|
36
44
|
const reservedIntervalIdKey = "intervalId";
|
|
37
45
|
|
|
@@ -60,6 +68,55 @@ export interface ISerializedInterval {
|
|
|
60
68
|
properties?: PropertySet;
|
|
61
69
|
}
|
|
62
70
|
|
|
71
|
+
/**
|
|
72
|
+
* A size optimization to avoid redundantly storing keys when serializing intervals
|
|
73
|
+
* as JSON. Intervals are of the format:
|
|
74
|
+
*
|
|
75
|
+
* [start, end, sequenceNumber, intervalType, properties]
|
|
76
|
+
*/
|
|
77
|
+
export type CompressedSerializedInterval = [number, number, number, IntervalType, PropertySet];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
export interface ISerializedIntervalCollectionV2 {
|
|
83
|
+
label: string;
|
|
84
|
+
version: 2;
|
|
85
|
+
intervals: CompressedSerializedInterval[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Decompress an interval after loading a summary from JSON. The exact format
|
|
90
|
+
* of this compression is unspecified and subject to change
|
|
91
|
+
*/
|
|
92
|
+
function decompressInterval(interval: CompressedSerializedInterval, label?: string): ISerializedInterval {
|
|
93
|
+
return {
|
|
94
|
+
start: interval[0],
|
|
95
|
+
end: interval[1],
|
|
96
|
+
sequenceNumber: interval[2],
|
|
97
|
+
intervalType: interval[3],
|
|
98
|
+
properties: { ...interval[4], [reservedRangeLabelsKey]: label },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Compress an interval prior to serialization as JSON. The exact format of this
|
|
104
|
+
* compression is unspecified and subject to change
|
|
105
|
+
*/
|
|
106
|
+
function compressInterval(interval: ISerializedInterval): CompressedSerializedInterval {
|
|
107
|
+
const { start, end, sequenceNumber, intervalType, properties } = interval;
|
|
108
|
+
|
|
109
|
+
return [
|
|
110
|
+
start,
|
|
111
|
+
end,
|
|
112
|
+
sequenceNumber,
|
|
113
|
+
intervalType,
|
|
114
|
+
// remove the `referenceRangeLabels` property as it is already stored
|
|
115
|
+
// in the `label` field of the summary
|
|
116
|
+
{ ...properties, [reservedRangeLabelsKey]: undefined },
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
63
120
|
export interface ISerializableInterval extends IInterval {
|
|
64
121
|
properties: PropertySet;
|
|
65
122
|
propertyManager: PropertiesManager;
|
|
@@ -82,7 +139,11 @@ export class Interval implements ISerializableInterval {
|
|
|
82
139
|
constructor(
|
|
83
140
|
public start: number,
|
|
84
141
|
public end: number,
|
|
85
|
-
props?: PropertySet
|
|
142
|
+
props?: PropertySet,
|
|
143
|
+
) {
|
|
144
|
+
this.propertyManager = new PropertiesManager();
|
|
145
|
+
this.properties = {};
|
|
146
|
+
|
|
86
147
|
if (props) {
|
|
87
148
|
this.addProperties(props);
|
|
88
149
|
}
|
|
@@ -107,7 +168,7 @@ export class Interval implements ISerializableInterval {
|
|
|
107
168
|
this.auxProps.push(props);
|
|
108
169
|
}
|
|
109
170
|
|
|
110
|
-
public serialize(client: Client) {
|
|
171
|
+
public serialize(client: Client): ISerializedInterval {
|
|
111
172
|
let seq = 0;
|
|
112
173
|
if (client) {
|
|
113
174
|
seq = client.getCurrentSeq();
|
|
@@ -181,12 +242,7 @@ export class Interval implements ISerializableInterval {
|
|
|
181
242
|
op?: ICombiningOp,
|
|
182
243
|
): PropertySet | undefined {
|
|
183
244
|
if (newProps) {
|
|
184
|
-
|
|
185
|
-
this.propertyManager = new PropertiesManager();
|
|
186
|
-
}
|
|
187
|
-
if (!this.properties) {
|
|
188
|
-
this.properties = createMap<any>();
|
|
189
|
-
}
|
|
245
|
+
this.initializeProperties();
|
|
190
246
|
return this.propertyManager.addProperties(this.properties, newProps, op, seq, collaborating);
|
|
191
247
|
}
|
|
192
248
|
}
|
|
@@ -198,7 +254,21 @@ export class Interval implements ISerializableInterval {
|
|
|
198
254
|
// Return undefined to indicate that no change is necessary.
|
|
199
255
|
return;
|
|
200
256
|
}
|
|
201
|
-
|
|
257
|
+
const newInterval = new Interval(startPos, endPos);
|
|
258
|
+
if (this.properties) {
|
|
259
|
+
newInterval.initializeProperties();
|
|
260
|
+
this.propertyManager.copyTo(this.properties, newInterval.properties, newInterval.propertyManager);
|
|
261
|
+
}
|
|
262
|
+
return newInterval;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private initializeProperties(): void {
|
|
266
|
+
if (!this.propertyManager) {
|
|
267
|
+
this.propertyManager = new PropertiesManager();
|
|
268
|
+
}
|
|
269
|
+
if (!this.properties) {
|
|
270
|
+
this.properties = createMap<any>();
|
|
271
|
+
}
|
|
202
272
|
}
|
|
203
273
|
}
|
|
204
274
|
|
|
@@ -210,13 +280,49 @@ export class SequenceInterval implements ISerializableInterval {
|
|
|
210
280
|
public start: LocalReference,
|
|
211
281
|
public end: LocalReference,
|
|
212
282
|
public intervalType: IntervalType,
|
|
213
|
-
props?: PropertySet
|
|
283
|
+
props?: PropertySet,
|
|
284
|
+
) {
|
|
285
|
+
this.propertyManager = new PropertiesManager();
|
|
286
|
+
this.properties = {};
|
|
287
|
+
|
|
214
288
|
if (props) {
|
|
215
289
|
this.addProperties(props);
|
|
216
290
|
}
|
|
217
291
|
}
|
|
218
292
|
|
|
219
|
-
|
|
293
|
+
private callbacks?: Record<"beforePositionChange" | "afterPositionChange", () => void>;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @internal
|
|
297
|
+
* Subscribes to position change events on this interval if there are no current listeners.
|
|
298
|
+
*/
|
|
299
|
+
public addPositionChangeListeners(beforePositionChange: () => void, afterPositionChange: () => void): void {
|
|
300
|
+
if (this.callbacks === undefined) {
|
|
301
|
+
this.callbacks = {
|
|
302
|
+
beforePositionChange,
|
|
303
|
+
afterPositionChange,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const startCbs = this.start.callbacks ??= {};
|
|
307
|
+
const endCbs = this.end.callbacks ??= {};
|
|
308
|
+
startCbs.beforeSlide = endCbs.beforeSlide = beforePositionChange;
|
|
309
|
+
startCbs.afterSlide = endCbs.afterSlide = afterPositionChange;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @internal
|
|
315
|
+
* Removes the currently subscribed position change listeners.
|
|
316
|
+
*/
|
|
317
|
+
public removePositionChangeListeners(): void {
|
|
318
|
+
if (this.callbacks) {
|
|
319
|
+
this.callbacks = undefined;
|
|
320
|
+
this.start.callbacks = undefined;
|
|
321
|
+
this.end.callbacks = undefined;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
public serialize(client: Client): ISerializedInterval {
|
|
220
326
|
const startPosition = this.start.toPosition();
|
|
221
327
|
const endPosition = this.end.toPosition();
|
|
222
328
|
const serializedInterval: ISerializedInterval = {
|
|
@@ -225,9 +331,11 @@ export class SequenceInterval implements ISerializableInterval {
|
|
|
225
331
|
sequenceNumber: client.getCurrentSeq(),
|
|
226
332
|
start: startPosition,
|
|
227
333
|
};
|
|
334
|
+
|
|
228
335
|
if (this.properties) {
|
|
229
336
|
serializedInterval.properties = this.properties;
|
|
230
337
|
}
|
|
338
|
+
|
|
231
339
|
return serializedInterval;
|
|
232
340
|
}
|
|
233
341
|
|
|
@@ -290,12 +398,7 @@ export class SequenceInterval implements ISerializableInterval {
|
|
|
290
398
|
seq?: number,
|
|
291
399
|
op?: ICombiningOp,
|
|
292
400
|
): PropertySet | undefined {
|
|
293
|
-
|
|
294
|
-
this.propertyManager = new PropertiesManager();
|
|
295
|
-
}
|
|
296
|
-
if (!this.properties) {
|
|
297
|
-
this.properties = createMap<any>();
|
|
298
|
-
}
|
|
401
|
+
this.initializeProperties();
|
|
299
402
|
return this.propertyManager.addProperties(this.properties, newProps, op, seq, collab);
|
|
300
403
|
}
|
|
301
404
|
|
|
@@ -306,21 +409,46 @@ export class SequenceInterval implements ISerializableInterval {
|
|
|
306
409
|
}
|
|
307
410
|
|
|
308
411
|
public modify(label: string, start: number, end: number, op?: ISequencedDocumentMessage) {
|
|
309
|
-
const
|
|
310
|
-
|
|
412
|
+
const getRefType = (baseType: ReferenceType): ReferenceType => {
|
|
413
|
+
let refType = baseType;
|
|
414
|
+
if (op === undefined) {
|
|
415
|
+
refType &= ~ReferenceType.SlideOnRemove;
|
|
416
|
+
refType |= ReferenceType.StayOnRemove;
|
|
417
|
+
}
|
|
418
|
+
return refType;
|
|
419
|
+
};
|
|
311
420
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
421
|
+
let startRef = this.start;
|
|
422
|
+
if (start !== undefined) {
|
|
423
|
+
startRef = createPositionReference(this.start.getClient(), start, getRefType(this.start.refType), op);
|
|
424
|
+
startRef.addProperties(this.start.properties);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let endRef = this.end;
|
|
428
|
+
if (end !== undefined) {
|
|
429
|
+
endRef = createPositionReference(this.end.getClient(), end, getRefType(this.end.refType), op);
|
|
430
|
+
endRef.addProperties(this.end.properties);
|
|
315
431
|
}
|
|
316
432
|
|
|
317
|
-
|
|
318
|
-
|
|
433
|
+
startRef.pairedRef = endRef;
|
|
434
|
+
endRef.pairedRef = startRef;
|
|
435
|
+
|
|
436
|
+
const newInterval = new SequenceInterval(startRef, endRef, this.intervalType);
|
|
319
437
|
if (this.properties) {
|
|
320
|
-
newInterval.
|
|
438
|
+
newInterval.initializeProperties();
|
|
439
|
+
this.propertyManager.copyTo(this.properties, newInterval.properties, newInterval.propertyManager);
|
|
321
440
|
}
|
|
322
441
|
return newInterval;
|
|
323
442
|
}
|
|
443
|
+
|
|
444
|
+
private initializeProperties(): void {
|
|
445
|
+
if (!this.propertyManager) {
|
|
446
|
+
this.propertyManager = new PropertiesManager();
|
|
447
|
+
}
|
|
448
|
+
if (!this.properties) {
|
|
449
|
+
this.properties = createMap<any>();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
324
452
|
}
|
|
325
453
|
|
|
326
454
|
function createPositionReferenceFromSegoff(
|
|
@@ -421,8 +549,9 @@ export function createIntervalIndex(conflict?: IntervalConflictResolver<Interval
|
|
|
421
549
|
export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
422
550
|
private readonly intervalTree = new IntervalTree<TInterval>();
|
|
423
551
|
private readonly endIntervalTree: RedBlackTree<TInterval, TInterval>;
|
|
424
|
-
private
|
|
425
|
-
private
|
|
552
|
+
private readonly intervalIdMap: Map<string, TInterval> = new Map();
|
|
553
|
+
private conflictResolver: IntervalConflictResolver<TInterval> | undefined;
|
|
554
|
+
private endConflictResolver: ConflictAction<TInterval, TInterval> | undefined;
|
|
426
555
|
|
|
427
556
|
private static readonly legacyIdPrefix = "legacy";
|
|
428
557
|
|
|
@@ -430,6 +559,8 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
430
559
|
private readonly client: Client,
|
|
431
560
|
private readonly label: string,
|
|
432
561
|
private readonly helpers: IIntervalHelpers<TInterval>,
|
|
562
|
+
/** Callback invoked each time one of the endpoints of an interval slides. */
|
|
563
|
+
private readonly onPositionChange?: (interval: TInterval) => void,
|
|
433
564
|
) {
|
|
434
565
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
435
566
|
this.endIntervalTree = new RedBlackTree<TInterval, TInterval>(helpers.compareEnds);
|
|
@@ -439,7 +570,7 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
439
570
|
this.conflictResolver = conflictResolver;
|
|
440
571
|
this.endConflictResolver =
|
|
441
572
|
(key: TInterval, currentKey: TInterval) => {
|
|
442
|
-
const ival =
|
|
573
|
+
const ival = conflictResolver(key, currentKey);
|
|
443
574
|
return {
|
|
444
575
|
data: ival,
|
|
445
576
|
key: ival,
|
|
@@ -457,12 +588,21 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
457
588
|
return `${LocalIntervalCollection.legacyIdPrefix}${start}-${end}`;
|
|
458
589
|
}
|
|
459
590
|
|
|
460
|
-
|
|
461
|
-
|
|
591
|
+
/**
|
|
592
|
+
* Validates that a serialized interval has the ID property. Creates an ID
|
|
593
|
+
* if one does not already exist
|
|
594
|
+
*
|
|
595
|
+
* @param serializedInterval - The interval to be checked
|
|
596
|
+
* @returns The interval's existing or newly created id
|
|
597
|
+
*/
|
|
598
|
+
public ensureSerializedId(serializedInterval: ISerializedInterval): string {
|
|
599
|
+
let id: string | undefined = serializedInterval.properties?.[reservedIntervalIdKey];
|
|
600
|
+
if (id === undefined) {
|
|
462
601
|
// An interval came over the wire without an ID, so create a non-unique one based on start/end.
|
|
463
602
|
// This will allow all clients to refer to this interval consistently.
|
|
603
|
+
id = this.createLegacyId(serializedInterval.start, serializedInterval.end);
|
|
464
604
|
const newProps = {
|
|
465
|
-
[reservedIntervalIdKey]:
|
|
605
|
+
[reservedIntervalIdKey]: id,
|
|
466
606
|
};
|
|
467
607
|
serializedInterval.properties = addProperties(serializedInterval.properties, newProps);
|
|
468
608
|
}
|
|
@@ -472,6 +612,8 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
472
612
|
enumerable: true,
|
|
473
613
|
writable: false,
|
|
474
614
|
});
|
|
615
|
+
|
|
616
|
+
return id;
|
|
475
617
|
}
|
|
476
618
|
|
|
477
619
|
public mapUntil(fn: (interval: TInterval) => boolean) {
|
|
@@ -595,9 +737,20 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
595
737
|
return transientInterval;
|
|
596
738
|
}
|
|
597
739
|
|
|
598
|
-
|
|
740
|
+
private removeIntervalFromIndex(interval: TInterval) {
|
|
599
741
|
this.intervalTree.removeExisting(interval);
|
|
600
742
|
this.endIntervalTree.remove(interval);
|
|
743
|
+
|
|
744
|
+
const id = interval.getIntervalId();
|
|
745
|
+
|
|
746
|
+
assert(id !== undefined, 0x311 /* expected id to exist on interval */);
|
|
747
|
+
|
|
748
|
+
this.intervalIdMap.delete(id);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
public removeExistingInterval(interval: TInterval) {
|
|
752
|
+
this.removeIntervalFromIndex(interval);
|
|
753
|
+
this.removeIntervalListeners(interval);
|
|
601
754
|
}
|
|
602
755
|
|
|
603
756
|
public createInterval(
|
|
@@ -631,9 +784,9 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
631
784
|
return interval;
|
|
632
785
|
}
|
|
633
786
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
787
|
+
private addIntervalToIndex(interval: TInterval) {
|
|
788
|
+
const id = interval.getIntervalId();
|
|
789
|
+
assert(id !== undefined, 0x2c0 /* "ID must be created before adding interval to collection" */);
|
|
637
790
|
// Make the ID immutable.
|
|
638
791
|
Object.defineProperty(interval.properties, reservedIntervalIdKey, {
|
|
639
792
|
configurable: false,
|
|
@@ -642,18 +795,16 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
642
795
|
});
|
|
643
796
|
this.intervalTree.put(interval, this.conflictResolver);
|
|
644
797
|
this.endIntervalTree.put(interval, interval, this.endConflictResolver);
|
|
798
|
+
this.intervalIdMap.set(id, interval);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
public add(interval: TInterval) {
|
|
802
|
+
this.addIntervalToIndex(interval);
|
|
803
|
+
this.addIntervalListeners(interval);
|
|
645
804
|
}
|
|
646
805
|
|
|
647
806
|
public getIntervalById(id: string) {
|
|
648
|
-
|
|
649
|
-
this.mapUntil((interval: TInterval) => {
|
|
650
|
-
if (interval.getIntervalId() === id) {
|
|
651
|
-
result = interval;
|
|
652
|
-
return false;
|
|
653
|
-
}
|
|
654
|
-
return true;
|
|
655
|
-
});
|
|
656
|
-
return result;
|
|
807
|
+
return this.intervalIdMap.get(id);
|
|
657
808
|
}
|
|
658
809
|
|
|
659
810
|
public changeInterval(interval: TInterval, start: number, end: number, op?: ISequencedDocumentMessage) {
|
|
@@ -665,10 +816,33 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
665
816
|
return newInterval;
|
|
666
817
|
}
|
|
667
818
|
|
|
668
|
-
public serialize() {
|
|
819
|
+
public serialize(): ISerializedIntervalCollectionV2 {
|
|
669
820
|
const client = this.client;
|
|
670
821
|
const intervals = this.intervalTree.intervals.keys();
|
|
671
|
-
|
|
822
|
+
|
|
823
|
+
return {
|
|
824
|
+
label: this.label,
|
|
825
|
+
intervals: intervals.map((interval) => compressInterval(interval.serialize(client))),
|
|
826
|
+
version: 2,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private addIntervalListeners(interval: TInterval) {
|
|
831
|
+
if (interval instanceof SequenceInterval) {
|
|
832
|
+
interval.addPositionChangeListeners(
|
|
833
|
+
() => this.removeIntervalFromIndex(interval),
|
|
834
|
+
() => {
|
|
835
|
+
this.addIntervalToIndex(interval);
|
|
836
|
+
this.onPositionChange?.(interval);
|
|
837
|
+
},
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
private removeIntervalListeners(interval: TInterval) {
|
|
843
|
+
if (interval instanceof SequenceInterval) {
|
|
844
|
+
interval.removePositionChangeListeners();
|
|
845
|
+
}
|
|
672
846
|
}
|
|
673
847
|
}
|
|
674
848
|
|
|
@@ -678,7 +852,7 @@ class SequenceIntervalCollectionFactory
|
|
|
678
852
|
implements IValueFactory<IntervalCollection<SequenceInterval>> {
|
|
679
853
|
public load(
|
|
680
854
|
emitter: IValueOpEmitter,
|
|
681
|
-
raw: ISerializedInterval[] = [],
|
|
855
|
+
raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
|
|
682
856
|
): IntervalCollection<SequenceInterval> {
|
|
683
857
|
const helpers: IIntervalHelpers<SequenceInterval> = {
|
|
684
858
|
compareEnds: compareSequenceIntervalEnds,
|
|
@@ -687,7 +861,7 @@ class SequenceIntervalCollectionFactory
|
|
|
687
861
|
return new IntervalCollection<SequenceInterval>(helpers, true, emitter, raw);
|
|
688
862
|
}
|
|
689
863
|
|
|
690
|
-
public store(value: IntervalCollection<SequenceInterval>):
|
|
864
|
+
public store(value: IntervalCollection<SequenceInterval>): ISerializedIntervalCollectionV2 {
|
|
691
865
|
return value.serializeInternal();
|
|
692
866
|
}
|
|
693
867
|
}
|
|
@@ -711,49 +885,27 @@ export class SequenceIntervalCollectionValueType
|
|
|
711
885
|
private static readonly _factory: IValueFactory<IntervalCollection<SequenceInterval>> =
|
|
712
886
|
new SequenceIntervalCollectionFactory();
|
|
713
887
|
|
|
714
|
-
private static readonly _ops
|
|
715
|
-
new Map<string, IValueOperation<IntervalCollection<SequenceInterval>>>(
|
|
716
|
-
[[
|
|
717
|
-
"add",
|
|
718
|
-
{
|
|
719
|
-
process: (value, params, local, op) => {
|
|
720
|
-
value.ackAdd(params, local, op);
|
|
721
|
-
},
|
|
722
|
-
},
|
|
723
|
-
],
|
|
724
|
-
[
|
|
725
|
-
"delete",
|
|
726
|
-
{
|
|
727
|
-
process: (value, params, local, op) => {
|
|
728
|
-
value.ackDelete(params, local, op);
|
|
729
|
-
},
|
|
730
|
-
},
|
|
731
|
-
],
|
|
732
|
-
[
|
|
733
|
-
"change",
|
|
734
|
-
{
|
|
735
|
-
process: (value, params, local, op) => {
|
|
736
|
-
value.ackChange(params, local, op);
|
|
737
|
-
},
|
|
738
|
-
},
|
|
739
|
-
]]);
|
|
888
|
+
private static readonly _ops = makeOpsMap<SequenceInterval>();
|
|
740
889
|
}
|
|
741
890
|
|
|
742
891
|
const compareIntervalEnds = (a: Interval, b: Interval) => a.end - b.end;
|
|
743
892
|
|
|
744
893
|
function createInterval(label: string, start: number, end: number, client: Client): Interval {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
};
|
|
894
|
+
const rangeProp: PropertySet = {};
|
|
895
|
+
|
|
896
|
+
if (label && label.length > 0) {
|
|
897
|
+
rangeProp[reservedRangeLabelsKey] = [label];
|
|
750
898
|
}
|
|
899
|
+
|
|
751
900
|
return new Interval(start, end, rangeProp);
|
|
752
901
|
}
|
|
753
902
|
|
|
754
903
|
class IntervalCollectionFactory
|
|
755
904
|
implements IValueFactory<IntervalCollection<Interval>> {
|
|
756
|
-
public load(
|
|
905
|
+
public load(
|
|
906
|
+
emitter: IValueOpEmitter,
|
|
907
|
+
raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
|
|
908
|
+
): IntervalCollection<Interval> {
|
|
757
909
|
const helpers: IIntervalHelpers<Interval> = {
|
|
758
910
|
compareEnds: compareIntervalEnds,
|
|
759
911
|
create: createInterval,
|
|
@@ -763,7 +915,7 @@ class IntervalCollectionFactory
|
|
|
763
915
|
return collection;
|
|
764
916
|
}
|
|
765
917
|
|
|
766
|
-
public store(value: IntervalCollection<Interval>):
|
|
918
|
+
public store(value: IntervalCollection<Interval>): ISerializedIntervalCollectionV2 {
|
|
767
919
|
return value.serializeInternal();
|
|
768
920
|
}
|
|
769
921
|
}
|
|
@@ -786,32 +938,52 @@ export class IntervalCollectionValueType
|
|
|
786
938
|
|
|
787
939
|
private static readonly _factory: IValueFactory<IntervalCollection<Interval>> =
|
|
788
940
|
new IntervalCollectionFactory();
|
|
789
|
-
private static readonly _ops
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
941
|
+
private static readonly _ops = makeOpsMap<Interval>();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function makeOpsMap<T extends ISerializableInterval>(): Map<string, IValueOperation<IntervalCollection<T>>> {
|
|
945
|
+
const rebase = (
|
|
946
|
+
collection: IntervalCollection<T>,
|
|
947
|
+
op: IValueTypeOperationValue,
|
|
948
|
+
localOpMetadata: IMapMessageLocalMetadata,
|
|
949
|
+
) => {
|
|
950
|
+
const { localSeq } = localOpMetadata;
|
|
951
|
+
const rebasedValue = collection.rebaseLocalInterval(op.opName, op.value, localSeq);
|
|
952
|
+
const rebasedOp = { ...op, value: rebasedValue };
|
|
953
|
+
return { rebasedOp, rebasedLocalOpMetadata: localOpMetadata };
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
return new Map<string, IValueOperation<IntervalCollection<T>>>(
|
|
957
|
+
[[
|
|
958
|
+
"add",
|
|
959
|
+
{
|
|
960
|
+
process: (collection, params, local, op) => {
|
|
961
|
+
collection.ackAdd(params, local, op);
|
|
797
962
|
},
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
963
|
+
rebase,
|
|
964
|
+
},
|
|
965
|
+
],
|
|
966
|
+
[
|
|
967
|
+
"delete",
|
|
968
|
+
{
|
|
969
|
+
process: (collection, params, local, op) => {
|
|
970
|
+
collection.ackDelete(params, local, op);
|
|
805
971
|
},
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
{
|
|
810
|
-
process: (value, params, local, op) => {
|
|
811
|
-
value.ackChange(params, local, op);
|
|
812
|
-
},
|
|
972
|
+
rebase: (collection, op, localOpMetadata) => {
|
|
973
|
+
// Deletion of intervals is based on id, so requires no rebasing.
|
|
974
|
+
return { rebasedOp: op, rebasedLocalOpMetadata: localOpMetadata };
|
|
813
975
|
},
|
|
814
|
-
|
|
976
|
+
},
|
|
977
|
+
],
|
|
978
|
+
[
|
|
979
|
+
"change",
|
|
980
|
+
{
|
|
981
|
+
process: (collection, params, local, op) => {
|
|
982
|
+
collection.ackChange(params, local, op);
|
|
983
|
+
},
|
|
984
|
+
rebase,
|
|
985
|
+
},
|
|
986
|
+
]]);
|
|
815
987
|
}
|
|
816
988
|
|
|
817
989
|
export type DeserializeCallback = (properties: PropertySet) => void;
|
|
@@ -848,7 +1020,18 @@ export class IntervalCollectionIterator<TInterval extends ISerializableInterval>
|
|
|
848
1020
|
}
|
|
849
1021
|
|
|
850
1022
|
export interface IIntervalCollectionEvent<TInterval extends ISerializableInterval> extends IEvent {
|
|
851
|
-
|
|
1023
|
+
/**
|
|
1024
|
+
* This event is invoked whenever the properties or endpoints of an interval may have changed.
|
|
1025
|
+
* This can happen on:
|
|
1026
|
+
* - endpoint modification (local or remote)
|
|
1027
|
+
* - ack of an endpoint modification
|
|
1028
|
+
* - property change (local or remote)
|
|
1029
|
+
* - position change due to segment sliding (will always appear as a local change)
|
|
1030
|
+
* The `interval` argument reflects the new values.
|
|
1031
|
+
*/
|
|
1032
|
+
(event: "changeInterval",
|
|
1033
|
+
listener: (interval: TInterval, local: boolean, op: ISequencedDocumentMessage | undefined) => void);
|
|
1034
|
+
(event: "addInterval" | "deleteInterval",
|
|
852
1035
|
listener: (interval: TInterval, local: boolean, op: ISequencedDocumentMessage) => void);
|
|
853
1036
|
(event: "propertyChanged", listener: (interval: TInterval, propertyArgs: PropertySet) => void);
|
|
854
1037
|
}
|
|
@@ -857,34 +1040,49 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
857
1040
|
extends TypedEventEmitter<IIntervalCollectionEvent<TInterval>> {
|
|
858
1041
|
private savedSerializedIntervals?: ISerializedInterval[];
|
|
859
1042
|
private localCollection: LocalIntervalCollection<TInterval>;
|
|
860
|
-
private onDeserialize: DeserializeCallback;
|
|
1043
|
+
private onDeserialize: DeserializeCallback | undefined;
|
|
861
1044
|
private client: Client;
|
|
862
|
-
private pendingChangesStart: Map<string, ISerializedInterval[]
|
|
863
|
-
private pendingChangesEnd: Map<string, ISerializedInterval[]
|
|
1045
|
+
private readonly pendingChangesStart: Map<string, ISerializedInterval[]> = new Map<string, ISerializedInterval[]>();
|
|
1046
|
+
private readonly pendingChangesEnd: Map<string, ISerializedInterval[]> = new Map<string, ISerializedInterval[]>();
|
|
864
1047
|
|
|
865
1048
|
public get attached(): boolean {
|
|
866
1049
|
return !!this.localCollection;
|
|
867
1050
|
}
|
|
868
1051
|
|
|
869
|
-
|
|
1052
|
+
/** @internal */
|
|
1053
|
+
constructor(
|
|
1054
|
+
private readonly helpers: IIntervalHelpers<TInterval>,
|
|
1055
|
+
private readonly requiresClient: boolean,
|
|
870
1056
|
private readonly emitter: IValueOpEmitter,
|
|
871
|
-
serializedIntervals: ISerializedInterval[]
|
|
1057
|
+
serializedIntervals: ISerializedInterval[] | ISerializedIntervalCollectionV2,
|
|
1058
|
+
) {
|
|
872
1059
|
super();
|
|
873
|
-
|
|
1060
|
+
|
|
1061
|
+
if (Array.isArray(serializedIntervals)) {
|
|
1062
|
+
this.savedSerializedIntervals = serializedIntervals;
|
|
1063
|
+
} else {
|
|
1064
|
+
this.savedSerializedIntervals =
|
|
1065
|
+
serializedIntervals.intervals.map((i) => decompressInterval(i, serializedIntervals.label));
|
|
1066
|
+
}
|
|
874
1067
|
}
|
|
875
1068
|
|
|
876
1069
|
public attachGraph(client: Client, label: string) {
|
|
877
1070
|
if (this.attached) {
|
|
878
|
-
throw new
|
|
1071
|
+
throw new LoggingError("Only supports one Sequence attach");
|
|
879
1072
|
}
|
|
880
1073
|
|
|
881
1074
|
if ((client === undefined) && (this.requiresClient)) {
|
|
882
|
-
throw new
|
|
1075
|
+
throw new LoggingError("Client required for this collection");
|
|
883
1076
|
}
|
|
884
1077
|
|
|
885
1078
|
// Instantiate the local interval collection based on the saved intervals
|
|
886
1079
|
this.client = client;
|
|
887
|
-
this.localCollection = new LocalIntervalCollection<TInterval>(
|
|
1080
|
+
this.localCollection = new LocalIntervalCollection<TInterval>(
|
|
1081
|
+
client,
|
|
1082
|
+
label,
|
|
1083
|
+
this.helpers,
|
|
1084
|
+
(interval) => this.emit("changeInterval", interval, true, undefined),
|
|
1085
|
+
);
|
|
888
1086
|
if (this.savedSerializedIntervals) {
|
|
889
1087
|
for (const serializedInterval of this.savedSerializedIntervals) {
|
|
890
1088
|
this.localCollection.ensureSerializedId(serializedInterval);
|
|
@@ -898,9 +1096,16 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
898
1096
|
this.savedSerializedIntervals = undefined;
|
|
899
1097
|
}
|
|
900
1098
|
|
|
1099
|
+
/**
|
|
1100
|
+
* Gets the next local sequence number, modifying this client's collab window in doing so.
|
|
1101
|
+
*/
|
|
1102
|
+
private getNextLocalSeq(): number {
|
|
1103
|
+
return ++this.client.getCollabWindow().localSeq;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
901
1106
|
public getIntervalById(id: string) {
|
|
902
1107
|
if (!this.attached) {
|
|
903
|
-
throw new
|
|
1108
|
+
throw new LoggingError("attach must be called before accessing intervals");
|
|
904
1109
|
}
|
|
905
1110
|
return this.localCollection.getIntervalById(id);
|
|
906
1111
|
}
|
|
@@ -920,10 +1125,10 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
920
1125
|
props?: PropertySet,
|
|
921
1126
|
) {
|
|
922
1127
|
if (!this.attached) {
|
|
923
|
-
throw new
|
|
1128
|
+
throw new LoggingError("attach must be called prior to adding intervals");
|
|
924
1129
|
}
|
|
925
1130
|
if (intervalType & IntervalType.Transient) {
|
|
926
|
-
throw new
|
|
1131
|
+
throw new LoggingError("Can not add transient intervals");
|
|
927
1132
|
}
|
|
928
1133
|
|
|
929
1134
|
const interval: TInterval = this.localCollection.addInterval(start, end, intervalType, props);
|
|
@@ -937,7 +1142,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
937
1142
|
start,
|
|
938
1143
|
};
|
|
939
1144
|
// Local ops get submitted to the server. Remote ops have the deserializer run.
|
|
940
|
-
this.emitter.emit("add", undefined, serializedInterval);
|
|
1145
|
+
this.emitter.emit("add", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
|
|
941
1146
|
}
|
|
942
1147
|
|
|
943
1148
|
this.emit("addInterval", interval, true, undefined);
|
|
@@ -945,13 +1150,19 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
945
1150
|
return interval;
|
|
946
1151
|
}
|
|
947
1152
|
|
|
948
|
-
private deleteExistingInterval(interval: TInterval, local: boolean, op
|
|
1153
|
+
private deleteExistingInterval(interval: TInterval, local: boolean, op?: ISequencedDocumentMessage) {
|
|
949
1154
|
// The given interval is known to exist in the collection.
|
|
950
1155
|
this.localCollection.removeExistingInterval(interval);
|
|
1156
|
+
|
|
951
1157
|
if (interval) {
|
|
952
1158
|
// Local ops get submitted to the server. Remote ops have the deserializer run.
|
|
953
1159
|
if (local) {
|
|
954
|
-
this.emitter.emit(
|
|
1160
|
+
this.emitter.emit(
|
|
1161
|
+
"delete",
|
|
1162
|
+
undefined,
|
|
1163
|
+
interval.serialize(this.client),
|
|
1164
|
+
{ localSeq: this.getNextLocalSeq() },
|
|
1165
|
+
);
|
|
955
1166
|
} else {
|
|
956
1167
|
if (this.onDeserialize) {
|
|
957
1168
|
this.onDeserialize(interval);
|
|
@@ -972,13 +1183,13 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
972
1183
|
|
|
973
1184
|
public changeProperties(id: string, props: PropertySet) {
|
|
974
1185
|
if (!this.attached) {
|
|
975
|
-
throw new
|
|
1186
|
+
throw new LoggingError("Attach must be called before accessing intervals");
|
|
976
1187
|
}
|
|
977
1188
|
if (typeof (id) !== "string") {
|
|
978
|
-
throw new
|
|
1189
|
+
throw new LoggingError("Change API requires an ID that is a string");
|
|
979
1190
|
}
|
|
980
1191
|
if (!props) {
|
|
981
|
-
throw new
|
|
1192
|
+
throw new LoggingError("changeProperties should be called with a property set");
|
|
982
1193
|
}
|
|
983
1194
|
|
|
984
1195
|
const interval = this.getIntervalById(id);
|
|
@@ -986,12 +1197,15 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
986
1197
|
// Pass Unassigned as the sequence number to indicate that this is a local op that is waiting for an ack.
|
|
987
1198
|
const deltaProps = interval.addProperties(props, true, UnassignedSequenceNumber);
|
|
988
1199
|
const serializedInterval: ISerializedInterval = interval.serialize(this.client);
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1200
|
+
|
|
1201
|
+
// Emit a change op that will only change properties. Add the ID to
|
|
1202
|
+
// the property bag provided by the caller.
|
|
1203
|
+
serializedInterval.start = undefined as any;
|
|
1204
|
+
serializedInterval.end = undefined as any;
|
|
1205
|
+
|
|
992
1206
|
serializedInterval.properties = props;
|
|
993
1207
|
serializedInterval.properties[reservedIntervalIdKey] = interval.getIntervalId();
|
|
994
|
-
this.emitter.emit("change", undefined, serializedInterval);
|
|
1208
|
+
this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
|
|
995
1209
|
this.emit("propertyChanged", interval, deltaProps);
|
|
996
1210
|
}
|
|
997
1211
|
this.emit("changeInterval", interval, true, undefined);
|
|
@@ -999,42 +1213,39 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
999
1213
|
|
|
1000
1214
|
public change(id: string, start?: number, end?: number): TInterval | undefined {
|
|
1001
1215
|
if (!this.attached) {
|
|
1002
|
-
throw new
|
|
1216
|
+
throw new LoggingError("Attach must be called before accessing intervals");
|
|
1003
1217
|
}
|
|
1218
|
+
|
|
1219
|
+
// Force id to be a string.
|
|
1004
1220
|
if (typeof (id) !== "string") {
|
|
1005
|
-
throw new
|
|
1221
|
+
throw new LoggingError("Change API requires an ID that is a string");
|
|
1006
1222
|
}
|
|
1007
1223
|
|
|
1008
|
-
// Force id to be a string.
|
|
1009
1224
|
const interval = this.getIntervalById(id);
|
|
1010
1225
|
if (interval) {
|
|
1011
|
-
this.localCollection.changeInterval(interval, start, end);
|
|
1226
|
+
const newInterval = this.localCollection.changeInterval(interval, start, end);
|
|
1012
1227
|
const serializedInterval: ISerializedInterval = interval.serialize(this.client);
|
|
1013
1228
|
serializedInterval.start = start;
|
|
1014
1229
|
serializedInterval.end = end;
|
|
1015
1230
|
// Emit a property bag containing only the ID, as we don't intend for this op to change any properties.
|
|
1016
1231
|
serializedInterval.properties =
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
this.emitter.emit("change", undefined, serializedInterval);
|
|
1232
|
+
{
|
|
1233
|
+
[reservedIntervalIdKey]: interval.getIntervalId(),
|
|
1234
|
+
};
|
|
1235
|
+
this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
|
|
1021
1236
|
this.addPendingChange(id, serializedInterval);
|
|
1237
|
+
this.emit("changeInterval", newInterval, true, undefined);
|
|
1238
|
+
return newInterval;
|
|
1022
1239
|
}
|
|
1023
|
-
|
|
1024
|
-
return
|
|
1240
|
+
// No interval to change
|
|
1241
|
+
return undefined;
|
|
1025
1242
|
}
|
|
1026
1243
|
|
|
1027
1244
|
private addPendingChange(id: string, serializedInterval: ISerializedInterval) {
|
|
1028
1245
|
if (serializedInterval.start !== undefined) {
|
|
1029
|
-
if (!this.pendingChangesStart) {
|
|
1030
|
-
this.pendingChangesStart = new Map<string, ISerializedInterval[]>();
|
|
1031
|
-
}
|
|
1032
1246
|
this.addPendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
|
|
1033
1247
|
}
|
|
1034
1248
|
if (serializedInterval.end !== undefined) {
|
|
1035
|
-
if (!this.pendingChangesEnd) {
|
|
1036
|
-
this.pendingChangesEnd = new Map<string, ISerializedInterval[]>();
|
|
1037
|
-
}
|
|
1038
1249
|
this.addPendingChangeHelper(id, this.pendingChangesEnd, serializedInterval);
|
|
1039
1250
|
}
|
|
1040
1251
|
}
|
|
@@ -1044,7 +1255,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1044
1255
|
pendingChanges: Map<string, ISerializedInterval[]>,
|
|
1045
1256
|
serializedInterval: ISerializedInterval,
|
|
1046
1257
|
) {
|
|
1047
|
-
let entries: ISerializedInterval[] = pendingChanges.get(id);
|
|
1258
|
+
let entries: ISerializedInterval[] | undefined = pendingChanges.get(id);
|
|
1048
1259
|
if (!entries) {
|
|
1049
1260
|
entries = [];
|
|
1050
1261
|
pendingChanges.set(id, entries);
|
|
@@ -1054,7 +1265,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1054
1265
|
|
|
1055
1266
|
private removePendingChange(serializedInterval: ISerializedInterval) {
|
|
1056
1267
|
// Change ops always have an ID.
|
|
1057
|
-
const id: string = serializedInterval.properties[reservedIntervalIdKey];
|
|
1268
|
+
const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
|
|
1058
1269
|
if (serializedInterval.start !== undefined) {
|
|
1059
1270
|
this.removePendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
|
|
1060
1271
|
}
|
|
@@ -1068,26 +1279,26 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1068
1279
|
pendingChanges: Map<string, ISerializedInterval[]>,
|
|
1069
1280
|
serializedInterval: ISerializedInterval,
|
|
1070
1281
|
) {
|
|
1071
|
-
const entries = pendingChanges
|
|
1282
|
+
const entries = pendingChanges.get(id);
|
|
1072
1283
|
if (entries) {
|
|
1073
1284
|
const pendingChange = entries.shift();
|
|
1074
1285
|
if (entries.length === 0) {
|
|
1075
1286
|
pendingChanges.delete(id);
|
|
1076
1287
|
}
|
|
1077
|
-
if (pendingChange
|
|
1078
|
-
pendingChange
|
|
1079
|
-
throw new
|
|
1288
|
+
if (pendingChange?.start !== serializedInterval.start ||
|
|
1289
|
+
pendingChange?.end !== serializedInterval.end) {
|
|
1290
|
+
throw new LoggingError("Mismatch in pending changes");
|
|
1080
1291
|
}
|
|
1081
1292
|
}
|
|
1082
1293
|
}
|
|
1083
1294
|
|
|
1084
1295
|
private hasPendingChangeStart(id: string) {
|
|
1085
|
-
const entries = this.pendingChangesStart
|
|
1296
|
+
const entries = this.pendingChangesStart.get(id);
|
|
1086
1297
|
return entries && entries.length !== 0;
|
|
1087
1298
|
}
|
|
1088
1299
|
|
|
1089
1300
|
private hasPendingChangeEnd(id: string) {
|
|
1090
|
-
const entries = this.pendingChangesEnd
|
|
1301
|
+
const entries = this.pendingChangesEnd.get(id);
|
|
1091
1302
|
return entries && entries.length !== 0;
|
|
1092
1303
|
}
|
|
1093
1304
|
|
|
@@ -1099,7 +1310,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1099
1310
|
/** @internal */
|
|
1100
1311
|
public ackChange(serializedInterval: ISerializedInterval, local: boolean, op: ISequencedDocumentMessage) {
|
|
1101
1312
|
if (!this.attached) {
|
|
1102
|
-
throw new
|
|
1313
|
+
throw new LoggingError("Attach must be called before accessing intervals");
|
|
1103
1314
|
}
|
|
1104
1315
|
|
|
1105
1316
|
let interval: TInterval | undefined;
|
|
@@ -1107,15 +1318,14 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1107
1318
|
if (local) {
|
|
1108
1319
|
// This is an ack from the server. Remove the pending change.
|
|
1109
1320
|
this.removePendingChange(serializedInterval);
|
|
1110
|
-
const id: string = serializedInterval.properties[reservedIntervalIdKey];
|
|
1111
|
-
// Could store the interval in the localOpMetadata to avoid the getIntervalById call
|
|
1321
|
+
const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
|
|
1112
1322
|
interval = this.getIntervalById(id);
|
|
1113
1323
|
if (interval) {
|
|
1114
1324
|
// Let the propertyManager prune its pending change-properties set.
|
|
1115
1325
|
interval.propertyManager?.ackPendingProperties(
|
|
1116
1326
|
{
|
|
1117
1327
|
type: MergeTreeDeltaType.ANNOTATE,
|
|
1118
|
-
props: serializedInterval.properties,
|
|
1328
|
+
props: serializedInterval.properties ?? {},
|
|
1119
1329
|
});
|
|
1120
1330
|
|
|
1121
1331
|
this.ackInterval(interval, op);
|
|
@@ -1157,7 +1367,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1157
1367
|
|
|
1158
1368
|
public addConflictResolver(conflictResolver: IntervalConflictResolver<TInterval>): void {
|
|
1159
1369
|
if (!this.attached) {
|
|
1160
|
-
throw new
|
|
1370
|
+
throw new LoggingError("attachSequence must be called");
|
|
1161
1371
|
}
|
|
1162
1372
|
this.localCollection.addConflictResolver(conflictResolver);
|
|
1163
1373
|
}
|
|
@@ -1173,15 +1383,46 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1173
1383
|
|
|
1174
1384
|
// Trigger the async prepare work across all values in the collection
|
|
1175
1385
|
this.localCollection.map((interval) => {
|
|
1176
|
-
|
|
1386
|
+
onDeserialize(interval);
|
|
1177
1387
|
});
|
|
1178
1388
|
}
|
|
1179
1389
|
|
|
1390
|
+
/** @internal */
|
|
1391
|
+
public rebaseLocalInterval(
|
|
1392
|
+
opName: string,
|
|
1393
|
+
serializedInterval: ISerializedInterval,
|
|
1394
|
+
localSeq: number,
|
|
1395
|
+
) {
|
|
1396
|
+
if (!this.attached) {
|
|
1397
|
+
throw new LoggingError("attachSequence must be called");
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const { start, end, intervalType, properties, sequenceNumber } = serializedInterval;
|
|
1401
|
+
const startRebased = start === undefined ? undefined :
|
|
1402
|
+
this.client.rebasePosition(start, sequenceNumber, localSeq);
|
|
1403
|
+
const endRebased = end === undefined ? undefined :
|
|
1404
|
+
this.client.rebasePosition(end, sequenceNumber, localSeq);
|
|
1405
|
+
|
|
1406
|
+
const intervalId = properties?.[reservedIntervalIdKey];
|
|
1407
|
+
const rebased: ISerializedInterval = {
|
|
1408
|
+
start: startRebased,
|
|
1409
|
+
end: endRebased,
|
|
1410
|
+
intervalType,
|
|
1411
|
+
sequenceNumber: this.client?.getCurrentSeq() ?? 0,
|
|
1412
|
+
properties,
|
|
1413
|
+
};
|
|
1414
|
+
if (opName === "change" && (this.hasPendingChangeStart(intervalId) || this.hasPendingChangeEnd(intervalId))) {
|
|
1415
|
+
this.removePendingChange(serializedInterval);
|
|
1416
|
+
this.addPendingChange(intervalId, rebased);
|
|
1417
|
+
}
|
|
1418
|
+
return rebased;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1180
1421
|
private getSlideToSegment(lref: LocalReference) {
|
|
1181
1422
|
const segoff = { segment: lref.segment, offset: lref.offset };
|
|
1182
1423
|
const newSegoff = this.client.getSlideToSegment(segoff);
|
|
1183
1424
|
const value: { segment: ISegment | undefined; offset: number | undefined; } | undefined
|
|
1184
|
-
= (segoff === newSegoff) ? undefined : newSegoff;
|
|
1425
|
+
= (segoff.segment === newSegoff.segment && segoff.offset === newSegoff.offset) ? undefined : newSegoff;
|
|
1185
1426
|
return value;
|
|
1186
1427
|
}
|
|
1187
1428
|
|
|
@@ -1198,27 +1439,50 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1198
1439
|
return;
|
|
1199
1440
|
}
|
|
1200
1441
|
|
|
1201
|
-
if (!refTypeIncludesFlag(interval.start, ReferenceType.StayOnRemove)
|
|
1442
|
+
if (!refTypeIncludesFlag(interval.start, ReferenceType.StayOnRemove) &&
|
|
1443
|
+
!refTypeIncludesFlag(interval.end, ReferenceType.StayOnRemove)) {
|
|
1202
1444
|
return;
|
|
1203
1445
|
}
|
|
1204
|
-
|
|
1205
|
-
0x2f7 /* start and end must both be StayOnRemove */);
|
|
1446
|
+
|
|
1206
1447
|
const newStart = this.getSlideToSegment(interval.start);
|
|
1207
1448
|
const newEnd = this.getSlideToSegment(interval.end);
|
|
1208
|
-
this.setSlideOnRemove(interval.start);
|
|
1209
|
-
this.setSlideOnRemove(interval.end);
|
|
1210
1449
|
|
|
1211
|
-
|
|
1450
|
+
const id = interval.properties[reservedIntervalIdKey];
|
|
1451
|
+
const hasPendingStartChange = this.hasPendingChangeStart(id);
|
|
1452
|
+
const hasPendingEndChange = this.hasPendingChangeEnd(id);
|
|
1453
|
+
|
|
1454
|
+
if (!hasPendingStartChange) {
|
|
1455
|
+
this.setSlideOnRemove(interval.start);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
if (!hasPendingEndChange) {
|
|
1459
|
+
this.setSlideOnRemove(interval.end);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const needsStartUpdate = newStart !== undefined && !hasPendingStartChange;
|
|
1463
|
+
const needsEndUpdate = newEnd !== undefined && !hasPendingEndChange;
|
|
1464
|
+
|
|
1465
|
+
if (needsStartUpdate || needsEndUpdate) {
|
|
1466
|
+
// In this case, where we change the start or end of an interval,
|
|
1467
|
+
// it is necessary to remove and re-add the interval listeners.
|
|
1468
|
+
// This ensures that the correct listeners are added to the ReferencePosition.
|
|
1212
1469
|
this.localCollection.removeExistingInterval(interval);
|
|
1213
|
-
|
|
1470
|
+
|
|
1471
|
+
if (needsStartUpdate) {
|
|
1214
1472
|
const props = interval.start.properties;
|
|
1473
|
+
this.client.removeLocalReferencePosition(interval.start);
|
|
1215
1474
|
interval.start = createPositionReferenceFromSegoff(this.client, newStart, interval.start.refType, op);
|
|
1216
|
-
|
|
1475
|
+
if (props) {
|
|
1476
|
+
interval.start.addProperties(props);
|
|
1477
|
+
}
|
|
1217
1478
|
}
|
|
1218
|
-
if (
|
|
1479
|
+
if (needsEndUpdate) {
|
|
1219
1480
|
const props = interval.end.properties;
|
|
1481
|
+
this.client.removeLocalReferencePosition(interval.end);
|
|
1220
1482
|
interval.end = createPositionReferenceFromSegoff(this.client, newEnd, interval.end.refType, op);
|
|
1221
|
-
|
|
1483
|
+
if (props) {
|
|
1484
|
+
interval.end.addProperties(props);
|
|
1485
|
+
}
|
|
1222
1486
|
}
|
|
1223
1487
|
this.localCollection.add(interval);
|
|
1224
1488
|
}
|
|
@@ -1238,8 +1502,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1238
1502
|
local: boolean,
|
|
1239
1503
|
op: ISequencedDocumentMessage) {
|
|
1240
1504
|
if (local) {
|
|
1241
|
-
const id: string = serializedInterval.properties[reservedIntervalIdKey];
|
|
1242
|
-
// Could store the interval in the localOpMetadata to avoid the getIntervalById call
|
|
1505
|
+
const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
|
|
1243
1506
|
const localInterval = this.getIntervalById(id);
|
|
1244
1507
|
if (localInterval) {
|
|
1245
1508
|
this.ackInterval(localInterval, op);
|
|
@@ -1248,7 +1511,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1248
1511
|
}
|
|
1249
1512
|
|
|
1250
1513
|
if (!this.attached) {
|
|
1251
|
-
throw new
|
|
1514
|
+
throw new LoggingError("attachSequence must be called");
|
|
1252
1515
|
}
|
|
1253
1516
|
|
|
1254
1517
|
this.localCollection.ensureSerializedId(serializedInterval);
|
|
@@ -1292,19 +1555,22 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1292
1555
|
}
|
|
1293
1556
|
|
|
1294
1557
|
if (!this.attached) {
|
|
1295
|
-
throw new
|
|
1558
|
+
throw new LoggingError("attach must be called prior to deleting intervals");
|
|
1296
1559
|
}
|
|
1297
1560
|
|
|
1298
|
-
this.localCollection.ensureSerializedId(serializedInterval);
|
|
1299
|
-
const interval = this.localCollection.getIntervalById(
|
|
1561
|
+
const id = this.localCollection.ensureSerializedId(serializedInterval);
|
|
1562
|
+
const interval = this.localCollection.getIntervalById(id);
|
|
1300
1563
|
if (interval) {
|
|
1301
1564
|
this.deleteExistingInterval(interval, local, op);
|
|
1302
1565
|
}
|
|
1303
1566
|
}
|
|
1304
1567
|
|
|
1305
|
-
|
|
1568
|
+
/**
|
|
1569
|
+
* @internal
|
|
1570
|
+
*/
|
|
1571
|
+
public serializeInternal(): ISerializedIntervalCollectionV2 {
|
|
1306
1572
|
if (!this.attached) {
|
|
1307
|
-
throw new
|
|
1573
|
+
throw new LoggingError("attachSequence must be called");
|
|
1308
1574
|
}
|
|
1309
1575
|
|
|
1310
1576
|
return this.localCollection.serialize();
|
|
@@ -1349,7 +1615,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1349
1615
|
|
|
1350
1616
|
public findOverlappingIntervals(startPosition: number, endPosition: number): TInterval[] {
|
|
1351
1617
|
if (!this.attached) {
|
|
1352
|
-
throw new
|
|
1618
|
+
throw new LoggingError("attachSequence must be called");
|
|
1353
1619
|
}
|
|
1354
1620
|
|
|
1355
1621
|
return this.localCollection.findOverlappingIntervals(startPosition, endPosition);
|
|
@@ -1357,7 +1623,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1357
1623
|
|
|
1358
1624
|
public map(fn: (interval: TInterval) => void) {
|
|
1359
1625
|
if (!this.attached) {
|
|
1360
|
-
throw new
|
|
1626
|
+
throw new LoggingError("attachSequence must be called");
|
|
1361
1627
|
}
|
|
1362
1628
|
|
|
1363
1629
|
this.localCollection.map(fn);
|
|
@@ -1365,7 +1631,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1365
1631
|
|
|
1366
1632
|
public previousInterval(pos: number): TInterval {
|
|
1367
1633
|
if (!this.attached) {
|
|
1368
|
-
throw new
|
|
1634
|
+
throw new LoggingError("attachSequence must be called");
|
|
1369
1635
|
}
|
|
1370
1636
|
|
|
1371
1637
|
return this.localCollection.previousInterval(pos);
|
|
@@ -1373,7 +1639,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1373
1639
|
|
|
1374
1640
|
public nextInterval(pos: number): TInterval {
|
|
1375
1641
|
if (!this.attached) {
|
|
1376
|
-
throw new
|
|
1642
|
+
throw new LoggingError("attachSequence must be called");
|
|
1377
1643
|
}
|
|
1378
1644
|
|
|
1379
1645
|
return this.localCollection.nextInterval(pos);
|