@fluidframework/merge-tree 2.30.0 → 2.31.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 (308) hide show
  1. package/CHANGELOG.md +403 -399
  2. package/api-report/merge-tree.legacy.alpha.api.md +1 -0
  3. package/dist/MergeTreeTextHelper.d.ts +9 -3
  4. package/dist/MergeTreeTextHelper.d.ts.map +1 -1
  5. package/dist/MergeTreeTextHelper.js +5 -5
  6. package/dist/MergeTreeTextHelper.js.map +1 -1
  7. package/dist/client.d.ts +7 -13
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +136 -110
  10. package/dist/client.js.map +1 -1
  11. package/dist/endOfTreeSegment.d.ts +12 -8
  12. package/dist/endOfTreeSegment.d.ts.map +1 -1
  13. package/dist/endOfTreeSegment.js +2 -4
  14. package/dist/endOfTreeSegment.js.map +1 -1
  15. package/dist/index.d.ts +6 -3
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/mergeTree.d.ts +37 -23
  20. package/dist/mergeTree.d.ts.map +1 -1
  21. package/dist/mergeTree.js +400 -483
  22. package/dist/mergeTree.js.map +1 -1
  23. package/dist/mergeTreeDeltaCallback.d.ts +4 -8
  24. package/dist/mergeTreeDeltaCallback.d.ts.map +1 -1
  25. package/dist/mergeTreeDeltaCallback.js.map +1 -1
  26. package/dist/mergeTreeNodes.d.ts +32 -10
  27. package/dist/mergeTreeNodes.d.ts.map +1 -1
  28. package/dist/mergeTreeNodes.js +43 -28
  29. package/dist/mergeTreeNodes.js.map +1 -1
  30. package/dist/partialLengths.d.ts +2 -2
  31. package/dist/partialLengths.d.ts.map +1 -1
  32. package/dist/partialLengths.js +181 -109
  33. package/dist/partialLengths.js.map +1 -1
  34. package/dist/perspective.d.ts +8 -27
  35. package/dist/perspective.d.ts.map +1 -1
  36. package/dist/perspective.js +7 -67
  37. package/dist/perspective.js.map +1 -1
  38. package/dist/revertibles.d.ts.map +1 -1
  39. package/dist/revertibles.js +2 -2
  40. package/dist/revertibles.js.map +1 -1
  41. package/dist/segmentInfos.d.ts +20 -106
  42. package/dist/segmentInfos.d.ts.map +1 -1
  43. package/dist/segmentInfos.js +28 -42
  44. package/dist/segmentInfos.js.map +1 -1
  45. package/dist/segmentPropertiesManager.d.ts +1 -14
  46. package/dist/segmentPropertiesManager.d.ts.map +1 -1
  47. package/dist/segmentPropertiesManager.js +3 -17
  48. package/dist/segmentPropertiesManager.js.map +1 -1
  49. package/dist/snapshotLoader.d.ts.map +1 -1
  50. package/dist/snapshotLoader.js +62 -19
  51. package/dist/snapshotLoader.js.map +1 -1
  52. package/dist/snapshotV1.d.ts.map +1 -1
  53. package/dist/snapshotV1.js +55 -24
  54. package/dist/snapshotV1.js.map +1 -1
  55. package/dist/snapshotlegacy.d.ts.map +1 -1
  56. package/dist/snapshotlegacy.js +6 -9
  57. package/dist/snapshotlegacy.js.map +1 -1
  58. package/dist/stamps.d.ts +1 -1
  59. package/dist/stamps.js +1 -1
  60. package/dist/stamps.js.map +1 -1
  61. package/dist/test/Insertion.perf.spec.js +6 -51
  62. package/dist/test/Insertion.perf.spec.js.map +1 -1
  63. package/dist/test/PartialLengths.perf.spec.js +18 -25
  64. package/dist/test/PartialLengths.perf.spec.js.map +1 -1
  65. package/dist/test/Removal.perf.spec.js +13 -41
  66. package/dist/test/Removal.perf.spec.js.map +1 -1
  67. package/dist/test/beastTest.spec.d.ts.map +1 -1
  68. package/dist/test/beastTest.spec.js +41 -66
  69. package/dist/test/beastTest.spec.js.map +1 -1
  70. package/dist/test/client.annotateMarker.spec.js +1 -11
  71. package/dist/test/client.annotateMarker.spec.js.map +1 -1
  72. package/dist/test/client.applyMsg.spec.js +14 -14
  73. package/dist/test/client.applyMsg.spec.js.map +1 -1
  74. package/dist/test/client.getPosition.spec.js +1 -1
  75. package/dist/test/client.getPosition.spec.js.map +1 -1
  76. package/dist/test/client.localReference.spec.js +1 -1
  77. package/dist/test/client.localReference.spec.js.map +1 -1
  78. package/dist/test/client.rollback.spec.js +49 -58
  79. package/dist/test/client.rollback.spec.js.map +1 -1
  80. package/dist/test/client.rollbackFarm.spec.js +1 -1
  81. package/dist/test/client.rollbackFarm.spec.js.map +1 -1
  82. package/dist/test/client.searchForMarker.spec.js +4 -21
  83. package/dist/test/client.searchForMarker.spec.js.map +1 -1
  84. package/dist/test/index.d.ts +2 -2
  85. package/dist/test/index.d.ts.map +1 -1
  86. package/dist/test/index.js +2 -6
  87. package/dist/test/index.js.map +1 -1
  88. package/dist/test/mergeTree.annotate.deltaCallback.spec.js +14 -59
  89. package/dist/test/mergeTree.annotate.deltaCallback.spec.js.map +1 -1
  90. package/dist/test/mergeTree.annotate.spec.js +47 -63
  91. package/dist/test/mergeTree.annotate.spec.js.map +1 -1
  92. package/dist/test/mergeTree.insert.deltaCallback.spec.js +9 -62
  93. package/dist/test/mergeTree.insert.deltaCallback.spec.js.map +1 -1
  94. package/dist/test/mergeTree.insertingWalk.spec.js +59 -125
  95. package/dist/test/mergeTree.insertingWalk.spec.js.map +1 -1
  96. package/dist/test/mergeTree.markRangeRemoved.deltaCallback.spec.js +12 -93
  97. package/dist/test/mergeTree.markRangeRemoved.deltaCallback.spec.js.map +1 -1
  98. package/dist/test/mergeTree.markRangeRemoved.spec.js +10 -7
  99. package/dist/test/mergeTree.markRangeRemoved.spec.js.map +1 -1
  100. package/dist/test/mergeTree.walk.spec.js +2 -14
  101. package/dist/test/mergeTree.walk.spec.js.map +1 -1
  102. package/dist/test/mergeTreeOperationRunner.js +2 -2
  103. package/dist/test/mergeTreeOperationRunner.js.map +1 -1
  104. package/dist/test/obliterate.concurrent.spec.js +18 -23
  105. package/dist/test/obliterate.concurrent.spec.js.map +1 -1
  106. package/dist/test/obliterate.partialLength.spec.js +166 -136
  107. package/dist/test/obliterate.partialLength.spec.js.map +1 -1
  108. package/dist/test/obliterate.spec.js +16 -126
  109. package/dist/test/obliterate.spec.js.map +1 -1
  110. package/dist/test/partialLength.spec.js +28 -196
  111. package/dist/test/partialLength.spec.js.map +1 -1
  112. package/dist/test/perspective.spec.js +34 -0
  113. package/dist/test/perspective.spec.js.map +1 -1
  114. package/dist/test/propertyManager.spec.js +1 -1
  115. package/dist/test/propertyManager.spec.js.map +1 -1
  116. package/dist/test/resetPendingSegmentsToOp.spec.js +0 -2
  117. package/dist/test/resetPendingSegmentsToOp.spec.js.map +1 -1
  118. package/dist/test/segmentGroupCollection.spec.js +10 -4
  119. package/dist/test/segmentGroupCollection.spec.js.map +1 -1
  120. package/dist/test/testClient.d.ts +1 -0
  121. package/dist/test/testClient.d.ts.map +1 -1
  122. package/dist/test/testClient.js +16 -26
  123. package/dist/test/testClient.js.map +1 -1
  124. package/dist/test/testClientLogger.d.ts.map +1 -1
  125. package/dist/test/testClientLogger.js +3 -10
  126. package/dist/test/testClientLogger.js.map +1 -1
  127. package/dist/test/testServer.d.ts +2 -1
  128. package/dist/test/testServer.d.ts.map +1 -1
  129. package/dist/test/testServer.js +7 -5
  130. package/dist/test/testServer.js.map +1 -1
  131. package/dist/test/testUtils.d.ts +36 -56
  132. package/dist/test/testUtils.d.ts.map +1 -1
  133. package/dist/test/testUtils.js +68 -77
  134. package/dist/test/testUtils.js.map +1 -1
  135. package/dist/test/text.d.ts +2 -2
  136. package/dist/test/text.d.ts.map +1 -1
  137. package/dist/test/text.js +5 -2
  138. package/dist/test/text.js.map +1 -1
  139. package/dist/textSegment.d.ts +0 -6
  140. package/dist/textSegment.d.ts.map +1 -1
  141. package/dist/textSegment.js.map +1 -1
  142. package/dist/zamboni.d.ts.map +1 -1
  143. package/dist/zamboni.js +53 -26
  144. package/dist/zamboni.js.map +1 -1
  145. package/lib/MergeTreeTextHelper.d.ts +9 -3
  146. package/lib/MergeTreeTextHelper.d.ts.map +1 -1
  147. package/lib/MergeTreeTextHelper.js +5 -5
  148. package/lib/MergeTreeTextHelper.js.map +1 -1
  149. package/lib/client.d.ts +7 -13
  150. package/lib/client.d.ts.map +1 -1
  151. package/lib/client.js +117 -116
  152. package/lib/client.js.map +1 -1
  153. package/lib/endOfTreeSegment.d.ts +12 -8
  154. package/lib/endOfTreeSegment.d.ts.map +1 -1
  155. package/lib/endOfTreeSegment.js +2 -4
  156. package/lib/endOfTreeSegment.js.map +1 -1
  157. package/lib/index.d.ts +6 -3
  158. package/lib/index.d.ts.map +1 -1
  159. package/lib/index.js +1 -1
  160. package/lib/index.js.map +1 -1
  161. package/lib/mergeTree.d.ts +37 -23
  162. package/lib/mergeTree.d.ts.map +1 -1
  163. package/lib/mergeTree.js +381 -488
  164. package/lib/mergeTree.js.map +1 -1
  165. package/lib/mergeTreeDeltaCallback.d.ts +4 -8
  166. package/lib/mergeTreeDeltaCallback.d.ts.map +1 -1
  167. package/lib/mergeTreeDeltaCallback.js.map +1 -1
  168. package/lib/mergeTreeNodes.d.ts +32 -10
  169. package/lib/mergeTreeNodes.d.ts.map +1 -1
  170. package/lib/mergeTreeNodes.js +42 -29
  171. package/lib/mergeTreeNodes.js.map +1 -1
  172. package/lib/partialLengths.d.ts +2 -2
  173. package/lib/partialLengths.d.ts.map +1 -1
  174. package/lib/partialLengths.js +160 -111
  175. package/lib/partialLengths.js.map +1 -1
  176. package/lib/perspective.d.ts +8 -27
  177. package/lib/perspective.d.ts.map +1 -1
  178. package/lib/perspective.js +8 -68
  179. package/lib/perspective.js.map +1 -1
  180. package/lib/revertibles.d.ts.map +1 -1
  181. package/lib/revertibles.js +2 -2
  182. package/lib/revertibles.js.map +1 -1
  183. package/lib/segmentInfos.d.ts +20 -106
  184. package/lib/segmentInfos.d.ts.map +1 -1
  185. package/lib/segmentInfos.js +26 -37
  186. package/lib/segmentInfos.js.map +1 -1
  187. package/lib/segmentPropertiesManager.d.ts +1 -14
  188. package/lib/segmentPropertiesManager.d.ts.map +1 -1
  189. package/lib/segmentPropertiesManager.js +2 -16
  190. package/lib/segmentPropertiesManager.js.map +1 -1
  191. package/lib/snapshotLoader.d.ts.map +1 -1
  192. package/lib/snapshotLoader.js +39 -19
  193. package/lib/snapshotLoader.js.map +1 -1
  194. package/lib/snapshotV1.d.ts.map +1 -1
  195. package/lib/snapshotV1.js +34 -26
  196. package/lib/snapshotV1.js.map +1 -1
  197. package/lib/snapshotlegacy.d.ts.map +1 -1
  198. package/lib/snapshotlegacy.js +7 -10
  199. package/lib/snapshotlegacy.js.map +1 -1
  200. package/lib/stamps.d.ts +1 -1
  201. package/lib/stamps.js +1 -1
  202. package/lib/stamps.js.map +1 -1
  203. package/lib/test/Insertion.perf.spec.js +6 -51
  204. package/lib/test/Insertion.perf.spec.js.map +1 -1
  205. package/lib/test/PartialLengths.perf.spec.js +18 -25
  206. package/lib/test/PartialLengths.perf.spec.js.map +1 -1
  207. package/lib/test/Removal.perf.spec.js +13 -41
  208. package/lib/test/Removal.perf.spec.js.map +1 -1
  209. package/lib/test/beastTest.spec.d.ts.map +1 -1
  210. package/lib/test/beastTest.spec.js +42 -67
  211. package/lib/test/beastTest.spec.js.map +1 -1
  212. package/lib/test/client.annotateMarker.spec.js +1 -11
  213. package/lib/test/client.annotateMarker.spec.js.map +1 -1
  214. package/lib/test/client.applyMsg.spec.js +14 -14
  215. package/lib/test/client.applyMsg.spec.js.map +1 -1
  216. package/lib/test/client.getPosition.spec.js +1 -1
  217. package/lib/test/client.getPosition.spec.js.map +1 -1
  218. package/lib/test/client.localReference.spec.js +1 -1
  219. package/lib/test/client.localReference.spec.js.map +1 -1
  220. package/lib/test/client.rollback.spec.js +50 -59
  221. package/lib/test/client.rollback.spec.js.map +1 -1
  222. package/lib/test/client.rollbackFarm.spec.js +1 -1
  223. package/lib/test/client.rollbackFarm.spec.js.map +1 -1
  224. package/lib/test/client.searchForMarker.spec.js +4 -21
  225. package/lib/test/client.searchForMarker.spec.js.map +1 -1
  226. package/lib/test/index.d.ts +2 -2
  227. package/lib/test/index.d.ts.map +1 -1
  228. package/lib/test/index.js +1 -1
  229. package/lib/test/index.js.map +1 -1
  230. package/lib/test/mergeTree.annotate.deltaCallback.spec.js +15 -60
  231. package/lib/test/mergeTree.annotate.deltaCallback.spec.js.map +1 -1
  232. package/lib/test/mergeTree.annotate.spec.js +48 -64
  233. package/lib/test/mergeTree.annotate.spec.js.map +1 -1
  234. package/lib/test/mergeTree.insert.deltaCallback.spec.js +10 -63
  235. package/lib/test/mergeTree.insert.deltaCallback.spec.js.map +1 -1
  236. package/lib/test/mergeTree.insertingWalk.spec.js +61 -127
  237. package/lib/test/mergeTree.insertingWalk.spec.js.map +1 -1
  238. package/lib/test/mergeTree.markRangeRemoved.deltaCallback.spec.js +13 -94
  239. package/lib/test/mergeTree.markRangeRemoved.deltaCallback.spec.js.map +1 -1
  240. package/lib/test/mergeTree.markRangeRemoved.spec.js +10 -7
  241. package/lib/test/mergeTree.markRangeRemoved.spec.js.map +1 -1
  242. package/lib/test/mergeTree.walk.spec.js +2 -14
  243. package/lib/test/mergeTree.walk.spec.js.map +1 -1
  244. package/lib/test/mergeTreeOperationRunner.js +3 -3
  245. package/lib/test/mergeTreeOperationRunner.js.map +1 -1
  246. package/lib/test/obliterate.concurrent.spec.js +18 -23
  247. package/lib/test/obliterate.concurrent.spec.js.map +1 -1
  248. package/lib/test/obliterate.partialLength.spec.js +167 -137
  249. package/lib/test/obliterate.partialLength.spec.js.map +1 -1
  250. package/lib/test/obliterate.spec.js +17 -127
  251. package/lib/test/obliterate.spec.js.map +1 -1
  252. package/lib/test/partialLength.spec.js +29 -197
  253. package/lib/test/partialLength.spec.js.map +1 -1
  254. package/lib/test/perspective.spec.js +34 -0
  255. package/lib/test/perspective.spec.js.map +1 -1
  256. package/lib/test/propertyManager.spec.js +2 -2
  257. package/lib/test/propertyManager.spec.js.map +1 -1
  258. package/lib/test/resetPendingSegmentsToOp.spec.js +0 -2
  259. package/lib/test/resetPendingSegmentsToOp.spec.js.map +1 -1
  260. package/lib/test/segmentGroupCollection.spec.js +10 -4
  261. package/lib/test/segmentGroupCollection.spec.js.map +1 -1
  262. package/lib/test/testClient.d.ts +1 -0
  263. package/lib/test/testClient.d.ts.map +1 -1
  264. package/lib/test/testClient.js +18 -28
  265. package/lib/test/testClient.js.map +1 -1
  266. package/lib/test/testClientLogger.d.ts.map +1 -1
  267. package/lib/test/testClientLogger.js +3 -10
  268. package/lib/test/testClientLogger.js.map +1 -1
  269. package/lib/test/testServer.d.ts +2 -1
  270. package/lib/test/testServer.d.ts.map +1 -1
  271. package/lib/test/testServer.js +7 -5
  272. package/lib/test/testServer.js.map +1 -1
  273. package/lib/test/testUtils.d.ts +36 -56
  274. package/lib/test/testUtils.d.ts.map +1 -1
  275. package/lib/test/testUtils.js +66 -48
  276. package/lib/test/testUtils.js.map +1 -1
  277. package/lib/test/text.d.ts +2 -2
  278. package/lib/test/text.d.ts.map +1 -1
  279. package/lib/test/text.js +6 -3
  280. package/lib/test/text.js.map +1 -1
  281. package/lib/textSegment.d.ts +0 -6
  282. package/lib/textSegment.d.ts.map +1 -1
  283. package/lib/textSegment.js.map +1 -1
  284. package/lib/tsdoc-metadata.json +1 -1
  285. package/lib/zamboni.d.ts.map +1 -1
  286. package/lib/zamboni.js +32 -28
  287. package/lib/zamboni.js.map +1 -1
  288. package/package.json +17 -20
  289. package/src/MergeTreeTextHelper.ts +17 -12
  290. package/src/client.ts +141 -197
  291. package/src/endOfTreeSegment.ts +11 -8
  292. package/src/index.ts +4 -3
  293. package/src/mergeTree.ts +482 -633
  294. package/src/mergeTreeDeltaCallback.ts +4 -8
  295. package/src/mergeTreeNodes.ts +66 -45
  296. package/src/partialLengths.ts +181 -137
  297. package/src/perspective.ts +17 -95
  298. package/src/revertibles.ts +2 -7
  299. package/src/segmentInfos.ts +48 -141
  300. package/src/segmentPropertiesManager.ts +2 -16
  301. package/src/snapshotLoader.ts +62 -30
  302. package/src/snapshotV1.ts +36 -28
  303. package/src/snapshotlegacy.ts +7 -16
  304. package/src/stamps.ts +1 -1
  305. package/src/textSegment.ts +0 -13
  306. package/src/zamboni.ts +38 -32
  307. package/tsconfig.json +1 -0
  308. package/prettier.config.cjs +0 -8
package/src/mergeTree.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  MergeTreeMaintenanceType,
36
36
  } from "./mergeTreeDeltaCallback.js";
37
37
  import {
38
+ LeafAction,
38
39
  NodeAction,
39
40
  backwardExcursion,
40
41
  depthFirstNodeWalk,
@@ -53,9 +54,10 @@ import {
53
54
  SegmentGroup,
54
55
  assertSegmentLeaf,
55
56
  assignChild,
57
+ getMinSeqPerspective,
58
+ getMinSeqStamp,
56
59
  isSegmentLeaf,
57
60
  reservedMarkerIdKey,
58
- seqLTE,
59
61
  type IMergeNodeBuilder,
60
62
  type ISegmentInternal,
61
63
  type ISegmentLeaf,
@@ -76,10 +78,11 @@ import {
76
78
  } from "./ops.js";
77
79
  import { PartialSequenceLengths } from "./partialLengths.js";
78
80
  import {
79
- LocalDefaultPerspective,
80
- LocalReconnectingPerspective,
81
81
  PriorPerspective,
82
+ LocalReconnectingPerspective,
82
83
  type Perspective,
84
+ LocalDefaultPerspective,
85
+ RemoteObliteratePerspective,
83
86
  } from "./perspective.js";
84
87
  import { PropertySet, createMap, extend, extendIfUndefined } from "./properties.js";
85
88
  import {
@@ -91,54 +94,45 @@ import {
91
94
  } from "./referencePositions.js";
92
95
  import { SegmentGroupCollection } from "./segmentGroupCollection.js";
93
96
  import {
94
- assertMoved,
95
97
  assertRemoved,
96
98
  isMergeNodeInfo,
97
- isMoved,
98
99
  isRemoved,
99
100
  overwriteInfo,
100
101
  removeRemovalInfo,
101
- toMoveInfo,
102
102
  toRemovalInfo,
103
- wasMovedOnInsert,
104
- type IInsertionInfo,
105
- type IMoveInfo,
106
- type IRemovalInfo,
103
+ type IHasInsertionInfo,
104
+ type IHasRemovalInfo,
107
105
  type SegmentWithInfo,
108
106
  } from "./segmentInfos.js";
109
107
  import {
110
108
  copyPropertiesAndManager,
111
109
  PropertiesManager,
112
- PropertiesRollback,
113
110
  type PropsOrAdjust,
114
111
  } from "./segmentPropertiesManager.js";
115
112
  import { Side, type InteriorSequencePlace } from "./sequencePlace.js";
116
113
  import { SortedSegmentSet } from "./sortedSegmentSet.js";
114
+ import type {
115
+ OperationStamp,
116
+ InsertOperationStamp,
117
+ RemoveOperationStamp,
118
+ SetRemoveOperationStamp,
119
+ SliceRemoveOperationStamp,
120
+ } from "./stamps.js";
121
+ import * as opstampUtils from "./stamps.js";
117
122
  import { zamboniSegments } from "./zamboni.js";
118
123
 
119
- function isRemovedAndAcked(segment: ISegmentPrivate): segment is ISegmentLeaf & IRemovalInfo {
124
+ export function isRemovedAndAcked(
125
+ segment: ISegmentPrivate,
126
+ ): segment is ISegmentLeaf & IHasRemovalInfo {
120
127
  const removalInfo = toRemovalInfo(segment);
121
- return removalInfo !== undefined && removalInfo.removedSeq !== UnassignedSequenceNumber;
122
- }
123
-
124
- function isMovedAndAcked(segment: ISegmentPrivate): segment is ISegmentLeaf & IMoveInfo {
125
- const moveInfo = toMoveInfo(segment);
126
- return moveInfo !== undefined && moveInfo.movedSeq !== UnassignedSequenceNumber;
127
- }
128
-
129
- function isRemovedAndAckedOrMovedAndAcked(segment: ISegmentPrivate): boolean {
130
- return isRemovedAndAcked(segment) || isMovedAndAcked(segment);
131
- }
132
-
133
- function isRemovedOrMoved(segment: ISegmentLeaf): boolean {
134
- return isRemoved(segment) || isMoved(segment);
128
+ return removalInfo !== undefined && opstampUtils.isAcked(removalInfo.removes[0]);
135
129
  }
136
130
 
137
131
  function nodeTotalLength(mergeTree: MergeTree, node: IMergeNode): number | undefined {
138
132
  if (!node.isLeaf()) {
139
133
  return node.cachedLength;
140
134
  }
141
- return mergeTree.localNetLength(node);
135
+ return mergeTree.leafLength(node);
142
136
  }
143
137
 
144
138
  const LRUSegmentComparer: IComparer<LRUSegment> = {
@@ -150,6 +144,7 @@ function ackSegment(
150
144
  segment: ISegmentLeaf,
151
145
  segmentGroup: SegmentGroup,
152
146
  opArgs: IMergeTreeDeltaOpArgs,
147
+ stamp: OperationStamp,
153
148
  ): boolean {
154
149
  const currentSegmentGroup = segment.segmentGroups?.dequeue();
155
150
  assert(currentSegmentGroup === segmentGroup, 0x043 /* "On ack, unexpected segmentGroup!" */);
@@ -158,6 +153,7 @@ function ackSegment(
158
153
  op,
159
154
  sequencedMessage: { sequenceNumber, minimumSequenceNumber },
160
155
  } = opArgs;
156
+ let allowIncrementalPartialLengthsUpdate = true;
161
157
  switch (op.type) {
162
158
  case MergeTreeDeltaType.ANNOTATE: {
163
159
  assert(
@@ -165,51 +161,59 @@ function ackSegment(
165
161
  0x044 /* "On annotate ack, missing segment property manager!" */,
166
162
  );
167
163
  segment.propertyManager.ack(sequenceNumber, minimumSequenceNumber, op);
168
- return true;
164
+ break;
169
165
  }
170
166
 
171
167
  case MergeTreeDeltaType.INSERT: {
172
168
  assert(
173
- segment.seq === UnassignedSequenceNumber,
169
+ opstampUtils.isLocal(segment.insert),
174
170
  0x045 /* "On insert, seq number already assigned!" */,
175
171
  );
176
- segment.seq = sequenceNumber;
177
- segment.localSeq = undefined;
178
- return true;
179
- }
180
172
 
181
- case MergeTreeDeltaType.REMOVE: {
182
- assertRemoved(segment);
183
- segment.localRemovedSeq = undefined;
184
- if (segment.removedSeq === UnassignedSequenceNumber) {
185
- segment.removedSeq = sequenceNumber;
186
- return true;
187
- }
188
- return false;
173
+ segment.insert = {
174
+ ...stamp,
175
+ type: "insert",
176
+ };
177
+ break;
189
178
  }
190
-
179
+ case MergeTreeDeltaType.REMOVE:
191
180
  case MergeTreeDeltaType.OBLITERATE:
192
181
  case MergeTreeDeltaType.OBLITERATE_SIDED: {
193
- assertMoved(segment);
194
- const obliterateInfo = segmentGroup.obliterateInfo;
195
- assert(obliterateInfo !== undefined, 0xa40 /* must have obliterate info */);
196
- segment.localMovedSeq = obliterateInfo.localSeq = undefined;
197
- const seqIdx = segment.movedSeqs.indexOf(UnassignedSequenceNumber);
198
- assert(seqIdx !== -1, 0x86f /* expected movedSeqs to contain unacked seq */);
199
- segment.movedSeqs[seqIdx] = sequenceNumber;
200
-
201
- if (segment.movedSeq === UnassignedSequenceNumber) {
202
- segment.movedSeq = sequenceNumber;
203
- return true;
204
- }
182
+ assertRemoved(segment);
183
+ const latestRemove = segment.removes[segment.removes.length - 1];
184
+ assert(
185
+ opstampUtils.isLocal(latestRemove),
186
+ 0xb5d /* Expected last remove to be unacked */,
187
+ );
188
+ assert(
189
+ segment.removes.length === 1 ||
190
+ opstampUtils.isAcked(segment.removes[segment.removes.length - 2]),
191
+ 0xb5e /* Expected prior remove to be acked */,
192
+ );
205
193
 
206
- return false;
194
+ allowIncrementalPartialLengthsUpdate = segment.removes.length === 1;
195
+ const removeStamp: RemoveOperationStamp = {
196
+ ...stamp,
197
+ type: op.type === MergeTreeDeltaType.REMOVE ? "setRemove" : "sliceRemove",
198
+ };
199
+ segment.removes[segment.removes.length - 1] = removeStamp;
200
+
201
+ const { obliterateInfo } = segmentGroup;
202
+ const hasObliterateInfo = obliterateInfo !== undefined;
203
+ const isObliterate = op.type !== MergeTreeDeltaType.REMOVE;
204
+ assert(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
205
+ if (hasObliterateInfo) {
206
+ obliterateInfo.stamp = removeStamp as SliceRemoveOperationStamp;
207
+ }
208
+ break;
207
209
  }
208
210
 
209
211
  default: {
210
212
  throw new Error(`${op.type} is in unrecognized operation type`);
211
213
  }
212
214
  }
215
+
216
+ return allowIncrementalPartialLengthsUpdate;
213
217
  }
214
218
 
215
219
  /**
@@ -399,11 +403,7 @@ function getSlideToSegment(
399
403
  cache?: Map<ISegmentLeaf, { seg?: ISegmentLeaf }>,
400
404
  useNewSlidingBehavior: boolean = false,
401
405
  ): [ISegmentLeaf | undefined, "start" | "end" | undefined] {
402
- if (
403
- !segment ||
404
- !isRemovedAndAckedOrMovedAndAcked(segment) ||
405
- segment.endpointType !== undefined
406
- ) {
406
+ if (!segment || !isRemovedAndAcked(segment) || segment.endpointType !== undefined) {
407
407
  return [segment, undefined];
408
408
  }
409
409
 
@@ -414,14 +414,13 @@ function getSlideToSegment(
414
414
  const result: { seg?: ISegmentLeaf } = {};
415
415
  cache?.set(segment, result);
416
416
  const goFurtherToFindSlideToSegment = (seg: ISegmentLeaf): boolean => {
417
- if (seg.seq !== UnassignedSequenceNumber && !isRemovedAndAckedOrMovedAndAcked(seg)) {
417
+ if (opstampUtils.isAcked(seg.insert) && !isRemovedAndAcked(seg)) {
418
418
  result.seg = seg;
419
419
  return false;
420
420
  }
421
421
  if (
422
422
  cache !== undefined &&
423
- (toRemovalInfo(seg)?.removedSeq === toRemovalInfo(segment)?.removedSeq ||
424
- toMoveInfo(seg)?.movedSeq === toMoveInfo(segment)?.movedSeq)
423
+ toRemovalInfo(seg)?.removes[0].seq === toRemovalInfo(segment)?.removes[0].seq
425
424
  ) {
426
425
  cache.set(seg, result);
427
426
  }
@@ -506,10 +505,10 @@ const backwardPred = (ref: LocalReferencePosition): boolean =>
506
505
 
507
506
  class Obliterates {
508
507
  /**
509
- * Array containing the all move operations within the
508
+ * Array containing the all obliterate operations within the
510
509
  * collab window.
511
510
  *
512
- * The moves are stored in sequence order which accelerates clean up in setMinSeq
511
+ * The obliterates are stored in sequence order which accelerates clean up in setMinSeq
513
512
  *
514
513
  * See https://github.com/microsoft/FluidFramework/blob/main/packages/dds/merge-tree/docs/Obliterate.md#remote-perspective
515
514
  * for additional context
@@ -528,7 +527,7 @@ class Obliterates {
528
527
 
529
528
  public setMinSeq(minSeq: number): void {
530
529
  // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
531
- while (!this.seqOrdered.empty && this.seqOrdered.first?.data.seq! <= minSeq) {
530
+ while (!this.seqOrdered.empty && this.seqOrdered.first?.data.stamp.seq! <= minSeq) {
532
531
  const ob = this.seqOrdered.shift()!;
533
532
  this.startOrdered.remove(ob.data.start);
534
533
  this.mergeTree.removeLocalReferencePosition(ob.data.start);
@@ -537,7 +536,10 @@ class Obliterates {
537
536
  }
538
537
 
539
538
  public addOrUpdate(obliterateInfo: ObliterateInfo): void {
540
- const { seq, start } = obliterateInfo;
539
+ const {
540
+ stamp: { seq },
541
+ start,
542
+ } = obliterateInfo;
541
543
  if (seq !== UnassignedSequenceNumber) {
542
544
  this.seqOrdered.push(obliterateInfo);
543
545
  }
@@ -567,6 +569,17 @@ class Obliterates {
567
569
  }
568
570
  }
569
571
 
572
+ interface InsertResult {
573
+ /**
574
+ * If the insertion necessitated rebalancing, this field contains a `MergeBlock` that should be inserted after the block that `insertRecursive` was called on.
575
+ */
576
+ remainder: MergeBlock | undefined;
577
+ /**
578
+ * Whether the insert changed anything (including recursive changes) in the subtree of the block that `insertRecursive` was called on.
579
+ */
580
+ hadChanges: boolean;
581
+ }
582
+
570
583
  /**
571
584
  * @internal
572
585
  */
@@ -577,6 +590,11 @@ export class MergeTree {
577
590
  zamboniSegments: true,
578
591
  };
579
592
 
593
+ /**
594
+ * A sentinel value that indicates an inserting walk should continue to the next block sibling.
595
+ * This can occur for example when tie-break forces insertion of a segment past an entire block (and
596
+ * the inserting walk first recurses into the block before realizing that).
597
+ */
580
598
  private static readonly theUnfinishedNode = { childCount: -1 } as unknown as MergeBlock;
581
599
 
582
600
  public readonly collabWindow = new CollaborationWindow();
@@ -587,9 +605,9 @@ export class MergeTree {
587
605
 
588
606
  public readonly attributionPolicy: AttributionPolicy | undefined;
589
607
 
590
- public localPerspective: Perspective = new LocalDefaultPerspective(
591
- this.collabWindow.clientId,
592
- );
608
+ public get localPerspective(): Perspective {
609
+ return this.collabWindow.localPerspective;
610
+ }
593
611
 
594
612
  /**
595
613
  * Whether or not all blocks in the mergeTree currently have information about local partial lengths computed.
@@ -630,76 +648,26 @@ export class MergeTree {
630
648
  }
631
649
 
632
650
  /**
633
- * Compute the net length of this segment from a local perspective.
634
- * @param segment - Segment whose length to find
635
- * @param localSeq - localSeq at which to find the length of this segment. If not provided,
636
- * default is to consider the local client's current perspective. Only local sequence
637
- * numbers corresponding to un-acked operations give valid results.
651
+ * Compute the net length of this segment leaf from some perspective.
652
+ * @returns - Undefined if the segment has been removed and its removal is common knowledge to all collaborators (and therefore
653
+ * may not even be present on clients that have loaded from a summary beyond this point). Otherwise, the length of the segment.
638
654
  */
639
- public localNetLength(
655
+ public leafLength(
640
656
  segment: ISegmentLeaf,
641
- refSeq?: number,
642
- localSeq?: number,
657
+ perspective: Perspective = this.localPerspective,
643
658
  ): number | undefined {
644
659
  const removalInfo = toRemovalInfo(segment);
645
- const moveInfo = toMoveInfo(segment);
646
- if (localSeq === undefined) {
647
- if (removalInfo !== undefined || moveInfo !== undefined) {
648
- if (
649
- (!!removalInfo && !seqLTE(removalInfo.removedSeq, this.collabWindow.minSeq)) ||
650
- (!!moveInfo && !seqLTE(moveInfo.movedSeq, this.collabWindow.minSeq))
651
- ) {
652
- return 0;
653
- }
654
- // this segment removed and outside the collab window which means it is zamboni eligible
655
- // this also means the segment could not exist, so we should not consider it
656
- // when making decisions about conflict resolutions
657
- return undefined;
658
- } else {
659
- return segment.cachedLength;
660
- }
660
+ if (
661
+ removalInfo &&
662
+ getMinSeqPerspective(this.collabWindow).hasOccurred(removalInfo.removes[0])
663
+ ) {
664
+ // this segment's removal has already moved outside the collab window which means it is zamboni eligible
665
+ // this also means the segment could be completely absent from other client's in-memory merge trees,
666
+ // so we should not consider it when making decisions about conflict resolutions
667
+ return undefined;
661
668
  }
662
669
 
663
- assert(
664
- refSeq !== undefined,
665
- 0x398 /* localSeq provided for local length without refSeq */,
666
- );
667
- assert(segment.seq !== undefined, 0x399 /* segment with no seq in mergeTree */);
668
- const { seq } = segment;
669
- const { removedSeq, localRemovedSeq } = removalInfo ?? {};
670
- const { movedSeq, localMovedSeq } = moveInfo ?? {};
671
- if (seq === UnassignedSequenceNumber) {
672
- assert(
673
- segment.localSeq !== undefined,
674
- 0x39a /* unacked segment with undefined localSeq */,
675
- );
676
- // inserted locally, still un-acked
677
- if (
678
- segment.localSeq > localSeq ||
679
- (localRemovedSeq !== undefined && localRemovedSeq <= localSeq) ||
680
- (localMovedSeq !== undefined && localMovedSeq <= localSeq)
681
- ) {
682
- return 0;
683
- }
684
- const { cachedLength } = segment;
685
- return cachedLength;
686
- } else {
687
- // inserted remotely
688
- if (
689
- seq > refSeq ||
690
- (removedSeq !== undefined &&
691
- removedSeq !== UnassignedSequenceNumber &&
692
- removedSeq <= refSeq) ||
693
- (movedSeq !== undefined &&
694
- movedSeq !== UnassignedSequenceNumber &&
695
- movedSeq <= refSeq) ||
696
- (localRemovedSeq !== undefined && localRemovedSeq <= localSeq) ||
697
- (localMovedSeq !== undefined && localMovedSeq <= localSeq)
698
- ) {
699
- return 0;
700
- }
701
- return segment.cachedLength;
702
- }
670
+ return perspective.isSegmentPresent(segment) ? segment.cachedLength : 0;
703
671
  }
704
672
 
705
673
  public unlinkMarker(marker: Marker): void {
@@ -715,7 +683,7 @@ export class MergeTree {
715
683
  return index;
716
684
  }
717
685
 
718
- public reloadFromSegments(segments: SegmentWithInfo<IInsertionInfo>[]): void {
686
+ public reloadFromSegments(segments: SegmentWithInfo<IHasInsertionInfo>[]): void {
719
687
  // This code assumes that a later call to `startCollaboration()` will initialize partial lengths.
720
688
  assert(
721
689
  !this.collabWindow.collaborating,
@@ -772,6 +740,7 @@ export class MergeTree {
772
740
  this.collabWindow.minSeq = minSeq;
773
741
  this.collabWindow.collaborating = true;
774
742
  this.collabWindow.currentSeq = currentSeq;
743
+ this.collabWindow.localPerspective = new LocalDefaultPerspective(localClientId);
775
744
  this.nodeUpdateLengthNewStructure(this.root, true);
776
745
  }
777
746
 
@@ -787,8 +756,8 @@ export class MergeTree {
787
756
  }
788
757
  }
789
758
 
790
- public getLength(refSeq: number, clientId: number): number {
791
- return this.blockLength(this.root, refSeq, clientId);
759
+ public getLength(perspective: Perspective): number {
760
+ return this.nodeLength(this.root, perspective) ?? 0;
792
761
  }
793
762
 
794
763
  /**
@@ -798,12 +767,7 @@ export class MergeTree {
798
767
  return this.root.cachedLength;
799
768
  }
800
769
 
801
- public getPosition(
802
- node: IMergeNode,
803
- refSeq: number,
804
- clientId: number,
805
- localSeq?: number,
806
- ): number {
770
+ public getPosition(node: IMergeNode, perspective: Perspective): number {
807
771
  if (node.isLeaf() && node.endpointType === "start") {
808
772
  return 0;
809
773
  }
@@ -818,7 +782,7 @@ export class MergeTree {
818
782
  if ((!!prevParent && child === prevParent) || child === node) {
819
783
  break;
820
784
  }
821
- totalOffset += this.nodeLength(child, refSeq, clientId, localSeq) ?? 0;
785
+ totalOffset += this.nodeLength(child, perspective) ?? 0;
822
786
  }
823
787
  prevParent = parent;
824
788
  parent = parent.parent;
@@ -828,17 +792,17 @@ export class MergeTree {
828
792
 
829
793
  public getContainingSegment(
830
794
  pos: number,
831
- refSeq: number,
832
- clientId: number,
833
- localSeq?: number,
795
+ perspective: Perspective,
834
796
  ): {
835
797
  segment: ISegmentLeaf | undefined;
836
798
  offset: number | undefined;
837
799
  } {
838
800
  assert(
839
- localSeq === undefined || clientId === this.collabWindow.clientId,
801
+ perspective.localSeq === undefined ||
802
+ perspective.clientId === this.collabWindow.clientId,
840
803
  0x39b /* localSeq provided for non-local client */,
841
804
  );
805
+
842
806
  let segment: ISegmentLeaf | undefined;
843
807
  let offset: number | undefined;
844
808
 
@@ -847,7 +811,7 @@ export class MergeTree {
847
811
  offset = start;
848
812
  return false;
849
813
  };
850
- this.nodeMap(refSeq, clientId, leaf, undefined, pos, pos + 1, localSeq);
814
+ this.nodeMap(perspective, leaf, undefined, pos, pos + 1);
851
815
  return { segment, offset };
852
816
  }
853
817
 
@@ -998,7 +962,7 @@ export class MergeTree {
998
962
  const backwardSegmentCache = new Map<ISegmentLeaf, { seg?: ISegmentLeaf }>();
999
963
  for (const segment of segments) {
1000
964
  assert(
1001
- isRemovedAndAckedOrMovedAndAcked(segment),
965
+ isRemovedAndAcked(segment),
1002
966
  0x2f1 /* slideReferences from a segment which has not been removed and acked */,
1003
967
  );
1004
968
  if (segment.localRefs === undefined || segment.localRefs.empty) {
@@ -1054,12 +1018,6 @@ export class MergeTree {
1054
1018
  );
1055
1019
  }
1056
1020
 
1057
- private blockLength(node: MergeBlock, refSeq: number, clientId: number): number {
1058
- return this.collabWindow.collaborating && clientId !== this.collabWindow.clientId
1059
- ? node.partialLengths!.getPartialLength(refSeq, clientId)
1060
- : (node.cachedLength ?? 0);
1061
- }
1062
-
1063
1021
  /**
1064
1022
  * Compute local partial length information
1065
1023
  *
@@ -1084,83 +1042,35 @@ export class MergeTree {
1084
1042
  this.localPartialsComputed = true;
1085
1043
  }
1086
1044
 
1087
- private nodeLength(
1088
- node: IMergeNode,
1089
- refSeq: number,
1090
- clientId: number,
1091
- localSeq?: number,
1092
- ): number | undefined {
1093
- if (!this.collabWindow.collaborating || this.collabWindow.clientId === clientId) {
1094
- if (node.isLeaf()) {
1095
- return this.localNetLength(node, refSeq, localSeq);
1096
- } else if (
1097
- localSeq === undefined ||
1098
- // All changes are visible. Small note on why we allow refSeq >= this.collabWindow.currentSeq rather than just equality:
1099
- // merge-tree eventing occurs before the collab window is updated to account for whatever op it is processing, and we want
1100
- // to support resolving positions from within the event handler which account for that op. e.g. undo-redo relies on this
1101
- // behavior with local references.
1102
- (localSeq === this.collabWindow.localSeq && refSeq >= this.collabWindow.currentSeq)
1103
- ) {
1104
- // Local client sees all segments, even when collaborating
1105
- return node.cachedLength;
1106
- } else {
1107
- this.computeLocalPartials(refSeq);
1108
-
1109
- // Local client should see all segments except those after localSeq.
1110
- const partialLen = node.partialLengths!.getPartialLength(refSeq, clientId, localSeq);
1111
-
1112
- PartialSequenceLengths.options.verifyExpected?.(
1113
- this,
1114
- node,
1115
- refSeq,
1116
- clientId,
1117
- localSeq,
1118
- );
1119
-
1120
- return partialLen;
1121
- }
1122
- } else {
1123
- // Sequence number within window
1124
- if (node.isLeaf()) {
1125
- const segment = node;
1126
- const removalInfo = toRemovalInfo(segment);
1127
- const moveInfo = toMoveInfo(segment);
1045
+ private nodeLength(node: IMergeNode, perspective: Perspective): number | undefined {
1046
+ if (node.isLeaf()) {
1047
+ return this.leafLength(node, perspective);
1048
+ }
1128
1049
 
1129
- if (removalInfo !== undefined) {
1130
- if (seqLTE(removalInfo.removedSeq, this.collabWindow.minSeq)) {
1131
- return undefined;
1132
- }
1133
- if (
1134
- seqLTE(removalInfo.removedSeq, refSeq) ||
1135
- removalInfo.removedClientIds.includes(clientId)
1136
- ) {
1137
- return 0;
1138
- }
1139
- }
1050
+ const { refSeq, clientId, localSeq } = perspective;
1140
1051
 
1141
- if (moveInfo !== undefined) {
1142
- if (seqLTE(moveInfo.movedSeq, this.collabWindow.minSeq)) {
1143
- return undefined;
1144
- }
1145
- if (
1146
- seqLTE(moveInfo.movedSeq, refSeq) ||
1147
- moveInfo.movedClientIds.includes(clientId)
1148
- ) {
1149
- return 0;
1150
- }
1151
- }
1052
+ const isLocalPerspective =
1053
+ !this.collabWindow.collaborating || this.collabWindow.clientId === clientId;
1054
+ if (
1055
+ isLocalPerspective &&
1056
+ (localSeq === undefined ||
1057
+ (localSeq === this.collabWindow.localSeq && refSeq >= this.collabWindow.currentSeq))
1058
+ ) {
1059
+ // All changes are visible. Small note on why we allow refSeq >= this.collabWindow.currentSeq rather than just equality:
1060
+ // merge-tree eventing occurs before the collab window is updated to account for whatever op it is processing, and we want
1061
+ // to support resolving positions from within the event handler which account for that op. e.g. undo-redo relies on this
1062
+ // behavior with local references.
1063
+ return node.cachedLength;
1064
+ }
1152
1065
 
1153
- return seqLTE(node.seq ?? 0, refSeq) || segment.clientId === clientId
1154
- ? segment.cachedLength
1155
- : 0;
1156
- } else {
1157
- const partialLen = node.partialLengths!.getPartialLength(refSeq, clientId);
1066
+ if (localSeq !== undefined) {
1067
+ this.computeLocalPartials(refSeq);
1068
+ }
1158
1069
 
1159
- PartialSequenceLengths.options.verifyExpected?.(this, node, refSeq, clientId);
1070
+ const length = node.partialLengths!.getPartialLength(refSeq, clientId, localSeq);
1160
1071
 
1161
- return partialLen;
1162
- }
1163
- }
1072
+ PartialSequenceLengths.options.verifyExpected?.(this, node, refSeq, clientId, localSeq);
1073
+ return length;
1164
1074
  }
1165
1075
 
1166
1076
  public setMinSeq(minSeq: number): void {
@@ -1199,7 +1109,7 @@ export class MergeTree {
1199
1109
  // from within event handlers, and the collab window's sequence numbers are not updated in time in all of those cases.
1200
1110
  refSeq = Number.MAX_SAFE_INTEGER,
1201
1111
  clientId = this.collabWindow.clientId,
1202
- localSeq: number | undefined = this.collabWindow.localSeq,
1112
+ localSeq: number | undefined = undefined,
1203
1113
  ): number {
1204
1114
  const perspective =
1205
1115
  clientId === this.collabWindow.clientId
@@ -1208,12 +1118,12 @@ export class MergeTree {
1208
1118
  : new LocalReconnectingPerspective(refSeq, clientId, localSeq)
1209
1119
  : new PriorPerspective(refSeq, clientId);
1210
1120
  const seg = refPos.getSegment();
1211
- if (!isSegmentLeaf(seg)) {
1121
+ if (seg === undefined || !isSegmentLeaf(seg)) {
1212
1122
  // We have no idea where this reference is, because it refers to a segment which is not in the tree.
1213
1123
  return DetachedReferencePosition;
1214
1124
  }
1215
1125
  if (refPos.isLeaf()) {
1216
- return this.getPosition(seg, refSeq, clientId, localSeq);
1126
+ return this.getPosition(seg, perspective);
1217
1127
  }
1218
1128
  if (refTypeIncludesFlag(refPos, ReferenceType.Transient) || seg.localRefs?.has(refPos)) {
1219
1129
  if (
@@ -1222,34 +1132,58 @@ export class MergeTree {
1222
1132
  !perspective.isSegmentPresent(seg)
1223
1133
  ) {
1224
1134
  const forward = refPos.slidingPreference === SlidingPreference.FORWARD;
1225
- const moveInfo = toMoveInfo(seg);
1226
1135
  const removeInfo = toRemovalInfo(seg);
1136
+ const firstRemove = removeInfo?.removes[0];
1227
1137
  const slideSeq =
1228
- moveInfo !== undefined && moveInfo.movedSeq !== UnassignedSequenceNumber
1229
- ? moveInfo.movedSeq
1230
- : removeInfo !== undefined && removeInfo.removedSeq !== UnassignedSequenceNumber
1231
- ? removeInfo.removedSeq
1232
- : refSeq;
1233
- const slideLocalSeq = moveInfo?.localMovedSeq ?? removeInfo?.localRemovedSeq;
1138
+ firstRemove !== undefined && opstampUtils.isAcked(firstRemove)
1139
+ ? firstRemove.seq
1140
+ : refSeq;
1141
+
1234
1142
  const slidePerspective =
1235
- slideLocalSeq === undefined
1143
+ firstRemove?.localSeq === undefined
1236
1144
  ? new PriorPerspective(slideSeq, this.collabWindow.clientId)
1237
1145
  : new LocalReconnectingPerspective(
1238
1146
  slideSeq,
1239
1147
  this.collabWindow.clientId,
1240
- slideLocalSeq,
1148
+ firstRemove.localSeq,
1241
1149
  );
1242
- const slidSegment = slidePerspective.nextSegment(this, seg, forward);
1150
+
1151
+ const slidSegment = this.nextSegment(slidePerspective, seg, forward);
1243
1152
  return (
1244
- this.getPosition(slidSegment, refSeq, clientId, localSeq) +
1153
+ this.getPosition(slidSegment, perspective) +
1245
1154
  (forward ? 0 : slidSegment.cachedLength === 0 ? 0 : slidSegment.cachedLength - 1)
1246
1155
  );
1247
1156
  }
1248
- return this.getPosition(seg, refSeq, clientId, localSeq) + refPos.getOffset();
1157
+ return this.getPosition(seg, perspective) + refPos.getOffset();
1249
1158
  }
1250
1159
  return DetachedReferencePosition;
1251
1160
  }
1252
1161
 
1162
+ /**
1163
+ * Returns the immediately adjacent segment in the specified direction from this perspective.
1164
+ * There may actually be multiple segments between the given segment and the returned segment,
1165
+ * but they were either inserted after this perspective, or have been removed before this perspective.
1166
+ *
1167
+ * @param segment - The segment to start from.
1168
+ * @param forward - The direction to search.
1169
+ * @returns the next segment in the specified direction, or the start or end of the tree if there is no next segment.
1170
+ */
1171
+ private nextSegment(
1172
+ perspective: Perspective,
1173
+ segment: ISegmentLeaf,
1174
+ forward: boolean = true,
1175
+ ): ISegmentLeaf {
1176
+ let next: ISegmentLeaf | undefined;
1177
+ const action = (seg: ISegmentLeaf): boolean | undefined => {
1178
+ if (perspective.isSegmentPresent(seg)) {
1179
+ next = seg;
1180
+ return LeafAction.Exit;
1181
+ }
1182
+ };
1183
+ (forward ? forwardExcursion : backwardExcursion)(segment, action);
1184
+ return next ?? (forward ? this.endOfTree : this.startOfTree);
1185
+ }
1186
+
1253
1187
  /**
1254
1188
  * Finds the nearest reference with ReferenceType.Tile to `startPos` in the direction dictated by `forwards`.
1255
1189
  * Uses depthFirstNodeWalk in addition to block-accelerated functionality. The search position will be included in
@@ -1264,13 +1198,12 @@ export class MergeTree {
1264
1198
  */
1265
1199
  public searchForMarker(
1266
1200
  startPos: number,
1267
- clientId: number,
1268
1201
  markerLabel: string,
1269
1202
  forwards = true,
1270
1203
  ): Marker | undefined {
1271
1204
  let foundMarker: Marker | undefined;
1272
1205
 
1273
- const { segment } = this.getContainingSegment(startPos, UniversalSequenceNumber, clientId);
1206
+ const { segment } = this.getContainingSegment(startPos, this.localPerspective);
1274
1207
  if (!isSegmentLeaf(segment)) {
1275
1208
  return undefined;
1276
1209
  }
@@ -1305,15 +1238,13 @@ export class MergeTree {
1305
1238
  return foundMarker;
1306
1239
  }
1307
1240
 
1308
- private updateRoot(splitNode: MergeBlock | undefined): void {
1309
- if (splitNode !== undefined) {
1310
- const newRoot = this.makeBlock(2);
1311
- assignChild(newRoot, this.root, 0, false);
1312
- assignChild(newRoot, splitNode, 1, false);
1313
- this.root = newRoot;
1314
- this.nodeUpdateOrdinals(this.root);
1315
- this.nodeUpdateLengthNewStructure(this.root);
1316
- }
1241
+ private updateRoot(splitNode: MergeBlock): void {
1242
+ const newRoot = this.makeBlock(2);
1243
+ assignChild(newRoot, this.root, 0, false);
1244
+ assignChild(newRoot, splitNode, 1, false);
1245
+ this.root = newRoot;
1246
+ this.nodeUpdateOrdinals(this.root);
1247
+ this.nodeUpdateLengthNewStructure(this.root);
1317
1248
  }
1318
1249
 
1319
1250
  /**
@@ -1322,6 +1253,10 @@ export class MergeTree {
1322
1253
  */
1323
1254
  public ackPendingSegment(opArgs: IMergeTreeDeltaOpArgs): void {
1324
1255
  const seq = opArgs.sequencedMessage!.sequenceNumber;
1256
+ const stamp: OperationStamp = {
1257
+ seq,
1258
+ clientId: this.collabWindow.clientId,
1259
+ };
1325
1260
  const pendingSegmentGroup = this.pendingSegments.shift()?.data;
1326
1261
  const nodesToUpdate: MergeBlock[] = [];
1327
1262
  let overwrite = false;
@@ -1329,9 +1264,14 @@ export class MergeTree {
1329
1264
  const deltaSegments: IMergeTreeSegmentDelta[] = [];
1330
1265
  const overlappingRemoves: boolean[] = [];
1331
1266
  pendingSegmentGroup.segments.map((pendingSegment: ISegmentLeaf) => {
1332
- const overlappingRemove = !ackSegment(pendingSegment, pendingSegmentGroup, opArgs);
1267
+ const overlappingRemove = !ackSegment(
1268
+ pendingSegment,
1269
+ pendingSegmentGroup,
1270
+ opArgs,
1271
+ stamp,
1272
+ );
1333
1273
 
1334
- overwrite ||= overlappingRemove || toMoveInfo(pendingSegment) !== undefined;
1274
+ overwrite ||= overlappingRemove;
1335
1275
 
1336
1276
  overlappingRemoves.push(overlappingRemove);
1337
1277
  if (MergeTree.options.zamboniSegments) {
@@ -1346,7 +1286,7 @@ export class MergeTree {
1346
1286
  });
1347
1287
 
1348
1288
  if (pendingSegmentGroup.obliterateInfo !== undefined) {
1349
- pendingSegmentGroup.obliterateInfo.seq = seq;
1289
+ pendingSegmentGroup.obliterateInfo.stamp = { type: "sliceRemove", ...stamp };
1350
1290
  this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo);
1351
1291
  }
1352
1292
 
@@ -1367,9 +1307,9 @@ export class MergeTree {
1367
1307
  },
1368
1308
  opArgs,
1369
1309
  );
1370
- const clientId = this.collabWindow.clientId;
1310
+
1371
1311
  for (const node of nodesToUpdate) {
1372
- this.blockUpdatePathLengths(node, seq, clientId, overwrite);
1312
+ this.blockUpdatePathLengths(node, stamp, overwrite);
1373
1313
  }
1374
1314
  }
1375
1315
  if (MergeTree.options.zamboniSegments) {
@@ -1415,11 +1355,7 @@ export class MergeTree {
1415
1355
  // TODO: error checking
1416
1356
  public getMarkerFromId(id: string): Marker | undefined {
1417
1357
  const marker = this.idToMarker.get(id);
1418
- return marker === undefined ||
1419
- isRemoved(marker) ||
1420
- (isMoved(marker) && marker.moveDst === undefined)
1421
- ? undefined
1422
- : marker;
1358
+ return marker === undefined || isRemoved(marker) ? undefined : marker;
1423
1359
  }
1424
1360
 
1425
1361
  /**
@@ -1429,18 +1365,14 @@ export class MergeTree {
1429
1365
  * @param refseq - The reference sequence number at which to compute the position.
1430
1366
  * @param clientId - The client id with which to compute the position.
1431
1367
  */
1432
- public posFromRelativePos(
1433
- relativePos: IRelativePosition,
1434
- refseq = this.collabWindow.currentSeq,
1435
- clientId = this.collabWindow.clientId,
1436
- ): number {
1368
+ public posFromRelativePos(relativePos: IRelativePosition, perspective: Perspective): number {
1437
1369
  let pos = -1;
1438
1370
  let marker: Marker | undefined;
1439
1371
  if (relativePos.id) {
1440
1372
  marker = this.getMarkerFromId(relativePos.id);
1441
1373
  }
1442
1374
  if (isSegmentLeaf(marker)) {
1443
- pos = this.getPosition(marker, refseq, clientId);
1375
+ pos = this.getPosition(marker, perspective);
1444
1376
  if (relativePos.before) {
1445
1377
  if (relativePos.offset !== undefined) {
1446
1378
  pos -= relativePos.offset;
@@ -1458,22 +1390,19 @@ export class MergeTree {
1458
1390
  public insertSegments(
1459
1391
  pos: number,
1460
1392
  segments: ISegmentPrivate[],
1461
- refSeq: number,
1462
- clientId: number,
1463
- seq: number,
1393
+ perspective: Perspective,
1394
+ stampArg: OperationStamp,
1464
1395
  opArgs: IMergeTreeDeltaOpArgs | undefined,
1465
1396
  ): void {
1466
- this.ensureIntervalBoundary(pos, refSeq, clientId);
1467
-
1468
- const localSeq =
1469
- seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
1397
+ const stamp: InsertOperationStamp = { ...stampArg, type: "insert" };
1398
+ this.ensureIntervalBoundary(pos, perspective);
1470
1399
 
1471
- this.blockInsert(pos, refSeq, clientId, seq, localSeq, segments);
1400
+ this.blockInsert(pos, perspective, stamp, segments);
1472
1401
 
1473
1402
  // opArgs == undefined => loading snapshot or test code
1474
1403
  if (opArgs !== undefined) {
1475
1404
  const deltaSegments = segments
1476
- .filter((segment) => !toMoveInfo(segment))
1405
+ .filter((segment) => !isRemoved(segment))
1477
1406
  .map((segment) => ({ segment }));
1478
1407
 
1479
1408
  if (deltaSegments.length > 0) {
@@ -1487,7 +1416,7 @@ export class MergeTree {
1487
1416
  if (
1488
1417
  this.collabWindow.collaborating &&
1489
1418
  MergeTree.options.zamboniSegments &&
1490
- seq !== UnassignedSequenceNumber
1419
+ opstampUtils.isAcked(stamp)
1491
1420
  ) {
1492
1421
  zamboniSegments(this);
1493
1422
  }
@@ -1516,30 +1445,23 @@ export class MergeTree {
1516
1445
  return undefined;
1517
1446
  }
1518
1447
 
1519
- const segmentInfo = this.getContainingSegment(
1520
- remoteClientPosition,
1521
- remoteClientRefSeq,
1522
- remoteClientId,
1523
- );
1524
-
1525
- const { currentSeq, clientId } = this.collabWindow;
1448
+ const remotePerspective = new PriorPerspective(remoteClientRefSeq, remoteClientId);
1449
+ const segmentInfo = this.getContainingSegment(remoteClientPosition, remotePerspective);
1526
1450
 
1527
1451
  if (isSegmentLeaf(segmentInfo?.segment)) {
1528
- const segmentPosition = this.getPosition(segmentInfo.segment, currentSeq, clientId);
1452
+ const segmentPosition = this.getPosition(segmentInfo.segment, this.localPerspective);
1529
1453
  return segmentPosition + segmentInfo.offset!;
1530
1454
  } else {
1531
- if (remoteClientPosition === this.getLength(remoteClientRefSeq, remoteClientId)) {
1532
- return this.getLength(currentSeq, clientId);
1455
+ if (remoteClientPosition === this.getLength(remotePerspective)) {
1456
+ return this.getLength(this.localPerspective);
1533
1457
  }
1534
1458
  }
1535
1459
  }
1536
1460
 
1537
1461
  private blockInsert<T extends ISegmentPrivate>(
1538
1462
  pos: number,
1539
- refSeq: number,
1540
- clientId: number,
1541
- seq: number,
1542
- localSeq: number | undefined,
1463
+ perspective: Perspective,
1464
+ stamp: InsertOperationStamp,
1543
1465
  newSegments: T[],
1544
1466
  ): void {
1545
1467
  // Keeping this function within the scope of blockInsert for readability.
@@ -1558,19 +1480,19 @@ export class MergeTree {
1558
1480
  // Save segment so we can assign sequence number when acked by server
1559
1481
  if (this.collabWindow.collaborating) {
1560
1482
  if (
1561
- locSegment.seq === UnassignedSequenceNumber &&
1562
- clientId === this.collabWindow.clientId
1483
+ opstampUtils.isLocal(locSegment.insert) &&
1484
+ stamp.clientId === this.collabWindow.clientId
1563
1485
  ) {
1564
- segmentGroup = this.addToPendingList(locSegment, segmentGroup, localSeq);
1486
+ segmentGroup = this.addToPendingList(locSegment, segmentGroup, stamp.localSeq);
1565
1487
  }
1566
1488
  // LocSegment.seq === 0 when coming from SharedSegmentSequence.loadBody()
1567
1489
  // In all other cases this has to be true (checked by addToLRUSet):
1568
1490
  // locSegment.seq > this.collabWindow.currentSeq
1569
1491
  else if (
1570
- locSegment.seq > this.collabWindow.minSeq &&
1571
- MergeTree.options.zamboniSegments
1492
+ MergeTree.options.zamboniSegments &&
1493
+ opstampUtils.greaterThan(locSegment.insert, getMinSeqStamp(this.collabWindow))
1572
1494
  ) {
1573
- this.addToLRUSet(locSegment, locSegment.seq);
1495
+ this.addToLRUSet(locSegment, locSegment.insert.seq);
1574
1496
  }
1575
1497
  }
1576
1498
  };
@@ -1592,16 +1514,11 @@ export class MergeTree {
1592
1514
  return segmentChanges;
1593
1515
  };
1594
1516
 
1595
- const insertInfo: IInsertionInfo = {
1596
- clientId,
1597
- seq,
1598
- localSeq,
1599
- };
1600
1517
  // TODO: build tree from segs and insert all at once
1601
1518
  let insertPos = pos;
1602
1519
  for (const newSegment of newSegments
1603
1520
  .filter((s) => s.cachedLength > 0)
1604
- .map((s) => overwriteInfo(s, insertInfo))) {
1521
+ .map((s) => overwriteInfo(s, { insert: stamp }))) {
1605
1522
  if (Marker.is(newSegment)) {
1606
1523
  const markerId = newSegment.getId();
1607
1524
  if (markerId) {
@@ -1609,7 +1526,7 @@ export class MergeTree {
1609
1526
  }
1610
1527
  }
1611
1528
 
1612
- const splitNode = this.insertingWalk(this.root, insertPos, refSeq, clientId, seq, {
1529
+ this.insertingWalk(insertPos, perspective, stamp, {
1613
1530
  leaf: onLeaf,
1614
1531
  candidateSegment: newSegment,
1615
1532
  continuePredicate: continueFrom,
@@ -1617,16 +1534,15 @@ export class MergeTree {
1617
1534
 
1618
1535
  if (!isSegmentLeaf(newSegment)) {
1619
1536
  // Indicates an attempt to insert past the end of the merge-tree's content.
1620
- const errorConstructor = localSeq === undefined ? DataProcessingError : UsageError;
1537
+ const errorConstructor =
1538
+ stamp.localSeq === undefined ? DataProcessingError : UsageError;
1621
1539
  throw new errorConstructor("MergeTree insert failed", {
1622
1540
  currentSeq: this.collabWindow.currentSeq,
1623
1541
  minSeq: this.collabWindow.minSeq,
1624
- segSeq: insertInfo.seq,
1542
+ segSeq: stamp.seq,
1625
1543
  });
1626
1544
  }
1627
1545
 
1628
- this.updateRoot(splitNode);
1629
-
1630
1546
  insertPos += newSegment.cachedLength;
1631
1547
 
1632
1548
  if (!this.options?.mergeTreeEnableObliterate || this.obliterates.empty()) {
@@ -1634,55 +1550,48 @@ export class MergeTree {
1634
1550
  continue;
1635
1551
  }
1636
1552
 
1553
+ const overlappingAckedObliterates: RemoveOperationStamp[] = [];
1637
1554
  let oldest: ObliterateInfo | undefined;
1638
- let normalizedOldestSeq: number = 0;
1639
1555
  let newest: ObliterateInfo | undefined;
1640
- let normalizedNewestSeq: number = 0;
1641
- const movedClientIds: number[] = [];
1642
- const movedSeqs: number[] = [];
1643
1556
  let newestAcked: ObliterateInfo | undefined;
1644
1557
  let oldestUnacked: ObliterateInfo | undefined;
1558
+ const refSeqStamp: OperationStamp = {
1559
+ seq: perspective.refSeq,
1560
+ clientId: stamp.clientId,
1561
+ localSeq: stamp.localSeq,
1562
+ };
1645
1563
  for (const ob of this.obliterates.findOverlapping(newSegment)) {
1646
- // compute a normalized seq that takes into account local seqs
1647
- // but is still comparable to remote seqs to keep the checks below easy
1648
- // REMOTE SEQUENCE NUMBERS LOCAL SEQUENCE NUMBERS
1649
- // [0, 1, 2, 3, ..., 100, ..., 1000, ..., (MAX - MaxLocalSeq), L1, L2, L3, L4, ..., L100, ..., L1000, ...(MAX)]
1650
- const normalizedObSeq =
1651
- ob.seq === UnassignedSequenceNumber
1652
- ? Number.MAX_SAFE_INTEGER - this.collabWindow.localSeq + ob.localSeq!
1653
- : ob.seq;
1654
- if (normalizedObSeq > refSeq) {
1564
+ if (opstampUtils.greaterThan(ob.stamp, refSeqStamp)) {
1655
1565
  // Any obliterate from the same client that's inserting this segment cannot cause the segment to be marked as
1656
1566
  // obliterated (since that client must have performed the obliterate before this insertion).
1657
1567
  // We still need to consider such obliterates when determining the winning obliterate for the insertion point,
1658
1568
  // see `obliteratePrecedingInsertion` docs.
1659
- if (clientId !== ob.clientId) {
1660
- if (oldest === undefined || normalizedOldestSeq > normalizedObSeq) {
1661
- normalizedOldestSeq = normalizedObSeq;
1569
+ if (stamp.clientId !== ob.stamp.clientId) {
1570
+ if (opstampUtils.isAcked(ob.stamp)) {
1571
+ overlappingAckedObliterates.push(ob.stamp);
1572
+ }
1573
+
1574
+ if (oldest === undefined || opstampUtils.lessThan(ob.stamp, oldest.stamp)) {
1662
1575
  oldest = ob;
1663
- movedClientIds.unshift(ob.clientId);
1664
- movedSeqs.unshift(ob.seq);
1665
- } else {
1666
- movedClientIds.push(ob.clientId);
1667
- movedSeqs.push(ob.seq);
1668
1576
  }
1669
1577
  }
1670
1578
 
1671
- if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
1672
- normalizedNewestSeq = normalizedObSeq;
1579
+ if (newest === undefined || opstampUtils.greaterThan(ob.stamp, newest.stamp)) {
1673
1580
  newest = ob;
1674
1581
  }
1675
1582
 
1676
1583
  if (
1677
- ob.seq !== UnassignedSequenceNumber &&
1678
- (newestAcked === undefined || newestAcked.seq < ob.seq)
1584
+ opstampUtils.isAcked(ob.stamp) &&
1585
+ (newestAcked === undefined ||
1586
+ opstampUtils.greaterThan(ob.stamp, newestAcked.stamp))
1679
1587
  ) {
1680
1588
  newestAcked = ob;
1681
1589
  }
1682
1590
 
1683
1591
  if (
1684
- ob.seq === UnassignedSequenceNumber &&
1685
- (oldestUnacked === undefined || oldestUnacked.localSeq! > ob.localSeq!)
1592
+ opstampUtils.isLocal(ob.stamp) &&
1593
+ (oldestUnacked === undefined ||
1594
+ opstampUtils.greaterThan(oldestUnacked.stamp, ob.stamp))
1686
1595
  ) {
1687
1596
  // There can be one local obliterate surrounding a segment if a client repeatedly obliterates
1688
1597
  // a region (ex: in the text ABCDEFG, obliterate D, then obliterate CE, then BF). In this case,
@@ -1696,40 +1605,27 @@ export class MergeTree {
1696
1605
  // See doc comment on obliteratePrecedingInsertion for more details: if the newest obliterate was performed
1697
1606
  // by the same client that's inserting this segment, we let them insert into this range and therefore don't
1698
1607
  // mark it obliterated.
1699
- if (oldest && newest?.clientId !== clientId) {
1700
- let moveInfo: IMoveInfo;
1701
- if (newestAcked === newest || newestAcked?.clientId !== clientId) {
1702
- moveInfo = {
1703
- movedClientIds,
1704
- movedSeq: oldest.seq,
1705
- movedSeqs,
1706
- localMovedSeq: oldestUnacked?.localSeq,
1707
- };
1708
- } else {
1709
- assert(
1710
- oldestUnacked !== undefined,
1711
- 0xb55 /* Expected local obliterate to be defined if newestAcked is not equal to newest */,
1712
- );
1713
- // There's a pending local obliterate for this range, so it will be marked as obliterated by us. However,
1714
- // all other clients are under the impression that the most recent acked obliterate won the right to insert
1715
- // in this range.
1716
- moveInfo = {
1717
- movedClientIds: [oldestUnacked.clientId],
1718
- movedSeq: oldestUnacked.seq,
1719
- movedSeqs: [oldestUnacked.seq],
1720
- localMovedSeq: oldestUnacked.localSeq,
1721
- };
1608
+ if (oldest && newest?.stamp.clientId !== stamp.clientId) {
1609
+ const removeInfo: IHasRemovalInfo = { removes: [] };
1610
+ if (newestAcked === newest || newestAcked?.stamp.clientId !== stamp.clientId) {
1611
+ removeInfo.removes = overlappingAckedObliterates;
1612
+ // Because we found these by looking at overlapping obliterates, they are not necessarily currently sorted by seq.
1613
+ // Address that now.
1614
+ removeInfo.removes.sort(opstampUtils.compare);
1722
1615
  }
1723
1616
 
1724
- overwriteInfo(newSegment, moveInfo);
1617
+ // Note that we don't need to worry about preserving any existing remove information since the segment is new.
1618
+ overwriteInfo(newSegment, removeInfo);
1619
+
1620
+ if (oldestUnacked !== undefined) {
1621
+ removeInfo.removes.push(oldestUnacked.stamp);
1725
1622
 
1726
- if (moveInfo.localMovedSeq !== undefined) {
1727
1623
  assert(
1728
- oldestUnacked?.segmentGroup !== undefined,
1624
+ oldestUnacked.segmentGroup !== undefined,
1729
1625
  0x86c /* expected segment group to exist */,
1730
1626
  );
1731
1627
 
1732
- this.addToPendingList(newSegment, oldestUnacked?.segmentGroup);
1628
+ this.addToPendingList(newSegment, oldestUnacked.segmentGroup);
1733
1629
  }
1734
1630
 
1735
1631
  if (newSegment.parent) {
@@ -1738,12 +1634,7 @@ export class MergeTree {
1738
1634
  // lengths inside the inserting walk, we'd be at risk of double-counting the insertion in any case if we allow
1739
1635
  // incremental updates here.
1740
1636
  const newStructure = true;
1741
- this.blockUpdatePathLengths(
1742
- newSegment.parent,
1743
- moveInfo.movedSeq,
1744
- clientId,
1745
- newStructure,
1746
- );
1637
+ this.blockUpdatePathLengths(newSegment.parent, removeInfo.removes[0], newStructure);
1747
1638
  }
1748
1639
  }
1749
1640
 
@@ -1786,55 +1677,55 @@ export class MergeTree {
1786
1677
  return { next };
1787
1678
  };
1788
1679
 
1789
- private ensureIntervalBoundary(pos: number, refSeq: number, clientId: number): void {
1790
- const splitNode = this.insertingWalk(
1791
- this.root,
1680
+ private ensureIntervalBoundary(pos: number, perspective: Perspective): void {
1681
+ this.insertingWalk(
1792
1682
  pos,
1793
- refSeq,
1794
- clientId,
1795
- TreeMaintenanceSequenceNumber,
1683
+ perspective,
1684
+ {
1685
+ seq: TreeMaintenanceSequenceNumber,
1686
+ clientId: perspective.clientId,
1687
+ },
1796
1688
  { leaf: this.splitLeafSegment },
1797
1689
  );
1798
- this.updateRoot(splitNode);
1799
1690
  }
1800
1691
 
1801
1692
  // Assume called only when pos == len
1802
- private breakTie(pos: number, node: IMergeNode, seq: number): boolean {
1693
+ private breakTie(pos: number, node: IMergeNode, insertStamp: OperationStamp): boolean {
1803
1694
  if (node.isLeaf()) {
1804
1695
  if (pos !== 0) {
1805
1696
  return false;
1806
1697
  }
1807
1698
 
1808
- // normalize the seq numbers
1809
- // if the new seg is local (UnassignedSequenceNumber) give it the highest possible
1810
- // seq for comparison, as it will get a seq higher than any other seq once sequences
1811
- // if the current seg is local (UnassignedSequenceNumber) give it the second highest
1812
- // possible seq, as the highest is reserved for the previous.
1813
- const newSeq = seq === UnassignedSequenceNumber ? Number.MAX_SAFE_INTEGER : seq;
1814
- const segSeq =
1815
- node.seq === UnassignedSequenceNumber ? Number.MAX_SAFE_INTEGER - 1 : (node.seq ?? 0);
1816
-
1817
1699
  return (
1818
- newSeq > segSeq ||
1819
- (isMoved(node) && node.movedSeq !== UnassignedSequenceNumber && node.movedSeq > seq) ||
1700
+ opstampUtils.greaterThan(insertStamp, node.insert) ||
1820
1701
  (isRemoved(node) &&
1821
- node.removedSeq !== UnassignedSequenceNumber &&
1822
- node.removedSeq > seq)
1702
+ opstampUtils.isAcked(node.removes[0]) &&
1703
+ opstampUtils.greaterThan(node.removes[0], insertStamp))
1823
1704
  );
1824
1705
  } else {
1825
1706
  return true;
1826
1707
  }
1827
1708
  }
1828
-
1829
1709
  private insertingWalk(
1710
+ pos: number,
1711
+ perspective: Perspective,
1712
+ stamp: OperationStamp,
1713
+ context: InsertContext,
1714
+ ): void {
1715
+ const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context);
1716
+ if (remainder !== undefined) {
1717
+ this.updateRoot(remainder);
1718
+ }
1719
+ }
1720
+
1721
+ private insertRecursive(
1830
1722
  block: MergeBlock,
1831
1723
  pos: number,
1832
- refSeq: number,
1833
- clientId: number,
1834
- seq: number,
1724
+ perspective: Perspective,
1725
+ stamp: OperationStamp,
1835
1726
  context: InsertContext,
1836
1727
  isLastChildBlock: boolean = true,
1837
- ): MergeBlock | undefined {
1728
+ ): InsertResult {
1838
1729
  let _pos: number = pos;
1839
1730
 
1840
1731
  const children = block.children;
@@ -1842,13 +1733,13 @@ export class MergeTree {
1842
1733
  let child: IMergeNode;
1843
1734
  let newNode: IMergeNodeBuilder | undefined;
1844
1735
  let fromSplit: MergeBlock | undefined;
1736
+ let hadChanges = false;
1845
1737
  for (childIndex = 0; childIndex < block.childCount; childIndex++) {
1846
1738
  child = children[childIndex];
1847
1739
  // ensure we walk down the far edge of the tree, even if all sub-tree is eligible for zamboni
1848
1740
  const isLastNonLeafBlock =
1849
1741
  isLastChildBlock && !child.isLeaf() && childIndex === block.childCount - 1;
1850
- const len =
1851
- this.nodeLength(child, refSeq, clientId) ?? (isLastChildBlock ? 0 : undefined);
1742
+ const len = this.nodeLength(child, perspective) ?? (isLastChildBlock ? 0 : undefined);
1852
1743
 
1853
1744
  if (len === undefined) {
1854
1745
  // if the seg len is undefined, the segment
@@ -1858,43 +1749,46 @@ export class MergeTree {
1858
1749
 
1859
1750
  assert(len >= 0, 0x4bc /* Length should not be negative */);
1860
1751
 
1861
- if (_pos < len || (_pos === len && this.breakTie(_pos, child, seq))) {
1752
+ if (_pos < len || (_pos === len && this.breakTie(_pos, child, stamp))) {
1862
1753
  // Found entry containing pos
1863
1754
  if (child.isLeaf()) {
1864
1755
  const segment = child;
1865
1756
  const segmentChanges = context.leaf(segment, _pos, context);
1866
1757
  if (segmentChanges.replaceCurrent) {
1758
+ hadChanges = true;
1867
1759
  assignChild(block, segmentChanges.replaceCurrent, childIndex, false);
1868
1760
  segmentChanges.replaceCurrent.ordinal = child.ordinal;
1869
1761
  }
1870
1762
  if (segmentChanges.next) {
1763
+ hadChanges = true;
1871
1764
  newNode = segmentChanges.next;
1872
1765
  childIndex++; // Insert after
1873
1766
  } else {
1874
- // No change
1875
- return undefined;
1767
+ return { remainder: undefined, hadChanges };
1876
1768
  }
1877
1769
  } else {
1878
1770
  const childBlock = child;
1879
1771
  // Internal node
1880
- const splitNode = this.insertingWalk(
1772
+ const insertResult = this.insertRecursive(
1881
1773
  childBlock,
1882
1774
  _pos,
1883
- refSeq,
1884
- clientId,
1885
- seq,
1775
+ perspective,
1776
+ stamp,
1886
1777
  context,
1887
1778
  isLastNonLeafBlock,
1888
1779
  );
1889
- if (splitNode === undefined) {
1890
- this.blockUpdateLength(block, seq, clientId);
1891
- return undefined;
1892
- } else if (splitNode === MergeTree.theUnfinishedNode) {
1780
+ hadChanges ||= insertResult.hadChanges;
1781
+ if (insertResult.remainder === undefined) {
1782
+ if (insertResult.hadChanges) {
1783
+ this.blockUpdateLength(block, stamp);
1784
+ }
1785
+ return insertResult;
1786
+ } else if (insertResult.remainder === MergeTree.theUnfinishedNode) {
1893
1787
  _pos -= len; // Act as if shifted segment
1894
1788
  continue;
1895
1789
  } else {
1896
- newNode = splitNode;
1897
- fromSplit = splitNode;
1790
+ newNode = insertResult.remainder;
1791
+ fromSplit = insertResult.remainder;
1898
1792
  childIndex++; // Insert after
1899
1793
  }
1900
1794
  }
@@ -1905,7 +1799,7 @@ export class MergeTree {
1905
1799
  }
1906
1800
  if (!newNode && _pos === 0) {
1907
1801
  if (context.continuePredicate?.(block)) {
1908
- return MergeTree.theUnfinishedNode;
1802
+ return { remainder: MergeTree.theUnfinishedNode, hadChanges };
1909
1803
  } else {
1910
1804
  const segmentChanges = context.leaf(undefined, _pos, context);
1911
1805
  newNode = segmentChanges.next;
@@ -1913,6 +1807,7 @@ export class MergeTree {
1913
1807
  }
1914
1808
  }
1915
1809
  if (newNode) {
1810
+ hadChanges = true;
1916
1811
  for (let i = block.childCount; i > childIndex; i--) {
1917
1812
  block.children[i] = block.children[i - 1];
1918
1813
  block.children[i].index = i;
@@ -1924,24 +1819,29 @@ export class MergeTree {
1924
1819
  if (fromSplit) {
1925
1820
  this.nodeUpdateOrdinals(fromSplit);
1926
1821
  }
1927
- this.blockUpdateLength(block, seq, clientId);
1928
- return undefined;
1822
+ this.blockUpdateLength(block, stamp);
1823
+ return { remainder: undefined, hadChanges };
1929
1824
  } else {
1930
1825
  // Don't update ordinals because higher block will do it
1931
1826
  const newNodeFromSplit = this.split(block);
1932
1827
 
1933
- PartialSequenceLengths.options.verifyExpected?.(this, block, refSeq, clientId);
1828
+ PartialSequenceLengths.options.verifyExpected?.(
1829
+ this,
1830
+ block,
1831
+ perspective.refSeq,
1832
+ stamp.clientId,
1833
+ );
1934
1834
  PartialSequenceLengths.options.verifyExpected?.(
1935
1835
  this,
1936
1836
  newNodeFromSplit,
1937
- refSeq,
1938
- clientId,
1837
+ perspective.refSeq,
1838
+ stamp.clientId,
1939
1839
  );
1940
1840
 
1941
- return newNodeFromSplit;
1841
+ return { remainder: newNodeFromSplit, hadChanges };
1942
1842
  }
1943
1843
  } else {
1944
- return undefined;
1844
+ return { remainder: undefined, hadChanges };
1945
1845
  }
1946
1846
  }
1947
1847
 
@@ -1979,28 +1879,22 @@ export class MergeTree {
1979
1879
  * @param clientId - The id of the client making the annotate
1980
1880
  * @param seq - The sequence number of the annotate operation
1981
1881
  * @param opArgs - The op args for the annotate op. this is passed to the merge tree callback if there is one
1982
- * @param rollback - Whether this is for a local rollback and what kind
1983
1882
  */
1984
1883
  public annotateRange(
1985
1884
  start: number,
1986
1885
  end: number,
1987
1886
  propsOrAdjust: PropsOrAdjust,
1988
- refSeq: number,
1989
- clientId: number,
1990
- seq: number,
1887
+ perspective: Perspective,
1888
+ stamp: OperationStamp,
1991
1889
  opArgs: IMergeTreeDeltaOpArgs,
1992
-
1993
- rollback: PropertiesRollback = PropertiesRollback.None,
1994
1890
  ): void {
1995
1891
  if (propsOrAdjust.adjust !== undefined) {
1996
1892
  errorIfOptionNotTrue(this.options, "mergeTreeEnableAnnotateAdjust");
1997
1893
  }
1998
1894
 
1999
- this.ensureIntervalBoundary(start, refSeq, clientId);
2000
- this.ensureIntervalBoundary(end, refSeq, clientId);
1895
+ this.ensureIntervalBoundary(start, perspective);
1896
+ this.ensureIntervalBoundary(end, perspective);
2001
1897
  const deltaSegments: IMergeTreeSegmentDelta[] = [];
2002
- const localSeq =
2003
- seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
2004
1898
 
2005
1899
  let segmentGroup: SegmentGroup | undefined;
2006
1900
  const opObj = propsOrAdjust.props ?? propsOrAdjust.adjust;
@@ -2016,33 +1910,33 @@ export class MergeTree {
2016
1910
  const propertyDeltas = propertyManager.handleProperties(
2017
1911
  propsOrAdjust,
2018
1912
  segment,
2019
- seq,
1913
+ stamp.seq,
2020
1914
  this.collabWindow.minSeq,
2021
1915
  this.collabWindow.collaborating,
2022
- rollback,
1916
+ opArgs?.rollback === true,
2023
1917
  );
2024
1918
 
2025
- if (!isRemovedOrMoved(segment)) {
1919
+ if (!isRemoved(segment)) {
2026
1920
  deltaSegments.push({ segment, propertyDeltas });
2027
1921
  }
2028
1922
  if (this.collabWindow.collaborating) {
2029
- if (seq === UnassignedSequenceNumber) {
1923
+ if (opstampUtils.isLocal(stamp)) {
2030
1924
  segmentGroup = this.addToPendingList(
2031
1925
  segment,
2032
1926
  segmentGroup,
2033
- localSeq,
1927
+ stamp.localSeq,
2034
1928
  propertyDeltas,
2035
1929
  );
2036
1930
  } else {
2037
1931
  if (MergeTree.options.zamboniSegments) {
2038
- this.addToLRUSet(segment, seq);
1932
+ this.addToLRUSet(segment, stamp.seq);
2039
1933
  }
2040
1934
  }
2041
1935
  }
2042
1936
  return true;
2043
1937
  };
2044
1938
 
2045
- this.nodeMap(refSeq, clientId, annotateSegment, undefined, start, end);
1939
+ this.nodeMap(perspective, annotateSegment, undefined, start, end);
2046
1940
 
2047
1941
  // OpArgs == undefined => test code
2048
1942
  if (deltaSegments.length > 0) {
@@ -2053,7 +1947,7 @@ export class MergeTree {
2053
1947
  }
2054
1948
  if (
2055
1949
  this.collabWindow.collaborating &&
2056
- seq !== UnassignedSequenceNumber &&
1950
+ opstampUtils.isAcked(stamp) &&
2057
1951
  MergeTree.options.zamboniSegments
2058
1952
  ) {
2059
1953
  zamboniSegments(this);
@@ -2063,40 +1957,30 @@ export class MergeTree {
2063
1957
  private obliterateRangeSided(
2064
1958
  start: InteriorSequencePlace,
2065
1959
  end: InteriorSequencePlace,
2066
- refSeq: number,
2067
- clientId: number,
2068
- seq: number,
1960
+ perspective: Perspective,
1961
+ stamp: SliceRemoveOperationStamp,
2069
1962
  opArgs: IMergeTreeDeltaOpArgs,
2070
1963
  ): void {
2071
1964
  const startPos = start.side === Side.Before ? start.pos : start.pos + 1;
2072
1965
  const endPos = end.side === Side.Before ? end.pos : end.pos + 1;
2073
1966
 
2074
- this.ensureIntervalBoundary(startPos, refSeq, clientId);
2075
- this.ensureIntervalBoundary(endPos, refSeq, clientId);
1967
+ this.ensureIntervalBoundary(startPos, perspective);
1968
+ this.ensureIntervalBoundary(endPos, perspective);
2076
1969
 
2077
1970
  let _overwrite = false;
2078
1971
  const localOverlapWithRefs: ISegmentLeaf[] = [];
2079
- const movedSegments: SegmentWithInfo<IMoveInfo, ISegmentLeaf>[] = [];
2080
- const localSeq =
2081
- seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
2082
-
2083
- const perspective =
2084
- seq === UnassignedSequenceNumber
2085
- ? this.localPerspective
2086
- : new PriorPerspective(refSeq, clientId);
1972
+ const removedSegments: SegmentWithInfo<IHasRemovalInfo, ISegmentLeaf>[] = [];
2087
1973
 
2088
1974
  const obliterate: ObliterateInfo = {
2089
- clientId,
2090
- end: createDetachedLocalReferencePosition(undefined),
2091
- refSeq,
2092
- seq,
2093
1975
  start: createDetachedLocalReferencePosition(undefined),
2094
- localSeq,
1976
+ end: createDetachedLocalReferencePosition(undefined),
1977
+ refSeq: perspective.refSeq,
1978
+ stamp,
2095
1979
  segmentGroup: undefined,
2096
1980
  };
2097
1981
 
2098
- const { segment: startSeg } = this.getContainingSegment(start.pos, refSeq, clientId);
2099
- const { segment: endSeg } = this.getContainingSegment(end.pos, refSeq, clientId);
1982
+ const { segment: startSeg } = this.getContainingSegment(start.pos, perspective);
1983
+ const { segment: endSeg } = this.getContainingSegment(end.pos, perspective);
2100
1984
  assert(
2101
1985
  isSegmentLeaf(startSeg) && isSegmentLeaf(endSeg),
2102
1986
  0xa3f /* segments cannot be undefined */,
@@ -2126,16 +2010,16 @@ export class MergeTree {
2126
2010
  // at which point they are added to the segment group.
2127
2011
  obliterate.segmentGroup = {
2128
2012
  segments: [],
2129
- localSeq,
2013
+ localSeq: stamp.localSeq,
2130
2014
  refSeq: this.collabWindow.currentSeq,
2131
2015
  obliterateInfo: obliterate,
2132
2016
  };
2133
- if (this.collabWindow.collaborating && clientId === this.collabWindow.clientId) {
2017
+ if (this.collabWindow.collaborating && stamp.clientId === this.collabWindow.clientId) {
2134
2018
  this.pendingSegments.push(obliterate.segmentGroup);
2135
2019
  }
2136
2020
  this.obliterates.addOrUpdate(obliterate);
2137
2021
 
2138
- const markMoved = (segment: ISegmentLeaf, pos: number): boolean => {
2022
+ const markRemoved = (segment: ISegmentLeaf, pos: number): boolean => {
2139
2023
  if (
2140
2024
  (start.side === Side.After && startPos === pos + segment.cachedLength) || // exclusive start segment
2141
2025
  (end.side === Side.Before && endPos === pos && perspective.isSegmentPresent(segment)) // exclusive end segment
@@ -2144,16 +2028,16 @@ export class MergeTree {
2144
2028
  // These segments are outside of the obliteration range though, so return true to keep walking.
2145
2029
  return true;
2146
2030
  }
2147
- const existingMoveInfo = toMoveInfo(segment);
2031
+ const existingRemoveInfo = toRemovalInfo(segment);
2148
2032
 
2149
2033
  // The "last-to-obliterate-gets-to-insert" policy described by the doc comment on `obliteratePrecedingInsertion`
2150
2034
  // is mostly handled by logic at insertion time, but we need a small bit of handling here.
2151
2035
  // Specifically, we want to avoid marking a local-only segment as obliterated when we know one of our own local obliterates
2152
2036
  // will win against the obliterate we're processing, hence the early exit.
2153
2037
  if (
2154
- segment.seq === UnassignedSequenceNumber &&
2155
- segment.obliteratePrecedingInsertion?.seq === UnassignedSequenceNumber &&
2156
- seq !== UnassignedSequenceNumber
2038
+ opstampUtils.isLocal(segment.insert) &&
2039
+ segment.obliteratePrecedingInsertion?.stamp.seq === UnassignedSequenceNumber &&
2040
+ opstampUtils.isAcked(stamp)
2157
2041
  ) {
2158
2042
  // We chose to not obliterate this segment because we are aware of an unacked local obliteration.
2159
2043
  // The local obliterate has not been sequenced yet, so it is still the newest obliterate we are aware of.
@@ -2162,94 +2046,69 @@ export class MergeTree {
2162
2046
  }
2163
2047
 
2164
2048
  // Partial lengths incrementality is not supported for overlapping obliterate/removes.
2165
- _overwrite ||= existingMoveInfo !== undefined || toRemovalInfo(segment) !== undefined;
2166
-
2167
- if (existingMoveInfo === undefined) {
2168
- const movedSeg = overwriteInfo<IMoveInfo, ISegmentLeaf>(segment, {
2169
- movedClientIds: [clientId],
2170
- movedSeq: seq,
2171
- localMovedSeq: localSeq,
2172
- movedSeqs: [seq],
2049
+ _overwrite ||= existingRemoveInfo !== undefined;
2050
+
2051
+ // - Record the segment as removed
2052
+ // - If this was the first thing to remove the segment from the local view, add it to removedSegments
2053
+ // - Otherwise, if it was the first thing to remove the segment from the acked view, add it to localOverlapWithRefs (so we can slide them)
2054
+ if (existingRemoveInfo === undefined) {
2055
+ const removed = overwriteInfo<IHasRemovalInfo, ISegmentLeaf>(segment, {
2056
+ removes: [stamp],
2173
2057
  });
2174
2058
 
2175
- const existingRemoval = toRemovalInfo(movedSeg);
2176
- if (existingRemoval === undefined) {
2177
- movedSegments.push(movedSeg);
2178
- } else if (
2179
- existingRemoval.removedSeq === UnassignedSequenceNumber &&
2059
+ removedSegments.push(removed);
2060
+ } else {
2061
+ // The segment has already been removed, so we don't need to add it to removedSegments. However,
2062
+ // if it's only been removed locally, we still need to slide any references that may exist on it.
2063
+ if (
2064
+ !opstampUtils.hasAnyAckedOperation(existingRemoveInfo.removes) &&
2180
2065
  segment.localRefs?.empty === false
2181
2066
  ) {
2182
- // We removed this locally already so we don't need to event it again, but it might have references
2183
- // that need sliding now that a move may have been acked.
2184
2067
  localOverlapWithRefs.push(segment);
2185
2068
  }
2186
- } else {
2187
- if (existingMoveInfo.movedSeq === UnassignedSequenceNumber) {
2188
- assert(
2189
- !wasMovedOnInsert(segment),
2190
- 0xab4 /* Local obliterate cannot have removed a segment as soon as it was inserted */,
2191
- );
2192
- assert(
2193
- seq !== UnassignedSequenceNumber,
2194
- 0xab5 /* Cannot obliterate the same segment locally twice */,
2195
- );
2196
-
2197
- // we moved this locally, but someone else moved it first
2198
- // so put them at the head of the list
2199
- // The list isn't ordered, but we keep the first move at the head
2200
- // for partialLengths bookkeeping purposes
2201
- existingMoveInfo.movedClientIds.unshift(clientId);
2202
-
2203
- existingMoveInfo.movedSeq = seq;
2204
- existingMoveInfo.movedSeqs.unshift(seq);
2205
- if (segment.localRefs?.empty === false) {
2206
- localOverlapWithRefs.push(segment);
2207
- }
2208
- } else {
2209
- // Do not replace earlier sequence number for move
2210
- existingMoveInfo.movedClientIds.push(clientId);
2211
- existingMoveInfo.movedSeqs.push(seq);
2212
- }
2069
+ opstampUtils.spliceIntoList(existingRemoveInfo.removes, stamp);
2213
2070
  }
2214
- assertMoved(segment);
2215
- // Save segment so can assign moved sequence number when acked by server
2071
+ assertRemoved(segment);
2072
+ // Save segment so can assign sequence number when acked by server
2216
2073
  if (this.collabWindow.collaborating) {
2217
2074
  if (
2218
- segment.movedSeq === UnassignedSequenceNumber &&
2219
- clientId === this.collabWindow.clientId
2075
+ opstampUtils.isLocal(segment.removes[0]) &&
2076
+ stamp.clientId === this.collabWindow.clientId
2220
2077
  ) {
2221
2078
  obliterate.segmentGroup = this.addToPendingList(
2222
2079
  segment,
2223
2080
  obliterate.segmentGroup,
2224
- localSeq,
2081
+ stamp.localSeq,
2225
2082
  );
2226
2083
  } else {
2227
2084
  if (MergeTree.options.zamboniSegments) {
2228
- this.addToLRUSet(segment, seq);
2085
+ this.addToLRUSet(segment, stamp.seq);
2229
2086
  }
2230
2087
  }
2231
2088
  }
2232
2089
  return true;
2233
2090
  };
2234
2091
 
2235
- const afterMarkMoved = (node: MergeBlock): boolean => {
2092
+ const afterMarkRemoved = (node: MergeBlock): boolean => {
2236
2093
  if (_overwrite) {
2237
2094
  this.nodeUpdateLengthNewStructure(node);
2238
2095
  } else {
2239
- this.blockUpdateLength(node, seq, clientId);
2096
+ this.blockUpdateLength(node, stamp);
2240
2097
  }
2241
2098
  return true;
2242
2099
  };
2243
2100
 
2244
2101
  this.nodeMap(
2245
- refSeq,
2246
- clientId,
2247
- markMoved,
2248
- afterMarkMoved,
2102
+ perspective,
2103
+ markRemoved,
2104
+ afterMarkRemoved,
2249
2105
  start.pos,
2250
2106
  end.pos + 1, // include the segment containing the end reference
2251
- undefined,
2252
- seq === UnassignedSequenceNumber ? undefined : seq,
2107
+ // Use a visibilityPerspective which includes all segments (including local ones) which are in the obliteration range.
2108
+ // This ensures that concurrently inserted segments will also be marked obliterated.
2109
+ opstampUtils.isLocal(stamp)
2110
+ ? perspective
2111
+ : new RemoteObliteratePerspective(stamp.clientId),
2253
2112
  );
2254
2113
 
2255
2114
  this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
@@ -2257,20 +2116,20 @@ export class MergeTree {
2257
2116
  if (start.pos !== end.pos || start.side !== end.side) {
2258
2117
  this.mergeTreeDeltaCallback?.(opArgs, {
2259
2118
  operation: MergeTreeDeltaType.OBLITERATE,
2260
- deltaSegments: movedSegments.map((segment) => ({ segment })),
2119
+ deltaSegments: removedSegments.map((segment) => ({ segment })),
2261
2120
  });
2262
2121
  }
2263
2122
 
2264
2123
  // these events are newly removed
2265
2124
  // so we slide after eventing in case the consumer wants to make reference
2266
2125
  // changes at remove time, like add a ref to track undo redo.
2267
- if (!this.collabWindow.collaborating || clientId !== this.collabWindow.clientId) {
2268
- this.slideAckedRemovedSegmentReferences(movedSegments);
2126
+ if (!this.collabWindow.collaborating || stamp.clientId !== this.collabWindow.clientId) {
2127
+ this.slideAckedRemovedSegmentReferences(removedSegments);
2269
2128
  }
2270
2129
 
2271
2130
  if (
2272
2131
  this.collabWindow.collaborating &&
2273
- seq !== UnassignedSequenceNumber &&
2132
+ opstampUtils.isAcked(stamp) &&
2274
2133
  MergeTree.options.zamboniSegments
2275
2134
  ) {
2276
2135
  zamboniSegments(this);
@@ -2280,18 +2139,18 @@ export class MergeTree {
2280
2139
  public obliterateRange(
2281
2140
  start: number | InteriorSequencePlace,
2282
2141
  end: number | InteriorSequencePlace,
2283
- refSeq: number,
2284
- clientId: number,
2285
- seq: number,
2142
+ perspective: Perspective,
2143
+ stampArg: OperationStamp,
2286
2144
  opArgs: IMergeTreeDeltaOpArgs,
2287
2145
  ): void {
2288
2146
  errorIfOptionNotTrue(this.options, "mergeTreeEnableObliterate");
2147
+ const stamp: SliceRemoveOperationStamp = { ...stampArg, type: "sliceRemove" };
2289
2148
  if (this.options?.mergeTreeEnableSidedObliterate) {
2290
2149
  assert(
2291
2150
  typeof start === "object" && typeof end === "object",
2292
2151
  0xa45 /* Start and end must be of type InteriorSequencePlace if mergeTreeEnableSidedObliterate is enabled. */,
2293
2152
  );
2294
- this.obliterateRangeSided(start, end, refSeq, clientId, seq, opArgs);
2153
+ this.obliterateRangeSided(start, end, perspective, stamp, opArgs);
2295
2154
  } else {
2296
2155
  assert(
2297
2156
  typeof start === "number" && typeof end === "number",
@@ -2300,9 +2159,8 @@ export class MergeTree {
2300
2159
  this.obliterateRangeSided(
2301
2160
  { pos: start, side: Side.Before },
2302
2161
  { pos: end - 1, side: Side.After },
2303
- refSeq,
2304
- clientId,
2305
- seq,
2162
+ perspective,
2163
+ stamp,
2306
2164
  opArgs,
2307
2165
  );
2308
2166
  }
@@ -2311,20 +2169,19 @@ export class MergeTree {
2311
2169
  public markRangeRemoved(
2312
2170
  start: number,
2313
2171
  end: number,
2314
- refSeq: number,
2315
- clientId: number,
2316
- seq: number,
2172
+ perspective: Perspective,
2173
+ stampArg: OperationStamp,
2317
2174
  opArgs: IMergeTreeDeltaOpArgs,
2318
2175
  ): void {
2319
2176
  let _overwrite = false;
2320
- this.ensureIntervalBoundary(start, refSeq, clientId);
2321
- this.ensureIntervalBoundary(end, refSeq, clientId);
2177
+ const stamp: SetRemoveOperationStamp = { ...stampArg, type: "setRemove" };
2178
+ this.ensureIntervalBoundary(start, perspective);
2179
+ this.ensureIntervalBoundary(end, perspective);
2322
2180
 
2323
2181
  let segmentGroup: SegmentGroup;
2324
- const removedSegments: SegmentWithInfo<IRemovalInfo, ISegmentLeaf>[] = [];
2182
+ const removedSegments: SegmentWithInfo<IHasRemovalInfo, ISegmentLeaf>[] = [];
2325
2183
  const localOverlapWithRefs: ISegmentLeaf[] = [];
2326
- const localSeq =
2327
- seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
2184
+
2328
2185
  const markRemoved = (
2329
2186
  segment: ISegmentLeaf,
2330
2187
  pos: number,
@@ -2334,53 +2191,34 @@ export class MergeTree {
2334
2191
  const existingRemovalInfo = toRemovalInfo(segment);
2335
2192
 
2336
2193
  // Partial lengths incrementality is not supported for overlapping obliterate/removes.
2337
- _overwrite ||= existingRemovalInfo !== undefined || toMoveInfo(segment) !== undefined;
2194
+ _overwrite ||= existingRemovalInfo !== undefined;
2338
2195
  if (existingRemovalInfo === undefined) {
2339
- const removed = overwriteInfo<IRemovalInfo, ISegmentLeaf>(segment, {
2340
- removedClientIds: [clientId],
2341
- removedSeq: seq,
2342
- localRemovedSeq: localSeq,
2196
+ const removed = overwriteInfo<IHasRemovalInfo, ISegmentLeaf>(segment, {
2197
+ removes: [stamp],
2343
2198
  });
2344
2199
 
2345
- const existingMoveInfo = toMoveInfo(removed);
2346
- if (existingMoveInfo === undefined) {
2347
- removedSegments.push(removed);
2348
- } else if (
2349
- existingMoveInfo.movedSeq === UnassignedSequenceNumber &&
2200
+ removedSegments.push(removed);
2201
+ } else {
2202
+ if (
2203
+ !opstampUtils.hasAnyAckedOperation(existingRemovalInfo.removes) &&
2350
2204
  segment.localRefs?.empty === false
2351
2205
  ) {
2352
- // We moved this locally already so we don't need to event it again, but it might have references
2353
- // that need sliding now that a remove may have been acked.
2354
2206
  localOverlapWithRefs.push(segment);
2355
2207
  }
2356
- } else {
2357
- if (existingRemovalInfo.removedSeq === UnassignedSequenceNumber) {
2358
- // we removed this locally, but someone else removed it first
2359
- // so put them at the head of the list
2360
- // The list isn't ordered, but we keep the first removal at the head
2361
- // for partialLengths bookkeeping purposes
2362
- existingRemovalInfo.removedClientIds.unshift(clientId);
2363
-
2364
- existingRemovalInfo.removedSeq = seq;
2365
- if (segment.localRefs?.empty === false) {
2366
- localOverlapWithRefs.push(segment);
2367
- }
2368
- } else {
2369
- // Do not replace earlier sequence number for remove
2370
- existingRemovalInfo.removedClientIds.push(clientId);
2371
- }
2208
+ opstampUtils.spliceIntoList(existingRemovalInfo.removes, stamp);
2372
2209
  }
2373
2210
  assertRemoved(segment);
2211
+
2374
2212
  // Save segment so we can assign removed sequence number when acked by server
2375
2213
  if (this.collabWindow.collaborating) {
2376
2214
  if (
2377
- segment.removedSeq === UnassignedSequenceNumber &&
2378
- clientId === this.collabWindow.clientId
2215
+ opstampUtils.isLocal(segment.removes[0]) &&
2216
+ stamp.clientId === this.collabWindow.clientId
2379
2217
  ) {
2380
- segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
2218
+ segmentGroup = this.addToPendingList(segment, segmentGroup, stamp.localSeq);
2381
2219
  } else {
2382
2220
  if (MergeTree.options.zamboniSegments) {
2383
- this.addToLRUSet(segment, seq);
2221
+ this.addToLRUSet(segment, stamp.seq);
2384
2222
  }
2385
2223
  }
2386
2224
  }
@@ -2390,11 +2228,11 @@ export class MergeTree {
2390
2228
  if (_overwrite) {
2391
2229
  this.nodeUpdateLengthNewStructure(node);
2392
2230
  } else {
2393
- this.blockUpdateLength(node, seq, clientId);
2231
+ this.blockUpdateLength(node, stamp);
2394
2232
  }
2395
2233
  return true;
2396
2234
  };
2397
- this.nodeMap(refSeq, clientId, markRemoved, afterMarkRemoved, start, end);
2235
+ this.nodeMap(perspective, markRemoved, afterMarkRemoved, start, end);
2398
2236
  // these segments are already viewed as being removed locally and are not event-ed
2399
2237
  // so can slide non-StayOnRemove refs immediately
2400
2238
  this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
@@ -2408,13 +2246,13 @@ export class MergeTree {
2408
2246
  // these events are newly removed
2409
2247
  // so we slide after eventing in case the consumer wants to make reference
2410
2248
  // changes at remove time, like add a ref to track undo redo.
2411
- if (!this.collabWindow.collaborating || clientId !== this.collabWindow.clientId) {
2249
+ if (!this.collabWindow.collaborating || stamp.clientId !== this.collabWindow.clientId) {
2412
2250
  this.slideAckedRemovedSegmentReferences(removedSegments);
2413
2251
  }
2414
2252
 
2415
2253
  if (
2416
2254
  this.collabWindow.collaborating &&
2417
- seq !== UnassignedSequenceNumber &&
2255
+ opstampUtils.isAcked(stamp) &&
2418
2256
  MergeTree.options.zamboniSegments
2419
2257
  ) {
2420
2258
  zamboniSegments(this);
@@ -2424,7 +2262,6 @@ export class MergeTree {
2424
2262
  /**
2425
2263
  * Revert an unacked local op
2426
2264
  */
2427
-
2428
2265
  public rollback(op: IMergeTreeDeltaOp, localOpMetadata: SegmentGroup): void {
2429
2266
  if (op.type === MergeTreeDeltaType.REMOVE) {
2430
2267
  const pendingSegmentGroup = this.pendingSegments.pop()?.data;
@@ -2441,24 +2278,30 @@ export class MergeTree {
2441
2278
  );
2442
2279
 
2443
2280
  assert(
2444
- isRemoved(segment) && segment.removedClientIds[0] === this.collabWindow.clientId,
2281
+ isRemoved(segment) &&
2282
+ segment.removes[0].clientId === this.collabWindow.clientId &&
2283
+ segment.removes[0].type === "setRemove",
2445
2284
  0x39d /* Rollback segment removedClientId does not match local client */,
2446
2285
  );
2447
2286
  let updateNode: MergeBlock | undefined = segment.parent;
2287
+ // This also removes obliterates, but that should be ok as we can only remove a segment once.
2288
+ // If we were able to remove it locally, that also means there are no remote removals (since rollback is synchronous).
2448
2289
  removeRemovalInfo(segment);
2449
2290
 
2450
2291
  for (updateNode; updateNode !== undefined; updateNode = updateNode.parent) {
2451
- this.blockUpdateLength(
2452
- updateNode,
2453
- UnassignedSequenceNumber,
2454
- this.collabWindow.clientId,
2455
- );
2292
+ this.blockUpdateLength(updateNode, {
2293
+ seq: UnassignedSequenceNumber,
2294
+ clientId: this.collabWindow.clientId,
2295
+ });
2456
2296
  }
2457
2297
 
2458
2298
  // Note: optional chaining short-circuits:
2459
2299
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting
2460
2300
  this.mergeTreeDeltaCallback?.(
2461
- { op: createInsertSegmentOp(this.findRollbackPosition(segment), segment) },
2301
+ {
2302
+ op: createInsertSegmentOp(this.findRollbackPosition(segment), segment),
2303
+ rollback: true,
2304
+ },
2462
2305
  {
2463
2306
  operation: MergeTreeDeltaType.INSERT,
2464
2307
  deltaSegments: [{ segment }],
@@ -2487,29 +2330,38 @@ export class MergeTree {
2487
2330
 
2488
2331
  const start = this.findRollbackPosition(segment);
2489
2332
  if (op.type === MergeTreeDeltaType.INSERT) {
2490
- segment.seq = UniversalSequenceNumber;
2491
- segment.localSeq = undefined;
2333
+ segment.insert = {
2334
+ type: "insert",
2335
+ seq: UniversalSequenceNumber,
2336
+ clientId: this.collabWindow.clientId,
2337
+ };
2492
2338
  const removeOp = createRemoveRangeOp(start, start + segment.cachedLength);
2339
+ const removeStamp: SetRemoveOperationStamp = {
2340
+ type: "setRemove",
2341
+ seq: UniversalSequenceNumber,
2342
+ clientId: this.collabWindow.clientId,
2343
+ };
2493
2344
  this.markRangeRemoved(
2494
2345
  start,
2495
2346
  start + segment.cachedLength,
2496
- UniversalSequenceNumber,
2497
- this.collabWindow.clientId,
2498
- UniversalSequenceNumber,
2499
- { op: removeOp },
2347
+ this.localPerspective,
2348
+ removeStamp,
2349
+ { op: removeOp, rollback: true },
2500
2350
  );
2501
2351
  } /* op.type === MergeTreeDeltaType.ANNOTATE */ else {
2502
2352
  const props = pendingSegmentGroup.previousProps![i];
2503
2353
  const annotateOp = createAnnotateRangeOp(start, start + segment.cachedLength, props);
2354
+ const annotateStamp: OperationStamp = {
2355
+ seq: UniversalSequenceNumber,
2356
+ clientId: this.collabWindow.clientId,
2357
+ };
2504
2358
  this.annotateRange(
2505
2359
  start,
2506
2360
  start + segment.cachedLength,
2507
2361
  { props },
2508
- UniversalSequenceNumber,
2509
- this.collabWindow.clientId,
2510
- UniversalSequenceNumber,
2511
- { op: annotateOp },
2512
- PropertiesRollback.Rollback,
2362
+ this.localPerspective,
2363
+ annotateStamp,
2364
+ { op: annotateOp, rollback: true },
2513
2365
  );
2514
2366
  i++;
2515
2367
  }
@@ -2570,7 +2422,7 @@ export class MergeTree {
2570
2422
  if (
2571
2423
  _segment !== "start" &&
2572
2424
  _segment !== "end" &&
2573
- isRemovedAndAckedOrMovedAndAcked(_segment) &&
2425
+ isRemovedAndAcked(_segment) &&
2574
2426
  !refTypeIncludesFlag(
2575
2427
  refType,
2576
2428
  ReferenceType.SlideOnRemove | ReferenceType.Transient | ReferenceType.StayOnRemove,
@@ -2637,7 +2489,7 @@ export class MergeTree {
2637
2489
  affectedSegments.insertAfter(lastLocalSegment, segmentToSlide.data);
2638
2490
  } else if (isRemoved(segmentToSlide.data)) {
2639
2491
  assert(
2640
- segmentToSlide.data.localRemovedSeq !== undefined,
2492
+ segmentToSlide.data.removes[0].seq !== undefined,
2641
2493
  0x54d /* Removed segment that hasnt had its removal acked should be locally removed */,
2642
2494
  );
2643
2495
  // Slide each locally removed item past all segments that have localSeq > lremoveItem.localSeq
@@ -2647,8 +2499,8 @@ export class MergeTree {
2647
2499
  while (
2648
2500
  scan !== undefined &&
2649
2501
  !isRemovedAndAcked(scan.data) &&
2650
- scan.data.localSeq !== undefined &&
2651
- scan.data.localSeq > segmentToSlide.data.localRemovedSeq
2502
+ scan.data.insert.localSeq !== undefined &&
2503
+ opstampUtils.greaterThan(scan.data.insert, segmentToSlide.data.removes[0])
2652
2504
  ) {
2653
2505
  cur = scan;
2654
2506
  scan = scan.next;
@@ -2741,11 +2593,11 @@ export class MergeTree {
2741
2593
  }
2742
2594
  };
2743
2595
  walkAllChildSegments(this.root, (seg) => {
2744
- if (isRemoved(seg) || seg.seq === UnassignedSequenceNumber) {
2596
+ if (isRemoved(seg) || opstampUtils.isLocal(seg.insert)) {
2745
2597
  if (isRemovedAndAcked(seg)) {
2746
2598
  rangeContainsRemoteRemovedSegs = true;
2747
2599
  }
2748
- if (seg.seq === UnassignedSequenceNumber) {
2600
+ if (opstampUtils.isLocal(seg.insert)) {
2749
2601
  rangeContainsLocalSegs = true;
2750
2602
  }
2751
2603
  currentRangeToNormalize.push(seg);
@@ -2776,7 +2628,7 @@ export class MergeTree {
2776
2628
  }
2777
2629
  if (node.isLeaf()) {
2778
2630
  const segment = node;
2779
- if ((this.localNetLength(segment) ?? 0) > 0 && Marker.is(segment)) {
2631
+ if ((this.leafLength(segment) ?? 0) > 0 && Marker.is(segment)) {
2780
2632
  const markerId = segment.getId();
2781
2633
  // Also in insertMarker but need for reload segs case
2782
2634
  // can add option for this only from reload segs
@@ -2809,8 +2661,7 @@ export class MergeTree {
2809
2661
 
2810
2662
  public blockUpdatePathLengths(
2811
2663
  startBlock: MergeBlock | undefined,
2812
- seq: number,
2813
- clientId: number,
2664
+ stamp: OperationStamp,
2814
2665
  newStructure = false,
2815
2666
  ): void {
2816
2667
  let block: MergeBlock | undefined = startBlock;
@@ -2818,31 +2669,31 @@ export class MergeTree {
2818
2669
  if (newStructure) {
2819
2670
  this.nodeUpdateLengthNewStructure(block);
2820
2671
  } else {
2821
- this.blockUpdateLength(block, seq, clientId);
2672
+ this.blockUpdateLength(block, stamp);
2822
2673
  }
2823
2674
  block = block.parent;
2824
2675
  }
2825
2676
  }
2826
2677
 
2827
- private blockUpdateLength(node: MergeBlock, seq: number, clientId: number): void {
2678
+ private blockUpdateLength(node: MergeBlock, stamp: OperationStamp): void {
2828
2679
  this.blockUpdate(node);
2829
2680
  this.localPartialsComputed = false;
2830
2681
  if (
2831
2682
  this.collabWindow.collaborating &&
2832
- seq !== UnassignedSequenceNumber &&
2833
- seq !== TreeMaintenanceSequenceNumber
2683
+ opstampUtils.isAcked(stamp) &&
2684
+ stamp.seq !== TreeMaintenanceSequenceNumber
2834
2685
  ) {
2835
2686
  if (
2836
2687
  node.partialLengths !== undefined &&
2837
2688
  MergeTree.options.incrementalUpdate &&
2838
- clientId !== NonCollabClient
2689
+ stamp.clientId !== NonCollabClient
2839
2690
  ) {
2840
- node.partialLengths.update(node, seq, clientId, this.collabWindow);
2691
+ node.partialLengths.update(node, stamp.seq, stamp.clientId, this.collabWindow);
2841
2692
  } else {
2842
2693
  node.partialLengths = PartialSequenceLengths.combine(node, this.collabWindow);
2843
2694
  }
2844
2695
 
2845
- PartialSequenceLengths.options.verifyExpected?.(this, node, seq, clientId);
2696
+ PartialSequenceLengths.options.verifyExpected?.(this, node, stamp.seq, stamp.clientId);
2846
2697
  }
2847
2698
  }
2848
2699
 
@@ -2855,31 +2706,29 @@ export class MergeTree {
2855
2706
  */
2856
2707
  public mapRange<TClientData>(
2857
2708
  handler: ISegmentAction<TClientData>,
2858
- refSeq: number,
2859
- clientId: number,
2709
+ perspective: Perspective,
2860
2710
  accum: TClientData,
2861
2711
  start?: number,
2862
2712
  end?: number,
2863
2713
  splitRange: boolean = false,
2864
- visibilitySeq: number = refSeq,
2714
+ visibilityPerspective: Perspective = perspective,
2865
2715
  ): void {
2866
2716
  if (splitRange) {
2867
2717
  if (start) {
2868
- this.ensureIntervalBoundary(start, refSeq, clientId);
2718
+ this.ensureIntervalBoundary(start, perspective);
2869
2719
  }
2870
2720
  if (end) {
2871
- this.ensureIntervalBoundary(end, refSeq, clientId);
2721
+ this.ensureIntervalBoundary(end, perspective);
2872
2722
  }
2873
2723
  }
2874
2724
  this.nodeMap(
2875
- refSeq,
2876
- clientId,
2877
- (seg, pos, _start, _end) => handler(seg, pos, refSeq, clientId, _start, _end, accum),
2725
+ perspective,
2726
+ (seg, pos, _start, _end) =>
2727
+ handler(seg, pos, perspective.refSeq, perspective.clientId, _start, _end, accum),
2878
2728
  undefined,
2879
2729
  start,
2880
2730
  end,
2881
- undefined,
2882
- visibilitySeq,
2731
+ visibilityPerspective,
2883
2732
  );
2884
2733
  }
2885
2734
 
@@ -2907,16 +2756,14 @@ export class MergeTree {
2907
2756
  * ignored for the purposes of tracking when traversal should end.
2908
2757
  */
2909
2758
  private nodeMap(
2910
- refSeq: number,
2911
- clientId: number,
2759
+ perspective: Perspective,
2912
2760
  leaf: (segment: ISegmentLeaf, pos: number, start: number, end: number) => boolean,
2913
2761
  post?: (block: MergeBlock) => boolean,
2914
2762
  start: number = 0,
2915
2763
  end?: number,
2916
- localSeq?: number,
2917
- visibilitySeq: number = refSeq,
2764
+ visibilityPerspective: Perspective = perspective,
2918
2765
  ): void {
2919
- const endPos = end ?? this.nodeLength(this.root, refSeq, clientId, localSeq) ?? 0;
2766
+ const endPos = end ?? this.nodeLength(this.root, perspective) ?? 0;
2920
2767
  if (endPos === start) {
2921
2768
  return;
2922
2769
  }
@@ -2931,16 +2778,18 @@ export class MergeTree {
2931
2778
  return NodeAction.Exit;
2932
2779
  }
2933
2780
 
2934
- const len = this.nodeLength(node, visibilitySeq, clientId, localSeq);
2781
+ const len = this.nodeLength(node, visibilityPerspective);
2935
2782
  const lenAtRefSeq =
2936
- (visibilitySeq === refSeq
2937
- ? len
2938
- : this.nodeLength(node, refSeq, clientId, localSeq)) ?? 0;
2783
+ (visibilityPerspective === perspective ? len : this.nodeLength(node, perspective)) ??
2784
+ 0;
2939
2785
 
2786
+ // NOTE: This code ensures that obliterates have a chance to mark segments which have been inserted locally
2787
+ // as also having been obliterated on the local client. With the introduction of RemoteObliteratePerspective,
2788
+ // it's feasible we could remove it if the `nodeLength` calculation also respects that perspective for blocks
2789
+ // and not just leaves.
2940
2790
  const isUnackedAndInObliterate =
2941
- visibilitySeq !== refSeq &&
2942
- (!node.isLeaf() || node.seq === UnassignedSequenceNumber);
2943
-
2791
+ visibilityPerspective !== perspective &&
2792
+ (!node.isLeaf() || opstampUtils.isLocal(node.insert));
2944
2793
  if (
2945
2794
  (len === undefined && lenAtRefSeq === 0) ||
2946
2795
  (len === 0 && !isUnackedAndInObliterate && lenAtRefSeq === 0)