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