@fluidframework/merge-tree 2.12.0 → 2.20.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 (307) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/api-report/merge-tree.legacy.alpha.api.md +0 -108
  3. package/dist/MergeTreeTextHelper.d.ts.map +1 -1
  4. package/dist/MergeTreeTextHelper.js +0 -2
  5. package/dist/MergeTreeTextHelper.js.map +1 -1
  6. package/dist/attributionPolicy.d.ts.map +1 -1
  7. package/dist/attributionPolicy.js +6 -16
  8. package/dist/attributionPolicy.js.map +1 -1
  9. package/dist/client.d.ts +3 -4
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +39 -28
  12. package/dist/client.js.map +1 -1
  13. package/dist/endOfTreeSegment.d.ts +2 -1
  14. package/dist/endOfTreeSegment.d.ts.map +1 -1
  15. package/dist/endOfTreeSegment.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -4
  19. package/dist/index.js.map +1 -1
  20. package/dist/legacy.d.ts +0 -4
  21. package/dist/localReference.d.ts +5 -7
  22. package/dist/localReference.d.ts.map +1 -1
  23. package/dist/localReference.js +1 -3
  24. package/dist/localReference.js.map +1 -1
  25. package/dist/mergeTree.d.ts +8 -7
  26. package/dist/mergeTree.d.ts.map +1 -1
  27. package/dist/mergeTree.js +187 -228
  28. package/dist/mergeTree.js.map +1 -1
  29. package/dist/mergeTreeNodeWalk.d.ts.map +1 -1
  30. package/dist/mergeTreeNodeWalk.js +3 -2
  31. package/dist/mergeTreeNodeWalk.js.map +1 -1
  32. package/dist/mergeTreeNodes.d.ts +65 -325
  33. package/dist/mergeTreeNodes.d.ts.map +1 -1
  34. package/dist/mergeTreeNodes.js +96 -130
  35. package/dist/mergeTreeNodes.js.map +1 -1
  36. package/dist/mergeTreeTracking.d.ts.map +1 -1
  37. package/dist/mergeTreeTracking.js +0 -2
  38. package/dist/mergeTreeTracking.js.map +1 -1
  39. package/dist/opBuilder.d.ts +0 -5
  40. package/dist/opBuilder.d.ts.map +1 -1
  41. package/dist/opBuilder.js +0 -5
  42. package/dist/opBuilder.js.map +1 -1
  43. package/dist/package.json +2 -1
  44. package/dist/partialLengths.d.ts +2 -2
  45. package/dist/partialLengths.d.ts.map +1 -1
  46. package/dist/partialLengths.js +29 -31
  47. package/dist/partialLengths.js.map +1 -1
  48. package/dist/perspective.d.ts +3 -2
  49. package/dist/perspective.d.ts.map +1 -1
  50. package/dist/perspective.js +5 -2
  51. package/dist/perspective.js.map +1 -1
  52. package/dist/referencePositions.d.ts.map +1 -1
  53. package/dist/referencePositions.js +4 -1
  54. package/dist/referencePositions.js.map +1 -1
  55. package/dist/revertibles.d.ts.map +1 -1
  56. package/dist/revertibles.js +10 -14
  57. package/dist/revertibles.js.map +1 -1
  58. package/dist/segmentGroupCollection.d.ts +4 -4
  59. package/dist/segmentGroupCollection.d.ts.map +1 -1
  60. package/dist/segmentGroupCollection.js +0 -6
  61. package/dist/segmentGroupCollection.js.map +1 -1
  62. package/dist/segmentInfos.d.ts +251 -0
  63. package/dist/segmentInfos.d.ts.map +1 -0
  64. package/dist/segmentInfos.js +166 -0
  65. package/dist/segmentInfos.js.map +1 -0
  66. package/dist/snapshotLoader.d.ts.map +1 -1
  67. package/dist/snapshotLoader.js +36 -44
  68. package/dist/snapshotLoader.js.map +1 -1
  69. package/dist/snapshotV1.d.ts.map +1 -1
  70. package/dist/snapshotV1.js +9 -12
  71. package/dist/snapshotV1.js.map +1 -1
  72. package/dist/snapshotlegacy.d.ts +2 -2
  73. package/dist/snapshotlegacy.d.ts.map +1 -1
  74. package/dist/snapshotlegacy.js +5 -3
  75. package/dist/snapshotlegacy.js.map +1 -1
  76. package/dist/sortedSegmentSet.d.ts.map +1 -1
  77. package/dist/sortedSegmentSet.js +5 -8
  78. package/dist/sortedSegmentSet.js.map +1 -1
  79. package/dist/test/beastTest.spec.d.ts +0 -2
  80. package/dist/test/beastTest.spec.d.ts.map +1 -1
  81. package/dist/test/beastTest.spec.js +1 -5
  82. package/dist/test/beastTest.spec.js.map +1 -1
  83. package/dist/test/client.annotateMarker.spec.js.map +1 -1
  84. package/dist/test/client.applyMsg.spec.js +15 -12
  85. package/dist/test/client.applyMsg.spec.js.map +1 -1
  86. package/dist/test/client.attributionFarm.spec.js.map +1 -1
  87. package/dist/test/client.getPosition.spec.js +3 -2
  88. package/dist/test/client.getPosition.spec.js.map +1 -1
  89. package/dist/test/client.localReference.spec.js +6 -6
  90. package/dist/test/client.localReference.spec.js.map +1 -1
  91. package/dist/test/client.localReferenceFarm.spec.js.map +1 -1
  92. package/dist/test/client.rollback.spec.js.map +1 -1
  93. package/dist/test/dirname.cjs +0 -1
  94. package/dist/test/dirname.cjs.map +1 -1
  95. package/dist/test/index.d.ts +1 -1
  96. package/dist/test/index.d.ts.map +1 -1
  97. package/dist/test/index.js +2 -4
  98. package/dist/test/index.js.map +1 -1
  99. package/dist/test/mergeTree.annotate.spec.js +3 -0
  100. package/dist/test/mergeTree.annotate.spec.js.map +1 -1
  101. package/dist/test/mergeTree.insertingWalk.spec.js +1 -1
  102. package/dist/test/mergeTree.insertingWalk.spec.js.map +1 -1
  103. package/dist/test/mergeTree.markRangeRemoved.spec.js +2 -0
  104. package/dist/test/mergeTree.markRangeRemoved.spec.js.map +1 -1
  105. package/dist/test/mergeTree.walk.spec.js.map +1 -1
  106. package/dist/test/mergeTreeOperationRunner.d.ts.map +1 -1
  107. package/dist/test/mergeTreeOperationRunner.js +2 -3
  108. package/dist/test/mergeTreeOperationRunner.js.map +1 -1
  109. package/dist/test/obliterate.spec.js.map +1 -1
  110. package/dist/test/propertyManager.spec.js.map +1 -1
  111. package/dist/test/reconnectHelper.d.ts +2 -1
  112. package/dist/test/reconnectHelper.d.ts.map +1 -1
  113. package/dist/test/reconnectHelper.js.map +1 -1
  114. package/dist/test/resetPendingSegmentsToOp.spec.js.map +1 -1
  115. package/dist/test/revertibleFarm.spec.js.map +1 -1
  116. package/dist/test/segmentGroupCollection.spec.js +15 -3
  117. package/dist/test/segmentGroupCollection.spec.js.map +1 -1
  118. package/dist/test/snapshot.utils.d.ts +2 -2
  119. package/dist/test/snapshot.utils.d.ts.map +1 -1
  120. package/dist/test/snapshot.utils.js.map +1 -1
  121. package/dist/test/sortedSegmentSet.spec.js +4 -3
  122. package/dist/test/sortedSegmentSet.spec.js.map +1 -1
  123. package/dist/test/testClient.d.ts +8 -6
  124. package/dist/test/testClient.d.ts.map +1 -1
  125. package/dist/test/testClient.js +28 -27
  126. package/dist/test/testClient.js.map +1 -1
  127. package/dist/test/testClientLogger.d.ts.map +1 -1
  128. package/dist/test/testClientLogger.js +6 -4
  129. package/dist/test/testClientLogger.js.map +1 -1
  130. package/dist/test/testUtils.d.ts +2 -2
  131. package/dist/test/testUtils.d.ts.map +1 -1
  132. package/dist/test/testUtils.js +32 -8
  133. package/dist/test/testUtils.js.map +1 -1
  134. package/dist/test/text.d.ts +2 -2
  135. package/dist/test/text.d.ts.map +1 -1
  136. package/dist/test/text.js +12 -6
  137. package/dist/test/text.js.map +1 -1
  138. package/dist/test/tracking.spec.js.map +1 -1
  139. package/dist/test/wordUnitTests.spec.js +1 -1
  140. package/dist/test/wordUnitTests.spec.js.map +1 -1
  141. package/dist/zamboni.d.ts.map +1 -1
  142. package/dist/zamboni.js +8 -7
  143. package/dist/zamboni.js.map +1 -1
  144. package/lib/MergeTreeTextHelper.d.ts.map +1 -1
  145. package/lib/MergeTreeTextHelper.js +0 -2
  146. package/lib/MergeTreeTextHelper.js.map +1 -1
  147. package/lib/attributionPolicy.d.ts.map +1 -1
  148. package/lib/attributionPolicy.js +6 -16
  149. package/lib/attributionPolicy.js.map +1 -1
  150. package/lib/client.d.ts +3 -4
  151. package/lib/client.d.ts.map +1 -1
  152. package/lib/client.js +40 -29
  153. package/lib/client.js.map +1 -1
  154. package/lib/endOfTreeSegment.d.ts +2 -1
  155. package/lib/endOfTreeSegment.d.ts.map +1 -1
  156. package/lib/endOfTreeSegment.js.map +1 -1
  157. package/lib/index.d.ts +1 -1
  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/legacy.d.ts +0 -4
  162. package/lib/localReference.d.ts +5 -7
  163. package/lib/localReference.d.ts.map +1 -1
  164. package/lib/localReference.js +1 -3
  165. package/lib/localReference.js.map +1 -1
  166. package/lib/mergeTree.d.ts +8 -7
  167. package/lib/mergeTree.d.ts.map +1 -1
  168. package/lib/mergeTree.js +175 -220
  169. package/lib/mergeTree.js.map +1 -1
  170. package/lib/mergeTreeNodeWalk.d.ts.map +1 -1
  171. package/lib/mergeTreeNodeWalk.js +3 -2
  172. package/lib/mergeTreeNodeWalk.js.map +1 -1
  173. package/lib/mergeTreeNodes.d.ts +65 -325
  174. package/lib/mergeTreeNodes.d.ts.map +1 -1
  175. package/lib/mergeTreeNodes.js +92 -127
  176. package/lib/mergeTreeNodes.js.map +1 -1
  177. package/lib/mergeTreeTracking.d.ts.map +1 -1
  178. package/lib/mergeTreeTracking.js +0 -2
  179. package/lib/mergeTreeTracking.js.map +1 -1
  180. package/lib/opBuilder.d.ts +0 -5
  181. package/lib/opBuilder.d.ts.map +1 -1
  182. package/lib/opBuilder.js +0 -5
  183. package/lib/opBuilder.js.map +1 -1
  184. package/lib/partialLengths.d.ts +2 -2
  185. package/lib/partialLengths.d.ts.map +1 -1
  186. package/lib/partialLengths.js +26 -28
  187. package/lib/partialLengths.js.map +1 -1
  188. package/lib/perspective.d.ts +3 -2
  189. package/lib/perspective.d.ts.map +1 -1
  190. package/lib/perspective.js +5 -2
  191. package/lib/perspective.js.map +1 -1
  192. package/lib/referencePositions.d.ts.map +1 -1
  193. package/lib/referencePositions.js +4 -1
  194. package/lib/referencePositions.js.map +1 -1
  195. package/lib/revertibles.d.ts.map +1 -1
  196. package/lib/revertibles.js +8 -12
  197. package/lib/revertibles.js.map +1 -1
  198. package/lib/segmentGroupCollection.d.ts +4 -4
  199. package/lib/segmentGroupCollection.d.ts.map +1 -1
  200. package/lib/segmentGroupCollection.js +0 -6
  201. package/lib/segmentGroupCollection.js.map +1 -1
  202. package/lib/segmentInfos.d.ts +251 -0
  203. package/lib/segmentInfos.d.ts.map +1 -0
  204. package/lib/segmentInfos.js +145 -0
  205. package/lib/segmentInfos.js.map +1 -0
  206. package/lib/snapshotLoader.d.ts.map +1 -1
  207. package/lib/snapshotLoader.js +36 -44
  208. package/lib/snapshotLoader.js.map +1 -1
  209. package/lib/snapshotV1.d.ts.map +1 -1
  210. package/lib/snapshotV1.js +9 -12
  211. package/lib/snapshotV1.js.map +1 -1
  212. package/lib/snapshotlegacy.d.ts +2 -2
  213. package/lib/snapshotlegacy.d.ts.map +1 -1
  214. package/lib/snapshotlegacy.js +5 -3
  215. package/lib/snapshotlegacy.js.map +1 -1
  216. package/lib/sortedSegmentSet.d.ts.map +1 -1
  217. package/lib/sortedSegmentSet.js +5 -8
  218. package/lib/sortedSegmentSet.js.map +1 -1
  219. package/lib/test/beastTest.spec.d.ts +0 -2
  220. package/lib/test/beastTest.spec.d.ts.map +1 -1
  221. package/lib/test/beastTest.spec.js +0 -3
  222. package/lib/test/beastTest.spec.js.map +1 -1
  223. package/lib/test/client.annotateMarker.spec.js.map +1 -1
  224. package/lib/test/client.applyMsg.spec.js +15 -12
  225. package/lib/test/client.applyMsg.spec.js.map +1 -1
  226. package/lib/test/client.attributionFarm.spec.js.map +1 -1
  227. package/lib/test/client.getPosition.spec.js +3 -2
  228. package/lib/test/client.getPosition.spec.js.map +1 -1
  229. package/lib/test/client.localReference.spec.js +1 -1
  230. package/lib/test/client.localReference.spec.js.map +1 -1
  231. package/lib/test/client.localReferenceFarm.spec.js.map +1 -1
  232. package/lib/test/client.rollback.spec.js +1 -1
  233. package/lib/test/client.rollback.spec.js.map +1 -1
  234. package/lib/test/dirname.cjs +0 -1
  235. package/lib/test/dirname.cjs.map +1 -1
  236. package/lib/test/index.d.ts +1 -1
  237. package/lib/test/index.d.ts.map +1 -1
  238. package/lib/test/index.js +1 -1
  239. package/lib/test/index.js.map +1 -1
  240. package/lib/test/mergeTree.annotate.spec.js +3 -0
  241. package/lib/test/mergeTree.annotate.spec.js.map +1 -1
  242. package/lib/test/mergeTree.insertingWalk.spec.js +2 -2
  243. package/lib/test/mergeTree.insertingWalk.spec.js.map +1 -1
  244. package/lib/test/mergeTree.markRangeRemoved.spec.js +2 -0
  245. package/lib/test/mergeTree.markRangeRemoved.spec.js.map +1 -1
  246. package/lib/test/mergeTree.walk.spec.js.map +1 -1
  247. package/lib/test/mergeTreeOperationRunner.d.ts.map +1 -1
  248. package/lib/test/mergeTreeOperationRunner.js +1 -2
  249. package/lib/test/mergeTreeOperationRunner.js.map +1 -1
  250. package/lib/test/obliterate.spec.js.map +1 -1
  251. package/lib/test/propertyManager.spec.js.map +1 -1
  252. package/lib/test/reconnectHelper.d.ts +2 -1
  253. package/lib/test/reconnectHelper.d.ts.map +1 -1
  254. package/lib/test/reconnectHelper.js.map +1 -1
  255. package/lib/test/resetPendingSegmentsToOp.spec.js.map +1 -1
  256. package/lib/test/revertibleFarm.spec.js.map +1 -1
  257. package/lib/test/segmentGroupCollection.spec.js +15 -3
  258. package/lib/test/segmentGroupCollection.spec.js.map +1 -1
  259. package/lib/test/snapshot.utils.d.ts +2 -2
  260. package/lib/test/snapshot.utils.d.ts.map +1 -1
  261. package/lib/test/snapshot.utils.js.map +1 -1
  262. package/lib/test/sortedSegmentSet.spec.js +4 -3
  263. package/lib/test/sortedSegmentSet.spec.js.map +1 -1
  264. package/lib/test/testClient.d.ts +8 -6
  265. package/lib/test/testClient.d.ts.map +1 -1
  266. package/lib/test/testClient.js +29 -28
  267. package/lib/test/testClient.js.map +1 -1
  268. package/lib/test/testClientLogger.d.ts.map +1 -1
  269. package/lib/test/testClientLogger.js +5 -3
  270. package/lib/test/testClientLogger.js.map +1 -1
  271. package/lib/test/testUtils.d.ts +2 -2
  272. package/lib/test/testUtils.d.ts.map +1 -1
  273. package/lib/test/testUtils.js +9 -8
  274. package/lib/test/testUtils.js.map +1 -1
  275. package/lib/test/text.d.ts +2 -2
  276. package/lib/test/text.d.ts.map +1 -1
  277. package/lib/test/text.js +12 -6
  278. package/lib/test/text.js.map +1 -1
  279. package/lib/test/tracking.spec.js.map +1 -1
  280. package/lib/test/wordUnitTests.spec.js +1 -1
  281. package/lib/test/wordUnitTests.spec.js.map +1 -1
  282. package/lib/zamboni.d.ts.map +1 -1
  283. package/lib/zamboni.js +7 -6
  284. package/lib/zamboni.js.map +1 -1
  285. package/package.json +77 -19
  286. package/src/MergeTreeTextHelper.ts +2 -4
  287. package/src/attributionPolicy.ts +5 -13
  288. package/src/client.ts +55 -44
  289. package/src/endOfTreeSegment.ts +3 -5
  290. package/src/index.ts +0 -7
  291. package/src/localReference.ts +6 -8
  292. package/src/mergeTree.ts +233 -290
  293. package/src/mergeTreeNodeWalk.ts +3 -2
  294. package/src/mergeTreeNodes.ts +160 -490
  295. package/src/mergeTreeTracking.ts +0 -3
  296. package/src/opBuilder.ts +0 -5
  297. package/src/partialLengths.ts +40 -29
  298. package/src/perspective.ts +23 -4
  299. package/src/referencePositions.ts +4 -1
  300. package/src/revertibles.ts +19 -16
  301. package/src/segmentGroupCollection.ts +7 -18
  302. package/src/segmentInfos.ts +371 -0
  303. package/src/snapshotLoader.ts +56 -57
  304. package/src/snapshotV1.ts +14 -16
  305. package/src/snapshotlegacy.ts +12 -17
  306. package/src/sortedSegmentSet.ts +6 -8
  307. package/src/zamboni.ts +10 -12
@@ -0,0 +1,371 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { assert, isObject } from "@fluidframework/core-utils/internal";
7
+
8
+ import { ISegmentInternal, ISegmentPrivate, MergeBlock } from "./mergeTreeNodes.js";
9
+ import type { ReferencePosition } from "./referencePositions.js";
10
+
11
+ export interface StringToType {
12
+ "string": string;
13
+ "number": number;
14
+ "object": object;
15
+ "array": [];
16
+ "boolean": boolean;
17
+ }
18
+
19
+ export function propExists<P extends string>(
20
+ thing: unknown,
21
+ prop: P,
22
+ ): thing is Record<P, unknown> {
23
+ return isObject(thing) && prop in thing;
24
+ }
25
+
26
+ export function hasProp<P extends string, T extends keyof StringToType>(
27
+ thing: unknown,
28
+ prop: P,
29
+ type: T,
30
+ ): thing is Record<P, StringToType[typeof type]> {
31
+ return (
32
+ propExists(thing, prop) &&
33
+ (type === "array" ? Array.isArray(thing[prop]) : typeof thing[prop] === type)
34
+ );
35
+ }
36
+
37
+ export function propInstanceOf<P extends string, T>(
38
+ thing: unknown,
39
+ prop: P,
40
+ type: new (...args: any[]) => T,
41
+ ): thing is Record<P, T> {
42
+ return propExists(thing, prop) && thing[prop] instanceof type;
43
+ }
44
+
45
+ /**
46
+ * Contains insertion information associated to an {@link ISegment}.
47
+ */
48
+ export interface IInsertionInfo {
49
+ /**
50
+ * Short clientId for the client that inserted this segment.
51
+ */
52
+ clientId: number;
53
+ /**
54
+ * Local seq at which this segment was inserted.
55
+ * This is defined if and only if the insertion of the segment is pending ack, i.e. `seq` is UnassignedSequenceNumber.
56
+ * Once the segment is acked, this field is cleared.
57
+ *
58
+ * @privateRemarks
59
+ * See {@link CollaborationWindow.localSeq} for more information on the semantics of localSeq.
60
+ */
61
+ localSeq?: number;
62
+ /**
63
+ * Seq at which this segment was inserted.
64
+ * If undefined, it is assumed the segment was inserted prior to the collab window's minimum sequence number.
65
+ */
66
+ seq: number;
67
+ }
68
+
69
+ /**
70
+ * Converts a segment-like object to an insertion info object if possible.
71
+ *
72
+ * @param segmentLike - The segment-like object to convert.
73
+ * @returns The insertion info object if the conversion is possible, otherwise undefined.
74
+ */
75
+ export const toInsertionInfo = (segmentLike: unknown): IInsertionInfo | undefined =>
76
+ hasProp(segmentLike, "clientId", "number") && hasProp(segmentLike, "seq", "number")
77
+ ? segmentLike
78
+ : undefined;
79
+
80
+ /**
81
+ * A type-guard which determines if the segment has insertion info, and
82
+ * returns true if it does, along with applying strong typing.
83
+ *
84
+ * @param segmentLike - The segment-like object to check.
85
+ * @returns True if the segment has insertion info, otherwise false.
86
+ */
87
+ export const isInserted = (segmentLike: unknown): segmentLike is IInsertionInfo =>
88
+ toInsertionInfo(segmentLike) !== undefined;
89
+
90
+ /**
91
+ * Asserts that the segment has insertion info. Usage of this function should not produce a user facing error.
92
+ *
93
+ * @param segmentLike - The segment-like object to check.
94
+ * @throws Will throw an error if the segment does not have insertion info.
95
+ */
96
+ export const assertInserted: <T extends Partial<IInsertionInfo> | undefined>(
97
+ segmentLike: ISegmentInternal | Partial<IInsertionInfo> | T,
98
+ ) => asserts segmentLike is IInsertionInfo | Exclude<T, Partial<IInsertionInfo>> = (
99
+ segmentLike,
100
+ ) =>
101
+ assert(
102
+ segmentLike === undefined || isInserted(segmentLike),
103
+ 0xaa0 /* must be insertionInfo */,
104
+ );
105
+
106
+ /**
107
+ * Common properties for a node in a merge tree.
108
+ */
109
+ export interface IMergeNodeInfo {
110
+ /**
111
+ * The parent merge block if the node is parented
112
+ */
113
+ parent: MergeBlock;
114
+
115
+ /**
116
+ * The index of this node in its parent's list of children.
117
+ */
118
+ index: number;
119
+
120
+ /**
121
+ * A string that can be used for comparing the location of this node to other `MergeNode`s in the same tree.
122
+ * `a.ordinal < b.ordinal` if and only if `a` comes before `b` in a pre-order traversal of the tree.
123
+ */
124
+ ordinal: string;
125
+ }
126
+
127
+ /**
128
+ * Converts a segment-like object to a merge node info object if possible.
129
+ *
130
+ * @param segmentLike - The segment-like object to convert.
131
+ * @returns The merge node info object if the conversion is possible, otherwise undefined.
132
+ */
133
+ export const toMergeNodeInfo = (nodeLike: unknown): IMergeNodeInfo | undefined =>
134
+ propInstanceOf(nodeLike, "parent", MergeBlock) &&
135
+ hasProp(nodeLike, "ordinal", "string") &&
136
+ hasProp(nodeLike, "index", "number")
137
+ ? nodeLike
138
+ : undefined;
139
+
140
+ /**
141
+ * A type-guard which determines if the segment has merge node info, and
142
+ * returns true if it does, along with applying strong typing.
143
+ *
144
+ * @param nodeLike - The segment-like object to check.
145
+ * @returns True if the segment has merge node info, otherwise false.
146
+ */
147
+ export const isMergeNodeInfo = (nodeLike: unknown): nodeLike is IMergeNodeInfo =>
148
+ toMergeNodeInfo(nodeLike) !== undefined;
149
+
150
+ /**
151
+ * Asserts that the segment has merge node info. Usage of this function should not produce a user facing error.
152
+ *
153
+ * @param segmentLike - The segment-like object to check.
154
+ * @throws Will throw an error if the segment does not have merge node info.
155
+ */
156
+ export const assertMergeNode: <T extends Partial<IMergeNodeInfo> | undefined>(
157
+ nodeLike: ISegmentInternal | ISegmentPrivate | Partial<IMergeNodeInfo> | T,
158
+ ) => asserts nodeLike is IMergeNodeInfo | Exclude<T, Partial<IMergeNodeInfo>> = (
159
+ segmentLike,
160
+ ) =>
161
+ assert(
162
+ segmentLike === undefined || isMergeNodeInfo(segmentLike),
163
+ 0xaa1 /* must be MergeNodeInfo */,
164
+ );
165
+
166
+ /**
167
+ * Removes the merge node info. This is used to remove nodes from the merge-tree.
168
+ * @param segmentLike - The segment-like object to check.
169
+ * @returns This function will change the type of the provided node like to never via an assertion. This
170
+ * ensures no further usage of the removed merge node info is allowed. if continued use is required other
171
+ * type coercion methods should be used to correctly re-type the variable.
172
+ */
173
+ export const removeMergeNodeInfo: (nodeLike: IMergeNodeInfo) => asserts nodeLike is never = (
174
+ nodeLike,
175
+ ) =>
176
+ Object.assign<IMergeNodeInfo, Record<keyof IMergeNodeInfo, undefined>>(nodeLike, {
177
+ parent: undefined,
178
+ index: undefined,
179
+ ordinal: undefined,
180
+ });
181
+
182
+ /**
183
+ * Contains removal information associated to an {@link ISegment}.
184
+ */
185
+ export interface IRemovalInfo {
186
+ /**
187
+ * Local seq at which this segment was removed, if the removal is yet-to-be acked.
188
+ */
189
+ localRemovedSeq?: number;
190
+ /**
191
+ * Seq at which this segment was removed.
192
+ */
193
+ removedSeq: number;
194
+ /**
195
+ * List of client IDs that have removed this segment.
196
+ * The client that actually removed the segment (i.e. whose removal op was sequenced first) is stored as the first
197
+ * client in this list. Other clients in the list have all issued concurrent ops to remove the segment.
198
+ * @remarks When this list has length \> 1, this is referred to as the "overlapping remove" case.
199
+ */
200
+ removedClientIds: number[];
201
+ }
202
+
203
+ /**
204
+ * Converts a segment-like object to a removal info object if possible.
205
+ *
206
+ * @param segmentLike - The segment-like object to convert.
207
+ * @returns The removal info object if the conversion is possible, otherwise undefined.
208
+ */
209
+ export const toRemovalInfo = (segmentLike: unknown): IRemovalInfo | undefined =>
210
+ hasProp(segmentLike, "removedClientIds", "array") &&
211
+ hasProp(segmentLike, "removedSeq", "number")
212
+ ? segmentLike
213
+ : undefined;
214
+
215
+ /**
216
+ * A type-guard which determines if the segment has removal info, and
217
+ * returns true if it does, along with applying strong typing.
218
+ *
219
+ * @param segmentLike - The segment-like object to check.
220
+ * @returns True if the segment has removal info, otherwise false.
221
+ */
222
+ export const isRemoved = (segmentLike: unknown): segmentLike is IRemovalInfo =>
223
+ toRemovalInfo(segmentLike) !== undefined;
224
+
225
+ /**
226
+ * Asserts that the segment has removal info. Usage of this function should not produce a user facing error.
227
+ *
228
+ * @param segmentLike - The segment-like object to check.
229
+ * @throws Will throw an error if the segment does not have removal info.
230
+ */
231
+ export const assertRemoved: <T extends Partial<IRemovalInfo> | undefined>(
232
+ segmentLike: ISegmentInternal | Partial<IRemovalInfo> | T,
233
+ ) => asserts segmentLike is IRemovalInfo | Exclude<T, Partial<IRemovalInfo>> = (segmentLike) =>
234
+ assert(segmentLike === undefined || isRemoved(segmentLike), 0xaa2 /* must be removalInfo */);
235
+
236
+ /**
237
+ * Removes the removal info. This is used in rollback.
238
+ * @param segmentLike - The segment-like object to check.
239
+ * @returns This function will change the type of the provided node like to never via an assertion. This
240
+ * ensures no further usage of the removed removal info is allowed. if continued use is required other
241
+ * type coercion methods should be use to correctly re-type the variable.
242
+ */
243
+ export const removeRemovalInfo: (nodeLike: IRemovalInfo) => asserts nodeLike is never = (
244
+ nodeLike,
245
+ ) =>
246
+ Object.assign<IRemovalInfo, Record<keyof IRemovalInfo, undefined>>(nodeLike, {
247
+ localRemovedSeq: undefined,
248
+ removedClientIds: undefined,
249
+ removedSeq: undefined,
250
+ });
251
+
252
+ /**
253
+ * Tracks information about when and where this segment was moved to.
254
+ *
255
+ * Note that merge-tree does not currently support moving and only supports
256
+ * obliterate. The fields below include "move" in their names to avoid renaming
257
+ * in the future, when moves _are_ supported.
258
+ */
259
+ export interface IMoveInfo {
260
+ /**
261
+ * Local seq at which this segment was moved if the move is yet-to-be
262
+ * acked.
263
+ */
264
+ localMovedSeq?: number;
265
+
266
+ /**
267
+ * The first seq at which this segment was moved.
268
+ */
269
+ movedSeq: number;
270
+
271
+ /**
272
+ * All seqs at which this segment was moved. In the case of overlapping,
273
+ * concurrent moves this array will contain multiple seqs.
274
+ *
275
+ * The seq at `movedSeqs[i]` corresponds to the client id at `movedClientIds[i]`.
276
+ *
277
+ * The first element corresponds to the seq of the first move
278
+ */
279
+ movedSeqs: number[];
280
+
281
+ /**
282
+ * A reference to the inserted destination segment corresponding to this
283
+ * segment's move.
284
+ *
285
+ * If undefined, the move was an obliterate.
286
+ *
287
+ * Currently this field is unused, as we only support obliterate operations
288
+ */
289
+ moveDst?: ReferencePosition;
290
+
291
+ /**
292
+ * List of client IDs that have moved this segment.
293
+ *
294
+ * The client that actually moved the segment (i.e. whose move op was sequenced
295
+ * first) is stored as the first client in this list. Other clients in the
296
+ * list have all issued concurrent ops to move the segment.
297
+ */
298
+ movedClientIds: number[];
299
+
300
+ /**
301
+ * If this segment was inserted into a concurrently moved range and
302
+ * the move op was sequenced before the insertion op. In this case,
303
+ * the segment is visible only to the inserting client
304
+ *
305
+ * `wasMovedOnInsert` only applies for acked obliterates. That is, if
306
+ * a segment inserted by a remote client is moved on insertion by a local
307
+ * and unacked obliterate, we do not consider it as having been moved
308
+ * on insert
309
+ *
310
+ * If a segment is moved on insertion, its length is only ever visible to
311
+ * the client that inserted the segment. This is relevant in partial length
312
+ * calculations
313
+ */
314
+ wasMovedOnInsert: boolean;
315
+ }
316
+ export const toMoveInfo = (segmentLike: unknown): IMoveInfo | undefined =>
317
+ hasProp(segmentLike, "movedClientIds", "array") &&
318
+ hasProp(segmentLike, "movedSeq", "number") &&
319
+ hasProp(segmentLike, "movedSeqs", "array") &&
320
+ hasProp(segmentLike, "wasMovedOnInsert", "boolean")
321
+ ? segmentLike
322
+ : undefined;
323
+
324
+ /**
325
+ * A type-guard which determines if the segment has move info, and
326
+ * returns true if it does, along with applying strong typing.
327
+ *
328
+ * @param segmentLike - The segment-like object to check.
329
+ * @returns True if the segment has move info, otherwise false.
330
+ */
331
+ export const isMoved = (segmentLike: unknown): segmentLike is IMoveInfo =>
332
+ toMoveInfo(segmentLike) !== undefined;
333
+
334
+ /**
335
+ * Asserts that the segment has move info. Usage of this function should not produce a user facing error.
336
+ *
337
+ * @param segmentLike - The segment-like object to check.
338
+ * @throws Will throw an error if the segment does not have move info.
339
+ */
340
+ export const assertMoved: <T extends Partial<IMoveInfo> | undefined>(
341
+ segmentLike: ISegmentInternal | Partial<IMoveInfo> | T,
342
+ ) => asserts segmentLike is IMoveInfo | Exclude<T, Partial<IMoveInfo>> = (segmentLike) =>
343
+ assert(segmentLike === undefined || isMoved(segmentLike), 0xaa3 /* must be moveInfo */);
344
+
345
+ /**
346
+ * A union type representing any segment info.
347
+ */
348
+ export type SegmentInfo = IMergeNodeInfo | IInsertionInfo | IMoveInfo | IRemovalInfo;
349
+
350
+ /**
351
+ * A type representing a segment with additional info.
352
+ */
353
+ export type SegmentWithInfo<
354
+ T extends SegmentInfo,
355
+ S extends ISegmentPrivate = ISegmentPrivate,
356
+ > = S & T;
357
+
358
+ /**
359
+ * Overwrites the segment info on a segment-like object.
360
+ *
361
+ * @param segmentLike - The segment-like object to set the info on.
362
+ * @param info - The segment info to overwrite.
363
+ * @returns The segment-like object with the info set.
364
+ */
365
+ export const overwriteInfo = <
366
+ T extends SegmentInfo,
367
+ S extends ISegmentPrivate = ISegmentPrivate,
368
+ >(
369
+ segmentLike: S,
370
+ info: T,
371
+ ): SegmentWithInfo<T, S> => Object.assign(segmentLike, info);
@@ -20,12 +20,18 @@ import {
20
20
  createChildLogger,
21
21
  } from "@fluidframework/telemetry-utils/internal";
22
22
 
23
- // eslint-disable-next-line import/no-deprecated
24
23
  import { Client } from "./client.js";
25
24
  import { NonCollabClient, UniversalSequenceNumber } from "./constants.js";
26
25
  import { MergeTree } from "./mergeTree.js";
27
- import { ISegmentLeaf } from "./mergeTreeNodes.js";
26
+ import { ISegmentPrivate } from "./mergeTreeNodes.js";
28
27
  import { IJSONSegment } from "./ops.js";
28
+ import {
29
+ IRemovalInfo,
30
+ overwriteInfo,
31
+ type IInsertionInfo,
32
+ type IMoveInfo,
33
+ type SegmentWithInfo,
34
+ } from "./segmentInfos.js";
29
35
  import {
30
36
  IJSONSegmentWithMergeInfo,
31
37
  MergeTreeChunkV1,
@@ -39,7 +45,7 @@ export class SnapshotLoader {
39
45
 
40
46
  constructor(
41
47
  private readonly runtime: IFluidDataStoreRuntime,
42
- // eslint-disable-next-line import/no-deprecated
48
+
43
49
  private readonly client: Client,
44
50
  private readonly mergeTree: MergeTree,
45
51
  logger: ITelemetryLoggerExt,
@@ -96,60 +102,55 @@ export class SnapshotLoader {
96
102
 
97
103
  private readonly specToSegment = (
98
104
  spec: IJSONSegment | IJSONSegmentWithMergeInfo,
99
- ): ISegmentLeaf => {
100
- let seg: ISegmentLeaf;
101
-
105
+ ): SegmentWithInfo<IInsertionInfo> => {
102
106
  if (hasMergeInfo(spec)) {
103
- seg = this.client.specToSegment(spec.json);
104
-
105
- // `specToSegment()` initializes `seg` with the LocalClientId. Overwrite this with
106
- // the `spec` client (if specified). Otherwise overwrite with `NonCollabClient`.
107
- seg.clientId =
108
- spec.client === undefined
109
- ? NonCollabClient
110
- : this.client.getOrAddShortClientId(spec.client);
111
-
112
- seg.seq = spec.seq ?? UniversalSequenceNumber;
107
+ const seg = overwriteInfo<IInsertionInfo>(this.client.specToSegment(spec.json), {
108
+ clientId:
109
+ spec.client === undefined
110
+ ? NonCollabClient
111
+ : this.client.getOrAddShortClientId(spec.client),
112
+ seq: spec.seq ?? UniversalSequenceNumber,
113
+ });
113
114
 
114
115
  if (spec.removedSeq !== undefined) {
115
- seg.removedSeq = spec.removedSeq;
116
+ // this format had a bug where it didn't store all the overlap clients
117
+ // this is for back compat, so we change the singular id to an array
118
+ // this will only cause problems if there is an overlapping delete
119
+ // spanning the snapshot, which should be rare
120
+ const specAsBuggyFormat: IJSONSegmentWithMergeInfo & { removedClient?: string } = spec;
121
+ if (specAsBuggyFormat.removedClient !== undefined) {
122
+ spec.removedClientIds ??= [specAsBuggyFormat.removedClient];
123
+ }
124
+ assert(spec.removedClientIds !== undefined, 0xaac /* must have removedClient ids */);
125
+ overwriteInfo<IRemovalInfo>(seg, {
126
+ removedSeq: spec.removedSeq,
127
+ removedClientIds: spec.removedClientIds.map((id) =>
128
+ this.client.getOrAddShortClientId(id),
129
+ ),
130
+ });
116
131
  }
117
132
  if (spec.movedSeq !== undefined) {
118
- seg.movedSeq = spec.movedSeq;
119
- }
120
- if (spec.movedSeqs !== undefined) {
121
- seg.movedSeqs = spec.movedSeqs;
122
- }
123
- // this format had a bug where it didn't store all the overlap clients
124
- // this is for back compat, so we change the singular id to an array
125
- // this will only cause problems if there is an overlapping delete
126
- // spanning the snapshot, which should be rare
127
- const specAsBuggyFormat: IJSONSegmentWithMergeInfo & { removedClient?: string } = spec;
128
- if (specAsBuggyFormat.removedClient !== undefined) {
129
- seg.removedClientIds = [
130
- this.client.getOrAddShortClientId(specAsBuggyFormat.removedClient),
131
- ];
132
- }
133
- if (spec.removedClientIds !== undefined) {
134
- seg.removedClientIds = spec.removedClientIds?.map((sid) =>
135
- this.client.getOrAddShortClientId(sid),
136
- );
137
- }
138
- if (spec.movedClientIds !== undefined) {
139
- seg.movedClientIds = spec.movedClientIds?.map((sid) =>
140
- this.client.getOrAddShortClientId(sid),
133
+ assert(
134
+ spec.movedClientIds !== undefined && spec.movedSeqs !== undefined,
135
+ 0xaa5 /* must have movedIds ids */,
141
136
  );
137
+ overwriteInfo<IMoveInfo>(seg, {
138
+ movedSeq: spec.movedSeq,
139
+ movedSeqs: spec.movedSeqs,
140
+ movedClientIds: spec.movedClientIds.map((id) =>
141
+ this.client.getOrAddShortClientId(id),
142
+ ),
143
+ // BUG? This isn't persisted
144
+ wasMovedOnInsert: false,
145
+ });
142
146
  }
143
- } else {
144
- seg = this.client.specToSegment(spec);
145
- seg.seq = UniversalSequenceNumber;
146
147
 
147
- // `specToSegment()` initializes `seg` with the LocalClientId. We must overwrite this with
148
- // `NonCollabClient`.
149
- seg.clientId = NonCollabClient;
148
+ return seg;
150
149
  }
151
-
152
- return seg;
150
+ return overwriteInfo(this.client.specToSegment(spec), {
151
+ seq: UniversalSequenceNumber,
152
+ clientId: NonCollabClient,
153
+ });
153
154
  };
154
155
 
155
156
  private loadHeader(header: string): MergeTreeChunkV1 {
@@ -207,7 +208,7 @@ export class SnapshotLoader {
207
208
  }
208
209
 
209
210
  let chunksWithAttribution = chunk1.attribution === undefined ? 0 : 1;
210
- const segs: ISegmentLeaf[] = [];
211
+ const segs: SegmentWithInfo<IInsertionInfo>[] = [];
211
212
  let lengthSofar = chunk1.length;
212
213
  for (
213
214
  let chunkIndex = 1;
@@ -244,7 +245,7 @@ export class SnapshotLoader {
244
245
 
245
246
  // Helper to insert segments at the end of the MergeTree.
246
247
  const mergeTree = this.mergeTree;
247
- const append = (segments: ISegmentLeaf[], cli: number, seq: number): void => {
248
+ const append = (segments: ISegmentPrivate[], cli: number, seq: number): void => {
248
249
  mergeTree.insertSegments(
249
250
  mergeTree.root.cachedLength ?? 0,
250
251
  segments,
@@ -256,7 +257,7 @@ export class SnapshotLoader {
256
257
  };
257
258
 
258
259
  // Helpers to batch-insert segments that are below the min seq
259
- const batch: ISegmentLeaf[] = [];
260
+ const batch: SegmentWithInfo<IInsertionInfo>[] = [];
260
261
  const flushBatch = (): void => {
261
262
  if (batch.length > 0) {
262
263
  append(batch, NonCollabClient, UniversalSequenceNumber);
@@ -264,23 +265,21 @@ export class SnapshotLoader {
264
265
  };
265
266
 
266
267
  for (const seg of segs) {
267
- const cli = seg.clientId;
268
- const seq = seg.seq;
269
-
268
+ const { clientId, seq } = seg;
270
269
  // If the segment can be batch inserted, add it to the 'batch' array. Otherwise, flush
271
270
  // any batched segments and then insert the current segment individually.
272
- if (cli === NonCollabClient && seq === UniversalSequenceNumber) {
271
+ if (clientId === NonCollabClient && seq === UniversalSequenceNumber) {
273
272
  batch.push(seg);
274
273
  } else {
275
274
  flushBatch();
276
- append([seg], cli, seq!);
275
+ append([seg], clientId, seq);
277
276
  }
278
277
  }
279
278
 
280
279
  flushBatch();
281
280
  }
282
281
 
283
- private extractAttribution(segments: ISegmentLeaf[], chunk: MergeTreeChunkV1): void {
282
+ private extractAttribution(segments: ISegmentPrivate[], chunk: MergeTreeChunkV1): void {
284
283
  if (chunk.attribution) {
285
284
  const { attributionPolicy } = this.mergeTree;
286
285
  if (attributionPolicy === undefined) {
package/src/snapshotV1.ts CHANGED
@@ -22,9 +22,10 @@ import { IAttributionCollection } from "./attributionCollection.js";
22
22
  import { UnassignedSequenceNumber } from "./constants.js";
23
23
  import { MergeTree } from "./mergeTree.js";
24
24
  import { walkAllChildSegments } from "./mergeTreeNodeWalk.js";
25
- import { ISegmentLeaf } from "./mergeTreeNodes.js";
25
+ import { ISegmentPrivate } from "./mergeTreeNodes.js";
26
26
  import type { IJSONSegment } from "./ops.js";
27
27
  import { PropertySet, matchProperties } from "./properties.js";
28
+ import { assertInserted, isMoved, isRemoved } from "./segmentInfos.js";
28
29
  import {
29
30
  IJSONSegmentWithMergeInfo,
30
31
  JsonSegmentSpecs,
@@ -208,7 +209,7 @@ export class SnapshotV1 {
208
209
  };
209
210
 
210
211
  // Helper to serialize the given `segment` and add it to the snapshot (if a segment is provided).
211
- const pushSeg = (segment?: ISegmentLeaf): void => {
212
+ const pushSeg = (segment?: ISegmentPrivate): void => {
212
213
  if (segment) {
213
214
  if (segment.properties !== undefined && Object.keys(segment.properties).length === 0) {
214
215
  segment.properties = undefined;
@@ -221,8 +222,9 @@ export class SnapshotV1 {
221
222
  }
222
223
  };
223
224
 
224
- let prev: ISegmentLeaf | undefined;
225
- const extractSegment = (segment: ISegmentLeaf): boolean => {
225
+ let prev: ISegmentPrivate | undefined;
226
+ const extractSegment = (segment: ISegmentPrivate): boolean => {
227
+ assertInserted(segment);
226
228
  // Elide segments that do not need to be included in the snapshot. A segment may be elided if
227
229
  // either condition is true:
228
230
  // a) The segment has not yet been ACKed. We do not need to snapshot unACKed segments because
@@ -231,10 +233,8 @@ export class SnapshotV1 {
231
233
  // segment, and therefore we can discard it.
232
234
  if (
233
235
  segment.seq === UnassignedSequenceNumber ||
234
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
235
- segment.removedSeq! <= minSeq ||
236
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
237
- segment.movedSeq! <= minSeq
236
+ (isRemoved(segment) && segment.removedSeq <= minSeq) ||
237
+ (isMoved(segment) && segment.movedSeq <= minSeq)
238
238
  ) {
239
239
  if (segment.seq !== UnassignedSequenceNumber) {
240
240
  originalSegments += 1;
@@ -248,11 +248,10 @@ export class SnapshotV1 {
248
248
  // (seq, client, etc.) This information is only needed if the segment is above the MSN (and doesn't
249
249
  // have a pending remove.)
250
250
  if (
251
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
252
- segment.seq! <= minSeq && // Segment is below the MSN, and...
253
- (segment.removedSeq === undefined || // .. Segment has not been removed, or...
251
+ segment.seq <= minSeq && // Segment is below the MSN, and...
252
+ (!isRemoved(segment) || // .. Segment has not been removed, or...
254
253
  segment.removedSeq === UnassignedSequenceNumber) && // .. Removal op to be delivered on reconnect
255
- (segment.movedSeq === undefined || segment.movedSeq === UnassignedSequenceNumber)
254
+ (!isMoved(segment) || segment.movedSeq === UnassignedSequenceNumber)
256
255
  ) {
257
256
  // This segment is below the MSN, which means that future ops will not reference it. Attempt to
258
257
  // coalesce the new segment with the previous (if any).
@@ -286,14 +285,13 @@ export class SnapshotV1 {
286
285
  json: segment.toJSONObject() as IJSONSegment,
287
286
  };
288
287
  // If the segment insertion is above the MSN, record the insertion merge info.
289
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
290
- if (segment.seq! > minSeq) {
288
+ if (segment.seq > minSeq) {
291
289
  raw.seq = segment.seq;
292
290
  raw.client = this.getLongClientId(segment.clientId);
293
291
  }
294
292
  // We have already dispensed with removed segments below the MSN and removed segments with unassigned
295
293
  // sequence numbers. Any remaining removal info should be preserved.
296
- if (segment.removedSeq !== undefined) {
294
+ if (isRemoved(segment)) {
297
295
  assert(
298
296
  segment.removedSeq !== UnassignedSequenceNumber && segment.removedSeq > minSeq,
299
297
  0x065 /* "On removal info preservation, segment has invalid removed sequence number!" */,
@@ -311,7 +309,7 @@ export class SnapshotV1 {
311
309
  );
312
310
  }
313
311
 
314
- if (segment.movedSeq !== undefined) {
312
+ if (isMoved(segment)) {
315
313
  assert(
316
314
  segment.movedSeq !== UnassignedSequenceNumber && segment.movedSeq > minSeq,
317
315
  0x873 /* On move info preservation, segment has invalid moved sequence number! */,