@fluidframework/merge-tree 2.20.0 → 2.22.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.
Files changed (183) hide show
  1. package/.eslintrc.cjs +0 -1
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +1 -0
  4. package/dist/attributionCollection.js +5 -7
  5. package/dist/attributionCollection.js.map +1 -1
  6. package/dist/localReference.js +6 -8
  7. package/dist/localReference.js.map +1 -1
  8. package/dist/mergeTree.d.ts.map +1 -1
  9. package/dist/mergeTree.js +69 -34
  10. package/dist/mergeTree.js.map +1 -1
  11. package/dist/mergeTreeNodes.d.ts +15 -4
  12. package/dist/mergeTreeNodes.d.ts.map +1 -1
  13. package/dist/mergeTreeNodes.js +1 -1
  14. package/dist/mergeTreeNodes.js.map +1 -1
  15. package/dist/partialLengths.d.ts +114 -144
  16. package/dist/partialLengths.d.ts.map +1 -1
  17. package/dist/partialLengths.js +431 -525
  18. package/dist/partialLengths.js.map +1 -1
  19. package/dist/perspective.d.ts +10 -1
  20. package/dist/perspective.d.ts.map +1 -1
  21. package/dist/perspective.js +10 -1
  22. package/dist/perspective.js.map +1 -1
  23. package/dist/properties.d.ts.map +1 -1
  24. package/dist/properties.js +2 -3
  25. package/dist/properties.js.map +1 -1
  26. package/dist/revertibles.js +3 -3
  27. package/dist/revertibles.js.map +1 -1
  28. package/dist/segmentInfos.d.ts +3 -0
  29. package/dist/segmentInfos.d.ts.map +1 -1
  30. package/dist/segmentInfos.js.map +1 -1
  31. package/dist/segmentPropertiesManager.js +3 -3
  32. package/dist/segmentPropertiesManager.js.map +1 -1
  33. package/dist/snapshotLoader.js +2 -2
  34. package/dist/snapshotLoader.js.map +1 -1
  35. package/dist/sortedSegmentSet.d.ts +5 -3
  36. package/dist/sortedSegmentSet.d.ts.map +1 -1
  37. package/dist/sortedSegmentSet.js +33 -41
  38. package/dist/sortedSegmentSet.js.map +1 -1
  39. package/dist/sortedSet.d.ts +20 -3
  40. package/dist/sortedSet.d.ts.map +1 -1
  41. package/dist/sortedSet.js +23 -14
  42. package/dist/sortedSet.js.map +1 -1
  43. package/dist/test/Snapshot.perf.spec.js +1 -1
  44. package/dist/test/Snapshot.perf.spec.js.map +1 -1
  45. package/dist/test/client.applyMsg.spec.js +20 -0
  46. package/dist/test/client.applyMsg.spec.js.map +1 -1
  47. package/dist/test/client.applyStashedOpFarm.spec.js +1 -1
  48. package/dist/test/client.applyStashedOpFarm.spec.js.map +1 -1
  49. package/dist/test/client.attributionFarm.spec.js +1 -1
  50. package/dist/test/client.attributionFarm.spec.js.map +1 -1
  51. package/dist/test/client.localReference.spec.js +48 -0
  52. package/dist/test/client.localReference.spec.js.map +1 -1
  53. package/dist/test/client.obliterateFarm.spec.d.ts +12 -0
  54. package/dist/test/client.obliterateFarm.spec.d.ts.map +1 -0
  55. package/dist/test/client.obliterateFarm.spec.js +89 -0
  56. package/dist/test/client.obliterateFarm.spec.js.map +1 -0
  57. package/dist/test/client.reconnectFarm.spec.js +1 -1
  58. package/dist/test/client.reconnectFarm.spec.js.map +1 -1
  59. package/dist/test/client.searchForMarker.spec.js +2 -2
  60. package/dist/test/client.searchForMarker.spec.js.map +1 -1
  61. package/dist/test/mergeTreeOperationRunner.d.ts +7 -2
  62. package/dist/test/mergeTreeOperationRunner.d.ts.map +1 -1
  63. package/dist/test/mergeTreeOperationRunner.js +31 -14
  64. package/dist/test/mergeTreeOperationRunner.js.map +1 -1
  65. package/dist/test/obliterate.concurrent.spec.js +45 -1
  66. package/dist/test/obliterate.concurrent.spec.js.map +1 -1
  67. package/dist/test/obliterate.rangeExpansion.spec.js +81 -5
  68. package/dist/test/obliterate.rangeExpansion.spec.js.map +1 -1
  69. package/dist/test/obliterate.spec.js +3 -3
  70. package/dist/test/obliterate.spec.js.map +1 -1
  71. package/dist/test/obliterateOperations.d.ts +15 -0
  72. package/dist/test/obliterateOperations.d.ts.map +1 -0
  73. package/dist/test/obliterateOperations.js +132 -0
  74. package/dist/test/obliterateOperations.js.map +1 -0
  75. package/dist/test/partialSyncHelper.d.ts +42 -0
  76. package/dist/test/partialSyncHelper.d.ts.map +1 -0
  77. package/dist/test/partialSyncHelper.js +96 -0
  78. package/dist/test/partialSyncHelper.js.map +1 -0
  79. package/dist/test/revertibles.spec.js +3 -3
  80. package/dist/test/revertibles.spec.js.map +1 -1
  81. package/dist/test/sortedSegmentSet.spec.js +21 -0
  82. package/dist/test/sortedSegmentSet.spec.js.map +1 -1
  83. package/dist/test/testClient.d.ts +1 -1
  84. package/dist/test/testClient.d.ts.map +1 -1
  85. package/dist/test/testClient.js +1 -0
  86. package/dist/test/testClient.js.map +1 -1
  87. package/dist/test/testUtils.js +2 -2
  88. package/dist/test/testUtils.js.map +1 -1
  89. package/lib/attributionCollection.js +5 -7
  90. package/lib/attributionCollection.js.map +1 -1
  91. package/lib/localReference.js +6 -8
  92. package/lib/localReference.js.map +1 -1
  93. package/lib/mergeTree.d.ts.map +1 -1
  94. package/lib/mergeTree.js +69 -34
  95. package/lib/mergeTree.js.map +1 -1
  96. package/lib/mergeTreeNodes.d.ts +15 -4
  97. package/lib/mergeTreeNodes.d.ts.map +1 -1
  98. package/lib/mergeTreeNodes.js +1 -1
  99. package/lib/mergeTreeNodes.js.map +1 -1
  100. package/lib/partialLengths.d.ts +114 -144
  101. package/lib/partialLengths.d.ts.map +1 -1
  102. package/lib/partialLengths.js +432 -525
  103. package/lib/partialLengths.js.map +1 -1
  104. package/lib/perspective.d.ts +10 -1
  105. package/lib/perspective.d.ts.map +1 -1
  106. package/lib/perspective.js +10 -1
  107. package/lib/perspective.js.map +1 -1
  108. package/lib/properties.d.ts.map +1 -1
  109. package/lib/properties.js +2 -3
  110. package/lib/properties.js.map +1 -1
  111. package/lib/revertibles.js +3 -3
  112. package/lib/revertibles.js.map +1 -1
  113. package/lib/segmentInfos.d.ts +3 -0
  114. package/lib/segmentInfos.d.ts.map +1 -1
  115. package/lib/segmentInfos.js.map +1 -1
  116. package/lib/segmentPropertiesManager.js +3 -3
  117. package/lib/segmentPropertiesManager.js.map +1 -1
  118. package/lib/snapshotLoader.js +2 -2
  119. package/lib/snapshotLoader.js.map +1 -1
  120. package/lib/sortedSegmentSet.d.ts +5 -3
  121. package/lib/sortedSegmentSet.d.ts.map +1 -1
  122. package/lib/sortedSegmentSet.js +33 -41
  123. package/lib/sortedSegmentSet.js.map +1 -1
  124. package/lib/sortedSet.d.ts +20 -3
  125. package/lib/sortedSet.d.ts.map +1 -1
  126. package/lib/sortedSet.js +23 -14
  127. package/lib/sortedSet.js.map +1 -1
  128. package/lib/test/Snapshot.perf.spec.js +1 -1
  129. package/lib/test/Snapshot.perf.spec.js.map +1 -1
  130. package/lib/test/client.applyMsg.spec.js +20 -0
  131. package/lib/test/client.applyMsg.spec.js.map +1 -1
  132. package/lib/test/client.applyStashedOpFarm.spec.js +1 -1
  133. package/lib/test/client.applyStashedOpFarm.spec.js.map +1 -1
  134. package/lib/test/client.attributionFarm.spec.js +1 -1
  135. package/lib/test/client.attributionFarm.spec.js.map +1 -1
  136. package/lib/test/client.localReference.spec.js +48 -0
  137. package/lib/test/client.localReference.spec.js.map +1 -1
  138. package/lib/test/client.obliterateFarm.spec.d.ts +12 -0
  139. package/lib/test/client.obliterateFarm.spec.d.ts.map +1 -0
  140. package/lib/test/client.obliterateFarm.spec.js +88 -0
  141. package/lib/test/client.obliterateFarm.spec.js.map +1 -0
  142. package/lib/test/client.reconnectFarm.spec.js +1 -1
  143. package/lib/test/client.reconnectFarm.spec.js.map +1 -1
  144. package/lib/test/client.searchForMarker.spec.js +2 -2
  145. package/lib/test/client.searchForMarker.spec.js.map +1 -1
  146. package/lib/test/mergeTreeOperationRunner.d.ts +7 -2
  147. package/lib/test/mergeTreeOperationRunner.d.ts.map +1 -1
  148. package/lib/test/mergeTreeOperationRunner.js +31 -14
  149. package/lib/test/mergeTreeOperationRunner.js.map +1 -1
  150. package/lib/test/obliterate.concurrent.spec.js +45 -1
  151. package/lib/test/obliterate.concurrent.spec.js.map +1 -1
  152. package/lib/test/obliterate.rangeExpansion.spec.js +81 -5
  153. package/lib/test/obliterate.rangeExpansion.spec.js.map +1 -1
  154. package/lib/test/obliterate.spec.js +3 -3
  155. package/lib/test/obliterate.spec.js.map +1 -1
  156. package/lib/test/obliterateOperations.d.ts +15 -0
  157. package/lib/test/obliterateOperations.d.ts.map +1 -0
  158. package/lib/test/obliterateOperations.js +123 -0
  159. package/lib/test/obliterateOperations.js.map +1 -0
  160. package/lib/test/partialSyncHelper.d.ts +42 -0
  161. package/lib/test/partialSyncHelper.d.ts.map +1 -0
  162. package/lib/test/partialSyncHelper.js +92 -0
  163. package/lib/test/partialSyncHelper.js.map +1 -0
  164. package/lib/test/revertibles.spec.js +3 -3
  165. package/lib/test/revertibles.spec.js.map +1 -1
  166. package/lib/test/sortedSegmentSet.spec.js +21 -0
  167. package/lib/test/sortedSegmentSet.spec.js.map +1 -1
  168. package/lib/test/testClient.d.ts +1 -1
  169. package/lib/test/testClient.d.ts.map +1 -1
  170. package/lib/test/testClient.js +1 -0
  171. package/lib/test/testClient.js.map +1 -1
  172. package/lib/test/testUtils.js +2 -2
  173. package/lib/test/testUtils.js.map +1 -1
  174. package/package.json +21 -79
  175. package/src/mergeTree.ts +80 -28
  176. package/src/mergeTreeNodes.ts +15 -4
  177. package/src/partialLengths.ts +559 -776
  178. package/src/perspective.ts +10 -1
  179. package/src/properties.ts +2 -3
  180. package/src/segmentInfos.ts +3 -0
  181. package/src/snapshotLoader.ts +1 -1
  182. package/src/sortedSegmentSet.ts +41 -50
  183. package/src/sortedSet.ts +32 -16
@@ -3,37 +3,36 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
  import { assert } from "@fluidframework/core-utils/internal";
6
- import { RedBlackTree } from "./collections/index.js";
7
6
  import { UnassignedSequenceNumber } from "./constants.js";
8
- import { compareNumbers, seqLTE, } from "./mergeTreeNodes.js";
9
- import { toRemovalInfo, toMoveInfo, assertInserted, isRemoved, } from "./segmentInfos.js";
7
+ import { seqLTE, } from "./mergeTreeNodes.js";
8
+ import { toRemovalInfo, toMoveInfo, assertInserted } from "./segmentInfos.js";
10
9
  import { SortedSet } from "./sortedSet.js";
11
10
  class PartialSequenceLengthsSet extends SortedSet {
12
- getKey(item) {
13
- return item.seq;
11
+ compare(a, b) {
12
+ return a.seq - b.seq;
14
13
  }
15
14
  addOrUpdate(newItem, update) {
15
+ if (newItem.seglen === 0) {
16
+ // Don't bother doing any updates for deltas of 0.
17
+ return;
18
+ }
16
19
  const prev = this.latestLeq(newItem.seq);
17
20
  if (prev?.seq !== newItem.seq) {
18
21
  // new element, update len
19
22
  newItem.len = (prev?.len ?? 0) + newItem.seglen;
20
23
  }
21
24
  // update the len of all following elements
22
- for (let i = this.keySortedItems.length - 1; i >= 0; i--) {
23
- const element = this.keySortedItems[i];
25
+ for (let i = this.sortedItems.length - 1; i >= 0; i--) {
26
+ const element = this.sortedItems[i];
24
27
  if (!element || element.seq <= newItem.seq) {
25
28
  break;
26
29
  }
27
30
  element.len += newItem.seglen;
28
31
  }
29
32
  super.addOrUpdate(newItem, (currentPartial, partialLength) => {
33
+ assert(partialLength.clientId === currentPartial.clientId, 0xab6 /* clientId mismatch */);
30
34
  currentPartial.seglen += partialLength.seglen;
31
- if (partialLength.remoteObliteratedLen) {
32
- currentPartial.remoteObliteratedLen ?? (currentPartial.remoteObliteratedLen = 0);
33
- currentPartial.remoteObliteratedLen += partialLength.remoteObliteratedLen;
34
- }
35
35
  currentPartial.len += partialLength.seglen;
36
- combineOverlapClients(currentPartial, partialLength);
37
36
  });
38
37
  }
39
38
  /**
@@ -42,7 +41,7 @@ class PartialSequenceLengthsSet extends SortedSet {
42
41
  * @param key - sequence number
43
42
  */
44
43
  latestLeq(key) {
45
- return this.keySortedItems[this.latestLeqIndex(key)];
44
+ return this.sortedItems[this.latestLeqIndex(key)];
46
45
  }
47
46
  /**
48
47
  * Returns the partial length whose sequence number is the lowest sequence
@@ -51,7 +50,7 @@ class PartialSequenceLengthsSet extends SortedSet {
51
50
  */
52
51
  firstGte(key) {
53
52
  const { index } = this.findItemPosition({ seq: key, len: 0, seglen: 0 });
54
- return this.keySortedItems[index];
53
+ return this.sortedItems[index];
55
54
  }
56
55
  latestLeqIndex(key) {
57
56
  const { exists, index } = this.findItemPosition({ seq: key, len: 0, seglen: 0 });
@@ -61,17 +60,17 @@ class PartialSequenceLengthsSet extends SortedSet {
61
60
  const mindex = this.latestLeqIndex(minSeq);
62
61
  let minLength = 0;
63
62
  if (mindex >= 0) {
64
- minLength = this.keySortedItems[mindex].len;
63
+ minLength = this.sortedItems[mindex].len;
65
64
  const seqCount = this.size;
66
65
  if (mindex <= seqCount - 1) {
67
66
  // Still some entries remaining
68
67
  const remainingCount = seqCount - mindex - 1;
69
68
  // Copy down
70
69
  for (let i = 0; i < remainingCount; i++) {
71
- this.keySortedItems[i] = this.keySortedItems[i + mindex + 1];
72
- this.keySortedItems[i].len -= minLength;
70
+ this.sortedItems[i] = this.sortedItems[i + mindex + 1];
71
+ this.sortedItems[i].len -= minLength;
73
72
  }
74
- this.keySortedItems.length = remainingCount;
73
+ this.sortedItems.length = remainingCount;
75
74
  }
76
75
  }
77
76
  return minLength;
@@ -85,13 +84,13 @@ class PartialSequenceLengthsSet extends SortedSet {
85
84
  * "What is the length of `block` from the perspective of some particular seq and clientId?".
86
85
  *
87
86
  * It also supports incremental updating of state for newly-sequenced ops that don't affect the structure of the
88
- * MergeTree.
87
+ * MergeTree (in most cases--see AB#31003 or comments on {@link PartialSequenceLengths.update}).
89
88
  *
90
89
  * To answer these queries, it pre-builds several lists which track the length of the block at a per-sequence-number
91
90
  * level. These lists are:
92
91
  *
93
92
  * 1. (`partialLengths`): Stores the total length of the block.
94
- * 2. (`clientSeqNumbers[clientId]`): Stores only the total lengths of segments submitted by `clientId`. [see footnote]
93
+ * 2. (`perClientAdjustments[clientId]`): Stores adjustments to the base length which account for all changes submitted by `clientId`. [see footnote]
95
94
  *
96
95
  * The reason both lists are necessary is that resolving the length of the block from the perspective of
97
96
  * (clientId, refSeq) requires including both of the following types of segments:
@@ -114,15 +113,96 @@ class PartialSequenceLengthsSet extends SortedSet {
114
113
  * (length of the block at the minimum sequence number)
115
114
  * + (partialLengths total length at refSeq)
116
115
  * + (unsequenced edits' total length submitted before localSeq)
117
- * - (overlapping remove of the unsequenced edits' total length at refSeq)
116
+ * + (adjustments for changes double-counted by happening at or before both localSeq and refSeq)
118
117
  *
119
118
  * This algorithm scales roughly linearly with number of editing clients and the size of the collab window.
120
119
  * (certain unlikely sequences of operations may introduce log factors on those variables)
121
120
  *
122
- * Note: there is some slight complication with clientSeqNumbers resulting from the possibility of different clients
123
- * concurrently removing the same segment. See the field's documentation for more details.
121
+ * @privateRemarks
122
+ * If you are looking to understand this class in more detail, a suggested order of internalization is:
123
+ *
124
+ * 1. The above description and how it relates to the implementation of `getPartialLength` (which implements the above high-level description
125
+ * 2. `PartialSequenceLengthsSet`, which allows binary searching for overall length deltas at a given sequence number and handles updates.
126
+ * 3. The `fromLeaves` method, which is the base case for the [potential] recursion in `combine`
127
+ * 4. The logic in `combine` to aggregate smaller block entries into larger ones
128
+ * 5. The incremental code path of `update`
124
129
  */
125
130
  export class PartialSequenceLengths {
131
+ constructor(
132
+ /**
133
+ * The minimumSequenceNumber as defined by the collab window used in the last call to `update`,
134
+ * or if no such calls have been made, the one used on construction.
135
+ */
136
+ minSeq, computeLocalPartials) {
137
+ this.minSeq = minSeq;
138
+ /**
139
+ * Length of the block this PartialSequenceLength corresponds to when viewed at `minSeq`.
140
+ */
141
+ this.minLength = 0;
142
+ /**
143
+ * Total number of segments in the subtree rooted at the block this PartialSequenceLength corresponds to.
144
+ */
145
+ this.segmentCount = 0;
146
+ /**
147
+ * List of PartialSequenceLength objects--ordered by increasing seq--giving length information about
148
+ * the block associated with this PartialSequenceLengths object.
149
+ *
150
+ * `minLength + partialLengths[i].len` gives the length of this block when considering the perspective of an observer
151
+ * client who has received edits up to (and including) sequence number `i`.
152
+ */
153
+ this.partialLengths = new PartialSequenceLengthsSet();
154
+ /**
155
+ * perClientAdjustments[clientId] contains a PartialSequenceLengthsSet of adjustments to the observer client's
156
+ * perspective (see {@link PartialSequenceLengths.partialLengths}) necessary to account for changes made by
157
+ * that client.
158
+ *
159
+ * As per doc comment on {@link PartialSequenceLengths}, the overall adjustment performed for the perspective of
160
+ * (clientId, refSeq) is given by the sum of length deltas in `perClientAdjustments[clientId]`
161
+ * for all sequence numbers S such that S \>= refSeq.
162
+ *
163
+ * (since these are ordered by sequence number and we cache cumulative sums, this is implemented using two lookups and a subtraction).
164
+ *
165
+ * The specific adjustments are roughly categorized as follows:
166
+ *
167
+ * - Ops submitted by a given client generally receive a partial lengths entry corresponding to their sequence number.
168
+ * e.g. insert of "ABC" at seq 5 will have a per-client adjustment entry of \{ seq: 5, seglen: 3 \}.
169
+ *
170
+ * - When client A deletes a segment concurrently with client B and loses the race (B's delete is sequenced first),
171
+ * A's per-client adjustments will contain an entry with a negative `seglen` corresponding to the length of the segment
172
+ * and a sequence number corresponding to that of B's delete. It will *not* receive a per-client adjustment for its own delete.
173
+ * This ensures that for perspectives (A, refSeq), the deleted segment will show up as a negative delta for all values of refSeq, since:
174
+ * 1. For refSeq \< B's delete, the per-client adjustment will apply and be added to the total length
175
+ * 2. For refSeq \>= B's delete, B's partial length entry in the overall set will apply, and the per-client adjustment will not apply
176
+ *
177
+ * - When client A attempts to insert a segment into a location that is concurrently obliterated by client B immediately upon insertion,
178
+ * A's per-client adjustments will again not include an entry for its own insert.
179
+ * Instead, the entry which would normally contain `seq` equal to that of A's insert would instead have `seq` equal to that of B's obliterate.
180
+ * This gives the overall correct behavior: for any perspective which isn't client A, there is no adjustment necessary anywhere (it's as if
181
+ * the segment never existed). For client A's perspective, the segment should be considered visible until A has acked B's obliterate.
182
+ * This is accomplished as for the perspective (A, refSeq):
183
+ * 1. For refSeq \< B's obliterate, the segment length will be included as part of the per-client adjustment for A
184
+ * 2. For refSeq \>= B's obliterate, the segment will be omitted from the per-client adjustment for A
185
+ *
186
+ * Note that the special-casing for inserting segments that are immediately obliterated is only necessary for segments that never were visible
187
+ * in the tree. If an insert and obliterate are concurrent but the insert is sequenced first, the normal per-client adjustment is fine.
188
+ *
189
+ * The second case (overlapping removal) applies to any combination of remove / obliterate operations.
190
+ */
191
+ this.perClientAdjustments = [];
192
+ /**
193
+ * If incremental update of partial lengths fails, this gets set to the seq of the failed update.
194
+ * When higher up blocks attempt to incrementally update, they first check if the seq they are updating for
195
+ * matches this value. If it does, they propagate a full refresh instead.
196
+ */
197
+ this.lastIncrementalInvalidationSeq = Number.NEGATIVE_INFINITY;
198
+ if (computeLocalPartials) {
199
+ this.unsequencedRecords = {
200
+ partialLengths: new PartialSequenceLengthsSet(),
201
+ perRefSeqAdjustments: new Map(),
202
+ cachedAdjustmentByRefSeq: new Map(),
203
+ };
204
+ }
205
+ }
126
206
  /**
127
207
  * Combine the partial lengths of block's children
128
208
  * @param block - an interior node. If `recur` is false, it is assumed that each interior node child of this block
@@ -163,7 +243,7 @@ export class PartialSequenceLengths {
163
243
  const childPartialsLen = childPartials.length;
164
244
  const childPartialLengths = [];
165
245
  const childUnsequencedPartialLengths = [];
166
- const childOverlapRemoves = [];
246
+ const childPerRefSeqAdjustments = [];
167
247
  for (let i = 0; i < childPartialsLen; i++) {
168
248
  const { segmentCount, minLength, partialLengths, unsequencedRecords } = childPartials[i];
169
249
  combinedPartialLengths.segmentCount += segmentCount;
@@ -171,19 +251,43 @@ export class PartialSequenceLengths {
171
251
  childPartialLengths.push(partialLengths.items);
172
252
  if (unsequencedRecords) {
173
253
  childUnsequencedPartialLengths.push(unsequencedRecords.partialLengths.items);
174
- childOverlapRemoves.push(unsequencedRecords.overlappingRemoves);
254
+ childPerRefSeqAdjustments.push(unsequencedRecords.perRefSeqAdjustments);
175
255
  }
176
256
  }
177
257
  mergePartialLengths(childPartialLengths, combinedPartialLengths.partialLengths);
178
258
  if (computeLocalPartials) {
179
259
  combinedPartialLengths.unsequencedRecords = {
180
260
  partialLengths: mergePartialLengths(childUnsequencedPartialLengths),
181
- overlappingRemoves: [...mergeSortedListsBySeq(childOverlapRemoves)],
182
- cachedOverlappingByRefSeq: new Map(),
261
+ cachedAdjustmentByRefSeq: new Map(),
262
+ perRefSeqAdjustments: new Map(),
183
263
  };
264
+ for (const perRefSeq of childPerRefSeqAdjustments) {
265
+ for (const [refSeq, partials] of perRefSeq) {
266
+ let combinedPartials = combinedPartialLengths.unsequencedRecords.perRefSeqAdjustments.get(refSeq);
267
+ if (combinedPartials === undefined) {
268
+ combinedPartials = new PartialSequenceLengthsSet();
269
+ combinedPartialLengths.unsequencedRecords.perRefSeqAdjustments.set(refSeq, combinedPartials);
270
+ }
271
+ for (const item of partials.items) {
272
+ combinedPartials.addOrUpdate({ ...item });
273
+ }
274
+ }
275
+ }
184
276
  }
185
- for (const partial of combinedPartialLengths.partialLengths.items) {
186
- combinedPartialLengths.addClientSeqNumberFromPartial(partial);
277
+ // could merge these like we do above rather than do out of order like this
278
+ for (let i = 0; i < childPartialsLen; i++) {
279
+ const { perClientAdjustments } = childPartials[i];
280
+ if (perClientAdjustments.length > 0) {
281
+ for (let clientId = 0; clientId < perClientAdjustments.length; clientId++) {
282
+ const clientAdjustment = perClientAdjustments[clientId];
283
+ if (clientAdjustment === undefined) {
284
+ continue;
285
+ }
286
+ for (const partial of perClientAdjustments[clientId].items) {
287
+ combinedPartialLengths.addClientAdjustment(clientId, partial.seq, partial.seglen);
288
+ }
289
+ }
290
+ }
187
291
  }
188
292
  }
189
293
  // TODO: incremental zamboni during build
@@ -194,10 +298,9 @@ export class PartialSequenceLengths {
194
298
  return combinedPartialLengths;
195
299
  }
196
300
  /**
197
- * Creates and returns a PartialSequenceLengths structure that tracks the lengths of only the
198
- * leaf children of the provided MergeBlock.
301
+ * Create a `PartialSequenceLengths` which tracks only changes incurred by direct child leaves of `block`.
199
302
  */
200
- static fromLeaves(block, collabWindow, computeLocalPartials) {
303
+ static fromLeaves(block, collabWindow, computeLocalPartials, retry = true) {
201
304
  const combinedPartialLengths = new PartialSequenceLengths(collabWindow.minSeq, computeLocalPartials);
202
305
  combinedPartialLengths.segmentCount = block.childCount;
203
306
  for (let i = 0; i < block.childCount; i++) {
@@ -205,193 +308,152 @@ export class PartialSequenceLengths {
205
308
  if (child.isLeaf()) {
206
309
  // Leaf segment
207
310
  const segment = child;
208
- if (segment.seq !== undefined && seqLTE(segment.seq, collabWindow.minSeq)) {
209
- combinedPartialLengths.minLength += segment.cachedLength;
210
- }
211
- else {
212
- PartialSequenceLengths.insertSegment(combinedPartialLengths, segment);
213
- }
214
- const removalInfo = toRemovalInfo(segment);
215
311
  const moveInfo = toMoveInfo(segment);
216
- if ((removalInfo?.removedSeq !== undefined &&
217
- seqLTE(removalInfo.removedSeq, collabWindow.minSeq)) ||
218
- (moveInfo?.movedSeq !== undefined && seqLTE(moveInfo.movedSeq, collabWindow.minSeq))) {
219
- combinedPartialLengths.minLength -= segment.cachedLength;
312
+ if (moveInfo?.wasMovedOnInsert) {
313
+ PartialSequenceLengths.accountForMoveOnInsert(combinedPartialLengths, segment, collabWindow);
220
314
  }
221
- else if (removalInfo !== undefined || moveInfo !== undefined) {
222
- PartialSequenceLengths.insertSegment(combinedPartialLengths, segment, removalInfo, moveInfo);
315
+ else {
316
+ PartialSequenceLengths.accountForInsertion(combinedPartialLengths, segment, collabWindow);
317
+ PartialSequenceLengths.accountForRemoval(combinedPartialLengths, segment, collabWindow);
223
318
  }
224
319
  }
225
320
  }
226
- // Post-process correctly-ordered partials computing sums and creating
227
- // lists for each present client id
228
- const seqPartials = combinedPartialLengths.partialLengths;
229
- let prevLen = 0;
230
- for (const partial of seqPartials.items) {
231
- partial.len = prevLen + partial.seglen;
232
- prevLen = partial.len;
233
- combinedPartialLengths.addClientSeqNumberFromPartial(partial);
234
- }
235
- prevLen = 0;
236
- if (combinedPartialLengths.unsequencedRecords !== undefined) {
237
- const localPartials = combinedPartialLengths.unsequencedRecords.partialLengths;
238
- for (const partial of localPartials.items) {
239
- partial.len = prevLen + partial.seglen;
240
- prevLen = partial.len;
241
- }
242
- }
243
321
  PartialSequenceLengths.options.verifier?.(combinedPartialLengths);
244
322
  return combinedPartialLengths;
245
323
  }
246
- static getOverlapClients(overlapClientIds, seglen) {
247
- const bst = new RedBlackTree(compareNumbers);
248
- for (const clientId of overlapClientIds) {
249
- bst.put(clientId, { clientId, seglen });
250
- }
251
- return bst;
252
- }
253
- static accumulateRemoveClientOverlap(partialLength, overlapRemoveClientIds, seglen) {
254
- if (partialLength.overlapRemoveClients) {
255
- for (const clientId of overlapRemoveClientIds) {
256
- const overlapClientNode = partialLength.overlapRemoveClients.get(clientId);
257
- if (overlapClientNode) {
258
- overlapClientNode.data.seglen += seglen;
259
- }
260
- else {
261
- partialLength.overlapRemoveClients.put(clientId, { clientId, seglen });
262
- }
263
- }
324
+ /**
325
+ * Assuming this segment was moved on insertion, inserts length information about that operation
326
+ * into the appropriate per-client adjustments (the overall view needs no such adjustment since
327
+ * from an observing client's perspective, the segment never exists).
328
+ */
329
+ static accountForMoveOnInsert(combinedPartialLengths, segment, collabWindow) {
330
+ assertInserted(segment);
331
+ const moveInfo = toMoveInfo(segment);
332
+ assert(moveInfo?.wasMovedOnInsert === true, 0xab7 /* Segment was not moved on insert */);
333
+ if (moveInfo.movedSeq <= collabWindow.minSeq) {
334
+ // This segment was obliterated as soon as it was inserted, and everyone was aware of the obliterate.
335
+ // Thus every single client treats this segment as length 0 from every perspective, and no adjustments
336
+ // are necessary.
337
+ return;
264
338
  }
265
- else {
266
- partialLength.overlapRemoveClients = PartialSequenceLengths.getOverlapClients(overlapRemoveClientIds, seglen);
339
+ const isLocal = segment.seq === UnassignedSequenceNumber;
340
+ const clientId = segment.clientId;
341
+ const partials = isLocal
342
+ ? combinedPartialLengths.unsequencedRecords?.partialLengths
343
+ : combinedPartialLengths.partialLengths;
344
+ if (partials === undefined) {
345
+ // Local partial but its computation isn't required
346
+ return;
267
347
  }
268
- }
269
- static accumulateMoveClientOverlap(partialLength, overlapMoveClientIds, seglen) {
270
- if (partialLength.overlapObliterateClients) {
271
- for (const clientId of overlapMoveClientIds) {
272
- const overlapClientNode = partialLength.overlapObliterateClients.get(clientId);
273
- if (overlapClientNode) {
274
- overlapClientNode.data.seglen += seglen;
275
- }
276
- else {
277
- partialLength.overlapObliterateClients.put(clientId, { clientId, seglen });
278
- }
279
- }
348
+ if (isLocal) {
349
+ // Implication -> this is a local segment which will be obliterated as soon as it is acked.
350
+ // For refSeqs preceding that movedSeq and localSeqs following the localSeq, it will be visible.
351
+ // For the rest, it will not be visible.
352
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
353
+ const localSeq = segment.localSeq;
354
+ partials.addOrUpdate({
355
+ seq: localSeq,
356
+ len: 0,
357
+ seglen: segment.cachedLength,
358
+ clientId,
359
+ });
360
+ combinedPartialLengths.addLocalAdjustment({
361
+ refSeq: moveInfo.movedSeq,
362
+ localSeq,
363
+ seglen: -segment.cachedLength,
364
+ });
280
365
  }
281
366
  else {
282
- partialLength.overlapObliterateClients = PartialSequenceLengths.getOverlapClients(overlapMoveClientIds, seglen);
367
+ // Segment was obliterated on insert. Generally this means it should be visible only to the
368
+ // inserting client (in which case we add an adjustment to only that client's perspective),
369
+ // but if that client has also removed it, we don't need to add anything.
370
+ const removeInfo = toRemovalInfo(segment);
371
+ const wasRemovedByInsertingClient = removeInfo !== undefined && removeInfo.removedClientIds.includes(clientId);
372
+ const wasMovedByInsertingClient = moveInfo !== undefined && moveInfo.movedClientIds.includes(clientId);
373
+ if (!wasRemovedByInsertingClient && !wasMovedByInsertingClient) {
374
+ const moveSeq = moveInfo?.movedSeq;
375
+ assert(moveSeq !== undefined, 0xab8 /* ObliterateOnInsertion implies moveSeq is defined */);
376
+ combinedPartialLengths.addClientAdjustment(clientId, moveSeq, segment.cachedLength);
377
+ }
283
378
  }
284
379
  }
285
380
  /**
286
- * Coalesce overlapping move lengths for a partial length entry that already
287
- * exists
288
- *
289
- * @param segmentLen - Length of segment with overlapping moves
290
- * @param segment - Segment with overlapping moves
291
- * @param firstGte - Existing partial length entry
292
- * @param clientIds - Ids of clients that concurrently obliterated this segment
381
+ * Inserts length information about the insertion of `segment` into
382
+ * `combinedPartialLengths.partialLengths` and the appropriate per-client adjustments.
293
383
  */
294
- static accumulateMoveOverlapForExisting(segmentLen, segment, firstGte, clientIds) {
384
+ static accountForInsertion(combinedPartialLengths, segment, collabWindow) {
295
385
  assertInserted(segment);
296
- const nonInsertingClientIds = clientIds.filter((id) => id !== segment.clientId);
297
- PartialSequenceLengths.accumulateMoveClientOverlap(firstGte, nonInsertingClientIds, segmentLen);
298
- // if this segment was obliterated by the client that inserted it,
299
- // and if it overlaps with the obliterate of another client, we need to
300
- // take into account whether it was obliterated on insert by the other
301
- // client
302
- if (clientIds.length !== nonInsertingClientIds.length) {
303
- PartialSequenceLengths.accumulateMoveClientOverlap(firstGte, [segment.clientId], toMoveInfo(segment)?.wasMovedOnInsert ? -segment.cachedLength : segmentLen);
386
+ if (segment.seq !== undefined && seqLTE(segment.seq, collabWindow.minSeq)) {
387
+ combinedPartialLengths.minLength += segment.cachedLength;
388
+ return;
304
389
  }
305
- }
306
- /**
307
- * Tracks which clients have made concurrent obliterates.
308
- *
309
- * @param obliterateOverlapLen - Length of segment with overlap
310
- * @param clientIds - Ids of clients that have concurrently obliterated this
311
- * segment
312
- */
313
- static getMoveOverlapForExisting(segment, obliterateOverlapLen, clientIds) {
314
- assertInserted(segment);
315
- const nonInsertingClientIds = clientIds.filter((id) => id !== segment.clientId);
316
- const overlapObliterateClients = PartialSequenceLengths.getOverlapClients(nonInsertingClientIds, obliterateOverlapLen);
317
- if (clientIds.length !== nonInsertingClientIds.length) {
318
- overlapObliterateClients.put(segment.clientId, {
319
- clientId: segment.clientId,
320
- seglen: toMoveInfo(segment)?.wasMovedOnInsert
321
- ? -segment.cachedLength
322
- : obliterateOverlapLen,
323
- });
390
+ const isLocal = segment.seq === UnassignedSequenceNumber;
391
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
392
+ const seqOrLocalSeq = isLocal ? segment.localSeq : segment.seq;
393
+ const segmentLen = segment.cachedLength;
394
+ const clientId = segment.clientId;
395
+ const partials = isLocal
396
+ ? combinedPartialLengths.unsequencedRecords?.partialLengths
397
+ : combinedPartialLengths.partialLengths;
398
+ if (!partials) {
399
+ // Local partial but its computation isn't required
400
+ return;
324
401
  }
325
- return overlapObliterateClients;
326
- }
327
- static updatePartialsAfterInsertion(segment, segmentLen, remoteObliteratedLen, obliterateOverlapLen = segmentLen, partials, seq, clientId, removeClientOverlap, moveClientOverlap) {
328
- const firstGte = partials.firstGte(seq);
329
- let partialLengthEntry;
330
- if (firstGte?.seq === seq) {
331
- partialLengthEntry = firstGte;
332
- // Existing entry at this seq--this occurs for ops that insert/delete
333
- // more than one segment.
334
- partialLengthEntry.seglen += segmentLen;
335
- if (remoteObliteratedLen) {
336
- partialLengthEntry.remoteObliteratedLen ?? (partialLengthEntry.remoteObliteratedLen = 0);
337
- partialLengthEntry.remoteObliteratedLen += remoteObliteratedLen;
338
- }
339
- if (removeClientOverlap) {
340
- PartialSequenceLengths.accumulateRemoveClientOverlap(firstGte, removeClientOverlap, obliterateOverlapLen);
341
- }
342
- if (moveClientOverlap) {
343
- PartialSequenceLengths.accumulateMoveOverlapForExisting(obliterateOverlapLen, segment, firstGte, moveClientOverlap);
344
- }
402
+ if (isLocal) {
403
+ partials.addOrUpdate({
404
+ seq: seqOrLocalSeq,
405
+ clientId,
406
+ len: 0,
407
+ seglen: segmentLen,
408
+ });
345
409
  }
346
410
  else {
347
- const overlapObliterateClients = moveClientOverlap
348
- ? PartialSequenceLengths.getMoveOverlapForExisting(segment, obliterateOverlapLen, moveClientOverlap)
349
- : undefined;
350
- partialLengthEntry = {
351
- seq,
411
+ partials.addOrUpdate({
412
+ seq: seqOrLocalSeq,
352
413
  clientId,
353
414
  len: 0,
354
415
  seglen: segmentLen,
355
- remoteObliteratedLen,
356
- overlapRemoveClients: removeClientOverlap
357
- ? PartialSequenceLengths.getOverlapClients(removeClientOverlap, obliterateOverlapLen)
358
- : undefined,
359
- overlapObliterateClients,
360
- };
361
- partials.addOrUpdate(partialLengthEntry);
416
+ });
417
+ combinedPartialLengths.addClientAdjustment(clientId, seqOrLocalSeq, segmentLen);
362
418
  }
363
419
  }
364
420
  /**
365
- * Inserts length information about the insertion of `segment` into
366
- * `combinedPartialLengths.partialLengths`.
367
- *
368
- * Does not update the clientSeqNumbers field to account for this segment.
369
- *
370
- * If `removalInfo` or `moveInfo` are defined, this operation updates the
371
- * bookkeeping to account for the (re)moval of this segment at the (re)movedSeq
372
- * instead.
373
- *
374
- * When the insertion or (re)moval of the segment is un-acked and
375
- * `combinedPartialLengths` is meant to compute such records, this does the
376
- * analogous addition to the bookkeeping for the local segment in
377
- * `combinedPartialLengths.unsequencedRecords`.
421
+ * Inserts length information about the removal or obliteration of `segment` into
422
+ * `combinedPartialLengths.partialLengths` and the appropriate per-client adjustments.
378
423
  */
379
- static insertSegment(combinedPartialLengths, segment, removalInfo, moveInfo) {
424
+ static accountForRemoval(combinedPartialLengths, segment, collabWindow) {
380
425
  assertInserted(segment);
426
+ const removalInfo = toRemovalInfo(segment);
427
+ const moveInfo = toMoveInfo(segment);
428
+ if (!removalInfo && !moveInfo) {
429
+ return;
430
+ }
431
+ if ((removalInfo?.removedSeq !== undefined &&
432
+ seqLTE(removalInfo.removedSeq, collabWindow.minSeq)) ||
433
+ (moveInfo?.movedSeq !== undefined && seqLTE(moveInfo.movedSeq, collabWindow.minSeq))) {
434
+ combinedPartialLengths.minLength -= segment.cachedLength;
435
+ return;
436
+ }
381
437
  const removalIsLocal = !!removalInfo && removalInfo.removedSeq === UnassignedSequenceNumber;
382
438
  const moveIsLocal = !!moveInfo && moveInfo.movedSeq === UnassignedSequenceNumber;
383
- const isLocal = segment.seq === UnassignedSequenceNumber ||
384
- (!!removalInfo && removalIsLocal && (!moveInfo || moveIsLocal)) ||
385
- (!!moveInfo && moveIsLocal && (!removalInfo || removalIsLocal));
386
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
387
- let seqOrLocalSeq = isLocal ? segment.localSeq : segment.seq;
388
- let segmentLen = segment.cachedLength;
389
- let clientId = segment.clientId;
390
- let removeClientOverlap;
391
- let moveClientOverlap;
392
- let remoteObliteratedLen;
439
+ const isLocalInsertion = segment.seq === UnassignedSequenceNumber;
440
+ const isOnlyLocalRemoval = removalIsLocal && (!moveInfo || moveIsLocal);
441
+ const isOnlyLocalMove = moveIsLocal && (!removalInfo || removalIsLocal);
442
+ const isLocal = isLocalInsertion || isOnlyLocalRemoval || isOnlyLocalMove;
443
+ if (segment.seq === UnassignedSequenceNumber &&
444
+ !(removalIsLocal && (!moveInfo || moveIsLocal)) &&
445
+ !(moveIsLocal && (!removalInfo || removalIsLocal))) {
446
+ throw new Error("Should have handled this codepath in wasMovedOnInsertion");
447
+ }
448
+ const lenDelta = -segment.cachedLength;
449
+ let clientId;
450
+ let seqOrLocalSeq;
393
451
  // it's not possible to have an overlapping obliterate and remove that are both local
394
452
  assert((!moveIsLocal && !removalIsLocal) || moveIsLocal !== removalIsLocal, 0x870 /* overlapping local obliterate and remove */);
453
+ const clientsWithRemoveOrObliterate = new Set([
454
+ ...(removalInfo?.removedClientIds ?? []),
455
+ ...(moveInfo?.movedClientIds ?? []),
456
+ ]);
395
457
  const removeHappenedFirst = removalInfo &&
396
458
  (!moveInfo ||
397
459
  moveIsLocal ||
@@ -399,36 +461,17 @@ export class PartialSequenceLengths {
399
461
  if (removeHappenedFirst) {
400
462
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
401
463
  seqOrLocalSeq = removalIsLocal ? removalInfo.localRemovedSeq : removalInfo.removedSeq;
402
- segmentLen = -segmentLen;
403
464
  // The client who performed the remove is always stored
404
465
  // in the first position of removalInfo.
405
466
  clientId = removalInfo.removedClientIds[0];
406
- const hasOverlap = removalInfo.removedClientIds.length > 1;
407
- removeClientOverlap = hasOverlap ? removalInfo.removedClientIds : undefined;
408
467
  }
409
- else if (moveInfo) {
468
+ else {
469
+ assert(moveInfo !== undefined, 0xab9 /* Expected move to exist if remove either did not exist or didn't happen first */);
410
470
  // The client who performed the move is always stored
411
471
  // in the first position of moveInfo.
412
472
  clientId = moveInfo.movedClientIds[0];
413
473
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
414
474
  seqOrLocalSeq = moveIsLocal ? moveInfo.localMovedSeq : moveInfo.movedSeq;
415
- if (moveInfo.wasMovedOnInsert) {
416
- assert(moveInfo.movedSeq !== -1, 0x871 /* wasMovedOnInsert should only be set on acked obliterates */);
417
- segmentLen = 0;
418
- }
419
- else {
420
- segmentLen = -segmentLen;
421
- }
422
- const hasOverlap = moveInfo.movedClientIds.length > 1;
423
- moveClientOverlap = hasOverlap ? moveInfo.movedClientIds : undefined;
424
- } // BUG BUG: something fishy here around how/when move info is passed or not
425
- // this condition only hits if it is not passed, so we can't rely on the passed move info
426
- // and need to inspect the segment directly. maybe related to AB#15630.
427
- else if (toMoveInfo(segment)?.wasMovedOnInsert) {
428
- // if this segment was obliterated on insert, its length is only
429
- // visible to the client that inserted it
430
- segmentLen = 0;
431
- remoteObliteratedLen = segment.cachedLength;
432
475
  }
433
476
  const partials = isLocal
434
477
  ? combinedPartialLengths.unsequencedRecords?.partialLengths
@@ -437,137 +480,105 @@ export class PartialSequenceLengths {
437
480
  // Local partial but its computation isn't required
438
481
  return;
439
482
  }
440
- // overlapping move and remove, remove happened first
441
- if (moveInfo && removalInfo && removeHappenedFirst && !moveIsLocal) {
442
- // The client who performed the remove is always stored
443
- // in the first position of removalInfo.
444
- const moveClientId = moveInfo.movedClientIds[0];
445
- const hasOverlap = moveInfo.movedClientIds.length > 1;
446
- PartialSequenceLengths.updatePartialsAfterInsertion(segment, 0, -segment.cachedLength, segmentLen, partials, moveInfo.movedSeq, moveClientId, undefined, hasOverlap ? moveInfo.movedClientIds : undefined);
447
- }
448
- if (removalInfo && !removeHappenedFirst && !removalIsLocal) {
449
- const removeSeqOrLocalSeq = removalIsLocal
450
- ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
451
- removalInfo.localRemovedSeq
452
- : removalInfo.removedSeq;
453
- // The client who performed the remove is always stored
454
- // in the first position of removalInfo.
455
- const removeClientId = removalInfo.removedClientIds[0];
456
- const hasOverlap = removalInfo.removedClientIds.length > 1;
457
- PartialSequenceLengths.updatePartialsAfterInsertion(segment, 0, -segment.cachedLength, segmentLen, partials, removeSeqOrLocalSeq, removeClientId, hasOverlap ? removalInfo.removedClientIds : undefined, undefined);
483
+ if (isLocal) {
484
+ // The segment is either inserted only locally or removed/moved only locally.
485
+ // We already accounted for the insertion in the accountForInsertion codepath.
486
+ // Only thing left to do is account for the removal.
487
+ partials.addOrUpdate({
488
+ seq: seqOrLocalSeq,
489
+ clientId,
490
+ len: 0,
491
+ seglen: lenDelta,
492
+ });
458
493
  }
459
- PartialSequenceLengths.updatePartialsAfterInsertion(segment, segmentLen, remoteObliteratedLen, undefined, partials, seqOrLocalSeq, clientId, removeClientOverlap, moveClientOverlap);
460
- // todo: the below block needs to be changed to handle obliterate, which
461
- // doesn't have great support for reconnect at the moment. see ADO #3714
462
- const { unsequencedRecords } = combinedPartialLengths;
463
- if (unsequencedRecords &&
464
- removeClientOverlap &&
465
- isRemoved(segment) &&
466
- segment.localRemovedSeq !== undefined) {
467
- const localSeq = segment.localRemovedSeq;
468
- const localPartialLengthEntry = {
494
+ else {
495
+ partials.addOrUpdate({
469
496
  seq: seqOrLocalSeq,
470
- localSeq,
471
497
  clientId,
472
498
  len: 0,
473
- seglen: segmentLen,
474
- };
475
- let localIndexFirstGTE = 0;
476
- for (; localIndexFirstGTE < unsequencedRecords.overlappingRemoves.length; localIndexFirstGTE++) {
477
- if (unsequencedRecords.overlappingRemoves[localIndexFirstGTE].seq >= seqOrLocalSeq) {
478
- break;
499
+ seglen: lenDelta,
500
+ });
501
+ for (const id of clientsWithRemoveOrObliterate) {
502
+ if (id === collabWindow.clientId) {
503
+ // The local client also removed or obliterated this segment.
504
+ const localSeq = moveInfo?.localMovedSeq ?? removalInfo?.localRemovedSeq;
505
+ if (localSeq === undefined) {
506
+ // Sure, the local client did it--but that change was already acked.
507
+ // No need to account for it in the unsequenced records.
508
+ continue;
509
+ }
510
+ const { unsequencedRecords } = combinedPartialLengths;
511
+ if (!unsequencedRecords) {
512
+ // Local partial but its computation isn't required.
513
+ continue;
514
+ }
515
+ assert(localSeq !== undefined, 0xaba /* Local client was in move/removed client ids but segment has no local seq for either */);
516
+ unsequencedRecords.partialLengths.addOrUpdate({
517
+ seq: localSeq,
518
+ clientId: collabWindow.clientId,
519
+ seglen: lenDelta,
520
+ len: 0,
521
+ });
522
+ // Because we've included deltas which take effect when either of localSeq or refSeq are high enough,
523
+ // we need to offset this with an adjustment that takes effect when both are high enough.
524
+ combinedPartialLengths.addLocalAdjustment({
525
+ refSeq: seqOrLocalSeq,
526
+ localSeq,
527
+ // combinedPartialLengths.partialLengths has an entry removing this segment from a perspective >= seqOrLocalSeq.
528
+ // combinedPartialLengths.unsequencedRecords.partialLengths now has an entry removing this segment from a perspective
529
+ // with local seq >= `localSeq`.
530
+ // In order to only remove this segment once, we add back in the length (where this entry only takes effect when
531
+ // both above are true due to logic in computeOverallRefSeqAdjustment).
532
+ seglen: segment.cachedLength,
533
+ });
479
534
  }
480
- }
481
- insertIntoList(unsequencedRecords.overlappingRemoves, localIndexFirstGTE, localPartialLengthEntry);
482
- const tweakedLocalPartialEntry = {
483
- ...localPartialLengthEntry,
484
- seq: localSeq,
485
- };
486
- unsequencedRecords.partialLengths.addOrUpdate(tweakedLocalPartialEntry);
487
- }
488
- }
489
- static addSeq(partialLengths, seq, seqSeglen, remoteObliteratedLen, clientId) {
490
- let seqPartialLen;
491
- let penultPartialLen;
492
- let pLen = partialLengths.latestLeq(seq);
493
- if (pLen) {
494
- if (pLen.seq === seq) {
495
- seqPartialLen = pLen;
496
- pLen = partialLengths.latestLeq(seq - 1);
497
- if (pLen) {
498
- penultPartialLen = pLen;
535
+ else {
536
+ // Note that all clients that have a remove or obliterate operation on this segment
537
+ // use the seq of the winning move/obliterate in their per-client adjustments!
538
+ combinedPartialLengths.addClientAdjustment(id, seqOrLocalSeq, lenDelta);
539
+ // Also ensure that all these clients have seen the segment as inserted before being removed
540
+ // This is technically not necessary for removes (we never ask for the length of this block with
541
+ // respect to a refSeq which this entry would affect), but it's simpler to just add it here.
542
+ // We already add this entry as part of the accountForInsertion codepath for the client that
543
+ // actually did insert the segment, hence not doing so [again] here.
544
+ if (segment.seq > collabWindow.minSeq && id !== segment.clientId) {
545
+ combinedPartialLengths.addClientAdjustment(id, segment.seq, segment.cachedLength);
546
+ }
499
547
  }
500
548
  }
501
- else {
502
- penultPartialLen = pLen;
503
- }
504
- }
505
- const len = penultPartialLen === undefined ? seqSeglen : penultPartialLen.len + seqSeglen;
506
- if (seqPartialLen === undefined) {
507
- seqPartialLen = {
508
- clientId,
509
- len,
510
- seglen: seqSeglen,
511
- seq,
512
- remoteObliteratedLen,
513
- };
514
- partialLengths.addOrUpdate(seqPartialLen);
515
- }
516
- else {
517
- seqPartialLen.remoteObliteratedLen = remoteObliteratedLen;
518
- seqPartialLen.seglen = seqSeglen;
519
- seqPartialLen.len = len;
520
- // Assert client id matches
521
549
  }
522
550
  }
523
- constructor(
524
- /**
525
- * The minimumSequenceNumber as defined by the collab window used in the last call to `update`,
526
- * or if no such calls have been made, the one used on construction.
527
- */
528
- minSeq, computeLocalPartials) {
529
- this.minSeq = minSeq;
530
- /**
531
- * Length of the block this PartialSequenceLength corresponds to when viewed at `minSeq`.
532
- */
533
- this.minLength = 0;
534
- /**
535
- * Total number of segments in the subtree rooted at the block this PartialSequenceLength corresponds to.
536
- */
537
- this.segmentCount = 0;
538
- /**
539
- * List of PartialSequenceLength objects--ordered by increasing seq--giving length information about
540
- * the block associated with this PartialSequenceLengths object.
541
- *
542
- * `partialLengths[i].len` contains the length of this block considering only sequenced segments with
543
- * `sequenceNumber <= partialLengths[i].seq`.
544
- */
545
- this.partialLengths = new PartialSequenceLengthsSet();
551
+ // Assume: seq is latest sequence number; no structural change to sub-tree, but this partial lengths
552
+ // entry needs to account for the change made by the client with `clientId` at sequence number `seq`.
553
+ // (and `update` has been called on all descendant PartialSequenceLengths).
554
+ // This implementation does not support overlapping removes: callers should recompute partial lengths
555
+ // using `combine` when the change that has just been applied involves such an operation.
556
+ // TODO: assert client id matches
557
+ update(node, seq, clientId, collabWindow) {
558
+ // In the current implementation, this method gets invoked multiple times for the same sequence number (i.e. mid-operation).
559
+ // We counter this by first zeroing out existing entries from previous updates, but it isn't ideal.
560
+ // Even if we fix this at the merge-tree level, the same type of issue can crop up with grouped batching enabled.
561
+ const latest = this.partialLengths.latestLeq(seq);
562
+ if (latest?.seq === seq) {
563
+ this.partialLengths.addOrUpdate({ seq, len: 0, seglen: -latest.seglen, clientId });
564
+ }
565
+ // .forEach natively ignores undefined entries.
566
+ // eslint-disable-next-line unicorn/no-array-for-each
567
+ this.perClientAdjustments.forEach((clientAdjustments) => {
568
+ const leqPartial = clientAdjustments.latestLeq(seq);
569
+ if (leqPartial && leqPartial.seq === seq) {
570
+ this.addClientAdjustment(clientId, seq, -leqPartial.seglen);
571
+ }
572
+ });
546
573
  /**
547
- * clientSeqNumbers[clientId] is a list of partial lengths for sequenced ops which either:
548
- * - were submitted by `clientId`.
549
- * - deleted a range containing segments that were concurrently deleted by `clientId`
574
+ * If any of the changes made by the client at `seq` necessitate partial length entries at sequence numbers other than `seq`,
575
+ * this flag is set to true. This propagates upwards when aggregating parents as well.
550
576
  *
551
- * The second case is referred to as the "overlapping delete" case. It is necessary to avoid double-counting
552
- * the removal of those segments in queries including clientId.
577
+ * Note: it seems feasible to update parents more incrementally by tracking the changes made to child blocks for a given update.
578
+ * There isn't a great place for this information to flow today.
553
579
  */
554
- this.clientSeqNumbers = [];
555
- if (computeLocalPartials) {
556
- this.unsequencedRecords = {
557
- partialLengths: new PartialSequenceLengthsSet(),
558
- overlappingRemoves: [],
559
- cachedOverlappingByRefSeq: new Map(),
560
- };
561
- }
562
- }
563
- // Assume: seq is latest sequence number; no structural change to sub-tree, but a segment
564
- // with sequence number seq has been added within the sub-tree (and `update` has been called
565
- // on all descendant PartialSequenceLengths)
566
- // TODO: assert client id matches
567
- update(node, seq, clientId, collabWindow) {
568
- var _a;
580
+ let failIncrementalPropagation = false;
569
581
  let seqSeglen = 0;
570
- let remoteObliteratedLen = 0;
571
582
  let segCount = 0;
572
583
  // Compute length for seq across children
573
584
  for (let i = 0; i < node.childCount; i++) {
@@ -576,12 +587,6 @@ export class PartialSequenceLengths {
576
587
  const segment = child;
577
588
  const removalInfo = toRemovalInfo(segment);
578
589
  const moveInfo = toMoveInfo(segment);
579
- const removalIsLocal = !!removalInfo && removalInfo.removedSeq === UnassignedSequenceNumber;
580
- const moveIsLocal = !!moveInfo && moveInfo.movedSeq === UnassignedSequenceNumber;
581
- const removeHappenedFirst = removalInfo &&
582
- (!moveInfo ||
583
- moveIsLocal ||
584
- (!removalIsLocal && moveInfo.movedSeq > removalInfo.removedSeq));
585
590
  if (seq === segment.seq) {
586
591
  // if this segment was moved on insert, its length should
587
592
  // only be visible to the inserting client
@@ -589,36 +594,23 @@ export class PartialSequenceLengths {
589
594
  moveInfo &&
590
595
  moveInfo.movedSeq < segment.seq &&
591
596
  moveInfo.wasMovedOnInsert) {
592
- remoteObliteratedLen += segment.cachedLength;
597
+ this.addClientAdjustment(clientId, moveInfo.movedSeq, segment.cachedLength);
598
+ failIncrementalPropagation = true;
593
599
  }
594
600
  else {
595
601
  seqSeglen += segment.cachedLength;
602
+ this.addClientAdjustment(clientId, seq, segment.cachedLength);
596
603
  }
597
604
  }
598
- if (seq === removalInfo?.removedSeq) {
599
- // if the remove op happened before an overlapping obliterate,
600
- // all clients can see the remove at this seq. otherwise, only
601
- // the removing client is aware of the remove
602
- if (removeHappenedFirst) {
603
- seqSeglen -= segment.cachedLength;
604
- }
605
- else {
606
- remoteObliteratedLen -= segment.cachedLength;
607
- }
608
- }
609
- if (seq === moveInfo?.movedSeq) {
610
- if (removeHappenedFirst) {
611
- remoteObliteratedLen -= segment.cachedLength;
612
- }
613
- else if (moveInfo.wasMovedOnInsert &&
614
- segment.seq !== UnassignedSequenceNumber &&
615
- segment.seq !== undefined &&
616
- moveInfo.movedSeq > segment.seq) {
617
- remoteObliteratedLen += segment.cachedLength;
618
- seqSeglen -= segment.cachedLength;
619
- }
620
- else if (segment.seq !== UnassignedSequenceNumber) {
621
- seqSeglen -= segment.cachedLength;
605
+ const earlierDeletion = Math.min(removalInfo?.removedSeq ?? Number.MAX_VALUE, moveInfo?.movedSeq ?? Number.MAX_VALUE);
606
+ if (segment.seq !== UnassignedSequenceNumber && seq === earlierDeletion) {
607
+ seqSeglen -= segment.cachedLength;
608
+ if (clientId !== collabWindow.clientId) {
609
+ this.addClientAdjustment(clientId, seq, -segment.cachedLength);
610
+ if (segment.seq > collabWindow.minSeq && segment.clientId !== clientId) {
611
+ this.addClientAdjustment(clientId, segment.seq, segment.cachedLength);
612
+ failIncrementalPropagation = true;
613
+ }
622
614
  }
623
615
  }
624
616
  segCount++;
@@ -627,20 +619,35 @@ export class PartialSequenceLengths {
627
619
  const childBlock = child;
628
620
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
629
621
  const branchPartialLengths = childBlock.partialLengths;
622
+ if (branchPartialLengths.lastIncrementalInvalidationSeq === seq) {
623
+ // Bail out.
624
+ const newPartials = PartialSequenceLengths.combine(node, collabWindow, false);
625
+ newPartials.lastIncrementalInvalidationSeq = seq;
626
+ node.partialLengths = newPartials;
627
+ return;
628
+ }
630
629
  const partialLengths = branchPartialLengths.partialLengths;
631
630
  const leqPartial = partialLengths.latestLeq(seq);
632
631
  if (leqPartial && leqPartial.seq === seq) {
633
632
  seqSeglen += leqPartial.seglen;
634
- remoteObliteratedLen += leqPartial.remoteObliteratedLen ?? 0;
635
633
  }
636
634
  segCount += branchPartialLengths.segmentCount;
635
+ // .forEach natively ignores undefined entries.
636
+ // eslint-disable-next-line unicorn/no-array-for-each
637
+ branchPartialLengths.perClientAdjustments.forEach((clientAdjustments) => {
638
+ const leqBranchPartial = clientAdjustments.latestLeq(seq);
639
+ if (leqBranchPartial && leqBranchPartial.seq === seq) {
640
+ this.addClientAdjustment(clientId, seq, leqBranchPartial.seglen);
641
+ }
642
+ });
637
643
  }
638
644
  }
645
+ if (failIncrementalPropagation) {
646
+ this.lastIncrementalInvalidationSeq = seq;
647
+ }
639
648
  this.segmentCount = segCount;
640
649
  this.unsequencedRecords = undefined;
641
- PartialSequenceLengths.addSeq(this.partialLengths, seq, seqSeglen, remoteObliteratedLen, clientId);
642
- (_a = this.clientSeqNumbers)[clientId] ?? (_a[clientId] = new PartialSequenceLengthsSet());
643
- PartialSequenceLengths.addSeq(this.clientSeqNumbers[clientId], seq, seqSeglen + remoteObliteratedLen, undefined, clientId);
650
+ this.partialLengths.addOrUpdate({ seq, seglen: seqSeglen, len: 0, clientId });
644
651
  if (PartialSequenceLengths.options.zamboni) {
645
652
  this.zamboni(collabWindow);
646
653
  }
@@ -657,22 +664,18 @@ export class PartialSequenceLengths {
657
664
  * constructed with `computeLocalPartials` set to true and not subsequently updated with `update`.
658
665
  */
659
666
  getPartialLength(refSeq, clientId, localSeq) {
660
- let pLen = this.minLength;
661
- const cliLatestIndex = this.cliLatest(clientId);
662
- const cliSeq = this.clientSeqNumbers[clientId];
663
- pLen += this.partialLengths.latestLeq(refSeq)?.len ?? 0;
667
+ let length = this.minLength;
668
+ length += this.partialLengths.latestLeq(refSeq)?.len ?? 0;
664
669
  if (localSeq === undefined) {
665
- if (cliLatestIndex >= 0) {
666
- const cliLatest = cliSeq.items[cliLatestIndex];
667
- if (cliLatest.seq > refSeq) {
668
- // The client has local edits after refSeq, add in the length adjustments
669
- pLen += cliLatest.len;
670
- const precedingCli = this.cliLatestLEQ(clientId, refSeq);
671
- if (precedingCli) {
672
- // Subtract out double-counted lengths: segments still in the collab window but before
673
- // the refSeq submitted by the client we're querying for were counted in each addition above.
674
- pLen -= precedingCli.len;
675
- }
670
+ const latestClientEntry = this.latestClientEntry(clientId);
671
+ if (latestClientEntry !== undefined && latestClientEntry.seq > refSeq) {
672
+ // The client has local edits after refSeq, add in the length adjustments
673
+ length += latestClientEntry.len;
674
+ const precedingCli = this.latestClientEntryLEQ(clientId, refSeq);
675
+ if (precedingCli) {
676
+ // Subtract out double-counted lengths: segments still in the collab window but before
677
+ // the refSeq submitted by the client we're querying for were counted in each addition above.
678
+ length -= precedingCli.len;
676
679
  }
677
680
  }
678
681
  }
@@ -682,32 +685,16 @@ export class PartialSequenceLengths {
682
685
  // Local segments at or before localSeq should also be included
683
686
  const local = unsequencedPartialLengths.latestLeq(localSeq);
684
687
  if (local) {
685
- pLen += local.len;
686
- // Lastly, we must subtract out any double-counted removes, which occur if a currently un-acked local
687
- // remove overlaps with a remote client's remove that occurred at sequence number <=refSeq.
688
- pLen -= this.computeOverlappingLocalRemoves(refSeq, localSeq);
688
+ length += local.len;
689
+ // Lastly, we must add in any additional adjustment due to double-counting removes and obliterations
690
+ // removing local-only segments.
691
+ length += this.computeOverallRefSeqAdjustment(refSeq, localSeq);
689
692
  }
690
693
  }
691
- return pLen;
694
+ return length;
692
695
  }
693
696
  /**
694
- * Computes the seglen for the double-counted removed overlap at (refSeq, localSeq). This logic is equivalent
695
- * to the following:
696
- *
697
- * ```typescript
698
- * let total = 0;
699
- * for (const partialLength of this.unsequencedRecords!.overlappingRemoves) {
700
- * if (partialLength.seq > refSeq) {
701
- * break;
702
- * }
703
- *
704
- * if (partialLength.localSeq <= localSeq) {
705
- * total += partialLength.seglen;
706
- * }
707
- * }
708
- *
709
- * return total;
710
- * ```
697
+ * Computes the seglen for the double-counted removed overlap at (refSeq, localSeq).
711
698
  *
712
699
  * Reconnect happens to only need to compute these lengths for two refSeq values: before and
713
700
  * after the rebase. Since these lists potentially scale with O(collab window * number of local edits)
@@ -715,24 +702,29 @@ export class PartialSequenceLengths {
715
702
  * we cache the results for a given refSeq in `this.unsequencedRecords.cachedOverlappingByRefSeq` so
716
703
  * that they can be binary-searched the same way the usual partialLengths lists are.
717
704
  */
718
- computeOverlappingLocalRemoves(refSeq, localSeq) {
705
+ computeOverallRefSeqAdjustment(refSeq, localSeq) {
719
706
  if (this.unsequencedRecords === undefined) {
720
707
  return 0;
721
708
  }
722
- let cachedOverlapPartials = this.unsequencedRecords.cachedOverlappingByRefSeq.get(refSeq);
723
- if (!cachedOverlapPartials) {
709
+ let cachedAdjustment = this.unsequencedRecords.cachedAdjustmentByRefSeq.get(refSeq);
710
+ if (!cachedAdjustment) {
724
711
  const partials = new PartialSequenceLengthsSet();
725
- for (const partial of this.unsequencedRecords.overlappingRemoves) {
726
- if (partial.seq > refSeq) {
727
- break;
712
+ for (const [seq, adjustments,] of this.unsequencedRecords.perRefSeqAdjustments.entries()) {
713
+ if (seq > refSeq) {
714
+ // TODO: Prior code path got away with an early exit here by sorting the entries by refSeq.
715
+ // We could do the same here if we wanted.
716
+ // Old codepath basically flattened the 2d array into a 1d array with both dimensions listed.
717
+ continue;
718
+ }
719
+ for (const partial of adjustments.items) {
720
+ // This coalesces entries with the same localSeq as well as computes overall lengths.
721
+ partials.addOrUpdate({ ...partial });
728
722
  }
729
- partials.addOrUpdate({ ...partial, seq: partial.localSeq, len: 0 });
730
723
  }
731
- // This coalesces entries with the same localSeq as well as computes overall lengths.
732
- cachedOverlapPartials = partials;
733
- this.unsequencedRecords.cachedOverlappingByRefSeq.set(refSeq, cachedOverlapPartials);
724
+ cachedAdjustment = partials;
725
+ this.unsequencedRecords.cachedAdjustmentByRefSeq.set(refSeq, cachedAdjustment);
734
726
  }
735
- const overlap = cachedOverlapPartials.latestLeq(localSeq);
727
+ const overlap = cachedAdjustment.latestLeq(localSeq);
736
728
  return overlap?.len ?? 0;
737
729
  }
738
730
  toString(glc, indentCount = 0) {
@@ -741,12 +733,12 @@ export class PartialSequenceLengths {
741
733
  buf += `(${partial.seq},${partial.len}) `;
742
734
  }
743
735
  // eslint-disable-next-line @typescript-eslint/no-for-in-array, no-restricted-syntax
744
- for (const clientId in this.clientSeqNumbers) {
745
- if (this.clientSeqNumbers[clientId].size > 0) {
736
+ for (const clientId in this.perClientAdjustments) {
737
+ if (this.perClientAdjustments[clientId].size > 0) {
746
738
  buf += `Client `;
747
739
  buf += glc ? `${glc(+clientId)}` : `${clientId}`;
748
740
  buf += "[";
749
- for (const partial of this.clientSeqNumbers[clientId].items) {
741
+ for (const partial of this.perClientAdjustments[clientId].items) {
750
742
  buf += `(${partial.seq},${partial.len})`;
751
743
  }
752
744
  buf += "]";
@@ -760,49 +752,39 @@ export class PartialSequenceLengths {
760
752
  this.minLength += this.partialLengths.copyDown(segmentWindow.minSeq);
761
753
  this.minSeq = segmentWindow.minSeq;
762
754
  // eslint-disable-next-line @typescript-eslint/no-for-in-array, guard-for-in, no-restricted-syntax
763
- for (const clientId in this.clientSeqNumbers) {
764
- const cliPartials = this.clientSeqNumbers[clientId];
755
+ for (const clientId in this.perClientAdjustments) {
756
+ const cliPartials = this.perClientAdjustments[clientId];
765
757
  if (cliPartials) {
766
758
  cliPartials.copyDown(segmentWindow.minSeq);
767
759
  }
768
760
  }
769
761
  }
770
- addClientSeqNumber(clientId, seq, seglen) {
771
- var _a;
772
- (_a = this.clientSeqNumbers)[clientId] ?? (_a[clientId] = new PartialSequenceLengthsSet());
773
- const cli = this.clientSeqNumbers[clientId];
762
+ addClientAdjustment(clientId, seq, seglen) {
763
+ this.perClientAdjustments[clientId] ??= new PartialSequenceLengthsSet();
764
+ const cli = this.perClientAdjustments[clientId];
774
765
  cli.addOrUpdate({ seq, len: 0, seglen });
775
766
  }
776
- // Assumes sequence number already coalesced and that this is called in increasing `seq` order.
777
- addClientSeqNumberFromPartial(partialLength) {
778
- const seglen = partialLength.seglen + (partialLength.remoteObliteratedLen ?? 0);
779
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
780
- this.addClientSeqNumber(partialLength.clientId, partialLength.seq, seglen);
781
- if (partialLength.overlapRemoveClients) {
782
- partialLength.overlapRemoveClients.map((oc) => {
783
- // Original client entry was handled above
784
- if (partialLength.clientId !== oc.data.clientId) {
785
- this.addClientSeqNumber(oc.data.clientId, partialLength.seq, oc.data.seglen);
786
- }
787
- return true;
788
- });
789
- }
790
- if (partialLength.overlapObliterateClients) {
791
- partialLength.overlapObliterateClients.map((oc) => {
792
- // Original client entry was handled above
793
- if (partialLength.clientId !== oc.data.clientId) {
794
- this.addClientSeqNumber(oc.data.clientId, partialLength.seq, oc.data.seglen);
795
- }
796
- return true;
797
- });
798
- }
767
+ addLocalAdjustment({ refSeq, localSeq, seglen, }) {
768
+ assert(this.unsequencedRecords !== undefined, 0xabb /* Local adjustment computed without partials */);
769
+ const adjustments = this.unsequencedRecords.perRefSeqAdjustments.get(refSeq) ??
770
+ new PartialSequenceLengthsSet();
771
+ this.unsequencedRecords.perRefSeqAdjustments.set(refSeq, adjustments);
772
+ adjustments.addOrUpdate({ seq: localSeq, len: 0, seglen });
799
773
  }
800
- cliLatestLEQ(clientId, refSeq) {
801
- return this.clientSeqNumbers[clientId]?.latestLeq(refSeq);
774
+ /**
775
+ * Returns the partial lengths associated with the latest change associated with `clientId` at or before `refSeq`.
776
+ * Returns undefined if no such change exists.
777
+ */
778
+ latestClientEntryLEQ(clientId, refSeq) {
779
+ return this.perClientAdjustments[clientId]?.latestLeq(refSeq);
802
780
  }
803
- cliLatest(clientId) {
804
- const cliSeqs = this.clientSeqNumbers[clientId];
805
- return cliSeqs && cliSeqs.size > 0 ? cliSeqs.size - 1 : -1;
781
+ /**
782
+ * Get the partial lengths associated with the most recent change received by `clientId`, or undefined
783
+ * if this client has made no changes in this block within the collab window.
784
+ */
785
+ latestClientEntry(clientId) {
786
+ const cliSeqs = this.perClientAdjustments[clientId];
787
+ return cliSeqs && cliSeqs.size > 0 ? cliSeqs.items[cliSeqs.size - 1] : undefined;
806
788
  }
807
789
  }
808
790
  PartialSequenceLengths.options = {
@@ -847,20 +829,6 @@ function verifyPartialLengthsInner(partialSeqLengths, partialLengths, clientPart
847
829
  assert(false, 0x057 /* "Negative length after length adjustment!" */);
848
830
  }
849
831
  }
850
- if (partialLength.overlapRemoveClients) {
851
- // Only the flat partialLengths can have overlapRemoveClients, the per client view shouldn't
852
- assert(!clientPartials, 0x058 /* "Both overlapRemoveClients and clientPartials are set!" */);
853
- // Each overlap client counts as one, but the first remove to sequence was already counted.
854
- // (this aligns with the logic to omit the removing client in `addClientSeqNumberFromPartial`)
855
- count += partialLength.overlapRemoveClients.size() - 1;
856
- }
857
- if (partialLength.overlapObliterateClients) {
858
- // Only the flat partialLengths can have overlapObliterateClients, the per client view shouldn't
859
- assert(!clientPartials, 0x872 /* Both overlapObliterateClients and clientPartials are set! */);
860
- // Each overlap client counts as one, but the first move to sequence was already counted.
861
- // (this aligns with the logic to omit the moving client in `addClientSeqNumberFromPartial`)
862
- count += partialLength.overlapObliterateClients.size() - 1;
863
- }
864
832
  }
865
833
  return count;
866
834
  }
@@ -885,13 +853,15 @@ export function verifyExpectedPartialLengths(mergeTree, node, refSeq, clientId,
885
853
  }
886
854
  }
887
855
  if (expected !== partialLen) {
856
+ const nonIncrementalPartials = PartialSequenceLengths.combine(node, mergeTree.collabWindow, false, true);
857
+ const nonIncrementalLength = nonIncrementalPartials.getPartialLength(refSeq, clientId, localSeq);
888
858
  node.partialLengths?.getPartialLength(refSeq, clientId, localSeq);
889
- throw new Error(`expected partial length of ${expected} but found ${partialLen}. refSeq: ${refSeq}, clientId: ${clientId}`);
859
+ throw new Error(`expected partial length of ${expected} but found ${partialLen}. refSeq: ${refSeq}, clientId: ${clientId}. (non-incremental codepath returned ${nonIncrementalLength})`);
890
860
  }
891
861
  }
892
862
  export function verifyPartialLengths(partialSeqLengths) {
893
- if (partialSeqLengths["clientSeqNumbers"]) {
894
- for (const cliSeq of partialSeqLengths["clientSeqNumbers"]) {
863
+ if (partialSeqLengths["perClientAdjustments"]) {
864
+ for (const cliSeq of partialSeqLengths["perClientAdjustments"]) {
895
865
  if (cliSeq) {
896
866
  verifyPartialLengthsInner(partialSeqLengths, cliSeq, true);
897
867
  }
@@ -906,56 +876,6 @@ export function verifyPartialLengths(partialSeqLengths) {
906
876
  }
907
877
  }
908
878
  /* eslint-enable @typescript-eslint/dot-notation */
909
- /**
910
- * Clones an `overlapRemoveClients` red-black tree.
911
- */
912
- function cloneOverlapRemoveClients(oldTree) {
913
- if (!oldTree) {
914
- return undefined;
915
- }
916
- const newTree = new RedBlackTree(compareNumbers);
917
- oldTree.map((bProp) => {
918
- newTree.put(bProp.data.clientId, { ...bProp.data });
919
- return true;
920
- });
921
- return newTree;
922
- }
923
- function combineForOverlapClients(treeA, treeB) {
924
- if (treeA) {
925
- if (treeB) {
926
- treeB.map((bProp) => {
927
- const aProp = treeA.get(bProp.key);
928
- if (aProp) {
929
- aProp.data.seglen += bProp.data.seglen;
930
- }
931
- else {
932
- treeA.put(bProp.data.clientId, { ...bProp.data });
933
- }
934
- return true;
935
- });
936
- }
937
- }
938
- else {
939
- return cloneOverlapRemoveClients(treeB);
940
- }
941
- }
942
- /**
943
- * Combines the `overlapRemoveClients` and `overlapObliterateClients` fields of
944
- * two `PartialSequenceLength` objects, modifying the first PartialSequenceLength's
945
- * bookkeeping in-place.
946
- *
947
- * Combination is performed additively on `seglen` on a per-client basis.
948
- */
949
- export function combineOverlapClients(a, b) {
950
- const overlapRemoveClients = combineForOverlapClients(a.overlapRemoveClients, b.overlapRemoveClients);
951
- if (overlapRemoveClients) {
952
- a.overlapRemoveClients = overlapRemoveClients;
953
- }
954
- const overlapObliterateClients = combineForOverlapClients(a.overlapObliterateClients, b.overlapObliterateClients);
955
- if (overlapObliterateClients) {
956
- a.overlapObliterateClients = overlapObliterateClients;
957
- }
958
- }
959
879
  /**
960
880
  * Given a number of seq-sorted `partialLength` lists, merges them into a combined seq-sorted `partialLength`
961
881
  * list. This merge includes coalescing `PartialSequenceLength` entries at the same seq.
@@ -974,8 +894,6 @@ function mergePartialLengths(childPartialLengths, mergedLengths = new PartialSeq
974
894
  for (const partialLength of mergeSortedListsBySeq(childPartialLengths)) {
975
895
  mergedLengths.addOrUpdate({
976
896
  ...partialLength,
977
- overlapRemoveClients: cloneOverlapRemoveClients(partialLength.overlapRemoveClients),
978
- overlapObliterateClients: cloneOverlapRemoveClients(partialLength.overlapObliterateClients),
979
897
  });
980
898
  }
981
899
  return mergedLengths;
@@ -1022,15 +940,4 @@ function mergeSortedListsBySeq(lists) {
1022
940
  }
1023
941
  return { [Symbol.iterator]: () => new PartialSequenceLengthIterator(lists) };
1024
942
  }
1025
- function insertIntoList(list, index, elem) {
1026
- if (index < list.length) {
1027
- for (let k = list.length; k > index; k--) {
1028
- list[k] = list[k - 1];
1029
- }
1030
- list[index] = elem;
1031
- }
1032
- else {
1033
- list.push(elem);
1034
- }
1035
- }
1036
943
  //# sourceMappingURL=partialLengths.js.map