@fluidframework/merge-tree 1.2.7 → 2.0.0-dev.1.3.0.96595

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 (244) hide show
  1. package/.mocharc.js +12 -0
  2. package/README.md +2 -2
  3. package/dist/MergeTreeTextHelper.d.ts +23 -0
  4. package/dist/MergeTreeTextHelper.d.ts.map +1 -0
  5. package/dist/MergeTreeTextHelper.js +133 -0
  6. package/dist/MergeTreeTextHelper.js.map +1 -0
  7. package/dist/base.d.ts +2 -26
  8. package/dist/base.d.ts.map +1 -1
  9. package/dist/base.js.map +1 -1
  10. package/dist/client.d.ts +27 -16
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +81 -101
  13. package/dist/client.js.map +1 -1
  14. package/dist/collections/heap.d.ts +28 -0
  15. package/dist/collections/heap.d.ts.map +1 -0
  16. package/dist/collections/heap.js +65 -0
  17. package/dist/collections/heap.js.map +1 -0
  18. package/dist/collections/index.d.ts +11 -0
  19. package/dist/collections/index.d.ts.map +1 -0
  20. package/dist/collections/index.js +23 -0
  21. package/dist/collections/index.js.map +1 -0
  22. package/dist/collections/intervalTree.d.ts +60 -0
  23. package/dist/collections/intervalTree.d.ts.map +1 -0
  24. package/dist/collections/intervalTree.js +99 -0
  25. package/dist/collections/intervalTree.js.map +1 -0
  26. package/dist/collections/list.d.ts +39 -0
  27. package/dist/collections/list.d.ts.map +1 -0
  28. package/dist/collections/list.js +155 -0
  29. package/dist/collections/list.js.map +1 -0
  30. package/dist/collections/rbTree.d.ts +154 -0
  31. package/dist/collections/rbTree.d.ts.map +1 -0
  32. package/dist/{collections.js → collections/rbTree.js} +15 -478
  33. package/dist/collections/rbTree.js.map +1 -0
  34. package/dist/collections/stack.d.ts +16 -0
  35. package/dist/collections/stack.d.ts.map +1 -0
  36. package/dist/collections/stack.js +30 -0
  37. package/dist/collections/stack.js.map +1 -0
  38. package/dist/collections/tst.d.ts +55 -0
  39. package/dist/collections/tst.d.ts.map +1 -0
  40. package/dist/collections/tst.js +171 -0
  41. package/dist/collections/tst.js.map +1 -0
  42. package/dist/index.d.ts +3 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +4 -2
  45. package/dist/index.js.map +1 -1
  46. package/dist/localReference.d.ts +48 -99
  47. package/dist/localReference.d.ts.map +1 -1
  48. package/dist/localReference.js +132 -169
  49. package/dist/localReference.js.map +1 -1
  50. package/dist/mergeTree.d.ts +71 -302
  51. package/dist/mergeTree.d.ts.map +1 -1
  52. package/dist/mergeTree.js +395 -642
  53. package/dist/mergeTree.js.map +1 -1
  54. package/dist/mergeTreeDeltaCallback.d.ts +1 -1
  55. package/dist/mergeTreeDeltaCallback.d.ts.map +1 -1
  56. package/dist/mergeTreeDeltaCallback.js.map +1 -1
  57. package/dist/mergeTreeNodes.d.ts +344 -0
  58. package/dist/mergeTreeNodes.d.ts.map +1 -0
  59. package/dist/mergeTreeNodes.js +383 -0
  60. package/dist/mergeTreeNodes.js.map +1 -0
  61. package/dist/mergeTreeTracking.d.ts +1 -1
  62. package/dist/mergeTreeTracking.d.ts.map +1 -1
  63. package/dist/mergeTreeTracking.js.map +1 -1
  64. package/dist/opBuilder.d.ts +1 -1
  65. package/dist/opBuilder.d.ts.map +1 -1
  66. package/dist/opBuilder.js.map +1 -1
  67. package/dist/partialLengths.d.ts +188 -18
  68. package/dist/partialLengths.d.ts.map +1 -1
  69. package/dist/partialLengths.js +495 -253
  70. package/dist/partialLengths.js.map +1 -1
  71. package/dist/properties.d.ts.map +1 -1
  72. package/dist/properties.js.map +1 -1
  73. package/dist/referencePositions.d.ts +6 -26
  74. package/dist/referencePositions.d.ts.map +1 -1
  75. package/dist/referencePositions.js +3 -20
  76. package/dist/referencePositions.js.map +1 -1
  77. package/dist/segmentGroupCollection.d.ts +3 -1
  78. package/dist/segmentGroupCollection.d.ts.map +1 -1
  79. package/dist/segmentGroupCollection.js +14 -1
  80. package/dist/segmentGroupCollection.js.map +1 -1
  81. package/dist/segmentPropertiesManager.d.ts +10 -1
  82. package/dist/segmentPropertiesManager.d.ts.map +1 -1
  83. package/dist/segmentPropertiesManager.js +42 -13
  84. package/dist/segmentPropertiesManager.js.map +1 -1
  85. package/dist/snapshotChunks.d.ts +2 -1
  86. package/dist/snapshotChunks.d.ts.map +1 -1
  87. package/dist/snapshotChunks.js.map +1 -1
  88. package/dist/snapshotLoader.d.ts.map +1 -1
  89. package/dist/snapshotLoader.js.map +1 -1
  90. package/dist/snapshotV1.d.ts +1 -1
  91. package/dist/snapshotV1.d.ts.map +1 -1
  92. package/dist/snapshotV1.js +1 -1
  93. package/dist/snapshotV1.js.map +1 -1
  94. package/dist/snapshotlegacy.d.ts +5 -1
  95. package/dist/snapshotlegacy.d.ts.map +1 -1
  96. package/dist/snapshotlegacy.js +4 -0
  97. package/dist/snapshotlegacy.js.map +1 -1
  98. package/dist/sortedSegmentSet.d.ts +1 -1
  99. package/dist/sortedSegmentSet.d.ts.map +1 -1
  100. package/dist/sortedSegmentSet.js.map +1 -1
  101. package/dist/textSegment.d.ts +7 -7
  102. package/dist/textSegment.d.ts.map +1 -1
  103. package/dist/textSegment.js +3 -125
  104. package/dist/textSegment.js.map +1 -1
  105. package/{DEV.md → docs/DEV.md} +2 -2
  106. package/docs/Obliterate.md +639 -0
  107. package/{REFERENCEPOSITIONS.md → docs/REFERENCEPOSITIONS.md} +2 -2
  108. package/lib/MergeTreeTextHelper.d.ts +23 -0
  109. package/lib/MergeTreeTextHelper.d.ts.map +1 -0
  110. package/lib/MergeTreeTextHelper.js +129 -0
  111. package/lib/MergeTreeTextHelper.js.map +1 -0
  112. package/lib/base.d.ts +2 -26
  113. package/lib/base.d.ts.map +1 -1
  114. package/lib/base.js.map +1 -1
  115. package/lib/client.d.ts +27 -16
  116. package/lib/client.d.ts.map +1 -1
  117. package/lib/client.js +79 -99
  118. package/lib/client.js.map +1 -1
  119. package/lib/collections/heap.d.ts +28 -0
  120. package/lib/collections/heap.d.ts.map +1 -0
  121. package/lib/collections/heap.js +61 -0
  122. package/lib/collections/heap.js.map +1 -0
  123. package/lib/collections/index.d.ts +11 -0
  124. package/lib/collections/index.d.ts.map +1 -0
  125. package/lib/collections/index.js +11 -0
  126. package/lib/collections/index.js.map +1 -0
  127. package/lib/collections/intervalTree.d.ts +60 -0
  128. package/lib/collections/intervalTree.d.ts.map +1 -0
  129. package/lib/collections/intervalTree.js +94 -0
  130. package/lib/collections/intervalTree.js.map +1 -0
  131. package/lib/collections/list.d.ts +39 -0
  132. package/lib/collections/list.d.ts.map +1 -0
  133. package/lib/collections/list.js +149 -0
  134. package/lib/collections/list.js.map +1 -0
  135. package/lib/collections/rbTree.d.ts +154 -0
  136. package/lib/collections/rbTree.d.ts.map +1 -0
  137. package/lib/{collections.js → collections/rbTree.js} +14 -469
  138. package/lib/collections/rbTree.js.map +1 -0
  139. package/lib/collections/stack.d.ts +16 -0
  140. package/lib/collections/stack.d.ts.map +1 -0
  141. package/lib/collections/stack.js +26 -0
  142. package/lib/collections/stack.js.map +1 -0
  143. package/lib/collections/tst.d.ts +55 -0
  144. package/lib/collections/tst.d.ts.map +1 -0
  145. package/lib/collections/tst.js +167 -0
  146. package/lib/collections/tst.js.map +1 -0
  147. package/lib/index.d.ts +3 -1
  148. package/lib/index.d.ts.map +1 -1
  149. package/lib/index.js +3 -1
  150. package/lib/index.js.map +1 -1
  151. package/lib/localReference.d.ts +48 -99
  152. package/lib/localReference.d.ts.map +1 -1
  153. package/lib/localReference.js +132 -170
  154. package/lib/localReference.js.map +1 -1
  155. package/lib/mergeTree.d.ts +71 -302
  156. package/lib/mergeTree.d.ts.map +1 -1
  157. package/lib/mergeTree.js +371 -607
  158. package/lib/mergeTree.js.map +1 -1
  159. package/lib/mergeTreeDeltaCallback.d.ts +1 -1
  160. package/lib/mergeTreeDeltaCallback.d.ts.map +1 -1
  161. package/lib/mergeTreeDeltaCallback.js.map +1 -1
  162. package/lib/mergeTreeNodes.d.ts +344 -0
  163. package/lib/mergeTreeNodes.d.ts.map +1 -0
  164. package/lib/mergeTreeNodes.js +369 -0
  165. package/lib/mergeTreeNodes.js.map +1 -0
  166. package/lib/mergeTreeTracking.d.ts +1 -1
  167. package/lib/mergeTreeTracking.d.ts.map +1 -1
  168. package/lib/mergeTreeTracking.js.map +1 -1
  169. package/lib/opBuilder.d.ts +1 -1
  170. package/lib/opBuilder.d.ts.map +1 -1
  171. package/lib/opBuilder.js.map +1 -1
  172. package/lib/partialLengths.d.ts +188 -18
  173. package/lib/partialLengths.d.ts.map +1 -1
  174. package/lib/partialLengths.js +491 -249
  175. package/lib/partialLengths.js.map +1 -1
  176. package/lib/properties.d.ts.map +1 -1
  177. package/lib/properties.js.map +1 -1
  178. package/lib/referencePositions.d.ts +6 -26
  179. package/lib/referencePositions.d.ts.map +1 -1
  180. package/lib/referencePositions.js +3 -20
  181. package/lib/referencePositions.js.map +1 -1
  182. package/lib/segmentGroupCollection.d.ts +3 -1
  183. package/lib/segmentGroupCollection.d.ts.map +1 -1
  184. package/lib/segmentGroupCollection.js +14 -1
  185. package/lib/segmentGroupCollection.js.map +1 -1
  186. package/lib/segmentPropertiesManager.d.ts +10 -1
  187. package/lib/segmentPropertiesManager.d.ts.map +1 -1
  188. package/lib/segmentPropertiesManager.js +42 -13
  189. package/lib/segmentPropertiesManager.js.map +1 -1
  190. package/lib/snapshotChunks.d.ts +2 -1
  191. package/lib/snapshotChunks.d.ts.map +1 -1
  192. package/lib/snapshotChunks.js.map +1 -1
  193. package/lib/snapshotLoader.d.ts.map +1 -1
  194. package/lib/snapshotLoader.js.map +1 -1
  195. package/lib/snapshotV1.d.ts +1 -1
  196. package/lib/snapshotV1.d.ts.map +1 -1
  197. package/lib/snapshotV1.js +1 -1
  198. package/lib/snapshotV1.js.map +1 -1
  199. package/lib/snapshotlegacy.d.ts +5 -1
  200. package/lib/snapshotlegacy.d.ts.map +1 -1
  201. package/lib/snapshotlegacy.js +4 -0
  202. package/lib/snapshotlegacy.js.map +1 -1
  203. package/lib/sortedSegmentSet.d.ts +1 -1
  204. package/lib/sortedSegmentSet.d.ts.map +1 -1
  205. package/lib/sortedSegmentSet.js.map +1 -1
  206. package/lib/textSegment.d.ts +7 -7
  207. package/lib/textSegment.d.ts.map +1 -1
  208. package/lib/textSegment.js +1 -122
  209. package/lib/textSegment.js.map +1 -1
  210. package/package.json +99 -20
  211. package/src/MergeTreeTextHelper.ts +170 -0
  212. package/src/base.ts +2 -35
  213. package/src/client.ts +91 -111
  214. package/src/collections/heap.ts +75 -0
  215. package/src/collections/index.ts +11 -0
  216. package/src/collections/intervalTree.ts +146 -0
  217. package/src/collections/list.ts +165 -0
  218. package/src/{collections.ts → collections/rbTree.ts} +84 -563
  219. package/src/collections/stack.ts +27 -0
  220. package/src/collections/tst.ts +212 -0
  221. package/src/index.ts +8 -2
  222. package/src/localReference.ts +152 -203
  223. package/src/mergeTree.ts +578 -996
  224. package/src/mergeTreeDeltaCallback.ts +1 -1
  225. package/src/mergeTreeNodes.ts +752 -0
  226. package/src/mergeTreeTracking.ts +1 -1
  227. package/src/opBuilder.ts +1 -1
  228. package/src/partialLengths.ts +631 -258
  229. package/src/properties.ts +1 -0
  230. package/src/referencePositions.ts +10 -44
  231. package/src/segmentGroupCollection.ts +17 -2
  232. package/src/segmentPropertiesManager.ts +46 -12
  233. package/src/snapshotChunks.ts +2 -1
  234. package/src/snapshotLoader.ts +2 -1
  235. package/src/snapshotV1.ts +3 -3
  236. package/src/snapshotlegacy.ts +6 -2
  237. package/src/sortedSegmentSet.ts +1 -1
  238. package/src/textSegment.ts +10 -157
  239. package/dist/collections.d.ts +0 -197
  240. package/dist/collections.d.ts.map +0 -1
  241. package/dist/collections.js.map +0 -1
  242. package/lib/collections.d.ts +0 -197
  243. package/lib/collections.d.ts.map +0 -1
  244. package/lib/collections.js.map +0 -1
@@ -4,8 +4,7 @@
4
4
  */
5
5
 
6
6
  import { assert } from "@fluidframework/common-utils";
7
- import { Property } from "./base";
8
- import { RedBlackTree } from "./collections";
7
+ import { Property, RedBlackTree } from "./collections";
9
8
  import { UnassignedSequenceNumber } from "./constants";
10
9
  import {
11
10
  CollaborationWindow,
@@ -13,9 +12,8 @@ import {
13
12
  IMergeBlock,
14
13
  IRemovalInfo,
15
14
  ISegment,
16
- MergeTree,
17
15
  toRemovalInfo,
18
- } from "./mergeTree";
16
+ } from "./mergeTreeNodes";
19
17
 
20
18
  interface IOverlapClient {
21
19
  clientId: number;
@@ -47,18 +45,137 @@ function latestLEQ(a: PartialSequenceLength[], key: number) {
47
45
  return best;
48
46
  }
49
47
 
48
+ /**
49
+ * Tracks length information for a part of a MergeTree (block) at a given time (seq).
50
+ * These objects are associated with internal nodes (i.e. blocks).
51
+ */
50
52
  export interface PartialSequenceLength {
53
+ /**
54
+ * Sequence number
55
+ */
51
56
  seq: number;
57
+ /**
58
+ * The length of the associated block.
59
+ */
52
60
  len: number;
61
+ /**
62
+ * The delta between the current length of the associated block and its length at the previous seq number.
63
+ */
53
64
  seglen: number;
65
+ /**
66
+ * clientId for the client that submitted the op with sequence number `seq`.
67
+ */
54
68
  clientId?: number;
69
+ /**
70
+ * This field maps each client to the size of the intersection between segments deleted at this seq
71
+ * and segments concurrently deleted by that client.
72
+ *
73
+ * For example, this PartialSequenceLength:
74
+ * ```typescript
75
+ * {
76
+ * seq: 5,
77
+ * len: 100,
78
+ * seglen: -10,
79
+ * clientId: 0,
80
+ * overlapRemoveClients: <RedBlack tree with key-values expressed by>{
81
+ * 1: { clientId: 1, seglen: -5 },
82
+ * 3: { clientId: 3, seglen: -10 }
83
+ * }
84
+ * }
85
+ * ```
86
+ *
87
+ * corresponds to an op submitted by client 0 which:
88
+ * - reduces the length of this block by 10 (it may have deleted a single segment of length 10,
89
+ * several segments totalling length 10, or even delete and add content for a total reduction of 10 length)
90
+ * - was concurrent to an op submitted by client 1 that also removed some of the same segments,
91
+ * whose length totalled 5
92
+ * - was concurrent to an op submitted by client 3 that removed some of the same segments,
93
+ * whose length totalled 10
94
+ */
55
95
  overlapRemoveClients?: RedBlackTree<number, IOverlapClient>;
56
96
  }
57
97
 
98
+ interface UnsequencedPartialLengthInfo {
99
+ /**
100
+ * Contains entries for all local operations.
101
+ * The "seq" field of each entry actually corresponds to the delta at that localSeq on the local client.
102
+ */
103
+ partialLengths: PartialSequenceLength[];
104
+
105
+ /**
106
+ * Only contains entries for segments (or aggregates thereof) which were concurrently deleted
107
+ * by another client. Ordered by `seq` of the removing client.
108
+ *
109
+ * The "length" field of these entries is not populated. This is because pre-computing the lengths
110
+ * of segments doesn't help given the usage pattern.
111
+ *
112
+ * These entries need both `seq` and `localSeq`, because a given segment remove is double-counted iff
113
+ * the refSeq exceeds the seq of the remote remove AND the localSeq exceeds the localSeq of the local remove.
114
+ */
115
+ overlappingRemoves: LocalPartialSequenceLength[];
116
+
117
+ /**
118
+ * Cached keyed on refSeq which stores length information for the total overlap of removed segments at
119
+ * that refSeq.
120
+ * This information is derivable from the entries of `overlappingRemoves`.
121
+ *
122
+ * Like the `partialLengths` field, `seq` on each entry is actually the local seq.
123
+ * See `computeOverlappingLocalRemoves` for more information.
124
+ */
125
+ cachedOverlappingByRefSeq: Map<number, PartialSequenceLength[]>;
126
+ }
127
+
128
+ interface LocalPartialSequenceLength extends PartialSequenceLength {
129
+ /**
130
+ * Local sequence number
131
+ */
132
+ localSeq: number;
133
+ }
134
+
58
135
  /**
59
- * Keep track of partial sums of segment lengths for all sequence numbers
60
- * in the current collaboration window (if any). Only used during active
61
- * collaboration.
136
+ * Keeps track of partial sums of segment lengths for all sequence numbers in the current collaboration window.
137
+ * Only used during active collaboration.
138
+ *
139
+ * This class is associated with an internal node (block) of a MergeTree. It efficiently answers queries of the form
140
+ * "What is the length of `block` from the perspective of some particular seq and clientId?".
141
+ *
142
+ * It also supports incremental updating of state for newly-sequenced ops that don't affect the structure of the
143
+ * MergeTree.
144
+ *
145
+ * To answer these queries, it pre-builds several lists which track the length of the block at a per-sequence-number
146
+ * level. These lists are:
147
+ *
148
+ * 1. (`partialLengths`): Stores the total length of the block.
149
+ * 2. (`clientSeqNumbers[clientId]`): Stores only the total lengths of segments submitted by `clientId`. [see footnote]
150
+ *
151
+ * The reason both lists are necessary is that resolving the length of the block from the perspective of
152
+ * (clientId, refSeq) requires including both of the following types of segments:
153
+ * 1. Segments sequenced before `refSeq`
154
+ * 2. Segments submitted by `clientId`
155
+ *
156
+ * This is possible with the above bookkeeping, using:
157
+ *
158
+ * (length of the block at the minimum sequence number)
159
+ * + (partialLengths total length at refSeq)
160
+ * + (clientSeqNumbers total length at most recent op)
161
+ * - (clientSeqNumbers total length at refSeq)
162
+ *
163
+ * where the subtraction avoids double-counting segments submitted by clientId sequenced within the collab window.
164
+ *
165
+ * To enable reconnect, if constructed with `computeLocalPartials === true` it also supports querying for the length of
166
+ * the block from the perspective of the local client at a particular `refSeq` and `localSeq`. This computation is
167
+ * similar to the above:
168
+ *
169
+ * (length of the block at the minimum sequence number)
170
+ * + (partialLengths total length at refSeq)
171
+ * + (unsequenced edits' total length submitted before localSeq)
172
+ * - (overlapping remove of the unsequenced edits' total length at refSeq)
173
+ *
174
+ * This algorithm scales roughly linearly with number of editing clients and the size of the collab window.
175
+ * (certain unlikely sequences of operations may introduce log factors on those variables)
176
+ *
177
+ * Note: there is some slight complication with clientSeqNumbers resulting from the possibility of different clients
178
+ * concurrently removing the same segment. See the field's documentation for more details.
62
179
  */
63
180
  export class PartialSequenceLengths {
64
181
  public static options = {
@@ -66,135 +183,78 @@ export class PartialSequenceLengths {
66
183
  zamboni: true,
67
184
  };
68
185
 
69
- public static combine(mergeTree: MergeTree, block: IMergeBlock, collabWindow: CollaborationWindow, recur = false) {
70
- return PartialSequenceLengths.combineBranch(mergeTree, block, collabWindow, recur);
71
- }
72
-
73
186
  /**
74
187
  * Combine the partial lengths of block's children
75
- * @param block - an interior node; it is assumed that each interior node child of this block
76
- * has its partials up to date
77
- * @param collabWindow - segment window of the segment tree containing textSegmentBlock
188
+ * @param block - an interior node. If `recur` is false, it is assumed that each interior node child of this block
189
+ * has its partials up to date.
190
+ * @param collabWindow - segment window of the segment tree containing `block`.
191
+ * @param recur - whether to recursively compute partial lengths for internal children of `block`.
192
+ * This incurs more work, but gives correct bookkeeping in the case that a descendant in the merge tree has been
193
+ * modified without bubbling up the resulting partial length change to this block's partials.
194
+ * @param computeLocalPartials - whether to compute partial length information about local unsequenced ops.
195
+ * This enables querying for the length of the block at a given localSeq, but incurs extra work.
196
+ * Local partial information doesn't support `update`.
78
197
  */
79
- private static combineBranch(
80
- mergeTree: MergeTree,
198
+ public static combine(
81
199
  block: IMergeBlock,
82
200
  collabWindow: CollaborationWindow,
83
- recur = false) {
84
- let combinedPartialLengths = new PartialSequenceLengths(collabWindow.minSeq);
85
- PartialSequenceLengths.fromLeaves(combinedPartialLengths, block, collabWindow);
86
- let prevPartial: PartialSequenceLength | undefined;
87
-
88
- function cloneOverlapRemoveClients(oldTree: RedBlackTree<number, IOverlapClient> | undefined) {
89
- if (!oldTree) { return undefined; }
90
- const newTree = new RedBlackTree<number, IOverlapClient>(compareNumbers);
91
- oldTree.map((bProp: Property<number, IOverlapClient>) => {
92
- newTree.put(bProp.data.clientId, { ...bProp.data });
93
- return true;
94
- });
95
- return newTree;
96
- }
97
-
98
- function combineOverlapClients(a: PartialSequenceLength, b: PartialSequenceLength) {
99
- const overlapRemoveClientsA = a.overlapRemoveClients;
100
- if (overlapRemoveClientsA) {
101
- if (b.overlapRemoveClients) {
102
- b.overlapRemoveClients.map((bProp: Property<number, IOverlapClient>) => {
103
- const aProp = overlapRemoveClientsA.get(bProp.key);
104
- if (aProp) {
105
- aProp.data.seglen += bProp.data.seglen;
106
- } else {
107
- overlapRemoveClientsA.put(bProp.data.clientId, { ...bProp.data });
108
- }
109
- return true;
110
- });
111
- }
112
- } else {
113
- a.overlapRemoveClients = cloneOverlapRemoveClients(b.overlapRemoveClients);
114
- }
115
- }
116
-
117
- function addNext(partialLength: PartialSequenceLength) {
118
- const seq = partialLength.seq;
119
- let pLen = 0;
120
-
121
- if (prevPartial) {
122
- if (prevPartial.seq === partialLength.seq) {
123
- prevPartial.seglen += partialLength.seglen;
124
- prevPartial.len += partialLength.seglen;
125
- combineOverlapClients(prevPartial, partialLength);
126
- return;
127
- } else {
128
- pLen = prevPartial.len;
129
- // Previous sequence number is finished
130
- combinedPartialLengths.addClientSeqNumberFromPartial(prevPartial);
131
- }
132
- }
133
- prevPartial = {
134
- clientId: partialLength.clientId,
135
- len: pLen + partialLength.seglen,
136
- overlapRemoveClients: cloneOverlapRemoveClients(partialLength.overlapRemoveClients),
137
- seglen: partialLength.seglen,
138
- seq,
139
- };
140
- combinedPartialLengths.partialLengths.push(prevPartial);
141
- }
201
+ recur = false,
202
+ computeLocalPartials = false,
203
+ ): PartialSequenceLengths {
204
+ const leafPartialLengths = PartialSequenceLengths.fromLeaves(block, collabWindow, computeLocalPartials);
142
205
 
206
+ let hasInternalChild = false;
143
207
  const childPartials: PartialSequenceLengths[] = [];
144
208
  for (let i = 0; i < block.childCount; i++) {
145
209
  const child = block.children[i];
146
210
  if (!child.isLeaf()) {
147
- const childBlock = child;
211
+ hasInternalChild = true;
148
212
  if (recur) {
149
- childBlock.partialLengths =
150
- PartialSequenceLengths.combine(mergeTree, childBlock, collabWindow, true);
213
+ child.partialLengths =
214
+ PartialSequenceLengths.combine(child, collabWindow, true, computeLocalPartials);
151
215
  }
152
216
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
153
- childPartials.push(childBlock.partialLengths!);
217
+ childPartials.push(child.partialLengths!);
154
218
  }
155
219
  }
156
- let childPartialsLen = childPartials.length;
157
- if (childPartialsLen !== 0) {
158
- // Some children are interior nodes
159
- if (combinedPartialLengths.partialLengths.length > 0) {
220
+
221
+ // If there are no internal children, the PartialSequenceLengths returns from `fromLeaves` is exactly correct.
222
+ // Otherwise, we must additively combine all of the children partial lengths to get this block's totals.
223
+ const combinedPartialLengths = hasInternalChild ?
224
+ new PartialSequenceLengths(collabWindow.minSeq, computeLocalPartials) : leafPartialLengths;
225
+ if (hasInternalChild) {
226
+ if (leafPartialLengths.partialLengths.length > 0) {
160
227
  // Some children were leaves; add combined partials from these segments
161
- childPartials.push(combinedPartialLengths);
162
- childPartialsLen++;
163
- combinedPartialLengths = new PartialSequenceLengths(collabWindow.minSeq);
228
+ childPartials.push(leafPartialLengths);
164
229
  }
165
- const indices = new Array<number>(childPartialsLen);
166
- const childPartialsCounts = new Array<number>(childPartialsLen);
230
+
231
+ const childPartialsLen = childPartials.length;
232
+
233
+ const childPartialLengths: PartialSequenceLength[][] = [];
234
+ const childUnsequencedPartialLengths: PartialSequenceLength[][] = [];
235
+ const childOverlapRemoves: LocalPartialSequenceLength[][] = [];
167
236
  for (let i = 0; i < childPartialsLen; i++) {
168
- indices[i] = 0;
169
- childPartialsCounts[i] = childPartials[i].partialLengths.length;
170
- combinedPartialLengths.minLength += childPartials[i].minLength;
171
- combinedPartialLengths.segmentCount += childPartials[i].segmentCount;
172
- }
173
- let outerIndexOfEarliest = 0;
174
- let earliestPartialLength: PartialSequenceLength;
175
- while (outerIndexOfEarliest >= 0) {
176
- outerIndexOfEarliest = -1;
177
- for (let k = 0; k < childPartialsLen; k++) {
178
- // Find next earliest sequence number
179
- if (indices[k] < childPartialsCounts[k]) {
180
- const cpLen = childPartials[k].partialLengths[indices[k]];
181
-
182
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
183
- if ((outerIndexOfEarliest < 0) || (cpLen.seq < earliestPartialLength!.seq)) {
184
- outerIndexOfEarliest = k;
185
- earliestPartialLength = cpLen;
186
- }
187
- }
188
- }
189
- if (outerIndexOfEarliest >= 0) {
190
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
191
- addNext(earliestPartialLength!);
192
- indices[outerIndexOfEarliest]++;
237
+ const { segmentCount, minLength, partialLengths, unsequencedRecords } = childPartials[i];
238
+ combinedPartialLengths.segmentCount += segmentCount;
239
+ combinedPartialLengths.minLength += minLength;
240
+ childPartialLengths.push(partialLengths);
241
+ if (unsequencedRecords) {
242
+ childUnsequencedPartialLengths.push(unsequencedRecords.partialLengths);
243
+ childOverlapRemoves.push(unsequencedRecords.overlappingRemoves);
193
244
  }
194
245
  }
195
- // Add client entry for last partial, if any
196
- if (prevPartial) {
197
- combinedPartialLengths.addClientSeqNumberFromPartial(prevPartial);
246
+
247
+ combinedPartialLengths.partialLengths.push(...mergePartialLengths(childPartialLengths));
248
+ if (computeLocalPartials) {
249
+ combinedPartialLengths.unsequencedRecords = {
250
+ partialLengths: mergePartialLengths(childUnsequencedPartialLengths),
251
+ overlappingRemoves: Array.from(mergeSortedListsBySeq(childOverlapRemoves)),
252
+ cachedOverlappingByRefSeq: new Map(),
253
+ };
254
+ }
255
+
256
+ for (const partial of combinedPartialLengths.partialLengths) {
257
+ combinedPartialLengths.addClientSeqNumberFromPartial(partial);
198
258
  }
199
259
  }
200
260
  // TODO: incremental zamboni during build
@@ -203,16 +263,22 @@ export class PartialSequenceLengths {
203
263
  }
204
264
 
205
265
  if (PartialSequenceLengths.options.verify) {
206
- combinedPartialLengths.verify();
266
+ verify(combinedPartialLengths);
207
267
  }
208
268
 
209
269
  return combinedPartialLengths;
210
270
  }
211
271
 
272
+ /**
273
+ * @returns a PartialSequenceLengths structure which tracks only lengths of leaf children of the provided
274
+ * IMergeBlock.
275
+ */
212
276
  private static fromLeaves(
213
- combinedPartialLengths: PartialSequenceLengths,
214
- block: IMergeBlock, collabWindow: CollaborationWindow) {
215
- combinedPartialLengths.minLength = 0;
277
+ block: IMergeBlock,
278
+ collabWindow: CollaborationWindow,
279
+ computeLocalPartials: boolean,
280
+ ): PartialSequenceLengths {
281
+ const combinedPartialLengths = new PartialSequenceLengths(collabWindow.minSeq, computeLocalPartials);
216
282
  combinedPartialLengths.segmentCount = block.childCount;
217
283
 
218
284
  function seqLTE(seq: number | undefined, minSeq: number) {
@@ -227,21 +293,16 @@ export class PartialSequenceLengths {
227
293
  if (seqLTE(segment.seq, collabWindow.minSeq)) {
228
294
  combinedPartialLengths.minLength += segment.cachedLength;
229
295
  } else {
230
- if (segment.seq !== UnassignedSequenceNumber) {
231
- PartialSequenceLengths.insertSegment(combinedPartialLengths, segment);
232
- }
296
+ PartialSequenceLengths.insertSegment(combinedPartialLengths, segment);
233
297
  }
234
298
  const removalInfo = toRemovalInfo(segment);
235
299
  if (seqLTE(removalInfo?.removedSeq, collabWindow.minSeq)) {
236
300
  combinedPartialLengths.minLength -= segment.cachedLength;
237
- } else {
238
- if (removalInfo !== undefined
239
- && removalInfo.removedSeq !== UnassignedSequenceNumber) {
240
- PartialSequenceLengths.insertSegment(
241
- combinedPartialLengths,
242
- segment,
243
- removalInfo);
244
- }
301
+ } else if (removalInfo !== undefined) {
302
+ PartialSequenceLengths.insertSegment(
303
+ combinedPartialLengths,
304
+ segment,
305
+ removalInfo);
245
306
  }
246
307
  }
247
308
  }
@@ -256,9 +317,20 @@ export class PartialSequenceLengths {
256
317
  prevLen = seqPartials[i].len;
257
318
  combinedPartialLengths.addClientSeqNumberFromPartial(seqPartials[i]);
258
319
  }
320
+ prevLen = 0;
321
+
322
+ if (combinedPartialLengths.unsequencedRecords !== undefined) {
323
+ const localPartials = combinedPartialLengths.unsequencedRecords.partialLengths;
324
+ for (const partial of localPartials) {
325
+ partial.len = prevLen + partial.seglen;
326
+ prevLen = partial.len;
327
+ }
328
+ }
329
+
259
330
  if (PartialSequenceLengths.options.verify) {
260
- combinedPartialLengths.verify();
331
+ verify(combinedPartialLengths);
261
332
  }
333
+ return combinedPartialLengths;
262
334
  }
263
335
 
264
336
  private static getOverlapClients(overlapClientIds: number[], seglen: number) {
@@ -288,63 +360,114 @@ export class PartialSequenceLengths {
288
360
  }
289
361
  }
290
362
 
363
+ /**
364
+ * Inserts length information about the insertion of `segment` into `combinedPartialLengths.partialLengths`.
365
+ * Does not update the clientSeqNumbers field to account for this segment.
366
+ * If `removalInfo` is defined, this operation updates the bookkeeping to account for the removal of this
367
+ * segment at the removedSeq instead.
368
+ * When the insertion or removal of the segment is un-acked and `combinedPartialLengths` is meant to compute
369
+ * such records, this does the analogous addition to the bookkeeping for the local segment in
370
+ * `combinedPartialLengths.unsequencedRecords`.
371
+ */
291
372
  private static insertSegment(
292
373
  combinedPartialLengths: PartialSequenceLengths,
293
374
  segment: ISegment,
294
375
  removalInfo?: IRemovalInfo) {
376
+ const isLocal = (removalInfo === undefined && segment.seq === UnassignedSequenceNumber)
377
+ || (removalInfo !== undefined && segment.removedSeq === UnassignedSequenceNumber);
295
378
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296
- let seq = segment.seq!;
379
+ let seqOrLocalSeq = isLocal ? segment.localSeq! : segment.seq!;
297
380
  let segmentLen = segment.cachedLength;
298
381
  let clientId = segment.clientId;
299
382
  let removeClientOverlap: number[] | undefined;
300
383
 
301
384
  if (removalInfo) {
302
- seq = removalInfo.removedSeq;
385
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
386
+ seqOrLocalSeq = isLocal ? removalInfo.localRemovedSeq! : removalInfo.removedSeq;
303
387
  segmentLen = -segmentLen;
304
- // this code still assume removed client id and
305
- // overlap clients are separate. so we need to pull
306
- // then apart first.
388
+ // The client who performed the remove is always stored
389
+ // in the first position of removalInfo.
307
390
  clientId = removalInfo.removedClientIds[0];
308
- removeClientOverlap = removalInfo.removedClientIds.length > 1
309
- ? removalInfo.removedClientIds.slice(1)
310
- : undefined;
391
+ const hasOverlap = removalInfo.removedClientIds.length > 1;
392
+ removeClientOverlap = hasOverlap ? removalInfo.removedClientIds : undefined;
311
393
  }
312
394
 
313
- const seqPartials = combinedPartialLengths.partialLengths;
314
- const seqPartialsLen = seqPartials.length;
395
+ const partials = isLocal ?
396
+ combinedPartialLengths.unsequencedRecords?.partialLengths : combinedPartialLengths.partialLengths;
397
+ if (partials === undefined) {
398
+ // Local partial but its computation isn't required
399
+ return;
400
+ }
401
+ const partialsLen = partials.length;
315
402
  // Find the first entry with sequence number greater or equal to seq
316
403
  let indexFirstGTE = 0;
317
- for (; indexFirstGTE < seqPartialsLen; indexFirstGTE++) {
318
- if (seqPartials[indexFirstGTE].seq >= seq) {
404
+ for (; indexFirstGTE < partialsLen; indexFirstGTE++) {
405
+ if (partials[indexFirstGTE].seq >= seqOrLocalSeq) {
319
406
  break;
320
407
  }
321
408
  }
322
- if ((indexFirstGTE < seqPartialsLen) && (seqPartials[indexFirstGTE].seq === seq)) {
323
- seqPartials[indexFirstGTE].seglen += segmentLen;
409
+
410
+ let partialLengthEntry: PartialSequenceLength;
411
+ if (partials[indexFirstGTE]?.seq === seqOrLocalSeq) {
412
+ partialLengthEntry = partials[indexFirstGTE];
413
+ // Existing entry at this seq--this occurs for ops that insert/delete more than one segment.
414
+ partialLengthEntry.seglen += segmentLen;
324
415
  if (removeClientOverlap) {
325
416
  PartialSequenceLengths.accumulateRemoveClientOverlap(
326
- seqPartials[indexFirstGTE],
417
+ partials[indexFirstGTE],
327
418
  removeClientOverlap,
328
419
  segmentLen);
329
420
  }
330
421
  } else {
331
- let pLen: PartialSequenceLength;
332
- if (removeClientOverlap) {
333
- const overlapClients = PartialSequenceLengths.getOverlapClients(removeClientOverlap, segmentLen);
334
- pLen = { seq, clientId, len: 0, seglen: segmentLen, overlapRemoveClients: overlapClients };
335
- } else {
336
- pLen = { seq, clientId, len: 0, seglen: segmentLen };
422
+ partialLengthEntry = {
423
+ seq: seqOrLocalSeq,
424
+ clientId,
425
+ len: 0,
426
+ seglen: segmentLen,
427
+ overlapRemoveClients: removeClientOverlap
428
+ ? PartialSequenceLengths.getOverlapClients(removeClientOverlap, segmentLen)
429
+ : undefined,
430
+ };
431
+
432
+ // TODO: investigate performance improvement using BST
433
+ insertIntoList(partials, indexFirstGTE, partialLengthEntry);
434
+ }
435
+
436
+ const { unsequencedRecords } = combinedPartialLengths;
437
+ if (unsequencedRecords && removeClientOverlap && segment.localRemovedSeq !== undefined) {
438
+ const localSeq = segment.localRemovedSeq;
439
+ const localPartialLengthEntry: LocalPartialSequenceLength = {
440
+ seq: seqOrLocalSeq,
441
+ localSeq,
442
+ clientId,
443
+ len: 0,
444
+ seglen: segmentLen,
445
+ };
446
+ let localIndexFirstGTE = 0;
447
+ for (; localIndexFirstGTE < unsequencedRecords.overlappingRemoves.length; localIndexFirstGTE++) {
448
+ if (unsequencedRecords.overlappingRemoves[localIndexFirstGTE].seq >= seqOrLocalSeq) {
449
+ break;
450
+ }
337
451
  }
338
452
 
339
- if (indexFirstGTE < seqPartialsLen) {
340
- // Shift entries with greater sequence numbers
341
- // TODO: investigate performance improvement using BST
342
- for (let k = seqPartialsLen; k > indexFirstGTE; k--) {
343
- seqPartials[k] = seqPartials[k - 1];
453
+ insertIntoList(unsequencedRecords.overlappingRemoves, localIndexFirstGTE, localPartialLengthEntry);
454
+
455
+ localIndexFirstGTE = 0;
456
+ for (; localIndexFirstGTE < unsequencedRecords.partialLengths.length; localIndexFirstGTE++) {
457
+ if (unsequencedRecords.partialLengths[localIndexFirstGTE].seq >= localSeq) {
458
+ break;
344
459
  }
345
- seqPartials[indexFirstGTE] = pLen;
460
+ }
461
+
462
+ const tweakedLocalPartialEntry = {
463
+ ...localPartialLengthEntry,
464
+ seq: localSeq,
465
+ };
466
+
467
+ if (unsequencedRecords.partialLengths[localIndexFirstGTE]?.seq === localSeq) {
468
+ unsequencedRecords.partialLengths[localIndexFirstGTE].seglen += localPartialLengthEntry.seglen;
346
469
  } else {
347
- seqPartials.push(pLen);
470
+ insertIntoList(unsequencedRecords.partialLengths, localIndexFirstGTE, tweakedLocalPartialEntry);
348
471
  }
349
472
  }
350
473
  }
@@ -365,38 +488,79 @@ export class PartialSequenceLengths {
365
488
  penultPartialLen = pLen;
366
489
  }
367
490
  }
491
+ const len = penultPartialLen !== undefined ? penultPartialLen.len + seqSeglen : seqSeglen;
368
492
  if (seqPartialLen === undefined) {
369
- // len will be assigned below, making this assertion true.
370
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
371
493
  seqPartialLen = {
372
494
  clientId,
495
+ len,
373
496
  seglen: seqSeglen,
374
497
  seq,
375
- } as PartialSequenceLength;
498
+ };
376
499
  partialLengths.push(seqPartialLen);
377
500
  } else {
378
501
  seqPartialLen.seglen = seqSeglen;
502
+ seqPartialLen.len = len;
379
503
  // Assert client id matches
380
504
  }
381
- if (penultPartialLen !== undefined) {
382
- seqPartialLen.len = seqPartialLen.seglen + penultPartialLen.len;
383
- } else {
384
- seqPartialLen.len = seqPartialLen.seglen;
385
- }
386
505
  }
387
- public minLength = 0;
388
- public segmentCount = 0;
389
- public partialLengths: PartialSequenceLength[] = [];
390
- public clientSeqNumbers: PartialSequenceLength[][] = [];
391
506
 
392
- constructor(public minSeq: number) {
393
- }
507
+ /**
508
+ * Length of the block this PartialSequenceLength corresponds to when viewed at `minSeq`.
509
+ */
510
+ private minLength = 0;
511
+
512
+ /**
513
+ * Total number of segments in the subtree rooted at the block this PartialSequenceLength corresponds to.
514
+ */
515
+ private segmentCount = 0;
516
+
517
+ /**
518
+ * List of PartialSequenceLength objects--ordered by increasing seq--giving length information about
519
+ * the block associated with this PartialSequenceLengths object.
520
+ *
521
+ * `partialLengths[i].len` contains the length of this block considering only sequenced segments with
522
+ * `sequenceNumber <= partialLengths[i].seq`.
523
+ */
524
+ private readonly partialLengths: PartialSequenceLength[] = [];
525
+
526
+ /**
527
+ * clientSeqNumbers[clientId] is a list of partial lengths for sequenced ops which either:
528
+ * - were submitted by `clientId`.
529
+ * - deleted a range containing segments that were concurrently deleted by `clientId`
530
+ *
531
+ * The second case is referred to as the "overlapping delete" case. It is necessary to avoid double-counting
532
+ * the removal of those segments in queries including clientId.
533
+ */
534
+ private readonly clientSeqNumbers: PartialSequenceLength[][] = [];
535
+
536
+ /**
537
+ * Contains information required to answer queries for the length of this segment from the perspective of
538
+ * the local client but not including all local segments (i.e., `localSeq !== collabWindow.localSeq`).
539
+ * This field is only computed if requested in the constructor (i.e. `computeLocalPartials === true`).
540
+ */
541
+ private unsequencedRecords: UnsequencedPartialLengthInfo | undefined;
542
+
543
+ constructor(
544
+ /**
545
+ * The minimumSequenceNumber as defined by the collab window used in the last call to `update`,
546
+ * or if no such calls have been made, the one used on construction.
547
+ */
548
+ public minSeq: number,
549
+ computeLocalPartials: boolean) {
550
+ if (computeLocalPartials) {
551
+ this.unsequencedRecords = {
552
+ partialLengths: [],
553
+ overlappingRemoves: [],
554
+ cachedOverlappingByRefSeq: new Map(),
555
+ };
556
+ }
557
+ }
394
558
 
395
559
  // Assume: seq is latest sequence number; no structural change to sub-tree, but a segment
396
- // with sequence number seq has been added within the sub-tree
560
+ // with sequence number seq has been added within the sub-tree (and `update` has been called
561
+ // on all descendant PartialSequenceLengths)
397
562
  // TODO: assert client id matches
398
563
  public update(
399
- mergeTree: MergeTree,
400
564
  node: IMergeBlock,
401
565
  seq: number,
402
566
  clientId: number,
@@ -436,6 +600,7 @@ export class PartialSequenceLengths {
436
600
  }
437
601
  }
438
602
  this.segmentCount = segCount;
603
+ this.unsequencedRecords = undefined;
439
604
 
440
605
  PartialSequenceLengths.addSeq(this.partialLengths, seq, seqSeglen, clientId);
441
606
  if (this.clientSeqNumbers[clientId] === undefined) {
@@ -446,42 +611,110 @@ export class PartialSequenceLengths {
446
611
  this.zamboni(collabWindow);
447
612
  }
448
613
  if (PartialSequenceLengths.options.verify) {
449
- this.verify();
614
+ verify(this);
450
615
  }
451
616
  }
452
617
 
453
- public getPartialLength(refSeq: number, clientId: number) {
618
+ /**
619
+ * Returns the length of this block as viewed from the perspective of `clientId` at `refSeq`.
620
+ * This is the total length of all segments sequenced at or before refSeq OR submitted by `clientId`.
621
+ * If `clientId` is the local client, `localSeq` can also be provided. In that case, it is the total
622
+ * length of all segments submitted at or before `refSeq` in addition to any local, unacked segments
623
+ * with `segment.localSeq <= localSeq`.
624
+ *
625
+ * Note: the local case (where `localSeq !== undefined`) is only supported on a PartialSequenceLength object
626
+ * constructed with `computeLocalPartials` set to true and not subsequently updated with `update`.
627
+ */
628
+ public getPartialLength(refSeq: number, clientId: number, localSeq?: number) {
454
629
  let pLen = this.minLength;
455
630
  const seqIndex = latestLEQ(this.partialLengths, refSeq);
456
631
  const cliLatestIndex = this.cliLatest(clientId);
457
632
  const cliSeq = this.clientSeqNumbers[clientId];
458
633
  if (seqIndex >= 0) {
459
- // Add the partial length up to refSeq
460
634
  pLen += this.partialLengths[seqIndex].len;
635
+ }
461
636
 
637
+ if (localSeq === undefined) {
462
638
  if (cliLatestIndex >= 0) {
463
639
  const cliLatest = cliSeq[cliLatestIndex];
464
-
465
640
  if (cliLatest.seq > refSeq) {
466
641
  // The client has local edits after refSeq, add in the length adjustments
467
642
  pLen += cliLatest.len;
468
643
  const precedingCliIndex = this.cliLatestLEQ(clientId, refSeq);
469
644
  if (precedingCliIndex >= 0) {
645
+ // Subtract out double-counted lengths: segments still in the collab window but before
646
+ // the refSeq submitted by the client we're querying for were counted in each addition above.
470
647
  pLen -= cliSeq[precedingCliIndex].len;
471
648
  }
472
649
  }
473
650
  }
474
651
  } else {
475
- // RefSeq is before any of the partial lengths
476
- // so just add in all local edits of that client (which should all be after the refSeq)
477
- if (cliLatestIndex >= 0) {
478
- const cliLatest = cliSeq[cliLatestIndex];
479
- pLen += cliLatest.len;
652
+ assert(this.unsequencedRecords !== undefined,
653
+ 0x39f /* Local getPartialLength invoked without computing local partials. */);
654
+ const unsequencedPartialLengths = this.unsequencedRecords.partialLengths;
655
+ // Local segments at or before localSeq should also be included
656
+ const localIndex = latestLEQ(unsequencedPartialLengths, localSeq);
657
+ if (localIndex >= 0) {
658
+ pLen += unsequencedPartialLengths[localIndex].len;
659
+
660
+ // Lastly, we must subtract out any double-counted removes, which occur if a currently un-acked local
661
+ // remove overlaps with a remote client's remove that occurred at sequence number <=refSeq.
662
+ pLen -= this.computeOverlappingLocalRemoves(refSeq, localSeq);
480
663
  }
481
664
  }
482
665
  return pLen;
483
666
  }
484
667
 
668
+ /**
669
+ * Computes the seglen for the double-counted removed overlap at (refSeq, localSeq). This logic is equivalent
670
+ * to the following:
671
+ *
672
+ * ```typescript
673
+ * let total = 0;
674
+ * for (const partialLength of this.unsequencedRecords!.overlappingRemoves) {
675
+ * if (partialLength.seq > refSeq) {
676
+ * break;
677
+ * }
678
+ *
679
+ * if (partialLength.localSeq <= localSeq) {
680
+ * total += partialLength.seglen;
681
+ * }
682
+ * }
683
+ *
684
+ * return total;
685
+ * ```
686
+ *
687
+ * Reconnect happens to only need to compute these lengths for two refSeq values: before and
688
+ * after the rebase. Since these lists potentially scale with O(collab window * number of local edits)
689
+ * and potentially need to be queried for each local op that gets rebased,
690
+ * we cache the results for a given refSeq in `this.unsequencedRecords.cachedOverlappingByRefSeq` so
691
+ * that they can be binary-searched the same way the usual partialLengths lists are.
692
+ */
693
+ private computeOverlappingLocalRemoves(refSeq: number, localSeq: number): number {
694
+ if (this.unsequencedRecords === undefined) {
695
+ return 0;
696
+ }
697
+
698
+ let cachedOverlapPartials = this.unsequencedRecords.cachedOverlappingByRefSeq.get(refSeq);
699
+ if (!cachedOverlapPartials) {
700
+ const partials: PartialSequenceLength[] = [];
701
+ for (const partial of this.unsequencedRecords.overlappingRemoves) {
702
+ if (partial.seq > refSeq) {
703
+ break;
704
+ }
705
+
706
+ partials.push({ ...partial, seq: partial.localSeq, len: 0 });
707
+ }
708
+ partials.sort((a, b) => a.seq - b.seq);
709
+ // This coalesces entries with the same localSeq as well as computes overall lengths.
710
+ cachedOverlapPartials = mergePartialLengths([partials]);
711
+ this.unsequencedRecords.cachedOverlappingByRefSeq.set(refSeq, cachedOverlapPartials);
712
+ }
713
+
714
+ const overlapIndex = latestLEQ(cachedOverlapPartials, localSeq);
715
+ return overlapIndex >= 0 ? cachedOverlapPartials[overlapIndex].len : 0;
716
+ }
717
+
485
718
  public toString(glc?: (id: number) => string, indentCount = 0) {
486
719
  let buf = "";
487
720
  for (const partial of this.partialLengths) {
@@ -492,11 +725,7 @@ export class PartialSequenceLengths {
492
725
  for (const clientId in this.clientSeqNumbers) {
493
726
  if (this.clientSeqNumbers[clientId].length > 0) {
494
727
  buf += `Client `;
495
- if (glc) {
496
- buf += `${glc(+clientId)}`;
497
- } else {
498
- buf += `${clientId}`;
499
- }
728
+ buf += glc ? `${glc(+clientId)}` : `${clientId}`;
500
729
  buf += "[";
501
730
  for (const partial of this.clientSeqNumbers[clientId]) {
502
731
  buf += `(${partial.seq},${partial.len})`;
@@ -530,6 +759,7 @@ export class PartialSequenceLengths {
530
759
  return minLength;
531
760
  }
532
761
  this.minLength += copyDown(this.partialLengths);
762
+ this.minSeq = segmentWindow.minSeq;
533
763
  // eslint-disable-next-line @typescript-eslint/no-for-in-array, guard-for-in, no-restricted-syntax
534
764
  for (const clientId in this.clientSeqNumbers) {
535
765
  const cliPartials = this.clientSeqNumbers[clientId];
@@ -551,13 +781,16 @@ export class PartialSequenceLengths {
551
781
  cli.push({ seq, len: pLen, seglen });
552
782
  }
553
783
 
554
- // Assumes sequence number already coalesced
784
+ // Assumes sequence number already coalesced and that this is called in increasing `seq` order.
555
785
  private addClientSeqNumberFromPartial(partialLength: PartialSequenceLength) {
556
786
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
557
787
  this.addClientSeqNumber(partialLength.clientId!, partialLength.seq, partialLength.seglen);
558
788
  if (partialLength.overlapRemoveClients) {
559
789
  partialLength.overlapRemoveClients.map((oc: Property<number, IOverlapClient>) => {
560
- this.addClientSeqNumber(oc.data.clientId, partialLength.seq, oc.data.seglen);
790
+ // Original client entry was handled above
791
+ if (partialLength.clientId !== oc.data.clientId) {
792
+ this.addClientSeqNumber(oc.data.clientId, partialLength.seq, oc.data.seglen);
793
+ }
561
794
  return true;
562
795
  });
563
796
  }
@@ -565,95 +798,235 @@ export class PartialSequenceLengths {
565
798
 
566
799
  private cliLatestLEQ(clientId: number, refSeq: number) {
567
800
  const cliSeqs = this.clientSeqNumbers[clientId];
568
- if (cliSeqs) {
569
- return latestLEQ(cliSeqs, refSeq);
570
- } else {
571
- return -1;
572
- }
801
+ return cliSeqs ? latestLEQ(cliSeqs, refSeq) : -1;
573
802
  }
574
803
 
575
804
  private cliLatest(clientId: number) {
576
805
  const cliSeqs = this.clientSeqNumbers[clientId];
577
- if (cliSeqs && (cliSeqs.length > 0)) {
578
- return cliSeqs.length - 1;
579
- } else {
580
- return -1;
581
- }
806
+ return cliSeqs && (cliSeqs.length > 0) ? cliSeqs.length - 1 : -1;
582
807
  }
808
+ }
583
809
 
584
- // Debug only
585
- private verifyPartialLengths(partialLengths: PartialSequenceLength[], clientPartials: boolean) {
586
- if (partialLengths.length === 0) { return 0; }
810
+ /* eslint-disable @typescript-eslint/dot-notation */
811
+ function verifyPartialLengths(
812
+ partialSeqLengths: PartialSequenceLengths,
813
+ partialLengths: PartialSequenceLength[],
814
+ clientPartials: boolean,
815
+ ) {
816
+ if (partialLengths.length === 0) { return 0; }
587
817
 
588
- let lastSeqNum = 0;
589
- let accumSegLen = 0;
590
- let count = 0;
818
+ let lastSeqNum = 0;
819
+ let accumSegLen = 0;
820
+ let count = 0;
591
821
 
592
- for (const partialLength of partialLengths) {
593
- // Count total number of partial length
594
- count++;
822
+ for (const partialLength of partialLengths) {
823
+ // Count total number of partial length
824
+ count++;
595
825
 
596
- // Sequence number should be larger or equal to minseq
597
- assert(this.minSeq <= partialLength.seq, 0x054 /* "Sequence number less than minSeq!" */);
826
+ // Sequence number should be larger or equal to minseq
827
+ assert(partialSeqLengths.minSeq <= partialLength.seq, 0x054 /* "Sequence number less than minSeq!" */);
598
828
 
599
- // Sequence number should be sorted
600
- assert(lastSeqNum < partialLength.seq, 0x055 /* "Sequence number is not sorted!" */);
601
- lastSeqNum = partialLength.seq;
829
+ // Sequence number should be sorted
830
+ assert(lastSeqNum < partialLength.seq, 0x055 /* "Sequence number is not sorted!" */);
831
+ lastSeqNum = partialLength.seq;
602
832
 
603
- // Len is a accumulation of all the seglen adjustments
604
- accumSegLen += partialLength.seglen;
605
- if (accumSegLen !== partialLength.len) {
606
- assert(false, 0x056 /* "Unexpected total for accumulation of all seglen adjustments!" */);
607
- }
833
+ // Len is a accumulation of all the seglen adjustments
834
+ accumSegLen += partialLength.seglen;
835
+ if (accumSegLen !== partialLength.len) {
836
+ assert(false, 0x056 /* "Unexpected total for accumulation of all seglen adjustments!" */);
837
+ }
608
838
 
609
- if (clientPartials) {
610
- // Client partials used to track local edits so we can account for them some refSeq.
611
- // But the information we keep track of are since minSeq, so we keep track of more history
612
- // then needed, and some of them doesn't make sense to be used for length calculations
613
- // e.g. if you have this sequence, where the minSeq is #5 because of other clients
614
- // seq 10: client 1: insert seg #1
615
- // seq 11: client 2: delete seg #2 refseq: 10
616
- // minLength is 0, we would have keep a record of seglen: -1 for clientPartialLengths for client 2
617
- // So if you ask for partial length for client 2 @ seq 5, we will have return -1.
618
- // However, that combination is invalid, since we should never see any ops with refseq < 10 for
619
- // client 2 after seq 11.
620
- } else {
621
- // Len adjustment should not make length negative
622
- if (this.minLength + partialLength.len < 0) {
623
- assert(false, 0x057 /* "Negative length after length adjustment!" */);
624
- }
839
+ if (clientPartials) {
840
+ // Client partials used to track local edits so we can account for them some refSeq.
841
+ // But the information we keep track of are since minSeq, so we keep track of more history
842
+ // then needed, and some of them doesn't make sense to be used for length calculations
843
+ // e.g. if you have this sequence, where the minSeq is #5 because of other clients
844
+ // seq 10: client 1: insert seg #1
845
+ // seq 11: client 2: delete seg #2 refseq: 10
846
+ // minLength is 0, we would have keep a record of seglen: -1 for clientPartialLengths for client 2
847
+ // So if you ask for partial length for client 2 @ seq 5, we will have return -1.
848
+ // However, that combination is invalid, since we should never see any ops with refseq < 10 for
849
+ // client 2 after seq 11.
850
+ } else {
851
+ // Len adjustment should not make length negative
852
+ if (partialSeqLengths["minLength"] + partialLength.len < 0) {
853
+ assert(false, 0x057 /* "Negative length after length adjustment!" */);
625
854
  }
855
+ }
626
856
 
627
- if (partialLength.overlapRemoveClients) {
628
- // Only the flat partialLengths can have overlapRemoveClients, the per client view shouldn't
629
- assert(!clientPartials, 0x058 /* "Both overlapRemoveClients and clientPartials are set!" */);
857
+ if (partialLength.overlapRemoveClients) {
858
+ // Only the flat partialLengths can have overlapRemoveClients, the per client view shouldn't
859
+ assert(!clientPartials, 0x058 /* "Both overlapRemoveClients and clientPartials are set!" */);
860
+
861
+ // Each overlap client count as one, but the first remove to sequence was already counted.
862
+ // (this aligns with the logic to omit the removing client in `addClientSeqNumberFromPartial`)
863
+ count += partialLength.overlapRemoveClients.size() - 1;
864
+ }
865
+ }
866
+ return count;
867
+ }
630
868
 
631
- // Each overlap client count as one
632
- count += partialLength.overlapRemoveClients.size();
869
+ function verify(partialSeqLengths: PartialSequenceLengths) {
870
+ if (partialSeqLengths["clientSeqNumbers"]) {
871
+ let cliCount = 0;
872
+ for (const cliSeq of partialSeqLengths["clientSeqNumbers"]) {
873
+ if (cliSeq) {
874
+ cliCount += verifyPartialLengths(partialSeqLengths, cliSeq, true);
633
875
  }
634
876
  }
635
- return count;
877
+
878
+ // If we have client view, we should have the flat view
879
+ assert(!!partialSeqLengths["partialLengths"], 0x059 /* "Client view exists but flat view does not!" */);
880
+ const flatCount = verifyPartialLengths(partialSeqLengths, partialSeqLengths["partialLengths"], false);
881
+
882
+ // The number of partial lengths on the client view and flat view should be the same
883
+ assert(flatCount === cliCount,
884
+ 0x05a /* "Mismatch between number of partial lengths on client and flat views!" */);
885
+ } else {
886
+ // If we don't have a client view, we shouldn't have the flat view either
887
+ assert(!partialSeqLengths["partialLengths"], 0x05b /* "Flat view exists but client view does not!" */);
888
+ }
889
+ }
890
+ /* eslint-enable @typescript-eslint/dot-notation */
891
+
892
+ /**
893
+ * Clones an `overlapRemoveClients` red-black tree.
894
+ */
895
+ function cloneOverlapRemoveClients(
896
+ oldTree: RedBlackTree<number, IOverlapClient> | undefined,
897
+ ): RedBlackTree<number, IOverlapClient> | undefined {
898
+ if (!oldTree) { return undefined; }
899
+ const newTree = new RedBlackTree<number, IOverlapClient>(compareNumbers);
900
+ oldTree.map((bProp: Property<number, IOverlapClient>) => {
901
+ newTree.put(bProp.data.clientId, { ...bProp.data });
902
+ return true;
903
+ });
904
+ return newTree;
905
+ }
906
+
907
+ /**
908
+ * Combines the `overlapRemoveClients` field of two `PartialSequenceLength` objects,
909
+ * modifying the first PartialSequenceLength's bookkeeping in-place.
910
+ *
911
+ * Combination is performed additively on `seglen` on a per-client basis.
912
+ */
913
+ function combineOverlapClients(a: PartialSequenceLength, b: PartialSequenceLength) {
914
+ const overlapRemoveClientsA = a.overlapRemoveClients;
915
+ if (overlapRemoveClientsA) {
916
+ if (b.overlapRemoveClients) {
917
+ b.overlapRemoveClients.map((bProp: Property<number, IOverlapClient>) => {
918
+ const aProp = overlapRemoveClientsA.get(bProp.key);
919
+ if (aProp) {
920
+ aProp.data.seglen += bProp.data.seglen;
921
+ } else {
922
+ overlapRemoveClientsA.put(bProp.data.clientId, { ...bProp.data });
923
+ }
924
+ return true;
925
+ });
926
+ }
927
+ } else {
928
+ a.overlapRemoveClients = cloneOverlapRemoveClients(b.overlapRemoveClients);
929
+ }
930
+ }
931
+
932
+ /**
933
+ * Given a number of seq-sorted `partialLength` lists, merges them into a combined seq-sorted `partialLength`
934
+ * list. This merge includes coalescing `PartialSequenceLength` entries at the same seq.
935
+ *
936
+ * Ex: merging the following two lists (some information omitted on each PartialSequenceLength):
937
+ * ```typescript
938
+ * [{ seq: 1, seglen: 5 }, { seq: 3, seglen: -1 }]
939
+ * [{ seq: 1, seglen: -3 }, { seq: 2: seglen: 4 }]
940
+ * ```
941
+ * would produce
942
+ * ```typescript
943
+ * [{ seq: 1, seglen: 2 }, { seq: 2, seglen: 4 }, { seq: 3, seglen: -1 }]
944
+ * ```
945
+ */
946
+ function mergePartialLengths<T extends PartialSequenceLength>(childPartialLengths: T[][]): T[] {
947
+ const mergedLengths: T[] = [];
948
+ // All child PartialSequenceLengths are now sorted temporally (i.e. by seq). Since
949
+ // a given MergeTree operation can affect multiple segments, there may be multiple entries
950
+ // for a given seq. We run through them in order, coalescing all length information for a given
951
+ // seq together into `combinedPartialLengths`.
952
+ let currentPartial: T | undefined;
953
+ for (const partialLength of mergeSortedListsBySeq(childPartialLengths)) {
954
+ if (!currentPartial || currentPartial.seq !== partialLength.seq) {
955
+ // Start a new seq entry.
956
+ currentPartial = {
957
+ ...partialLength,
958
+ len: (currentPartial?.len ?? 0) + partialLength.seglen,
959
+ overlapRemoveClients: cloneOverlapRemoveClients(partialLength.overlapRemoveClients),
960
+ };
961
+ mergedLengths.push(currentPartial);
962
+ } else {
963
+ // Update existing entry
964
+ currentPartial.seglen += partialLength.seglen;
965
+ currentPartial.len += partialLength.seglen;
966
+ combineOverlapClients(currentPartial, partialLength);
967
+ }
636
968
  }
969
+ return mergedLengths;
970
+ }
971
+
972
+ /**
973
+ * Given a collection of PartialSequenceLength lists--each sorted by sequence number--returns an iterable that yields
974
+ * each PartialSequenceLength in sequence order.
975
+ *
976
+ * This is equivalent to flattening the input list and sorting it by sequence number. If the number of lists to merge is
977
+ * a constant, however, this approach is advantageous asymptotically.
978
+ */
979
+ function mergeSortedListsBySeq<T extends PartialSequenceLength>(lists: T[][]): Iterable<T> {
980
+ class PartialSequenceLengthIterator {
981
+ /**
982
+ * nextSmallestIndex[i] is the next element of sublists[i] to check.
983
+ * In other words, the iterator has already yielded elements of sublists[i] *up through*
984
+ * sublists[i][nextSmallestIndex[i] - 1].
985
+ */
986
+ private readonly nextSmallestIndex: number[];
637
987
 
638
- private verify() {
639
- if (this.clientSeqNumbers) {
640
- let cliCount = 0;
641
- for (const cliSeq of this.clientSeqNumbers) {
642
- if (cliSeq) {
643
- cliCount += this.verifyPartialLengths(cliSeq, true);
988
+ constructor(private readonly sublists: T[][]) {
989
+ this.nextSmallestIndex = new Array(sublists.length);
990
+ for (let i = 0; i < sublists.length; i++) {
991
+ this.nextSmallestIndex[i] = 0;
992
+ }
993
+ }
994
+
995
+ public next(): { value: T; done: false; } | { value: undefined; done: true; } {
996
+ const len = this.sublists.length;
997
+ let currentMin: T | undefined;
998
+ let currentMinIndex: number | undefined;
999
+ for (let i = 0; i < len; i++) {
1000
+ const candidateIndex = this.nextSmallestIndex[i];
1001
+ if (candidateIndex < this.sublists[i].length) {
1002
+ const candidate = this.sublists[i][candidateIndex];
1003
+ if (!currentMin || candidate.seq < currentMin.seq) {
1004
+ currentMin = candidate;
1005
+ currentMinIndex = i;
1006
+ }
644
1007
  }
645
1008
  }
646
1009
 
647
- // If we have client view, we should have the flat view
648
- assert(!!this.partialLengths, 0x059 /* "Client view exists but flat view does not!" */);
649
- const flatCount = this.verifyPartialLengths(this.partialLengths, false);
1010
+ if (currentMin) {
1011
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1012
+ this.nextSmallestIndex[currentMinIndex!]++;
1013
+ return { value: currentMin, done: false };
1014
+ } else {
1015
+ return { value: undefined, done: true };
1016
+ }
1017
+ }
1018
+ }
650
1019
 
651
- // The number of partial lengths on the client view and flat view should be the same
652
- assert(flatCount === cliCount,
653
- 0x05a /* "Mismatch between number of partial lengths on client and flat views!" */);
654
- } else {
655
- // If we don't have a client view, we shouldn't have the flat view either
656
- assert(!this.partialLengths, 0x05b /* "Flat view exists but client view does not!" */);
1020
+ return { [Symbol.iterator]: () => new PartialSequenceLengthIterator(lists) };
1021
+ }
1022
+
1023
+ function insertIntoList<T>(list: T[], index: number, elem: T): void {
1024
+ if (index < list.length) {
1025
+ for (let k = list.length; k > index; k--) {
1026
+ list[k] = list[k - 1];
657
1027
  }
1028
+ list[index] = elem;
1029
+ } else {
1030
+ list.push(elem);
658
1031
  }
659
1032
  }