@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/lib/mergeTree.js CHANGED
@@ -11,89 +11,83 @@ import { NonCollabClient, TreeMaintenanceSequenceNumber, UnassignedSequenceNumbe
11
11
  import { EndOfTreeSegment, StartOfTreeSegment } from "./endOfTreeSegment.js";
12
12
  import { LocalReferenceCollection, SlidingPreference, anyLocalReferencePosition, createDetachedLocalReferencePosition, filterLocalReferencePositions, } from "./localReference.js";
13
13
  import { MergeTreeMaintenanceType, } from "./mergeTreeDeltaCallback.js";
14
- import { NodeAction, backwardExcursion, depthFirstNodeWalk, forwardExcursion, walkAllChildSegments, } from "./mergeTreeNodeWalk.js";
15
- import { CollaborationWindow, Marker, MaxNodesInBlock, MergeBlock, assertSegmentLeaf, assignChild, isSegmentLeaf, reservedMarkerIdKey, seqLTE, } from "./mergeTreeNodes.js";
14
+ import { LeafAction, NodeAction, backwardExcursion, depthFirstNodeWalk, forwardExcursion, walkAllChildSegments, } from "./mergeTreeNodeWalk.js";
15
+ import { CollaborationWindow, Marker, MaxNodesInBlock, MergeBlock, assertSegmentLeaf, assignChild, getMinSeqPerspective, getMinSeqStamp, isSegmentLeaf, reservedMarkerIdKey, } from "./mergeTreeNodes.js";
16
16
  import { createAnnotateRangeOp, createInsertSegmentOp, createRemoveRangeOp, } from "./opBuilder.js";
17
17
  import { MergeTreeDeltaType, ReferenceType, } from "./ops.js";
18
18
  import { PartialSequenceLengths } from "./partialLengths.js";
19
- import { LocalDefaultPerspective, LocalReconnectingPerspective, PriorPerspective, } from "./perspective.js";
19
+ import { PriorPerspective, LocalReconnectingPerspective, LocalDefaultPerspective, RemoteObliteratePerspective, } from "./perspective.js";
20
20
  import { createMap, extend, extendIfUndefined } from "./properties.js";
21
21
  import { DetachedReferencePosition, refGetTileLabels, refHasTileLabel, refTypeIncludesFlag, } from "./referencePositions.js";
22
22
  import { SegmentGroupCollection } from "./segmentGroupCollection.js";
23
- import { assertMoved, assertRemoved, isMergeNodeInfo, isMoved, isRemoved, overwriteInfo, removeRemovalInfo, toMoveInfo, toRemovalInfo, wasMovedOnInsert, } from "./segmentInfos.js";
24
- import { copyPropertiesAndManager, PropertiesManager, PropertiesRollback, } from "./segmentPropertiesManager.js";
23
+ import { assertRemoved, isMergeNodeInfo, isRemoved, overwriteInfo, removeRemovalInfo, toRemovalInfo, } from "./segmentInfos.js";
24
+ import { copyPropertiesAndManager, PropertiesManager, } from "./segmentPropertiesManager.js";
25
25
  import { Side } from "./sequencePlace.js";
26
26
  import { SortedSegmentSet } from "./sortedSegmentSet.js";
27
+ import * as opstampUtils from "./stamps.js";
27
28
  import { zamboniSegments } from "./zamboni.js";
28
- function isRemovedAndAcked(segment) {
29
+ export function isRemovedAndAcked(segment) {
29
30
  const removalInfo = toRemovalInfo(segment);
30
- return removalInfo !== undefined && removalInfo.removedSeq !== UnassignedSequenceNumber;
31
- }
32
- function isMovedAndAcked(segment) {
33
- const moveInfo = toMoveInfo(segment);
34
- return moveInfo !== undefined && moveInfo.movedSeq !== UnassignedSequenceNumber;
35
- }
36
- function isRemovedAndAckedOrMovedAndAcked(segment) {
37
- return isRemovedAndAcked(segment) || isMovedAndAcked(segment);
38
- }
39
- function isRemovedOrMoved(segment) {
40
- return isRemoved(segment) || isMoved(segment);
31
+ return removalInfo !== undefined && opstampUtils.isAcked(removalInfo.removes[0]);
41
32
  }
42
33
  function nodeTotalLength(mergeTree, node) {
43
34
  if (!node.isLeaf()) {
44
35
  return node.cachedLength;
45
36
  }
46
- return mergeTree.localNetLength(node);
37
+ return mergeTree.leafLength(node);
47
38
  }
48
39
  const LRUSegmentComparer = {
49
40
  min: { maxSeq: -2 },
50
41
  compare: (a, b) => a.maxSeq - b.maxSeq,
51
42
  };
52
- function ackSegment(segment, segmentGroup, opArgs) {
43
+ function ackSegment(segment, segmentGroup, opArgs, stamp) {
53
44
  const currentSegmentGroup = segment.segmentGroups?.dequeue();
54
45
  assert(currentSegmentGroup === segmentGroup, 0x043 /* "On ack, unexpected segmentGroup!" */);
55
46
  assert(opArgs.sequencedMessage !== undefined, 0xa6e /* must have sequencedMessage */);
56
47
  const { op, sequencedMessage: { sequenceNumber, minimumSequenceNumber }, } = opArgs;
48
+ let allowIncrementalPartialLengthsUpdate = true;
57
49
  switch (op.type) {
58
50
  case MergeTreeDeltaType.ANNOTATE: {
59
51
  assert(!!segment.propertyManager, 0x044 /* "On annotate ack, missing segment property manager!" */);
60
52
  segment.propertyManager.ack(sequenceNumber, minimumSequenceNumber, op);
61
- return true;
53
+ break;
62
54
  }
63
55
  case MergeTreeDeltaType.INSERT: {
64
- assert(segment.seq === UnassignedSequenceNumber, 0x045 /* "On insert, seq number already assigned!" */);
65
- segment.seq = sequenceNumber;
66
- segment.localSeq = undefined;
67
- return true;
68
- }
69
- case MergeTreeDeltaType.REMOVE: {
70
- assertRemoved(segment);
71
- segment.localRemovedSeq = undefined;
72
- if (segment.removedSeq === UnassignedSequenceNumber) {
73
- segment.removedSeq = sequenceNumber;
74
- return true;
75
- }
76
- return false;
56
+ assert(opstampUtils.isLocal(segment.insert), 0x045 /* "On insert, seq number already assigned!" */);
57
+ segment.insert = {
58
+ ...stamp,
59
+ type: "insert",
60
+ };
61
+ break;
77
62
  }
63
+ case MergeTreeDeltaType.REMOVE:
78
64
  case MergeTreeDeltaType.OBLITERATE:
79
65
  case MergeTreeDeltaType.OBLITERATE_SIDED: {
80
- assertMoved(segment);
81
- const obliterateInfo = segmentGroup.obliterateInfo;
82
- assert(obliterateInfo !== undefined, 0xa40 /* must have obliterate info */);
83
- segment.localMovedSeq = obliterateInfo.localSeq = undefined;
84
- const seqIdx = segment.movedSeqs.indexOf(UnassignedSequenceNumber);
85
- assert(seqIdx !== -1, 0x86f /* expected movedSeqs to contain unacked seq */);
86
- segment.movedSeqs[seqIdx] = sequenceNumber;
87
- if (segment.movedSeq === UnassignedSequenceNumber) {
88
- segment.movedSeq = sequenceNumber;
89
- return true;
66
+ assertRemoved(segment);
67
+ const latestRemove = segment.removes[segment.removes.length - 1];
68
+ assert(opstampUtils.isLocal(latestRemove), 0xb5d /* Expected last remove to be unacked */);
69
+ assert(segment.removes.length === 1 ||
70
+ opstampUtils.isAcked(segment.removes[segment.removes.length - 2]), 0xb5e /* Expected prior remove to be acked */);
71
+ allowIncrementalPartialLengthsUpdate = segment.removes.length === 1;
72
+ const removeStamp = {
73
+ ...stamp,
74
+ type: op.type === MergeTreeDeltaType.REMOVE ? "setRemove" : "sliceRemove",
75
+ };
76
+ segment.removes[segment.removes.length - 1] = removeStamp;
77
+ const { obliterateInfo } = segmentGroup;
78
+ const hasObliterateInfo = obliterateInfo !== undefined;
79
+ const isObliterate = op.type !== MergeTreeDeltaType.REMOVE;
80
+ assert(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
81
+ if (hasObliterateInfo) {
82
+ obliterateInfo.stamp = removeStamp;
90
83
  }
91
- return false;
84
+ break;
92
85
  }
93
86
  default: {
94
87
  throw new Error(`${op.type} is in unrecognized operation type`);
95
88
  }
96
89
  }
90
+ return allowIncrementalPartialLengthsUpdate;
97
91
  }
98
92
  export function errorIfOptionNotTrue(options, option) {
99
93
  if (options?.[option] !== true) {
@@ -124,9 +118,7 @@ export function findRootMergeBlock(segmentOrNode) {
124
118
  * SlideOnRemove references is removed.
125
119
  */
126
120
  function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWARD, cache, useNewSlidingBehavior = false) {
127
- if (!segment ||
128
- !isRemovedAndAckedOrMovedAndAcked(segment) ||
129
- segment.endpointType !== undefined) {
121
+ if (!segment || !isRemovedAndAcked(segment) || segment.endpointType !== undefined) {
130
122
  return [segment, undefined];
131
123
  }
132
124
  const cachedSegment = cache?.get(segment);
@@ -136,13 +128,12 @@ function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWAR
136
128
  const result = {};
137
129
  cache?.set(segment, result);
138
130
  const goFurtherToFindSlideToSegment = (seg) => {
139
- if (seg.seq !== UnassignedSequenceNumber && !isRemovedAndAckedOrMovedAndAcked(seg)) {
131
+ if (opstampUtils.isAcked(seg.insert) && !isRemovedAndAcked(seg)) {
140
132
  result.seg = seg;
141
133
  return false;
142
134
  }
143
135
  if (cache !== undefined &&
144
- (toRemovalInfo(seg)?.removedSeq === toRemovalInfo(segment)?.removedSeq ||
145
- toMoveInfo(seg)?.movedSeq === toMoveInfo(segment)?.movedSeq)) {
136
+ toRemovalInfo(seg)?.removes[0].seq === toRemovalInfo(segment)?.removes[0].seq) {
146
137
  cache.set(seg, result);
147
138
  }
148
139
  return true;
@@ -208,10 +199,10 @@ class Obliterates {
208
199
  constructor(mergeTree) {
209
200
  this.mergeTree = mergeTree;
210
201
  /**
211
- * Array containing the all move operations within the
202
+ * Array containing the all obliterate operations within the
212
203
  * collab window.
213
204
  *
214
- * The moves are stored in sequence order which accelerates clean up in setMinSeq
205
+ * The obliterates are stored in sequence order which accelerates clean up in setMinSeq
215
206
  *
216
207
  * See https://github.com/microsoft/FluidFramework/blob/main/packages/dds/merge-tree/docs/Obliterate.md#remote-perspective
217
208
  * for additional context
@@ -226,7 +217,7 @@ class Obliterates {
226
217
  }
227
218
  setMinSeq(minSeq) {
228
219
  // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
229
- while (!this.seqOrdered.empty && this.seqOrdered.first?.data.seq <= minSeq) {
220
+ while (!this.seqOrdered.empty && this.seqOrdered.first?.data.stamp.seq <= minSeq) {
230
221
  const ob = this.seqOrdered.shift();
231
222
  this.startOrdered.remove(ob.data.start);
232
223
  this.mergeTree.removeLocalReferencePosition(ob.data.start);
@@ -234,7 +225,7 @@ class Obliterates {
234
225
  }
235
226
  }
236
227
  addOrUpdate(obliterateInfo) {
237
- const { seq, start } = obliterateInfo;
228
+ const { stamp: { seq }, start, } = obliterateInfo;
238
229
  if (seq !== UnassignedSequenceNumber) {
239
230
  this.seqOrdered.push(obliterateInfo);
240
231
  }
@@ -266,12 +257,14 @@ class Obliterates {
266
257
  * @internal
267
258
  */
268
259
  export class MergeTree {
260
+ get localPerspective() {
261
+ return this.collabWindow.localPerspective;
262
+ }
269
263
  constructor(options) {
270
264
  this.options = options;
271
265
  this.collabWindow = new CollaborationWindow();
272
266
  this.pendingSegments = new DoublyLinkedList();
273
267
  this.segmentsToScour = new Heap(LRUSegmentComparer);
274
- this.localPerspective = new LocalDefaultPerspective(this.collabWindow.clientId);
275
268
  /**
276
269
  * Whether or not all blocks in the mergeTree currently have information about local partial lengths computed.
277
270
  * This information is only necessary on reconnect, and otherwise costly to bookkeep.
@@ -326,61 +319,20 @@ export class MergeTree {
326
319
  return block;
327
320
  }
328
321
  /**
329
- * Compute the net length of this segment from a local perspective.
330
- * @param segment - Segment whose length to find
331
- * @param localSeq - localSeq at which to find the length of this segment. If not provided,
332
- * default is to consider the local client's current perspective. Only local sequence
333
- * numbers corresponding to un-acked operations give valid results.
322
+ * Compute the net length of this segment leaf from some perspective.
323
+ * @returns - Undefined if the segment has been removed and its removal is common knowledge to all collaborators (and therefore
324
+ * may not even be present on clients that have loaded from a summary beyond this point). Otherwise, the length of the segment.
334
325
  */
335
- localNetLength(segment, refSeq, localSeq) {
326
+ leafLength(segment, perspective = this.localPerspective) {
336
327
  const removalInfo = toRemovalInfo(segment);
337
- const moveInfo = toMoveInfo(segment);
338
- if (localSeq === undefined) {
339
- if (removalInfo !== undefined || moveInfo !== undefined) {
340
- if ((!!removalInfo && !seqLTE(removalInfo.removedSeq, this.collabWindow.minSeq)) ||
341
- (!!moveInfo && !seqLTE(moveInfo.movedSeq, this.collabWindow.minSeq))) {
342
- return 0;
343
- }
344
- // this segment removed and outside the collab window which means it is zamboni eligible
345
- // this also means the segment could not exist, so we should not consider it
346
- // when making decisions about conflict resolutions
347
- return undefined;
348
- }
349
- else {
350
- return segment.cachedLength;
351
- }
352
- }
353
- assert(refSeq !== undefined, 0x398 /* localSeq provided for local length without refSeq */);
354
- assert(segment.seq !== undefined, 0x399 /* segment with no seq in mergeTree */);
355
- const { seq } = segment;
356
- const { removedSeq, localRemovedSeq } = removalInfo ?? {};
357
- const { movedSeq, localMovedSeq } = moveInfo ?? {};
358
- if (seq === UnassignedSequenceNumber) {
359
- assert(segment.localSeq !== undefined, 0x39a /* unacked segment with undefined localSeq */);
360
- // inserted locally, still un-acked
361
- if (segment.localSeq > localSeq ||
362
- (localRemovedSeq !== undefined && localRemovedSeq <= localSeq) ||
363
- (localMovedSeq !== undefined && localMovedSeq <= localSeq)) {
364
- return 0;
365
- }
366
- const { cachedLength } = segment;
367
- return cachedLength;
368
- }
369
- else {
370
- // inserted remotely
371
- if (seq > refSeq ||
372
- (removedSeq !== undefined &&
373
- removedSeq !== UnassignedSequenceNumber &&
374
- removedSeq <= refSeq) ||
375
- (movedSeq !== undefined &&
376
- movedSeq !== UnassignedSequenceNumber &&
377
- movedSeq <= refSeq) ||
378
- (localRemovedSeq !== undefined && localRemovedSeq <= localSeq) ||
379
- (localMovedSeq !== undefined && localMovedSeq <= localSeq)) {
380
- return 0;
381
- }
382
- return segment.cachedLength;
328
+ if (removalInfo &&
329
+ getMinSeqPerspective(this.collabWindow).hasOccurred(removalInfo.removes[0])) {
330
+ // this segment's removal has already moved outside the collab window which means it is zamboni eligible
331
+ // this also means the segment could be completely absent from other client's in-memory merge trees,
332
+ // so we should not consider it when making decisions about conflict resolutions
333
+ return undefined;
383
334
  }
335
+ return perspective.isSegmentPresent(segment) ? segment.cachedLength : 0;
384
336
  }
385
337
  unlinkMarker(marker) {
386
338
  const id = marker.getId();
@@ -438,6 +390,7 @@ export class MergeTree {
438
390
  this.collabWindow.minSeq = minSeq;
439
391
  this.collabWindow.collaborating = true;
440
392
  this.collabWindow.currentSeq = currentSeq;
393
+ this.collabWindow.localPerspective = new LocalDefaultPerspective(localClientId);
441
394
  this.nodeUpdateLengthNewStructure(this.root, true);
442
395
  }
443
396
  addToLRUSet(leaf, seq) {
@@ -450,8 +403,8 @@ export class MergeTree {
450
403
  this.segmentsToScour.add({ segment: leaf, maxSeq: seq });
451
404
  }
452
405
  }
453
- getLength(refSeq, clientId) {
454
- return this.blockLength(this.root, refSeq, clientId);
406
+ getLength(perspective) {
407
+ return this.nodeLength(this.root, perspective) ?? 0;
455
408
  }
456
409
  /**
457
410
  * Returns the current length of the MergeTree for the local client.
@@ -459,7 +412,7 @@ export class MergeTree {
459
412
  get length() {
460
413
  return this.root.cachedLength;
461
414
  }
462
- getPosition(node, refSeq, clientId, localSeq) {
415
+ getPosition(node, perspective) {
463
416
  if (node.isLeaf() && node.endpointType === "start") {
464
417
  return 0;
465
418
  }
@@ -473,15 +426,16 @@ export class MergeTree {
473
426
  if ((!!prevParent && child === prevParent) || child === node) {
474
427
  break;
475
428
  }
476
- totalOffset += this.nodeLength(child, refSeq, clientId, localSeq) ?? 0;
429
+ totalOffset += this.nodeLength(child, perspective) ?? 0;
477
430
  }
478
431
  prevParent = parent;
479
432
  parent = parent.parent;
480
433
  }
481
434
  return totalOffset;
482
435
  }
483
- getContainingSegment(pos, refSeq, clientId, localSeq) {
484
- assert(localSeq === undefined || clientId === this.collabWindow.clientId, 0x39b /* localSeq provided for non-local client */);
436
+ getContainingSegment(pos, perspective) {
437
+ assert(perspective.localSeq === undefined ||
438
+ perspective.clientId === this.collabWindow.clientId, 0x39b /* localSeq provided for non-local client */);
485
439
  let segment;
486
440
  let offset;
487
441
  const leaf = (leafSeg, _, start) => {
@@ -489,7 +443,7 @@ export class MergeTree {
489
443
  offset = start;
490
444
  return false;
491
445
  };
492
- this.nodeMap(refSeq, clientId, leaf, undefined, pos, pos + 1, localSeq);
446
+ this.nodeMap(perspective, leaf, undefined, pos, pos + 1);
493
447
  return { segment, offset };
494
448
  }
495
449
  /**
@@ -582,7 +536,7 @@ export class MergeTree {
582
536
  const forwardSegmentCache = new Map();
583
537
  const backwardSegmentCache = new Map();
584
538
  for (const segment of segments) {
585
- assert(isRemovedAndAckedOrMovedAndAcked(segment), 0x2f1 /* slideReferences from a segment which has not been removed and acked */);
539
+ assert(isRemovedAndAcked(segment), 0x2f1 /* slideReferences from a segment which has not been removed and acked */);
586
540
  if (segment.localRefs === undefined || segment.localRefs.empty) {
587
541
  continue;
588
542
  }
@@ -602,11 +556,6 @@ export class MergeTree {
602
556
  slideGroup(currentForwardSlideDestination, currentForwardSlideIsForward, currentForwardSlideGroup, forwardPred, currentForwardMaybeEndpoint);
603
557
  slideGroup(currentBackwardSlideDestination, currentBackwardSlideIsForward, currentBackwardSlideGroup, backwardPred, currentBackwardMaybeEndpoint);
604
558
  }
605
- blockLength(node, refSeq, clientId) {
606
- return this.collabWindow.collaborating && clientId !== this.collabWindow.clientId
607
- ? node.partialLengths.getPartialLength(refSeq, clientId)
608
- : (node.cachedLength ?? 0);
609
- }
610
559
  /**
611
560
  * Compute local partial length information
612
561
  *
@@ -624,62 +573,27 @@ export class MergeTree {
624
573
  this.root.partialLengths = PartialSequenceLengths.combine(this.root, rebaseCollabWindow, true, true);
625
574
  this.localPartialsComputed = true;
626
575
  }
627
- nodeLength(node, refSeq, clientId, localSeq) {
628
- if (!this.collabWindow.collaborating || this.collabWindow.clientId === clientId) {
629
- if (node.isLeaf()) {
630
- return this.localNetLength(node, refSeq, localSeq);
631
- }
632
- else if (localSeq === undefined ||
633
- // All changes are visible. Small note on why we allow refSeq >= this.collabWindow.currentSeq rather than just equality:
634
- // merge-tree eventing occurs before the collab window is updated to account for whatever op it is processing, and we want
635
- // to support resolving positions from within the event handler which account for that op. e.g. undo-redo relies on this
636
- // behavior with local references.
637
- (localSeq === this.collabWindow.localSeq && refSeq >= this.collabWindow.currentSeq)) {
638
- // Local client sees all segments, even when collaborating
639
- return node.cachedLength;
640
- }
641
- else {
642
- this.computeLocalPartials(refSeq);
643
- // Local client should see all segments except those after localSeq.
644
- const partialLen = node.partialLengths.getPartialLength(refSeq, clientId, localSeq);
645
- PartialSequenceLengths.options.verifyExpected?.(this, node, refSeq, clientId, localSeq);
646
- return partialLen;
647
- }
648
- }
649
- else {
650
- // Sequence number within window
651
- if (node.isLeaf()) {
652
- const segment = node;
653
- const removalInfo = toRemovalInfo(segment);
654
- const moveInfo = toMoveInfo(segment);
655
- if (removalInfo !== undefined) {
656
- if (seqLTE(removalInfo.removedSeq, this.collabWindow.minSeq)) {
657
- return undefined;
658
- }
659
- if (seqLTE(removalInfo.removedSeq, refSeq) ||
660
- removalInfo.removedClientIds.includes(clientId)) {
661
- return 0;
662
- }
663
- }
664
- if (moveInfo !== undefined) {
665
- if (seqLTE(moveInfo.movedSeq, this.collabWindow.minSeq)) {
666
- return undefined;
667
- }
668
- if (seqLTE(moveInfo.movedSeq, refSeq) ||
669
- moveInfo.movedClientIds.includes(clientId)) {
670
- return 0;
671
- }
672
- }
673
- return seqLTE(node.seq ?? 0, refSeq) || segment.clientId === clientId
674
- ? segment.cachedLength
675
- : 0;
676
- }
677
- else {
678
- const partialLen = node.partialLengths.getPartialLength(refSeq, clientId);
679
- PartialSequenceLengths.options.verifyExpected?.(this, node, refSeq, clientId);
680
- return partialLen;
681
- }
682
- }
576
+ nodeLength(node, perspective) {
577
+ if (node.isLeaf()) {
578
+ return this.leafLength(node, perspective);
579
+ }
580
+ const { refSeq, clientId, localSeq } = perspective;
581
+ const isLocalPerspective = !this.collabWindow.collaborating || this.collabWindow.clientId === clientId;
582
+ if (isLocalPerspective &&
583
+ (localSeq === undefined ||
584
+ (localSeq === this.collabWindow.localSeq && refSeq >= this.collabWindow.currentSeq))) {
585
+ // All changes are visible. Small note on why we allow refSeq >= this.collabWindow.currentSeq rather than just equality:
586
+ // merge-tree eventing occurs before the collab window is updated to account for whatever op it is processing, and we want
587
+ // to support resolving positions from within the event handler which account for that op. e.g. undo-redo relies on this
588
+ // behavior with local references.
589
+ return node.cachedLength;
590
+ }
591
+ if (localSeq !== undefined) {
592
+ this.computeLocalPartials(refSeq);
593
+ }
594
+ const length = node.partialLengths.getPartialLength(refSeq, clientId, localSeq);
595
+ PartialSequenceLengths.options.verifyExpected?.(this, node, refSeq, clientId, localSeq);
596
+ return length;
683
597
  }
684
598
  setMinSeq(minSeq) {
685
599
  assert(minSeq <= this.collabWindow.currentSeq, 0x04e /* "Trying to set minSeq above currentSeq of collab window!" */);
@@ -705,44 +619,61 @@ export class MergeTree {
705
619
  referencePositionToLocalPosition(refPos,
706
620
  // Note: this is not `this.collabWindow.currentSeq` because we want to support resolving local reference positions to positions
707
621
  // from within event handlers, and the collab window's sequence numbers are not updated in time in all of those cases.
708
- refSeq = Number.MAX_SAFE_INTEGER, clientId = this.collabWindow.clientId, localSeq = this.collabWindow.localSeq) {
622
+ refSeq = Number.MAX_SAFE_INTEGER, clientId = this.collabWindow.clientId, localSeq = undefined) {
709
623
  const perspective = clientId === this.collabWindow.clientId
710
624
  ? localSeq === undefined
711
625
  ? this.localPerspective
712
626
  : new LocalReconnectingPerspective(refSeq, clientId, localSeq)
713
627
  : new PriorPerspective(refSeq, clientId);
714
628
  const seg = refPos.getSegment();
715
- if (!isSegmentLeaf(seg)) {
629
+ if (seg === undefined || !isSegmentLeaf(seg)) {
716
630
  // We have no idea where this reference is, because it refers to a segment which is not in the tree.
717
631
  return DetachedReferencePosition;
718
632
  }
719
633
  if (refPos.isLeaf()) {
720
- return this.getPosition(seg, refSeq, clientId, localSeq);
634
+ return this.getPosition(seg, perspective);
721
635
  }
722
636
  if (refTypeIncludesFlag(refPos, ReferenceType.Transient) || seg.localRefs?.has(refPos)) {
723
637
  if (seg !== this.startOfTree &&
724
638
  seg !== this.endOfTree &&
725
639
  !perspective.isSegmentPresent(seg)) {
726
640
  const forward = refPos.slidingPreference === SlidingPreference.FORWARD;
727
- const moveInfo = toMoveInfo(seg);
728
641
  const removeInfo = toRemovalInfo(seg);
729
- const slideSeq = moveInfo !== undefined && moveInfo.movedSeq !== UnassignedSequenceNumber
730
- ? moveInfo.movedSeq
731
- : removeInfo !== undefined && removeInfo.removedSeq !== UnassignedSequenceNumber
732
- ? removeInfo.removedSeq
733
- : refSeq;
734
- const slideLocalSeq = moveInfo?.localMovedSeq ?? removeInfo?.localRemovedSeq;
735
- const slidePerspective = slideLocalSeq === undefined
642
+ const firstRemove = removeInfo?.removes[0];
643
+ const slideSeq = firstRemove !== undefined && opstampUtils.isAcked(firstRemove)
644
+ ? firstRemove.seq
645
+ : refSeq;
646
+ const slidePerspective = firstRemove?.localSeq === undefined
736
647
  ? new PriorPerspective(slideSeq, this.collabWindow.clientId)
737
- : new LocalReconnectingPerspective(slideSeq, this.collabWindow.clientId, slideLocalSeq);
738
- const slidSegment = slidePerspective.nextSegment(this, seg, forward);
739
- return (this.getPosition(slidSegment, refSeq, clientId, localSeq) +
648
+ : new LocalReconnectingPerspective(slideSeq, this.collabWindow.clientId, firstRemove.localSeq);
649
+ const slidSegment = this.nextSegment(slidePerspective, seg, forward);
650
+ return (this.getPosition(slidSegment, perspective) +
740
651
  (forward ? 0 : slidSegment.cachedLength === 0 ? 0 : slidSegment.cachedLength - 1));
741
652
  }
742
- return this.getPosition(seg, refSeq, clientId, localSeq) + refPos.getOffset();
653
+ return this.getPosition(seg, perspective) + refPos.getOffset();
743
654
  }
744
655
  return DetachedReferencePosition;
745
656
  }
657
+ /**
658
+ * Returns the immediately adjacent segment in the specified direction from this perspective.
659
+ * There may actually be multiple segments between the given segment and the returned segment,
660
+ * but they were either inserted after this perspective, or have been removed before this perspective.
661
+ *
662
+ * @param segment - The segment to start from.
663
+ * @param forward - The direction to search.
664
+ * @returns the next segment in the specified direction, or the start or end of the tree if there is no next segment.
665
+ */
666
+ nextSegment(perspective, segment, forward = true) {
667
+ let next;
668
+ const action = (seg) => {
669
+ if (perspective.isSegmentPresent(seg)) {
670
+ next = seg;
671
+ return LeafAction.Exit;
672
+ }
673
+ };
674
+ (forward ? forwardExcursion : backwardExcursion)(segment, action);
675
+ return next ?? (forward ? this.endOfTree : this.startOfTree);
676
+ }
746
677
  /**
747
678
  * Finds the nearest reference with ReferenceType.Tile to `startPos` in the direction dictated by `forwards`.
748
679
  * Uses depthFirstNodeWalk in addition to block-accelerated functionality. The search position will be included in
@@ -755,9 +686,9 @@ export class MergeTree {
755
686
  * @param markerLabel - Label of the marker to search for
756
687
  * @param forwards - Whether the string should be searched in the forward or backward direction
757
688
  */
758
- searchForMarker(startPos, clientId, markerLabel, forwards = true) {
689
+ searchForMarker(startPos, markerLabel, forwards = true) {
759
690
  let foundMarker;
760
- const { segment } = this.getContainingSegment(startPos, UniversalSequenceNumber, clientId);
691
+ const { segment } = this.getContainingSegment(startPos, this.localPerspective);
761
692
  if (!isSegmentLeaf(segment)) {
762
693
  return undefined;
763
694
  }
@@ -781,14 +712,12 @@ export class MergeTree {
781
712
  return foundMarker;
782
713
  }
783
714
  updateRoot(splitNode) {
784
- if (splitNode !== undefined) {
785
- const newRoot = this.makeBlock(2);
786
- assignChild(newRoot, this.root, 0, false);
787
- assignChild(newRoot, splitNode, 1, false);
788
- this.root = newRoot;
789
- this.nodeUpdateOrdinals(this.root);
790
- this.nodeUpdateLengthNewStructure(this.root);
791
- }
715
+ const newRoot = this.makeBlock(2);
716
+ assignChild(newRoot, this.root, 0, false);
717
+ assignChild(newRoot, splitNode, 1, false);
718
+ this.root = newRoot;
719
+ this.nodeUpdateOrdinals(this.root);
720
+ this.nodeUpdateLengthNewStructure(this.root);
792
721
  }
793
722
  /**
794
723
  * Assign sequence number to existing segment; update partial lengths to reflect the change
@@ -796,6 +725,10 @@ export class MergeTree {
796
725
  */
797
726
  ackPendingSegment(opArgs) {
798
727
  const seq = opArgs.sequencedMessage.sequenceNumber;
728
+ const stamp = {
729
+ seq,
730
+ clientId: this.collabWindow.clientId,
731
+ };
799
732
  const pendingSegmentGroup = this.pendingSegments.shift()?.data;
800
733
  const nodesToUpdate = [];
801
734
  let overwrite = false;
@@ -803,8 +736,8 @@ export class MergeTree {
803
736
  const deltaSegments = [];
804
737
  const overlappingRemoves = [];
805
738
  pendingSegmentGroup.segments.map((pendingSegment) => {
806
- const overlappingRemove = !ackSegment(pendingSegment, pendingSegmentGroup, opArgs);
807
- overwrite ||= overlappingRemove || toMoveInfo(pendingSegment) !== undefined;
739
+ const overlappingRemove = !ackSegment(pendingSegment, pendingSegmentGroup, opArgs, stamp);
740
+ overwrite ||= overlappingRemove;
808
741
  overlappingRemoves.push(overlappingRemove);
809
742
  if (MergeTree.options.zamboniSegments) {
810
743
  this.addToLRUSet(pendingSegment, seq);
@@ -817,7 +750,7 @@ export class MergeTree {
817
750
  });
818
751
  });
819
752
  if (pendingSegmentGroup.obliterateInfo !== undefined) {
820
- pendingSegmentGroup.obliterateInfo.seq = seq;
753
+ pendingSegmentGroup.obliterateInfo.stamp = { type: "sliceRemove", ...stamp };
821
754
  this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo);
822
755
  }
823
756
  // Perform slides after all segments have been acked, so that
@@ -831,9 +764,8 @@ export class MergeTree {
831
764
  deltaSegments,
832
765
  operation: MergeTreeMaintenanceType.ACKNOWLEDGED,
833
766
  }, opArgs);
834
- const clientId = this.collabWindow.clientId;
835
767
  for (const node of nodesToUpdate) {
836
- this.blockUpdatePathLengths(node, seq, clientId, overwrite);
768
+ this.blockUpdatePathLengths(node, stamp, overwrite);
837
769
  }
838
770
  }
839
771
  if (MergeTree.options.zamboniSegments) {
@@ -867,11 +799,7 @@ export class MergeTree {
867
799
  // TODO: error checking
868
800
  getMarkerFromId(id) {
869
801
  const marker = this.idToMarker.get(id);
870
- return marker === undefined ||
871
- isRemoved(marker) ||
872
- (isMoved(marker) && marker.moveDst === undefined)
873
- ? undefined
874
- : marker;
802
+ return marker === undefined || isRemoved(marker) ? undefined : marker;
875
803
  }
876
804
  /**
877
805
  * Given a position specified relative to a marker id, lookup the marker
@@ -880,14 +808,14 @@ export class MergeTree {
880
808
  * @param refseq - The reference sequence number at which to compute the position.
881
809
  * @param clientId - The client id with which to compute the position.
882
810
  */
883
- posFromRelativePos(relativePos, refseq = this.collabWindow.currentSeq, clientId = this.collabWindow.clientId) {
811
+ posFromRelativePos(relativePos, perspective) {
884
812
  let pos = -1;
885
813
  let marker;
886
814
  if (relativePos.id) {
887
815
  marker = this.getMarkerFromId(relativePos.id);
888
816
  }
889
817
  if (isSegmentLeaf(marker)) {
890
- pos = this.getPosition(marker, refseq, clientId);
818
+ pos = this.getPosition(marker, perspective);
891
819
  if (relativePos.before) {
892
820
  if (relativePos.offset !== undefined) {
893
821
  pos -= relativePos.offset;
@@ -902,14 +830,14 @@ export class MergeTree {
902
830
  }
903
831
  return pos;
904
832
  }
905
- insertSegments(pos, segments, refSeq, clientId, seq, opArgs) {
906
- this.ensureIntervalBoundary(pos, refSeq, clientId);
907
- const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
908
- this.blockInsert(pos, refSeq, clientId, seq, localSeq, segments);
833
+ insertSegments(pos, segments, perspective, stampArg, opArgs) {
834
+ const stamp = { ...stampArg, type: "insert" };
835
+ this.ensureIntervalBoundary(pos, perspective);
836
+ this.blockInsert(pos, perspective, stamp, segments);
909
837
  // opArgs == undefined => loading snapshot or test code
910
838
  if (opArgs !== undefined) {
911
839
  const deltaSegments = segments
912
- .filter((segment) => !toMoveInfo(segment))
840
+ .filter((segment) => !isRemoved(segment))
913
841
  .map((segment) => ({ segment }));
914
842
  if (deltaSegments.length > 0) {
915
843
  this.mergeTreeDeltaCallback?.(opArgs, {
@@ -920,7 +848,7 @@ export class MergeTree {
920
848
  }
921
849
  if (this.collabWindow.collaborating &&
922
850
  MergeTree.options.zamboniSegments &&
923
- seq !== UnassignedSequenceNumber) {
851
+ opstampUtils.isAcked(stamp)) {
924
852
  zamboniSegments(this);
925
853
  }
926
854
  }
@@ -942,19 +870,19 @@ export class MergeTree {
942
870
  if (remoteClientRefSeq < this.collabWindow.minSeq) {
943
871
  return undefined;
944
872
  }
945
- const segmentInfo = this.getContainingSegment(remoteClientPosition, remoteClientRefSeq, remoteClientId);
946
- const { currentSeq, clientId } = this.collabWindow;
873
+ const remotePerspective = new PriorPerspective(remoteClientRefSeq, remoteClientId);
874
+ const segmentInfo = this.getContainingSegment(remoteClientPosition, remotePerspective);
947
875
  if (isSegmentLeaf(segmentInfo?.segment)) {
948
- const segmentPosition = this.getPosition(segmentInfo.segment, currentSeq, clientId);
876
+ const segmentPosition = this.getPosition(segmentInfo.segment, this.localPerspective);
949
877
  return segmentPosition + segmentInfo.offset;
950
878
  }
951
879
  else {
952
- if (remoteClientPosition === this.getLength(remoteClientRefSeq, remoteClientId)) {
953
- return this.getLength(currentSeq, clientId);
880
+ if (remoteClientPosition === this.getLength(remotePerspective)) {
881
+ return this.getLength(this.localPerspective);
954
882
  }
955
883
  }
956
884
  }
957
- blockInsert(pos, refSeq, clientId, seq, localSeq, newSegments) {
885
+ blockInsert(pos, perspective, stamp, newSegments) {
958
886
  // Keeping this function within the scope of blockInsert for readability.
959
887
  // eslint-disable-next-line unicorn/consistent-function-scoping
960
888
  const continueFrom = (node) => {
@@ -969,16 +897,16 @@ export class MergeTree {
969
897
  const saveIfLocal = (locSegment) => {
970
898
  // Save segment so we can assign sequence number when acked by server
971
899
  if (this.collabWindow.collaborating) {
972
- if (locSegment.seq === UnassignedSequenceNumber &&
973
- clientId === this.collabWindow.clientId) {
974
- segmentGroup = this.addToPendingList(locSegment, segmentGroup, localSeq);
900
+ if (opstampUtils.isLocal(locSegment.insert) &&
901
+ stamp.clientId === this.collabWindow.clientId) {
902
+ segmentGroup = this.addToPendingList(locSegment, segmentGroup, stamp.localSeq);
975
903
  }
976
904
  // LocSegment.seq === 0 when coming from SharedSegmentSequence.loadBody()
977
905
  // In all other cases this has to be true (checked by addToLRUSet):
978
906
  // locSegment.seq > this.collabWindow.currentSeq
979
- else if (locSegment.seq > this.collabWindow.minSeq &&
980
- MergeTree.options.zamboniSegments) {
981
- this.addToLRUSet(locSegment, locSegment.seq);
907
+ else if (MergeTree.options.zamboniSegments &&
908
+ opstampUtils.greaterThan(locSegment.insert, getMinSeqStamp(this.collabWindow))) {
909
+ this.addToLRUSet(locSegment, locSegment.insert.seq);
982
910
  }
983
911
  }
984
912
  };
@@ -994,85 +922,71 @@ export class MergeTree {
994
922
  }
995
923
  return segmentChanges;
996
924
  };
997
- const insertInfo = {
998
- clientId,
999
- seq,
1000
- localSeq,
1001
- };
1002
925
  // TODO: build tree from segs and insert all at once
1003
926
  let insertPos = pos;
1004
927
  for (const newSegment of newSegments
1005
928
  .filter((s) => s.cachedLength > 0)
1006
- .map((s) => overwriteInfo(s, insertInfo))) {
929
+ .map((s) => overwriteInfo(s, { insert: stamp }))) {
1007
930
  if (Marker.is(newSegment)) {
1008
931
  const markerId = newSegment.getId();
1009
932
  if (markerId) {
1010
933
  this.idToMarker.set(markerId, newSegment);
1011
934
  }
1012
935
  }
1013
- const splitNode = this.insertingWalk(this.root, insertPos, refSeq, clientId, seq, {
936
+ this.insertingWalk(insertPos, perspective, stamp, {
1014
937
  leaf: onLeaf,
1015
938
  candidateSegment: newSegment,
1016
939
  continuePredicate: continueFrom,
1017
940
  });
1018
941
  if (!isSegmentLeaf(newSegment)) {
1019
942
  // Indicates an attempt to insert past the end of the merge-tree's content.
1020
- const errorConstructor = localSeq === undefined ? DataProcessingError : UsageError;
943
+ const errorConstructor = stamp.localSeq === undefined ? DataProcessingError : UsageError;
1021
944
  throw new errorConstructor("MergeTree insert failed", {
1022
945
  currentSeq: this.collabWindow.currentSeq,
1023
946
  minSeq: this.collabWindow.minSeq,
1024
- segSeq: insertInfo.seq,
947
+ segSeq: stamp.seq,
1025
948
  });
1026
949
  }
1027
- this.updateRoot(splitNode);
1028
950
  insertPos += newSegment.cachedLength;
1029
951
  if (!this.options?.mergeTreeEnableObliterate || this.obliterates.empty()) {
1030
952
  saveIfLocal(newSegment);
1031
953
  continue;
1032
954
  }
955
+ const overlappingAckedObliterates = [];
1033
956
  let oldest;
1034
- let normalizedOldestSeq = 0;
1035
957
  let newest;
1036
- let normalizedNewestSeq = 0;
1037
- const movedClientIds = [];
1038
- const movedSeqs = [];
1039
958
  let newestAcked;
1040
959
  let oldestUnacked;
960
+ const refSeqStamp = {
961
+ seq: perspective.refSeq,
962
+ clientId: stamp.clientId,
963
+ localSeq: stamp.localSeq,
964
+ };
1041
965
  for (const ob of this.obliterates.findOverlapping(newSegment)) {
1042
- // compute a normalized seq that takes into account local seqs
1043
- // but is still comparable to remote seqs to keep the checks below easy
1044
- // REMOTE SEQUENCE NUMBERS LOCAL SEQUENCE NUMBERS
1045
- // [0, 1, 2, 3, ..., 100, ..., 1000, ..., (MAX - MaxLocalSeq), L1, L2, L3, L4, ..., L100, ..., L1000, ...(MAX)]
1046
- const normalizedObSeq = ob.seq === UnassignedSequenceNumber
1047
- ? Number.MAX_SAFE_INTEGER - this.collabWindow.localSeq + ob.localSeq
1048
- : ob.seq;
1049
- if (normalizedObSeq > refSeq) {
966
+ if (opstampUtils.greaterThan(ob.stamp, refSeqStamp)) {
1050
967
  // Any obliterate from the same client that's inserting this segment cannot cause the segment to be marked as
1051
968
  // obliterated (since that client must have performed the obliterate before this insertion).
1052
969
  // We still need to consider such obliterates when determining the winning obliterate for the insertion point,
1053
970
  // see `obliteratePrecedingInsertion` docs.
1054
- if (clientId !== ob.clientId) {
1055
- if (oldest === undefined || normalizedOldestSeq > normalizedObSeq) {
1056
- normalizedOldestSeq = normalizedObSeq;
1057
- oldest = ob;
1058
- movedClientIds.unshift(ob.clientId);
1059
- movedSeqs.unshift(ob.seq);
971
+ if (stamp.clientId !== ob.stamp.clientId) {
972
+ if (opstampUtils.isAcked(ob.stamp)) {
973
+ overlappingAckedObliterates.push(ob.stamp);
1060
974
  }
1061
- else {
1062
- movedClientIds.push(ob.clientId);
1063
- movedSeqs.push(ob.seq);
975
+ if (oldest === undefined || opstampUtils.lessThan(ob.stamp, oldest.stamp)) {
976
+ oldest = ob;
1064
977
  }
1065
978
  }
1066
- if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
1067
- normalizedNewestSeq = normalizedObSeq;
979
+ if (newest === undefined || opstampUtils.greaterThan(ob.stamp, newest.stamp)) {
1068
980
  newest = ob;
1069
981
  }
1070
- if (ob.seq !== UnassignedSequenceNumber &&
1071
- (newestAcked === undefined || newestAcked.seq < ob.seq)) {
982
+ if (opstampUtils.isAcked(ob.stamp) &&
983
+ (newestAcked === undefined ||
984
+ opstampUtils.greaterThan(ob.stamp, newestAcked.stamp))) {
1072
985
  newestAcked = ob;
1073
986
  }
1074
- if (ob.seq === UnassignedSequenceNumber &&
1075
- (oldestUnacked === undefined || oldestUnacked.localSeq > ob.localSeq)) {
987
+ if (opstampUtils.isLocal(ob.stamp) &&
988
+ (oldestUnacked === undefined ||
989
+ opstampUtils.greaterThan(oldestUnacked.stamp, ob.stamp))) {
1076
990
  // There can be one local obliterate surrounding a segment if a client repeatedly obliterates
1077
991
  // a region (ex: in the text ABCDEFG, obliterate D, then obliterate CE, then BF). In this case,
1078
992
  // the first one that's applied will be the one that actually removes the segment.
@@ -1084,32 +998,20 @@ export class MergeTree {
1084
998
  // See doc comment on obliteratePrecedingInsertion for more details: if the newest obliterate was performed
1085
999
  // by the same client that's inserting this segment, we let them insert into this range and therefore don't
1086
1000
  // mark it obliterated.
1087
- if (oldest && newest?.clientId !== clientId) {
1088
- let moveInfo;
1089
- if (newestAcked === newest || newestAcked?.clientId !== clientId) {
1090
- moveInfo = {
1091
- movedClientIds,
1092
- movedSeq: oldest.seq,
1093
- movedSeqs,
1094
- localMovedSeq: oldestUnacked?.localSeq,
1095
- };
1096
- }
1097
- else {
1098
- assert(oldestUnacked !== undefined, 0xb55 /* Expected local obliterate to be defined if newestAcked is not equal to newest */);
1099
- // There's a pending local obliterate for this range, so it will be marked as obliterated by us. However,
1100
- // all other clients are under the impression that the most recent acked obliterate won the right to insert
1101
- // in this range.
1102
- moveInfo = {
1103
- movedClientIds: [oldestUnacked.clientId],
1104
- movedSeq: oldestUnacked.seq,
1105
- movedSeqs: [oldestUnacked.seq],
1106
- localMovedSeq: oldestUnacked.localSeq,
1107
- };
1001
+ if (oldest && newest?.stamp.clientId !== stamp.clientId) {
1002
+ const removeInfo = { removes: [] };
1003
+ if (newestAcked === newest || newestAcked?.stamp.clientId !== stamp.clientId) {
1004
+ removeInfo.removes = overlappingAckedObliterates;
1005
+ // Because we found these by looking at overlapping obliterates, they are not necessarily currently sorted by seq.
1006
+ // Address that now.
1007
+ removeInfo.removes.sort(opstampUtils.compare);
1108
1008
  }
1109
- overwriteInfo(newSegment, moveInfo);
1110
- if (moveInfo.localMovedSeq !== undefined) {
1111
- assert(oldestUnacked?.segmentGroup !== undefined, 0x86c /* expected segment group to exist */);
1112
- this.addToPendingList(newSegment, oldestUnacked?.segmentGroup);
1009
+ // Note that we don't need to worry about preserving any existing remove information since the segment is new.
1010
+ overwriteInfo(newSegment, removeInfo);
1011
+ if (oldestUnacked !== undefined) {
1012
+ removeInfo.removes.push(oldestUnacked.stamp);
1013
+ assert(oldestUnacked.segmentGroup !== undefined, 0x86c /* expected segment group to exist */);
1014
+ this.addToPendingList(newSegment, oldestUnacked.segmentGroup);
1113
1015
  }
1114
1016
  if (newSegment.parent) {
1115
1017
  // The incremental update codepath in theory can handle most cases where segments are obliterated upon insertion,
@@ -1117,90 +1019,95 @@ export class MergeTree {
1117
1019
  // lengths inside the inserting walk, we'd be at risk of double-counting the insertion in any case if we allow
1118
1020
  // incremental updates here.
1119
1021
  const newStructure = true;
1120
- this.blockUpdatePathLengths(newSegment.parent, moveInfo.movedSeq, clientId, newStructure);
1022
+ this.blockUpdatePathLengths(newSegment.parent, removeInfo.removes[0], newStructure);
1121
1023
  }
1122
1024
  }
1123
1025
  saveIfLocal(newSegment);
1124
1026
  }
1125
1027
  }
1126
- ensureIntervalBoundary(pos, refSeq, clientId) {
1127
- const splitNode = this.insertingWalk(this.root, pos, refSeq, clientId, TreeMaintenanceSequenceNumber, { leaf: this.splitLeafSegment });
1128
- this.updateRoot(splitNode);
1028
+ ensureIntervalBoundary(pos, perspective) {
1029
+ this.insertingWalk(pos, perspective, {
1030
+ seq: TreeMaintenanceSequenceNumber,
1031
+ clientId: perspective.clientId,
1032
+ }, { leaf: this.splitLeafSegment });
1129
1033
  }
1130
1034
  // Assume called only when pos == len
1131
- breakTie(pos, node, seq) {
1035
+ breakTie(pos, node, insertStamp) {
1132
1036
  if (node.isLeaf()) {
1133
1037
  if (pos !== 0) {
1134
1038
  return false;
1135
1039
  }
1136
- // normalize the seq numbers
1137
- // if the new seg is local (UnassignedSequenceNumber) give it the highest possible
1138
- // seq for comparison, as it will get a seq higher than any other seq once sequences
1139
- // if the current seg is local (UnassignedSequenceNumber) give it the second highest
1140
- // possible seq, as the highest is reserved for the previous.
1141
- const newSeq = seq === UnassignedSequenceNumber ? Number.MAX_SAFE_INTEGER : seq;
1142
- const segSeq = node.seq === UnassignedSequenceNumber ? Number.MAX_SAFE_INTEGER - 1 : (node.seq ?? 0);
1143
- return (newSeq > segSeq ||
1144
- (isMoved(node) && node.movedSeq !== UnassignedSequenceNumber && node.movedSeq > seq) ||
1040
+ return (opstampUtils.greaterThan(insertStamp, node.insert) ||
1145
1041
  (isRemoved(node) &&
1146
- node.removedSeq !== UnassignedSequenceNumber &&
1147
- node.removedSeq > seq));
1042
+ opstampUtils.isAcked(node.removes[0]) &&
1043
+ opstampUtils.greaterThan(node.removes[0], insertStamp)));
1148
1044
  }
1149
1045
  else {
1150
1046
  return true;
1151
1047
  }
1152
1048
  }
1153
- insertingWalk(block, pos, refSeq, clientId, seq, context, isLastChildBlock = true) {
1049
+ insertingWalk(pos, perspective, stamp, context) {
1050
+ const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context);
1051
+ if (remainder !== undefined) {
1052
+ this.updateRoot(remainder);
1053
+ }
1054
+ }
1055
+ insertRecursive(block, pos, perspective, stamp, context, isLastChildBlock = true) {
1154
1056
  let _pos = pos;
1155
1057
  const children = block.children;
1156
1058
  let childIndex;
1157
1059
  let child;
1158
1060
  let newNode;
1159
1061
  let fromSplit;
1062
+ let hadChanges = false;
1160
1063
  for (childIndex = 0; childIndex < block.childCount; childIndex++) {
1161
1064
  child = children[childIndex];
1162
1065
  // ensure we walk down the far edge of the tree, even if all sub-tree is eligible for zamboni
1163
1066
  const isLastNonLeafBlock = isLastChildBlock && !child.isLeaf() && childIndex === block.childCount - 1;
1164
- const len = this.nodeLength(child, refSeq, clientId) ?? (isLastChildBlock ? 0 : undefined);
1067
+ const len = this.nodeLength(child, perspective) ?? (isLastChildBlock ? 0 : undefined);
1165
1068
  if (len === undefined) {
1166
1069
  // if the seg len is undefined, the segment
1167
1070
  // will be removed, so should just be skipped for now
1168
1071
  continue;
1169
1072
  }
1170
1073
  assert(len >= 0, 0x4bc /* Length should not be negative */);
1171
- if (_pos < len || (_pos === len && this.breakTie(_pos, child, seq))) {
1074
+ if (_pos < len || (_pos === len && this.breakTie(_pos, child, stamp))) {
1172
1075
  // Found entry containing pos
1173
1076
  if (child.isLeaf()) {
1174
1077
  const segment = child;
1175
1078
  const segmentChanges = context.leaf(segment, _pos, context);
1176
1079
  if (segmentChanges.replaceCurrent) {
1080
+ hadChanges = true;
1177
1081
  assignChild(block, segmentChanges.replaceCurrent, childIndex, false);
1178
1082
  segmentChanges.replaceCurrent.ordinal = child.ordinal;
1179
1083
  }
1180
1084
  if (segmentChanges.next) {
1085
+ hadChanges = true;
1181
1086
  newNode = segmentChanges.next;
1182
1087
  childIndex++; // Insert after
1183
1088
  }
1184
1089
  else {
1185
- // No change
1186
- return undefined;
1090
+ return { remainder: undefined, hadChanges };
1187
1091
  }
1188
1092
  }
1189
1093
  else {
1190
1094
  const childBlock = child;
1191
1095
  // Internal node
1192
- const splitNode = this.insertingWalk(childBlock, _pos, refSeq, clientId, seq, context, isLastNonLeafBlock);
1193
- if (splitNode === undefined) {
1194
- this.blockUpdateLength(block, seq, clientId);
1195
- return undefined;
1096
+ const insertResult = this.insertRecursive(childBlock, _pos, perspective, stamp, context, isLastNonLeafBlock);
1097
+ hadChanges ||= insertResult.hadChanges;
1098
+ if (insertResult.remainder === undefined) {
1099
+ if (insertResult.hadChanges) {
1100
+ this.blockUpdateLength(block, stamp);
1101
+ }
1102
+ return insertResult;
1196
1103
  }
1197
- else if (splitNode === MergeTree.theUnfinishedNode) {
1104
+ else if (insertResult.remainder === MergeTree.theUnfinishedNode) {
1198
1105
  _pos -= len; // Act as if shifted segment
1199
1106
  continue;
1200
1107
  }
1201
1108
  else {
1202
- newNode = splitNode;
1203
- fromSplit = splitNode;
1109
+ newNode = insertResult.remainder;
1110
+ fromSplit = insertResult.remainder;
1204
1111
  childIndex++; // Insert after
1205
1112
  }
1206
1113
  }
@@ -1212,7 +1119,7 @@ export class MergeTree {
1212
1119
  }
1213
1120
  if (!newNode && _pos === 0) {
1214
1121
  if (context.continuePredicate?.(block)) {
1215
- return MergeTree.theUnfinishedNode;
1122
+ return { remainder: MergeTree.theUnfinishedNode, hadChanges };
1216
1123
  }
1217
1124
  else {
1218
1125
  const segmentChanges = context.leaf(undefined, _pos, context);
@@ -1221,6 +1128,7 @@ export class MergeTree {
1221
1128
  }
1222
1129
  }
1223
1130
  if (newNode) {
1131
+ hadChanges = true;
1224
1132
  for (let i = block.childCount; i > childIndex; i--) {
1225
1133
  block.children[i] = block.children[i - 1];
1226
1134
  block.children[i].index = i;
@@ -1232,19 +1140,19 @@ export class MergeTree {
1232
1140
  if (fromSplit) {
1233
1141
  this.nodeUpdateOrdinals(fromSplit);
1234
1142
  }
1235
- this.blockUpdateLength(block, seq, clientId);
1236
- return undefined;
1143
+ this.blockUpdateLength(block, stamp);
1144
+ return { remainder: undefined, hadChanges };
1237
1145
  }
1238
1146
  else {
1239
1147
  // Don't update ordinals because higher block will do it
1240
1148
  const newNodeFromSplit = this.split(block);
1241
- PartialSequenceLengths.options.verifyExpected?.(this, block, refSeq, clientId);
1242
- PartialSequenceLengths.options.verifyExpected?.(this, newNodeFromSplit, refSeq, clientId);
1243
- return newNodeFromSplit;
1149
+ PartialSequenceLengths.options.verifyExpected?.(this, block, perspective.refSeq, stamp.clientId);
1150
+ PartialSequenceLengths.options.verifyExpected?.(this, newNodeFromSplit, perspective.refSeq, stamp.clientId);
1151
+ return { remainder: newNodeFromSplit, hadChanges };
1244
1152
  }
1245
1153
  }
1246
1154
  else {
1247
- return undefined;
1155
+ return { remainder: undefined, hadChanges };
1248
1156
  }
1249
1157
  }
1250
1158
  split(node) {
@@ -1279,16 +1187,14 @@ export class MergeTree {
1279
1187
  * @param clientId - The id of the client making the annotate
1280
1188
  * @param seq - The sequence number of the annotate operation
1281
1189
  * @param opArgs - The op args for the annotate op. this is passed to the merge tree callback if there is one
1282
- * @param rollback - Whether this is for a local rollback and what kind
1283
1190
  */
1284
- annotateRange(start, end, propsOrAdjust, refSeq, clientId, seq, opArgs, rollback = PropertiesRollback.None) {
1191
+ annotateRange(start, end, propsOrAdjust, perspective, stamp, opArgs) {
1285
1192
  if (propsOrAdjust.adjust !== undefined) {
1286
1193
  errorIfOptionNotTrue(this.options, "mergeTreeEnableAnnotateAdjust");
1287
1194
  }
1288
- this.ensureIntervalBoundary(start, refSeq, clientId);
1289
- this.ensureIntervalBoundary(end, refSeq, clientId);
1195
+ this.ensureIntervalBoundary(start, perspective);
1196
+ this.ensureIntervalBoundary(end, perspective);
1290
1197
  const deltaSegments = [];
1291
- const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
1292
1198
  let segmentGroup;
1293
1199
  const opObj = propsOrAdjust.props ?? propsOrAdjust.adjust;
1294
1200
  const annotateSegment = (segment) => {
@@ -1296,23 +1202,23 @@ export class MergeTree {
1296
1202
  !(reservedMarkerIdKey in opObj) ||
1297
1203
  opObj.markerId === segment.properties?.markerId, 0x5ad /* Cannot change the markerId of an existing marker */);
1298
1204
  const propertyManager = (segment.propertyManager ??= new PropertiesManager());
1299
- const propertyDeltas = propertyManager.handleProperties(propsOrAdjust, segment, seq, this.collabWindow.minSeq, this.collabWindow.collaborating, rollback);
1300
- if (!isRemovedOrMoved(segment)) {
1205
+ const propertyDeltas = propertyManager.handleProperties(propsOrAdjust, segment, stamp.seq, this.collabWindow.minSeq, this.collabWindow.collaborating, opArgs?.rollback === true);
1206
+ if (!isRemoved(segment)) {
1301
1207
  deltaSegments.push({ segment, propertyDeltas });
1302
1208
  }
1303
1209
  if (this.collabWindow.collaborating) {
1304
- if (seq === UnassignedSequenceNumber) {
1305
- segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq, propertyDeltas);
1210
+ if (opstampUtils.isLocal(stamp)) {
1211
+ segmentGroup = this.addToPendingList(segment, segmentGroup, stamp.localSeq, propertyDeltas);
1306
1212
  }
1307
1213
  else {
1308
1214
  if (MergeTree.options.zamboniSegments) {
1309
- this.addToLRUSet(segment, seq);
1215
+ this.addToLRUSet(segment, stamp.seq);
1310
1216
  }
1311
1217
  }
1312
1218
  }
1313
1219
  return true;
1314
1220
  };
1315
- this.nodeMap(refSeq, clientId, annotateSegment, undefined, start, end);
1221
+ this.nodeMap(perspective, annotateSegment, undefined, start, end);
1316
1222
  // OpArgs == undefined => test code
1317
1223
  if (deltaSegments.length > 0) {
1318
1224
  this.mergeTreeDeltaCallback?.(opArgs, {
@@ -1321,34 +1227,28 @@ export class MergeTree {
1321
1227
  });
1322
1228
  }
1323
1229
  if (this.collabWindow.collaborating &&
1324
- seq !== UnassignedSequenceNumber &&
1230
+ opstampUtils.isAcked(stamp) &&
1325
1231
  MergeTree.options.zamboniSegments) {
1326
1232
  zamboniSegments(this);
1327
1233
  }
1328
1234
  }
1329
- obliterateRangeSided(start, end, refSeq, clientId, seq, opArgs) {
1235
+ obliterateRangeSided(start, end, perspective, stamp, opArgs) {
1330
1236
  const startPos = start.side === Side.Before ? start.pos : start.pos + 1;
1331
1237
  const endPos = end.side === Side.Before ? end.pos : end.pos + 1;
1332
- this.ensureIntervalBoundary(startPos, refSeq, clientId);
1333
- this.ensureIntervalBoundary(endPos, refSeq, clientId);
1238
+ this.ensureIntervalBoundary(startPos, perspective);
1239
+ this.ensureIntervalBoundary(endPos, perspective);
1334
1240
  let _overwrite = false;
1335
1241
  const localOverlapWithRefs = [];
1336
- const movedSegments = [];
1337
- const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
1338
- const perspective = seq === UnassignedSequenceNumber
1339
- ? this.localPerspective
1340
- : new PriorPerspective(refSeq, clientId);
1242
+ const removedSegments = [];
1341
1243
  const obliterate = {
1342
- clientId,
1343
- end: createDetachedLocalReferencePosition(undefined),
1344
- refSeq,
1345
- seq,
1346
1244
  start: createDetachedLocalReferencePosition(undefined),
1347
- localSeq,
1245
+ end: createDetachedLocalReferencePosition(undefined),
1246
+ refSeq: perspective.refSeq,
1247
+ stamp,
1348
1248
  segmentGroup: undefined,
1349
1249
  };
1350
- const { segment: startSeg } = this.getContainingSegment(start.pos, refSeq, clientId);
1351
- const { segment: endSeg } = this.getContainingSegment(end.pos, refSeq, clientId);
1250
+ const { segment: startSeg } = this.getContainingSegment(start.pos, perspective);
1251
+ const { segment: endSeg } = this.getContainingSegment(end.pos, perspective);
1352
1252
  assert(isSegmentLeaf(startSeg) && isSegmentLeaf(endSeg), 0xa3f /* segments cannot be undefined */);
1353
1253
  obliterate.start = this.createLocalReferencePosition(startSeg, start.side === Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0), ReferenceType.StayOnRemove, {
1354
1254
  obliterate,
@@ -1362,15 +1262,15 @@ export class MergeTree {
1362
1262
  // at which point they are added to the segment group.
1363
1263
  obliterate.segmentGroup = {
1364
1264
  segments: [],
1365
- localSeq,
1265
+ localSeq: stamp.localSeq,
1366
1266
  refSeq: this.collabWindow.currentSeq,
1367
1267
  obliterateInfo: obliterate,
1368
1268
  };
1369
- if (this.collabWindow.collaborating && clientId === this.collabWindow.clientId) {
1269
+ if (this.collabWindow.collaborating && stamp.clientId === this.collabWindow.clientId) {
1370
1270
  this.pendingSegments.push(obliterate.segmentGroup);
1371
1271
  }
1372
1272
  this.obliterates.addOrUpdate(obliterate);
1373
- const markMoved = (segment, pos) => {
1273
+ const markRemoved = (segment, pos) => {
1374
1274
  if ((start.side === Side.After && startPos === pos + segment.cachedLength) || // exclusive start segment
1375
1275
  (end.side === Side.Before && endPos === pos && perspective.isSegmentPresent(segment)) // exclusive end segment
1376
1276
  ) {
@@ -1378,173 +1278,136 @@ export class MergeTree {
1378
1278
  // These segments are outside of the obliteration range though, so return true to keep walking.
1379
1279
  return true;
1380
1280
  }
1381
- const existingMoveInfo = toMoveInfo(segment);
1281
+ const existingRemoveInfo = toRemovalInfo(segment);
1382
1282
  // The "last-to-obliterate-gets-to-insert" policy described by the doc comment on `obliteratePrecedingInsertion`
1383
1283
  // is mostly handled by logic at insertion time, but we need a small bit of handling here.
1384
1284
  // Specifically, we want to avoid marking a local-only segment as obliterated when we know one of our own local obliterates
1385
1285
  // will win against the obliterate we're processing, hence the early exit.
1386
- if (segment.seq === UnassignedSequenceNumber &&
1387
- segment.obliteratePrecedingInsertion?.seq === UnassignedSequenceNumber &&
1388
- seq !== UnassignedSequenceNumber) {
1286
+ if (opstampUtils.isLocal(segment.insert) &&
1287
+ segment.obliteratePrecedingInsertion?.stamp.seq === UnassignedSequenceNumber &&
1288
+ opstampUtils.isAcked(stamp)) {
1389
1289
  // We chose to not obliterate this segment because we are aware of an unacked local obliteration.
1390
1290
  // The local obliterate has not been sequenced yet, so it is still the newest obliterate we are aware of.
1391
1291
  // Other clients will also choose not to obliterate this segment because the most recent obliteration has the same clientId
1392
1292
  return true;
1393
1293
  }
1394
1294
  // Partial lengths incrementality is not supported for overlapping obliterate/removes.
1395
- _overwrite ||= existingMoveInfo !== undefined || toRemovalInfo(segment) !== undefined;
1396
- if (existingMoveInfo === undefined) {
1397
- const movedSeg = overwriteInfo(segment, {
1398
- movedClientIds: [clientId],
1399
- movedSeq: seq,
1400
- localMovedSeq: localSeq,
1401
- movedSeqs: [seq],
1295
+ _overwrite ||= existingRemoveInfo !== undefined;
1296
+ // - Record the segment as removed
1297
+ // - If this was the first thing to remove the segment from the local view, add it to removedSegments
1298
+ // - Otherwise, if it was the first thing to remove the segment from the acked view, add it to localOverlapWithRefs (so we can slide them)
1299
+ if (existingRemoveInfo === undefined) {
1300
+ const removed = overwriteInfo(segment, {
1301
+ removes: [stamp],
1402
1302
  });
1403
- const existingRemoval = toRemovalInfo(movedSeg);
1404
- if (existingRemoval === undefined) {
1405
- movedSegments.push(movedSeg);
1406
- }
1407
- else if (existingRemoval.removedSeq === UnassignedSequenceNumber &&
1408
- segment.localRefs?.empty === false) {
1409
- // We removed this locally already so we don't need to event it again, but it might have references
1410
- // that need sliding now that a move may have been acked.
1411
- localOverlapWithRefs.push(segment);
1412
- }
1303
+ removedSegments.push(removed);
1413
1304
  }
1414
1305
  else {
1415
- if (existingMoveInfo.movedSeq === UnassignedSequenceNumber) {
1416
- assert(!wasMovedOnInsert(segment), 0xab4 /* Local obliterate cannot have removed a segment as soon as it was inserted */);
1417
- assert(seq !== UnassignedSequenceNumber, 0xab5 /* Cannot obliterate the same segment locally twice */);
1418
- // we moved this locally, but someone else moved it first
1419
- // so put them at the head of the list
1420
- // The list isn't ordered, but we keep the first move at the head
1421
- // for partialLengths bookkeeping purposes
1422
- existingMoveInfo.movedClientIds.unshift(clientId);
1423
- existingMoveInfo.movedSeq = seq;
1424
- existingMoveInfo.movedSeqs.unshift(seq);
1425
- if (segment.localRefs?.empty === false) {
1426
- localOverlapWithRefs.push(segment);
1427
- }
1428
- }
1429
- else {
1430
- // Do not replace earlier sequence number for move
1431
- existingMoveInfo.movedClientIds.push(clientId);
1432
- existingMoveInfo.movedSeqs.push(seq);
1306
+ // The segment has already been removed, so we don't need to add it to removedSegments. However,
1307
+ // if it's only been removed locally, we still need to slide any references that may exist on it.
1308
+ if (!opstampUtils.hasAnyAckedOperation(existingRemoveInfo.removes) &&
1309
+ segment.localRefs?.empty === false) {
1310
+ localOverlapWithRefs.push(segment);
1433
1311
  }
1312
+ opstampUtils.spliceIntoList(existingRemoveInfo.removes, stamp);
1434
1313
  }
1435
- assertMoved(segment);
1436
- // Save segment so can assign moved sequence number when acked by server
1314
+ assertRemoved(segment);
1315
+ // Save segment so can assign sequence number when acked by server
1437
1316
  if (this.collabWindow.collaborating) {
1438
- if (segment.movedSeq === UnassignedSequenceNumber &&
1439
- clientId === this.collabWindow.clientId) {
1440
- obliterate.segmentGroup = this.addToPendingList(segment, obliterate.segmentGroup, localSeq);
1317
+ if (opstampUtils.isLocal(segment.removes[0]) &&
1318
+ stamp.clientId === this.collabWindow.clientId) {
1319
+ obliterate.segmentGroup = this.addToPendingList(segment, obliterate.segmentGroup, stamp.localSeq);
1441
1320
  }
1442
1321
  else {
1443
1322
  if (MergeTree.options.zamboniSegments) {
1444
- this.addToLRUSet(segment, seq);
1323
+ this.addToLRUSet(segment, stamp.seq);
1445
1324
  }
1446
1325
  }
1447
1326
  }
1448
1327
  return true;
1449
1328
  };
1450
- const afterMarkMoved = (node) => {
1329
+ const afterMarkRemoved = (node) => {
1451
1330
  if (_overwrite) {
1452
1331
  this.nodeUpdateLengthNewStructure(node);
1453
1332
  }
1454
1333
  else {
1455
- this.blockUpdateLength(node, seq, clientId);
1334
+ this.blockUpdateLength(node, stamp);
1456
1335
  }
1457
1336
  return true;
1458
1337
  };
1459
- this.nodeMap(refSeq, clientId, markMoved, afterMarkMoved, start.pos, end.pos + 1, // include the segment containing the end reference
1460
- undefined, seq === UnassignedSequenceNumber ? undefined : seq);
1338
+ this.nodeMap(perspective, markRemoved, afterMarkRemoved, start.pos, end.pos + 1, // include the segment containing the end reference
1339
+ // Use a visibilityPerspective which includes all segments (including local ones) which are in the obliteration range.
1340
+ // This ensures that concurrently inserted segments will also be marked obliterated.
1341
+ opstampUtils.isLocal(stamp)
1342
+ ? perspective
1343
+ : new RemoteObliteratePerspective(stamp.clientId));
1461
1344
  this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
1462
1345
  // opArgs == undefined => test code
1463
1346
  if (start.pos !== end.pos || start.side !== end.side) {
1464
1347
  this.mergeTreeDeltaCallback?.(opArgs, {
1465
1348
  operation: MergeTreeDeltaType.OBLITERATE,
1466
- deltaSegments: movedSegments.map((segment) => ({ segment })),
1349
+ deltaSegments: removedSegments.map((segment) => ({ segment })),
1467
1350
  });
1468
1351
  }
1469
1352
  // these events are newly removed
1470
1353
  // so we slide after eventing in case the consumer wants to make reference
1471
1354
  // changes at remove time, like add a ref to track undo redo.
1472
- if (!this.collabWindow.collaborating || clientId !== this.collabWindow.clientId) {
1473
- this.slideAckedRemovedSegmentReferences(movedSegments);
1355
+ if (!this.collabWindow.collaborating || stamp.clientId !== this.collabWindow.clientId) {
1356
+ this.slideAckedRemovedSegmentReferences(removedSegments);
1474
1357
  }
1475
1358
  if (this.collabWindow.collaborating &&
1476
- seq !== UnassignedSequenceNumber &&
1359
+ opstampUtils.isAcked(stamp) &&
1477
1360
  MergeTree.options.zamboniSegments) {
1478
1361
  zamboniSegments(this);
1479
1362
  }
1480
1363
  }
1481
- obliterateRange(start, end, refSeq, clientId, seq, opArgs) {
1364
+ obliterateRange(start, end, perspective, stampArg, opArgs) {
1482
1365
  errorIfOptionNotTrue(this.options, "mergeTreeEnableObliterate");
1366
+ const stamp = { ...stampArg, type: "sliceRemove" };
1483
1367
  if (this.options?.mergeTreeEnableSidedObliterate) {
1484
1368
  assert(typeof start === "object" && typeof end === "object", 0xa45 /* Start and end must be of type InteriorSequencePlace if mergeTreeEnableSidedObliterate is enabled. */);
1485
- this.obliterateRangeSided(start, end, refSeq, clientId, seq, opArgs);
1369
+ this.obliterateRangeSided(start, end, perspective, stamp, opArgs);
1486
1370
  }
1487
1371
  else {
1488
1372
  assert(typeof start === "number" && typeof end === "number", 0xa46 /* Start and end must be numbers if mergeTreeEnableSidedObliterate is not enabled. */);
1489
- this.obliterateRangeSided({ pos: start, side: Side.Before }, { pos: end - 1, side: Side.After }, refSeq, clientId, seq, opArgs);
1373
+ this.obliterateRangeSided({ pos: start, side: Side.Before }, { pos: end - 1, side: Side.After }, perspective, stamp, opArgs);
1490
1374
  }
1491
1375
  }
1492
- markRangeRemoved(start, end, refSeq, clientId, seq, opArgs) {
1376
+ markRangeRemoved(start, end, perspective, stampArg, opArgs) {
1493
1377
  let _overwrite = false;
1494
- this.ensureIntervalBoundary(start, refSeq, clientId);
1495
- this.ensureIntervalBoundary(end, refSeq, clientId);
1378
+ const stamp = { ...stampArg, type: "setRemove" };
1379
+ this.ensureIntervalBoundary(start, perspective);
1380
+ this.ensureIntervalBoundary(end, perspective);
1496
1381
  let segmentGroup;
1497
1382
  const removedSegments = [];
1498
1383
  const localOverlapWithRefs = [];
1499
- const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
1500
1384
  const markRemoved = (segment, pos, _start, _end) => {
1501
1385
  const existingRemovalInfo = toRemovalInfo(segment);
1502
1386
  // Partial lengths incrementality is not supported for overlapping obliterate/removes.
1503
- _overwrite ||= existingRemovalInfo !== undefined || toMoveInfo(segment) !== undefined;
1387
+ _overwrite ||= existingRemovalInfo !== undefined;
1504
1388
  if (existingRemovalInfo === undefined) {
1505
1389
  const removed = overwriteInfo(segment, {
1506
- removedClientIds: [clientId],
1507
- removedSeq: seq,
1508
- localRemovedSeq: localSeq,
1390
+ removes: [stamp],
1509
1391
  });
1510
- const existingMoveInfo = toMoveInfo(removed);
1511
- if (existingMoveInfo === undefined) {
1512
- removedSegments.push(removed);
1513
- }
1514
- else if (existingMoveInfo.movedSeq === UnassignedSequenceNumber &&
1515
- segment.localRefs?.empty === false) {
1516
- // We moved this locally already so we don't need to event it again, but it might have references
1517
- // that need sliding now that a remove may have been acked.
1518
- localOverlapWithRefs.push(segment);
1519
- }
1392
+ removedSegments.push(removed);
1520
1393
  }
1521
1394
  else {
1522
- if (existingRemovalInfo.removedSeq === UnassignedSequenceNumber) {
1523
- // we removed this locally, but someone else removed it first
1524
- // so put them at the head of the list
1525
- // The list isn't ordered, but we keep the first removal at the head
1526
- // for partialLengths bookkeeping purposes
1527
- existingRemovalInfo.removedClientIds.unshift(clientId);
1528
- existingRemovalInfo.removedSeq = seq;
1529
- if (segment.localRefs?.empty === false) {
1530
- localOverlapWithRefs.push(segment);
1531
- }
1532
- }
1533
- else {
1534
- // Do not replace earlier sequence number for remove
1535
- existingRemovalInfo.removedClientIds.push(clientId);
1395
+ if (!opstampUtils.hasAnyAckedOperation(existingRemovalInfo.removes) &&
1396
+ segment.localRefs?.empty === false) {
1397
+ localOverlapWithRefs.push(segment);
1536
1398
  }
1399
+ opstampUtils.spliceIntoList(existingRemovalInfo.removes, stamp);
1537
1400
  }
1538
1401
  assertRemoved(segment);
1539
1402
  // Save segment so we can assign removed sequence number when acked by server
1540
1403
  if (this.collabWindow.collaborating) {
1541
- if (segment.removedSeq === UnassignedSequenceNumber &&
1542
- clientId === this.collabWindow.clientId) {
1543
- segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
1404
+ if (opstampUtils.isLocal(segment.removes[0]) &&
1405
+ stamp.clientId === this.collabWindow.clientId) {
1406
+ segmentGroup = this.addToPendingList(segment, segmentGroup, stamp.localSeq);
1544
1407
  }
1545
1408
  else {
1546
1409
  if (MergeTree.options.zamboniSegments) {
1547
- this.addToLRUSet(segment, seq);
1410
+ this.addToLRUSet(segment, stamp.seq);
1548
1411
  }
1549
1412
  }
1550
1413
  }
@@ -1555,11 +1418,11 @@ export class MergeTree {
1555
1418
  this.nodeUpdateLengthNewStructure(node);
1556
1419
  }
1557
1420
  else {
1558
- this.blockUpdateLength(node, seq, clientId);
1421
+ this.blockUpdateLength(node, stamp);
1559
1422
  }
1560
1423
  return true;
1561
1424
  };
1562
- this.nodeMap(refSeq, clientId, markRemoved, afterMarkRemoved, start, end);
1425
+ this.nodeMap(perspective, markRemoved, afterMarkRemoved, start, end);
1563
1426
  // these segments are already viewed as being removed locally and are not event-ed
1564
1427
  // so can slide non-StayOnRemove refs immediately
1565
1428
  this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
@@ -1573,11 +1436,11 @@ export class MergeTree {
1573
1436
  // these events are newly removed
1574
1437
  // so we slide after eventing in case the consumer wants to make reference
1575
1438
  // changes at remove time, like add a ref to track undo redo.
1576
- if (!this.collabWindow.collaborating || clientId !== this.collabWindow.clientId) {
1439
+ if (!this.collabWindow.collaborating || stamp.clientId !== this.collabWindow.clientId) {
1577
1440
  this.slideAckedRemovedSegmentReferences(removedSegments);
1578
1441
  }
1579
1442
  if (this.collabWindow.collaborating &&
1580
- seq !== UnassignedSequenceNumber &&
1443
+ opstampUtils.isAcked(stamp) &&
1581
1444
  MergeTree.options.zamboniSegments) {
1582
1445
  zamboniSegments(this);
1583
1446
  }
@@ -1596,15 +1459,25 @@ export class MergeTree {
1596
1459
  pendingSegmentGroup.segments.forEach((segment) => {
1597
1460
  const segmentSegmentGroup = segment?.segmentGroups?.pop();
1598
1461
  assert(segmentSegmentGroup === pendingSegmentGroup, 0x3ee /* Unexpected segmentGroup in segment */);
1599
- assert(isRemoved(segment) && segment.removedClientIds[0] === this.collabWindow.clientId, 0x39d /* Rollback segment removedClientId does not match local client */);
1462
+ assert(isRemoved(segment) &&
1463
+ segment.removes[0].clientId === this.collabWindow.clientId &&
1464
+ segment.removes[0].type === "setRemove", 0x39d /* Rollback segment removedClientId does not match local client */);
1600
1465
  let updateNode = segment.parent;
1466
+ // This also removes obliterates, but that should be ok as we can only remove a segment once.
1467
+ // If we were able to remove it locally, that also means there are no remote removals (since rollback is synchronous).
1601
1468
  removeRemovalInfo(segment);
1602
1469
  for (updateNode; updateNode !== undefined; updateNode = updateNode.parent) {
1603
- this.blockUpdateLength(updateNode, UnassignedSequenceNumber, this.collabWindow.clientId);
1470
+ this.blockUpdateLength(updateNode, {
1471
+ seq: UnassignedSequenceNumber,
1472
+ clientId: this.collabWindow.clientId,
1473
+ });
1604
1474
  }
1605
1475
  // Note: optional chaining short-circuits:
1606
1476
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting
1607
- this.mergeTreeDeltaCallback?.({ op: createInsertSegmentOp(this.findRollbackPosition(segment), segment) }, {
1477
+ this.mergeTreeDeltaCallback?.({
1478
+ op: createInsertSegmentOp(this.findRollbackPosition(segment), segment),
1479
+ rollback: true,
1480
+ }, {
1608
1481
  operation: MergeTreeDeltaType.INSERT,
1609
1482
  deltaSegments: [{ segment }],
1610
1483
  });
@@ -1624,15 +1497,27 @@ export class MergeTree {
1624
1497
  assert(segmentSegmentGroup === pendingSegmentGroup, 0x3ef /* Unexpected segmentGroup in segment */);
1625
1498
  const start = this.findRollbackPosition(segment);
1626
1499
  if (op.type === MergeTreeDeltaType.INSERT) {
1627
- segment.seq = UniversalSequenceNumber;
1628
- segment.localSeq = undefined;
1500
+ segment.insert = {
1501
+ type: "insert",
1502
+ seq: UniversalSequenceNumber,
1503
+ clientId: this.collabWindow.clientId,
1504
+ };
1629
1505
  const removeOp = createRemoveRangeOp(start, start + segment.cachedLength);
1630
- this.markRangeRemoved(start, start + segment.cachedLength, UniversalSequenceNumber, this.collabWindow.clientId, UniversalSequenceNumber, { op: removeOp });
1506
+ const removeStamp = {
1507
+ type: "setRemove",
1508
+ seq: UniversalSequenceNumber,
1509
+ clientId: this.collabWindow.clientId,
1510
+ };
1511
+ this.markRangeRemoved(start, start + segment.cachedLength, this.localPerspective, removeStamp, { op: removeOp, rollback: true });
1631
1512
  } /* op.type === MergeTreeDeltaType.ANNOTATE */
1632
1513
  else {
1633
1514
  const props = pendingSegmentGroup.previousProps[i];
1634
1515
  const annotateOp = createAnnotateRangeOp(start, start + segment.cachedLength, props);
1635
- this.annotateRange(start, start + segment.cachedLength, { props }, UniversalSequenceNumber, this.collabWindow.clientId, UniversalSequenceNumber, { op: annotateOp }, PropertiesRollback.Rollback);
1516
+ const annotateStamp = {
1517
+ seq: UniversalSequenceNumber,
1518
+ clientId: this.collabWindow.clientId,
1519
+ };
1520
+ this.annotateRange(start, start + segment.cachedLength, { props }, this.localPerspective, annotateStamp, { op: annotateOp, rollback: true });
1636
1521
  i++;
1637
1522
  }
1638
1523
  }
@@ -1673,7 +1558,7 @@ export class MergeTree {
1673
1558
  createLocalReferencePosition(_segment, offset, refType, properties, slidingPreference, canSlideToEndpoint) {
1674
1559
  if (_segment !== "start" &&
1675
1560
  _segment !== "end" &&
1676
- isRemovedAndAckedOrMovedAndAcked(_segment) &&
1561
+ isRemovedAndAcked(_segment) &&
1677
1562
  !refTypeIncludesFlag(refType, ReferenceType.SlideOnRemove | ReferenceType.Transient | ReferenceType.StayOnRemove) &&
1678
1563
  _segment.endpointType === undefined) {
1679
1564
  throw new UsageError("Can only create SlideOnRemove or Transient local reference position on a removed or obliterated segment");
@@ -1717,15 +1602,15 @@ export class MergeTree {
1717
1602
  affectedSegments.insertAfter(lastLocalSegment, segmentToSlide.data);
1718
1603
  }
1719
1604
  else if (isRemoved(segmentToSlide.data)) {
1720
- assert(segmentToSlide.data.localRemovedSeq !== undefined, 0x54d /* Removed segment that hasnt had its removal acked should be locally removed */);
1605
+ assert(segmentToSlide.data.removes[0].seq !== undefined, 0x54d /* Removed segment that hasnt had its removal acked should be locally removed */);
1721
1606
  // Slide each locally removed item past all segments that have localSeq > lremoveItem.localSeq
1722
1607
  // but not past remotely removed segments;
1723
1608
  let cur = segmentToSlide;
1724
1609
  let scan = cur.next;
1725
1610
  while (scan !== undefined &&
1726
1611
  !isRemovedAndAcked(scan.data) &&
1727
- scan.data.localSeq !== undefined &&
1728
- scan.data.localSeq > segmentToSlide.data.localRemovedSeq) {
1612
+ scan.data.insert.localSeq !== undefined &&
1613
+ opstampUtils.greaterThan(scan.data.insert, segmentToSlide.data.removes[0])) {
1729
1614
  cur = scan;
1730
1615
  scan = scan.next;
1731
1616
  }
@@ -1810,11 +1695,11 @@ export class MergeTree {
1810
1695
  }
1811
1696
  };
1812
1697
  walkAllChildSegments(this.root, (seg) => {
1813
- if (isRemoved(seg) || seg.seq === UnassignedSequenceNumber) {
1698
+ if (isRemoved(seg) || opstampUtils.isLocal(seg.insert)) {
1814
1699
  if (isRemovedAndAcked(seg)) {
1815
1700
  rangeContainsRemoteRemovedSegs = true;
1816
1701
  }
1817
- if (seg.seq === UnassignedSequenceNumber) {
1702
+ if (opstampUtils.isLocal(seg.insert)) {
1818
1703
  rangeContainsLocalSegs = true;
1819
1704
  }
1820
1705
  currentRangeToNormalize.push(seg);
@@ -1842,7 +1727,7 @@ export class MergeTree {
1842
1727
  }
1843
1728
  if (node.isLeaf()) {
1844
1729
  const segment = node;
1845
- if ((this.localNetLength(segment) ?? 0) > 0 && Marker.is(segment)) {
1730
+ if ((this.leafLength(segment) ?? 0) > 0 && Marker.is(segment)) {
1846
1731
  const markerId = segment.getId();
1847
1732
  // Also in insertMarker but need for reload segs case
1848
1733
  // can add option for this only from reload segs
@@ -1872,33 +1757,33 @@ export class MergeTree {
1872
1757
  block.rightmostTiles = rightmostTiles;
1873
1758
  block.cachedLength = len;
1874
1759
  }
1875
- blockUpdatePathLengths(startBlock, seq, clientId, newStructure = false) {
1760
+ blockUpdatePathLengths(startBlock, stamp, newStructure = false) {
1876
1761
  let block = startBlock;
1877
1762
  while (block !== undefined) {
1878
1763
  if (newStructure) {
1879
1764
  this.nodeUpdateLengthNewStructure(block);
1880
1765
  }
1881
1766
  else {
1882
- this.blockUpdateLength(block, seq, clientId);
1767
+ this.blockUpdateLength(block, stamp);
1883
1768
  }
1884
1769
  block = block.parent;
1885
1770
  }
1886
1771
  }
1887
- blockUpdateLength(node, seq, clientId) {
1772
+ blockUpdateLength(node, stamp) {
1888
1773
  this.blockUpdate(node);
1889
1774
  this.localPartialsComputed = false;
1890
1775
  if (this.collabWindow.collaborating &&
1891
- seq !== UnassignedSequenceNumber &&
1892
- seq !== TreeMaintenanceSequenceNumber) {
1776
+ opstampUtils.isAcked(stamp) &&
1777
+ stamp.seq !== TreeMaintenanceSequenceNumber) {
1893
1778
  if (node.partialLengths !== undefined &&
1894
1779
  MergeTree.options.incrementalUpdate &&
1895
- clientId !== NonCollabClient) {
1896
- node.partialLengths.update(node, seq, clientId, this.collabWindow);
1780
+ stamp.clientId !== NonCollabClient) {
1781
+ node.partialLengths.update(node, stamp.seq, stamp.clientId, this.collabWindow);
1897
1782
  }
1898
1783
  else {
1899
1784
  node.partialLengths = PartialSequenceLengths.combine(node, this.collabWindow);
1900
1785
  }
1901
- PartialSequenceLengths.options.verifyExpected?.(this, node, seq, clientId);
1786
+ PartialSequenceLengths.options.verifyExpected?.(this, node, stamp.seq, stamp.clientId);
1902
1787
  }
1903
1788
  }
1904
1789
  /**
@@ -1908,16 +1793,16 @@ export class MergeTree {
1908
1793
  *
1909
1794
  * See `this.nodeMap` for additional documentation
1910
1795
  */
1911
- mapRange(handler, refSeq, clientId, accum, start, end, splitRange = false, visibilitySeq = refSeq) {
1796
+ mapRange(handler, perspective, accum, start, end, splitRange = false, visibilityPerspective = perspective) {
1912
1797
  if (splitRange) {
1913
1798
  if (start) {
1914
- this.ensureIntervalBoundary(start, refSeq, clientId);
1799
+ this.ensureIntervalBoundary(start, perspective);
1915
1800
  }
1916
1801
  if (end) {
1917
- this.ensureIntervalBoundary(end, refSeq, clientId);
1802
+ this.ensureIntervalBoundary(end, perspective);
1918
1803
  }
1919
1804
  }
1920
- this.nodeMap(refSeq, clientId, (seg, pos, _start, _end) => handler(seg, pos, refSeq, clientId, _start, _end, accum), undefined, start, end, undefined, visibilitySeq);
1805
+ this.nodeMap(perspective, (seg, pos, _start, _end) => handler(seg, pos, perspective.refSeq, perspective.clientId, _start, _end, accum), undefined, start, end, visibilityPerspective);
1921
1806
  }
1922
1807
  /**
1923
1808
  * Map over all visible segments in a given range
@@ -1942,8 +1827,8 @@ export class MergeTree {
1942
1827
  * but it will not count as a segment within the range. That is, it will be
1943
1828
  * ignored for the purposes of tracking when traversal should end.
1944
1829
  */
1945
- nodeMap(refSeq, clientId, leaf, post, start = 0, end, localSeq, visibilitySeq = refSeq) {
1946
- const endPos = end ?? this.nodeLength(this.root, refSeq, clientId, localSeq) ?? 0;
1830
+ nodeMap(perspective, leaf, post, start = 0, end, visibilityPerspective = perspective) {
1831
+ const endPos = end ?? this.nodeLength(this.root, perspective) ?? 0;
1947
1832
  if (endPos === start) {
1948
1833
  return;
1949
1834
  }
@@ -1952,12 +1837,15 @@ export class MergeTree {
1952
1837
  if (endPos <= pos) {
1953
1838
  return NodeAction.Exit;
1954
1839
  }
1955
- const len = this.nodeLength(node, visibilitySeq, clientId, localSeq);
1956
- const lenAtRefSeq = (visibilitySeq === refSeq
1957
- ? len
1958
- : this.nodeLength(node, refSeq, clientId, localSeq)) ?? 0;
1959
- const isUnackedAndInObliterate = visibilitySeq !== refSeq &&
1960
- (!node.isLeaf() || node.seq === UnassignedSequenceNumber);
1840
+ const len = this.nodeLength(node, visibilityPerspective);
1841
+ const lenAtRefSeq = (visibilityPerspective === perspective ? len : this.nodeLength(node, perspective)) ??
1842
+ 0;
1843
+ // NOTE: This code ensures that obliterates have a chance to mark segments which have been inserted locally
1844
+ // as also having been obliterated on the local client. With the introduction of RemoteObliteratePerspective,
1845
+ // it's feasible we could remove it if the `nodeLength` calculation also respects that perspective for blocks
1846
+ // and not just leaves.
1847
+ const isUnackedAndInObliterate = visibilityPerspective !== perspective &&
1848
+ (!node.isLeaf() || opstampUtils.isLocal(node.insert));
1961
1849
  if ((len === undefined && lenAtRefSeq === 0) ||
1962
1850
  (len === 0 && !isUnackedAndInObliterate && lenAtRefSeq === 0)) {
1963
1851
  return NodeAction.Skip;
@@ -1982,5 +1870,10 @@ MergeTree.options = {
1982
1870
  insertAfterRemovedSegs: true,
1983
1871
  zamboniSegments: true,
1984
1872
  };
1873
+ /**
1874
+ * A sentinel value that indicates an inserting walk should continue to the next block sibling.
1875
+ * This can occur for example when tie-break forces insertion of a segment past an entire block (and
1876
+ * the inserting walk first recurses into the block before realizing that).
1877
+ */
1985
1878
  MergeTree.theUnfinishedNode = { childCount: -1 };
1986
1879
  //# sourceMappingURL=mergeTree.js.map