@fluidframework/sequence 0.59.4001 → 1.1.0-75972
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/.eslintrc.js +1 -1
- 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 +93 -8
- package/dist/intervalCollection.d.ts.map +1 -1
- package/dist/intervalCollection.js +434 -178
- package/dist/intervalCollection.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/sequence.d.ts +6 -7
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +17 -21
- package/dist/sequence.js.map +1 -1
- package/dist/sharedIntervalCollection.d.ts.map +1 -1
- package/dist/sharedIntervalCollection.js +2 -2
- 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 +93 -8
- package/lib/intervalCollection.d.ts.map +1 -1
- package/lib/intervalCollection.js +435 -179
- package/lib/intervalCollection.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/sequence.d.ts +6 -7
- package/lib/sequence.d.ts.map +1 -1
- package/lib/sequence.js +18 -22
- package/lib/sequence.js.map +1 -1
- package/lib/sharedIntervalCollection.d.ts.map +1 -1
- package/lib/sharedIntervalCollection.js +2 -2
- 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 +23 -45
- package/src/defaultMap.ts +39 -41
- package/src/defaultMapInterfaces.ts +28 -3
- package/src/index.ts +3 -0
- package/src/intervalCollection.ts +575 -211
- package/src/packageVersion.ts +1 -1
- package/src/sequence.ts +36 -38
- package/src/sharedIntervalCollection.ts +4 -3
- package/src/sharedSequence.ts +1 -1
- package/src/sparsematrix.ts +2 -2
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
|
|
9
9
|
import { IEvent } from "@fluidframework/common-definitions";
|
|
10
|
+
import { UsageError } from "@fluidframework/container-utils";
|
|
10
11
|
import {
|
|
11
12
|
addProperties,
|
|
12
13
|
Client,
|
|
@@ -17,25 +18,45 @@ import {
|
|
|
17
18
|
IntervalConflictResolver,
|
|
18
19
|
IntervalNode,
|
|
19
20
|
IntervalTree,
|
|
21
|
+
ISegment,
|
|
20
22
|
LocalReference,
|
|
21
23
|
MergeTreeDeltaType,
|
|
22
24
|
PropertiesManager,
|
|
23
25
|
PropertySet,
|
|
24
26
|
RedBlackTree,
|
|
25
27
|
ReferenceType,
|
|
28
|
+
refTypeIncludesFlag,
|
|
26
29
|
reservedRangeLabelsKey,
|
|
27
30
|
UnassignedSequenceNumber,
|
|
28
31
|
} from "@fluidframework/merge-tree";
|
|
29
32
|
import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
|
|
33
|
+
import { LoggingError } from "@fluidframework/telemetry-utils";
|
|
30
34
|
import { v4 as uuid } from "uuid";
|
|
31
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
IMapMessageLocalMetadata,
|
|
37
|
+
IValueFactory,
|
|
38
|
+
IValueOpEmitter,
|
|
39
|
+
IValueOperation,
|
|
40
|
+
IValueType,
|
|
41
|
+
IValueTypeOperationValue,
|
|
42
|
+
} from "./defaultMapInterfaces";
|
|
32
43
|
|
|
33
44
|
const reservedIntervalIdKey = "intervalId";
|
|
34
45
|
|
|
35
46
|
export enum IntervalType {
|
|
36
47
|
Simple = 0x0,
|
|
37
48
|
Nest = 0x1,
|
|
38
|
-
|
|
49
|
+
/**
|
|
50
|
+
* SlideOnRemove indicates that the ends of the interval will slide if the segment
|
|
51
|
+
* they reference is removed and acked.
|
|
52
|
+
* See `packages\dds\merge-tree\REFERENCEPOSITIONS.md` for details
|
|
53
|
+
* SlideOnRemove is the default interval behavior and does not need to be specified.
|
|
54
|
+
*/
|
|
55
|
+
SlideOnRemove = 0x2, // SlideOnRemove is default behavior - all intervals are SlideOnRemove
|
|
56
|
+
/**
|
|
57
|
+
* @internal
|
|
58
|
+
* A temporary interval, used internally
|
|
59
|
+
*/
|
|
39
60
|
Transient = 0x4,
|
|
40
61
|
}
|
|
41
62
|
|
|
@@ -47,6 +68,55 @@ export interface ISerializedInterval {
|
|
|
47
68
|
properties?: PropertySet;
|
|
48
69
|
}
|
|
49
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
|
+
|
|
50
120
|
export interface ISerializableInterval extends IInterval {
|
|
51
121
|
properties: PropertySet;
|
|
52
122
|
propertyManager: PropertiesManager;
|
|
@@ -69,7 +139,11 @@ export class Interval implements ISerializableInterval {
|
|
|
69
139
|
constructor(
|
|
70
140
|
public start: number,
|
|
71
141
|
public end: number,
|
|
72
|
-
props?: PropertySet
|
|
142
|
+
props?: PropertySet,
|
|
143
|
+
) {
|
|
144
|
+
this.propertyManager = new PropertiesManager();
|
|
145
|
+
this.properties = {};
|
|
146
|
+
|
|
73
147
|
if (props) {
|
|
74
148
|
this.addProperties(props);
|
|
75
149
|
}
|
|
@@ -94,7 +168,7 @@ export class Interval implements ISerializableInterval {
|
|
|
94
168
|
this.auxProps.push(props);
|
|
95
169
|
}
|
|
96
170
|
|
|
97
|
-
public serialize(client: Client) {
|
|
171
|
+
public serialize(client: Client): ISerializedInterval {
|
|
98
172
|
let seq = 0;
|
|
99
173
|
if (client) {
|
|
100
174
|
seq = client.getCurrentSeq();
|
|
@@ -168,12 +242,7 @@ export class Interval implements ISerializableInterval {
|
|
|
168
242
|
op?: ICombiningOp,
|
|
169
243
|
): PropertySet | undefined {
|
|
170
244
|
if (newProps) {
|
|
171
|
-
|
|
172
|
-
this.propertyManager = new PropertiesManager();
|
|
173
|
-
}
|
|
174
|
-
if (!this.properties) {
|
|
175
|
-
this.properties = createMap<any>();
|
|
176
|
-
}
|
|
245
|
+
this.initializeProperties();
|
|
177
246
|
return this.propertyManager.addProperties(this.properties, newProps, op, seq, collaborating);
|
|
178
247
|
}
|
|
179
248
|
}
|
|
@@ -185,7 +254,21 @@ export class Interval implements ISerializableInterval {
|
|
|
185
254
|
// Return undefined to indicate that no change is necessary.
|
|
186
255
|
return;
|
|
187
256
|
}
|
|
188
|
-
|
|
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
|
+
}
|
|
189
272
|
}
|
|
190
273
|
}
|
|
191
274
|
|
|
@@ -197,13 +280,49 @@ export class SequenceInterval implements ISerializableInterval {
|
|
|
197
280
|
public start: LocalReference,
|
|
198
281
|
public end: LocalReference,
|
|
199
282
|
public intervalType: IntervalType,
|
|
200
|
-
props?: PropertySet
|
|
283
|
+
props?: PropertySet,
|
|
284
|
+
) {
|
|
285
|
+
this.propertyManager = new PropertiesManager();
|
|
286
|
+
this.properties = {};
|
|
287
|
+
|
|
201
288
|
if (props) {
|
|
202
289
|
this.addProperties(props);
|
|
203
290
|
}
|
|
204
291
|
}
|
|
205
292
|
|
|
206
|
-
|
|
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 {
|
|
207
326
|
const startPosition = this.start.toPosition();
|
|
208
327
|
const endPosition = this.end.toPosition();
|
|
209
328
|
const serializedInterval: ISerializedInterval = {
|
|
@@ -212,9 +331,11 @@ export class SequenceInterval implements ISerializableInterval {
|
|
|
212
331
|
sequenceNumber: client.getCurrentSeq(),
|
|
213
332
|
start: startPosition,
|
|
214
333
|
};
|
|
334
|
+
|
|
215
335
|
if (this.properties) {
|
|
216
336
|
serializedInterval.properties = this.properties;
|
|
217
337
|
}
|
|
338
|
+
|
|
218
339
|
return serializedInterval;
|
|
219
340
|
}
|
|
220
341
|
|
|
@@ -277,12 +398,7 @@ export class SequenceInterval implements ISerializableInterval {
|
|
|
277
398
|
seq?: number,
|
|
278
399
|
op?: ICombiningOp,
|
|
279
400
|
): PropertySet | undefined {
|
|
280
|
-
|
|
281
|
-
this.propertyManager = new PropertiesManager();
|
|
282
|
-
}
|
|
283
|
-
if (!this.properties) {
|
|
284
|
-
this.properties = createMap<any>();
|
|
285
|
-
}
|
|
401
|
+
this.initializeProperties();
|
|
286
402
|
return this.propertyManager.addProperties(this.properties, newProps, op, seq, collab);
|
|
287
403
|
}
|
|
288
404
|
|
|
@@ -293,21 +409,62 @@ export class SequenceInterval implements ISerializableInterval {
|
|
|
293
409
|
}
|
|
294
410
|
|
|
295
411
|
public modify(label: string, start: number, end: number, op?: ISequencedDocumentMessage) {
|
|
296
|
-
const
|
|
297
|
-
|
|
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
|
+
};
|
|
298
420
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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);
|
|
302
425
|
}
|
|
303
426
|
|
|
304
|
-
|
|
305
|
-
|
|
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);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
startRef.pairedRef = endRef;
|
|
434
|
+
endRef.pairedRef = startRef;
|
|
435
|
+
|
|
436
|
+
const newInterval = new SequenceInterval(startRef, endRef, this.intervalType);
|
|
306
437
|
if (this.properties) {
|
|
307
|
-
newInterval.
|
|
438
|
+
newInterval.initializeProperties();
|
|
439
|
+
this.propertyManager.copyTo(this.properties, newInterval.properties, newInterval.propertyManager);
|
|
308
440
|
}
|
|
309
441
|
return newInterval;
|
|
310
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
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function createPositionReferenceFromSegoff(
|
|
455
|
+
client: Client,
|
|
456
|
+
segoff: { segment: ISegment | undefined; offset: number | undefined; },
|
|
457
|
+
refType: ReferenceType,
|
|
458
|
+
op?: ISequencedDocumentMessage): LocalReference {
|
|
459
|
+
if (segoff.segment) {
|
|
460
|
+
const ref = client.createLocalReferencePosition(segoff.segment, segoff.offset, refType, undefined);
|
|
461
|
+
return ref as LocalReference;
|
|
462
|
+
} else {
|
|
463
|
+
if (!op && !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
|
|
464
|
+
throw new UsageError("Non-transient references need segment");
|
|
465
|
+
}
|
|
466
|
+
return new LocalReference(client, undefined, 0, refType);
|
|
467
|
+
}
|
|
311
468
|
}
|
|
312
469
|
|
|
313
470
|
function createPositionReference(
|
|
@@ -315,15 +472,16 @@ function createPositionReference(
|
|
|
315
472
|
pos: number,
|
|
316
473
|
refType: ReferenceType,
|
|
317
474
|
op?: ISequencedDocumentMessage): LocalReference {
|
|
318
|
-
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
475
|
+
let segoff;
|
|
476
|
+
if (op) {
|
|
477
|
+
assert((refType & ReferenceType.SlideOnRemove) !== 0, 0x2f5 /* op create references must be SlideOnRemove */);
|
|
478
|
+
segoff = client.getContainingSegment(pos, op);
|
|
479
|
+
segoff = client.getSlideToSegment(segoff);
|
|
480
|
+
} else {
|
|
481
|
+
assert((refType & ReferenceType.SlideOnRemove) === 0, 0x2f6 /* SlideOnRemove references must be op created */);
|
|
482
|
+
segoff = client.getContainingSegment(pos);
|
|
325
483
|
}
|
|
326
|
-
return
|
|
484
|
+
return createPositionReferenceFromSegoff(client, segoff, refType, op);
|
|
327
485
|
}
|
|
328
486
|
|
|
329
487
|
function createSequenceInterval(
|
|
@@ -331,38 +489,42 @@ function createSequenceInterval(
|
|
|
331
489
|
start: number,
|
|
332
490
|
end: number,
|
|
333
491
|
client: Client,
|
|
334
|
-
intervalType
|
|
492
|
+
intervalType?: IntervalType,
|
|
335
493
|
op?: ISequencedDocumentMessage): SequenceInterval {
|
|
336
494
|
let beginRefType = ReferenceType.RangeBegin;
|
|
337
495
|
let endRefType = ReferenceType.RangeEnd;
|
|
338
|
-
if (intervalType === IntervalType.
|
|
339
|
-
beginRefType = ReferenceType.NestBegin;
|
|
340
|
-
endRefType = ReferenceType.NestEnd;
|
|
341
|
-
} else if (intervalType === IntervalType.Transient) {
|
|
496
|
+
if (intervalType === IntervalType.Transient) {
|
|
342
497
|
beginRefType = ReferenceType.Transient;
|
|
343
498
|
endRefType = ReferenceType.Transient;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
499
|
+
} else {
|
|
500
|
+
if (intervalType === IntervalType.Nest) {
|
|
501
|
+
beginRefType = ReferenceType.NestBegin;
|
|
502
|
+
endRefType = ReferenceType.NestEnd;
|
|
503
|
+
}
|
|
504
|
+
// All non-transient interval references must eventually be SlideOnRemove
|
|
505
|
+
// To ensure eventual consistency, they must start as StayOnRemove when
|
|
506
|
+
// pending (created locally and creation op is not acked)
|
|
507
|
+
if (op) {
|
|
508
|
+
beginRefType |= ReferenceType.SlideOnRemove;
|
|
509
|
+
endRefType |= ReferenceType.SlideOnRemove;
|
|
510
|
+
} else {
|
|
511
|
+
beginRefType |= ReferenceType.StayOnRemove;
|
|
512
|
+
endRefType |= ReferenceType.StayOnRemove;
|
|
513
|
+
}
|
|
350
514
|
}
|
|
351
515
|
|
|
352
516
|
const startLref = createPositionReference(client, start, beginRefType, op);
|
|
353
517
|
const endLref = createPositionReference(client, end, endRefType, op);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
endLref.addProperties(rangeProp);
|
|
518
|
+
startLref.pairedRef = endLref;
|
|
519
|
+
endLref.pairedRef = startLref;
|
|
520
|
+
const rangeProp = {
|
|
521
|
+
[reservedRangeLabelsKey]: [label],
|
|
522
|
+
};
|
|
523
|
+
startLref.addProperties(rangeProp);
|
|
524
|
+
endLref.addProperties(rangeProp);
|
|
362
525
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
526
|
+
const ival = new SequenceInterval(startLref, endLref, intervalType, rangeProp);
|
|
527
|
+
return ival;
|
|
366
528
|
}
|
|
367
529
|
|
|
368
530
|
export function defaultIntervalConflictResolver(a: Interval, b: Interval) {
|
|
@@ -387,8 +549,9 @@ export function createIntervalIndex(conflict?: IntervalConflictResolver<Interval
|
|
|
387
549
|
export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
388
550
|
private readonly intervalTree = new IntervalTree<TInterval>();
|
|
389
551
|
private readonly endIntervalTree: RedBlackTree<TInterval, TInterval>;
|
|
390
|
-
private
|
|
391
|
-
private
|
|
552
|
+
private readonly intervalIdMap: Map<string, TInterval> = new Map();
|
|
553
|
+
private conflictResolver: IntervalConflictResolver<TInterval> | undefined;
|
|
554
|
+
private endConflictResolver: ConflictAction<TInterval, TInterval> | undefined;
|
|
392
555
|
|
|
393
556
|
private static readonly legacyIdPrefix = "legacy";
|
|
394
557
|
|
|
@@ -396,6 +559,8 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
396
559
|
private readonly client: Client,
|
|
397
560
|
private readonly label: string,
|
|
398
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,
|
|
399
564
|
) {
|
|
400
565
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
401
566
|
this.endIntervalTree = new RedBlackTree<TInterval, TInterval>(helpers.compareEnds);
|
|
@@ -405,7 +570,7 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
405
570
|
this.conflictResolver = conflictResolver;
|
|
406
571
|
this.endConflictResolver =
|
|
407
572
|
(key: TInterval, currentKey: TInterval) => {
|
|
408
|
-
const ival =
|
|
573
|
+
const ival = conflictResolver(key, currentKey);
|
|
409
574
|
return {
|
|
410
575
|
data: ival,
|
|
411
576
|
key: ival,
|
|
@@ -423,12 +588,21 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
423
588
|
return `${LocalIntervalCollection.legacyIdPrefix}${start}-${end}`;
|
|
424
589
|
}
|
|
425
590
|
|
|
426
|
-
|
|
427
|
-
|
|
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) {
|
|
428
601
|
// An interval came over the wire without an ID, so create a non-unique one based on start/end.
|
|
429
602
|
// This will allow all clients to refer to this interval consistently.
|
|
603
|
+
id = this.createLegacyId(serializedInterval.start, serializedInterval.end);
|
|
430
604
|
const newProps = {
|
|
431
|
-
[reservedIntervalIdKey]:
|
|
605
|
+
[reservedIntervalIdKey]: id,
|
|
432
606
|
};
|
|
433
607
|
serializedInterval.properties = addProperties(serializedInterval.properties, newProps);
|
|
434
608
|
}
|
|
@@ -438,6 +612,8 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
438
612
|
enumerable: true,
|
|
439
613
|
writable: false,
|
|
440
614
|
});
|
|
615
|
+
|
|
616
|
+
return id;
|
|
441
617
|
}
|
|
442
618
|
|
|
443
619
|
public mapUntil(fn: (interval: TInterval) => boolean) {
|
|
@@ -520,20 +696,19 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
520
696
|
}
|
|
521
697
|
|
|
522
698
|
public findOverlappingIntervals(startPosition: number, endPosition: number) {
|
|
523
|
-
if (
|
|
524
|
-
const transientInterval =
|
|
525
|
-
this.helpers.create(
|
|
526
|
-
"transient",
|
|
527
|
-
startPosition,
|
|
528
|
-
endPosition,
|
|
529
|
-
this.client,
|
|
530
|
-
IntervalType.Transient);
|
|
531
|
-
|
|
532
|
-
const overlappingIntervalNodes = this.intervalTree.match(transientInterval);
|
|
533
|
-
return overlappingIntervalNodes.map((node) => node.key);
|
|
534
|
-
} else {
|
|
699
|
+
if (endPosition < startPosition || this.intervalTree.intervals.isEmpty()) {
|
|
535
700
|
return [];
|
|
536
701
|
}
|
|
702
|
+
const transientInterval =
|
|
703
|
+
this.helpers.create(
|
|
704
|
+
"transient",
|
|
705
|
+
startPosition,
|
|
706
|
+
endPosition,
|
|
707
|
+
this.client,
|
|
708
|
+
IntervalType.Transient);
|
|
709
|
+
|
|
710
|
+
const overlappingIntervalNodes = this.intervalTree.match(transientInterval);
|
|
711
|
+
return overlappingIntervalNodes.map((node) => node.key);
|
|
537
712
|
}
|
|
538
713
|
|
|
539
714
|
public previousInterval(pos: number) {
|
|
@@ -562,9 +737,20 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
562
737
|
return transientInterval;
|
|
563
738
|
}
|
|
564
739
|
|
|
565
|
-
|
|
740
|
+
private removeIntervalFromIndex(interval: TInterval) {
|
|
566
741
|
this.intervalTree.removeExisting(interval);
|
|
567
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);
|
|
568
754
|
}
|
|
569
755
|
|
|
570
756
|
public createInterval(
|
|
@@ -598,9 +784,9 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
598
784
|
return interval;
|
|
599
785
|
}
|
|
600
786
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
787
|
+
private addIntervalToIndex(interval: TInterval) {
|
|
788
|
+
const id = interval.getIntervalId();
|
|
789
|
+
assert(id !== undefined, 0x2c0 /* "ID must be created before adding interval to collection" */);
|
|
604
790
|
// Make the ID immutable.
|
|
605
791
|
Object.defineProperty(interval.properties, reservedIntervalIdKey, {
|
|
606
792
|
configurable: false,
|
|
@@ -609,18 +795,16 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
609
795
|
});
|
|
610
796
|
this.intervalTree.put(interval, this.conflictResolver);
|
|
611
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);
|
|
612
804
|
}
|
|
613
805
|
|
|
614
806
|
public getIntervalById(id: string) {
|
|
615
|
-
|
|
616
|
-
this.mapUntil((interval: TInterval) => {
|
|
617
|
-
if (interval.getIntervalId() === id) {
|
|
618
|
-
result = interval;
|
|
619
|
-
return false;
|
|
620
|
-
}
|
|
621
|
-
return true;
|
|
622
|
-
});
|
|
623
|
-
return result;
|
|
807
|
+
return this.intervalIdMap.get(id);
|
|
624
808
|
}
|
|
625
809
|
|
|
626
810
|
public changeInterval(interval: TInterval, start: number, end: number, op?: ISequencedDocumentMessage) {
|
|
@@ -632,10 +816,33 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
|
|
|
632
816
|
return newInterval;
|
|
633
817
|
}
|
|
634
818
|
|
|
635
|
-
public serialize() {
|
|
819
|
+
public serialize(): ISerializedIntervalCollectionV2 {
|
|
636
820
|
const client = this.client;
|
|
637
821
|
const intervals = this.intervalTree.intervals.keys();
|
|
638
|
-
|
|
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
|
+
}
|
|
639
846
|
}
|
|
640
847
|
}
|
|
641
848
|
|
|
@@ -645,7 +852,7 @@ class SequenceIntervalCollectionFactory
|
|
|
645
852
|
implements IValueFactory<IntervalCollection<SequenceInterval>> {
|
|
646
853
|
public load(
|
|
647
854
|
emitter: IValueOpEmitter,
|
|
648
|
-
raw: ISerializedInterval[] = [],
|
|
855
|
+
raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
|
|
649
856
|
): IntervalCollection<SequenceInterval> {
|
|
650
857
|
const helpers: IIntervalHelpers<SequenceInterval> = {
|
|
651
858
|
compareEnds: compareSequenceIntervalEnds,
|
|
@@ -654,7 +861,7 @@ class SequenceIntervalCollectionFactory
|
|
|
654
861
|
return new IntervalCollection<SequenceInterval>(helpers, true, emitter, raw);
|
|
655
862
|
}
|
|
656
863
|
|
|
657
|
-
public store(value: IntervalCollection<SequenceInterval>):
|
|
864
|
+
public store(value: IntervalCollection<SequenceInterval>): ISerializedIntervalCollectionV2 {
|
|
658
865
|
return value.serializeInternal();
|
|
659
866
|
}
|
|
660
867
|
}
|
|
@@ -678,49 +885,27 @@ export class SequenceIntervalCollectionValueType
|
|
|
678
885
|
private static readonly _factory: IValueFactory<IntervalCollection<SequenceInterval>> =
|
|
679
886
|
new SequenceIntervalCollectionFactory();
|
|
680
887
|
|
|
681
|
-
private static readonly _ops
|
|
682
|
-
new Map<string, IValueOperation<IntervalCollection<SequenceInterval>>>(
|
|
683
|
-
[[
|
|
684
|
-
"add",
|
|
685
|
-
{
|
|
686
|
-
process: (value, params, local, op) => {
|
|
687
|
-
value.ackAdd(params, local, op);
|
|
688
|
-
},
|
|
689
|
-
},
|
|
690
|
-
],
|
|
691
|
-
[
|
|
692
|
-
"delete",
|
|
693
|
-
{
|
|
694
|
-
process: (value, params, local, op) => {
|
|
695
|
-
value.ackDelete(params, local, op);
|
|
696
|
-
},
|
|
697
|
-
},
|
|
698
|
-
],
|
|
699
|
-
[
|
|
700
|
-
"change",
|
|
701
|
-
{
|
|
702
|
-
process: (value, params, local, op) => {
|
|
703
|
-
value.ackChange(params, local, op);
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
]]);
|
|
888
|
+
private static readonly _ops = makeOpsMap<SequenceInterval>();
|
|
707
889
|
}
|
|
708
890
|
|
|
709
891
|
const compareIntervalEnds = (a: Interval, b: Interval) => a.end - b.end;
|
|
710
892
|
|
|
711
893
|
function createInterval(label: string, start: number, end: number, client: Client): Interval {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
};
|
|
894
|
+
const rangeProp: PropertySet = {};
|
|
895
|
+
|
|
896
|
+
if (label && label.length > 0) {
|
|
897
|
+
rangeProp[reservedRangeLabelsKey] = [label];
|
|
717
898
|
}
|
|
899
|
+
|
|
718
900
|
return new Interval(start, end, rangeProp);
|
|
719
901
|
}
|
|
720
902
|
|
|
721
903
|
class IntervalCollectionFactory
|
|
722
904
|
implements IValueFactory<IntervalCollection<Interval>> {
|
|
723
|
-
public load(
|
|
905
|
+
public load(
|
|
906
|
+
emitter: IValueOpEmitter,
|
|
907
|
+
raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
|
|
908
|
+
): IntervalCollection<Interval> {
|
|
724
909
|
const helpers: IIntervalHelpers<Interval> = {
|
|
725
910
|
compareEnds: compareIntervalEnds,
|
|
726
911
|
create: createInterval,
|
|
@@ -730,7 +915,7 @@ class IntervalCollectionFactory
|
|
|
730
915
|
return collection;
|
|
731
916
|
}
|
|
732
917
|
|
|
733
|
-
public store(value: IntervalCollection<Interval>):
|
|
918
|
+
public store(value: IntervalCollection<Interval>): ISerializedIntervalCollectionV2 {
|
|
734
919
|
return value.serializeInternal();
|
|
735
920
|
}
|
|
736
921
|
}
|
|
@@ -753,32 +938,52 @@ export class IntervalCollectionValueType
|
|
|
753
938
|
|
|
754
939
|
private static readonly _factory: IValueFactory<IntervalCollection<Interval>> =
|
|
755
940
|
new IntervalCollectionFactory();
|
|
756
|
-
private static readonly _ops
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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);
|
|
962
|
+
},
|
|
963
|
+
rebase,
|
|
964
|
+
},
|
|
965
|
+
],
|
|
966
|
+
[
|
|
967
|
+
"delete",
|
|
968
|
+
{
|
|
969
|
+
process: (collection, params, local, op) => {
|
|
970
|
+
collection.ackDelete(params, local, op);
|
|
764
971
|
},
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
{
|
|
769
|
-
process: (value, params, local, op) => {
|
|
770
|
-
value.ackDelete(params, local, op);
|
|
771
|
-
},
|
|
972
|
+
rebase: (collection, op, localOpMetadata) => {
|
|
973
|
+
// Deletion of intervals is based on id, so requires no rebasing.
|
|
974
|
+
return { rebasedOp: op, rebasedLocalOpMetadata: localOpMetadata };
|
|
772
975
|
},
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
976
|
+
},
|
|
977
|
+
],
|
|
978
|
+
[
|
|
979
|
+
"change",
|
|
980
|
+
{
|
|
981
|
+
process: (collection, params, local, op) => {
|
|
982
|
+
collection.ackChange(params, local, op);
|
|
780
983
|
},
|
|
781
|
-
|
|
984
|
+
rebase,
|
|
985
|
+
},
|
|
986
|
+
]]);
|
|
782
987
|
}
|
|
783
988
|
|
|
784
989
|
export type DeserializeCallback = (properties: PropertySet) => void;
|
|
@@ -815,7 +1020,18 @@ export class IntervalCollectionIterator<TInterval extends ISerializableInterval>
|
|
|
815
1020
|
}
|
|
816
1021
|
|
|
817
1022
|
export interface IIntervalCollectionEvent<TInterval extends ISerializableInterval> extends IEvent {
|
|
818
|
-
|
|
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",
|
|
819
1035
|
listener: (interval: TInterval, local: boolean, op: ISequencedDocumentMessage) => void);
|
|
820
1036
|
(event: "propertyChanged", listener: (interval: TInterval, propertyArgs: PropertySet) => void);
|
|
821
1037
|
}
|
|
@@ -824,34 +1040,49 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
824
1040
|
extends TypedEventEmitter<IIntervalCollectionEvent<TInterval>> {
|
|
825
1041
|
private savedSerializedIntervals?: ISerializedInterval[];
|
|
826
1042
|
private localCollection: LocalIntervalCollection<TInterval>;
|
|
827
|
-
private onDeserialize: DeserializeCallback;
|
|
1043
|
+
private onDeserialize: DeserializeCallback | undefined;
|
|
828
1044
|
private client: Client;
|
|
829
|
-
private pendingChangesStart: Map<string, ISerializedInterval[]
|
|
830
|
-
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[]>();
|
|
831
1047
|
|
|
832
1048
|
public get attached(): boolean {
|
|
833
1049
|
return !!this.localCollection;
|
|
834
1050
|
}
|
|
835
1051
|
|
|
836
|
-
|
|
1052
|
+
/** @internal */
|
|
1053
|
+
constructor(
|
|
1054
|
+
private readonly helpers: IIntervalHelpers<TInterval>,
|
|
1055
|
+
private readonly requiresClient: boolean,
|
|
837
1056
|
private readonly emitter: IValueOpEmitter,
|
|
838
|
-
serializedIntervals: ISerializedInterval[]
|
|
1057
|
+
serializedIntervals: ISerializedInterval[] | ISerializedIntervalCollectionV2,
|
|
1058
|
+
) {
|
|
839
1059
|
super();
|
|
840
|
-
|
|
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
|
+
}
|
|
841
1067
|
}
|
|
842
1068
|
|
|
843
1069
|
public attachGraph(client: Client, label: string) {
|
|
844
1070
|
if (this.attached) {
|
|
845
|
-
throw new
|
|
1071
|
+
throw new LoggingError("Only supports one Sequence attach");
|
|
846
1072
|
}
|
|
847
1073
|
|
|
848
1074
|
if ((client === undefined) && (this.requiresClient)) {
|
|
849
|
-
throw new
|
|
1075
|
+
throw new LoggingError("Client required for this collection");
|
|
850
1076
|
}
|
|
851
1077
|
|
|
852
1078
|
// Instantiate the local interval collection based on the saved intervals
|
|
853
1079
|
this.client = client;
|
|
854
|
-
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
|
+
);
|
|
855
1086
|
if (this.savedSerializedIntervals) {
|
|
856
1087
|
for (const serializedInterval of this.savedSerializedIntervals) {
|
|
857
1088
|
this.localCollection.ensureSerializedId(serializedInterval);
|
|
@@ -865,13 +1096,28 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
865
1096
|
this.savedSerializedIntervals = undefined;
|
|
866
1097
|
}
|
|
867
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
|
+
|
|
868
1106
|
public getIntervalById(id: string) {
|
|
869
1107
|
if (!this.attached) {
|
|
870
|
-
throw new
|
|
1108
|
+
throw new LoggingError("attach must be called before accessing intervals");
|
|
871
1109
|
}
|
|
872
1110
|
return this.localCollection.getIntervalById(id);
|
|
873
1111
|
}
|
|
874
1112
|
|
|
1113
|
+
/**
|
|
1114
|
+
* Create a new interval and add it to the collection
|
|
1115
|
+
* @param start - interval start position
|
|
1116
|
+
* @param end - interval end position
|
|
1117
|
+
* @param intervalType - type of the interval. All intervals are SlideOnRemove. Intervals may not be Transient.
|
|
1118
|
+
* @param props - properties of the interval
|
|
1119
|
+
* @returns - the created interval
|
|
1120
|
+
*/
|
|
875
1121
|
public add(
|
|
876
1122
|
start: number,
|
|
877
1123
|
end: number,
|
|
@@ -879,7 +1125,10 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
879
1125
|
props?: PropertySet,
|
|
880
1126
|
) {
|
|
881
1127
|
if (!this.attached) {
|
|
882
|
-
throw new
|
|
1128
|
+
throw new LoggingError("attach must be called prior to adding intervals");
|
|
1129
|
+
}
|
|
1130
|
+
if (intervalType & IntervalType.Transient) {
|
|
1131
|
+
throw new LoggingError("Can not add transient intervals");
|
|
883
1132
|
}
|
|
884
1133
|
|
|
885
1134
|
const interval: TInterval = this.localCollection.addInterval(start, end, intervalType, props);
|
|
@@ -893,7 +1142,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
893
1142
|
start,
|
|
894
1143
|
};
|
|
895
1144
|
// Local ops get submitted to the server. Remote ops have the deserializer run.
|
|
896
|
-
this.emitter.emit("add", undefined, serializedInterval);
|
|
1145
|
+
this.emitter.emit("add", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
|
|
897
1146
|
}
|
|
898
1147
|
|
|
899
1148
|
this.emit("addInterval", interval, true, undefined);
|
|
@@ -901,13 +1150,19 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
901
1150
|
return interval;
|
|
902
1151
|
}
|
|
903
1152
|
|
|
904
|
-
private deleteExistingInterval(interval: TInterval, local: boolean, op
|
|
1153
|
+
private deleteExistingInterval(interval: TInterval, local: boolean, op?: ISequencedDocumentMessage) {
|
|
905
1154
|
// The given interval is known to exist in the collection.
|
|
906
1155
|
this.localCollection.removeExistingInterval(interval);
|
|
1156
|
+
|
|
907
1157
|
if (interval) {
|
|
908
1158
|
// Local ops get submitted to the server. Remote ops have the deserializer run.
|
|
909
1159
|
if (local) {
|
|
910
|
-
this.emitter.emit(
|
|
1160
|
+
this.emitter.emit(
|
|
1161
|
+
"delete",
|
|
1162
|
+
undefined,
|
|
1163
|
+
interval.serialize(this.client),
|
|
1164
|
+
{ localSeq: this.getNextLocalSeq() },
|
|
1165
|
+
);
|
|
911
1166
|
} else {
|
|
912
1167
|
if (this.onDeserialize) {
|
|
913
1168
|
this.onDeserialize(interval);
|
|
@@ -928,13 +1183,13 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
928
1183
|
|
|
929
1184
|
public changeProperties(id: string, props: PropertySet) {
|
|
930
1185
|
if (!this.attached) {
|
|
931
|
-
throw new
|
|
1186
|
+
throw new LoggingError("Attach must be called before accessing intervals");
|
|
932
1187
|
}
|
|
933
1188
|
if (typeof (id) !== "string") {
|
|
934
|
-
throw new
|
|
1189
|
+
throw new LoggingError("Change API requires an ID that is a string");
|
|
935
1190
|
}
|
|
936
1191
|
if (!props) {
|
|
937
|
-
throw new
|
|
1192
|
+
throw new LoggingError("changeProperties should be called with a property set");
|
|
938
1193
|
}
|
|
939
1194
|
|
|
940
1195
|
const interval = this.getIntervalById(id);
|
|
@@ -942,12 +1197,15 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
942
1197
|
// Pass Unassigned as the sequence number to indicate that this is a local op that is waiting for an ack.
|
|
943
1198
|
const deltaProps = interval.addProperties(props, true, UnassignedSequenceNumber);
|
|
944
1199
|
const serializedInterval: ISerializedInterval = interval.serialize(this.client);
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
+
|
|
948
1206
|
serializedInterval.properties = props;
|
|
949
1207
|
serializedInterval.properties[reservedIntervalIdKey] = interval.getIntervalId();
|
|
950
|
-
this.emitter.emit("change", undefined, serializedInterval);
|
|
1208
|
+
this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
|
|
951
1209
|
this.emit("propertyChanged", interval, deltaProps);
|
|
952
1210
|
}
|
|
953
1211
|
this.emit("changeInterval", interval, true, undefined);
|
|
@@ -955,42 +1213,39 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
955
1213
|
|
|
956
1214
|
public change(id: string, start?: number, end?: number): TInterval | undefined {
|
|
957
1215
|
if (!this.attached) {
|
|
958
|
-
throw new
|
|
1216
|
+
throw new LoggingError("Attach must be called before accessing intervals");
|
|
959
1217
|
}
|
|
1218
|
+
|
|
1219
|
+
// Force id to be a string.
|
|
960
1220
|
if (typeof (id) !== "string") {
|
|
961
|
-
throw new
|
|
1221
|
+
throw new LoggingError("Change API requires an ID that is a string");
|
|
962
1222
|
}
|
|
963
1223
|
|
|
964
|
-
// Force id to be a string.
|
|
965
1224
|
const interval = this.getIntervalById(id);
|
|
966
1225
|
if (interval) {
|
|
967
|
-
this.localCollection.changeInterval(interval, start, end);
|
|
1226
|
+
const newInterval = this.localCollection.changeInterval(interval, start, end);
|
|
968
1227
|
const serializedInterval: ISerializedInterval = interval.serialize(this.client);
|
|
969
1228
|
serializedInterval.start = start;
|
|
970
1229
|
serializedInterval.end = end;
|
|
971
1230
|
// Emit a property bag containing only the ID, as we don't intend for this op to change any properties.
|
|
972
1231
|
serializedInterval.properties =
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
this.emitter.emit("change", undefined, serializedInterval);
|
|
1232
|
+
{
|
|
1233
|
+
[reservedIntervalIdKey]: interval.getIntervalId(),
|
|
1234
|
+
};
|
|
1235
|
+
this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
|
|
977
1236
|
this.addPendingChange(id, serializedInterval);
|
|
1237
|
+
this.emit("changeInterval", newInterval, true, undefined);
|
|
1238
|
+
return newInterval;
|
|
978
1239
|
}
|
|
979
|
-
|
|
980
|
-
return
|
|
1240
|
+
// No interval to change
|
|
1241
|
+
return undefined;
|
|
981
1242
|
}
|
|
982
1243
|
|
|
983
1244
|
private addPendingChange(id: string, serializedInterval: ISerializedInterval) {
|
|
984
1245
|
if (serializedInterval.start !== undefined) {
|
|
985
|
-
if (!this.pendingChangesStart) {
|
|
986
|
-
this.pendingChangesStart = new Map<string, ISerializedInterval[]>();
|
|
987
|
-
}
|
|
988
1246
|
this.addPendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
|
|
989
1247
|
}
|
|
990
1248
|
if (serializedInterval.end !== undefined) {
|
|
991
|
-
if (!this.pendingChangesEnd) {
|
|
992
|
-
this.pendingChangesEnd = new Map<string, ISerializedInterval[]>();
|
|
993
|
-
}
|
|
994
1249
|
this.addPendingChangeHelper(id, this.pendingChangesEnd, serializedInterval);
|
|
995
1250
|
}
|
|
996
1251
|
}
|
|
@@ -1000,7 +1255,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1000
1255
|
pendingChanges: Map<string, ISerializedInterval[]>,
|
|
1001
1256
|
serializedInterval: ISerializedInterval,
|
|
1002
1257
|
) {
|
|
1003
|
-
let entries: ISerializedInterval[] = pendingChanges.get(id);
|
|
1258
|
+
let entries: ISerializedInterval[] | undefined = pendingChanges.get(id);
|
|
1004
1259
|
if (!entries) {
|
|
1005
1260
|
entries = [];
|
|
1006
1261
|
pendingChanges.set(id, entries);
|
|
@@ -1010,7 +1265,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1010
1265
|
|
|
1011
1266
|
private removePendingChange(serializedInterval: ISerializedInterval) {
|
|
1012
1267
|
// Change ops always have an ID.
|
|
1013
|
-
const id: string = serializedInterval.properties[reservedIntervalIdKey];
|
|
1268
|
+
const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
|
|
1014
1269
|
if (serializedInterval.start !== undefined) {
|
|
1015
1270
|
this.removePendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
|
|
1016
1271
|
}
|
|
@@ -1024,26 +1279,26 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1024
1279
|
pendingChanges: Map<string, ISerializedInterval[]>,
|
|
1025
1280
|
serializedInterval: ISerializedInterval,
|
|
1026
1281
|
) {
|
|
1027
|
-
const entries = pendingChanges
|
|
1282
|
+
const entries = pendingChanges.get(id);
|
|
1028
1283
|
if (entries) {
|
|
1029
1284
|
const pendingChange = entries.shift();
|
|
1030
1285
|
if (entries.length === 0) {
|
|
1031
1286
|
pendingChanges.delete(id);
|
|
1032
1287
|
}
|
|
1033
|
-
if (pendingChange
|
|
1034
|
-
pendingChange
|
|
1035
|
-
throw new
|
|
1288
|
+
if (pendingChange?.start !== serializedInterval.start ||
|
|
1289
|
+
pendingChange?.end !== serializedInterval.end) {
|
|
1290
|
+
throw new LoggingError("Mismatch in pending changes");
|
|
1036
1291
|
}
|
|
1037
1292
|
}
|
|
1038
1293
|
}
|
|
1039
1294
|
|
|
1040
1295
|
private hasPendingChangeStart(id: string) {
|
|
1041
|
-
const entries = this.pendingChangesStart
|
|
1296
|
+
const entries = this.pendingChangesStart.get(id);
|
|
1042
1297
|
return entries && entries.length !== 0;
|
|
1043
1298
|
}
|
|
1044
1299
|
|
|
1045
1300
|
private hasPendingChangeEnd(id: string) {
|
|
1046
|
-
const entries = this.pendingChangesEnd
|
|
1301
|
+
const entries = this.pendingChangesEnd.get(id);
|
|
1047
1302
|
return entries && entries.length !== 0;
|
|
1048
1303
|
}
|
|
1049
1304
|
|
|
@@ -1055,7 +1310,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1055
1310
|
/** @internal */
|
|
1056
1311
|
public ackChange(serializedInterval: ISerializedInterval, local: boolean, op: ISequencedDocumentMessage) {
|
|
1057
1312
|
if (!this.attached) {
|
|
1058
|
-
throw new
|
|
1313
|
+
throw new LoggingError("Attach must be called before accessing intervals");
|
|
1059
1314
|
}
|
|
1060
1315
|
|
|
1061
1316
|
let interval: TInterval | undefined;
|
|
@@ -1063,15 +1318,17 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1063
1318
|
if (local) {
|
|
1064
1319
|
// This is an ack from the server. Remove the pending change.
|
|
1065
1320
|
this.removePendingChange(serializedInterval);
|
|
1066
|
-
const id: string = serializedInterval.properties[reservedIntervalIdKey];
|
|
1321
|
+
const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
|
|
1067
1322
|
interval = this.getIntervalById(id);
|
|
1068
1323
|
if (interval) {
|
|
1069
1324
|
// Let the propertyManager prune its pending change-properties set.
|
|
1070
1325
|
interval.propertyManager?.ackPendingProperties(
|
|
1071
1326
|
{
|
|
1072
1327
|
type: MergeTreeDeltaType.ANNOTATE,
|
|
1073
|
-
props: serializedInterval.properties,
|
|
1328
|
+
props: serializedInterval.properties ?? {},
|
|
1074
1329
|
});
|
|
1330
|
+
|
|
1331
|
+
this.ackInterval(interval, op);
|
|
1075
1332
|
}
|
|
1076
1333
|
} else {
|
|
1077
1334
|
// If there are pending changes with this ID, don't apply the remote start/end change, as the local ack
|
|
@@ -1110,7 +1367,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1110
1367
|
|
|
1111
1368
|
public addConflictResolver(conflictResolver: IntervalConflictResolver<TInterval>): void {
|
|
1112
1369
|
if (!this.attached) {
|
|
1113
|
-
throw new
|
|
1370
|
+
throw new LoggingError("attachSequence must be called");
|
|
1114
1371
|
}
|
|
1115
1372
|
this.localCollection.addConflictResolver(conflictResolver);
|
|
1116
1373
|
}
|
|
@@ -1126,10 +1383,111 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1126
1383
|
|
|
1127
1384
|
// Trigger the async prepare work across all values in the collection
|
|
1128
1385
|
this.localCollection.map((interval) => {
|
|
1129
|
-
|
|
1386
|
+
onDeserialize(interval);
|
|
1130
1387
|
});
|
|
1131
1388
|
}
|
|
1132
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
|
+
|
|
1421
|
+
private getSlideToSegment(lref: LocalReference) {
|
|
1422
|
+
const segoff = { segment: lref.segment, offset: lref.offset };
|
|
1423
|
+
const newSegoff = this.client.getSlideToSegment(segoff);
|
|
1424
|
+
const value: { segment: ISegment | undefined; offset: number | undefined; } | undefined
|
|
1425
|
+
= (segoff.segment === newSegoff.segment && segoff.offset === newSegoff.offset) ? undefined : newSegoff;
|
|
1426
|
+
return value;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
private setSlideOnRemove(lref: LocalReference) {
|
|
1430
|
+
let refType = lref.refType;
|
|
1431
|
+
refType = refType & ~ReferenceType.StayOnRemove;
|
|
1432
|
+
refType = refType | ReferenceType.SlideOnRemove;
|
|
1433
|
+
lref.refType = refType;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
private ackInterval(interval: TInterval, op: ISequencedDocumentMessage) {
|
|
1437
|
+
// in current usage, interval is always a SequenceInterval
|
|
1438
|
+
if (!(interval instanceof SequenceInterval)) {
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (!refTypeIncludesFlag(interval.start, ReferenceType.StayOnRemove) &&
|
|
1443
|
+
!refTypeIncludesFlag(interval.end, ReferenceType.StayOnRemove)) {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const newStart = this.getSlideToSegment(interval.start);
|
|
1448
|
+
const newEnd = this.getSlideToSegment(interval.end);
|
|
1449
|
+
|
|
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.
|
|
1469
|
+
this.localCollection.removeExistingInterval(interval);
|
|
1470
|
+
|
|
1471
|
+
if (needsStartUpdate) {
|
|
1472
|
+
const props = interval.start.properties;
|
|
1473
|
+
this.client.removeLocalReferencePosition(interval.start);
|
|
1474
|
+
interval.start = createPositionReferenceFromSegoff(this.client, newStart, interval.start.refType, op);
|
|
1475
|
+
if (props) {
|
|
1476
|
+
interval.start.addProperties(props);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if (needsEndUpdate) {
|
|
1480
|
+
const props = interval.end.properties;
|
|
1481
|
+
this.client.removeLocalReferencePosition(interval.end);
|
|
1482
|
+
interval.end = createPositionReferenceFromSegoff(this.client, newEnd, interval.end.refType, op);
|
|
1483
|
+
if (props) {
|
|
1484
|
+
interval.end.addProperties(props);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
this.localCollection.add(interval);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1133
1491
|
/** @deprecated - use ackAdd */
|
|
1134
1492
|
public addInternal(
|
|
1135
1493
|
serializedInterval: ISerializedInterval,
|
|
@@ -1144,13 +1502,16 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1144
1502
|
local: boolean,
|
|
1145
1503
|
op: ISequencedDocumentMessage) {
|
|
1146
1504
|
if (local) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1505
|
+
const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
|
|
1506
|
+
const localInterval = this.getIntervalById(id);
|
|
1507
|
+
if (localInterval) {
|
|
1508
|
+
this.ackInterval(localInterval, op);
|
|
1509
|
+
}
|
|
1149
1510
|
return;
|
|
1150
1511
|
}
|
|
1151
1512
|
|
|
1152
1513
|
if (!this.attached) {
|
|
1153
|
-
throw new
|
|
1514
|
+
throw new LoggingError("attachSequence must be called");
|
|
1154
1515
|
}
|
|
1155
1516
|
|
|
1156
1517
|
this.localCollection.ensureSerializedId(serializedInterval);
|
|
@@ -1194,19 +1555,22 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1194
1555
|
}
|
|
1195
1556
|
|
|
1196
1557
|
if (!this.attached) {
|
|
1197
|
-
throw new
|
|
1558
|
+
throw new LoggingError("attach must be called prior to deleting intervals");
|
|
1198
1559
|
}
|
|
1199
1560
|
|
|
1200
|
-
this.localCollection.ensureSerializedId(serializedInterval);
|
|
1201
|
-
const interval = this.localCollection.getIntervalById(
|
|
1561
|
+
const id = this.localCollection.ensureSerializedId(serializedInterval);
|
|
1562
|
+
const interval = this.localCollection.getIntervalById(id);
|
|
1202
1563
|
if (interval) {
|
|
1203
1564
|
this.deleteExistingInterval(interval, local, op);
|
|
1204
1565
|
}
|
|
1205
1566
|
}
|
|
1206
1567
|
|
|
1207
|
-
|
|
1568
|
+
/**
|
|
1569
|
+
* @internal
|
|
1570
|
+
*/
|
|
1571
|
+
public serializeInternal(): ISerializedIntervalCollectionV2 {
|
|
1208
1572
|
if (!this.attached) {
|
|
1209
|
-
throw new
|
|
1573
|
+
throw new LoggingError("attachSequence must be called");
|
|
1210
1574
|
}
|
|
1211
1575
|
|
|
1212
1576
|
return this.localCollection.serialize();
|
|
@@ -1251,7 +1615,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1251
1615
|
|
|
1252
1616
|
public findOverlappingIntervals(startPosition: number, endPosition: number): TInterval[] {
|
|
1253
1617
|
if (!this.attached) {
|
|
1254
|
-
throw new
|
|
1618
|
+
throw new LoggingError("attachSequence must be called");
|
|
1255
1619
|
}
|
|
1256
1620
|
|
|
1257
1621
|
return this.localCollection.findOverlappingIntervals(startPosition, endPosition);
|
|
@@ -1259,7 +1623,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1259
1623
|
|
|
1260
1624
|
public map(fn: (interval: TInterval) => void) {
|
|
1261
1625
|
if (!this.attached) {
|
|
1262
|
-
throw new
|
|
1626
|
+
throw new LoggingError("attachSequence must be called");
|
|
1263
1627
|
}
|
|
1264
1628
|
|
|
1265
1629
|
this.localCollection.map(fn);
|
|
@@ -1267,7 +1631,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1267
1631
|
|
|
1268
1632
|
public previousInterval(pos: number): TInterval {
|
|
1269
1633
|
if (!this.attached) {
|
|
1270
|
-
throw new
|
|
1634
|
+
throw new LoggingError("attachSequence must be called");
|
|
1271
1635
|
}
|
|
1272
1636
|
|
|
1273
1637
|
return this.localCollection.previousInterval(pos);
|
|
@@ -1275,7 +1639,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
|
|
|
1275
1639
|
|
|
1276
1640
|
public nextInterval(pos: number): TInterval {
|
|
1277
1641
|
if (!this.attached) {
|
|
1278
|
-
throw new
|
|
1642
|
+
throw new LoggingError("attachSequence must be called");
|
|
1279
1643
|
}
|
|
1280
1644
|
|
|
1281
1645
|
return this.localCollection.nextInterval(pos);
|