btree-core 3.2.1

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 (42) hide show
  1. package/LICENSE +23 -0
  2. package/b+tree.d.ts +429 -0
  3. package/b+tree.js +1545 -0
  4. package/b+tree.min.js +1 -0
  5. package/extended/bulkLoad.d.ts +14 -0
  6. package/extended/bulkLoad.js +113 -0
  7. package/extended/bulkLoad.min.js +1 -0
  8. package/extended/decompose.d.ts +1 -0
  9. package/extended/decompose.js +680 -0
  10. package/extended/decompose.min.js +1 -0
  11. package/extended/diffAgainst.d.ts +23 -0
  12. package/extended/diffAgainst.js +254 -0
  13. package/extended/diffAgainst.min.js +1 -0
  14. package/extended/forEachKeyInBoth.d.ts +19 -0
  15. package/extended/forEachKeyInBoth.js +73 -0
  16. package/extended/forEachKeyInBoth.min.js +1 -0
  17. package/extended/forEachKeyNotIn.d.ts +18 -0
  18. package/extended/forEachKeyNotIn.js +87 -0
  19. package/extended/forEachKeyNotIn.min.js +1 -0
  20. package/extended/index.d.ts +133 -0
  21. package/extended/index.js +200 -0
  22. package/extended/index.min.js +1 -0
  23. package/extended/intersect.d.ts +16 -0
  24. package/extended/intersect.js +44 -0
  25. package/extended/intersect.min.js +1 -0
  26. package/extended/parallelWalk.d.ts +1 -0
  27. package/extended/parallelWalk.js +188 -0
  28. package/extended/parallelWalk.min.js +1 -0
  29. package/extended/shared.d.ts +1 -0
  30. package/extended/shared.js +64 -0
  31. package/extended/shared.min.js +1 -0
  32. package/extended/subtract.d.ts +16 -0
  33. package/extended/subtract.js +35 -0
  34. package/extended/subtract.min.js +1 -0
  35. package/extended/union.d.ts +16 -0
  36. package/extended/union.js +36 -0
  37. package/extended/union.min.js +1 -0
  38. package/interfaces.d.ts +307 -0
  39. package/package.json +122 -0
  40. package/readme.md +420 -0
  41. package/sorted-array.d.ts +22 -0
  42. package/sorted-array.js +71 -0
@@ -0,0 +1,680 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildFromDecomposition = exports.decompose = void 0;
4
+ var b_tree_1 = require("../b+tree");
5
+ var shared_1 = require("./shared");
6
+ var parallelWalk_1 = require("./parallelWalk");
7
+ var decomposeLoadFactor = 0.7;
8
+ /**
9
+ * Decomposes two trees into disjoint nodes. Reuses interior nodes when they do not overlap/intersect with any leaf nodes
10
+ * in the other tree. Overlapping leaf nodes are broken down into new leaf nodes containing merged entries.
11
+ * The algorithm is a parallel tree walk using two cursors. The trailing cursor (behind in key space) is walked forward
12
+ * until it is at or after the leading cursor. As it does this, any whole nodes or subtrees it passes are guaranteed to
13
+ * be disjoint. This is true because the leading cursor was also previously walked in this way, and is thus pointing to
14
+ * the first key at or after the trailing cursor's previous position.
15
+ * The cursor walk is efficient, meaning it skips over disjoint subtrees entirely rather than visiting every leaf.
16
+ * Note: some of the returned leaves may be underfilled.
17
+ * @internal
18
+ */
19
+ function decompose(left, right, combineFn, ignoreRight) {
20
+ if (ignoreRight === void 0) { ignoreRight = false; }
21
+ var maxNodeSize = left._maxNodeSize;
22
+ var cmp = left._compare;
23
+ (0, b_tree_1.check)(left._root.size() > 0 && right._root.size() > 0, "decompose requires non-empty inputs");
24
+ // Holds the disjoint nodes that result from decomposition.
25
+ // Stored as parallel arrays of (height, node) to avoid creating many tiny tuples
26
+ var disjointHeights = [];
27
+ var disjointNodes = [];
28
+ // During the decomposition, leaves that are not disjoint are decomposed into individual entries
29
+ // that accumulate in this array in sorted order. They are flushed into leaf nodes whenever a reused
30
+ // disjoint subtree is added to the disjoint set.
31
+ // Note that there are unavoidable cases in which this will generate underfilled leaves.
32
+ // An example of this would be a leaf in one tree that contained keys [0, 100, 101, 102].
33
+ // In the other tree, there is a leaf that contains [2, 3, 4, 5]. This leaf can be reused entirely,
34
+ // but the first tree's leaf must be decomposed into [0] and [100, 101, 102]
35
+ var pendingKeys = [];
36
+ var pendingValues = [];
37
+ var tallestIndex = -1, tallestHeight = -1;
38
+ // During the upward part of the cursor walk, this holds the highest disjoint node seen so far.
39
+ // This is done because we cannot know immediately whether we can add the node to the disjoint set
40
+ // because its ancestor may also be disjoint and should be reused instead.
41
+ var highestDisjoint
42
+ // Have to do this as cast to convince TS it's ever assigned
43
+ = undefined;
44
+ var minSize = Math.floor(maxNodeSize / 2);
45
+ var onLeafCreation = function (leaf) {
46
+ var height = leaf.keys.length < minSize ? -1 : 0;
47
+ disjointHeights.push(height);
48
+ disjointNodes.push(leaf);
49
+ };
50
+ var addSharedNodeToDisjointSet = function (node, height) {
51
+ // flush pending entries
52
+ (0, shared_1.makeLeavesFrom)(pendingKeys, pendingValues, maxNodeSize, decomposeLoadFactor, onLeafCreation);
53
+ pendingKeys.length = 0;
54
+ pendingValues.length = 0;
55
+ // Don't share underfilled leaves, instead mark them as needing merging
56
+ if (node.isLeaf && node.keys.length < minSize) {
57
+ disjointHeights.push(-1);
58
+ disjointNodes.push(node.clone());
59
+ }
60
+ else {
61
+ node.isShared = true;
62
+ disjointHeights.push(height);
63
+ disjointNodes.push(node);
64
+ }
65
+ if (height > tallestHeight) {
66
+ tallestIndex = disjointHeights.length - 1;
67
+ tallestHeight = height;
68
+ }
69
+ };
70
+ var addHighestDisjoint = function () {
71
+ if (highestDisjoint !== undefined) {
72
+ addSharedNodeToDisjointSet(highestDisjoint.node, highestDisjoint.height);
73
+ highestDisjoint = undefined;
74
+ }
75
+ };
76
+ // Mark all nodes at or above depthFrom in the cursor spine as disqualified (non-disjoint)
77
+ var disqualifySpine = function (cursor, depthFrom) {
78
+ var spine = cursor.spine;
79
+ for (var i = depthFrom; i >= 0; --i) {
80
+ var payload = spine[i].payload;
81
+ // Safe to early out because we always disqualify all ancestors of a disqualified node
82
+ // That is correct because every ancestor of a non-disjoint node is also non-disjoint
83
+ // because it must enclose the non-disjoint range.
84
+ if (payload.disqualified)
85
+ break;
86
+ payload.disqualified = true;
87
+ }
88
+ };
89
+ // Cursor payload factory
90
+ var makePayload = function () { return ({ disqualified: false }); };
91
+ var pushLeafRange = function (leaf, from, toExclusive) {
92
+ var keys = leaf.keys;
93
+ var values = leaf.values;
94
+ for (var i = from; i < toExclusive; ++i) {
95
+ pendingKeys.push(keys[i]);
96
+ pendingValues.push(values[i]);
97
+ }
98
+ };
99
+ var onMoveInLeaf = function (leaf, payload, fromIndex, toIndex, startedEqual) {
100
+ (0, b_tree_1.check)(payload.disqualified === true, "onMoveInLeaf: leaf must be disqualified");
101
+ var start = startedEqual ? fromIndex + 1 : fromIndex;
102
+ if (start < toIndex)
103
+ pushLeafRange(leaf, start, toIndex);
104
+ };
105
+ var onExitLeaf = function (leaf, payload, startingIndex, startedEqual, cursorThis) {
106
+ highestDisjoint = undefined;
107
+ if (!payload.disqualified) {
108
+ highestDisjoint = { node: leaf, height: 0 };
109
+ if (cursorThis.spine.length === 0) {
110
+ // if we are exiting a leaf and there are no internal nodes, we will reach the end of the tree.
111
+ // In this case we need to add the leaf now because step up will not be called.
112
+ addHighestDisjoint();
113
+ }
114
+ }
115
+ else {
116
+ var start = startedEqual ? startingIndex + 1 : startingIndex;
117
+ var leafSize = leaf.keys.length;
118
+ if (start < leafSize)
119
+ pushLeafRange(leaf, start, leafSize);
120
+ }
121
+ };
122
+ var onStepUp = function (parent, height, payload, fromIndex, spineIndex, stepDownIndex, cursorThis) {
123
+ var children = parent.children;
124
+ var nextHeight = height - 1;
125
+ if (stepDownIndex !== stepDownIndex /* NaN: still walking up */
126
+ || stepDownIndex === Number.POSITIVE_INFINITY /* target key is beyond edge of tree, done with walk */) {
127
+ if (!payload.disqualified) {
128
+ if (stepDownIndex === Number.POSITIVE_INFINITY) {
129
+ // We have finished our walk, and we won't be stepping down, so add the root
130
+ // Roots are allowed to be underfilled, so break the root up here if so to avoid
131
+ // creating underfilled interior nodes during reconstruction.
132
+ // Note: the main btree implementation allows underfilled nodes in general, this algorithm
133
+ // guarantees that no additional underfilled nodes are created beyond what was already present.
134
+ if (parent.keys.length < minSize) {
135
+ for (var i = fromIndex; i < children.length; ++i)
136
+ addSharedNodeToDisjointSet(children[i], nextHeight);
137
+ }
138
+ else {
139
+ addSharedNodeToDisjointSet(parent, height);
140
+ }
141
+ highestDisjoint = undefined;
142
+ }
143
+ else {
144
+ highestDisjoint = { node: parent, height: height };
145
+ }
146
+ }
147
+ else {
148
+ addHighestDisjoint();
149
+ var len = children.length;
150
+ for (var i = fromIndex + 1; i < len; ++i)
151
+ addSharedNodeToDisjointSet(children[i], nextHeight);
152
+ }
153
+ }
154
+ else {
155
+ // We have a valid step down index, so we need to disqualify the spine if needed.
156
+ // This is identical to the step down logic, but we must also perform it here because
157
+ // in the case of stepping down into a leaf, the step down callback is never called.
158
+ if (stepDownIndex > 0) {
159
+ disqualifySpine(cursorThis, spineIndex);
160
+ }
161
+ addHighestDisjoint();
162
+ for (var i = fromIndex + 1; i < stepDownIndex; ++i)
163
+ addSharedNodeToDisjointSet(children[i], nextHeight);
164
+ }
165
+ };
166
+ var onStepDown = function (node, height, spineIndex, stepDownIndex, cursorThis) {
167
+ if (stepDownIndex > 0) {
168
+ // When we step down into a node, we know that we have walked from a key that is less than our target.
169
+ // Because of this, if we are not stepping down into the first child, we know that all children before
170
+ // the stepDownIndex must overlap with the other tree because they must be before our target key. Since
171
+ // the child we are stepping into has a key greater than our target key, this node must overlap.
172
+ // If a child overlaps, the entire spine overlaps because a parent in a btree always encloses the range
173
+ // of its children.
174
+ disqualifySpine(cursorThis, spineIndex);
175
+ var children = node.children;
176
+ var nextHeight = height - 1;
177
+ for (var i = 0; i < stepDownIndex; ++i)
178
+ addSharedNodeToDisjointSet(children[i], nextHeight);
179
+ }
180
+ };
181
+ var onEnterLeaf = function (leaf, destIndex, cursorThis, cursorOther) {
182
+ if (destIndex > 0
183
+ || (0, b_tree_1.areOverlapping)(leaf.minKey(), leaf.maxKey(), (0, parallelWalk_1.getKey)(cursorOther), cursorOther.leaf.maxKey(), cmp)) {
184
+ // Similar logic to the step-down case, except in this case we also know the leaf in the other
185
+ // tree overlaps a leaf in this tree (this leaf, specifically). Thus, we can disqualify both spines.
186
+ cursorThis.leafPayload.disqualified = true;
187
+ cursorOther.leafPayload.disqualified = true;
188
+ disqualifySpine(cursorThis, cursorThis.spine.length - 1);
189
+ disqualifySpine(cursorOther, cursorOther.spine.length - 1);
190
+ pushLeafRange(leaf, 0, destIndex);
191
+ }
192
+ };
193
+ // Need the max key of both trees to perform the "finishing" walk of which ever cursor finishes second
194
+ var maxKeyLeft = left._root.maxKey();
195
+ var maxKeyRight = right._root.maxKey();
196
+ var maxKey = cmp(maxKeyLeft, maxKeyRight) >= 0 ? maxKeyLeft : maxKeyRight;
197
+ // Initialize cursors at minimum keys.
198
+ var curA = (0, parallelWalk_1.createCursor)(left, makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown);
199
+ var curB;
200
+ if (ignoreRight) {
201
+ var dummyPayload_1 = { disqualified: true };
202
+ var onStepUpIgnore = function (_1, _2, _3, _4, spineIndex, stepDownIndex, cursorThis) {
203
+ if (stepDownIndex > 0) {
204
+ disqualifySpine(cursorThis, spineIndex);
205
+ }
206
+ };
207
+ var onStepDownIgnore = function (_, __, spineIndex, stepDownIndex, cursorThis) {
208
+ if (stepDownIndex > 0) {
209
+ disqualifySpine(cursorThis, spineIndex);
210
+ }
211
+ };
212
+ var onEnterLeafIgnore = function (leaf, destIndex, _, cursorOther) {
213
+ if (destIndex > 0
214
+ || (0, b_tree_1.areOverlapping)(leaf.minKey(), leaf.maxKey(), (0, parallelWalk_1.getKey)(cursorOther), cursorOther.leaf.maxKey(), cmp)) {
215
+ cursorOther.leafPayload.disqualified = true;
216
+ disqualifySpine(cursorOther, cursorOther.spine.length - 1);
217
+ }
218
+ };
219
+ curB = (0, parallelWalk_1.createCursor)(right, function () { return dummyPayload_1; }, onEnterLeafIgnore, parallelWalk_1.noop, parallelWalk_1.noop, onStepUpIgnore, onStepDownIgnore);
220
+ }
221
+ else {
222
+ curB = (0, parallelWalk_1.createCursor)(right, makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown);
223
+ }
224
+ // The guarantee that no overlapping interior nodes are accidentally reused relies on the careful
225
+ // alternating hopping walk of the cursors: WLOG, cursorA always--with one exception--walks from a key just behind (in key space)
226
+ // the key of cursorB to the first key >= cursorB. Call this transition a "crossover point." All interior nodes that
227
+ // overlap cause a crossover point, and all crossover points are guaranteed to be walked using this method. Thus,
228
+ // all overlapping interior nodes will be found if they are checked for on step-down.
229
+ // The one exception mentioned above is when they start at the same key. In this case, they are both advanced forward and then
230
+ // their new ordering determines how they walk from there.
231
+ // The one issue then is detecting any overlaps that occur based on their very initial position (minimum key of each tree).
232
+ // This is handled by the initial disqualification step below, which essentially emulates the step down disqualification for each spine.
233
+ // Initialize disqualification w.r.t. opposite leaf.
234
+ var initDisqualify = function (cur, other) {
235
+ var minKey = (0, parallelWalk_1.getKey)(cur);
236
+ var otherMin = (0, parallelWalk_1.getKey)(other);
237
+ var otherMax = other.leaf.maxKey();
238
+ if ((0, b_tree_1.areOverlapping)(minKey, cur.leaf.maxKey(), otherMin, otherMax, cmp))
239
+ cur.leafPayload.disqualified = true;
240
+ for (var i = 0; i < cur.spine.length; ++i) {
241
+ var entry = cur.spine[i];
242
+ // Since we are on the left side of the tree, we can use the leaf min key for every spine node
243
+ if ((0, b_tree_1.areOverlapping)(minKey, entry.node.maxKey(), otherMin, otherMax, cmp))
244
+ entry.payload.disqualified = true;
245
+ }
246
+ };
247
+ initDisqualify(curA, curB);
248
+ initDisqualify(curB, curA);
249
+ var leading = curA;
250
+ var trailing = curB;
251
+ var order = cmp((0, parallelWalk_1.getKey)(leading), (0, parallelWalk_1.getKey)(trailing));
252
+ // Walk both cursors in alternating hops
253
+ while (true) {
254
+ var areEqual = order === 0;
255
+ if (areEqual) {
256
+ var key = (0, parallelWalk_1.getKey)(leading);
257
+ var vA = curA.leaf.values[curA.leafIndex];
258
+ var vB = curB.leaf.values[curB.leafIndex];
259
+ // Perform the actual merge of values here. The cursors will avoid adding a duplicate of this key/value
260
+ // to pending because they respect the areEqual flag during their moves.
261
+ var combined = combineFn(key, vA, vB);
262
+ if (combined !== undefined) {
263
+ pendingKeys.push(key);
264
+ pendingValues.push(combined);
265
+ }
266
+ var outTrailing = (0, parallelWalk_1.moveForwardOne)(trailing, leading);
267
+ var outLeading = (0, parallelWalk_1.moveForwardOne)(leading, trailing);
268
+ if (outTrailing || outLeading) {
269
+ if (!outTrailing || !outLeading) {
270
+ // In these cases, we pass areEqual=false because a return value of "out of tree" means
271
+ // the cursor did not move. This must be true because they started equal and one of them had more tree
272
+ // to walk (one is !out), so they cannot be equal at this point.
273
+ if (outTrailing) {
274
+ (0, parallelWalk_1.moveTo)(leading, trailing, maxKey, false, false);
275
+ }
276
+ else {
277
+ (0, parallelWalk_1.moveTo)(trailing, leading, maxKey, false, false);
278
+ }
279
+ }
280
+ break;
281
+ }
282
+ order = cmp((0, parallelWalk_1.getKey)(leading), (0, parallelWalk_1.getKey)(trailing));
283
+ }
284
+ else {
285
+ if (order < 0) {
286
+ var tmp = trailing;
287
+ trailing = leading;
288
+ leading = tmp;
289
+ }
290
+ var _a = (0, parallelWalk_1.moveTo)(trailing, leading, (0, parallelWalk_1.getKey)(leading), true, areEqual), out = _a[0], nowEqual = _a[1];
291
+ if (out) {
292
+ (0, parallelWalk_1.moveTo)(leading, trailing, maxKey, false, areEqual);
293
+ break;
294
+ }
295
+ else if (nowEqual) {
296
+ order = 0;
297
+ }
298
+ else {
299
+ order = -1;
300
+ }
301
+ }
302
+ }
303
+ // Ensure any trailing non-disjoint entries are added
304
+ (0, shared_1.makeLeavesFrom)(pendingKeys, pendingValues, maxNodeSize, decomposeLoadFactor, onLeafCreation);
305
+ // In cases like full interleaving, no leaves may be created until now
306
+ if (tallestHeight < 0 && disjointHeights.length > 0) {
307
+ tallestIndex = 0;
308
+ }
309
+ return { heights: disjointHeights, nodes: disjointNodes, tallestIndex: tallestIndex };
310
+ }
311
+ exports.decompose = decompose;
312
+ /**
313
+ * Constructs a B-Tree from the result of a decomposition (set of disjoint nodes).
314
+ * @internal
315
+ */
316
+ function buildFromDecomposition(constructor, branchingFactor, decomposed, cmp, maxNodeSize) {
317
+ var heights = decomposed.heights, nodes = decomposed.nodes, tallestIndex = decomposed.tallestIndex;
318
+ (0, b_tree_1.check)(heights.length === nodes.length, "Decompose result has mismatched heights and nodes.");
319
+ var disjointEntryCount = heights.length;
320
+ // Now we have a set of disjoint subtrees and we need to merge them into a single tree.
321
+ // To do this, we start with the tallest subtree from the disjoint set and, for all subtrees
322
+ // to the "right" and "left" of it in sorted order, we append them onto the appropriate side
323
+ // of the current tree, splitting nodes as necessary to maintain balance.
324
+ // A "side" is referred to as a frontier, as it is a linked list of nodes from the root down to
325
+ // the leaf level on that side of the tree. Each appended subtree is appended to the node at the
326
+ // same height as itself on the frontier. Each tree is guaranteed to be at most as tall as the
327
+ // current frontier because we start from the tallest subtree and work outward.
328
+ var initialRoot = nodes[tallestIndex];
329
+ var frontier = [initialRoot];
330
+ var rightContext = {
331
+ branchingFactor: branchingFactor,
332
+ spine: frontier,
333
+ sideIndex: getRightmostIndex,
334
+ sideInsertionIndex: getRightInsertionIndex,
335
+ splitOffSide: splitOffRightSide,
336
+ balanceLeaves: balanceLeavesRight,
337
+ updateMax: updateRightMax,
338
+ mergeLeaves: mergeRightEntries
339
+ };
340
+ // Process all subtrees to the right of the tallest subtree
341
+ if (tallestIndex + 1 <= disjointEntryCount - 1) {
342
+ updateFrontier(rightContext, 0);
343
+ processSide(heights, nodes, tallestIndex + 1, disjointEntryCount, 1, rightContext);
344
+ }
345
+ var leftContext = {
346
+ branchingFactor: branchingFactor,
347
+ spine: frontier,
348
+ sideIndex: getLeftmostIndex,
349
+ sideInsertionIndex: getLeftmostIndex,
350
+ splitOffSide: splitOffLeftSide,
351
+ balanceLeaves: balanceLeavesLeft,
352
+ updateMax: parallelWalk_1.noop,
353
+ mergeLeaves: mergeLeftEntries
354
+ };
355
+ // Process all subtrees to the left of the current tree
356
+ if (tallestIndex - 1 >= 0) {
357
+ // Note we need to update the frontier here because the right-side processing may have grown the tree taller.
358
+ updateFrontier(leftContext, 0);
359
+ processSide(heights, nodes, tallestIndex - 1, -1, -1, leftContext);
360
+ }
361
+ var reconstructed = new constructor(undefined, cmp, maxNodeSize);
362
+ reconstructed._root = frontier[0];
363
+ // Return the resulting tree
364
+ return reconstructed;
365
+ }
366
+ exports.buildFromDecomposition = buildFromDecomposition;
367
+ /**
368
+ * Processes one side (left or right) of the disjoint subtree set during a reconstruction operation.
369
+ * Merges each subtree in the disjoint set from start to end (exclusive) into the given spine.
370
+ * @internal
371
+ */
372
+ function processSide(heights, nodes, start, end, step, context) {
373
+ var spine = context.spine, sideIndex = context.sideIndex;
374
+ // Determine the depth of the first shared node on the frontier.
375
+ // Appending subtrees to the frontier must respect the copy-on-write semantics by cloning
376
+ // any shared nodes down to the insertion point. We track it by depth to avoid a log(n) walk of the
377
+ // frontier for each insertion as that would fundamentally change our asymptotics.
378
+ var isSharedFrontierDepth = 0;
379
+ var cur = spine[0];
380
+ // Find the first shared node on the frontier
381
+ while (!cur.isShared && isSharedFrontierDepth < spine.length - 1) {
382
+ isSharedFrontierDepth++;
383
+ cur = cur.children[sideIndex(cur)];
384
+ }
385
+ // This array holds the sum of sizes of nodes that have been inserted but not yet propagated upward.
386
+ // For example, if a subtree of size 5 is inserted at depth 2, then unflushedSizes[1] += 5.
387
+ // These sizes are added to the depth above the insertion point because the insertion updates the direct parent of the insertion.
388
+ // These sizes are flushed upward any time we need to insert at level higher than pending unflushed sizes.
389
+ // E.g. in our example, if we later insert at depth 0, we will add 5 to the node at depth 1 and the root at depth 0 before inserting.
390
+ // This scheme enables us to avoid a log(n) propagation of sizes for each insertion.
391
+ var unflushedSizes = new Array(spine.length).fill(0); // pre-fill to avoid "holey" array
392
+ for (var i = start; i != end; i += step) {
393
+ var currentHeight = spine.length - 1; // height is number of internal levels; 0 means leaf
394
+ var subtree = nodes[i];
395
+ var subtreeHeight = heights[i];
396
+ var isEntryInsertion = subtreeHeight === -1;
397
+ (0, b_tree_1.check)(subtreeHeight <= currentHeight, "Subtree taller than spine during reconstruction.");
398
+ // If subtree height is -1 (indicating underfilled leaf), then this indicates insertion into a leaf
399
+ // otherwise, it points to a node whose children have height === subtreeHeight
400
+ var insertionDepth = currentHeight - (subtreeHeight + 1);
401
+ // Ensure path is unshared before mutation
402
+ ensureNotShared(context, isSharedFrontierDepth, insertionDepth);
403
+ var insertionCount = // non-recursive
404
+ void 0; // non-recursive
405
+ var insertionSize = // recursive
406
+ void 0; // recursive
407
+ if (isEntryInsertion) {
408
+ (0, b_tree_1.check)(subtree.isShared !== true);
409
+ insertionCount = insertionSize = subtree.keys.length;
410
+ }
411
+ else {
412
+ insertionCount = 1;
413
+ insertionSize = subtree.size();
414
+ }
415
+ var cascadeEndDepth = findSplitCascadeEndDepth(context, insertionDepth, insertionCount);
416
+ // Calculate expansion depth (first ancestor with capacity)
417
+ var expansionDepth = Math.max(0, // -1 indicates we will cascade to new root
418
+ cascadeEndDepth);
419
+ // Update sizes on spine above the shared ancestor before we expand
420
+ updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, expansionDepth);
421
+ var newRoot = undefined;
422
+ var sizeChangeDepth = void 0;
423
+ if (isEntryInsertion) {
424
+ newRoot = splitUpwardsAndInsertEntries(context, insertionDepth, subtree);
425
+ // if we are inserting entries, we don't have to update a cached size on the leaf as they simply return count of keys
426
+ sizeChangeDepth = insertionDepth - 1;
427
+ }
428
+ else {
429
+ newRoot = splitUpwardsAndInsert(context, insertionDepth, subtree)[0];
430
+ sizeChangeDepth = insertionDepth;
431
+ }
432
+ if (newRoot) {
433
+ // Set the spine root to the highest up new node; the rest of the spine is updated below
434
+ spine[0] = newRoot;
435
+ unflushedSizes.push(0); // new root level, keep unflushed sizes in sync
436
+ sizeChangeDepth++; // account for the spine lengthening
437
+ }
438
+ isSharedFrontierDepth = sizeChangeDepth + 1;
439
+ unflushedSizes[sizeChangeDepth] += insertionSize;
440
+ // Finally, update the frontier from the highest new node downward
441
+ // Note that this is often the point where the new subtree is attached,
442
+ // but in the case of cascaded splits it may be higher up.
443
+ updateFrontier(context, expansionDepth);
444
+ (0, b_tree_1.check)(isSharedFrontierDepth === spine.length - 1 || spine[isSharedFrontierDepth].isShared === true, "Non-leaf subtrees must be shared.");
445
+ (0, b_tree_1.check)(unflushedSizes.length === spine.length, "Unflushed sizes length mismatch after root split.");
446
+ // Useful for debugging:
447
+ //updateSizeAndMax(context, unflushedSizes, spine.length - 1, 0);
448
+ //spine[0].checkValid(0, { _compare: cmp } as unknown as BTree<K, V>, 0);
449
+ }
450
+ // Finally, propagate any remaining unflushed sizes upward and update max keys
451
+ updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, 0);
452
+ }
453
+ ;
454
+ /**
455
+ * Cascade splits upward if capacity needed, then append a subtree at a given depth on the chosen side.
456
+ * All un-propagated sizes must have already been applied to the spine up to the end of any cascading expansions.
457
+ * This method guarantees that the size of the inserted subtree will not propagate upward beyond the insertion point.
458
+ * Returns a new root if the root was split, otherwise undefined, and the node into which the subtree was inserted.
459
+ */
460
+ function splitUpwardsAndInsert(context, insertionDepth, subtree) {
461
+ var spine = context.spine, branchingFactor = context.branchingFactor, sideIndex = context.sideIndex, sideInsertionIndex = context.sideInsertionIndex, splitOffSide = context.splitOffSide, updateMax = context.updateMax;
462
+ // We must take care to avoid accidental propagation upward of the size of the inserted subtree
463
+ // To do this, we first split nodes upward from the insertion point until we find a node with capacity
464
+ // or create a new root. Since all un-propagated sizes have already been applied to the spine up to this point,
465
+ // inserting at the end ensures no accidental propagation.
466
+ // Depth is -1 if the subtree is the same height as the current tree
467
+ if (insertionDepth >= 0) {
468
+ var carry = undefined;
469
+ // Determine initially where to insert after any splits
470
+ var insertTarget = spine[insertionDepth];
471
+ if (insertTarget.keys.length === branchingFactor) {
472
+ insertTarget = carry = splitOffSide(insertTarget);
473
+ }
474
+ var d = insertionDepth - 1;
475
+ while (carry && d >= 0) {
476
+ var parent = spine[d];
477
+ var sideChildIndex = sideIndex(parent);
478
+ // Refresh last key since child was split
479
+ updateMax(parent, parent.children[sideChildIndex].maxKey());
480
+ if (parent.keys.length < branchingFactor) {
481
+ // We have reached the end of the cascade
482
+ insertNoCount(parent, sideInsertionIndex(parent), carry);
483
+ carry = undefined;
484
+ }
485
+ else {
486
+ // Splitting the parent here requires care to avoid incorrectly double counting sizes
487
+ // Example: a node is at max capacity 4, with children each of size 4 for 16 total.
488
+ // We split the node into two nodes of 2 children each, but this does *not* modify the size
489
+ // of its parent. Therefore when we insert the carry into the torn-off node, we must not
490
+ // increase its size or we will double-count the size of the carry subtree.
491
+ var tornOff = splitOffSide(parent);
492
+ insertNoCount(tornOff, sideInsertionIndex(tornOff), carry);
493
+ carry = tornOff;
494
+ }
495
+ d--;
496
+ }
497
+ var newRoot = undefined;
498
+ if (carry !== undefined) {
499
+ // Expansion reached the root, need a new root to hold carry
500
+ var oldRoot = spine[0];
501
+ newRoot = new b_tree_1.BNodeInternal([oldRoot], oldRoot.size() + carry.size());
502
+ insertNoCount(newRoot, sideInsertionIndex(newRoot), carry);
503
+ }
504
+ // Finally, insert the subtree at the insertion point
505
+ insertNoCount(insertTarget, sideInsertionIndex(insertTarget), subtree);
506
+ return [newRoot, insertTarget];
507
+ }
508
+ else {
509
+ // Insertion of subtree with equal height to current tree
510
+ var oldRoot = spine[0];
511
+ var newRoot = new b_tree_1.BNodeInternal([oldRoot], oldRoot.size());
512
+ insertNoCount(newRoot, sideInsertionIndex(newRoot), subtree);
513
+ return [newRoot, newRoot];
514
+ }
515
+ }
516
+ ;
517
+ /**
518
+ * Inserts an underfilled leaf (entryContainer), merging with its sibling if possible and splitting upward if not.
519
+ */
520
+ function splitUpwardsAndInsertEntries(context, insertionDepth, entryContainer) {
521
+ var branchingFactor = context.branchingFactor, spine = context.spine, balanceLeaves = context.balanceLeaves, mergeLeaves = context.mergeLeaves;
522
+ var entryCount = entryContainer.keys.length;
523
+ var parent = spine[insertionDepth];
524
+ var parentSize = parent.keys.length;
525
+ if (parentSize + entryCount <= branchingFactor) {
526
+ // Sibling has capacity, just merge into it
527
+ mergeLeaves(parent, entryContainer);
528
+ return undefined;
529
+ }
530
+ else {
531
+ // As with the internal node splitUpwardsAndInsert method, this method also must make all structural changes
532
+ // to the tree before inserting any new content. This is to avoid accidental propagation of sizes upward.
533
+ var _a = splitUpwardsAndInsert(context, insertionDepth - 1, entryContainer), newRoot = _a[0], grandparent = _a[1];
534
+ var minSize = Math.floor(branchingFactor / 2);
535
+ var toTake = minSize - entryCount;
536
+ balanceLeaves(grandparent, entryContainer, toTake);
537
+ return newRoot;
538
+ }
539
+ }
540
+ /**
541
+ * Clone along the spine from [isSharedFrontierDepth to depthTo] inclusive so path is safe to mutate.
542
+ * Short-circuits if first shared node is deeper than depthTo (the insertion depth).
543
+ */
544
+ function ensureNotShared(context, isSharedFrontierDepth, depthToInclusive) {
545
+ var spine = context.spine, sideIndex = context.sideIndex;
546
+ if (depthToInclusive < 0 /* new root case */)
547
+ return; // nothing to clone when root is a leaf; equal-height case will handle this
548
+ // Clone root if needed first (depth 0)
549
+ if (isSharedFrontierDepth === 0) {
550
+ var root = spine[0];
551
+ spine[0] = root.clone();
552
+ }
553
+ // Clone downward along the frontier to 'depthToInclusive'
554
+ for (var depth = Math.max(isSharedFrontierDepth, 1); depth <= depthToInclusive; depth++) {
555
+ var parent = spine[depth - 1];
556
+ var childIndex = sideIndex(parent);
557
+ var clone = parent.children[childIndex].clone();
558
+ parent.children[childIndex] = clone;
559
+ spine[depth] = clone;
560
+ }
561
+ }
562
+ ;
563
+ /**
564
+ * Propagates size updates and updates max keys for nodes in (isSharedFrontierDepth, depthTo)
565
+ */
566
+ function updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, depthUpToInclusive) {
567
+ var spine = context.spine, updateMax = context.updateMax;
568
+ // If isSharedFrontierDepth is <= depthUpToInclusive there is nothing to update because
569
+ // the insertion point is inside a shared node which will always have correct sizes
570
+ var maxKey = spine[isSharedFrontierDepth].maxKey();
571
+ var startDepth = isSharedFrontierDepth - 1;
572
+ for (var depth = startDepth; depth >= depthUpToInclusive; depth--) {
573
+ var sizeAtLevel = unflushedSizes[depth];
574
+ unflushedSizes[depth] = 0; // we are propagating it now
575
+ if (depth > 0) {
576
+ // propagate size upward, will be added lazily, either when a subtree is appended at or above that level or
577
+ // at the end of processing the entire side
578
+ unflushedSizes[depth - 1] += sizeAtLevel;
579
+ }
580
+ var node = spine[depth];
581
+ node._size += sizeAtLevel;
582
+ // No-op if left side, as max keys in parents are unchanged by appending to the beginning of a node
583
+ updateMax(node, maxKey);
584
+ }
585
+ }
586
+ ;
587
+ /**
588
+ * Update a spine (frontier) from a specific depth down, inclusive.
589
+ * Extends the frontier array if it is not already as long as the frontier.
590
+ */
591
+ function updateFrontier(context, depthLastValid) {
592
+ var frontier = context.spine, sideIndex = context.sideIndex;
593
+ (0, b_tree_1.check)(frontier.length > depthLastValid, "updateFrontier: depthLastValid exceeds frontier height");
594
+ var startingAncestor = frontier[depthLastValid];
595
+ if (startingAncestor.isLeaf)
596
+ return;
597
+ var internal = startingAncestor;
598
+ var cur = internal.children[sideIndex(internal)];
599
+ var depth = depthLastValid + 1;
600
+ while (!cur.isLeaf) {
601
+ var ni = cur;
602
+ frontier[depth] = ni;
603
+ cur = ni.children[sideIndex(ni)];
604
+ depth++;
605
+ }
606
+ frontier[depth] = cur;
607
+ }
608
+ ;
609
+ /**
610
+ * Find the first ancestor (starting at insertionDepth) with capacity.
611
+ */
612
+ function findSplitCascadeEndDepth(context, insertionDepth, insertionCount) {
613
+ var spine = context.spine, branchingFactor = context.branchingFactor;
614
+ if (insertionDepth >= 0) {
615
+ var depth = insertionDepth;
616
+ if (spine[depth].keys.length + insertionCount <= branchingFactor) {
617
+ return depth;
618
+ }
619
+ depth--;
620
+ while (depth >= 0) {
621
+ if (spine[depth].keys.length < branchingFactor)
622
+ return depth;
623
+ depth--;
624
+ }
625
+ }
626
+ return -1; // no capacity, will need a new root
627
+ }
628
+ ;
629
+ /**
630
+ * Inserts the child without updating cached size counts.
631
+ */
632
+ function insertNoCount(parent, index, child) {
633
+ parent.children.splice(index, 0, child);
634
+ parent.keys.splice(index, 0, child.maxKey());
635
+ }
636
+ // ---- Side-specific delegates for merging subtrees into a frontier ----
637
+ function getLeftmostIndex() {
638
+ return 0;
639
+ }
640
+ function getRightmostIndex(node) {
641
+ return node.children.length - 1;
642
+ }
643
+ function getRightInsertionIndex(node) {
644
+ return node.children.length;
645
+ }
646
+ function splitOffRightSide(node) {
647
+ return node.splitOffRightSide();
648
+ }
649
+ function splitOffLeftSide(node) {
650
+ return node.splitOffLeftSide();
651
+ }
652
+ function balanceLeavesRight(parent, underfilled, toTake) {
653
+ var siblingIndex = parent.children.length - 2;
654
+ var sibling = parent.children[siblingIndex];
655
+ var index = sibling.keys.length - toTake;
656
+ var movedKeys = sibling.keys.splice(index);
657
+ var movedValues = sibling.values.splice(index);
658
+ underfilled.keys.unshift.apply(underfilled.keys, movedKeys);
659
+ underfilled.values.unshift.apply(underfilled.values, movedValues);
660
+ parent.keys[siblingIndex] = sibling.maxKey();
661
+ }
662
+ function balanceLeavesLeft(parent, underfilled, toTake) {
663
+ var sibling = parent.children[1];
664
+ var movedKeys = sibling.keys.splice(0, toTake);
665
+ var movedValues = sibling.values.splice(0, toTake);
666
+ underfilled.keys.push.apply(underfilled.keys, movedKeys);
667
+ underfilled.values.push.apply(underfilled.values, movedValues);
668
+ parent.keys[0] = underfilled.maxKey();
669
+ }
670
+ function updateRightMax(node, maxBelow) {
671
+ node.keys[node.keys.length - 1] = maxBelow;
672
+ }
673
+ function mergeRightEntries(leaf, entries) {
674
+ leaf.keys.push.apply(leaf.keys, entries.keys);
675
+ leaf.values.push.apply(leaf.values, entries.values);
676
+ }
677
+ function mergeLeftEntries(leaf, entries) {
678
+ leaf.keys.unshift.apply(leaf.keys, entries.keys);
679
+ leaf.values.unshift.apply(leaf.values, entries.values);
680
+ }