sorted-btree
Version:
A sorted list of key-value pairs in a fast, typed in-memory B+ tree with a powerful API.
681 lines (680 loc) • 35.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildFromDecomposition = exports.decompose = void 0;
var b_tree_1 = require("../b+tree");
var shared_1 = require("./shared");
var parallelWalk_1 = require("./parallelWalk");
var decomposeLoadFactor = 0.7;
/**
* Decomposes two trees into disjoint nodes. Reuses interior nodes when they do not overlap/intersect with any leaf nodes
* in the other tree. Overlapping leaf nodes are broken down into new leaf nodes containing merged entries.
* The algorithm is a parallel tree walk using two cursors. The trailing cursor (behind in key space) is walked forward
* until it is at or after the leading cursor. As it does this, any whole nodes or subtrees it passes are guaranteed to
* be disjoint. This is true because the leading cursor was also previously walked in this way, and is thus pointing to
* the first key at or after the trailing cursor's previous position.
* The cursor walk is efficient, meaning it skips over disjoint subtrees entirely rather than visiting every leaf.
* Note: some of the returned leaves may be underfilled.
* @internal
*/
function decompose(left, right, combineFn, ignoreRight) {
if (ignoreRight === void 0) { ignoreRight = false; }
var maxNodeSize = left._maxNodeSize;
var cmp = left._compare;
(0, b_tree_1.check)(left._root.size() > 0 && right._root.size() > 0, "decompose requires non-empty inputs");
// Holds the disjoint nodes that result from decomposition.
// Stored as parallel arrays of (height, node) to avoid creating many tiny tuples
var disjointHeights = [];
var disjointNodes = [];
// During the decomposition, leaves that are not disjoint are decomposed into individual entries
// that accumulate in this array in sorted order. They are flushed into leaf nodes whenever a reused
// disjoint subtree is added to the disjoint set.
// Note that there are unavoidable cases in which this will generate underfilled leaves.
// An example of this would be a leaf in one tree that contained keys [0, 100, 101, 102].
// In the other tree, there is a leaf that contains [2, 3, 4, 5]. This leaf can be reused entirely,
// but the first tree's leaf must be decomposed into [0] and [100, 101, 102]
var pendingKeys = [];
var pendingValues = [];
var tallestIndex = -1, tallestHeight = -1;
// During the upward part of the cursor walk, this holds the highest disjoint node seen so far.
// This is done because we cannot know immediately whether we can add the node to the disjoint set
// because its ancestor may also be disjoint and should be reused instead.
var highestDisjoint
// Have to do this as cast to convince TS it's ever assigned
= undefined;
var minSize = Math.floor(maxNodeSize / 2);
var onLeafCreation = function (leaf) {
var height = leaf.keys.length < minSize ? -1 : 0;
disjointHeights.push(height);
disjointNodes.push(leaf);
};
var addSharedNodeToDisjointSet = function (node, height) {
// flush pending entries
(0, shared_1.makeLeavesFrom)(pendingKeys, pendingValues, maxNodeSize, decomposeLoadFactor, onLeafCreation);
pendingKeys.length = 0;
pendingValues.length = 0;
// Don't share underfilled leaves, instead mark them as needing merging
if (node.isLeaf && node.keys.length < minSize) {
disjointHeights.push(-1);
disjointNodes.push(node.clone());
}
else {
node.isShared = true;
disjointHeights.push(height);
disjointNodes.push(node);
}
if (height > tallestHeight) {
tallestIndex = disjointHeights.length - 1;
tallestHeight = height;
}
};
var addHighestDisjoint = function () {
if (highestDisjoint !== undefined) {
addSharedNodeToDisjointSet(highestDisjoint.node, highestDisjoint.height);
highestDisjoint = undefined;
}
};
// Mark all nodes at or above depthFrom in the cursor spine as disqualified (non-disjoint)
var disqualifySpine = function (cursor, depthFrom) {
var spine = cursor.spine;
for (var i = depthFrom; i >= 0; --i) {
var payload = spine[i].payload;
// Safe to early out because we always disqualify all ancestors of a disqualified node
// That is correct because every ancestor of a non-disjoint node is also non-disjoint
// because it must enclose the non-disjoint range.
if (payload.disqualified)
break;
payload.disqualified = true;
}
};
// Cursor payload factory
var makePayload = function () { return ({ disqualified: false }); };
var pushLeafRange = function (leaf, from, toExclusive) {
var keys = leaf.keys;
var values = leaf.values;
for (var i = from; i < toExclusive; ++i) {
pendingKeys.push(keys[i]);
pendingValues.push(values[i]);
}
};
var onMoveInLeaf = function (leaf, payload, fromIndex, toIndex, startedEqual) {
(0, b_tree_1.check)(payload.disqualified === true, "onMoveInLeaf: leaf must be disqualified");
var start = startedEqual ? fromIndex + 1 : fromIndex;
if (start < toIndex)
pushLeafRange(leaf, start, toIndex);
};
var onExitLeaf = function (leaf, payload, startingIndex, startedEqual, cursorThis) {
highestDisjoint = undefined;
if (!payload.disqualified) {
highestDisjoint = { node: leaf, height: 0 };
if (cursorThis.spine.length === 0) {
// if we are exiting a leaf and there are no internal nodes, we will reach the end of the tree.
// In this case we need to add the leaf now because step up will not be called.
addHighestDisjoint();
}
}
else {
var start = startedEqual ? startingIndex + 1 : startingIndex;
var leafSize = leaf.keys.length;
if (start < leafSize)
pushLeafRange(leaf, start, leafSize);
}
};
var onStepUp = function (parent, height, payload, fromIndex, spineIndex, stepDownIndex, cursorThis) {
var children = parent.children;
var nextHeight = height - 1;
if (stepDownIndex !== stepDownIndex /* NaN: still walking up */
|| stepDownIndex === Number.POSITIVE_INFINITY /* target key is beyond edge of tree, done with walk */) {
if (!payload.disqualified) {
if (stepDownIndex === Number.POSITIVE_INFINITY) {
// We have finished our walk, and we won't be stepping down, so add the root
// Roots are allowed to be underfilled, so break the root up here if so to avoid
// creating underfilled interior nodes during reconstruction.
// Note: the main btree implementation allows underfilled nodes in general, this algorithm
// guarantees that no additional underfilled nodes are created beyond what was already present.
if (parent.keys.length < minSize) {
for (var i = fromIndex; i < children.length; ++i)
addSharedNodeToDisjointSet(children[i], nextHeight);
}
else {
addSharedNodeToDisjointSet(parent, height);
}
highestDisjoint = undefined;
}
else {
highestDisjoint = { node: parent, height: height };
}
}
else {
addHighestDisjoint();
var len = children.length;
for (var i = fromIndex + 1; i < len; ++i)
addSharedNodeToDisjointSet(children[i], nextHeight);
}
}
else {
// We have a valid step down index, so we need to disqualify the spine if needed.
// This is identical to the step down logic, but we must also perform it here because
// in the case of stepping down into a leaf, the step down callback is never called.
if (stepDownIndex > 0) {
disqualifySpine(cursorThis, spineIndex);
}
addHighestDisjoint();
for (var i = fromIndex + 1; i < stepDownIndex; ++i)
addSharedNodeToDisjointSet(children[i], nextHeight);
}
};
var onStepDown = function (node, height, spineIndex, stepDownIndex, cursorThis) {
if (stepDownIndex > 0) {
// When we step down into a node, we know that we have walked from a key that is less than our target.
// Because of this, if we are not stepping down into the first child, we know that all children before
// the stepDownIndex must overlap with the other tree because they must be before our target key. Since
// the child we are stepping into has a key greater than our target key, this node must overlap.
// If a child overlaps, the entire spine overlaps because a parent in a btree always encloses the range
// of its children.
disqualifySpine(cursorThis, spineIndex);
var children = node.children;
var nextHeight = height - 1;
for (var i = 0; i < stepDownIndex; ++i)
addSharedNodeToDisjointSet(children[i], nextHeight);
}
};
var onEnterLeaf = function (leaf, destIndex, cursorThis, cursorOther) {
if (destIndex > 0
|| (0, b_tree_1.areOverlapping)(leaf.minKey(), leaf.maxKey(), (0, parallelWalk_1.getKey)(cursorOther), cursorOther.leaf.maxKey(), cmp)) {
// Similar logic to the step-down case, except in this case we also know the leaf in the other
// tree overlaps a leaf in this tree (this leaf, specifically). Thus, we can disqualify both spines.
cursorThis.leafPayload.disqualified = true;
cursorOther.leafPayload.disqualified = true;
disqualifySpine(cursorThis, cursorThis.spine.length - 1);
disqualifySpine(cursorOther, cursorOther.spine.length - 1);
pushLeafRange(leaf, 0, destIndex);
}
};
// Need the max key of both trees to perform the "finishing" walk of which ever cursor finishes second
var maxKeyLeft = left._root.maxKey();
var maxKeyRight = right._root.maxKey();
var maxKey = cmp(maxKeyLeft, maxKeyRight) >= 0 ? maxKeyLeft : maxKeyRight;
// Initialize cursors at minimum keys.
var curA = (0, parallelWalk_1.createCursor)(left, makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown);
var curB;
if (ignoreRight) {
var dummyPayload_1 = { disqualified: true };
var onStepUpIgnore = function (_1, _2, _3, _4, spineIndex, stepDownIndex, cursorThis) {
if (stepDownIndex > 0) {
disqualifySpine(cursorThis, spineIndex);
}
};
var onStepDownIgnore = function (_, __, spineIndex, stepDownIndex, cursorThis) {
if (stepDownIndex > 0) {
disqualifySpine(cursorThis, spineIndex);
}
};
var onEnterLeafIgnore = function (leaf, destIndex, _, cursorOther) {
if (destIndex > 0
|| (0, b_tree_1.areOverlapping)(leaf.minKey(), leaf.maxKey(), (0, parallelWalk_1.getKey)(cursorOther), cursorOther.leaf.maxKey(), cmp)) {
cursorOther.leafPayload.disqualified = true;
disqualifySpine(cursorOther, cursorOther.spine.length - 1);
}
};
curB = (0, parallelWalk_1.createCursor)(right, function () { return dummyPayload_1; }, onEnterLeafIgnore, parallelWalk_1.noop, parallelWalk_1.noop, onStepUpIgnore, onStepDownIgnore);
}
else {
curB = (0, parallelWalk_1.createCursor)(right, makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown);
}
// The guarantee that no overlapping interior nodes are accidentally reused relies on the careful
// alternating hopping walk of the cursors: WLOG, cursorA always--with one exception--walks from a key just behind (in key space)
// the key of cursorB to the first key >= cursorB. Call this transition a "crossover point." All interior nodes that
// overlap cause a crossover point, and all crossover points are guaranteed to be walked using this method. Thus,
// all overlapping interior nodes will be found if they are checked for on step-down.
// The one exception mentioned above is when they start at the same key. In this case, they are both advanced forward and then
// their new ordering determines how they walk from there.
// The one issue then is detecting any overlaps that occur based on their very initial position (minimum key of each tree).
// This is handled by the initial disqualification step below, which essentially emulates the step down disqualification for each spine.
// Initialize disqualification w.r.t. opposite leaf.
var initDisqualify = function (cur, other) {
var minKey = (0, parallelWalk_1.getKey)(cur);
var otherMin = (0, parallelWalk_1.getKey)(other);
var otherMax = other.leaf.maxKey();
if ((0, b_tree_1.areOverlapping)(minKey, cur.leaf.maxKey(), otherMin, otherMax, cmp))
cur.leafPayload.disqualified = true;
for (var i = 0; i < cur.spine.length; ++i) {
var entry = cur.spine[i];
// Since we are on the left side of the tree, we can use the leaf min key for every spine node
if ((0, b_tree_1.areOverlapping)(minKey, entry.node.maxKey(), otherMin, otherMax, cmp))
entry.payload.disqualified = true;
}
};
initDisqualify(curA, curB);
initDisqualify(curB, curA);
var leading = curA;
var trailing = curB;
var order = cmp((0, parallelWalk_1.getKey)(leading), (0, parallelWalk_1.getKey)(trailing));
// Walk both cursors in alternating hops
while (true) {
var areEqual = order === 0;
if (areEqual) {
var key = (0, parallelWalk_1.getKey)(leading);
var vA = curA.leaf.values[curA.leafIndex];
var vB = curB.leaf.values[curB.leafIndex];
// Perform the actual merge of values here. The cursors will avoid adding a duplicate of this key/value
// to pending because they respect the areEqual flag during their moves.
var combined = combineFn(key, vA, vB);
if (combined !== undefined) {
pendingKeys.push(key);
pendingValues.push(combined);
}
var outTrailing = (0, parallelWalk_1.moveForwardOne)(trailing, leading);
var outLeading = (0, parallelWalk_1.moveForwardOne)(leading, trailing);
if (outTrailing || outLeading) {
if (!outTrailing || !outLeading) {
// In these cases, we pass areEqual=false because a return value of "out of tree" means
// the cursor did not move. This must be true because they started equal and one of them had more tree
// to walk (one is !out), so they cannot be equal at this point.
if (outTrailing) {
(0, parallelWalk_1.moveTo)(leading, trailing, maxKey, false, false);
}
else {
(0, parallelWalk_1.moveTo)(trailing, leading, maxKey, false, false);
}
}
break;
}
order = cmp((0, parallelWalk_1.getKey)(leading), (0, parallelWalk_1.getKey)(trailing));
}
else {
if (order < 0) {
var tmp = trailing;
trailing = leading;
leading = tmp;
}
var _a = (0, parallelWalk_1.moveTo)(trailing, leading, (0, parallelWalk_1.getKey)(leading), true, areEqual), out = _a[0], nowEqual = _a[1];
if (out) {
(0, parallelWalk_1.moveTo)(leading, trailing, maxKey, false, areEqual);
break;
}
else if (nowEqual) {
order = 0;
}
else {
order = -1;
}
}
}
// Ensure any trailing non-disjoint entries are added
(0, shared_1.makeLeavesFrom)(pendingKeys, pendingValues, maxNodeSize, decomposeLoadFactor, onLeafCreation);
// In cases like full interleaving, no leaves may be created until now
if (tallestHeight < 0 && disjointHeights.length > 0) {
tallestIndex = 0;
}
return { heights: disjointHeights, nodes: disjointNodes, tallestIndex: tallestIndex };
}
exports.decompose = decompose;
/**
* Constructs a B-Tree from the result of a decomposition (set of disjoint nodes).
* @internal
*/
function buildFromDecomposition(constructor, branchingFactor, decomposed, cmp, maxNodeSize) {
var heights = decomposed.heights, nodes = decomposed.nodes, tallestIndex = decomposed.tallestIndex;
(0, b_tree_1.check)(heights.length === nodes.length, "Decompose result has mismatched heights and nodes.");
var disjointEntryCount = heights.length;
// Now we have a set of disjoint subtrees and we need to merge them into a single tree.
// To do this, we start with the tallest subtree from the disjoint set and, for all subtrees
// to the "right" and "left" of it in sorted order, we append them onto the appropriate side
// of the current tree, splitting nodes as necessary to maintain balance.
// A "side" is referred to as a frontier, as it is a linked list of nodes from the root down to
// the leaf level on that side of the tree. Each appended subtree is appended to the node at the
// same height as itself on the frontier. Each tree is guaranteed to be at most as tall as the
// current frontier because we start from the tallest subtree and work outward.
var initialRoot = nodes[tallestIndex];
var frontier = [initialRoot];
var rightContext = {
branchingFactor: branchingFactor,
spine: frontier,
sideIndex: getRightmostIndex,
sideInsertionIndex: getRightInsertionIndex,
splitOffSide: splitOffRightSide,
balanceLeaves: balanceLeavesRight,
updateMax: updateRightMax,
mergeLeaves: mergeRightEntries
};
// Process all subtrees to the right of the tallest subtree
if (tallestIndex + 1 <= disjointEntryCount - 1) {
updateFrontier(rightContext, 0);
processSide(heights, nodes, tallestIndex + 1, disjointEntryCount, 1, rightContext);
}
var leftContext = {
branchingFactor: branchingFactor,
spine: frontier,
sideIndex: getLeftmostIndex,
sideInsertionIndex: getLeftmostIndex,
splitOffSide: splitOffLeftSide,
balanceLeaves: balanceLeavesLeft,
updateMax: parallelWalk_1.noop,
mergeLeaves: mergeLeftEntries
};
// Process all subtrees to the left of the current tree
if (tallestIndex - 1 >= 0) {
// Note we need to update the frontier here because the right-side processing may have grown the tree taller.
updateFrontier(leftContext, 0);
processSide(heights, nodes, tallestIndex - 1, -1, -1, leftContext);
}
var reconstructed = new constructor(undefined, cmp, maxNodeSize);
reconstructed._root = frontier[0];
// Return the resulting tree
return reconstructed;
}
exports.buildFromDecomposition = buildFromDecomposition;
/**
* Processes one side (left or right) of the disjoint subtree set during a reconstruction operation.
* Merges each subtree in the disjoint set from start to end (exclusive) into the given spine.
* @internal
*/
function processSide(heights, nodes, start, end, step, context) {
var spine = context.spine, sideIndex = context.sideIndex;
// Determine the depth of the first shared node on the frontier.
// Appending subtrees to the frontier must respect the copy-on-write semantics by cloning
// any shared nodes down to the insertion point. We track it by depth to avoid a log(n) walk of the
// frontier for each insertion as that would fundamentally change our asymptotics.
var isSharedFrontierDepth = 0;
var cur = spine[0];
// Find the first shared node on the frontier
while (!cur.isShared && isSharedFrontierDepth < spine.length - 1) {
isSharedFrontierDepth++;
cur = cur.children[sideIndex(cur)];
}
// This array holds the sum of sizes of nodes that have been inserted but not yet propagated upward.
// For example, if a subtree of size 5 is inserted at depth 2, then unflushedSizes[1] += 5.
// These sizes are added to the depth above the insertion point because the insertion updates the direct parent of the insertion.
// These sizes are flushed upward any time we need to insert at level higher than pending unflushed sizes.
// 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.
// This scheme enables us to avoid a log(n) propagation of sizes for each insertion.
var unflushedSizes = new Array(spine.length).fill(0); // pre-fill to avoid "holey" array
for (var i = start; i != end; i += step) {
var currentHeight = spine.length - 1; // height is number of internal levels; 0 means leaf
var subtree = nodes[i];
var subtreeHeight = heights[i];
var isEntryInsertion = subtreeHeight === -1;
(0, b_tree_1.check)(subtreeHeight <= currentHeight, "Subtree taller than spine during reconstruction.");
// If subtree height is -1 (indicating underfilled leaf), then this indicates insertion into a leaf
// otherwise, it points to a node whose children have height === subtreeHeight
var insertionDepth = currentHeight - (subtreeHeight + 1);
// Ensure path is unshared before mutation
ensureNotShared(context, isSharedFrontierDepth, insertionDepth);
var insertionCount = // non-recursive
void 0; // non-recursive
var insertionSize = // recursive
void 0; // recursive
if (isEntryInsertion) {
(0, b_tree_1.check)(subtree.isShared !== true);
insertionCount = insertionSize = subtree.keys.length;
}
else {
insertionCount = 1;
insertionSize = subtree.size();
}
var cascadeEndDepth = findSplitCascadeEndDepth(context, insertionDepth, insertionCount);
// Calculate expansion depth (first ancestor with capacity)
var expansionDepth = Math.max(0, // -1 indicates we will cascade to new root
cascadeEndDepth);
// Update sizes on spine above the shared ancestor before we expand
updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, expansionDepth);
var newRoot = undefined;
var sizeChangeDepth = void 0;
if (isEntryInsertion) {
newRoot = splitUpwardsAndInsertEntries(context, insertionDepth, subtree);
// if we are inserting entries, we don't have to update a cached size on the leaf as they simply return count of keys
sizeChangeDepth = insertionDepth - 1;
}
else {
newRoot = splitUpwardsAndInsert(context, insertionDepth, subtree)[0];
sizeChangeDepth = insertionDepth;
}
if (newRoot) {
// Set the spine root to the highest up new node; the rest of the spine is updated below
spine[0] = newRoot;
unflushedSizes.push(0); // new root level, keep unflushed sizes in sync
sizeChangeDepth++; // account for the spine lengthening
}
isSharedFrontierDepth = sizeChangeDepth + 1;
unflushedSizes[sizeChangeDepth] += insertionSize;
// Finally, update the frontier from the highest new node downward
// Note that this is often the point where the new subtree is attached,
// but in the case of cascaded splits it may be higher up.
updateFrontier(context, expansionDepth);
(0, b_tree_1.check)(isSharedFrontierDepth === spine.length - 1 || spine[isSharedFrontierDepth].isShared === true, "Non-leaf subtrees must be shared.");
(0, b_tree_1.check)(unflushedSizes.length === spine.length, "Unflushed sizes length mismatch after root split.");
// Useful for debugging:
//updateSizeAndMax(context, unflushedSizes, spine.length - 1, 0);
//spine[0].checkValid(0, { _compare: cmp } as unknown as BTree<K, V>, 0);
}
// Finally, propagate any remaining unflushed sizes upward and update max keys
updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, 0);
}
;
/**
* Cascade splits upward if capacity needed, then append a subtree at a given depth on the chosen side.
* All un-propagated sizes must have already been applied to the spine up to the end of any cascading expansions.
* This method guarantees that the size of the inserted subtree will not propagate upward beyond the insertion point.
* Returns a new root if the root was split, otherwise undefined, and the node into which the subtree was inserted.
*/
function splitUpwardsAndInsert(context, insertionDepth, subtree) {
var spine = context.spine, branchingFactor = context.branchingFactor, sideIndex = context.sideIndex, sideInsertionIndex = context.sideInsertionIndex, splitOffSide = context.splitOffSide, updateMax = context.updateMax;
// We must take care to avoid accidental propagation upward of the size of the inserted subtree
// To do this, we first split nodes upward from the insertion point until we find a node with capacity
// or create a new root. Since all un-propagated sizes have already been applied to the spine up to this point,
// inserting at the end ensures no accidental propagation.
// Depth is -1 if the subtree is the same height as the current tree
if (insertionDepth >= 0) {
var carry = undefined;
// Determine initially where to insert after any splits
var insertTarget = spine[insertionDepth];
if (insertTarget.keys.length === branchingFactor) {
insertTarget = carry = splitOffSide(insertTarget);
}
var d = insertionDepth - 1;
while (carry && d >= 0) {
var parent = spine[d];
var sideChildIndex = sideIndex(parent);
// Refresh last key since child was split
updateMax(parent, parent.children[sideChildIndex].maxKey());
if (parent.keys.length < branchingFactor) {
// We have reached the end of the cascade
insertNoCount(parent, sideInsertionIndex(parent), carry);
carry = undefined;
}
else {
// Splitting the parent here requires care to avoid incorrectly double counting sizes
// Example: a node is at max capacity 4, with children each of size 4 for 16 total.
// We split the node into two nodes of 2 children each, but this does *not* modify the size
// of its parent. Therefore when we insert the carry into the torn-off node, we must not
// increase its size or we will double-count the size of the carry subtree.
var tornOff = splitOffSide(parent);
insertNoCount(tornOff, sideInsertionIndex(tornOff), carry);
carry = tornOff;
}
d--;
}
var newRoot = undefined;
if (carry !== undefined) {
// Expansion reached the root, need a new root to hold carry
var oldRoot = spine[0];
newRoot = new b_tree_1.BNodeInternal([oldRoot], oldRoot.size() + carry.size());
insertNoCount(newRoot, sideInsertionIndex(newRoot), carry);
}
// Finally, insert the subtree at the insertion point
insertNoCount(insertTarget, sideInsertionIndex(insertTarget), subtree);
return [newRoot, insertTarget];
}
else {
// Insertion of subtree with equal height to current tree
var oldRoot = spine[0];
var newRoot = new b_tree_1.BNodeInternal([oldRoot], oldRoot.size());
insertNoCount(newRoot, sideInsertionIndex(newRoot), subtree);
return [newRoot, newRoot];
}
}
;
/**
* Inserts an underfilled leaf (entryContainer), merging with its sibling if possible and splitting upward if not.
*/
function splitUpwardsAndInsertEntries(context, insertionDepth, entryContainer) {
var branchingFactor = context.branchingFactor, spine = context.spine, balanceLeaves = context.balanceLeaves, mergeLeaves = context.mergeLeaves;
var entryCount = entryContainer.keys.length;
var parent = spine[insertionDepth];
var parentSize = parent.keys.length;
if (parentSize + entryCount <= branchingFactor) {
// Sibling has capacity, just merge into it
mergeLeaves(parent, entryContainer);
return undefined;
}
else {
// As with the internal node splitUpwardsAndInsert method, this method also must make all structural changes
// to the tree before inserting any new content. This is to avoid accidental propagation of sizes upward.
var _a = splitUpwardsAndInsert(context, insertionDepth - 1, entryContainer), newRoot = _a[0], grandparent = _a[1];
var minSize = Math.floor(branchingFactor / 2);
var toTake = minSize - entryCount;
balanceLeaves(grandparent, entryContainer, toTake);
return newRoot;
}
}
/**
* Clone along the spine from [isSharedFrontierDepth to depthTo] inclusive so path is safe to mutate.
* Short-circuits if first shared node is deeper than depthTo (the insertion depth).
*/
function ensureNotShared(context, isSharedFrontierDepth, depthToInclusive) {
var spine = context.spine, sideIndex = context.sideIndex;
if (depthToInclusive < 0 /* new root case */)
return; // nothing to clone when root is a leaf; equal-height case will handle this
// Clone root if needed first (depth 0)
if (isSharedFrontierDepth === 0) {
var root = spine[0];
spine[0] = root.clone();
}
// Clone downward along the frontier to 'depthToInclusive'
for (var depth = Math.max(isSharedFrontierDepth, 1); depth <= depthToInclusive; depth++) {
var parent = spine[depth - 1];
var childIndex = sideIndex(parent);
var clone = parent.children[childIndex].clone();
parent.children[childIndex] = clone;
spine[depth] = clone;
}
}
;
/**
* Propagates size updates and updates max keys for nodes in (isSharedFrontierDepth, depthTo)
*/
function updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, depthUpToInclusive) {
var spine = context.spine, updateMax = context.updateMax;
// If isSharedFrontierDepth is <= depthUpToInclusive there is nothing to update because
// the insertion point is inside a shared node which will always have correct sizes
var maxKey = spine[isSharedFrontierDepth].maxKey();
var startDepth = isSharedFrontierDepth - 1;
for (var depth = startDepth; depth >= depthUpToInclusive; depth--) {
var sizeAtLevel = unflushedSizes[depth];
unflushedSizes[depth] = 0; // we are propagating it now
if (depth > 0) {
// propagate size upward, will be added lazily, either when a subtree is appended at or above that level or
// at the end of processing the entire side
unflushedSizes[depth - 1] += sizeAtLevel;
}
var node = spine[depth];
node._size += sizeAtLevel;
// No-op if left side, as max keys in parents are unchanged by appending to the beginning of a node
updateMax(node, maxKey);
}
}
;
/**
* Update a spine (frontier) from a specific depth down, inclusive.
* Extends the frontier array if it is not already as long as the frontier.
*/
function updateFrontier(context, depthLastValid) {
var frontier = context.spine, sideIndex = context.sideIndex;
(0, b_tree_1.check)(frontier.length > depthLastValid, "updateFrontier: depthLastValid exceeds frontier height");
var startingAncestor = frontier[depthLastValid];
if (startingAncestor.isLeaf)
return;
var internal = startingAncestor;
var cur = internal.children[sideIndex(internal)];
var depth = depthLastValid + 1;
while (!cur.isLeaf) {
var ni = cur;
frontier[depth] = ni;
cur = ni.children[sideIndex(ni)];
depth++;
}
frontier[depth] = cur;
}
;
/**
* Find the first ancestor (starting at insertionDepth) with capacity.
*/
function findSplitCascadeEndDepth(context, insertionDepth, insertionCount) {
var spine = context.spine, branchingFactor = context.branchingFactor;
if (insertionDepth >= 0) {
var depth = insertionDepth;
if (spine[depth].keys.length + insertionCount <= branchingFactor) {
return depth;
}
depth--;
while (depth >= 0) {
if (spine[depth].keys.length < branchingFactor)
return depth;
depth--;
}
}
return -1; // no capacity, will need a new root
}
;
/**
* Inserts the child without updating cached size counts.
*/
function insertNoCount(parent, index, child) {
parent.children.splice(index, 0, child);
parent.keys.splice(index, 0, child.maxKey());
}
// ---- Side-specific delegates for merging subtrees into a frontier ----
function getLeftmostIndex() {
return 0;
}
function getRightmostIndex(node) {
return node.children.length - 1;
}
function getRightInsertionIndex(node) {
return node.children.length;
}
function splitOffRightSide(node) {
return node.splitOffRightSide();
}
function splitOffLeftSide(node) {
return node.splitOffLeftSide();
}
function balanceLeavesRight(parent, underfilled, toTake) {
var siblingIndex = parent.children.length - 2;
var sibling = parent.children[siblingIndex];
var index = sibling.keys.length - toTake;
var movedKeys = sibling.keys.splice(index);
var movedValues = sibling.values.splice(index);
underfilled.keys.unshift.apply(underfilled.keys, movedKeys);
underfilled.values.unshift.apply(underfilled.values, movedValues);
parent.keys[siblingIndex] = sibling.maxKey();
}
function balanceLeavesLeft(parent, underfilled, toTake) {
var sibling = parent.children[1];
var movedKeys = sibling.keys.splice(0, toTake);
var movedValues = sibling.values.splice(0, toTake);
underfilled.keys.push.apply(underfilled.keys, movedKeys);
underfilled.values.push.apply(underfilled.values, movedValues);
parent.keys[0] = underfilled.maxKey();
}
function updateRightMax(node, maxBelow) {
node.keys[node.keys.length - 1] = maxBelow;
}
function mergeRightEntries(leaf, entries) {
leaf.keys.push.apply(leaf.keys, entries.keys);
leaf.values.push.apply(leaf.values, entries.values);
}
function mergeLeftEntries(leaf, entries) {
leaf.keys.unshift.apply(leaf.keys, entries.keys);
leaf.values.unshift.apply(leaf.values, entries.values);
}