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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/.mocharc.js +12 -0
  2. package/README.md +2 -2
  3. package/dist/MergeTreeTextHelper.d.ts +23 -0
  4. package/dist/MergeTreeTextHelper.d.ts.map +1 -0
  5. package/dist/MergeTreeTextHelper.js +133 -0
  6. package/dist/MergeTreeTextHelper.js.map +1 -0
  7. package/dist/base.d.ts +2 -26
  8. package/dist/base.d.ts.map +1 -1
  9. package/dist/base.js.map +1 -1
  10. package/dist/client.d.ts +27 -16
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +81 -101
  13. package/dist/client.js.map +1 -1
  14. package/dist/collections/heap.d.ts +28 -0
  15. package/dist/collections/heap.d.ts.map +1 -0
  16. package/dist/collections/heap.js +65 -0
  17. package/dist/collections/heap.js.map +1 -0
  18. package/dist/collections/index.d.ts +11 -0
  19. package/dist/collections/index.d.ts.map +1 -0
  20. package/dist/collections/index.js +23 -0
  21. package/dist/collections/index.js.map +1 -0
  22. package/dist/collections/intervalTree.d.ts +60 -0
  23. package/dist/collections/intervalTree.d.ts.map +1 -0
  24. package/dist/collections/intervalTree.js +99 -0
  25. package/dist/collections/intervalTree.js.map +1 -0
  26. package/dist/collections/list.d.ts +39 -0
  27. package/dist/collections/list.d.ts.map +1 -0
  28. package/dist/collections/list.js +155 -0
  29. package/dist/collections/list.js.map +1 -0
  30. package/dist/collections/rbTree.d.ts +154 -0
  31. package/dist/collections/rbTree.d.ts.map +1 -0
  32. package/dist/{collections.js → collections/rbTree.js} +15 -478
  33. package/dist/collections/rbTree.js.map +1 -0
  34. package/dist/collections/stack.d.ts +16 -0
  35. package/dist/collections/stack.d.ts.map +1 -0
  36. package/dist/collections/stack.js +30 -0
  37. package/dist/collections/stack.js.map +1 -0
  38. package/dist/collections/tst.d.ts +55 -0
  39. package/dist/collections/tst.d.ts.map +1 -0
  40. package/dist/collections/tst.js +171 -0
  41. package/dist/collections/tst.js.map +1 -0
  42. package/dist/index.d.ts +3 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +4 -2
  45. package/dist/index.js.map +1 -1
  46. package/dist/localReference.d.ts +48 -99
  47. package/dist/localReference.d.ts.map +1 -1
  48. package/dist/localReference.js +132 -169
  49. package/dist/localReference.js.map +1 -1
  50. package/dist/mergeTree.d.ts +71 -302
  51. package/dist/mergeTree.d.ts.map +1 -1
  52. package/dist/mergeTree.js +395 -642
  53. package/dist/mergeTree.js.map +1 -1
  54. package/dist/mergeTreeDeltaCallback.d.ts +1 -1
  55. package/dist/mergeTreeDeltaCallback.d.ts.map +1 -1
  56. package/dist/mergeTreeDeltaCallback.js.map +1 -1
  57. package/dist/mergeTreeNodes.d.ts +344 -0
  58. package/dist/mergeTreeNodes.d.ts.map +1 -0
  59. package/dist/mergeTreeNodes.js +383 -0
  60. package/dist/mergeTreeNodes.js.map +1 -0
  61. package/dist/mergeTreeTracking.d.ts +1 -1
  62. package/dist/mergeTreeTracking.d.ts.map +1 -1
  63. package/dist/mergeTreeTracking.js.map +1 -1
  64. package/dist/opBuilder.d.ts +1 -1
  65. package/dist/opBuilder.d.ts.map +1 -1
  66. package/dist/opBuilder.js.map +1 -1
  67. package/dist/partialLengths.d.ts +188 -18
  68. package/dist/partialLengths.d.ts.map +1 -1
  69. package/dist/partialLengths.js +495 -253
  70. package/dist/partialLengths.js.map +1 -1
  71. package/dist/properties.d.ts.map +1 -1
  72. package/dist/properties.js.map +1 -1
  73. package/dist/referencePositions.d.ts +6 -26
  74. package/dist/referencePositions.d.ts.map +1 -1
  75. package/dist/referencePositions.js +3 -20
  76. package/dist/referencePositions.js.map +1 -1
  77. package/dist/segmentGroupCollection.d.ts +3 -1
  78. package/dist/segmentGroupCollection.d.ts.map +1 -1
  79. package/dist/segmentGroupCollection.js +14 -1
  80. package/dist/segmentGroupCollection.js.map +1 -1
  81. package/dist/segmentPropertiesManager.d.ts +10 -1
  82. package/dist/segmentPropertiesManager.d.ts.map +1 -1
  83. package/dist/segmentPropertiesManager.js +42 -13
  84. package/dist/segmentPropertiesManager.js.map +1 -1
  85. package/dist/snapshotChunks.d.ts +2 -1
  86. package/dist/snapshotChunks.d.ts.map +1 -1
  87. package/dist/snapshotChunks.js.map +1 -1
  88. package/dist/snapshotLoader.d.ts.map +1 -1
  89. package/dist/snapshotLoader.js.map +1 -1
  90. package/dist/snapshotV1.d.ts +1 -1
  91. package/dist/snapshotV1.d.ts.map +1 -1
  92. package/dist/snapshotV1.js +1 -1
  93. package/dist/snapshotV1.js.map +1 -1
  94. package/dist/snapshotlegacy.d.ts +5 -1
  95. package/dist/snapshotlegacy.d.ts.map +1 -1
  96. package/dist/snapshotlegacy.js +4 -0
  97. package/dist/snapshotlegacy.js.map +1 -1
  98. package/dist/sortedSegmentSet.d.ts +1 -1
  99. package/dist/sortedSegmentSet.d.ts.map +1 -1
  100. package/dist/sortedSegmentSet.js.map +1 -1
  101. package/dist/textSegment.d.ts +7 -7
  102. package/dist/textSegment.d.ts.map +1 -1
  103. package/dist/textSegment.js +3 -125
  104. package/dist/textSegment.js.map +1 -1
  105. package/{DEV.md → docs/DEV.md} +2 -2
  106. package/docs/Obliterate.md +639 -0
  107. package/{REFERENCEPOSITIONS.md → docs/REFERENCEPOSITIONS.md} +2 -2
  108. package/lib/MergeTreeTextHelper.d.ts +23 -0
  109. package/lib/MergeTreeTextHelper.d.ts.map +1 -0
  110. package/lib/MergeTreeTextHelper.js +129 -0
  111. package/lib/MergeTreeTextHelper.js.map +1 -0
  112. package/lib/base.d.ts +2 -26
  113. package/lib/base.d.ts.map +1 -1
  114. package/lib/base.js.map +1 -1
  115. package/lib/client.d.ts +27 -16
  116. package/lib/client.d.ts.map +1 -1
  117. package/lib/client.js +79 -99
  118. package/lib/client.js.map +1 -1
  119. package/lib/collections/heap.d.ts +28 -0
  120. package/lib/collections/heap.d.ts.map +1 -0
  121. package/lib/collections/heap.js +61 -0
  122. package/lib/collections/heap.js.map +1 -0
  123. package/lib/collections/index.d.ts +11 -0
  124. package/lib/collections/index.d.ts.map +1 -0
  125. package/lib/collections/index.js +11 -0
  126. package/lib/collections/index.js.map +1 -0
  127. package/lib/collections/intervalTree.d.ts +60 -0
  128. package/lib/collections/intervalTree.d.ts.map +1 -0
  129. package/lib/collections/intervalTree.js +94 -0
  130. package/lib/collections/intervalTree.js.map +1 -0
  131. package/lib/collections/list.d.ts +39 -0
  132. package/lib/collections/list.d.ts.map +1 -0
  133. package/lib/collections/list.js +149 -0
  134. package/lib/collections/list.js.map +1 -0
  135. package/lib/collections/rbTree.d.ts +154 -0
  136. package/lib/collections/rbTree.d.ts.map +1 -0
  137. package/lib/{collections.js → collections/rbTree.js} +14 -469
  138. package/lib/collections/rbTree.js.map +1 -0
  139. package/lib/collections/stack.d.ts +16 -0
  140. package/lib/collections/stack.d.ts.map +1 -0
  141. package/lib/collections/stack.js +26 -0
  142. package/lib/collections/stack.js.map +1 -0
  143. package/lib/collections/tst.d.ts +55 -0
  144. package/lib/collections/tst.d.ts.map +1 -0
  145. package/lib/collections/tst.js +167 -0
  146. package/lib/collections/tst.js.map +1 -0
  147. package/lib/index.d.ts +3 -1
  148. package/lib/index.d.ts.map +1 -1
  149. package/lib/index.js +3 -1
  150. package/lib/index.js.map +1 -1
  151. package/lib/localReference.d.ts +48 -99
  152. package/lib/localReference.d.ts.map +1 -1
  153. package/lib/localReference.js +132 -170
  154. package/lib/localReference.js.map +1 -1
  155. package/lib/mergeTree.d.ts +71 -302
  156. package/lib/mergeTree.d.ts.map +1 -1
  157. package/lib/mergeTree.js +371 -607
  158. package/lib/mergeTree.js.map +1 -1
  159. package/lib/mergeTreeDeltaCallback.d.ts +1 -1
  160. package/lib/mergeTreeDeltaCallback.d.ts.map +1 -1
  161. package/lib/mergeTreeDeltaCallback.js.map +1 -1
  162. package/lib/mergeTreeNodes.d.ts +344 -0
  163. package/lib/mergeTreeNodes.d.ts.map +1 -0
  164. package/lib/mergeTreeNodes.js +369 -0
  165. package/lib/mergeTreeNodes.js.map +1 -0
  166. package/lib/mergeTreeTracking.d.ts +1 -1
  167. package/lib/mergeTreeTracking.d.ts.map +1 -1
  168. package/lib/mergeTreeTracking.js.map +1 -1
  169. package/lib/opBuilder.d.ts +1 -1
  170. package/lib/opBuilder.d.ts.map +1 -1
  171. package/lib/opBuilder.js.map +1 -1
  172. package/lib/partialLengths.d.ts +188 -18
  173. package/lib/partialLengths.d.ts.map +1 -1
  174. package/lib/partialLengths.js +491 -249
  175. package/lib/partialLengths.js.map +1 -1
  176. package/lib/properties.d.ts.map +1 -1
  177. package/lib/properties.js.map +1 -1
  178. package/lib/referencePositions.d.ts +6 -26
  179. package/lib/referencePositions.d.ts.map +1 -1
  180. package/lib/referencePositions.js +3 -20
  181. package/lib/referencePositions.js.map +1 -1
  182. package/lib/segmentGroupCollection.d.ts +3 -1
  183. package/lib/segmentGroupCollection.d.ts.map +1 -1
  184. package/lib/segmentGroupCollection.js +14 -1
  185. package/lib/segmentGroupCollection.js.map +1 -1
  186. package/lib/segmentPropertiesManager.d.ts +10 -1
  187. package/lib/segmentPropertiesManager.d.ts.map +1 -1
  188. package/lib/segmentPropertiesManager.js +42 -13
  189. package/lib/segmentPropertiesManager.js.map +1 -1
  190. package/lib/snapshotChunks.d.ts +2 -1
  191. package/lib/snapshotChunks.d.ts.map +1 -1
  192. package/lib/snapshotChunks.js.map +1 -1
  193. package/lib/snapshotLoader.d.ts.map +1 -1
  194. package/lib/snapshotLoader.js.map +1 -1
  195. package/lib/snapshotV1.d.ts +1 -1
  196. package/lib/snapshotV1.d.ts.map +1 -1
  197. package/lib/snapshotV1.js +1 -1
  198. package/lib/snapshotV1.js.map +1 -1
  199. package/lib/snapshotlegacy.d.ts +5 -1
  200. package/lib/snapshotlegacy.d.ts.map +1 -1
  201. package/lib/snapshotlegacy.js +4 -0
  202. package/lib/snapshotlegacy.js.map +1 -1
  203. package/lib/sortedSegmentSet.d.ts +1 -1
  204. package/lib/sortedSegmentSet.d.ts.map +1 -1
  205. package/lib/sortedSegmentSet.js.map +1 -1
  206. package/lib/textSegment.d.ts +7 -7
  207. package/lib/textSegment.d.ts.map +1 -1
  208. package/lib/textSegment.js +1 -122
  209. package/lib/textSegment.js.map +1 -1
  210. package/package.json +99 -20
  211. package/src/MergeTreeTextHelper.ts +170 -0
  212. package/src/base.ts +2 -35
  213. package/src/client.ts +91 -111
  214. package/src/collections/heap.ts +75 -0
  215. package/src/collections/index.ts +11 -0
  216. package/src/collections/intervalTree.ts +146 -0
  217. package/src/collections/list.ts +165 -0
  218. package/src/{collections.ts → collections/rbTree.ts} +84 -563
  219. package/src/collections/stack.ts +27 -0
  220. package/src/collections/tst.ts +212 -0
  221. package/src/index.ts +8 -2
  222. package/src/localReference.ts +152 -203
  223. package/src/mergeTree.ts +578 -996
  224. package/src/mergeTreeDeltaCallback.ts +1 -1
  225. package/src/mergeTreeNodes.ts +752 -0
  226. package/src/mergeTreeTracking.ts +1 -1
  227. package/src/opBuilder.ts +1 -1
  228. package/src/partialLengths.ts +631 -258
  229. package/src/properties.ts +1 -0
  230. package/src/referencePositions.ts +10 -44
  231. package/src/segmentGroupCollection.ts +17 -2
  232. package/src/segmentPropertiesManager.ts +46 -12
  233. package/src/snapshotChunks.ts +2 -1
  234. package/src/snapshotLoader.ts +2 -1
  235. package/src/snapshotV1.ts +3 -3
  236. package/src/snapshotlegacy.ts +6 -2
  237. package/src/sortedSegmentSet.ts +1 -1
  238. package/src/textSegment.ts +10 -157
  239. package/dist/collections.d.ts +0 -197
  240. package/dist/collections.d.ts.map +0 -1
  241. package/dist/collections.js.map +0 -1
  242. package/lib/collections.d.ts +0 -197
  243. package/lib/collections.d.ts.map +0 -1
  244. package/lib/collections.js.map +0 -1
package/src/mergeTree.ts CHANGED
@@ -24,7 +24,35 @@ import {
24
24
  UnassignedSequenceNumber,
25
25
  UniversalSequenceNumber,
26
26
  } from "./constants";
27
- import { LocalReference, LocalReferenceCollection, LocalReferencePosition } from "./localReference";
27
+ import {
28
+ assertLocalReferences,
29
+ LocalReferenceCollection,
30
+ LocalReferencePosition,
31
+ } from "./localReference";
32
+ import {
33
+ BaseSegment,
34
+ BlockUpdateActions,
35
+ CollaborationWindow,
36
+ IHierBlock,
37
+ IMergeBlock,
38
+ IMergeNode,
39
+ IncrementalExecOp,
40
+ IncrementalMapState,
41
+ InsertContext,
42
+ internedSpaces,
43
+ ISegment,
44
+ ISegmentAction,
45
+ ISegmentChanges,
46
+ Marker,
47
+ MaxNodesInBlock,
48
+ MergeBlock,
49
+ MergeNode,
50
+ MergeTreeStats,
51
+ MinListener,
52
+ SegmentActions,
53
+ SegmentGroup,
54
+ toRemovalInfo,
55
+ } from "./mergeTreeNodes";
28
56
  import {
29
57
  IMergeTreeDeltaOpArgs,
30
58
  IMergeTreeSegmentDelta,
@@ -32,83 +60,38 @@ import {
32
60
  MergeTreeMaintenanceCallback,
33
61
  MergeTreeMaintenanceType,
34
62
  } from "./mergeTreeDeltaCallback";
35
- import { TrackingGroupCollection } from "./mergeTreeTracking";
63
+ import { createAnnotateRangeOp, createInsertSegmentOp, createRemoveRangeOp } from "./opBuilder";
36
64
  import {
37
65
  ICombiningOp,
38
- IJSONSegment,
39
- IMarkerDef,
66
+ IMergeTreeDeltaOp,
40
67
  IRelativePosition,
41
68
  MergeTreeDeltaType,
42
69
  ReferenceType,
43
70
  } from "./ops";
44
71
  import { PartialSequenceLengths } from "./partialLengths";
45
72
  import {
46
- clone,
47
73
  createMap,
48
74
  extend,
49
- extendIfUndefined,
50
75
  MapLike,
51
76
  matchProperties,
52
77
  PropertySet,
53
78
  } from "./properties";
54
79
  import {
55
80
  refTypeIncludesFlag,
56
- RangeStackMap,
57
81
  ReferencePosition,
58
- refGetRangeLabels,
59
- refGetTileLabels,
60
- refHasRangeLabel,
61
- refHasRangeLabels,
62
- refHasTileLabel,
63
- refHasTileLabels,
82
+ DetachedReferencePosition,
83
+ RangeStackMap,
84
+ refHasRangeLabel,
85
+ refGetRangeLabels,
86
+ refGetTileLabels,
87
+ refHasTileLabel,
64
88
  } from "./referencePositions";
65
- import { SegmentGroupCollection } from "./segmentGroupCollection";
66
- import { PropertiesManager } from "./segmentPropertiesManager";
67
- import { Client } from "./client";
68
-
69
- export interface IMergeNodeCommon {
70
- parent?: IMergeBlock;
71
- /**
72
- * The length of the contents of the node.
73
- */
74
- cachedLength: number;
75
- index: number;
76
- ordinal: string;
77
- isLeaf(): this is ISegment;
78
- }
79
-
80
- export type IMergeNode = IMergeBlock | ISegment;
81
-
82
- // Node with segments as children
83
- export interface IMergeBlock extends IMergeNodeCommon {
84
- needsScour?: boolean;
85
- childCount: number;
86
- children: IMergeNode[];
87
- partialLengths?: PartialSequenceLengths;
88
- hierBlock(): IHierBlock | undefined;
89
- assignChild(child: IMergeNode, index: number, updateOrdinal?: boolean): void;
90
- setOrdinal(child: IMergeNode, index: number): void;
91
- }
92
-
93
- export interface IHierBlock extends IMergeBlock {
94
- hierToString(indentCount: number): string;
95
- addNodeReferences(mergeTree: MergeTree, node: IMergeNode): void;
96
- rightmostTiles: MapLike<ReferencePosition>;
97
- leftmostTiles: MapLike<ReferencePosition>;
98
- rangeStacks: RangeStackMap;
99
- }
89
+ import { PropertiesRollback } from "./segmentPropertiesManager";
100
90
 
101
- export interface IRemovalInfo {
102
- removedSeq: number;
103
- removedClientIds: number[];
104
- }
105
- export function toRemovalInfo(maybe: Partial<IRemovalInfo> | undefined): IRemovalInfo | undefined {
106
- if (maybe?.removedClientIds !== undefined && maybe?.removedSeq !== undefined) {
107
- return maybe as IRemovalInfo;
108
- }
109
- assert(maybe?.removedClientIds === undefined && maybe?.removedSeq === undefined,
110
- 0x2bf /* "both removedClientIds and removedSeq should be set or not set" */);
111
- }
91
+ const minListenerComparer: Comparer<MinListener> = {
92
+ min: { minRequired: Number.MIN_VALUE, onMinGE: () => { assert(false, 0x048 /* "onMinGE()" */); } },
93
+ compare: (a, b) => a.minRequired - b.minRequired,
94
+ };
112
95
 
113
96
  function isRemoved(segment: ISegment): boolean {
114
97
  return toRemovalInfo(segment) !== undefined;
@@ -119,153 +102,111 @@ function isRemovedAndAcked(segment: ISegment): boolean {
119
102
  return removalInfo !== undefined && removalInfo.removedSeq !== UnassignedSequenceNumber;
120
103
  }
121
104
 
122
- /**
123
- * A segment representing a portion of the merge tree.
124
- */
125
- export interface ISegment extends IMergeNodeCommon, Partial<IRemovalInfo> {
126
- readonly type: string;
127
- readonly segmentGroups: SegmentGroupCollection;
128
- readonly trackingCollection: TrackingGroupCollection;
129
- propertyManager?: PropertiesManager;
130
- localSeq?: number;
131
- localRemovedSeq?: number;
132
- seq?: number; // If not present assumed to be previous to window min
133
- clientId: number;
134
- localRefs?: LocalReferenceCollection;
135
- properties?: PropertySet;
136
- addProperties(
137
- newProps: PropertySet,
138
- op?: ICombiningOp,
139
- seq?: number,
140
- collabWindow?: CollaborationWindow,
141
- ): PropertySet | undefined;
142
- clone(): ISegment;
143
- canAppend(segment: ISegment): boolean;
144
- append(segment: ISegment): void;
145
- splitAt(pos: number): ISegment | undefined;
146
- toJSONObject(): any;
147
- /**
148
- * Acks the current segment against the segment group, op, and merge tree.
149
- *
150
- * Throws error if the segment state doesn't match segment group or op.
151
- * E.g. Segment group not first is pending queue.
152
- * Inserted segment does not have unassigned sequence number.
153
- *
154
- * Returns true if the op modifies the segment, otherwise false.
155
- * The only current false case is overlapping remove, where a segment is removed
156
- * by a previously sequenced operation before the current operation is acked.
157
- */
158
- ack(segmentGroup: SegmentGroup, opArgs: IMergeTreeDeltaOpArgs, mergeTree: MergeTree): boolean;
159
- }
160
-
161
- export interface IMarkerModifiedAction {
162
- // eslint-disable-next-line @typescript-eslint/prefer-function-type
163
- (marker: Marker): void;
164
- }
165
-
166
- export interface ISegmentAction<TClientData> {
167
- // eslint-disable-next-line @typescript-eslint/prefer-function-type
168
- (segment: ISegment, pos: number, refSeq: number, clientId: number, start: number,
169
- end: number, accum: TClientData): boolean;
170
- }
171
-
172
- export interface ISegmentChanges {
173
- next?: ISegment;
174
- replaceCurrent?: ISegment;
175
- }
176
-
177
- export interface BlockAction<TClientData> {
178
- // eslint-disable-next-line @typescript-eslint/prefer-function-type
179
- (
180
- block: IMergeBlock,
181
- pos: number,
182
- refSeq: number,
183
- clientId: number,
184
- start: number | undefined,
185
- end: number | undefined,
186
- accum: TClientData,
187
- ): boolean;
188
- }
189
-
190
- export interface NodeAction<TClientData> {
191
- // eslint-disable-next-line @typescript-eslint/prefer-function-type
192
- (
193
- node: IMergeNode,
194
- pos: number,
195
- refSeq: number,
196
- clientId: number,
197
- start: number | undefined,
198
- end: number | undefined,
199
- clientData: TClientData,
200
- ): boolean;
201
- }
202
-
203
- export interface IncrementalSegmentAction<TContext> {
204
- (segment: ISegment, state: IncrementalMapState<TContext>);
205
- }
206
-
207
- export interface IncrementalBlockAction<TContext> {
208
- (state: IncrementalMapState<TContext>);
105
+ function nodeTotalLength(mergeTree: MergeTree, node: IMergeNode) {
106
+ if (!node.isLeaf()) {
107
+ return node.cachedLength;
108
+ }
109
+ return mergeTree.localNetLength(node);
209
110
  }
210
111
 
211
- export interface BlockUpdateActions {
212
- child: (block: IMergeBlock, index: number) => void;
213
- }
112
+ const LRUSegmentComparer: Comparer<LRUSegment> = {
113
+ min: { maxSeq: -2 },
114
+ compare: (a, b) => a.maxSeq - b.maxSeq,
115
+ };
214
116
 
215
- export interface InsertContext {
216
- candidateSegment?: ISegment;
217
- prepareEvents?: boolean;
218
- structureChange?: boolean;
219
- leaf: (segment: ISegment | undefined, pos: number, ic: InsertContext) => ISegmentChanges;
220
- continuePredicate?: (continueFromBlock: IMergeBlock) => boolean;
117
+ interface IReferenceSearchInfo {
118
+ mergeTree: MergeTree;
119
+ tileLabel: string;
120
+ tilePrecedesPos?: boolean;
121
+ tile?: ReferencePosition;
221
122
  }
222
123
 
223
- export interface SegmentActions<TClientData> {
224
- leaf?: ISegmentAction<TClientData>;
225
- shift?: NodeAction<TClientData>;
226
- contains?: NodeAction<TClientData>;
227
- pre?: BlockAction<TClientData>;
228
- post?: BlockAction<TClientData>;
124
+ interface IMarkerSearchRangeInfo {
125
+ mergeTree: MergeTree;
126
+ rangeLabels: string[];
127
+ stacks: RangeStackMap;
229
128
  }
230
129
 
231
- export interface IncrementalSegmentActions<TContext> {
232
- leaf: IncrementalSegmentAction<TContext>;
233
- pre?: IncrementalBlockAction<TContext>;
234
- post?: IncrementalBlockAction<TContext>;
130
+ function applyLeafRangeMarker(marker: Marker, searchInfo: IMarkerSearchRangeInfo) {
131
+ for (const rangeLabel of searchInfo.rangeLabels) {
132
+ if (refHasRangeLabel(marker, rangeLabel)) {
133
+ let currentStack = searchInfo.stacks[rangeLabel];
134
+ if (currentStack === undefined) {
135
+ currentStack = new Stack<Marker>();
136
+ searchInfo.stacks[rangeLabel] = currentStack;
137
+ }
138
+ applyRangeReference(currentStack, marker);
139
+ }
140
+ }
235
141
  }
236
142
 
237
- export interface SearchResult {
238
- text: string;
239
- pos: number;
143
+ function recordRangeLeaf(
144
+ segment: ISegment, segpos: number,
145
+ refSeq: number, clientId: number, start: number | undefined, end: number | undefined,
146
+ searchInfo: IMarkerSearchRangeInfo) {
147
+ if (Marker.is(segment)) {
148
+ if (segment.refType &
149
+ (ReferenceType.NestBegin | ReferenceType.NestEnd)) {
150
+ applyLeafRangeMarker(segment, searchInfo);
151
+ }
152
+ }
153
+ return false;
240
154
  }
241
155
 
242
- export interface MergeTreeStats {
243
- maxHeight: number;
244
- nodeCount: number;
245
- leafCount: number;
246
- removedLeafCount: number;
247
- liveCount: number;
248
- histo: number[];
249
- windowTime?: number;
250
- packTime?: number;
251
- ordTime?: number;
252
- maxOrdTime?: number;
156
+ function rangeShift(
157
+ node: IMergeNode, segpos: number, refSeq: number, clientId: number,
158
+ offset: number | undefined, end: number | undefined, searchInfo: IMarkerSearchRangeInfo) {
159
+ if (node.isLeaf()) {
160
+ const seg = node;
161
+ if (((searchInfo.mergeTree.localNetLength(seg) ?? 0) > 0) && Marker.is(seg)) {
162
+ if (seg.refType &
163
+ (ReferenceType.NestBegin | ReferenceType.NestEnd)) {
164
+ applyLeafRangeMarker(seg, searchInfo);
165
+ }
166
+ }
167
+ } else {
168
+ const block = <IHierBlock>node;
169
+ applyStackDelta(searchInfo.stacks, block.rangeStacks);
170
+ }
171
+ return true;
253
172
  }
254
173
 
255
- export interface SegmentGroup {
256
- segments: ISegment[];
257
- localSeq: number;
174
+ function recordTileStart(
175
+ segment: ISegment,
176
+ segpos: number,
177
+ refSeq: number,
178
+ clientId: number,
179
+ start: number,
180
+ end: number,
181
+ searchInfo: IReferenceSearchInfo) {
182
+ if (Marker.is(segment)) {
183
+ if (refHasTileLabel(segment, searchInfo.tileLabel)) {
184
+ searchInfo.tile = segment;
185
+ }
186
+ }
187
+ return false;
258
188
  }
259
189
 
260
- export class MergeNode implements IMergeNodeCommon {
261
- index: number = 0;
262
- ordinal: string = "";
263
- parent?: IMergeBlock;
264
- cachedLength: number = 0;
265
-
266
- isLeaf() {
267
- return false;
190
+ function tileShift(
191
+ node: IMergeNode, segpos: number, refSeq: number, clientId: number,
192
+ offset: number | undefined, end: number | undefined, searchInfo: IReferenceSearchInfo) {
193
+ if (node.isLeaf()) {
194
+ const seg = node;
195
+ if ((searchInfo.mergeTree.localNetLength(seg) > 0) && Marker.is(seg)) {
196
+ if (refHasTileLabel(seg, searchInfo.tileLabel)) {
197
+ searchInfo.tile = seg;
198
+ }
199
+ }
200
+ } else {
201
+ const block = <IHierBlock>node;
202
+ const marker = searchInfo.tilePrecedesPos
203
+ ? <Marker>block.rightmostTiles[searchInfo.tileLabel]
204
+ : <Marker>block.leftmostTiles[searchInfo.tileLabel];
205
+ if (marker !== undefined) {
206
+ searchInfo.tile = marker;
207
+ }
268
208
  }
209
+ return true;
269
210
  }
270
211
 
271
212
  function addTile(tile: ReferencePosition, tiles: object) {
@@ -328,685 +269,144 @@ function addNodeReferences(
328
269
  leftmostTiles: MapLike<ReferencePosition>, rangeStacks: RangeStackMap) {
329
270
  function updateRangeInfo(label: string, refPos: ReferencePosition) {
330
271
  let stack = rangeStacks[label];
331
- if (stack === undefined) {
332
- stack = new Stack<ReferencePosition>();
333
- rangeStacks[label] = stack;
334
- }
335
- applyRangeReference(stack, refPos);
336
- }
337
- if (node.isLeaf()) {
338
- const segment = node;
339
- if ((mergeTree.localNetLength(segment) ?? 0) > 0) {
340
- if (Marker.is(segment)) {
341
- const markerId = segment.getId();
342
- // Also in insertMarker but need for reload segs case
343
- // can add option for this only from reload segs
344
- if (markerId) {
345
- mergeTree.mapIdToSegment(markerId, segment);
346
- }
347
- if (refTypeIncludesFlag(segment, ReferenceType.Tile)) {
348
- addTile(segment, rightmostTiles);
349
- addTileIfNotPresent(segment, leftmostTiles);
350
- }
351
- if (segment.refType & (ReferenceType.NestBegin | ReferenceType.NestEnd)) {
352
- const rangeLabels = refGetRangeLabels(segment);
353
- if (rangeLabels) {
354
- for (const label of rangeLabels) {
355
- updateRangeInfo(label, segment);
356
- }
357
- }
358
- }
359
- } else {
360
- const baseSegment = node as BaseSegment;
361
- if (baseSegment.localRefs && (baseSegment.localRefs.hierRefCount !== undefined) &&
362
- (baseSegment.localRefs.hierRefCount > 0)) {
363
- for (const lref of baseSegment.localRefs) {
364
- if (refTypeIncludesFlag(lref, ReferenceType.Tile)) {
365
- addTile(lref, rightmostTiles);
366
- addTileIfNotPresent(lref, leftmostTiles);
367
- }
368
- if (lref.refType & (ReferenceType.NestBegin | ReferenceType.NestEnd)) {
369
- for (const label of refGetRangeLabels(lref)!) {
370
- updateRangeInfo(label, lref);
371
- }
372
- }
373
- }
374
- }
375
- }
376
- }
377
- } else {
378
- const block = <IHierBlock>node;
379
- applyStackDelta(rangeStacks, block.rangeStacks);
380
- extend(rightmostTiles, block.rightmostTiles);
381
- extendIfUndefined(leftmostTiles, block.leftmostTiles);
382
- }
383
- }
384
-
385
- export function ordinalToArray(ord: string) {
386
- const a: number[] = [];
387
- if (ord) {
388
- for (let i = 0, len = ord.length; i < len; i++) {
389
- a.push(ord.charCodeAt(i));
390
- }
391
- }
392
- return a;
393
- }
394
-
395
- // Note that the actual branching factor of the MergeTree is `MaxNodesInBlock - 1`. This is because
396
- // the MergeTree always inserts first, then checks for overflow and splits if the child count equals
397
- // `MaxNodesInBlock`. (i.e., `MaxNodesInBlock` contains 1 extra slot for temporary storage to
398
- // facilitate splits.)
399
- export const MaxNodesInBlock = 8;
400
-
401
- export class MergeBlock extends MergeNode implements IMergeBlock {
402
- public children: IMergeNode[];
403
- public constructor(public childCount: number) {
404
- super();
405
- this.children = new Array<IMergeNode>(MaxNodesInBlock);
406
- }
407
-
408
- public hierBlock(): HierMergeBlock | undefined {
409
- return undefined;
410
- }
411
-
412
- public setOrdinal(child: IMergeNode, index: number) {
413
- let childCount = this.childCount;
414
- if (childCount === 8) {
415
- childCount = 7;
416
- }
417
- assert((childCount >= 1) && (childCount <= 7), 0x040 /* "Child count is not within [1,7] range!" */);
418
- let localOrdinal: number;
419
- const ordinalWidth = 1 << (MaxNodesInBlock - (childCount + 1));
420
- if (index === 0) {
421
- localOrdinal = ordinalWidth - 1;
422
- } else {
423
- const prevOrd = this.children[index - 1].ordinal;
424
- const prevOrdCode = prevOrd.charCodeAt(prevOrd.length - 1);
425
- localOrdinal = prevOrdCode + ordinalWidth;
426
- }
427
- child.ordinal = this.ordinal + String.fromCharCode(localOrdinal);
428
- assert(child.ordinal.length === (this.ordinal.length + 1), 0x041 /* "Unexpected child ordinal length!" */);
429
- if (index > 0) {
430
- assert(
431
- child.ordinal > this.children[index - 1].ordinal,
432
- 0x042, /* "Child ordinal <= previous sibling ordinal!" */
433
- );
434
- }
435
- }
436
-
437
- public assignChild(child: IMergeNode, index: number, updateOrdinal = true) {
438
- child.parent = this;
439
- child.index = index;
440
- if (updateOrdinal) {
441
- this.setOrdinal(child, index);
442
- }
443
- this.children[index] = child;
444
- }
445
- }
446
-
447
- class HierMergeBlock extends MergeBlock implements IMergeBlock {
448
- public rightmostTiles: MapLike<ReferencePosition>;
449
- public leftmostTiles: MapLike<ReferencePosition>;
450
- public rangeStacks: MapLike<Stack<ReferencePosition>>;
451
-
452
- constructor(childCount: number) {
453
- super(childCount);
454
- this.rightmostTiles = createMap<ReferencePosition>();
455
- this.leftmostTiles = createMap<ReferencePosition>();
456
- this.rangeStacks = createMap<Stack<ReferencePosition>>();
457
- }
458
-
459
- public addNodeReferences(mergeTree: MergeTree, node: IMergeNode) {
460
- addNodeReferences(mergeTree, node, this.rightmostTiles, this.leftmostTiles,
461
- this.rangeStacks);
462
- }
463
-
464
- public hierBlock() {
465
- return this;
466
- }
467
-
468
- public hierToString(indentCount: number) {
469
- let strbuf = "";
470
- // eslint-disable-next-line guard-for-in, no-restricted-syntax
471
- for (const key in this.rangeStacks) {
472
- const stack = this.rangeStacks[key];
473
- strbuf += internedSpaces(indentCount);
474
- strbuf += `${key}: `;
475
- for (const item of stack.items) {
476
- strbuf += `${item.toString()} `;
477
- }
478
- strbuf += "\n";
479
- }
480
- return strbuf;
481
- }
482
- }
483
-
484
- function nodeTotalLength(mergeTree: MergeTree, node: IMergeNode) {
485
- if (!node.isLeaf()) {
486
- return node.cachedLength;
487
- }
488
- return mergeTree.localNetLength(node);
489
- }
490
-
491
- export abstract class BaseSegment extends MergeNode implements ISegment {
492
- public clientId: number = LocalClientId;
493
- public seq: number = UniversalSequenceNumber;
494
- public removedSeq?: number;
495
- public removedClientIds?: number[];
496
- public readonly segmentGroups: SegmentGroupCollection = new SegmentGroupCollection(this);
497
- public readonly trackingCollection: TrackingGroupCollection = new TrackingGroupCollection(this);
498
- public propertyManager?: PropertiesManager;
499
- public properties?: PropertySet;
500
- public localRefs?: LocalReferenceCollection;
501
- public abstract readonly type: string;
502
- public localSeq?: number;
503
- public localRemovedSeq?: number;
504
-
505
- public addProperties(newProps: PropertySet, op?: ICombiningOp, seq?: number, collabWindow?: CollaborationWindow) {
506
- if (!this.propertyManager) {
507
- this.propertyManager = new PropertiesManager();
508
- }
509
- if (!this.properties) {
510
- this.properties = createMap<any>();
511
- }
512
- return this.propertyManager.addProperties(
513
- this.properties,
514
- newProps,
515
- op,
516
- seq,
517
- collabWindow && collabWindow.collaborating,
518
- );
519
- }
520
-
521
- public hasProperty(key: string): boolean {
522
- return !!this.properties && (this.properties[key] !== undefined);
523
- }
524
-
525
- public isLeaf() {
526
- return true;
527
- }
528
-
529
- protected cloneInto(b: ISegment) {
530
- b.clientId = this.clientId;
531
- // TODO: deep clone properties
532
- b.properties = clone(this.properties);
533
- b.removedClientIds = this.removedClientIds?.slice();
534
- // TODO: copy removed client overlap and branch removal info
535
- b.removedSeq = this.removedSeq;
536
- b.seq = this.seq;
537
- }
538
-
539
- public canAppend(segment: ISegment): boolean {
540
- return false;
541
- }
542
-
543
- protected addSerializedProps(jseg: IJSONSegment) {
544
- if (this.properties) {
545
- jseg.props = this.properties;
546
- }
547
- }
548
-
549
- public abstract toJSONObject(): any;
550
-
551
- public ack(segmentGroup: SegmentGroup, opArgs: IMergeTreeDeltaOpArgs, mergeTree: MergeTree): boolean {
552
- const currentSegmentGroup = this.segmentGroups.dequeue();
553
- assert(currentSegmentGroup === segmentGroup, 0x043 /* "On ack, unexpected segmentGroup!" */);
554
- switch (opArgs.op.type) {
555
- case MergeTreeDeltaType.ANNOTATE:
556
- assert(!!this.propertyManager, 0x044 /* "On annotate ack, missing segment property manager!" */);
557
- this.propertyManager.ackPendingProperties(opArgs.op);
558
- return true;
559
-
560
- case MergeTreeDeltaType.INSERT:
561
- assert(this.seq === UnassignedSequenceNumber, 0x045 /* "On insert, seq number already assigned!" */);
562
- this.seq = opArgs.sequencedMessage!.sequenceNumber;
563
- this.localSeq = undefined;
564
- return true;
565
-
566
- case MergeTreeDeltaType.REMOVE:
567
- const removalInfo: IRemovalInfo | undefined = toRemovalInfo(this);
568
- assert(removalInfo !== undefined, 0x046 /* "On remove ack, missing removal info!" */);
569
- this.localRemovedSeq = undefined;
570
- if (removalInfo.removedSeq === UnassignedSequenceNumber) {
571
- removalInfo.removedSeq = opArgs.sequencedMessage!.sequenceNumber;
572
- return true;
573
- }
574
- return false;
575
-
576
- default:
577
- throw new Error(`${opArgs.op.type} is in unrecognized operation type`);
578
- }
579
- }
580
-
581
- public splitAt(pos: number): ISegment | undefined {
582
- if (pos > 0) {
583
- const leafSegment = this.createSplitSegmentAt(pos);
584
- if (leafSegment) {
585
- this.copyPropertiesTo(leafSegment);
586
- leafSegment.parent = this.parent;
587
-
588
- // Give the leaf a temporary yet valid ordinal.
589
- // when this segment is put in the tree, it will get it's real ordinal,
590
- // but this ordinal meets all the necessary invariants for now.
591
- leafSegment.ordinal = this.ordinal + String.fromCharCode(0);
592
-
593
- leafSegment.removedClientIds = this.removedClientIds?.slice();
594
- leafSegment.removedSeq = this.removedSeq;
595
- leafSegment.localRemovedSeq = this.localRemovedSeq;
596
- leafSegment.seq = this.seq;
597
- leafSegment.localSeq = this.localSeq;
598
- leafSegment.clientId = this.clientId;
599
- this.segmentGroups.copyTo(leafSegment);
600
- this.trackingCollection.copyTo(leafSegment);
601
- if (this.localRefs) {
602
- this.localRefs.split(pos, leafSegment);
603
- }
604
- }
605
- return leafSegment;
606
- }
607
- }
608
-
609
- private copyPropertiesTo(other: ISegment) {
610
- if (this.propertyManager) {
611
- if (this.properties) {
612
- other.propertyManager = new PropertiesManager();
613
- other.properties = this.propertyManager.copyTo(
614
- this.properties,
615
- other.properties,
616
- other.propertyManager,
617
- );
618
- }
619
- }
620
- }
621
-
622
- public abstract clone(): ISegment;
623
- public abstract append(segment: ISegment): void;
624
- protected abstract createSplitSegmentAt(pos: number): BaseSegment | undefined;
625
- }
626
-
627
- export const reservedMarkerIdKey = "markerId";
628
- export const reservedMarkerSimpleTypeKey = "markerSimpleType";
629
-
630
- export interface IJSONMarkerSegment extends IJSONSegment {
631
- marker: IMarkerDef;
632
- }
633
-
634
- export class Marker extends BaseSegment implements ReferencePosition {
635
- public static readonly type = "Marker";
636
- public static is(segment: ISegment): segment is Marker {
637
- return segment.type === Marker.type;
638
- }
639
- public readonly type = Marker.type;
640
-
641
- public static make(
642
- refType: ReferenceType, props?: PropertySet) {
643
- const marker = new Marker(refType);
644
- if (props) {
645
- marker.addProperties(props);
646
- }
647
- return marker;
648
- }
649
-
650
- constructor(public refType: ReferenceType) {
651
- super();
652
- this.cachedLength = 1;
653
- }
654
-
655
- toJSONObject() {
656
- const obj: IJSONMarkerSegment = { marker: { refType: this.refType } };
657
- super.addSerializedProps(obj);
658
- return obj;
659
- }
660
-
661
- static fromJSONObject(spec: any) {
662
- if (spec && typeof spec === "object" && "marker" in spec) {
663
- return Marker.make(
664
- spec.marker.refType,
665
- spec.props as PropertySet);
666
- }
667
- return undefined;
668
- }
669
-
670
- clone() {
671
- const b = Marker.make(this.refType, this.properties);
672
- this.cloneInto(b);
673
- return b;
674
- }
675
-
676
- getSegment() {
677
- return this;
678
- }
679
-
680
- getOffset() {
681
- return 0;
682
- }
683
-
684
- hasSimpleType(simpleTypeName: string) {
685
- return !!this.properties &&
686
- this.properties[reservedMarkerSimpleTypeKey] === simpleTypeName;
687
- }
688
-
689
- getProperties() {
690
- return this.properties;
691
- }
692
-
693
- getId(): string | undefined {
694
- if (this.properties && this.properties[reservedMarkerIdKey]) {
695
- return this.properties[reservedMarkerIdKey] as string;
696
- }
697
- }
698
-
699
- /**
700
- * @deprecated - use refHasTileLabels
701
- */
702
- hasTileLabels() {
703
- return refHasTileLabels(this);
704
- }
705
- /**
706
- * @deprecated - use refHasRangeLabels
707
- */
708
- hasRangeLabels() {
709
- return refHasRangeLabels(this);
710
- }
711
- /**
712
- * @deprecated - use refHasTileLabel
713
- */
714
- hasTileLabel(label: string): boolean {
715
- return refHasTileLabel(this, label);
716
- }
717
- /**
718
- * @deprecated - use refHasRangeLabel
719
- */
720
- hasRangeLabel(label: string): boolean {
721
- return refHasRangeLabel(this, label);
722
- }
723
- /**
724
- * @deprecated - use refGetTileLabels
725
- */
726
- getTileLabels(): string[] | undefined {
727
- return refGetTileLabels(this);
728
- }
729
- /**
730
- * @deprecated - use refGetRangeLabels
731
- */
732
- getRangeLabels(): string[] | undefined {
733
- return refGetRangeLabels(this);
734
- }
735
-
736
- toString() {
737
- let bbuf = "";
738
- if (refTypeIncludesFlag(this, ReferenceType.Tile)) {
739
- bbuf += "Tile";
740
- }
741
- if (refTypeIncludesFlag(this, ReferenceType.NestBegin)) {
742
- if (bbuf.length > 0) {
743
- bbuf += "; ";
744
- }
745
- bbuf += "RangeBegin";
746
- }
747
- if (refTypeIncludesFlag(this, ReferenceType.NestEnd)) {
748
- if (bbuf.length > 0) {
749
- bbuf += "; ";
750
- }
751
- bbuf += "RangeEnd";
752
- }
753
- let lbuf = "";
754
- const id = this.getId();
755
- if (id) {
756
- bbuf += ` (${id}) `;
757
- }
758
- const tileLabels = refGetTileLabels(this);
759
- if (tileLabels) {
760
- lbuf += "tile -- ";
761
- for (let i = 0, len = tileLabels.length; i < len; i++) {
762
- const tileLabel = tileLabels[i];
763
- if (i > 0) {
764
- lbuf += "; ";
765
- }
766
- lbuf += tileLabel;
767
- }
768
- }
769
- const rangeLabels = refGetRangeLabels(this);
770
- if (rangeLabels) {
771
- let rangeKind = "begin";
772
- if (refTypeIncludesFlag(this, ReferenceType.NestEnd)) {
773
- rangeKind = "end";
774
- }
775
- if (tileLabels) {
776
- lbuf += " ";
777
- }
778
- lbuf += `range ${rangeKind} -- `;
779
- const labels = rangeLabels;
780
- for (let i = 0, len = labels.length; i < len; i++) {
781
- const rangeLabel = labels[i];
782
- if (i > 0) {
783
- lbuf += "; ";
784
- }
785
- lbuf += rangeLabel;
786
- }
787
- }
788
- let pbuf = "";
789
- if (this.properties) {
790
- pbuf += JSON.stringify(this.properties, (key, value) => {
791
- // Avoid circular reference when stringifying makers containing handles.
792
- // (Substitute a debug string instead.)
793
- const handle = !!value && value.IFluidHandle;
794
-
795
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
796
- return handle
797
- ? `#Handle(${handle.routeContext.path}/${handle.path})`
798
- : value;
799
- });
800
- }
801
- return `M ${bbuf}: ${lbuf} ${pbuf}`;
802
- }
803
-
804
- protected createSplitSegmentAt(pos: number) {
805
- return undefined;
806
- }
807
-
808
- canAppend(segment: ISegment): boolean {
809
- return false;
810
- }
811
-
812
- append() { throw new Error("Can not append to marker"); }
813
- }
814
-
815
- export enum IncrementalExecOp {
816
- Go,
817
- Stop,
818
- Yield,
819
- }
820
-
821
- export class IncrementalMapState<TContext> {
822
- op = IncrementalExecOp.Go;
823
- constructor(
824
- public block: IMergeBlock,
825
- public actions: IncrementalSegmentActions<TContext>,
826
- public pos: number,
827
- public refSeq: number,
828
- public clientId: number,
829
- public context: TContext,
830
- public start: number,
831
- public end: number,
832
- public childIndex = 0,
833
- ) {
834
- }
835
- }
836
-
837
- export class CollaborationWindow {
838
- clientId = LocalClientId;
839
- collaborating = false;
840
- // Lowest-numbered segment in window; no client can reference a state before this one
841
- minSeq = 0;
842
- // Highest-numbered segment in window and current
843
- // reference segment for this client
844
- currentSeq = 0;
845
-
846
- localSeq = 0;
847
-
848
- loadFrom(a: CollaborationWindow) {
849
- this.clientId = a.clientId;
850
- this.collaborating = a.collaborating;
851
- this.minSeq = a.minSeq;
852
- this.currentSeq = a.currentSeq;
853
- }
854
- }
855
-
856
- export const compareNumbers = (a: number, b: number) => a - b;
857
-
858
- export const compareStrings = (a: string, b: string) => a.localeCompare(b);
859
-
860
- const indentStrings = ["", " ", " "];
861
- export function internedSpaces(n: number) {
862
- if (indentStrings[n] === undefined) {
863
- indentStrings[n] = "";
864
- for (let i = 0; i < n; i++) {
865
- indentStrings[n] += " ";
866
- }
867
- }
868
- return indentStrings[n];
869
- }
870
-
871
- export interface IConsensusInfo {
872
- marker: Marker;
873
- callback: (m: Marker) => void;
874
- }
875
-
876
- export interface ClientSeq {
877
- refSeq: number;
878
- clientId: string;
879
- }
880
-
881
- export const clientSeqComparer: Comparer<ClientSeq> = {
882
- min: { refSeq: -1, clientId: "" },
883
- compare: (a, b) => a.refSeq - b.refSeq,
884
- };
885
-
886
- export interface LRUSegment {
887
- segment?: ISegment;
888
- maxSeq: number;
889
- }
890
-
891
- const LRUSegmentComparer: Comparer<LRUSegment> = {
892
- min: { maxSeq: -2 },
893
- compare: (a, b) => a.maxSeq - b.maxSeq,
894
- };
895
-
896
- export interface SegmentAccumulator {
897
- segments: ISegment[];
898
- }
899
-
900
- interface IReferenceSearchInfo {
901
- mergeTree: MergeTree;
902
- tileLabel: string;
903
- posPrecedesTile?: boolean;
904
- tile?: ReferencePosition;
905
- }
906
-
907
- interface IMarkerSearchRangeInfo {
908
- mergeTree: MergeTree;
909
- rangeLabels: string[];
910
- stacks: RangeStackMap;
911
- }
912
-
913
- function applyLeafRangeMarker(marker: Marker, searchInfo: IMarkerSearchRangeInfo) {
914
- for (const rangeLabel of searchInfo.rangeLabels) {
915
- if (refHasRangeLabel(marker, rangeLabel)) {
916
- let currentStack = searchInfo.stacks[rangeLabel];
917
- if (currentStack === undefined) {
918
- currentStack = new Stack<Marker>();
919
- searchInfo.stacks[rangeLabel] = currentStack;
920
- }
921
- applyRangeReference(currentStack, marker);
922
- }
923
- }
924
- }
925
- function recordRangeLeaf(
926
- segment: ISegment, segpos: number,
927
- refSeq: number, clientId: number, start: number | undefined, end: number | undefined,
928
- searchInfo: IMarkerSearchRangeInfo) {
929
- if (Marker.is(segment)) {
930
- if (segment.refType &
931
- (ReferenceType.NestBegin | ReferenceType.NestEnd)) {
932
- applyLeafRangeMarker(segment, searchInfo);
272
+ if (stack === undefined) {
273
+ stack = new Stack<ReferencePosition>();
274
+ rangeStacks[label] = stack;
933
275
  }
276
+ applyRangeReference(stack, refPos);
934
277
  }
935
- return false;
936
- }
937
-
938
- function rangeShift(
939
- node: IMergeNode, segpos: number, refSeq: number, clientId: number,
940
- offset: number | undefined, end: number | undefined, searchInfo: IMarkerSearchRangeInfo) {
941
278
  if (node.isLeaf()) {
942
- const seg = node;
943
- if (((searchInfo.mergeTree.localNetLength(seg) ?? 0) > 0) && Marker.is(seg)) {
944
- if (seg.refType &
945
- (ReferenceType.NestBegin | ReferenceType.NestEnd)) {
946
- applyLeafRangeMarker(seg, searchInfo);
279
+ const segment = node;
280
+ if ((mergeTree.localNetLength(segment) ?? 0) > 0) {
281
+ if (Marker.is(segment)) {
282
+ const markerId = segment.getId();
283
+ // Also in insertMarker but need for reload segs case
284
+ // can add option for this only from reload segs
285
+ if (markerId) {
286
+ mergeTree.mapIdToSegment(markerId, segment);
287
+ }
288
+ if (refTypeIncludesFlag(segment, ReferenceType.Tile)) {
289
+ addTile(segment, rightmostTiles);
290
+ addTileIfNotPresent(segment, leftmostTiles);
291
+ }
292
+ if (segment.refType & (ReferenceType.NestBegin | ReferenceType.NestEnd)) {
293
+ const rangeLabels = refGetRangeLabels(segment);
294
+ if (rangeLabels) {
295
+ for (const label of rangeLabels) {
296
+ updateRangeInfo(label, segment);
297
+ }
298
+ }
299
+ }
300
+ } else {
301
+ const baseSegment = node as BaseSegment;
302
+ if (baseSegment.localRefs && (baseSegment.localRefs.hierRefCount !== undefined) &&
303
+ (baseSegment.localRefs.hierRefCount > 0)) {
304
+ for (const lref of baseSegment.localRefs) {
305
+ if (refTypeIncludesFlag(lref, ReferenceType.Tile)) {
306
+ addTile(lref, rightmostTiles);
307
+ addTileIfNotPresent(lref, leftmostTiles);
308
+ }
309
+ if (lref.refType & (ReferenceType.NestBegin | ReferenceType.NestEnd)) {
310
+ for (const label of refGetRangeLabels(lref)!) {
311
+ updateRangeInfo(label, lref);
312
+ }
313
+ }
314
+ }
315
+ }
947
316
  }
948
317
  }
949
318
  } else {
950
319
  const block = <IHierBlock>node;
951
- applyStackDelta(searchInfo.stacks, block.rangeStacks);
320
+ applyStackDelta(rangeStacks, block.rangeStacks);
321
+ extend(rightmostTiles, block.rightmostTiles);
322
+ extendIfUndefined(leftmostTiles, block.leftmostTiles);
952
323
  }
953
- return true;
954
324
  }
955
325
 
956
- function recordTileStart(
957
- segment: ISegment,
958
- segpos: number,
959
- refSeq: number,
960
- clientId: number,
961
- start: number,
962
- end: number,
963
- searchInfo: IReferenceSearchInfo) {
964
- if (Marker.is(segment)) {
965
- if (refHasTileLabel(segment, searchInfo.tileLabel)) {
966
- searchInfo.tile = segment;
326
+ function extendIfUndefined<T>(base: MapLike<T>, extension: MapLike<T> | undefined) {
327
+ if (extension !== undefined) {
328
+ // eslint-disable-next-line no-restricted-syntax
329
+ for (const key in extension) {
330
+ if (base[key] === undefined) {
331
+ base[key] = extension[key];
332
+ }
967
333
  }
968
334
  }
969
- return false;
335
+ return base;
970
336
  }
971
337
 
972
- function tileShift(
973
- node: IMergeNode, segpos: number, refSeq: number, clientId: number,
974
- offset: number | undefined, end: number | undefined, searchInfo: IReferenceSearchInfo) {
975
- if (node.isLeaf()) {
976
- const seg = node;
977
- if ((searchInfo.mergeTree.localNetLength(seg) > 0) && Marker.is(seg)) {
978
- if (refHasTileLabel(seg, searchInfo.tileLabel)) {
979
- searchInfo.tile = seg;
338
+ class HierMergeBlock extends MergeBlock implements IHierBlock {
339
+ public rightmostTiles: MapLike<ReferencePosition>;
340
+ public leftmostTiles: MapLike<ReferencePosition>;
341
+ public rangeStacks: MapLike<Stack<ReferencePosition>>;
342
+
343
+ constructor(childCount: number) {
344
+ super(childCount);
345
+ this.rightmostTiles = createMap<ReferencePosition>();
346
+ this.leftmostTiles = createMap<ReferencePosition>();
347
+ this.rangeStacks = createMap<Stack<ReferencePosition>>();
348
+ }
349
+
350
+ /**
351
+ * @deprecated For internal use only. public export will be removed.
352
+ * @internal
353
+ */
354
+ public addNodeReferences(mergeTree: MergeTree, node: IMergeNode) {
355
+ addNodeReferences(mergeTree, node, this.rightmostTiles, this.leftmostTiles,
356
+ this.rangeStacks);
357
+ }
358
+
359
+ public hierBlock() {
360
+ return this;
361
+ }
362
+
363
+ public hierToString(indentCount: number) {
364
+ let strbuf = "";
365
+ // eslint-disable-next-line guard-for-in, no-restricted-syntax
366
+ for (const key in this.rangeStacks) {
367
+ const stack = this.rangeStacks[key];
368
+ strbuf += internedSpaces(indentCount);
369
+ strbuf += `${key}: `;
370
+ for (const item of stack.items) {
371
+ strbuf += `${item.toString()} `;
980
372
  }
373
+ strbuf += "\n";
981
374
  }
982
- } else {
983
- const block = <IHierBlock>node;
984
- let marker: Marker;
985
- if (searchInfo.posPrecedesTile) {
986
- marker = <Marker>block.rightmostTiles[searchInfo.tileLabel];
987
- } else {
988
- marker = <Marker>block.leftmostTiles[searchInfo.tileLabel];
989
- }
990
- if (marker !== undefined) {
991
- searchInfo.tile = marker;
992
- }
375
+ return strbuf;
993
376
  }
994
- return true;
995
377
  }
996
378
 
997
- export interface MinListener {
998
- minRequired: number;
999
- onMinGE(minSeq: number): void;
379
+ /**
380
+ * @deprecated For internal use only. public export will be removed.
381
+ * @internal
382
+ */
383
+ export interface ClientSeq {
384
+ refSeq: number;
385
+ clientId: string;
1000
386
  }
1001
387
 
1002
- const minListenerComparer: Comparer<MinListener> = {
1003
- min: { minRequired: Number.MIN_VALUE, onMinGE: () => { assert(false, 0x048 /* "onMinGE()" */); } },
1004
- compare: (a, b) => a.minRequired - b.minRequired,
388
+ /**
389
+ * @deprecated For internal use only. public export will be removed.
390
+ * @internal
391
+ */
392
+ export const clientSeqComparer: Comparer<ClientSeq> = {
393
+ min: { refSeq: -1, clientId: "" },
394
+ compare: (a, b) => a.refSeq - b.refSeq,
1005
395
  };
1006
396
 
1007
- export type LocalReferenceMapper = (id: string) => LocalReference;
397
+ /**
398
+ * @deprecated For internal use only. public export will be removed.
399
+ * @internal
400
+ */
401
+ export interface LRUSegment {
402
+ segment?: ISegment;
403
+ maxSeq: number;
404
+ }
1008
405
 
1009
- // Represents a sequence of text segments
406
+ /**
407
+ * @deprecated For internal use only. public export will be removed.
408
+ * @internal
409
+ */
1010
410
  export class MergeTree {
1011
411
  private static readonly zamboniSegmentsMaxCount = 2;
1012
412
  public static readonly options = {
@@ -1021,8 +421,19 @@ export class MergeTree {
1021
421
  root: IMergeBlock;
1022
422
  private readonly blockUpdateActions: BlockUpdateActions = MergeTree.initBlockUpdateActions;
1023
423
  public readonly collabWindow = new CollaborationWindow();
424
+
425
+ /**
426
+ * @deprecated for internal use only. public export will be removed.
427
+ * @internal
428
+ */
1024
429
  public pendingSegments: List<SegmentGroup> | undefined;
1025
430
  private segmentsToScour: Heap<LRUSegment> | undefined;
431
+ /**
432
+ * Whether or not all blocks in the mergeTree currently have information about local partial lengths computed.
433
+ * This information is only necessary on reconnect, and otherwise costly to bookkeep.
434
+ * This field enables tracking whether partials need to be recomputed using localSeq information.
435
+ */
436
+ private localPartialsComputed = false;
1026
437
  // TODO: add remove on segment remove
1027
438
  // for now assume only markers have ids and so point directly at the Segment
1028
439
  // if we need to have pointers to non-markers, we can change to point at local refs
@@ -1072,11 +483,36 @@ export class MergeTree {
1072
483
  return b;
1073
484
  }
1074
485
 
1075
- public localNetLength(segment: ISegment) {
486
+ /**
487
+ * Compute the net length of this segment from a local perspective.
488
+ * @param segment - Segment whose length to find
489
+ * @param localSeq - localSeq at which to find the length of this segment. If not provided,
490
+ * default is to consider the local client's current perspective. Only local sequence
491
+ * numbers corresponding to un-acked operations give valid results.
492
+ */
493
+ public localNetLength(segment: ISegment, refSeq?: number, localSeq?: number) {
1076
494
  const removalInfo = toRemovalInfo(segment);
1077
- if (removalInfo !== undefined) {
1078
- return 0;
495
+ if (localSeq === undefined) {
496
+ return removalInfo !== undefined ? 0 : segment.cachedLength;
497
+ }
498
+
499
+ assert(refSeq !== undefined, 0x398 /* localSeq provided for local length without refSeq */);
500
+ assert(segment.seq !== undefined, 0x399 /* segment with no seq in mergeTree */);
501
+ const { seq, removedSeq, localRemovedSeq } = segment;
502
+ if (seq !== UnassignedSequenceNumber) {
503
+ // inserted remotely
504
+ if (seq > refSeq
505
+ || (removedSeq !== undefined && removedSeq !== UnassignedSequenceNumber && removedSeq <= refSeq)
506
+ || (localRemovedSeq !== undefined && localRemovedSeq <= localSeq)) {
507
+ return 0;
508
+ }
509
+ return segment.cachedLength;
1079
510
  } else {
511
+ assert(segment.localSeq !== undefined, 0x39a /* unacked segment with undefined localSeq */);
512
+ // inserted locally, still un-acked
513
+ if (segment.localSeq > localSeq || (localRemovedSeq !== undefined && localRemovedSeq <= localSeq)) {
514
+ return 0;
515
+ }
1080
516
  return segment.cachedLength;
1081
517
  }
1082
518
  }
@@ -1218,11 +654,7 @@ export class MergeTree {
1218
654
  segment.trackingCollection.trackingGroups.forEach((tg) => tg.unlink(segment));
1219
655
  } else {
1220
656
  holdNodes.push(segment);
1221
- if (this.localNetLength(segment) > 0) {
1222
- prevSegment = segment;
1223
- } else {
1224
- prevSegment = undefined;
1225
- }
657
+ prevSegment = this.localNetLength(segment) > 0 ? segment : undefined;
1226
658
  }
1227
659
  } else {
1228
660
  holdNodes.push(segment);
@@ -1390,7 +822,7 @@ export class MergeTree {
1390
822
  */
1391
823
  public get length() { return this.root.cachedLength; }
1392
824
 
1393
- public getPosition(node: MergeNode, refSeq: number, clientId: number) {
825
+ public getPosition(node: MergeNode, refSeq: number, clientId: number, localSeq?: number) {
1394
826
  let totalOffset = 0;
1395
827
  let parent = node.parent;
1396
828
  let prevParent: IMergeBlock | undefined;
@@ -1401,7 +833,7 @@ export class MergeTree {
1401
833
  if ((prevParent && (child === prevParent)) || (child === node)) {
1402
834
  break;
1403
835
  }
1404
- totalOffset += this.nodeLength(child, refSeq, clientId) ?? 0;
836
+ totalOffset += this.nodeLength(child, refSeq, clientId, localSeq) ?? 0;
1405
837
  }
1406
838
  prevParent = parent;
1407
839
  parent = parent.parent;
@@ -1409,7 +841,9 @@ export class MergeTree {
1409
841
  return totalOffset;
1410
842
  }
1411
843
 
1412
- public getContainingSegment<T extends ISegment>(pos: number, refSeq: number, clientId: number) {
844
+ public getContainingSegment<T extends ISegment>(pos: number, refSeq: number, clientId: number, localSeq?: number) {
845
+ assert(localSeq === undefined || clientId === this.collabWindow.clientId,
846
+ 0x39b /* localSeq provided for non-local client */);
1413
847
  let segment: T | undefined;
1414
848
  let offset: number | undefined;
1415
849
 
@@ -1418,18 +852,19 @@ export class MergeTree {
1418
852
  offset = start;
1419
853
  return false;
1420
854
  };
1421
- this.searchBlock(this.root, pos, 0, refSeq, clientId, { leaf }, undefined);
855
+ this.searchBlock(this.root, pos, 0, refSeq, clientId, { leaf }, undefined, localSeq);
1422
856
  return { segment, offset };
1423
857
  }
1424
858
 
1425
859
  /**
1426
- * @internal must only be used by client
1427
- * @param segoff - The segment and offset to slide from
1428
- * @returns The segment and offset to slide to
860
+ * @remarks Must only be used by client.
861
+ * @param segment - The segment to slide from.
862
+ * @returns The segment to.
863
+ * @internal
1429
864
  */
1430
- public _getSlideToSegment(segoff: { segment: ISegment | undefined; offset: number | undefined; }) {
1431
- if (!segoff.segment || !isRemovedAndAcked(segoff.segment)) {
1432
- return segoff;
865
+ public _getSlideToSegment(segment: ISegment | undefined): ISegment | undefined {
866
+ if (!segment || !isRemovedAndAcked(segment)) {
867
+ return segment;
1433
868
  }
1434
869
  let slideToSegment: ISegment | undefined;
1435
870
  const goFurtherToFindSlideToSegment = (seg) => {
@@ -1440,22 +875,13 @@ export class MergeTree {
1440
875
  return true;
1441
876
  };
1442
877
  // Slide to the next farthest valid segment in the tree.
1443
- this.rightExcursion(segoff.segment, goFurtherToFindSlideToSegment);
878
+ this.rightExcursion(segment, goFurtherToFindSlideToSegment);
1444
879
  if (slideToSegment) {
1445
- return { segment: slideToSegment, offset: 0 };
880
+ return slideToSegment;
1446
881
  }
1447
882
  // If no such segment is found, slide to the last valid segment.
1448
- this.leftExcursion(segoff.segment, goFurtherToFindSlideToSegment);
1449
-
1450
- // Workaround TypeScript issue (https://github.com/microsoft/TypeScript/issues/9998)
1451
- slideToSegment = slideToSegment as ISegment | undefined;
1452
-
1453
- if (slideToSegment) {
1454
- // If slid nearer then offset should be at the end of the segment
1455
- return { segment: slideToSegment, offset: slideToSegment.cachedLength - 1 };
1456
- }
1457
-
1458
- return { segment: undefined, offset: 0 };
883
+ this.leftExcursion(segment, goFurtherToFindSlideToSegment);
884
+ return slideToSegment;
1459
885
  }
1460
886
 
1461
887
  /**
@@ -1464,31 +890,26 @@ export class MergeTree {
1464
890
  * Otherwise eventual consistency is not guaranteed.
1465
891
  * See `packages\dds\merge-tree\REFERENCEPOSITIONS.md`
1466
892
  */
1467
- private slideReferences(segment: ISegment, refsToSlide: LocalReference[]) {
893
+ private slideReferences(segment: ISegment, refsToSlide: Iterable<LocalReferencePosition>) {
1468
894
  assert(
1469
895
  isRemovedAndAcked(segment),
1470
896
  0x2f1 /* slideReferences from a segment which has not been removed and acked */);
1471
897
  assert(!!segment.localRefs, 0x2f2 /* Ref not in the segment localRefs */);
1472
- const newSegoff = this._getSlideToSegment({ segment, offset: 0 });
1473
- const newSegment = newSegoff.segment;
1474
- if (newSegment && !newSegment.localRefs) {
1475
- newSegment.localRefs = new LocalReferenceCollection(newSegment);
1476
- }
1477
- for (const ref of refsToSlide) {
1478
- ref.callbacks?.beforeSlide?.();
1479
- const removedRef = segment.localRefs.removeLocalRef(ref);
1480
- assert(ref === removedRef, 0x2f3 /* Ref not in the segment localRefs */);
1481
- if (!newSegment) {
1482
- // No valid segments (all nodes removed or not yet created)
1483
- ref.segment = undefined;
1484
- ref.offset = 0;
898
+ const newSegment = this._getSlideToSegment(segment);
899
+ if (newSegment) {
900
+ const localRefs = newSegment.localRefs ??= new LocalReferenceCollection(newSegment);
901
+ if (newSegment.ordinal < segment.ordinal) {
902
+ localRefs.addAfterTombstones(refsToSlide);
1485
903
  } else {
1486
- ref.segment = newSegment;
1487
- ref.offset = newSegoff.offset ?? 0;
1488
- assert(!!newSegment.localRefs, 0x2f4 /* localRefs must be allocated */);
1489
- newSegment.localRefs.addLocalRef(ref);
904
+ localRefs.addBeforeTombstones(refsToSlide);
905
+ }
906
+ } else {
907
+ for (const ref of refsToSlide) {
908
+ ref.callbacks?.beforeSlide?.();
909
+ assertLocalReferences(ref);
910
+ ref.link(undefined, 0, undefined);
911
+ ref.callbacks?.afterSlide?.();
1490
912
  }
1491
- ref.callbacks?.afterSlide?.();
1492
913
  }
1493
914
  // TODO is it required to update the path lengths?
1494
915
  if (newSegment) {
@@ -1497,21 +918,23 @@ export class MergeTree {
1497
918
  }
1498
919
  }
1499
920
 
1500
- private updateSegmentRefsAfterMarkRemoved(segment: ISegment, pending: boolean) {
921
+ private updateSegmentRefsAfterMarkRemoved(segment: ISegment, pending: boolean):
922
+ LocalReferencePosition[] | undefined {
1501
923
  if (!segment.localRefs || segment.localRefs.empty) {
1502
- return;
924
+ return undefined;
1503
925
  }
1504
- const refsToSlide: LocalReference[] = [];
1505
- const refsToStay: LocalReference[] = [];
926
+ const refsToSlide: ReferencePosition[] = [];
927
+ const removedRefs: LocalReferencePosition[] = [];
1506
928
  for (const lref of segment.localRefs) {
1507
929
  if (refTypeIncludesFlag(lref, ReferenceType.StayOnRemove)) {
1508
- refsToStay.push(lref);
1509
- } else if (refTypeIncludesFlag(lref, ReferenceType.SlideOnRemove)) {
1510
- if (pending) {
1511
- refsToStay.push(lref);
1512
- } else {
930
+ continue;
931
+ } if (refTypeIncludesFlag(lref, ReferenceType.SlideOnRemove)) {
932
+ if (!pending) {
1513
933
  refsToSlide.push(lref);
1514
934
  }
935
+ } else {
936
+ segment.localRefs.removeLocalRef(lref);
937
+ removedRefs.push(lref);
1515
938
  }
1516
939
  }
1517
940
  // Rethink implementation of keeping and sliding refs once other reference
@@ -1519,28 +942,40 @@ export class MergeTree {
1519
942
  if (!pending) {
1520
943
  this.slideReferences(segment, refsToSlide);
1521
944
  }
1522
- segment.localRefs.clear();
1523
- for (const lref of refsToStay) {
1524
- lref.segment = segment;
1525
- segment.localRefs.addLocalRef(lref);
1526
- }
945
+ // return only the refs that have been entirely removed
946
+ return removedRefs.length > 0 ? removedRefs : undefined;
1527
947
  }
1528
948
 
1529
949
  private blockLength(node: IMergeBlock, refSeq: number, clientId: number) {
1530
- if ((this.collabWindow.collaborating) && (clientId !== this.collabWindow.clientId)) {
1531
- return node.partialLengths!.getPartialLength(refSeq, clientId);
1532
- } else {
1533
- return node.cachedLength;
1534
- }
950
+ return (this.collabWindow.collaborating) && (clientId !== this.collabWindow.clientId)
951
+ ? node.partialLengths!.getPartialLength(refSeq, clientId)
952
+ : node.cachedLength;
1535
953
  }
1536
954
 
1537
- private nodeLength(node: IMergeNode, refSeq: number, clientId: number) {
955
+ private nodeLength(node: IMergeNode, refSeq: number, clientId: number, localSeq?: number) {
1538
956
  if ((!this.collabWindow.collaborating) || (this.collabWindow.clientId === clientId)) {
1539
- // Local client sees all segments, even when collaborating
1540
- if (!node.isLeaf()) {
957
+ if (node.isLeaf()) {
958
+ return this.localNetLength(node, refSeq, localSeq);
959
+ } else if (localSeq === undefined) {
960
+ // Local client sees all segments, even when collaborating
1541
961
  return node.cachedLength;
1542
962
  } else {
1543
- return this.localNetLength(node);
963
+ if (!this.localPartialsComputed) {
964
+ const rebaseCollabWindow = new CollaborationWindow();
965
+ rebaseCollabWindow.loadFrom(this.collabWindow);
966
+ if (refSeq < this.collabWindow.minSeq) {
967
+ rebaseCollabWindow.minSeq = refSeq;
968
+ }
969
+ this.root.partialLengths = PartialSequenceLengths.combine(
970
+ this.root,
971
+ rebaseCollabWindow,
972
+ true,
973
+ true,
974
+ );
975
+ this.localPartialsComputed = true;
976
+ }
977
+ // Local client should see all segments except those after localSeq.
978
+ return node.partialLengths!.getPartialLength(refSeq, clientId, localSeq);
1544
979
  }
1545
980
  } else {
1546
981
  // Sequence number within window
@@ -1562,11 +997,7 @@ export class MergeTree {
1562
997
  ((segment.seq !== UnassignedSequenceNumber) && (segment.seq! <= refSeq)))) {
1563
998
  // Segment happened by reference sequence number or segment from requesting client
1564
999
  if (removalInfo !== undefined) {
1565
- if (removalInfo.removedClientIds.includes(clientId)) {
1566
- return 0;
1567
- } else {
1568
- return segment.cachedLength;
1569
- }
1000
+ return removalInfo.removedClientIds.includes(clientId) ? 0 : segment.cachedLength;
1570
1001
  } else {
1571
1002
  return segment.cachedLength;
1572
1003
  }
@@ -1626,13 +1057,23 @@ export class MergeTree {
1626
1057
  refSeq = this.collabWindow.currentSeq,
1627
1058
  clientId = this.collabWindow.clientId) {
1628
1059
  const seg = refPos.getSegment();
1629
- if (seg && seg.parent) {
1630
- const offset = !seg.removedSeq ? refPos.getOffset() : 0;
1631
- return offset + this.getPosition(seg, refSeq, clientId);
1060
+ if (seg?.parent === undefined) {
1061
+ return DetachedReferencePosition;
1632
1062
  }
1633
- return LocalReference.DetachedPosition;
1063
+ if (refPos.isLeaf()) {
1064
+ return this.getPosition(refPos, refSeq, clientId);
1065
+ }
1066
+ if (refTypeIncludesFlag(refPos, ReferenceType.Transient)
1067
+ || seg.localRefs?.has(refPos)) {
1068
+ const offset = isRemoved(seg) ? 0 : refPos.getOffset();
1069
+ return offset + this.getPosition(seg, refSeq, clientId);
1070
+ }
1071
+ return DetachedReferencePosition;
1634
1072
  }
1635
-
1073
+ /**
1074
+ * @deprecated for internal use only. public export will be removed.
1075
+ * @internal
1076
+ */
1636
1077
  public getStackContext(startPos: number, clientId: number, rangeLabels: string[]) {
1637
1078
  const searchInfo: IMarkerSearchRangeInfo = {
1638
1079
  mergeTree: this,
@@ -1646,14 +1087,22 @@ export class MergeTree {
1646
1087
  }
1647
1088
 
1648
1089
  // TODO: filter function
1649
- public findTile(startPos: number, clientId: number, tileLabel: string, posPrecedesTile = true) {
1090
+ /**
1091
+ * Finds the nearest reference with ReferenceType.Tile to `startPos` in the direction dictated by `tilePrecedesPos`.
1092
+ *
1093
+ * @param startPos - Position at which to start the search
1094
+ * @param clientId - clientId dictating the perspective to search from
1095
+ * @param tileLabel - Label of the tile to search for
1096
+ * @param tilePrecedesPos - Whether the desired tile comes before (true) or after (false) `startPos`
1097
+ */
1098
+ public findTile(startPos: number, clientId: number, tileLabel: string, tilePrecedesPos = true) {
1650
1099
  const searchInfo: IReferenceSearchInfo = {
1651
1100
  mergeTree: this,
1652
- posPrecedesTile,
1101
+ tilePrecedesPos,
1653
1102
  tileLabel,
1654
1103
  };
1655
1104
 
1656
- if (posPrecedesTile) {
1105
+ if (tilePrecedesPos) {
1657
1106
  this.search(startPos, UniversalSequenceNumber, clientId,
1658
1107
  { leaf: recordTileStart, shift: tileShift }, searchInfo);
1659
1108
  } else {
@@ -1667,8 +1116,8 @@ export class MergeTree {
1667
1116
  const marker = <Marker>searchInfo.tile;
1668
1117
  pos = this.getPosition(marker, UniversalSequenceNumber, clientId);
1669
1118
  } else {
1670
- const localRef = <LocalReference>searchInfo.tile;
1671
- pos = localRef.toPosition();
1119
+ const localRef = searchInfo.tile;
1120
+ pos = this.referencePositionToLocalPosition(localRef, UniversalSequenceNumber, clientId);
1672
1121
  }
1673
1122
  return { tile: searchInfo.tile, pos };
1674
1123
  }
@@ -1681,8 +1130,15 @@ export class MergeTree {
1681
1130
  }
1682
1131
 
1683
1132
  private searchBlock<TClientData>(
1684
- block: IMergeBlock, pos: number, segpos: number, refSeq: number, clientId: number,
1685
- actions: SegmentActions<TClientData> | undefined, clientData: TClientData): ISegment | undefined {
1133
+ block: IMergeBlock,
1134
+ pos: number,
1135
+ segpos: number,
1136
+ refSeq: number,
1137
+ clientId: number,
1138
+ actions: SegmentActions<TClientData> | undefined,
1139
+ clientData: TClientData,
1140
+ localSeq?: number,
1141
+ ): ISegment | undefined {
1686
1142
  let _pos = pos;
1687
1143
  let _segpos = segpos;
1688
1144
  const children = block.children;
@@ -1692,14 +1148,14 @@ export class MergeTree {
1692
1148
  const contains = actions && actions.contains;
1693
1149
  for (let childIndex = 0; childIndex < block.childCount; childIndex++) {
1694
1150
  const child = children[childIndex];
1695
- const len = this.nodeLength(child, refSeq, clientId) ?? 0;
1151
+ const len = this.nodeLength(child, refSeq, clientId, localSeq) ?? 0;
1696
1152
  if (
1697
1153
  (!contains && _pos < len)
1698
1154
  || (contains && contains(child, _pos, refSeq, clientId, undefined, undefined, clientData))
1699
1155
  ) {
1700
1156
  // Found entry containing pos
1701
1157
  if (!child.isLeaf()) {
1702
- return this.searchBlock(child, _pos, _segpos, refSeq, clientId, actions, clientData);
1158
+ return this.searchBlock(child, _pos, _segpos, refSeq, clientId, actions, clientData, localSeq);
1703
1159
  } else {
1704
1160
  if (actions && actions.leaf) {
1705
1161
  actions.leaf(child, _segpos, refSeq, clientId, _pos, -1, clientData);
@@ -1824,13 +1280,26 @@ export class MergeTree {
1824
1280
  }
1825
1281
  }
1826
1282
 
1827
- private addToPendingList(segment: ISegment, segmentGroup?: SegmentGroup, localSeq?: number) {
1283
+ private addToPendingList(segment: ISegment, segmentGroup?: SegmentGroup, localSeq?: number,
1284
+ previousProps?: PropertySet) {
1828
1285
  let _segmentGroup = segmentGroup;
1829
1286
  if (_segmentGroup === undefined) {
1830
1287
  // TODO: review the cast
1831
1288
  _segmentGroup = { segments: [], localSeq } as SegmentGroup;
1289
+ if (previousProps) {
1290
+ _segmentGroup.previousProps = [];
1291
+ }
1832
1292
  this.pendingSegments!.enqueue(_segmentGroup);
1833
1293
  }
1294
+
1295
+ if ((!_segmentGroup.previousProps && previousProps) ||
1296
+ (_segmentGroup.previousProps && !previousProps)) {
1297
+ throw new Error("All segments in group should have previousProps or none");
1298
+ }
1299
+ if (previousProps) {
1300
+ _segmentGroup.previousProps!.push(previousProps);
1301
+ }
1302
+
1834
1303
  segment.segmentGroups.enqueue(_segmentGroup);
1835
1304
  return _segmentGroup;
1836
1305
  }
@@ -1929,7 +1398,7 @@ export class MergeTree {
1929
1398
  const splitNode = this.split(block);
1930
1399
  if (block === this.root) {
1931
1400
  this.updateRoot(splitNode);
1932
- // Update root already updates all it's children ordinals
1401
+ // Update root already updates all its children ordinals
1933
1402
  ordinalUpdateNode = undefined;
1934
1403
  } else {
1935
1404
  this.insertChildNode(block.parent!, splitNode, block.index + 1);
@@ -2398,10 +1867,12 @@ export class MergeTree {
2398
1867
  * @param clientId - The id of the client making the annotate
2399
1868
  * @param seq - The sequence number of the annotate operation
2400
1869
  * @param opArgs - The op args for the annotate op. this is passed to the merge tree callback if there is one
1870
+ * @param rollback - Whether this is for a local rollback and what kind
2401
1871
  */
2402
1872
  public annotateRange(
2403
1873
  start: number, end: number, props: PropertySet, combiningOp: ICombiningOp | undefined, refSeq: number,
2404
- clientId: number, seq: number, opArgs: IMergeTreeDeltaOpArgs) {
1874
+ clientId: number, seq: number, opArgs: IMergeTreeDeltaOpArgs,
1875
+ rollback: PropertiesRollback = PropertiesRollback.None) {
2405
1876
  this.ensureIntervalBoundary(start, refSeq, clientId);
2406
1877
  this.ensureIntervalBoundary(end, refSeq, clientId);
2407
1878
  const deltaSegments: IMergeTreeSegmentDelta[] = [];
@@ -2409,11 +1880,12 @@ export class MergeTree {
2409
1880
  let segmentGroup: SegmentGroup | undefined;
2410
1881
 
2411
1882
  const annotateSegment = (segment: ISegment) => {
2412
- const propertyDeltas = segment.addProperties(props, combiningOp, seq, this.collabWindow);
1883
+ const propertyDeltas = segment.addProperties(props, combiningOp, seq, this.collabWindow, rollback);
2413
1884
  deltaSegments.push({ segment, propertyDeltas });
2414
1885
  if (this.collabWindow.collaborating) {
2415
1886
  if (seq === UnassignedSequenceNumber) {
2416
- segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
1887
+ segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq,
1888
+ propertyDeltas ? propertyDeltas : {});
2417
1889
  } else {
2418
1890
  if (MergeTree.options.zamboniSegments) {
2419
1891
  this.addToLRUSet(segment, seq);
@@ -2449,13 +1921,13 @@ export class MergeTree {
2449
1921
  seq: number,
2450
1922
  overwrite = false,
2451
1923
  opArgs: IMergeTreeDeltaOpArgs,
2452
- ) {
1924
+ ): void {
2453
1925
  let _overwrite = overwrite;
2454
1926
  this.ensureIntervalBoundary(start, refSeq, clientId);
2455
1927
  this.ensureIntervalBoundary(end, refSeq, clientId);
2456
1928
  let segmentGroup: SegmentGroup;
2457
1929
  const removedSegments: IMergeTreeSegmentDelta[] = [];
2458
- const segmentsWithRefs: ISegment[] = [];
1930
+ const localOverlapWithRefs: ISegment[] = [];
2459
1931
  const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
2460
1932
  const markRemoved = (segment: ISegment, pos: number, _start: number, _end: number) => {
2461
1933
  const existingRemovalInfo = toRemovalInfo(segment);
@@ -2464,11 +1936,14 @@ export class MergeTree {
2464
1936
  if (existingRemovalInfo.removedSeq === UnassignedSequenceNumber) {
2465
1937
  // we removed this locally, but someone else removed it first
2466
1938
  // so put them at the head of the list
2467
- // the list isn't ordered, but we
2468
- // keep first removal at the head.
1939
+ // The list isn't ordered, but we keep the first removal at the head
1940
+ // for partialLengths bookkeeping purposes
2469
1941
  existingRemovalInfo.removedClientIds.unshift(clientId);
1942
+
2470
1943
  existingRemovalInfo.removedSeq = seq;
2471
- segment.localRemovedSeq = undefined;
1944
+ if (segment.localRefs?.empty === false) {
1945
+ localOverlapWithRefs.push(segment);
1946
+ }
2472
1947
  } else {
2473
1948
  // Do not replace earlier sequence number for remove
2474
1949
  existingRemovalInfo.removedClientIds.push(clientId);
@@ -2480,9 +1955,6 @@ export class MergeTree {
2480
1955
 
2481
1956
  removedSegments.push({ segment });
2482
1957
  }
2483
- if (segment.localRefs && !segment.localRefs.empty) {
2484
- segmentsWithRefs.push(segment);
2485
- }
2486
1958
 
2487
1959
  // Save segment so can assign removed sequence number when acked by server
2488
1960
  if (this.collabWindow.collaborating) {
@@ -2505,11 +1977,12 @@ export class MergeTree {
2505
1977
  return true;
2506
1978
  };
2507
1979
  this.mapRange({ leaf: markRemoved, post: afterMarkRemoved }, refSeq, clientId, undefined, start, end);
2508
- const pending = this.collabWindow.collaborating && clientId === this.collabWindow.clientId;
2509
- for (const segment of segmentsWithRefs) {
2510
- this.updateSegmentRefsAfterMarkRemoved(segment, pending);
2511
- }
2512
-
1980
+ // these segments are already viewed as being removed locally and are not event-ed
1981
+ // so can slide non-StayOnRemove refs immediately
1982
+ localOverlapWithRefs.forEach(
1983
+ (s) => this.slideReferences(s, Array.from(s.localRefs!)
1984
+ .filter((localRef) => !refTypeIncludesFlag(localRef, ReferenceType.StayOnRemove))),
1985
+ );
2513
1986
  // opArgs == undefined => test code
2514
1987
  if (this.mergeTreeDeltaCallback && removedSegments.length > 0) {
2515
1988
  this.mergeTreeDeltaCallback(
@@ -2519,6 +1992,20 @@ export class MergeTree {
2519
1992
  deltaSegments: removedSegments,
2520
1993
  });
2521
1994
  }
1995
+ const pending = this.collabWindow.collaborating && clientId === this.collabWindow.clientId;
1996
+ // these events are newly removed
1997
+ // so we slide after eventing in case the consumer wants to make reference
1998
+ // changes at remove time, like add a ref to track undo redo.
1999
+ removedSegments.forEach((rSeg) => {
2000
+ const removedRefs = this.updateSegmentRefsAfterMarkRemoved(rSeg.segment, pending);
2001
+ if (segmentGroup && removedRefs) {
2002
+ if (!segmentGroup.removedReferences) {
2003
+ segmentGroup.removedReferences = [];
2004
+ }
2005
+ segmentGroup.removedReferences.push(...removedRefs);
2006
+ }
2007
+ });
2008
+
2522
2009
  if (this.collabWindow.collaborating && (seq !== UnassignedSequenceNumber)) {
2523
2010
  if (MergeTree.options.zamboniSegments) {
2524
2011
  this.zamboniSegments();
@@ -2526,10 +2013,122 @@ export class MergeTree {
2526
2013
  }
2527
2014
  }
2528
2015
 
2016
+ /**
2017
+ * Revert an unacked local op
2018
+ */
2019
+ public rollback(op: IMergeTreeDeltaOp, localOpMetadata: SegmentGroup) {
2020
+ if (op.type === MergeTreeDeltaType.REMOVE) {
2021
+ const pendingSegmentGroup = this.pendingSegments?.pop?.();
2022
+ if (pendingSegmentGroup === undefined || pendingSegmentGroup !== localOpMetadata) {
2023
+ throw new Error("Rollback op doesn't match last edit");
2024
+ }
2025
+ for (const segment of pendingSegmentGroup.segments) {
2026
+ const segmentSegmentGroup = segment.segmentGroups.pop ? segment.segmentGroups.pop() : undefined;
2027
+ assert(segmentSegmentGroup === pendingSegmentGroup, 0x39c /* Unexpected segmentGroup in segment */);
2028
+
2029
+ assert(segment.removedClientIds !== undefined
2030
+ && segment.removedClientIds[0] === this.collabWindow.clientId,
2031
+ 0x39d /* Rollback segment removedClientId does not match local client */);
2032
+ segment.removedClientIds = undefined;
2033
+ segment.removedSeq = undefined;
2034
+ segment.localRemovedSeq = undefined;
2035
+
2036
+ if (this.mergeTreeDeltaCallback) {
2037
+ const insertOp = createInsertSegmentOp(this.findRollbackPosition(segment), segment);
2038
+ this.mergeTreeDeltaCallback(
2039
+ { op: insertOp },
2040
+ {
2041
+ operation: MergeTreeDeltaType.INSERT,
2042
+ deltaSegments: [{ segment }],
2043
+ });
2044
+ }
2045
+
2046
+ for (let updateNode = segment.parent; updateNode !== undefined; updateNode = updateNode.parent) {
2047
+ this.blockUpdateLength(updateNode, UnassignedSequenceNumber, this.collabWindow.clientId);
2048
+ }
2049
+ }
2050
+ if (pendingSegmentGroup.removedReferences) {
2051
+ for (const ref of pendingSegmentGroup.removedReferences) {
2052
+ const seg = ref.getSegment();
2053
+ if (!seg) {
2054
+ throw new Error("Cannot rollback reference without segment");
2055
+ }
2056
+ const localRefs = seg.localRefs ??= new LocalReferenceCollection(seg);
2057
+ localRefs.addLocalRef(ref, ref.getOffset());
2058
+ }
2059
+ }
2060
+ } else if (op.type === MergeTreeDeltaType.INSERT || op.type === MergeTreeDeltaType.ANNOTATE) {
2061
+ const pendingSegmentGroup = this.pendingSegments?.pop?.();
2062
+ if (pendingSegmentGroup === undefined || pendingSegmentGroup !== localOpMetadata
2063
+ || (op.type === MergeTreeDeltaType.ANNOTATE && !pendingSegmentGroup.previousProps)) {
2064
+ throw new Error("Rollback op doesn't match last edit");
2065
+ }
2066
+ let i = 0;
2067
+ for (const segment of pendingSegmentGroup.segments) {
2068
+ const segmentSegmentGroup = segment.segmentGroups.pop ? segment.segmentGroups.pop() : undefined;
2069
+ assert(segmentSegmentGroup === pendingSegmentGroup, 0x39e /* Unexpected segmentGroup in segment */);
2070
+
2071
+ const start = this.findRollbackPosition(segment);
2072
+ if (op.type === MergeTreeDeltaType.INSERT) {
2073
+ const removeOp = createRemoveRangeOp(start, start + segment.cachedLength);
2074
+ this.markRangeRemoved(
2075
+ start,
2076
+ start + segment.cachedLength,
2077
+ UniversalSequenceNumber,
2078
+ this.collabWindow.clientId,
2079
+ UniversalSequenceNumber,
2080
+ false,
2081
+ { op: removeOp });
2082
+ } else /* op.type === MergeTreeDeltaType.ANNOTATE */ {
2083
+ const props = pendingSegmentGroup.previousProps![i];
2084
+ const rollbackType = (op.combiningOp && op.combiningOp.name === "rewrite") ?
2085
+ PropertiesRollback.Rewrite : PropertiesRollback.Rollback;
2086
+ const annotateOp = createAnnotateRangeOp(start, start + segment.cachedLength, props, undefined);
2087
+ this.annotateRange(
2088
+ start,
2089
+ start + segment.cachedLength,
2090
+ props,
2091
+ undefined,
2092
+ UniversalSequenceNumber,
2093
+ this.collabWindow.clientId,
2094
+ UniversalSequenceNumber,
2095
+ { op: annotateOp },
2096
+ rollbackType);
2097
+ i++;
2098
+ }
2099
+ }
2100
+ } else {
2101
+ throw new Error("Unsupported op type for rollback");
2102
+ }
2103
+ }
2104
+
2105
+ /**
2106
+ * Walk the segments up to the current segment and calculate its position
2107
+ */
2108
+ private findRollbackPosition(segment: ISegment) {
2109
+ let segmentPosition = 0;
2110
+ this.walkAllSegments(this.root, (seg) => {
2111
+ // If we've found the desired segment, terminate the walk and return 'segmentPosition'.
2112
+ if (seg === segment) {
2113
+ return false;
2114
+ }
2115
+
2116
+ // If not removed, increase position
2117
+ if (seg.removedSeq === undefined) {
2118
+ segmentPosition += seg.cachedLength;
2119
+ }
2120
+
2121
+ return true;
2122
+ });
2123
+
2124
+ return segmentPosition;
2125
+ }
2126
+
2529
2127
  private nodeUpdateLengthNewStructure(node: IMergeBlock, recur = false) {
2530
2128
  this.blockUpdate(node);
2531
2129
  if (this.collabWindow.collaborating) {
2532
- node.partialLengths = PartialSequenceLengths.combine(this, node, this.collabWindow, recur);
2130
+ this.localPartialsComputed = false;
2131
+ node.partialLengths = PartialSequenceLengths.combine(node, this.collabWindow, recur);
2533
2132
  }
2534
2133
  }
2535
2134
 
@@ -2545,53 +2144,26 @@ export class MergeTree {
2545
2144
  }
2546
2145
  }
2547
2146
  public createLocalReferencePosition(
2548
- segment: ISegment, offset: number | undefined, refType: ReferenceType, properties: PropertySet | undefined,
2549
- client: Client,
2147
+ segment: ISegment, offset: number, refType: ReferenceType, properties: PropertySet | undefined,
2550
2148
  ): LocalReferencePosition {
2551
- if (isRemoved(segment)) {
2552
- if (!refTypeIncludesFlag(refType, ReferenceType.SlideOnRemove | ReferenceType.Transient)) {
2553
- throw new UsageError(
2554
- "Can only create SlideOnRemove or Transient local reference position on a removed segment");
2555
- }
2149
+ if (
2150
+ isRemovedAndAcked(segment)
2151
+ && !refTypeIncludesFlag(refType, ReferenceType.SlideOnRemove | ReferenceType.Transient)
2152
+ ) {
2153
+ throw new UsageError(
2154
+ "Can only create SlideOnRemove or Transient local reference position on a removed segment",
2155
+ );
2556
2156
  }
2557
2157
  const localRefs = segment.localRefs ?? new LocalReferenceCollection(segment);
2558
2158
  segment.localRefs = localRefs;
2559
2159
 
2560
- const segRef = localRefs.createLocalRef(offset, refType, properties, client);
2160
+ const segRef = localRefs.createLocalRef(offset, refType, properties);
2561
2161
 
2562
2162
  this.blockUpdatePathLengths(segment.parent, TreeMaintenanceSequenceNumber,
2563
2163
  LocalClientId);
2564
2164
  return segRef;
2565
2165
  }
2566
2166
 
2567
- /**
2568
- * @deprecated - use removeLocalReferencePosition
2569
- */
2570
- public removeLocalReference(segment: ISegment, lref: LocalReference) {
2571
- if (segment.localRefs) {
2572
- const removedRef = segment.localRefs.removeLocalRef(lref);
2573
- if (removedRef) {
2574
- this.blockUpdatePathLengths(segment.parent, TreeMaintenanceSequenceNumber,
2575
- LocalClientId);
2576
- }
2577
- }
2578
- }
2579
-
2580
- /**
2581
- * @deprecated - use createLocalReference
2582
- */
2583
- public addLocalReference(lref: LocalReference) {
2584
- const segment = lref.segment!;
2585
- let localRefs = segment.localRefs;
2586
- if (!localRefs) {
2587
- localRefs = new LocalReferenceCollection(segment);
2588
- segment.localRefs = localRefs;
2589
- }
2590
- localRefs.addLocalRef(lref);
2591
- this.blockUpdatePathLengths(segment.parent, TreeMaintenanceSequenceNumber,
2592
- LocalClientId);
2593
- }
2594
-
2595
2167
  private blockUpdate(block: IMergeBlock) {
2596
2168
  let len = 0;
2597
2169
  const hierBlock = block.hierBlock();
@@ -2604,8 +2176,13 @@ export class MergeTree {
2604
2176
  const child = block.children[i];
2605
2177
  len += nodeTotalLength(this, child) ?? 0;
2606
2178
  if (hierBlock) {
2607
- hierBlock.addNodeReferences(this, child);
2608
- }
2179
+ addNodeReferences(
2180
+ this,
2181
+ child,
2182
+ hierBlock.rightmostTiles,
2183
+ hierBlock.leftmostTiles,
2184
+ hierBlock.rangeStacks);
2185
+ }
2609
2186
  if (this.blockUpdateActions) {
2610
2187
  this.blockUpdateActions.child(block, i);
2611
2188
  }
@@ -2632,6 +2209,7 @@ export class MergeTree {
2632
2209
 
2633
2210
  private blockUpdateLength(node: IMergeBlock, seq: number, clientId: number) {
2634
2211
  this.blockUpdate(node);
2212
+ this.localPartialsComputed = false;
2635
2213
  if (
2636
2214
  this.collabWindow.collaborating
2637
2215
  && seq !== UnassignedSequenceNumber
@@ -2642,9 +2220,9 @@ export class MergeTree {
2642
2220
  && MergeTree.options.incrementalUpdate
2643
2221
  && clientId !== NonCollabClient
2644
2222
  ) {
2645
- node.partialLengths.update(this, node, seq, clientId, this.collabWindow);
2223
+ node.partialLengths.update(node, seq, clientId, this.collabWindow);
2646
2224
  } else {
2647
- node.partialLengths = PartialSequenceLengths.combine(this, node, this.collabWindow);
2225
+ node.partialLengths = PartialSequenceLengths.combine(node, this.collabWindow);
2648
2226
  }
2649
2227
  }
2650
2228
  }
@@ -2679,6 +2257,10 @@ export class MergeTree {
2679
2257
  this.nodeMap(this.root, actions, 0, refSeq, clientId, accum, start, end);
2680
2258
  }
2681
2259
 
2260
+ /**
2261
+ * @deprecated for internal use only. public export will be removed.
2262
+ * @internal
2263
+ */
2682
2264
  public incrementalBlockMap<TContext>(stateStack: Stack<IncrementalMapState<TContext>>) {
2683
2265
  while (!stateStack.empty()) {
2684
2266
  // We already check the stack is not empty