@fluidframework/merge-tree 1.2.7 → 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.
- package/.mocharc.js +12 -0
- package/README.md +2 -2
- package/dist/MergeTreeTextHelper.d.ts +23 -0
- package/dist/MergeTreeTextHelper.d.ts.map +1 -0
- package/dist/MergeTreeTextHelper.js +133 -0
- package/dist/MergeTreeTextHelper.js.map +1 -0
- package/dist/base.d.ts +2 -26
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js.map +1 -1
- package/dist/client.d.ts +27 -16
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +81 -101
- package/dist/client.js.map +1 -1
- package/dist/collections/heap.d.ts +28 -0
- package/dist/collections/heap.d.ts.map +1 -0
- package/dist/collections/heap.js +65 -0
- package/dist/collections/heap.js.map +1 -0
- package/dist/collections/index.d.ts +11 -0
- package/dist/collections/index.d.ts.map +1 -0
- package/dist/collections/index.js +23 -0
- package/dist/collections/index.js.map +1 -0
- package/dist/collections/intervalTree.d.ts +60 -0
- package/dist/collections/intervalTree.d.ts.map +1 -0
- package/dist/collections/intervalTree.js +99 -0
- package/dist/collections/intervalTree.js.map +1 -0
- package/dist/collections/list.d.ts +39 -0
- package/dist/collections/list.d.ts.map +1 -0
- package/dist/collections/list.js +155 -0
- package/dist/collections/list.js.map +1 -0
- package/dist/collections/rbTree.d.ts +154 -0
- package/dist/collections/rbTree.d.ts.map +1 -0
- package/dist/{collections.js → collections/rbTree.js} +15 -478
- package/dist/collections/rbTree.js.map +1 -0
- package/dist/collections/stack.d.ts +16 -0
- package/dist/collections/stack.d.ts.map +1 -0
- package/dist/collections/stack.js +30 -0
- package/dist/collections/stack.js.map +1 -0
- package/dist/collections/tst.d.ts +55 -0
- package/dist/collections/tst.d.ts.map +1 -0
- package/dist/collections/tst.js +171 -0
- package/dist/collections/tst.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/localReference.d.ts +48 -99
- package/dist/localReference.d.ts.map +1 -1
- package/dist/localReference.js +132 -169
- package/dist/localReference.js.map +1 -1
- package/dist/mergeTree.d.ts +71 -302
- package/dist/mergeTree.d.ts.map +1 -1
- package/dist/mergeTree.js +395 -642
- package/dist/mergeTree.js.map +1 -1
- package/dist/mergeTreeDeltaCallback.d.ts +1 -1
- package/dist/mergeTreeDeltaCallback.d.ts.map +1 -1
- package/dist/mergeTreeDeltaCallback.js.map +1 -1
- package/dist/mergeTreeNodes.d.ts +344 -0
- package/dist/mergeTreeNodes.d.ts.map +1 -0
- package/dist/mergeTreeNodes.js +383 -0
- package/dist/mergeTreeNodes.js.map +1 -0
- package/dist/mergeTreeTracking.d.ts +1 -1
- package/dist/mergeTreeTracking.d.ts.map +1 -1
- package/dist/mergeTreeTracking.js.map +1 -1
- package/dist/opBuilder.d.ts +1 -1
- package/dist/opBuilder.d.ts.map +1 -1
- package/dist/opBuilder.js.map +1 -1
- package/dist/partialLengths.d.ts +188 -18
- package/dist/partialLengths.d.ts.map +1 -1
- package/dist/partialLengths.js +495 -253
- package/dist/partialLengths.js.map +1 -1
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js.map +1 -1
- package/dist/referencePositions.d.ts +6 -26
- package/dist/referencePositions.d.ts.map +1 -1
- package/dist/referencePositions.js +3 -20
- package/dist/referencePositions.js.map +1 -1
- package/dist/segmentGroupCollection.d.ts +3 -1
- package/dist/segmentGroupCollection.d.ts.map +1 -1
- package/dist/segmentGroupCollection.js +14 -1
- package/dist/segmentGroupCollection.js.map +1 -1
- package/dist/segmentPropertiesManager.d.ts +10 -1
- package/dist/segmentPropertiesManager.d.ts.map +1 -1
- package/dist/segmentPropertiesManager.js +42 -13
- package/dist/segmentPropertiesManager.js.map +1 -1
- package/dist/snapshotChunks.d.ts +2 -1
- package/dist/snapshotChunks.d.ts.map +1 -1
- package/dist/snapshotChunks.js.map +1 -1
- package/dist/snapshotLoader.d.ts.map +1 -1
- package/dist/snapshotLoader.js.map +1 -1
- package/dist/snapshotV1.d.ts +1 -1
- package/dist/snapshotV1.d.ts.map +1 -1
- package/dist/snapshotV1.js +1 -1
- package/dist/snapshotV1.js.map +1 -1
- package/dist/snapshotlegacy.d.ts +5 -1
- package/dist/snapshotlegacy.d.ts.map +1 -1
- package/dist/snapshotlegacy.js +4 -0
- package/dist/snapshotlegacy.js.map +1 -1
- package/dist/sortedSegmentSet.d.ts +1 -1
- package/dist/sortedSegmentSet.d.ts.map +1 -1
- package/dist/sortedSegmentSet.js.map +1 -1
- package/dist/textSegment.d.ts +7 -7
- package/dist/textSegment.d.ts.map +1 -1
- package/dist/textSegment.js +3 -125
- package/dist/textSegment.js.map +1 -1
- package/{DEV.md → docs/DEV.md} +2 -2
- package/docs/Obliterate.md +639 -0
- package/{REFERENCEPOSITIONS.md → docs/REFERENCEPOSITIONS.md} +2 -2
- package/lib/MergeTreeTextHelper.d.ts +23 -0
- package/lib/MergeTreeTextHelper.d.ts.map +1 -0
- package/lib/MergeTreeTextHelper.js +129 -0
- package/lib/MergeTreeTextHelper.js.map +1 -0
- package/lib/base.d.ts +2 -26
- package/lib/base.d.ts.map +1 -1
- package/lib/base.js.map +1 -1
- package/lib/client.d.ts +27 -16
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +79 -99
- package/lib/client.js.map +1 -1
- package/lib/collections/heap.d.ts +28 -0
- package/lib/collections/heap.d.ts.map +1 -0
- package/lib/collections/heap.js +61 -0
- package/lib/collections/heap.js.map +1 -0
- package/lib/collections/index.d.ts +11 -0
- package/lib/collections/index.d.ts.map +1 -0
- package/lib/collections/index.js +11 -0
- package/lib/collections/index.js.map +1 -0
- package/lib/collections/intervalTree.d.ts +60 -0
- package/lib/collections/intervalTree.d.ts.map +1 -0
- package/lib/collections/intervalTree.js +94 -0
- package/lib/collections/intervalTree.js.map +1 -0
- package/lib/collections/list.d.ts +39 -0
- package/lib/collections/list.d.ts.map +1 -0
- package/lib/collections/list.js +149 -0
- package/lib/collections/list.js.map +1 -0
- package/lib/collections/rbTree.d.ts +154 -0
- package/lib/collections/rbTree.d.ts.map +1 -0
- package/lib/{collections.js → collections/rbTree.js} +14 -469
- package/lib/collections/rbTree.js.map +1 -0
- package/lib/collections/stack.d.ts +16 -0
- package/lib/collections/stack.d.ts.map +1 -0
- package/lib/collections/stack.js +26 -0
- package/lib/collections/stack.js.map +1 -0
- package/lib/collections/tst.d.ts +55 -0
- package/lib/collections/tst.d.ts.map +1 -0
- package/lib/collections/tst.js +167 -0
- package/lib/collections/tst.js.map +1 -0
- package/lib/index.d.ts +3 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/localReference.d.ts +48 -99
- package/lib/localReference.d.ts.map +1 -1
- package/lib/localReference.js +132 -170
- package/lib/localReference.js.map +1 -1
- package/lib/mergeTree.d.ts +71 -302
- package/lib/mergeTree.d.ts.map +1 -1
- package/lib/mergeTree.js +371 -607
- package/lib/mergeTree.js.map +1 -1
- package/lib/mergeTreeDeltaCallback.d.ts +1 -1
- package/lib/mergeTreeDeltaCallback.d.ts.map +1 -1
- package/lib/mergeTreeDeltaCallback.js.map +1 -1
- package/lib/mergeTreeNodes.d.ts +344 -0
- package/lib/mergeTreeNodes.d.ts.map +1 -0
- package/lib/mergeTreeNodes.js +369 -0
- package/lib/mergeTreeNodes.js.map +1 -0
- package/lib/mergeTreeTracking.d.ts +1 -1
- package/lib/mergeTreeTracking.d.ts.map +1 -1
- package/lib/mergeTreeTracking.js.map +1 -1
- package/lib/opBuilder.d.ts +1 -1
- package/lib/opBuilder.d.ts.map +1 -1
- package/lib/opBuilder.js.map +1 -1
- package/lib/partialLengths.d.ts +188 -18
- package/lib/partialLengths.d.ts.map +1 -1
- package/lib/partialLengths.js +491 -249
- package/lib/partialLengths.js.map +1 -1
- package/lib/properties.d.ts.map +1 -1
- package/lib/properties.js.map +1 -1
- package/lib/referencePositions.d.ts +6 -26
- package/lib/referencePositions.d.ts.map +1 -1
- package/lib/referencePositions.js +3 -20
- package/lib/referencePositions.js.map +1 -1
- package/lib/segmentGroupCollection.d.ts +3 -1
- package/lib/segmentGroupCollection.d.ts.map +1 -1
- package/lib/segmentGroupCollection.js +14 -1
- package/lib/segmentGroupCollection.js.map +1 -1
- package/lib/segmentPropertiesManager.d.ts +10 -1
- package/lib/segmentPropertiesManager.d.ts.map +1 -1
- package/lib/segmentPropertiesManager.js +42 -13
- package/lib/segmentPropertiesManager.js.map +1 -1
- package/lib/snapshotChunks.d.ts +2 -1
- package/lib/snapshotChunks.d.ts.map +1 -1
- package/lib/snapshotChunks.js.map +1 -1
- package/lib/snapshotLoader.d.ts.map +1 -1
- package/lib/snapshotLoader.js.map +1 -1
- package/lib/snapshotV1.d.ts +1 -1
- package/lib/snapshotV1.d.ts.map +1 -1
- package/lib/snapshotV1.js +1 -1
- package/lib/snapshotV1.js.map +1 -1
- package/lib/snapshotlegacy.d.ts +5 -1
- package/lib/snapshotlegacy.d.ts.map +1 -1
- package/lib/snapshotlegacy.js +4 -0
- package/lib/snapshotlegacy.js.map +1 -1
- package/lib/sortedSegmentSet.d.ts +1 -1
- package/lib/sortedSegmentSet.d.ts.map +1 -1
- package/lib/sortedSegmentSet.js.map +1 -1
- package/lib/textSegment.d.ts +7 -7
- package/lib/textSegment.d.ts.map +1 -1
- package/lib/textSegment.js +1 -122
- package/lib/textSegment.js.map +1 -1
- package/package.json +99 -20
- package/src/MergeTreeTextHelper.ts +170 -0
- package/src/base.ts +2 -35
- package/src/client.ts +91 -111
- package/src/collections/heap.ts +75 -0
- package/src/collections/index.ts +11 -0
- package/src/collections/intervalTree.ts +146 -0
- package/src/collections/list.ts +165 -0
- package/src/{collections.ts → collections/rbTree.ts} +84 -563
- package/src/collections/stack.ts +27 -0
- package/src/collections/tst.ts +212 -0
- package/src/index.ts +8 -2
- package/src/localReference.ts +152 -203
- package/src/mergeTree.ts +578 -996
- package/src/mergeTreeDeltaCallback.ts +1 -1
- package/src/mergeTreeNodes.ts +752 -0
- package/src/mergeTreeTracking.ts +1 -1
- package/src/opBuilder.ts +1 -1
- package/src/partialLengths.ts +631 -258
- package/src/properties.ts +1 -0
- package/src/referencePositions.ts +10 -44
- package/src/segmentGroupCollection.ts +17 -2
- package/src/segmentPropertiesManager.ts +46 -12
- package/src/snapshotChunks.ts +2 -1
- package/src/snapshotLoader.ts +2 -1
- package/src/snapshotV1.ts +3 -3
- package/src/snapshotlegacy.ts +6 -2
- package/src/sortedSegmentSet.ts +1 -1
- package/src/textSegment.ts +10 -157
- package/dist/collections.d.ts +0 -197
- package/dist/collections.d.ts.map +0 -1
- package/dist/collections.js.map +0 -1
- package/lib/collections.d.ts +0 -197
- package/lib/collections.d.ts.map +0 -1
- package/lib/collections.js.map +0 -1
package/src/partialLengths.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { assert } from "@fluidframework/common-utils";
|
|
7
|
-
import { Property } from "./
|
|
8
|
-
import { RedBlackTree } from "./collections";
|
|
7
|
+
import { Property, RedBlackTree } from "./collections";
|
|
9
8
|
import { UnassignedSequenceNumber } from "./constants";
|
|
10
9
|
import {
|
|
11
10
|
CollaborationWindow,
|
|
@@ -13,9 +12,8 @@ import {
|
|
|
13
12
|
IMergeBlock,
|
|
14
13
|
IRemovalInfo,
|
|
15
14
|
ISegment,
|
|
16
|
-
MergeTree,
|
|
17
15
|
toRemovalInfo,
|
|
18
|
-
} from "./
|
|
16
|
+
} from "./mergeTreeNodes";
|
|
19
17
|
|
|
20
18
|
interface IOverlapClient {
|
|
21
19
|
clientId: number;
|
|
@@ -47,18 +45,137 @@ function latestLEQ(a: PartialSequenceLength[], key: number) {
|
|
|
47
45
|
return best;
|
|
48
46
|
}
|
|
49
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Tracks length information for a part of a MergeTree (block) at a given time (seq).
|
|
50
|
+
* These objects are associated with internal nodes (i.e. blocks).
|
|
51
|
+
*/
|
|
50
52
|
export interface PartialSequenceLength {
|
|
53
|
+
/**
|
|
54
|
+
* Sequence number
|
|
55
|
+
*/
|
|
51
56
|
seq: number;
|
|
57
|
+
/**
|
|
58
|
+
* The length of the associated block.
|
|
59
|
+
*/
|
|
52
60
|
len: number;
|
|
61
|
+
/**
|
|
62
|
+
* The delta between the current length of the associated block and its length at the previous seq number.
|
|
63
|
+
*/
|
|
53
64
|
seglen: number;
|
|
65
|
+
/**
|
|
66
|
+
* clientId for the client that submitted the op with sequence number `seq`.
|
|
67
|
+
*/
|
|
54
68
|
clientId?: number;
|
|
69
|
+
/**
|
|
70
|
+
* This field maps each client to the size of the intersection between segments deleted at this seq
|
|
71
|
+
* and segments concurrently deleted by that client.
|
|
72
|
+
*
|
|
73
|
+
* For example, this PartialSequenceLength:
|
|
74
|
+
* ```typescript
|
|
75
|
+
* {
|
|
76
|
+
* seq: 5,
|
|
77
|
+
* len: 100,
|
|
78
|
+
* seglen: -10,
|
|
79
|
+
* clientId: 0,
|
|
80
|
+
* overlapRemoveClients: <RedBlack tree with key-values expressed by>{
|
|
81
|
+
* 1: { clientId: 1, seglen: -5 },
|
|
82
|
+
* 3: { clientId: 3, seglen: -10 }
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* corresponds to an op submitted by client 0 which:
|
|
88
|
+
* - reduces the length of this block by 10 (it may have deleted a single segment of length 10,
|
|
89
|
+
* several segments totalling length 10, or even delete and add content for a total reduction of 10 length)
|
|
90
|
+
* - was concurrent to an op submitted by client 1 that also removed some of the same segments,
|
|
91
|
+
* whose length totalled 5
|
|
92
|
+
* - was concurrent to an op submitted by client 3 that removed some of the same segments,
|
|
93
|
+
* whose length totalled 10
|
|
94
|
+
*/
|
|
55
95
|
overlapRemoveClients?: RedBlackTree<number, IOverlapClient>;
|
|
56
96
|
}
|
|
57
97
|
|
|
98
|
+
interface UnsequencedPartialLengthInfo {
|
|
99
|
+
/**
|
|
100
|
+
* Contains entries for all local operations.
|
|
101
|
+
* The "seq" field of each entry actually corresponds to the delta at that localSeq on the local client.
|
|
102
|
+
*/
|
|
103
|
+
partialLengths: PartialSequenceLength[];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Only contains entries for segments (or aggregates thereof) which were concurrently deleted
|
|
107
|
+
* by another client. Ordered by `seq` of the removing client.
|
|
108
|
+
*
|
|
109
|
+
* The "length" field of these entries is not populated. This is because pre-computing the lengths
|
|
110
|
+
* of segments doesn't help given the usage pattern.
|
|
111
|
+
*
|
|
112
|
+
* These entries need both `seq` and `localSeq`, because a given segment remove is double-counted iff
|
|
113
|
+
* the refSeq exceeds the seq of the remote remove AND the localSeq exceeds the localSeq of the local remove.
|
|
114
|
+
*/
|
|
115
|
+
overlappingRemoves: LocalPartialSequenceLength[];
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Cached keyed on refSeq which stores length information for the total overlap of removed segments at
|
|
119
|
+
* that refSeq.
|
|
120
|
+
* This information is derivable from the entries of `overlappingRemoves`.
|
|
121
|
+
*
|
|
122
|
+
* Like the `partialLengths` field, `seq` on each entry is actually the local seq.
|
|
123
|
+
* See `computeOverlappingLocalRemoves` for more information.
|
|
124
|
+
*/
|
|
125
|
+
cachedOverlappingByRefSeq: Map<number, PartialSequenceLength[]>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface LocalPartialSequenceLength extends PartialSequenceLength {
|
|
129
|
+
/**
|
|
130
|
+
* Local sequence number
|
|
131
|
+
*/
|
|
132
|
+
localSeq: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
58
135
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
136
|
+
* Keeps track of partial sums of segment lengths for all sequence numbers in the current collaboration window.
|
|
137
|
+
* Only used during active collaboration.
|
|
138
|
+
*
|
|
139
|
+
* This class is associated with an internal node (block) of a MergeTree. It efficiently answers queries of the form
|
|
140
|
+
* "What is the length of `block` from the perspective of some particular seq and clientId?".
|
|
141
|
+
*
|
|
142
|
+
* It also supports incremental updating of state for newly-sequenced ops that don't affect the structure of the
|
|
143
|
+
* MergeTree.
|
|
144
|
+
*
|
|
145
|
+
* To answer these queries, it pre-builds several lists which track the length of the block at a per-sequence-number
|
|
146
|
+
* level. These lists are:
|
|
147
|
+
*
|
|
148
|
+
* 1. (`partialLengths`): Stores the total length of the block.
|
|
149
|
+
* 2. (`clientSeqNumbers[clientId]`): Stores only the total lengths of segments submitted by `clientId`. [see footnote]
|
|
150
|
+
*
|
|
151
|
+
* The reason both lists are necessary is that resolving the length of the block from the perspective of
|
|
152
|
+
* (clientId, refSeq) requires including both of the following types of segments:
|
|
153
|
+
* 1. Segments sequenced before `refSeq`
|
|
154
|
+
* 2. Segments submitted by `clientId`
|
|
155
|
+
*
|
|
156
|
+
* This is possible with the above bookkeeping, using:
|
|
157
|
+
*
|
|
158
|
+
* (length of the block at the minimum sequence number)
|
|
159
|
+
* + (partialLengths total length at refSeq)
|
|
160
|
+
* + (clientSeqNumbers total length at most recent op)
|
|
161
|
+
* - (clientSeqNumbers total length at refSeq)
|
|
162
|
+
*
|
|
163
|
+
* where the subtraction avoids double-counting segments submitted by clientId sequenced within the collab window.
|
|
164
|
+
*
|
|
165
|
+
* To enable reconnect, if constructed with `computeLocalPartials === true` it also supports querying for the length of
|
|
166
|
+
* the block from the perspective of the local client at a particular `refSeq` and `localSeq`. This computation is
|
|
167
|
+
* similar to the above:
|
|
168
|
+
*
|
|
169
|
+
* (length of the block at the minimum sequence number)
|
|
170
|
+
* + (partialLengths total length at refSeq)
|
|
171
|
+
* + (unsequenced edits' total length submitted before localSeq)
|
|
172
|
+
* - (overlapping remove of the unsequenced edits' total length at refSeq)
|
|
173
|
+
*
|
|
174
|
+
* This algorithm scales roughly linearly with number of editing clients and the size of the collab window.
|
|
175
|
+
* (certain unlikely sequences of operations may introduce log factors on those variables)
|
|
176
|
+
*
|
|
177
|
+
* Note: there is some slight complication with clientSeqNumbers resulting from the possibility of different clients
|
|
178
|
+
* concurrently removing the same segment. See the field's documentation for more details.
|
|
62
179
|
*/
|
|
63
180
|
export class PartialSequenceLengths {
|
|
64
181
|
public static options = {
|
|
@@ -66,135 +183,78 @@ export class PartialSequenceLengths {
|
|
|
66
183
|
zamboni: true,
|
|
67
184
|
};
|
|
68
185
|
|
|
69
|
-
public static combine(mergeTree: MergeTree, block: IMergeBlock, collabWindow: CollaborationWindow, recur = false) {
|
|
70
|
-
return PartialSequenceLengths.combineBranch(mergeTree, block, collabWindow, recur);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
186
|
/**
|
|
74
187
|
* Combine the partial lengths of block's children
|
|
75
|
-
* @param block - an interior node
|
|
76
|
-
* has its partials up to date
|
|
77
|
-
* @param collabWindow - segment window of the segment tree containing
|
|
188
|
+
* @param block - an interior node. If `recur` is false, it is assumed that each interior node child of this block
|
|
189
|
+
* has its partials up to date.
|
|
190
|
+
* @param collabWindow - segment window of the segment tree containing `block`.
|
|
191
|
+
* @param recur - whether to recursively compute partial lengths for internal children of `block`.
|
|
192
|
+
* This incurs more work, but gives correct bookkeeping in the case that a descendant in the merge tree has been
|
|
193
|
+
* modified without bubbling up the resulting partial length change to this block's partials.
|
|
194
|
+
* @param computeLocalPartials - whether to compute partial length information about local unsequenced ops.
|
|
195
|
+
* This enables querying for the length of the block at a given localSeq, but incurs extra work.
|
|
196
|
+
* Local partial information doesn't support `update`.
|
|
78
197
|
*/
|
|
79
|
-
|
|
80
|
-
mergeTree: MergeTree,
|
|
198
|
+
public static combine(
|
|
81
199
|
block: IMergeBlock,
|
|
82
200
|
collabWindow: CollaborationWindow,
|
|
83
|
-
recur = false
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
function cloneOverlapRemoveClients(oldTree: RedBlackTree<number, IOverlapClient> | undefined) {
|
|
89
|
-
if (!oldTree) { return undefined; }
|
|
90
|
-
const newTree = new RedBlackTree<number, IOverlapClient>(compareNumbers);
|
|
91
|
-
oldTree.map((bProp: Property<number, IOverlapClient>) => {
|
|
92
|
-
newTree.put(bProp.data.clientId, { ...bProp.data });
|
|
93
|
-
return true;
|
|
94
|
-
});
|
|
95
|
-
return newTree;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function combineOverlapClients(a: PartialSequenceLength, b: PartialSequenceLength) {
|
|
99
|
-
const overlapRemoveClientsA = a.overlapRemoveClients;
|
|
100
|
-
if (overlapRemoveClientsA) {
|
|
101
|
-
if (b.overlapRemoveClients) {
|
|
102
|
-
b.overlapRemoveClients.map((bProp: Property<number, IOverlapClient>) => {
|
|
103
|
-
const aProp = overlapRemoveClientsA.get(bProp.key);
|
|
104
|
-
if (aProp) {
|
|
105
|
-
aProp.data.seglen += bProp.data.seglen;
|
|
106
|
-
} else {
|
|
107
|
-
overlapRemoveClientsA.put(bProp.data.clientId, { ...bProp.data });
|
|
108
|
-
}
|
|
109
|
-
return true;
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
a.overlapRemoveClients = cloneOverlapRemoveClients(b.overlapRemoveClients);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function addNext(partialLength: PartialSequenceLength) {
|
|
118
|
-
const seq = partialLength.seq;
|
|
119
|
-
let pLen = 0;
|
|
120
|
-
|
|
121
|
-
if (prevPartial) {
|
|
122
|
-
if (prevPartial.seq === partialLength.seq) {
|
|
123
|
-
prevPartial.seglen += partialLength.seglen;
|
|
124
|
-
prevPartial.len += partialLength.seglen;
|
|
125
|
-
combineOverlapClients(prevPartial, partialLength);
|
|
126
|
-
return;
|
|
127
|
-
} else {
|
|
128
|
-
pLen = prevPartial.len;
|
|
129
|
-
// Previous sequence number is finished
|
|
130
|
-
combinedPartialLengths.addClientSeqNumberFromPartial(prevPartial);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
prevPartial = {
|
|
134
|
-
clientId: partialLength.clientId,
|
|
135
|
-
len: pLen + partialLength.seglen,
|
|
136
|
-
overlapRemoveClients: cloneOverlapRemoveClients(partialLength.overlapRemoveClients),
|
|
137
|
-
seglen: partialLength.seglen,
|
|
138
|
-
seq,
|
|
139
|
-
};
|
|
140
|
-
combinedPartialLengths.partialLengths.push(prevPartial);
|
|
141
|
-
}
|
|
201
|
+
recur = false,
|
|
202
|
+
computeLocalPartials = false,
|
|
203
|
+
): PartialSequenceLengths {
|
|
204
|
+
const leafPartialLengths = PartialSequenceLengths.fromLeaves(block, collabWindow, computeLocalPartials);
|
|
142
205
|
|
|
206
|
+
let hasInternalChild = false;
|
|
143
207
|
const childPartials: PartialSequenceLengths[] = [];
|
|
144
208
|
for (let i = 0; i < block.childCount; i++) {
|
|
145
209
|
const child = block.children[i];
|
|
146
210
|
if (!child.isLeaf()) {
|
|
147
|
-
|
|
211
|
+
hasInternalChild = true;
|
|
148
212
|
if (recur) {
|
|
149
|
-
|
|
150
|
-
PartialSequenceLengths.combine(
|
|
213
|
+
child.partialLengths =
|
|
214
|
+
PartialSequenceLengths.combine(child, collabWindow, true, computeLocalPartials);
|
|
151
215
|
}
|
|
152
216
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
153
|
-
childPartials.push(
|
|
217
|
+
childPartials.push(child.partialLengths!);
|
|
154
218
|
}
|
|
155
219
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
220
|
+
|
|
221
|
+
// If there are no internal children, the PartialSequenceLengths returns from `fromLeaves` is exactly correct.
|
|
222
|
+
// Otherwise, we must additively combine all of the children partial lengths to get this block's totals.
|
|
223
|
+
const combinedPartialLengths = hasInternalChild ?
|
|
224
|
+
new PartialSequenceLengths(collabWindow.minSeq, computeLocalPartials) : leafPartialLengths;
|
|
225
|
+
if (hasInternalChild) {
|
|
226
|
+
if (leafPartialLengths.partialLengths.length > 0) {
|
|
160
227
|
// Some children were leaves; add combined partials from these segments
|
|
161
|
-
childPartials.push(
|
|
162
|
-
childPartialsLen++;
|
|
163
|
-
combinedPartialLengths = new PartialSequenceLengths(collabWindow.minSeq);
|
|
228
|
+
childPartials.push(leafPartialLengths);
|
|
164
229
|
}
|
|
165
|
-
|
|
166
|
-
const
|
|
230
|
+
|
|
231
|
+
const childPartialsLen = childPartials.length;
|
|
232
|
+
|
|
233
|
+
const childPartialLengths: PartialSequenceLength[][] = [];
|
|
234
|
+
const childUnsequencedPartialLengths: PartialSequenceLength[][] = [];
|
|
235
|
+
const childOverlapRemoves: LocalPartialSequenceLength[][] = [];
|
|
167
236
|
for (let i = 0; i < childPartialsLen; i++) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
combinedPartialLengths.minLength +=
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
while (outerIndexOfEarliest >= 0) {
|
|
176
|
-
outerIndexOfEarliest = -1;
|
|
177
|
-
for (let k = 0; k < childPartialsLen; k++) {
|
|
178
|
-
// Find next earliest sequence number
|
|
179
|
-
if (indices[k] < childPartialsCounts[k]) {
|
|
180
|
-
const cpLen = childPartials[k].partialLengths[indices[k]];
|
|
181
|
-
|
|
182
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
183
|
-
if ((outerIndexOfEarliest < 0) || (cpLen.seq < earliestPartialLength!.seq)) {
|
|
184
|
-
outerIndexOfEarliest = k;
|
|
185
|
-
earliestPartialLength = cpLen;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (outerIndexOfEarliest >= 0) {
|
|
190
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
191
|
-
addNext(earliestPartialLength!);
|
|
192
|
-
indices[outerIndexOfEarliest]++;
|
|
237
|
+
const { segmentCount, minLength, partialLengths, unsequencedRecords } = childPartials[i];
|
|
238
|
+
combinedPartialLengths.segmentCount += segmentCount;
|
|
239
|
+
combinedPartialLengths.minLength += minLength;
|
|
240
|
+
childPartialLengths.push(partialLengths);
|
|
241
|
+
if (unsequencedRecords) {
|
|
242
|
+
childUnsequencedPartialLengths.push(unsequencedRecords.partialLengths);
|
|
243
|
+
childOverlapRemoves.push(unsequencedRecords.overlappingRemoves);
|
|
193
244
|
}
|
|
194
245
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
246
|
+
|
|
247
|
+
combinedPartialLengths.partialLengths.push(...mergePartialLengths(childPartialLengths));
|
|
248
|
+
if (computeLocalPartials) {
|
|
249
|
+
combinedPartialLengths.unsequencedRecords = {
|
|
250
|
+
partialLengths: mergePartialLengths(childUnsequencedPartialLengths),
|
|
251
|
+
overlappingRemoves: Array.from(mergeSortedListsBySeq(childOverlapRemoves)),
|
|
252
|
+
cachedOverlappingByRefSeq: new Map(),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const partial of combinedPartialLengths.partialLengths) {
|
|
257
|
+
combinedPartialLengths.addClientSeqNumberFromPartial(partial);
|
|
198
258
|
}
|
|
199
259
|
}
|
|
200
260
|
// TODO: incremental zamboni during build
|
|
@@ -203,16 +263,22 @@ export class PartialSequenceLengths {
|
|
|
203
263
|
}
|
|
204
264
|
|
|
205
265
|
if (PartialSequenceLengths.options.verify) {
|
|
206
|
-
|
|
266
|
+
verify(combinedPartialLengths);
|
|
207
267
|
}
|
|
208
268
|
|
|
209
269
|
return combinedPartialLengths;
|
|
210
270
|
}
|
|
211
271
|
|
|
272
|
+
/**
|
|
273
|
+
* @returns a PartialSequenceLengths structure which tracks only lengths of leaf children of the provided
|
|
274
|
+
* IMergeBlock.
|
|
275
|
+
*/
|
|
212
276
|
private static fromLeaves(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
277
|
+
block: IMergeBlock,
|
|
278
|
+
collabWindow: CollaborationWindow,
|
|
279
|
+
computeLocalPartials: boolean,
|
|
280
|
+
): PartialSequenceLengths {
|
|
281
|
+
const combinedPartialLengths = new PartialSequenceLengths(collabWindow.minSeq, computeLocalPartials);
|
|
216
282
|
combinedPartialLengths.segmentCount = block.childCount;
|
|
217
283
|
|
|
218
284
|
function seqLTE(seq: number | undefined, minSeq: number) {
|
|
@@ -227,21 +293,16 @@ export class PartialSequenceLengths {
|
|
|
227
293
|
if (seqLTE(segment.seq, collabWindow.minSeq)) {
|
|
228
294
|
combinedPartialLengths.minLength += segment.cachedLength;
|
|
229
295
|
} else {
|
|
230
|
-
|
|
231
|
-
PartialSequenceLengths.insertSegment(combinedPartialLengths, segment);
|
|
232
|
-
}
|
|
296
|
+
PartialSequenceLengths.insertSegment(combinedPartialLengths, segment);
|
|
233
297
|
}
|
|
234
298
|
const removalInfo = toRemovalInfo(segment);
|
|
235
299
|
if (seqLTE(removalInfo?.removedSeq, collabWindow.minSeq)) {
|
|
236
300
|
combinedPartialLengths.minLength -= segment.cachedLength;
|
|
237
|
-
} else {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
segment,
|
|
243
|
-
removalInfo);
|
|
244
|
-
}
|
|
301
|
+
} else if (removalInfo !== undefined) {
|
|
302
|
+
PartialSequenceLengths.insertSegment(
|
|
303
|
+
combinedPartialLengths,
|
|
304
|
+
segment,
|
|
305
|
+
removalInfo);
|
|
245
306
|
}
|
|
246
307
|
}
|
|
247
308
|
}
|
|
@@ -256,9 +317,20 @@ export class PartialSequenceLengths {
|
|
|
256
317
|
prevLen = seqPartials[i].len;
|
|
257
318
|
combinedPartialLengths.addClientSeqNumberFromPartial(seqPartials[i]);
|
|
258
319
|
}
|
|
320
|
+
prevLen = 0;
|
|
321
|
+
|
|
322
|
+
if (combinedPartialLengths.unsequencedRecords !== undefined) {
|
|
323
|
+
const localPartials = combinedPartialLengths.unsequencedRecords.partialLengths;
|
|
324
|
+
for (const partial of localPartials) {
|
|
325
|
+
partial.len = prevLen + partial.seglen;
|
|
326
|
+
prevLen = partial.len;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
259
330
|
if (PartialSequenceLengths.options.verify) {
|
|
260
|
-
|
|
331
|
+
verify(combinedPartialLengths);
|
|
261
332
|
}
|
|
333
|
+
return combinedPartialLengths;
|
|
262
334
|
}
|
|
263
335
|
|
|
264
336
|
private static getOverlapClients(overlapClientIds: number[], seglen: number) {
|
|
@@ -288,63 +360,114 @@ export class PartialSequenceLengths {
|
|
|
288
360
|
}
|
|
289
361
|
}
|
|
290
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Inserts length information about the insertion of `segment` into `combinedPartialLengths.partialLengths`.
|
|
365
|
+
* Does not update the clientSeqNumbers field to account for this segment.
|
|
366
|
+
* If `removalInfo` is defined, this operation updates the bookkeeping to account for the removal of this
|
|
367
|
+
* segment at the removedSeq instead.
|
|
368
|
+
* When the insertion or removal of the segment is un-acked and `combinedPartialLengths` is meant to compute
|
|
369
|
+
* such records, this does the analogous addition to the bookkeeping for the local segment in
|
|
370
|
+
* `combinedPartialLengths.unsequencedRecords`.
|
|
371
|
+
*/
|
|
291
372
|
private static insertSegment(
|
|
292
373
|
combinedPartialLengths: PartialSequenceLengths,
|
|
293
374
|
segment: ISegment,
|
|
294
375
|
removalInfo?: IRemovalInfo) {
|
|
376
|
+
const isLocal = (removalInfo === undefined && segment.seq === UnassignedSequenceNumber)
|
|
377
|
+
|| (removalInfo !== undefined && segment.removedSeq === UnassignedSequenceNumber);
|
|
295
378
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
296
|
-
let
|
|
379
|
+
let seqOrLocalSeq = isLocal ? segment.localSeq! : segment.seq!;
|
|
297
380
|
let segmentLen = segment.cachedLength;
|
|
298
381
|
let clientId = segment.clientId;
|
|
299
382
|
let removeClientOverlap: number[] | undefined;
|
|
300
383
|
|
|
301
384
|
if (removalInfo) {
|
|
302
|
-
|
|
385
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
386
|
+
seqOrLocalSeq = isLocal ? removalInfo.localRemovedSeq! : removalInfo.removedSeq;
|
|
303
387
|
segmentLen = -segmentLen;
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
// then apart first.
|
|
388
|
+
// The client who performed the remove is always stored
|
|
389
|
+
// in the first position of removalInfo.
|
|
307
390
|
clientId = removalInfo.removedClientIds[0];
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
: undefined;
|
|
391
|
+
const hasOverlap = removalInfo.removedClientIds.length > 1;
|
|
392
|
+
removeClientOverlap = hasOverlap ? removalInfo.removedClientIds : undefined;
|
|
311
393
|
}
|
|
312
394
|
|
|
313
|
-
const
|
|
314
|
-
|
|
395
|
+
const partials = isLocal ?
|
|
396
|
+
combinedPartialLengths.unsequencedRecords?.partialLengths : combinedPartialLengths.partialLengths;
|
|
397
|
+
if (partials === undefined) {
|
|
398
|
+
// Local partial but its computation isn't required
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const partialsLen = partials.length;
|
|
315
402
|
// Find the first entry with sequence number greater or equal to seq
|
|
316
403
|
let indexFirstGTE = 0;
|
|
317
|
-
for (; indexFirstGTE <
|
|
318
|
-
if (
|
|
404
|
+
for (; indexFirstGTE < partialsLen; indexFirstGTE++) {
|
|
405
|
+
if (partials[indexFirstGTE].seq >= seqOrLocalSeq) {
|
|
319
406
|
break;
|
|
320
407
|
}
|
|
321
408
|
}
|
|
322
|
-
|
|
323
|
-
|
|
409
|
+
|
|
410
|
+
let partialLengthEntry: PartialSequenceLength;
|
|
411
|
+
if (partials[indexFirstGTE]?.seq === seqOrLocalSeq) {
|
|
412
|
+
partialLengthEntry = partials[indexFirstGTE];
|
|
413
|
+
// Existing entry at this seq--this occurs for ops that insert/delete more than one segment.
|
|
414
|
+
partialLengthEntry.seglen += segmentLen;
|
|
324
415
|
if (removeClientOverlap) {
|
|
325
416
|
PartialSequenceLengths.accumulateRemoveClientOverlap(
|
|
326
|
-
|
|
417
|
+
partials[indexFirstGTE],
|
|
327
418
|
removeClientOverlap,
|
|
328
419
|
segmentLen);
|
|
329
420
|
}
|
|
330
421
|
} else {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
422
|
+
partialLengthEntry = {
|
|
423
|
+
seq: seqOrLocalSeq,
|
|
424
|
+
clientId,
|
|
425
|
+
len: 0,
|
|
426
|
+
seglen: segmentLen,
|
|
427
|
+
overlapRemoveClients: removeClientOverlap
|
|
428
|
+
? PartialSequenceLengths.getOverlapClients(removeClientOverlap, segmentLen)
|
|
429
|
+
: undefined,
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// TODO: investigate performance improvement using BST
|
|
433
|
+
insertIntoList(partials, indexFirstGTE, partialLengthEntry);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const { unsequencedRecords } = combinedPartialLengths;
|
|
437
|
+
if (unsequencedRecords && removeClientOverlap && segment.localRemovedSeq !== undefined) {
|
|
438
|
+
const localSeq = segment.localRemovedSeq;
|
|
439
|
+
const localPartialLengthEntry: LocalPartialSequenceLength = {
|
|
440
|
+
seq: seqOrLocalSeq,
|
|
441
|
+
localSeq,
|
|
442
|
+
clientId,
|
|
443
|
+
len: 0,
|
|
444
|
+
seglen: segmentLen,
|
|
445
|
+
};
|
|
446
|
+
let localIndexFirstGTE = 0;
|
|
447
|
+
for (; localIndexFirstGTE < unsequencedRecords.overlappingRemoves.length; localIndexFirstGTE++) {
|
|
448
|
+
if (unsequencedRecords.overlappingRemoves[localIndexFirstGTE].seq >= seqOrLocalSeq) {
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
337
451
|
}
|
|
338
452
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
453
|
+
insertIntoList(unsequencedRecords.overlappingRemoves, localIndexFirstGTE, localPartialLengthEntry);
|
|
454
|
+
|
|
455
|
+
localIndexFirstGTE = 0;
|
|
456
|
+
for (; localIndexFirstGTE < unsequencedRecords.partialLengths.length; localIndexFirstGTE++) {
|
|
457
|
+
if (unsequencedRecords.partialLengths[localIndexFirstGTE].seq >= localSeq) {
|
|
458
|
+
break;
|
|
344
459
|
}
|
|
345
|
-
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const tweakedLocalPartialEntry = {
|
|
463
|
+
...localPartialLengthEntry,
|
|
464
|
+
seq: localSeq,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
if (unsequencedRecords.partialLengths[localIndexFirstGTE]?.seq === localSeq) {
|
|
468
|
+
unsequencedRecords.partialLengths[localIndexFirstGTE].seglen += localPartialLengthEntry.seglen;
|
|
346
469
|
} else {
|
|
347
|
-
|
|
470
|
+
insertIntoList(unsequencedRecords.partialLengths, localIndexFirstGTE, tweakedLocalPartialEntry);
|
|
348
471
|
}
|
|
349
472
|
}
|
|
350
473
|
}
|
|
@@ -365,38 +488,79 @@ export class PartialSequenceLengths {
|
|
|
365
488
|
penultPartialLen = pLen;
|
|
366
489
|
}
|
|
367
490
|
}
|
|
491
|
+
const len = penultPartialLen !== undefined ? penultPartialLen.len + seqSeglen : seqSeglen;
|
|
368
492
|
if (seqPartialLen === undefined) {
|
|
369
|
-
// len will be assigned below, making this assertion true.
|
|
370
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
371
493
|
seqPartialLen = {
|
|
372
494
|
clientId,
|
|
495
|
+
len,
|
|
373
496
|
seglen: seqSeglen,
|
|
374
497
|
seq,
|
|
375
|
-
}
|
|
498
|
+
};
|
|
376
499
|
partialLengths.push(seqPartialLen);
|
|
377
500
|
} else {
|
|
378
501
|
seqPartialLen.seglen = seqSeglen;
|
|
502
|
+
seqPartialLen.len = len;
|
|
379
503
|
// Assert client id matches
|
|
380
504
|
}
|
|
381
|
-
if (penultPartialLen !== undefined) {
|
|
382
|
-
seqPartialLen.len = seqPartialLen.seglen + penultPartialLen.len;
|
|
383
|
-
} else {
|
|
384
|
-
seqPartialLen.len = seqPartialLen.seglen;
|
|
385
|
-
}
|
|
386
505
|
}
|
|
387
|
-
public minLength = 0;
|
|
388
|
-
public segmentCount = 0;
|
|
389
|
-
public partialLengths: PartialSequenceLength[] = [];
|
|
390
|
-
public clientSeqNumbers: PartialSequenceLength[][] = [];
|
|
391
506
|
|
|
392
|
-
|
|
393
|
-
|
|
507
|
+
/**
|
|
508
|
+
* Length of the block this PartialSequenceLength corresponds to when viewed at `minSeq`.
|
|
509
|
+
*/
|
|
510
|
+
private minLength = 0;
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Total number of segments in the subtree rooted at the block this PartialSequenceLength corresponds to.
|
|
514
|
+
*/
|
|
515
|
+
private segmentCount = 0;
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* List of PartialSequenceLength objects--ordered by increasing seq--giving length information about
|
|
519
|
+
* the block associated with this PartialSequenceLengths object.
|
|
520
|
+
*
|
|
521
|
+
* `partialLengths[i].len` contains the length of this block considering only sequenced segments with
|
|
522
|
+
* `sequenceNumber <= partialLengths[i].seq`.
|
|
523
|
+
*/
|
|
524
|
+
private readonly partialLengths: PartialSequenceLength[] = [];
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* clientSeqNumbers[clientId] is a list of partial lengths for sequenced ops which either:
|
|
528
|
+
* - were submitted by `clientId`.
|
|
529
|
+
* - deleted a range containing segments that were concurrently deleted by `clientId`
|
|
530
|
+
*
|
|
531
|
+
* The second case is referred to as the "overlapping delete" case. It is necessary to avoid double-counting
|
|
532
|
+
* the removal of those segments in queries including clientId.
|
|
533
|
+
*/
|
|
534
|
+
private readonly clientSeqNumbers: PartialSequenceLength[][] = [];
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Contains information required to answer queries for the length of this segment from the perspective of
|
|
538
|
+
* the local client but not including all local segments (i.e., `localSeq !== collabWindow.localSeq`).
|
|
539
|
+
* This field is only computed if requested in the constructor (i.e. `computeLocalPartials === true`).
|
|
540
|
+
*/
|
|
541
|
+
private unsequencedRecords: UnsequencedPartialLengthInfo | undefined;
|
|
542
|
+
|
|
543
|
+
constructor(
|
|
544
|
+
/**
|
|
545
|
+
* The minimumSequenceNumber as defined by the collab window used in the last call to `update`,
|
|
546
|
+
* or if no such calls have been made, the one used on construction.
|
|
547
|
+
*/
|
|
548
|
+
public minSeq: number,
|
|
549
|
+
computeLocalPartials: boolean) {
|
|
550
|
+
if (computeLocalPartials) {
|
|
551
|
+
this.unsequencedRecords = {
|
|
552
|
+
partialLengths: [],
|
|
553
|
+
overlappingRemoves: [],
|
|
554
|
+
cachedOverlappingByRefSeq: new Map(),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
394
558
|
|
|
395
559
|
// Assume: seq is latest sequence number; no structural change to sub-tree, but a segment
|
|
396
|
-
// with sequence number seq has been added within the sub-tree
|
|
560
|
+
// with sequence number seq has been added within the sub-tree (and `update` has been called
|
|
561
|
+
// on all descendant PartialSequenceLengths)
|
|
397
562
|
// TODO: assert client id matches
|
|
398
563
|
public update(
|
|
399
|
-
mergeTree: MergeTree,
|
|
400
564
|
node: IMergeBlock,
|
|
401
565
|
seq: number,
|
|
402
566
|
clientId: number,
|
|
@@ -436,6 +600,7 @@ export class PartialSequenceLengths {
|
|
|
436
600
|
}
|
|
437
601
|
}
|
|
438
602
|
this.segmentCount = segCount;
|
|
603
|
+
this.unsequencedRecords = undefined;
|
|
439
604
|
|
|
440
605
|
PartialSequenceLengths.addSeq(this.partialLengths, seq, seqSeglen, clientId);
|
|
441
606
|
if (this.clientSeqNumbers[clientId] === undefined) {
|
|
@@ -446,42 +611,110 @@ export class PartialSequenceLengths {
|
|
|
446
611
|
this.zamboni(collabWindow);
|
|
447
612
|
}
|
|
448
613
|
if (PartialSequenceLengths.options.verify) {
|
|
449
|
-
|
|
614
|
+
verify(this);
|
|
450
615
|
}
|
|
451
616
|
}
|
|
452
617
|
|
|
453
|
-
|
|
618
|
+
/**
|
|
619
|
+
* Returns the length of this block as viewed from the perspective of `clientId` at `refSeq`.
|
|
620
|
+
* This is the total length of all segments sequenced at or before refSeq OR submitted by `clientId`.
|
|
621
|
+
* If `clientId` is the local client, `localSeq` can also be provided. In that case, it is the total
|
|
622
|
+
* length of all segments submitted at or before `refSeq` in addition to any local, unacked segments
|
|
623
|
+
* with `segment.localSeq <= localSeq`.
|
|
624
|
+
*
|
|
625
|
+
* Note: the local case (where `localSeq !== undefined`) is only supported on a PartialSequenceLength object
|
|
626
|
+
* constructed with `computeLocalPartials` set to true and not subsequently updated with `update`.
|
|
627
|
+
*/
|
|
628
|
+
public getPartialLength(refSeq: number, clientId: number, localSeq?: number) {
|
|
454
629
|
let pLen = this.minLength;
|
|
455
630
|
const seqIndex = latestLEQ(this.partialLengths, refSeq);
|
|
456
631
|
const cliLatestIndex = this.cliLatest(clientId);
|
|
457
632
|
const cliSeq = this.clientSeqNumbers[clientId];
|
|
458
633
|
if (seqIndex >= 0) {
|
|
459
|
-
// Add the partial length up to refSeq
|
|
460
634
|
pLen += this.partialLengths[seqIndex].len;
|
|
635
|
+
}
|
|
461
636
|
|
|
637
|
+
if (localSeq === undefined) {
|
|
462
638
|
if (cliLatestIndex >= 0) {
|
|
463
639
|
const cliLatest = cliSeq[cliLatestIndex];
|
|
464
|
-
|
|
465
640
|
if (cliLatest.seq > refSeq) {
|
|
466
641
|
// The client has local edits after refSeq, add in the length adjustments
|
|
467
642
|
pLen += cliLatest.len;
|
|
468
643
|
const precedingCliIndex = this.cliLatestLEQ(clientId, refSeq);
|
|
469
644
|
if (precedingCliIndex >= 0) {
|
|
645
|
+
// Subtract out double-counted lengths: segments still in the collab window but before
|
|
646
|
+
// the refSeq submitted by the client we're querying for were counted in each addition above.
|
|
470
647
|
pLen -= cliSeq[precedingCliIndex].len;
|
|
471
648
|
}
|
|
472
649
|
}
|
|
473
650
|
}
|
|
474
651
|
} else {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
652
|
+
assert(this.unsequencedRecords !== undefined,
|
|
653
|
+
0x39f /* Local getPartialLength invoked without computing local partials. */);
|
|
654
|
+
const unsequencedPartialLengths = this.unsequencedRecords.partialLengths;
|
|
655
|
+
// Local segments at or before localSeq should also be included
|
|
656
|
+
const localIndex = latestLEQ(unsequencedPartialLengths, localSeq);
|
|
657
|
+
if (localIndex >= 0) {
|
|
658
|
+
pLen += unsequencedPartialLengths[localIndex].len;
|
|
659
|
+
|
|
660
|
+
// Lastly, we must subtract out any double-counted removes, which occur if a currently un-acked local
|
|
661
|
+
// remove overlaps with a remote client's remove that occurred at sequence number <=refSeq.
|
|
662
|
+
pLen -= this.computeOverlappingLocalRemoves(refSeq, localSeq);
|
|
480
663
|
}
|
|
481
664
|
}
|
|
482
665
|
return pLen;
|
|
483
666
|
}
|
|
484
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Computes the seglen for the double-counted removed overlap at (refSeq, localSeq). This logic is equivalent
|
|
670
|
+
* to the following:
|
|
671
|
+
*
|
|
672
|
+
* ```typescript
|
|
673
|
+
* let total = 0;
|
|
674
|
+
* for (const partialLength of this.unsequencedRecords!.overlappingRemoves) {
|
|
675
|
+
* if (partialLength.seq > refSeq) {
|
|
676
|
+
* break;
|
|
677
|
+
* }
|
|
678
|
+
*
|
|
679
|
+
* if (partialLength.localSeq <= localSeq) {
|
|
680
|
+
* total += partialLength.seglen;
|
|
681
|
+
* }
|
|
682
|
+
* }
|
|
683
|
+
*
|
|
684
|
+
* return total;
|
|
685
|
+
* ```
|
|
686
|
+
*
|
|
687
|
+
* Reconnect happens to only need to compute these lengths for two refSeq values: before and
|
|
688
|
+
* after the rebase. Since these lists potentially scale with O(collab window * number of local edits)
|
|
689
|
+
* and potentially need to be queried for each local op that gets rebased,
|
|
690
|
+
* we cache the results for a given refSeq in `this.unsequencedRecords.cachedOverlappingByRefSeq` so
|
|
691
|
+
* that they can be binary-searched the same way the usual partialLengths lists are.
|
|
692
|
+
*/
|
|
693
|
+
private computeOverlappingLocalRemoves(refSeq: number, localSeq: number): number {
|
|
694
|
+
if (this.unsequencedRecords === undefined) {
|
|
695
|
+
return 0;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
let cachedOverlapPartials = this.unsequencedRecords.cachedOverlappingByRefSeq.get(refSeq);
|
|
699
|
+
if (!cachedOverlapPartials) {
|
|
700
|
+
const partials: PartialSequenceLength[] = [];
|
|
701
|
+
for (const partial of this.unsequencedRecords.overlappingRemoves) {
|
|
702
|
+
if (partial.seq > refSeq) {
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
partials.push({ ...partial, seq: partial.localSeq, len: 0 });
|
|
707
|
+
}
|
|
708
|
+
partials.sort((a, b) => a.seq - b.seq);
|
|
709
|
+
// This coalesces entries with the same localSeq as well as computes overall lengths.
|
|
710
|
+
cachedOverlapPartials = mergePartialLengths([partials]);
|
|
711
|
+
this.unsequencedRecords.cachedOverlappingByRefSeq.set(refSeq, cachedOverlapPartials);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const overlapIndex = latestLEQ(cachedOverlapPartials, localSeq);
|
|
715
|
+
return overlapIndex >= 0 ? cachedOverlapPartials[overlapIndex].len : 0;
|
|
716
|
+
}
|
|
717
|
+
|
|
485
718
|
public toString(glc?: (id: number) => string, indentCount = 0) {
|
|
486
719
|
let buf = "";
|
|
487
720
|
for (const partial of this.partialLengths) {
|
|
@@ -492,11 +725,7 @@ export class PartialSequenceLengths {
|
|
|
492
725
|
for (const clientId in this.clientSeqNumbers) {
|
|
493
726
|
if (this.clientSeqNumbers[clientId].length > 0) {
|
|
494
727
|
buf += `Client `;
|
|
495
|
-
|
|
496
|
-
buf += `${glc(+clientId)}`;
|
|
497
|
-
} else {
|
|
498
|
-
buf += `${clientId}`;
|
|
499
|
-
}
|
|
728
|
+
buf += glc ? `${glc(+clientId)}` : `${clientId}`;
|
|
500
729
|
buf += "[";
|
|
501
730
|
for (const partial of this.clientSeqNumbers[clientId]) {
|
|
502
731
|
buf += `(${partial.seq},${partial.len})`;
|
|
@@ -530,6 +759,7 @@ export class PartialSequenceLengths {
|
|
|
530
759
|
return minLength;
|
|
531
760
|
}
|
|
532
761
|
this.minLength += copyDown(this.partialLengths);
|
|
762
|
+
this.minSeq = segmentWindow.minSeq;
|
|
533
763
|
// eslint-disable-next-line @typescript-eslint/no-for-in-array, guard-for-in, no-restricted-syntax
|
|
534
764
|
for (const clientId in this.clientSeqNumbers) {
|
|
535
765
|
const cliPartials = this.clientSeqNumbers[clientId];
|
|
@@ -551,13 +781,16 @@ export class PartialSequenceLengths {
|
|
|
551
781
|
cli.push({ seq, len: pLen, seglen });
|
|
552
782
|
}
|
|
553
783
|
|
|
554
|
-
// Assumes sequence number already coalesced
|
|
784
|
+
// Assumes sequence number already coalesced and that this is called in increasing `seq` order.
|
|
555
785
|
private addClientSeqNumberFromPartial(partialLength: PartialSequenceLength) {
|
|
556
786
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
557
787
|
this.addClientSeqNumber(partialLength.clientId!, partialLength.seq, partialLength.seglen);
|
|
558
788
|
if (partialLength.overlapRemoveClients) {
|
|
559
789
|
partialLength.overlapRemoveClients.map((oc: Property<number, IOverlapClient>) => {
|
|
560
|
-
|
|
790
|
+
// Original client entry was handled above
|
|
791
|
+
if (partialLength.clientId !== oc.data.clientId) {
|
|
792
|
+
this.addClientSeqNumber(oc.data.clientId, partialLength.seq, oc.data.seglen);
|
|
793
|
+
}
|
|
561
794
|
return true;
|
|
562
795
|
});
|
|
563
796
|
}
|
|
@@ -565,95 +798,235 @@ export class PartialSequenceLengths {
|
|
|
565
798
|
|
|
566
799
|
private cliLatestLEQ(clientId: number, refSeq: number) {
|
|
567
800
|
const cliSeqs = this.clientSeqNumbers[clientId];
|
|
568
|
-
|
|
569
|
-
return latestLEQ(cliSeqs, refSeq);
|
|
570
|
-
} else {
|
|
571
|
-
return -1;
|
|
572
|
-
}
|
|
801
|
+
return cliSeqs ? latestLEQ(cliSeqs, refSeq) : -1;
|
|
573
802
|
}
|
|
574
803
|
|
|
575
804
|
private cliLatest(clientId: number) {
|
|
576
805
|
const cliSeqs = this.clientSeqNumbers[clientId];
|
|
577
|
-
|
|
578
|
-
return cliSeqs.length - 1;
|
|
579
|
-
} else {
|
|
580
|
-
return -1;
|
|
581
|
-
}
|
|
806
|
+
return cliSeqs && (cliSeqs.length > 0) ? cliSeqs.length - 1 : -1;
|
|
582
807
|
}
|
|
808
|
+
}
|
|
583
809
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
810
|
+
/* eslint-disable @typescript-eslint/dot-notation */
|
|
811
|
+
function verifyPartialLengths(
|
|
812
|
+
partialSeqLengths: PartialSequenceLengths,
|
|
813
|
+
partialLengths: PartialSequenceLength[],
|
|
814
|
+
clientPartials: boolean,
|
|
815
|
+
) {
|
|
816
|
+
if (partialLengths.length === 0) { return 0; }
|
|
587
817
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
818
|
+
let lastSeqNum = 0;
|
|
819
|
+
let accumSegLen = 0;
|
|
820
|
+
let count = 0;
|
|
591
821
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
822
|
+
for (const partialLength of partialLengths) {
|
|
823
|
+
// Count total number of partial length
|
|
824
|
+
count++;
|
|
595
825
|
|
|
596
|
-
|
|
597
|
-
|
|
826
|
+
// Sequence number should be larger or equal to minseq
|
|
827
|
+
assert(partialSeqLengths.minSeq <= partialLength.seq, 0x054 /* "Sequence number less than minSeq!" */);
|
|
598
828
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
829
|
+
// Sequence number should be sorted
|
|
830
|
+
assert(lastSeqNum < partialLength.seq, 0x055 /* "Sequence number is not sorted!" */);
|
|
831
|
+
lastSeqNum = partialLength.seq;
|
|
602
832
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
833
|
+
// Len is a accumulation of all the seglen adjustments
|
|
834
|
+
accumSegLen += partialLength.seglen;
|
|
835
|
+
if (accumSegLen !== partialLength.len) {
|
|
836
|
+
assert(false, 0x056 /* "Unexpected total for accumulation of all seglen adjustments!" */);
|
|
837
|
+
}
|
|
608
838
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
839
|
+
if (clientPartials) {
|
|
840
|
+
// Client partials used to track local edits so we can account for them some refSeq.
|
|
841
|
+
// But the information we keep track of are since minSeq, so we keep track of more history
|
|
842
|
+
// then needed, and some of them doesn't make sense to be used for length calculations
|
|
843
|
+
// e.g. if you have this sequence, where the minSeq is #5 because of other clients
|
|
844
|
+
// seq 10: client 1: insert seg #1
|
|
845
|
+
// seq 11: client 2: delete seg #2 refseq: 10
|
|
846
|
+
// minLength is 0, we would have keep a record of seglen: -1 for clientPartialLengths for client 2
|
|
847
|
+
// So if you ask for partial length for client 2 @ seq 5, we will have return -1.
|
|
848
|
+
// However, that combination is invalid, since we should never see any ops with refseq < 10 for
|
|
849
|
+
// client 2 after seq 11.
|
|
850
|
+
} else {
|
|
851
|
+
// Len adjustment should not make length negative
|
|
852
|
+
if (partialSeqLengths["minLength"] + partialLength.len < 0) {
|
|
853
|
+
assert(false, 0x057 /* "Negative length after length adjustment!" */);
|
|
625
854
|
}
|
|
855
|
+
}
|
|
626
856
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
857
|
+
if (partialLength.overlapRemoveClients) {
|
|
858
|
+
// Only the flat partialLengths can have overlapRemoveClients, the per client view shouldn't
|
|
859
|
+
assert(!clientPartials, 0x058 /* "Both overlapRemoveClients and clientPartials are set!" */);
|
|
860
|
+
|
|
861
|
+
// Each overlap client count as one, but the first remove to sequence was already counted.
|
|
862
|
+
// (this aligns with the logic to omit the removing client in `addClientSeqNumberFromPartial`)
|
|
863
|
+
count += partialLength.overlapRemoveClients.size() - 1;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return count;
|
|
867
|
+
}
|
|
630
868
|
|
|
631
|
-
|
|
632
|
-
|
|
869
|
+
function verify(partialSeqLengths: PartialSequenceLengths) {
|
|
870
|
+
if (partialSeqLengths["clientSeqNumbers"]) {
|
|
871
|
+
let cliCount = 0;
|
|
872
|
+
for (const cliSeq of partialSeqLengths["clientSeqNumbers"]) {
|
|
873
|
+
if (cliSeq) {
|
|
874
|
+
cliCount += verifyPartialLengths(partialSeqLengths, cliSeq, true);
|
|
633
875
|
}
|
|
634
876
|
}
|
|
635
|
-
|
|
877
|
+
|
|
878
|
+
// If we have client view, we should have the flat view
|
|
879
|
+
assert(!!partialSeqLengths["partialLengths"], 0x059 /* "Client view exists but flat view does not!" */);
|
|
880
|
+
const flatCount = verifyPartialLengths(partialSeqLengths, partialSeqLengths["partialLengths"], false);
|
|
881
|
+
|
|
882
|
+
// The number of partial lengths on the client view and flat view should be the same
|
|
883
|
+
assert(flatCount === cliCount,
|
|
884
|
+
0x05a /* "Mismatch between number of partial lengths on client and flat views!" */);
|
|
885
|
+
} else {
|
|
886
|
+
// If we don't have a client view, we shouldn't have the flat view either
|
|
887
|
+
assert(!partialSeqLengths["partialLengths"], 0x05b /* "Flat view exists but client view does not!" */);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/* eslint-enable @typescript-eslint/dot-notation */
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Clones an `overlapRemoveClients` red-black tree.
|
|
894
|
+
*/
|
|
895
|
+
function cloneOverlapRemoveClients(
|
|
896
|
+
oldTree: RedBlackTree<number, IOverlapClient> | undefined,
|
|
897
|
+
): RedBlackTree<number, IOverlapClient> | undefined {
|
|
898
|
+
if (!oldTree) { return undefined; }
|
|
899
|
+
const newTree = new RedBlackTree<number, IOverlapClient>(compareNumbers);
|
|
900
|
+
oldTree.map((bProp: Property<number, IOverlapClient>) => {
|
|
901
|
+
newTree.put(bProp.data.clientId, { ...bProp.data });
|
|
902
|
+
return true;
|
|
903
|
+
});
|
|
904
|
+
return newTree;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Combines the `overlapRemoveClients` field of two `PartialSequenceLength` objects,
|
|
909
|
+
* modifying the first PartialSequenceLength's bookkeeping in-place.
|
|
910
|
+
*
|
|
911
|
+
* Combination is performed additively on `seglen` on a per-client basis.
|
|
912
|
+
*/
|
|
913
|
+
function combineOverlapClients(a: PartialSequenceLength, b: PartialSequenceLength) {
|
|
914
|
+
const overlapRemoveClientsA = a.overlapRemoveClients;
|
|
915
|
+
if (overlapRemoveClientsA) {
|
|
916
|
+
if (b.overlapRemoveClients) {
|
|
917
|
+
b.overlapRemoveClients.map((bProp: Property<number, IOverlapClient>) => {
|
|
918
|
+
const aProp = overlapRemoveClientsA.get(bProp.key);
|
|
919
|
+
if (aProp) {
|
|
920
|
+
aProp.data.seglen += bProp.data.seglen;
|
|
921
|
+
} else {
|
|
922
|
+
overlapRemoveClientsA.put(bProp.data.clientId, { ...bProp.data });
|
|
923
|
+
}
|
|
924
|
+
return true;
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
a.overlapRemoveClients = cloneOverlapRemoveClients(b.overlapRemoveClients);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Given a number of seq-sorted `partialLength` lists, merges them into a combined seq-sorted `partialLength`
|
|
934
|
+
* list. This merge includes coalescing `PartialSequenceLength` entries at the same seq.
|
|
935
|
+
*
|
|
936
|
+
* Ex: merging the following two lists (some information omitted on each PartialSequenceLength):
|
|
937
|
+
* ```typescript
|
|
938
|
+
* [{ seq: 1, seglen: 5 }, { seq: 3, seglen: -1 }]
|
|
939
|
+
* [{ seq: 1, seglen: -3 }, { seq: 2: seglen: 4 }]
|
|
940
|
+
* ```
|
|
941
|
+
* would produce
|
|
942
|
+
* ```typescript
|
|
943
|
+
* [{ seq: 1, seglen: 2 }, { seq: 2, seglen: 4 }, { seq: 3, seglen: -1 }]
|
|
944
|
+
* ```
|
|
945
|
+
*/
|
|
946
|
+
function mergePartialLengths<T extends PartialSequenceLength>(childPartialLengths: T[][]): T[] {
|
|
947
|
+
const mergedLengths: T[] = [];
|
|
948
|
+
// All child PartialSequenceLengths are now sorted temporally (i.e. by seq). Since
|
|
949
|
+
// a given MergeTree operation can affect multiple segments, there may be multiple entries
|
|
950
|
+
// for a given seq. We run through them in order, coalescing all length information for a given
|
|
951
|
+
// seq together into `combinedPartialLengths`.
|
|
952
|
+
let currentPartial: T | undefined;
|
|
953
|
+
for (const partialLength of mergeSortedListsBySeq(childPartialLengths)) {
|
|
954
|
+
if (!currentPartial || currentPartial.seq !== partialLength.seq) {
|
|
955
|
+
// Start a new seq entry.
|
|
956
|
+
currentPartial = {
|
|
957
|
+
...partialLength,
|
|
958
|
+
len: (currentPartial?.len ?? 0) + partialLength.seglen,
|
|
959
|
+
overlapRemoveClients: cloneOverlapRemoveClients(partialLength.overlapRemoveClients),
|
|
960
|
+
};
|
|
961
|
+
mergedLengths.push(currentPartial);
|
|
962
|
+
} else {
|
|
963
|
+
// Update existing entry
|
|
964
|
+
currentPartial.seglen += partialLength.seglen;
|
|
965
|
+
currentPartial.len += partialLength.seglen;
|
|
966
|
+
combineOverlapClients(currentPartial, partialLength);
|
|
967
|
+
}
|
|
636
968
|
}
|
|
969
|
+
return mergedLengths;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Given a collection of PartialSequenceLength lists--each sorted by sequence number--returns an iterable that yields
|
|
974
|
+
* each PartialSequenceLength in sequence order.
|
|
975
|
+
*
|
|
976
|
+
* This is equivalent to flattening the input list and sorting it by sequence number. If the number of lists to merge is
|
|
977
|
+
* a constant, however, this approach is advantageous asymptotically.
|
|
978
|
+
*/
|
|
979
|
+
function mergeSortedListsBySeq<T extends PartialSequenceLength>(lists: T[][]): Iterable<T> {
|
|
980
|
+
class PartialSequenceLengthIterator {
|
|
981
|
+
/**
|
|
982
|
+
* nextSmallestIndex[i] is the next element of sublists[i] to check.
|
|
983
|
+
* In other words, the iterator has already yielded elements of sublists[i] *up through*
|
|
984
|
+
* sublists[i][nextSmallestIndex[i] - 1].
|
|
985
|
+
*/
|
|
986
|
+
private readonly nextSmallestIndex: number[];
|
|
637
987
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
let
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
988
|
+
constructor(private readonly sublists: T[][]) {
|
|
989
|
+
this.nextSmallestIndex = new Array(sublists.length);
|
|
990
|
+
for (let i = 0; i < sublists.length; i++) {
|
|
991
|
+
this.nextSmallestIndex[i] = 0;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
public next(): { value: T; done: false; } | { value: undefined; done: true; } {
|
|
996
|
+
const len = this.sublists.length;
|
|
997
|
+
let currentMin: T | undefined;
|
|
998
|
+
let currentMinIndex: number | undefined;
|
|
999
|
+
for (let i = 0; i < len; i++) {
|
|
1000
|
+
const candidateIndex = this.nextSmallestIndex[i];
|
|
1001
|
+
if (candidateIndex < this.sublists[i].length) {
|
|
1002
|
+
const candidate = this.sublists[i][candidateIndex];
|
|
1003
|
+
if (!currentMin || candidate.seq < currentMin.seq) {
|
|
1004
|
+
currentMin = candidate;
|
|
1005
|
+
currentMinIndex = i;
|
|
1006
|
+
}
|
|
644
1007
|
}
|
|
645
1008
|
}
|
|
646
1009
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1010
|
+
if (currentMin) {
|
|
1011
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1012
|
+
this.nextSmallestIndex[currentMinIndex!]++;
|
|
1013
|
+
return { value: currentMin, done: false };
|
|
1014
|
+
} else {
|
|
1015
|
+
return { value: undefined, done: true };
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
650
1019
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1020
|
+
return { [Symbol.iterator]: () => new PartialSequenceLengthIterator(lists) };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function insertIntoList<T>(list: T[], index: number, elem: T): void {
|
|
1024
|
+
if (index < list.length) {
|
|
1025
|
+
for (let k = list.length; k > index; k--) {
|
|
1026
|
+
list[k] = list[k - 1];
|
|
657
1027
|
}
|
|
1028
|
+
list[index] = elem;
|
|
1029
|
+
} else {
|
|
1030
|
+
list.push(elem);
|
|
658
1031
|
}
|
|
659
1032
|
}
|