@fluidframework/merge-tree 0.58.3000-61081 → 0.59.2000-61729

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/merge-tree",
3
- "version": "0.58.3000-61081",
3
+ "version": "0.59.2000-61729",
4
4
  "description": "Merge tree",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -60,21 +60,21 @@
60
60
  "dependencies": {
61
61
  "@fluidframework/common-definitions": "^0.20.1",
62
62
  "@fluidframework/common-utils": "^0.32.1",
63
- "@fluidframework/container-definitions": "^0.47.1000",
64
- "@fluidframework/core-interfaces": "^0.42.0",
65
- "@fluidframework/datastore-definitions": "0.58.3000-61081",
66
- "@fluidframework/protocol-definitions": "^0.1027.1000",
67
- "@fluidframework/runtime-definitions": "0.58.3000-61081",
68
- "@fluidframework/runtime-utils": "0.58.3000-61081",
69
- "@fluidframework/shared-object-base": "0.58.3000-61081",
70
- "@fluidframework/telemetry-utils": "0.58.3000-61081"
63
+ "@fluidframework/container-definitions": "^0.48.1000-0",
64
+ "@fluidframework/core-interfaces": "^0.43.1000-0",
65
+ "@fluidframework/datastore-definitions": "0.59.2000-61729",
66
+ "@fluidframework/protocol-definitions": "^0.1028.1000-0",
67
+ "@fluidframework/runtime-definitions": "0.59.2000-61729",
68
+ "@fluidframework/runtime-utils": "0.59.2000-61729",
69
+ "@fluidframework/shared-object-base": "0.59.2000-61729",
70
+ "@fluidframework/telemetry-utils": "0.59.2000-61729"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@fluidframework/build-common": "^0.23.0",
74
- "@fluidframework/eslint-config-fluid": "^0.27.2000-59622",
75
- "@fluidframework/merge-tree-previous": "npm:@fluidframework/merge-tree@0.58.2000",
76
- "@fluidframework/mocha-test-setup": "0.58.3000-61081",
77
- "@fluidframework/test-runtime-utils": "0.58.3000-61081",
74
+ "@fluidframework/eslint-config-fluid": "^0.28.1000-61189",
75
+ "@fluidframework/merge-tree-previous": "npm:@fluidframework/merge-tree@^0.58.0",
76
+ "@fluidframework/mocha-test-setup": "0.59.2000-61729",
77
+ "@fluidframework/test-runtime-utils": "0.59.2000-61729",
78
78
  "@microsoft/api-extractor": "^7.16.1",
79
79
  "@rushstack/eslint-config": "^2.5.1",
80
80
  "@types/diff": "^3.5.1",
@@ -103,7 +103,7 @@
103
103
  "typescript-formatter": "7.1.0"
104
104
  },
105
105
  "typeValidation": {
106
- "version": "0.58.3000",
106
+ "version": "0.59.1000",
107
107
  "broken": {},
108
108
  "disabled": true
109
109
  }
@@ -134,7 +134,8 @@ export class LocalReferenceCollection {
134
134
  if (!seg1.localRefs) {
135
135
  seg1.localRefs = new LocalReferenceCollection(seg1);
136
136
  }
137
- assert(seg1.localRefs.refsByOffset.length === seg1.cachedLength, "LocalReferences array contains a gap");
137
+ assert(seg1.localRefs.refsByOffset.length === seg1.cachedLength,
138
+ 0x2be /* "LocalReferences array contains a gap" */);
138
139
  seg1.localRefs.append(seg2.localRefs);
139
140
  }
140
141
  else if (seg1.localRefs) {
package/src/mergeTree.ts CHANGED
@@ -104,15 +104,21 @@ export interface IHierBlock extends IMergeBlock {
104
104
  }
105
105
 
106
106
  export interface IRemovalInfo {
107
- removedSeq?: number;
108
- removedClientId?: number;
109
- removedClientOverlap?: number[];
107
+ removedSeq: number;
108
+ removedClientIds: number[];
109
+ }
110
+ export function toRemovalInfo(maybe: Partial<IRemovalInfo> | undefined): IRemovalInfo | undefined {
111
+ if (maybe?.removedClientIds !== undefined && maybe?.removedSeq !== undefined) {
112
+ return maybe as IRemovalInfo;
113
+ }
114
+ assert(maybe?.removedClientIds === undefined && maybe?.removedSeq === undefined,
115
+ 0x2bf /* "both removedClientIds and removedSeq should be set or not set" */);
110
116
  }
111
117
 
112
118
  /**
113
119
  * A segment representing a portion of the merge tree.
114
120
  */
115
- export interface ISegment extends IMergeNodeCommon, IRemovalInfo {
121
+ export interface ISegment extends IMergeNodeCommon, Partial<IRemovalInfo> {
116
122
  readonly type: string;
117
123
  readonly segmentGroups: SegmentGroupCollection;
118
124
  readonly trackingCollection: TrackingGroupCollection;
@@ -482,8 +488,7 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
482
488
  public clientId: number = LocalClientId;
483
489
  public seq: number = UniversalSequenceNumber;
484
490
  public removedSeq?: number;
485
- public removedClientId?: number;
486
- public removedClientOverlap?: number[];
491
+ public removedClientIds?: number[];
487
492
  public readonly segmentGroups: SegmentGroupCollection = new SegmentGroupCollection(this);
488
493
  public readonly trackingCollection: TrackingGroupCollection = new TrackingGroupCollection(this);
489
494
  public propertyManager?: PropertiesManager;
@@ -521,7 +526,7 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
521
526
  b.clientId = this.clientId;
522
527
  // TODO: deep clone properties
523
528
  b.properties = clone(this.properties);
524
- b.removedClientId = this.removedClientId;
529
+ b.removedClientIds = this.removedClientIds?.slice();
525
530
  // TODO: copy removed client overlap and branch removal info
526
531
  b.removedSeq = this.removedSeq;
527
532
  b.seq = this.seq;
@@ -555,10 +560,9 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
555
560
  return true;
556
561
 
557
562
  case MergeTreeDeltaType.REMOVE:
558
- // eslint-disable-next-line @typescript-eslint/no-this-alias
559
- const removalInfo: IRemovalInfo = this;
560
- assert(!!removalInfo, 0x046 /* "On remove ack, missing removal info!" */);
561
- assert(!!removalInfo.removedSeq, 0x047 /* "On remove ack, missing removed sequence number!" */);
563
+
564
+ const removalInfo: IRemovalInfo | undefined = toRemovalInfo(this);
565
+ assert(removalInfo !== undefined, 0x046 /* "On remove ack, missing removal info!" */);
562
566
  this.localRemovedSeq = undefined;
563
567
  if (removalInfo.removedSeq === UnassignedSequenceNumber) {
564
568
  removalInfo.removedSeq = opArgs.sequencedMessage!.sequenceNumber;
@@ -584,15 +588,12 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
584
588
  // but this ordinal meets all the necessary invariants for now.
585
589
  leafSegment.ordinal = this.ordinal + String.fromCharCode(0);
586
590
 
587
- leafSegment.removedClientId = this.removedClientId;
591
+ leafSegment.removedClientIds = this.removedClientIds?.slice();
588
592
  leafSegment.removedSeq = this.removedSeq;
589
593
  leafSegment.localRemovedSeq = this.localRemovedSeq;
590
594
  leafSegment.seq = this.seq;
591
595
  leafSegment.localSeq = this.localSeq;
592
596
  leafSegment.clientId = this.clientId;
593
- if (this.removedClientOverlap) {
594
- leafSegment.removedClientOverlap = [...this.removedClientOverlap];
595
- }
596
597
  this.segmentGroups.copyTo(leafSegment);
597
598
  this.trackingCollection.copyTo(leafSegment);
598
599
  if (this.localRefs) {
@@ -1091,8 +1092,8 @@ export class MergeTree {
1091
1092
  }
1092
1093
 
1093
1094
  public localNetLength(segment: ISegment) {
1094
- const removalInfo: IRemovalInfo = segment;
1095
- if (removalInfo.removedSeq !== undefined) {
1095
+ const removalInfo = toRemovalInfo(segment);
1096
+ if (removalInfo !== undefined) {
1096
1097
  return 0;
1097
1098
  } else {
1098
1099
  return segment.cachedLength;
@@ -1458,9 +1459,9 @@ export class MergeTree {
1458
1459
  return node.partialLengths!.getPartialLength(refSeq, clientId);
1459
1460
  } else {
1460
1461
  const segment = node;
1461
- const removalInfo: IRemovalInfo = segment;
1462
+ const removalInfo = toRemovalInfo(segment);
1462
1463
 
1463
- if(removalInfo.removedSeq !== undefined
1464
+ if(removalInfo !== undefined
1464
1465
  && removalInfo.removedSeq !== UnassignedSequenceNumber
1465
1466
  && removalInfo.removedSeq <= refSeq) {
1466
1467
  // this segment is a tombstone eligible for zamboni
@@ -1471,12 +1472,8 @@ export class MergeTree {
1471
1472
  if (((segment.clientId === clientId) ||
1472
1473
  ((segment.seq !== UnassignedSequenceNumber) && (segment.seq! <= refSeq)))) {
1473
1474
  // Segment happened by reference sequence number or segment from requesting client
1474
- if (removalInfo.removedSeq !== undefined) {
1475
- if (
1476
- removalInfo.removedClientId === clientId
1477
- || (removalInfo.removedClientOverlap
1478
- && removalInfo.removedClientOverlap.includes(clientId))
1479
- ) {
1475
+ if (removalInfo !== undefined) {
1476
+ if (removalInfo.removedClientIds.includes(clientId)) {
1480
1477
  return 0;
1481
1478
  } else {
1482
1479
  return segment.cachedLength;
@@ -1488,7 +1485,7 @@ export class MergeTree {
1488
1485
  // the segment was inserted and removed before the
1489
1486
  // this context, so it will never exist for this
1490
1487
  // context
1491
- if(removalInfo.removedSeq !== undefined
1488
+ if(removalInfo !== undefined
1492
1489
  && removalInfo.removedSeq !== UnassignedSequenceNumber) {
1493
1490
  return undefined;
1494
1491
  }
@@ -2296,13 +2293,6 @@ export class MergeTree {
2296
2293
  }
2297
2294
  }
2298
2295
 
2299
- private addOverlappingClient(removalInfo: IRemovalInfo, clientId: number) {
2300
- if (!removalInfo.removedClientOverlap) {
2301
- removalInfo.removedClientOverlap = <number[]>[];
2302
- }
2303
- removalInfo.removedClientOverlap.push(clientId);
2304
- }
2305
-
2306
2296
  /**
2307
2297
  * Annotate a range with properties
2308
2298
  * @param start - The inclusive start position of the range to annotate
@@ -2373,21 +2363,24 @@ export class MergeTree {
2373
2363
  const savedLocalRefs: LocalReferenceCollection[] = [];
2374
2364
  const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
2375
2365
  const markRemoved = (segment: ISegment, pos: number, _start: number, _end: number) => {
2376
- const removalInfo: IRemovalInfo = segment;
2377
- if (removalInfo.removedSeq !== undefined) {
2366
+ const existingRemovalInfo = toRemovalInfo(segment);
2367
+ if (existingRemovalInfo !== undefined) {
2378
2368
  _overwrite = true;
2379
- if (removalInfo.removedSeq === UnassignedSequenceNumber) {
2380
- // replace because comes later
2381
- removalInfo.removedClientId = clientId;
2382
- removalInfo.removedSeq = seq;
2369
+ if (existingRemovalInfo.removedSeq === UnassignedSequenceNumber) {
2370
+ // we removed this locally, but someone else removed it first
2371
+ // so put them at the head of the list
2372
+ // the list isn't ordered, but we
2373
+ // keep first removal at the head.
2374
+ existingRemovalInfo.removedClientIds.unshift(clientId);
2375
+ existingRemovalInfo.removedSeq = seq;
2383
2376
  segment.localRemovedSeq = undefined;
2384
2377
  } else {
2385
2378
  // Do not replace earlier sequence number for remove
2386
- this.addOverlappingClient(removalInfo, clientId);
2379
+ existingRemovalInfo.removedClientIds.push(clientId);
2387
2380
  }
2388
2381
  } else {
2389
- removalInfo.removedClientId = clientId;
2390
- removalInfo.removedSeq = seq;
2382
+ segment.removedClientIds = [clientId];
2383
+ segment.removedSeq = seq;
2391
2384
  segment.localRemovedSeq = localSeq;
2392
2385
 
2393
2386
  removedSegments.push({ segment });
@@ -2399,9 +2392,7 @@ export class MergeTree {
2399
2392
 
2400
2393
  // Save segment so can assign removed sequence number when acked by server
2401
2394
  if (this.collabWindow.collaborating) {
2402
- // Use removal information
2403
- const _removalInfo: IRemovalInfo = segment;
2404
- if (_removalInfo.removedSeq === UnassignedSequenceNumber && clientId === this.collabWindow.clientId) {
2395
+ if (segment.removedSeq === UnassignedSequenceNumber && clientId === this.collabWindow.clientId) {
2405
2396
  segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
2406
2397
  } else {
2407
2398
  if (MergeTree.options.zamboniSegments) {
@@ -14,6 +14,7 @@ import {
14
14
  IRemovalInfo,
15
15
  ISegment,
16
16
  MergeTree,
17
+ toRemovalInfo,
17
18
  } from "./mergeTree";
18
19
 
19
20
  interface IOverlapClient {
@@ -81,7 +82,7 @@ export class PartialSequenceLengths {
81
82
  collabWindow: CollaborationWindow,
82
83
  recur = false) {
83
84
  let combinedPartialLengths = new PartialSequenceLengths(collabWindow.minSeq);
84
- PartialSequenceLengths.fromLeaves(mergeTree, combinedPartialLengths, block, collabWindow);
85
+ PartialSequenceLengths.fromLeaves(combinedPartialLengths, block, collabWindow);
85
86
  let prevPartial: PartialSequenceLength | undefined;
86
87
 
87
88
  function cloneOverlapRemoveClients(oldTree: RedBlackTree<number, IOverlapClient> | undefined) {
@@ -209,13 +210,13 @@ export class PartialSequenceLengths {
209
210
  }
210
211
 
211
212
  private static fromLeaves(
212
- mergeTree: MergeTree, combinedPartialLengths: PartialSequenceLengths,
213
+ combinedPartialLengths: PartialSequenceLengths,
213
214
  block: IMergeBlock, collabWindow: CollaborationWindow) {
214
215
  combinedPartialLengths.minLength = 0;
215
216
  combinedPartialLengths.segmentCount = block.childCount;
216
217
 
217
- function seqLTE(seq: number, minSeq: number) {
218
- return (seq !== UnassignedSequenceNumber) && (seq <= minSeq);
218
+ function seqLTE(seq: number | undefined, minSeq: number) {
219
+ return seq !== undefined && seq !== UnassignedSequenceNumber && seq <= minSeq;
219
220
  }
220
221
 
221
222
  for (let i = 0; i < block.childCount; i++) {
@@ -223,21 +224,19 @@ export class PartialSequenceLengths {
223
224
  if (child.isLeaf()) {
224
225
  // Leaf segment
225
226
  const segment = child;
226
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
227
- if (seqLTE(segment.seq!, collabWindow.minSeq)) {
227
+ if (seqLTE(segment.seq, collabWindow.minSeq)) {
228
228
  combinedPartialLengths.minLength += segment.cachedLength;
229
229
  } else {
230
230
  if (segment.seq !== UnassignedSequenceNumber) {
231
231
  PartialSequenceLengths.insertSegment(combinedPartialLengths, segment);
232
232
  }
233
233
  }
234
- const removalInfo: IRemovalInfo = segment;
235
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
236
- if (seqLTE(removalInfo.removedSeq!, collabWindow.minSeq)) {
234
+ const removalInfo = toRemovalInfo(segment);
235
+ if (seqLTE(removalInfo?.removedSeq, collabWindow.minSeq)) {
237
236
  combinedPartialLengths.minLength -= segment.cachedLength;
238
237
  } else {
239
- if ((removalInfo.removedSeq !== undefined) &&
240
- (removalInfo.removedSeq !== UnassignedSequenceNumber)) {
238
+ if (removalInfo !== undefined
239
+ && removalInfo.removedSeq !== UnassignedSequenceNumber) {
241
240
  PartialSequenceLengths.insertSegment(
242
241
  combinedPartialLengths,
243
242
  segment,
@@ -300,14 +299,15 @@ export class PartialSequenceLengths {
300
299
  let removeClientOverlap: number[] | undefined;
301
300
 
302
301
  if (removalInfo) {
303
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
304
- seq = removalInfo.removedSeq!;
302
+ seq = removalInfo.removedSeq;
305
303
  segmentLen = -segmentLen;
306
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
307
- clientId = removalInfo.removedClientId!;
308
- if (removalInfo.removedClientOverlap) {
309
- removeClientOverlap = removalInfo.removedClientOverlap;
310
- }
304
+ // this code still assume removed client id and
305
+ // overlap clients are separate. so we need to pull
306
+ // then apart first.
307
+ clientId = removalInfo.removedClientIds[0];
308
+ removeClientOverlap = removalInfo.removedClientIds.length > 1
309
+ ? removalInfo.removedClientIds.slice(1)
310
+ : undefined;
311
311
  }
312
312
 
313
313
  const seqPartials = combinedPartialLengths.partialLengths;
@@ -421,14 +421,14 @@ export class PartialSequenceLengths {
421
421
  segCount += branchPartialLengths.segmentCount;
422
422
  } else {
423
423
  const segment = child;
424
- const removalInfo: IRemovalInfo = segment;
424
+ const removalInfo = toRemovalInfo(segment);
425
425
 
426
426
  if (segment.seq === seq) {
427
- if (removalInfo.removedSeq !== seq) {
427
+ if (removalInfo?.removedSeq !== seq) {
428
428
  seqSeglen += segment.cachedLength;
429
429
  }
430
430
  } else {
431
- if (removalInfo.removedSeq === seq) {
431
+ if (removalInfo?.removedSeq === seq) {
432
432
  seqSeglen -= segment.cachedLength;
433
433
  }
434
434
  }
@@ -107,14 +107,11 @@ export class SnapshotLoader {
107
107
  // this will only cause problems if there is an overlapping delete
108
108
  // spanning the snapshot, which should be rare
109
109
  if (spec.removedClient !== undefined) {
110
- seg.removedClientId = this.client.getOrAddShortClientId(spec.removedClient);
110
+ seg.removedClientIds = [this.client.getOrAddShortClientId(spec.removedClient)];
111
111
  }
112
112
  if (spec.removedClientIds !== undefined) {
113
- seg.removedClientId = this.client.getOrAddShortClientId(spec.removedClientIds[0]);
114
- if(spec.removedClientIds.length > 1) {
115
- seg.removedClientOverlap = spec.removedClientIds.slice(1).map(
116
- (sid)=> this.client.getOrAddShortClientId(sid));
117
- }
113
+ seg.removedClientIds = spec.removedClientIds?.map(
114
+ (sid)=> this.client.getOrAddShortClientId(sid));
118
115
  }
119
116
  } else {
120
117
  seg = this.client.specToSegment(spec);
package/src/snapshotV1.ts CHANGED
@@ -220,15 +220,12 @@ export class SnapshotV1 {
220
220
  raw.removedSeq = segment.removedSeq;
221
221
 
222
222
  // back compat for when we split overlap and removed client
223
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
224
- raw.removedClient = this.getLongClientId(segment.removedClientId!);
223
+ raw.removedClient =
224
+ segment.removedClientIds !== undefined
225
+ ? this.getLongClientId(segment.removedClientIds[0])
226
+ : undefined;
225
227
 
226
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
227
- const removedClientIds = [segment.removedClientId!];
228
- if(segment.removedClientOverlap !== undefined) {
229
- removedClientIds.push(... segment.removedClientOverlap);
230
- }
231
- raw.removedClientIds = removedClientIds.map((id)=>this.getLongClientId(id));
228
+ raw.removedClientIds = segment.removedClientIds?.map((id)=>this.getLongClientId(id));
232
229
  }
233
230
 
234
231
  // Sanity check that we are preserving either the seq < minSeq or a removed segment's info.