@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
@@ -5,22 +5,22 @@
5
5
 
6
6
  import { assert } from "@fluidframework/core-utils/internal";
7
7
 
8
- import { UnassignedSequenceNumber } from "./constants.js";
9
8
  import { MergeTree } from "./mergeTree.js";
10
9
  import {
11
10
  CollaborationWindow,
11
+ getMinSeqStamp,
12
12
  IMergeNode,
13
13
  ISegmentPrivate,
14
- seqLTE,
15
14
  type MergeBlock,
16
15
  } from "./mergeTreeNodes.js";
17
16
  import {
18
- toRemovalInfo,
19
- toMoveInfo,
20
- assertInserted,
21
- wasMovedOnInsert,
22
- } from "./segmentInfos.js";
17
+ LocalDefaultPerspective,
18
+ LocalReconnectingPerspective,
19
+ PriorPerspective,
20
+ } from "./perspective.js";
21
+ import { toRemovalInfo, assertInserted, wasRemovedOnInsert } from "./segmentInfos.js";
23
22
  import { SortedSet } from "./sortedSet.js";
23
+ import * as opstampUtils from "./stamps.js";
24
24
 
25
25
  class PartialSequenceLengthsSet extends SortedSet<PartialSequenceLength> {
26
26
  protected compare(a: PartialSequenceLength, b: PartialSequenceLength): number {
@@ -477,8 +477,8 @@ export class PartialSequenceLengths {
477
477
  if (child.isLeaf()) {
478
478
  // Leaf segment
479
479
  const segment = child;
480
- if (wasMovedOnInsert(segment)) {
481
- PartialSequenceLengths.accountForMoveOnInsert(
480
+ if (wasRemovedOnInsert(segment)) {
481
+ PartialSequenceLengths.accountForRemoveOnInsert(
482
482
  combinedPartialLengths,
483
483
  segment,
484
484
  collabWindow,
@@ -504,30 +504,32 @@ export class PartialSequenceLengths {
504
504
  }
505
505
 
506
506
  /**
507
- * Assuming this segment was moved on insertion, inserts length information about that operation
507
+ * Assuming this segment was removed on insertion, inserts length information about that operation
508
508
  * into the appropriate per-client adjustments (the overall view needs no such adjustment since
509
509
  * from an observing client's perspective, the segment never exists).
510
510
  */
511
- private static accountForMoveOnInsert(
511
+ private static accountForRemoveOnInsert(
512
512
  combinedPartialLengths: PartialSequenceLengths,
513
513
  segment: ISegmentPrivate,
514
514
  collabWindow: CollaborationWindow,
515
515
  ): void {
516
516
  assertInserted(segment);
517
- const moveInfo = toMoveInfo(segment);
517
+ const removeInfo = toRemovalInfo(segment);
518
518
  assert(
519
- moveInfo !== undefined && wasMovedOnInsert(segment),
520
- 0xab7 /* Segment was not moved on insert */,
519
+ removeInfo !== undefined && wasRemovedOnInsert(segment),
520
+ 0xab7 /* Segment was not removed on insert */,
521
521
  );
522
- if (moveInfo.movedSeq <= collabWindow.minSeq) {
522
+ const firstRemove = removeInfo?.removes[0];
523
+ if (opstampUtils.lte(firstRemove, getMinSeqStamp(collabWindow))) {
523
524
  // This segment was obliterated as soon as it was inserted, and everyone was aware of the obliterate.
524
525
  // Thus every single client treats this segment as length 0 from every perspective, and no adjustments
525
526
  // are necessary.
526
527
  return;
527
528
  }
528
529
 
529
- const isLocal = segment.seq === UnassignedSequenceNumber;
530
- const clientId = segment.clientId;
530
+ const { insert, cachedLength } = segment;
531
+ const isLocal = opstampUtils.isLocal(insert);
532
+ const { clientId } = insert;
531
533
 
532
534
  const partials = isLocal
533
535
  ? combinedPartialLengths.unsequencedRecords?.partialLengths
@@ -539,40 +541,58 @@ export class PartialSequenceLengths {
539
541
 
540
542
  if (isLocal) {
541
543
  // Implication -> this is a local segment which will be obliterated as soon as it is acked.
542
- // For refSeqs preceding that movedSeq and localSeqs following the localSeq, it will be visible.
544
+ // For refSeqs preceding that removedSeq and localSeqs following the localSeq, it will be visible.
543
545
  // For the rest, it will not be visible.
544
546
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
545
- const localSeq = segment.localSeq!;
547
+ const localSeq = insert.localSeq!;
546
548
  partials.addOrUpdate({
547
549
  seq: localSeq,
548
550
  len: 0,
549
- seglen: segment.cachedLength,
551
+ seglen: cachedLength,
550
552
  clientId,
551
553
  });
552
554
 
553
555
  combinedPartialLengths.addLocalAdjustment({
554
- refSeq: moveInfo.movedSeq,
556
+ refSeq: firstRemove.seq,
555
557
  localSeq,
556
- seglen: -segment.cachedLength,
558
+ seglen: -cachedLength,
557
559
  });
560
+
561
+ const lastRemove = removeInfo.removes[removeInfo.removes.length - 1];
562
+ if (opstampUtils.isLocal(lastRemove)) {
563
+ // In addition to a remote sliceRemove causing this segment to be removed as soon as its insertion is acked,
564
+ // the local client has also removed it before its insertion was acked.
565
+ // It will therefore not be visible for a local reconnecting perspective beyond the removed localSeq.
566
+ partials.addOrUpdate({
567
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
568
+ seq: lastRemove.localSeq!,
569
+ len: 0,
570
+ seglen: -cachedLength,
571
+ clientId,
572
+ });
573
+
574
+ combinedPartialLengths.addLocalAdjustment({
575
+ refSeq: firstRemove.seq,
576
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
577
+ localSeq: lastRemove.localSeq!,
578
+ seglen: cachedLength,
579
+ });
580
+ }
558
581
  } else {
559
582
  // Segment was obliterated on insert. Generally this means it should be visible only to the
560
583
  // inserting client (in which case we add an adjustment to only that client's perspective),
561
584
  // but if that client has also removed it, we don't need to add anything.
562
- const removeInfo = toRemovalInfo(segment);
563
-
564
585
  const wasRemovedByInsertingClient =
565
- removeInfo !== undefined && removeInfo.removedClientIds.includes(clientId);
566
- const wasMovedByInsertingClient =
567
- moveInfo !== undefined && moveInfo.movedClientIds.includes(clientId);
586
+ removeInfo !== undefined &&
587
+ removeInfo.removes.some((remove) => remove.clientId === clientId);
568
588
 
569
- if (!wasRemovedByInsertingClient && !wasMovedByInsertingClient) {
570
- const moveSeq = moveInfo?.movedSeq;
589
+ if (!wasRemovedByInsertingClient) {
590
+ const removeSeq = firstRemove?.seq;
571
591
  assert(
572
- moveSeq !== undefined,
573
- 0xab8 /* ObliterateOnInsertion implies moveSeq is defined */,
592
+ removeSeq !== undefined,
593
+ 0xab8 /* ObliterateOnInsertion implies removeSeq is defined */,
574
594
  );
575
- combinedPartialLengths.addClientAdjustment(clientId, moveSeq, segment.cachedLength);
595
+ combinedPartialLengths.addClientAdjustment(clientId, removeSeq, cachedLength);
576
596
  }
577
597
  }
578
598
  }
@@ -587,16 +607,17 @@ export class PartialSequenceLengths {
587
607
  collabWindow: CollaborationWindow,
588
608
  ): void {
589
609
  assertInserted(segment);
590
- if (segment.seq !== undefined && seqLTE(segment.seq, collabWindow.minSeq)) {
610
+ if (opstampUtils.lte(segment.insert, getMinSeqStamp(collabWindow))) {
591
611
  combinedPartialLengths.minLength += segment.cachedLength;
592
612
  return;
593
613
  }
594
614
 
595
- const isLocal = segment.seq === UnassignedSequenceNumber;
615
+ const { insert, cachedLength: segmentLen } = segment;
616
+
617
+ const isLocal = opstampUtils.isLocal(insert);
596
618
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
597
- const seqOrLocalSeq = isLocal ? segment.localSeq! : segment.seq;
598
- const segmentLen = segment.cachedLength;
599
- const clientId = segment.clientId;
619
+ const seqOrLocalSeq = isLocal ? insert.localSeq! : insert.seq;
620
+ const clientId = insert.clientId;
600
621
 
601
622
  const partials = isLocal
602
623
  ? combinedPartialLengths.unsequencedRecords?.partialLengths
@@ -636,76 +657,35 @@ export class PartialSequenceLengths {
636
657
  assertInserted(segment);
637
658
 
638
659
  const removalInfo = toRemovalInfo(segment);
639
- const moveInfo = toMoveInfo(segment);
640
- if (!removalInfo && !moveInfo) {
660
+ if (!removalInfo) {
641
661
  return;
642
662
  }
643
663
 
644
- if (
645
- (removalInfo?.removedSeq !== undefined &&
646
- seqLTE(removalInfo.removedSeq, collabWindow.minSeq)) ||
647
- (moveInfo?.movedSeq !== undefined && seqLTE(moveInfo.movedSeq, collabWindow.minSeq))
648
- ) {
664
+ const firstRemove = removalInfo?.removes[0];
665
+ const minSeqStamp = getMinSeqStamp(collabWindow);
666
+ if (firstRemove !== undefined && opstampUtils.lte(firstRemove, minSeqStamp)) {
649
667
  combinedPartialLengths.minLength -= segment.cachedLength;
650
668
  return;
651
669
  }
652
670
 
653
- const removalIsLocal =
654
- !!removalInfo && removalInfo.removedSeq === UnassignedSequenceNumber;
655
- const moveIsLocal = !!moveInfo && moveInfo.movedSeq === UnassignedSequenceNumber;
656
- const isLocalInsertion = segment.seq === UnassignedSequenceNumber;
657
- const isOnlyLocalRemoval = removalIsLocal && (!moveInfo || moveIsLocal);
658
- const isOnlyLocalMove = moveIsLocal && (!removalInfo || removalIsLocal);
659
- const isLocal = isLocalInsertion || isOnlyLocalRemoval || isOnlyLocalMove;
660
-
661
- if (
662
- segment.seq === UnassignedSequenceNumber &&
663
- !(removalIsLocal && (!moveInfo || moveIsLocal)) &&
664
- !(moveIsLocal && (!removalInfo || removalIsLocal))
665
- ) {
666
- throw new Error("Should have handled this codepath in wasMovedOnInsertion");
671
+ const removalIsLocal = !!firstRemove && opstampUtils.isLocal(firstRemove);
672
+ const isLocalInsertion = opstampUtils.isLocal(segment.insert);
673
+ const isOnlyLocalRemoval = removalIsLocal;
674
+ const isLocal = isLocalInsertion || isOnlyLocalRemoval;
675
+
676
+ if (isLocalInsertion && !removalIsLocal) {
677
+ throw new Error("Should have handled this codepath in wasRemovedOnInsertion");
667
678
  }
668
679
 
669
680
  const lenDelta = -segment.cachedLength;
670
- let clientId: number;
671
- let seqOrLocalSeq: number;
681
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
682
+ const seqOrLocalSeq = removalIsLocal ? firstRemove.localSeq! : firstRemove.seq;
683
+ const clientId = firstRemove.clientId;
672
684
 
673
- // it's not possible to have an overlapping obliterate and remove that are both local
674
- assert(
675
- (!moveIsLocal && !removalIsLocal) || moveIsLocal !== removalIsLocal,
676
- 0x870 /* overlapping local obliterate and remove */,
685
+ const clientsWithRemoveOrObliterate = new Set<number>(
686
+ removalInfo?.removes.map((stamp) => stamp.clientId),
677
687
  );
678
688
 
679
- const clientsWithRemoveOrObliterate = new Set<number>([
680
- ...(removalInfo?.removedClientIds ?? []),
681
- ...(moveInfo?.movedClientIds ?? []),
682
- ]);
683
-
684
- const removeHappenedFirst =
685
- removalInfo &&
686
- (!moveInfo ||
687
- moveIsLocal ||
688
- (!removalIsLocal && moveInfo.movedSeq > removalInfo.removedSeq));
689
-
690
- if (removeHappenedFirst) {
691
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
692
- seqOrLocalSeq = removalIsLocal ? removalInfo.localRemovedSeq! : removalInfo.removedSeq;
693
- // The client who performed the remove is always stored
694
- // in the first position of removalInfo.
695
- clientId = removalInfo.removedClientIds[0];
696
- } else {
697
- assert(
698
- moveInfo !== undefined,
699
- 0xab9 /* Expected move to exist if remove either did not exist or didn't happen first */,
700
- );
701
- // The client who performed the move is always stored
702
- // in the first position of moveInfo.
703
- clientId = moveInfo.movedClientIds[0];
704
-
705
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
706
- seqOrLocalSeq = moveIsLocal ? moveInfo.localMovedSeq! : moveInfo.movedSeq;
707
- }
708
-
709
689
  const partials = isLocal
710
690
  ? combinedPartialLengths.unsequencedRecords?.partialLengths
711
691
  : combinedPartialLengths.partialLengths;
@@ -715,15 +695,31 @@ export class PartialSequenceLengths {
715
695
  }
716
696
 
717
697
  if (isLocal) {
718
- // The segment is either inserted only locally or removed/moved only locally.
698
+ // The segment is either inserted only locally or removed only locally.
719
699
  // We already accounted for the insertion in the accountForInsertion codepath.
720
- // Only thing left to do is account for the removal.
721
- partials.addOrUpdate({
722
- seq: seqOrLocalSeq,
723
- clientId,
724
- len: 0,
725
- seglen: lenDelta,
726
- });
700
+ // Only thing left to do is account for the removal in local partial lengths.
701
+ // One thing to be careful about is that the removal should only apply to perspectives that saw
702
+ // the segment's insertion in the first place.
703
+ // When the segment's insertion was common knowledge (at or below minSeq) or also by the local client,
704
+ // all possible perspectives will have seen it.
705
+ if (
706
+ opstampUtils.lte(segment.insert, minSeqStamp) ||
707
+ collabWindow.clientId === segment.insert.clientId
708
+ ) {
709
+ partials.addOrUpdate({
710
+ seq: seqOrLocalSeq,
711
+ clientId,
712
+ len: 0,
713
+ seglen: lenDelta,
714
+ });
715
+ } else {
716
+ // ... otherwise, it's only visible to reconnecting perspectives above the seq of the insert.
717
+ combinedPartialLengths.addLocalAdjustment({
718
+ localSeq: seqOrLocalSeq,
719
+ refSeq: segment.insert.seq,
720
+ seglen: lenDelta,
721
+ });
722
+ }
727
723
  } else {
728
724
  partials.addOrUpdate({
729
725
  seq: seqOrLocalSeq,
@@ -735,7 +731,7 @@ export class PartialSequenceLengths {
735
731
  for (const id of clientsWithRemoveOrObliterate) {
736
732
  if (id === collabWindow.clientId) {
737
733
  // The local client also removed or obliterated this segment.
738
- const localSeq = moveInfo?.localMovedSeq ?? removalInfo?.localRemovedSeq;
734
+ const { localSeq } = removalInfo.removes[removalInfo.removes.length - 1];
739
735
  if (localSeq === undefined) {
740
736
  // Sure, the local client did it--but that change was already acked.
741
737
  // No need to account for it in the unsequenced records.
@@ -748,15 +744,53 @@ export class PartialSequenceLengths {
748
744
  }
749
745
  assert(
750
746
  localSeq !== undefined,
751
- 0xaba /* Local client was in move/removed client ids but segment has no local seq for either */,
747
+ 0xaba /* Local client was in removed client ids but segment has no local seq for either */,
752
748
  );
753
749
 
754
- unsequencedRecords.partialLengths.addOrUpdate({
755
- seq: localSeq,
756
- clientId: collabWindow.clientId,
757
- seglen: lenDelta,
758
- len: 0,
759
- });
750
+ // Here...
751
+ // - The segment was removed locally at `localSeq`
752
+ // - The segment was also removed remotely at `seqOrLocalSeq`
753
+ // - We're ensuring that partial lengths works for arbitrary `LocalReconnectingPerspective`s.
754
+ //
755
+ // Visualize an arbitrary local reconnecting perspective in 2d space, where the x-axis is `seq` and the y-axis is `localSeq`.
756
+ // The events that have occurred to this segment divide the space into up to 6 regions:
757
+ //
758
+ // (localSeq)
759
+ // | | |
760
+ // | 1 | 2 | 3
761
+ // local remove |-----------------------------
762
+ // | | |
763
+ // | 4 | 5 | 6
764
+ // |----------------------------- (seq)
765
+ // insert remove
766
+ // In all regions but region 5, the segment has length 0 (it was not inserted yet in regions 1 or 4, and removed locally and/or remotely
767
+ // in regions 2, 3, and 6).
768
+ // `accountForInsertion` already added a partial lengths adjustment of +segment.cachedLength for regions 2, 3, 5, and 6.
769
+ // Above in this function, we've added a partial lengths adjustment of -segment.cachedLength for regions 3 and 6.
770
+ //
771
+ // Note that in this picture:
772
+ // - Adding entries to `unsequencedRecords.partialLengths` is like adding adjustments that affect anything above a given Y value
773
+ // - Adding entries to `combinedPartialLengths.partialLengths` is like adding adjustments that affect anything above a given X value
774
+ // - Adding entries with `addLocalAdjustment` is like adding adjustments that affect anything above a given X *and* Y value
775
+ // The remainder this block adds the necessary adjustments to make the length appear 0 in region 2 as well, keeping in mind that
776
+ // region 1 may or may not exist depending on if the insertion is in the collab window.
777
+ if (
778
+ opstampUtils.isAcked(segment.insert) &&
779
+ opstampUtils.greaterThan(segment.insert, minSeqStamp)
780
+ ) {
781
+ combinedPartialLengths.addLocalAdjustment({
782
+ refSeq: segment.insert.seq,
783
+ localSeq,
784
+ seglen: lenDelta,
785
+ });
786
+ } else {
787
+ unsequencedRecords.partialLengths.addOrUpdate({
788
+ seq: localSeq,
789
+ clientId: collabWindow.clientId,
790
+ seglen: lenDelta,
791
+ len: 0,
792
+ });
793
+ }
760
794
 
761
795
  // Because we've included deltas which take effect when either of localSeq or refSeq are high enough,
762
796
  // we need to offset this with an adjustment that takes effect when both are high enough.
@@ -772,16 +806,23 @@ export class PartialSequenceLengths {
772
806
  });
773
807
  } else {
774
808
  // Note that all clients that have a remove or obliterate operation on this segment
775
- // use the seq of the winning move/obliterate in their per-client adjustments!
809
+ // use the seq of the winning obliterate in their per-client adjustments!
776
810
  combinedPartialLengths.addClientAdjustment(id, seqOrLocalSeq, lenDelta);
777
811
 
778
812
  // Also ensure that all these clients have seen the segment as inserted before being removed
779
- // This is technically not necessary for removes (we never ask for the length of this block with
813
+ // This is technically not necessary for setRemoves (we never ask for the length of this block with
780
814
  // respect to a refSeq which this entry would affect), but it's simpler to just add it here.
781
815
  // We already add this entry as part of the accountForInsertion codepath for the client that
782
816
  // actually did insert the segment, hence not doing so [again] here.
783
- if (segment.seq > collabWindow.minSeq && id !== segment.clientId) {
784
- combinedPartialLengths.addClientAdjustment(id, segment.seq, segment.cachedLength);
817
+ if (
818
+ opstampUtils.greaterThan(segment.insert, minSeqStamp) &&
819
+ id !== segment.insert.clientId
820
+ ) {
821
+ combinedPartialLengths.addClientAdjustment(
822
+ id,
823
+ segment.insert.seq,
824
+ segment.cachedLength,
825
+ );
785
826
  }
786
827
  }
787
828
  }
@@ -842,17 +883,16 @@ export class PartialSequenceLengths {
842
883
  if (child.isLeaf()) {
843
884
  const segment = child;
844
885
  const removalInfo = toRemovalInfo(segment);
845
- const moveInfo = toMoveInfo(segment);
846
- if (seq === segment.seq) {
847
- // if this segment was moved on insert, its length should
886
+ const firstRemove = removalInfo?.removes[0];
887
+ if (seq === segment.insert.seq) {
888
+ // if this segment was removed on insert, its length should
848
889
  // only be visible to the inserting client
849
890
  if (
850
- segment.seq !== undefined &&
851
- moveInfo &&
852
- moveInfo.movedSeq < segment.seq &&
853
- wasMovedOnInsert(segment)
891
+ segment.insert.seq !== undefined &&
892
+ firstRemove !== undefined &&
893
+ wasRemovedOnInsert(segment)
854
894
  ) {
855
- this.addClientAdjustment(clientId, moveInfo.movedSeq, segment.cachedLength);
895
+ this.addClientAdjustment(clientId, firstRemove.seq, segment.cachedLength);
856
896
  failIncrementalPropagation = true;
857
897
  } else {
858
898
  seqSeglen += segment.cachedLength;
@@ -860,16 +900,15 @@ export class PartialSequenceLengths {
860
900
  }
861
901
  }
862
902
 
863
- const earlierDeletion = Math.min(
864
- removalInfo?.removedSeq ?? Number.MAX_VALUE,
865
- moveInfo?.movedSeq ?? Number.MAX_VALUE,
866
- );
867
- if (segment.seq !== UnassignedSequenceNumber && seq === earlierDeletion) {
903
+ if (opstampUtils.isAcked(segment.insert) && seq === firstRemove?.seq) {
868
904
  seqSeglen -= segment.cachedLength;
869
905
  if (clientId !== collabWindow.clientId) {
870
906
  this.addClientAdjustment(clientId, seq, -segment.cachedLength);
871
- if (segment.seq > collabWindow.minSeq && segment.clientId !== clientId) {
872
- this.addClientAdjustment(clientId, segment.seq, segment.cachedLength);
907
+ if (
908
+ opstampUtils.greaterThan(segment.insert, getMinSeqStamp(collabWindow)) &&
909
+ segment.insert.clientId !== clientId
910
+ ) {
911
+ this.addClientAdjustment(clientId, segment.insert.seq, segment.cachedLength);
873
912
  failIncrementalPropagation = true;
874
913
  }
875
914
  }
@@ -951,14 +990,13 @@ export class PartialSequenceLengths {
951
990
  );
952
991
  const unsequencedPartialLengths = this.unsequencedRecords.partialLengths;
953
992
  // Local segments at or before localSeq should also be included
954
- const local = unsequencedPartialLengths.latestLeq(localSeq);
955
- if (local) {
956
- length += local.len;
993
+ length += unsequencedPartialLengths.latestLeq(localSeq)?.len ?? 0;
957
994
 
958
- // Lastly, we must add in any additional adjustment due to double-counting removes and obliterations
959
- // removing local-only segments.
960
- length += this.computeOverallRefSeqAdjustment(refSeq, localSeq);
961
- }
995
+ // Lastly, we must add in any additional adjustment that should only take effect when both
996
+ // refSeq AND localSeq are above some threshold. This accounts for things like double-counting
997
+ // local+remote removes, or only subtracting the length out of a local remove if we've also seen
998
+ // the insert of the segment it affects. (see addLocalAdjustment usages for examples)
999
+ length += this.computeOverallRefSeqAdjustment(refSeq, localSeq);
962
1000
  }
963
1001
  return length;
964
1002
  }
@@ -1159,6 +1197,12 @@ export function verifyExpectedPartialLengths(
1159
1197
 
1160
1198
  let expected = 0;
1161
1199
  const nodesToVisit: IMergeNode[] = [node];
1200
+ const perspective =
1201
+ clientId === mergeTree.collabWindow.clientId
1202
+ ? localSeq === undefined
1203
+ ? new LocalDefaultPerspective(clientId)
1204
+ : new LocalReconnectingPerspective(refSeq, clientId, localSeq)
1205
+ : new PriorPerspective(refSeq, clientId);
1162
1206
 
1163
1207
  while (nodesToVisit.length > 0) {
1164
1208
  const thisNode = nodesToVisit.pop();
@@ -1166,7 +1210,7 @@ export function verifyExpectedPartialLengths(
1166
1210
  continue;
1167
1211
  }
1168
1212
  if (thisNode.isLeaf()) {
1169
- expected += mergeTree["nodeLength"](thisNode, refSeq, clientId, localSeq) ?? 0;
1213
+ expected += mergeTree["nodeLength"](thisNode, perspective) ?? 0;
1170
1214
  } else {
1171
1215
  nodesToVisit.push(...thisNode.children.slice(0, thisNode.childCount));
1172
1216
  }
@@ -3,18 +3,16 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { LocalClientId, UnassignedSequenceNumber } from "./constants.js";
7
- import { type MergeTree } from "./mergeTree.js";
8
- import { LeafAction, backwardExcursion, forwardExcursion } from "./mergeTreeNodeWalk.js";
9
- import { seqLTE, type ISegmentLeaf } from "./mergeTreeNodes.js";
10
- import { isInserted, toMoveInfo, toRemovalInfo } from "./segmentInfos.js";
6
+ import { seqLTE, type ISegment } from "./mergeTreeNodes.js";
7
+ import { isInserted, isRemoved } from "./segmentInfos.js";
11
8
  import * as opstampUtils from "./stamps.js";
12
- import type { OperationStamp, InsertOperationStamp, RemoveOperationStamp } from "./stamps.js";
9
+ import type { OperationStamp, RemoveOperationStamp } from "./stamps.js";
13
10
 
14
11
  /**
15
12
  * A perspective which includes some subset of operations known to the local client.
16
13
  *
17
14
  * This helps the local client reason about the state of other clients when they issued an operation.
15
+ * @internal
18
16
  */
19
17
  export interface Perspective {
20
18
  /**
@@ -47,104 +45,23 @@ export interface Perspective {
47
45
  /**
48
46
  * @returns Whether the segment is present (visible) from this perspective
49
47
  */
50
- isSegmentPresent(segment: ISegmentLeaf): boolean;
48
+ isSegmentPresent(segment: ISegment): boolean;
51
49
 
52
50
  /**
53
51
  * @returns Whether this perspective has seen the given operation.
54
52
  */
55
- hasOccurred(stamp: RemoveOperationStamp | InsertOperationStamp): boolean;
56
-
57
- nextSegment(mergeTree: MergeTree, segment: ISegmentLeaf, forward?: boolean): ISegmentLeaf;
58
- previousSegment(mergeTree: MergeTree, segment: ISegmentLeaf): ISegmentLeaf;
53
+ hasOccurred(stamp: OperationStamp): boolean;
59
54
  }
60
55
 
61
56
  abstract class PerspectiveBase {
62
- abstract hasOccurred(stamp: RemoveOperationStamp | InsertOperationStamp): boolean;
57
+ abstract hasOccurred(stamp: OperationStamp): boolean;
63
58
 
64
- /**
65
- * Returns the immediately adjacent segment in the specified direction from this perspective.
66
- * There may actually be multiple segments between the given segment and the returned segment,
67
- * but they were either inserted after this perspective, or have been removed before this perspective.
68
- *
69
- * @param segment - The segment to start from.
70
- * @param forward - The direction to search.
71
- * @returns the next segment in the specified direction, or the start or end of the tree if there is no next segment.
72
- */
73
- public nextSegment(
74
- mergeTree: MergeTree,
75
- segment: ISegmentLeaf,
76
- forward: boolean = true,
77
- ): ISegmentLeaf {
78
- let next: ISegmentLeaf | undefined;
79
- const action = (seg: ISegmentLeaf): boolean | undefined => {
80
- if (this.isSegmentPresent(seg)) {
81
- next = seg;
82
- return LeafAction.Exit;
83
- }
84
- };
85
- (forward ? forwardExcursion : backwardExcursion)(segment, action);
86
- return next ?? (forward ? mergeTree.endOfTree : mergeTree.startOfTree);
87
- }
88
-
89
- /**
90
- * Finds the segment prior to the given segment.
91
- * @param segment - The segment to start from.
92
- * @returns the previous segment, or the start of the tree if there is no previous segment.
93
- * @remarks This is a convenient equivalent to calling `nextSegment(segment, false)`.
94
- */
95
- public previousSegment(mergeTree: MergeTree, segment: ISegmentLeaf): ISegmentLeaf {
96
- return this.nextSegment(mergeTree, segment, false);
97
- }
98
-
99
- public isSegmentPresent(seg: ISegmentLeaf): boolean {
100
- const insert: InsertOperationStamp = {
101
- type: "insert",
102
- clientId: seg.clientId,
103
- seq: seg.seq,
104
- localSeq: seg.localSeq,
105
- };
106
- if (isInserted(seg) && !this.hasOccurred(insert)) {
59
+ public isSegmentPresent(seg: ISegment): boolean {
60
+ if (isInserted(seg) && !this.hasOccurred(seg.insert)) {
107
61
  return false;
108
62
  }
109
63
 
110
- const removes: RemoveOperationStamp[] = [];
111
- const removalInfo = toRemovalInfo(seg);
112
- if (removalInfo !== undefined) {
113
- removes.push(
114
- ...removalInfo.removedClientIds.map((clientId) =>
115
- (clientId === LocalClientId || clientId === 0) &&
116
- removalInfo.localRemovedSeq !== undefined
117
- ? ({
118
- type: "setRemove",
119
- seq: UnassignedSequenceNumber,
120
- clientId,
121
- localSeq: removalInfo.localRemovedSeq,
122
- } as const)
123
- : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
124
- ({ type: "setRemove", seq: removalInfo.removedSeq, clientId } as const),
125
- ),
126
- );
127
- }
128
-
129
- const moveInfo = toMoveInfo(seg);
130
- if (moveInfo !== undefined) {
131
- removes.push(
132
- ...moveInfo.movedClientIds.map((clientId, index) =>
133
- (clientId === LocalClientId || clientId === 0) &&
134
- moveInfo.localMovedSeq !== undefined
135
- ? ({
136
- type: "sliceRemove",
137
- seq: UnassignedSequenceNumber,
138
- clientId,
139
- localSeq: moveInfo.localMovedSeq,
140
- } as const)
141
- : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
142
- ({ type: "setRemove", seq: moveInfo.movedSeqs[index]!, clientId } as const),
143
- ),
144
- );
145
- }
146
-
147
- if (removes.some((remove) => this.hasOccurred(remove))) {
64
+ if (isRemoved(seg) && seg.removes.some((remove) => this.hasOccurred(remove))) {
148
65
  return false;
149
66
  }
150
67
 
@@ -234,16 +151,21 @@ export class RemoteObliteratePerspective extends PerspectiveBase implements Pers
234
151
  super();
235
152
  }
236
153
 
237
- public hasOccurred(stamp: InsertOperationStamp | RemoveOperationStamp): boolean {
154
+ public hasOccurred(stamp: OperationStamp): boolean {
238
155
  // Local-only removals are not visible to an obliterate operation, since this means the local removal was concurrent
239
156
  // to a remote obliterate and we may need to mark the segment appropriately to reflect this overlapping remove.
240
157
  // Every other type of operation is visible: obliterates do not affect segments that have already been removed and acked,
241
158
  // and they always affect segments within their range that have not been removed, even if those segments were inserted
242
159
  // after the obliterate's refSeq.
243
- if (stamp.type !== "insert" && opstampUtils.isLocal(stamp)) {
160
+ if (isRemoveOperationStamp(stamp) && opstampUtils.isLocal(stamp)) {
244
161
  return false;
245
162
  }
246
163
 
247
164
  return true;
248
165
  }
249
166
  }
167
+
168
+ function isRemoveOperationStamp(stamp: OperationStamp): stamp is RemoveOperationStamp {
169
+ const { type } = stamp as unknown as RemoveOperationStamp;
170
+ return type === "setRemove" || type === "sliceRemove";
171
+ }