@tanstack/db
Version:
A reactive client store for building super fast apps on sync
678 lines (677 loc) • 24.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
class BTree {
/**
* Initializes an empty B+ tree.
* @param compare Custom function to compare pairs of elements in the tree.
* If not specified, defaultComparator will be used which is valid as long as K extends DefaultComparable.
* @param entries A set of key-value pairs to initialize the tree
* @param maxNodeSize Branching factor (maximum items or children per node)
* Must be in range 4..256. If undefined or <4 then default is used; if >256 then 256.
*/
constructor(compare, entries, maxNodeSize) {
this._root = EmptyLeaf;
this._size = 0;
this._maxNodeSize = maxNodeSize >= 4 ? Math.min(maxNodeSize, 256) : 32;
this._compare = compare;
if (entries) this.setPairs(entries);
}
// ///////////////////////////////////////////////////////////////////////////
// ES6 Map<K,V> methods /////////////////////////////////////////////////////
/** Gets the number of key-value pairs in the tree. */
get size() {
return this._size;
}
/** Gets the number of key-value pairs in the tree. */
get length() {
return this._size;
}
/** Returns true iff the tree contains no key-value pairs. */
get isEmpty() {
return this._size === 0;
}
/** Releases the tree so that its size is 0. */
clear() {
this._root = EmptyLeaf;
this._size = 0;
}
/**
* Finds a pair in the tree and returns the associated value.
* @param defaultValue a value to return if the key was not found.
* @returns the value, or defaultValue if the key was not found.
* @description Computational complexity: O(log size)
*/
get(key, defaultValue) {
return this._root.get(key, defaultValue, this);
}
/**
* Adds or overwrites a key-value pair in the B+ tree.
* @param key the key is used to determine the sort order of
* data in the tree.
* @param value data to associate with the key (optional)
* @param overwrite Whether to overwrite an existing key-value pair
* (default: true). If this is false and there is an existing
* key-value pair then this method has no effect.
* @returns true if a new key-value pair was added.
* @description Computational complexity: O(log size)
* Note: when overwriting a previous entry, the key is updated
* as well as the value. This has no effect unless the new key
* has data that does not affect its sort order.
*/
set(key, value, overwrite) {
if (this._root.isShared) this._root = this._root.clone();
const result = this._root.set(key, value, overwrite, this);
if (result === true || result === false) return result;
this._root = new BNodeInternal([this._root, result]);
return true;
}
/**
* Returns true if the key exists in the B+ tree, false if not.
* Use get() for best performance; use has() if you need to
* distinguish between "undefined value" and "key not present".
* @param key Key to detect
* @description Computational complexity: O(log size)
*/
has(key) {
return this.forRange(key, key, true, void 0) !== 0;
}
/**
* Removes a single key-value pair from the B+ tree.
* @param key Key to find
* @returns true if a pair was found and removed, false otherwise.
* @description Computational complexity: O(log size)
*/
delete(key) {
return this.editRange(key, key, true, DeleteRange) !== 0;
}
// ///////////////////////////////////////////////////////////////////////////
// Additional methods ///////////////////////////////////////////////////////
/** Returns the maximum number of children/values before nodes will split. */
get maxNodeSize() {
return this._maxNodeSize;
}
/** Gets the lowest key in the tree. Complexity: O(log size) */
minKey() {
return this._root.minKey();
}
/** Gets the highest key in the tree. Complexity: O(1) */
maxKey() {
return this._root.maxKey();
}
/** Gets an array of all keys, sorted */
keysArray() {
const results = [];
this._root.forRange(
this.minKey(),
this.maxKey(),
true,
false,
this,
0,
(k, _v) => {
results.push(k);
}
);
return results;
}
/** Returns the next pair whose key is larger than the specified key (or undefined if there is none).
* If key === undefined, this function returns the lowest pair.
* @param key The key to search for.
* @param reusedArray Optional array used repeatedly to store key-value pairs, to
* avoid creating a new array on every iteration.
*/
nextHigherPair(key, reusedArray) {
reusedArray = reusedArray || [];
if (key === void 0) {
return this._root.minPair(reusedArray);
}
return this._root.getPairOrNextHigher(
key,
this._compare,
false,
reusedArray
);
}
/** Returns the next pair whose key is smaller than the specified key (or undefined if there is none).
* If key === undefined, this function returns the highest pair.
* @param key The key to search for.
* @param reusedArray Optional array used repeatedly to store key-value pairs, to
* avoid creating a new array each time you call this method.
*/
nextLowerPair(key, reusedArray) {
reusedArray = reusedArray || [];
if (key === void 0) {
return this._root.maxPair(reusedArray);
}
return this._root.getPairOrNextLower(key, this._compare, false, reusedArray);
}
/** Adds all pairs from a list of key-value pairs.
* @param pairs Pairs to add to this tree. If there are duplicate keys,
* later pairs currently overwrite earlier ones (e.g. [[0,1],[0,7]]
* associates 0 with 7.)
* @param overwrite Whether to overwrite pairs that already exist (if false,
* pairs[i] is ignored when the key pairs[i][0] already exists.)
* @returns The number of pairs added to the collection.
* @description Computational complexity: O(pairs.length * log(size + pairs.length))
*/
setPairs(pairs, overwrite) {
let added = 0;
for (const pair of pairs) {
if (this.set(pair[0], pair[1], overwrite)) added++;
}
return added;
}
/**
* Scans the specified range of keys, in ascending order by key.
* Note: the callback `onFound` must not insert or remove items in the
* collection. Doing so may cause incorrect data to be sent to the
* callback afterward.
* @param low The first key scanned will be greater than or equal to `low`.
* @param high Scanning stops when a key larger than this is reached.
* @param includeHigh If the `high` key is present, `onFound` is called for
* that final pair if and only if this parameter is true.
* @param onFound A function that is called for each key-value pair. This
* function can return {break:R} to stop early with result R.
* @param initialCounter Initial third argument of onFound. This value
* increases by one each time `onFound` is called. Default: 0
* @returns The number of values found, or R if the callback returned
* `{break:R}` to stop early.
* @description Computational complexity: O(number of items scanned + log size)
*/
forRange(low, high, includeHigh, onFound, initialCounter) {
const r = this._root.forRange(
low,
high,
includeHigh,
false,
this,
initialCounter || 0,
onFound
);
return typeof r === `number` ? r : r.break;
}
/**
* Scans and potentially modifies values for a subsequence of keys.
* Note: the callback `onFound` should ideally be a pure function.
* Specfically, it must not insert items, call clone(), or change
* the collection except via return value; out-of-band editing may
* cause an exception or may cause incorrect data to be sent to
* the callback (duplicate or missed items). It must not cause a
* clone() of the collection, otherwise the clone could be modified
* by changes requested by the callback.
* @param low The first key scanned will be greater than or equal to `low`.
* @param high Scanning stops when a key larger than this is reached.
* @param includeHigh If the `high` key is present, `onFound` is called for
* that final pair if and only if this parameter is true.
* @param onFound A function that is called for each key-value pair. This
* function can return `{value:v}` to change the value associated
* with the current key, `{delete:true}` to delete the current pair,
* `{break:R}` to stop early with result R, or it can return nothing
* (undefined or {}) to cause no effect and continue iterating.
* `{break:R}` can be combined with one of the other two commands.
* The third argument `counter` is the number of items iterated
* previously; it equals 0 when `onFound` is called the first time.
* @returns The number of values scanned, or R if the callback returned
* `{break:R}` to stop early.
* @description
* Computational complexity: O(number of items scanned + log size)
* Note: if the tree has been cloned with clone(), any shared
* nodes are copied before `onFound` is called. This takes O(n) time
* where n is proportional to the amount of shared data scanned.
*/
editRange(low, high, includeHigh, onFound, initialCounter) {
let root = this._root;
if (root.isShared) this._root = root = root.clone();
try {
const r = root.forRange(
low,
high,
includeHigh,
true,
this,
initialCounter || 0,
onFound
);
return typeof r === `number` ? r : r.break;
} finally {
let isShared;
while (root.keys.length <= 1 && !root.isLeaf) {
isShared || (isShared = root.isShared);
this._root = root = root.keys.length === 0 ? EmptyLeaf : root.children[0];
}
if (isShared) {
root.isShared = true;
}
}
}
}
class BNode {
get isLeaf() {
return this.children === void 0;
}
constructor(keys = [], values) {
this.keys = keys;
this.values = values || undefVals;
this.isShared = void 0;
}
// /////////////////////////////////////////////////////////////////////////
// Shared methods /////////////////////////////////////////////////////////
maxKey() {
return this.keys[this.keys.length - 1];
}
// If key not found, returns i^failXor where i is the insertion index.
// Callers that don't care whether there was a match will set failXor=0.
indexOf(key, failXor, cmp) {
const keys = this.keys;
let lo = 0, hi = keys.length, mid = hi >> 1;
while (lo < hi) {
const c = cmp(keys[mid], key);
if (c < 0) lo = mid + 1;
else if (c > 0)
hi = mid;
else if (c === 0) return mid;
else {
if (key === key)
return keys.length;
else throw new Error(`BTree: NaN was used as a key`);
}
mid = lo + hi >> 1;
}
return mid ^ failXor;
}
// ///////////////////////////////////////////////////////////////////////////
// Leaf Node: misc //////////////////////////////////////////////////////////
minKey() {
return this.keys[0];
}
minPair(reusedArray) {
if (this.keys.length === 0) return void 0;
reusedArray[0] = this.keys[0];
reusedArray[1] = this.values[0];
return reusedArray;
}
maxPair(reusedArray) {
if (this.keys.length === 0) return void 0;
const lastIndex = this.keys.length - 1;
reusedArray[0] = this.keys[lastIndex];
reusedArray[1] = this.values[lastIndex];
return reusedArray;
}
clone() {
const v = this.values;
return new BNode(this.keys.slice(0), v === undefVals ? v : v.slice(0));
}
get(key, defaultValue, tree) {
const i = this.indexOf(key, -1, tree._compare);
return i < 0 ? defaultValue : this.values[i];
}
getPairOrNextLower(key, compare, inclusive, reusedArray) {
const i = this.indexOf(key, -1, compare);
const indexOrLower = i < 0 ? ~i - 1 : inclusive ? i : i - 1;
if (indexOrLower >= 0) {
reusedArray[0] = this.keys[indexOrLower];
reusedArray[1] = this.values[indexOrLower];
return reusedArray;
}
return void 0;
}
getPairOrNextHigher(key, compare, inclusive, reusedArray) {
const i = this.indexOf(key, -1, compare);
const indexOrLower = i < 0 ? ~i : inclusive ? i : i + 1;
const keys = this.keys;
if (indexOrLower < keys.length) {
reusedArray[0] = keys[indexOrLower];
reusedArray[1] = this.values[indexOrLower];
return reusedArray;
}
return void 0;
}
// ///////////////////////////////////////////////////////////////////////////
// Leaf Node: set & node splitting //////////////////////////////////////////
set(key, value, overwrite, tree) {
let i = this.indexOf(key, -1, tree._compare);
if (i < 0) {
i = ~i;
tree._size++;
if (this.keys.length < tree._maxNodeSize) {
return this.insertInLeaf(i, key, value, tree);
} else {
const newRightSibling = this.splitOffRightSide();
let target = this;
if (i > this.keys.length) {
i -= this.keys.length;
target = newRightSibling;
}
target.insertInLeaf(i, key, value, tree);
return newRightSibling;
}
} else {
if (overwrite !== false) {
if (value !== void 0) this.reifyValues();
this.keys[i] = key;
this.values[i] = value;
}
return false;
}
}
reifyValues() {
if (this.values === undefVals)
return this.values = this.values.slice(0, this.keys.length);
return this.values;
}
insertInLeaf(i, key, value, tree) {
this.keys.splice(i, 0, key);
if (this.values === undefVals) {
while (undefVals.length < tree._maxNodeSize) undefVals.push(void 0);
if (value === void 0) {
return true;
} else {
this.values = undefVals.slice(0, this.keys.length - 1);
}
}
this.values.splice(i, 0, value);
return true;
}
takeFromRight(rhs) {
let v = this.values;
if (rhs.values === undefVals) {
if (v !== undefVals) v.push(void 0);
} else {
v = this.reifyValues();
v.push(rhs.values.shift());
}
this.keys.push(rhs.keys.shift());
}
takeFromLeft(lhs) {
let v = this.values;
if (lhs.values === undefVals) {
if (v !== undefVals) v.unshift(void 0);
} else {
v = this.reifyValues();
v.unshift(lhs.values.pop());
}
this.keys.unshift(lhs.keys.pop());
}
splitOffRightSide() {
const half = this.keys.length >> 1, keys = this.keys.splice(half);
const values = this.values === undefVals ? undefVals : this.values.splice(half);
return new BNode(keys, values);
}
// ///////////////////////////////////////////////////////////////////////////
// Leaf Node: scanning & deletions //////////////////////////////////////////
forRange(low, high, includeHigh, editMode, tree, count, onFound) {
const cmp = tree._compare;
let iLow, iHigh;
if (high === low) {
if (!includeHigh) return count;
iHigh = (iLow = this.indexOf(low, -1, cmp)) + 1;
if (iLow < 0) return count;
} else {
iLow = this.indexOf(low, 0, cmp);
iHigh = this.indexOf(high, -1, cmp);
if (iHigh < 0) iHigh = ~iHigh;
else if (includeHigh === true) iHigh++;
}
const keys = this.keys, values = this.values;
if (onFound !== void 0) {
for (let i = iLow; i < iHigh; i++) {
const key = keys[i];
const result = onFound(key, values[i], count++);
if (result !== void 0) {
if (editMode === true) {
if (key !== keys[i] || this.isShared === true)
throw new Error(`BTree illegally changed or cloned in editRange`);
if (result.delete) {
this.keys.splice(i, 1);
if (this.values !== undefVals) this.values.splice(i, 1);
tree._size--;
i--;
iHigh--;
} else if (result.hasOwnProperty(`value`)) {
values[i] = result.value;
}
}
if (result.break !== void 0) return result;
}
}
} else count += iHigh - iLow;
return count;
}
/** Adds entire contents of right-hand sibling (rhs is left unchanged) */
mergeSibling(rhs, _) {
this.keys.push.apply(this.keys, rhs.keys);
if (this.values === undefVals) {
if (rhs.values === undefVals) return;
this.values = this.values.slice(0, this.keys.length);
}
this.values.push.apply(this.values, rhs.reifyValues());
}
}
class BNodeInternal extends BNode {
/**
* This does not mark `children` as shared, so it is the responsibility of the caller
* to ensure children are either marked shared, or aren't included in another tree.
*/
constructor(children, keys) {
if (!keys) {
keys = [];
for (let i = 0; i < children.length; i++) keys[i] = children[i].maxKey();
}
super(keys);
this.children = children;
}
minKey() {
return this.children[0].minKey();
}
minPair(reusedArray) {
return this.children[0].minPair(reusedArray);
}
maxPair(reusedArray) {
return this.children[this.children.length - 1].maxPair(reusedArray);
}
get(key, defaultValue, tree) {
const i = this.indexOf(key, 0, tree._compare), children = this.children;
return i < children.length ? children[i].get(key, defaultValue, tree) : void 0;
}
getPairOrNextLower(key, compare, inclusive, reusedArray) {
const i = this.indexOf(key, 0, compare), children = this.children;
if (i >= children.length) return this.maxPair(reusedArray);
const result = children[i].getPairOrNextLower(
key,
compare,
inclusive,
reusedArray
);
if (result === void 0 && i > 0) {
return children[i - 1].maxPair(reusedArray);
}
return result;
}
getPairOrNextHigher(key, compare, inclusive, reusedArray) {
const i = this.indexOf(key, 0, compare), children = this.children, length = children.length;
if (i >= length) return void 0;
const result = children[i].getPairOrNextHigher(
key,
compare,
inclusive,
reusedArray
);
if (result === void 0 && i < length - 1) {
return children[i + 1].minPair(reusedArray);
}
return result;
}
// ///////////////////////////////////////////////////////////////////////////
// Internal Node: set & node splitting //////////////////////////////////////
set(key, value, overwrite, tree) {
const c = this.children, max = tree._maxNodeSize, cmp = tree._compare;
let i = Math.min(this.indexOf(key, 0, cmp), c.length - 1), child = c[i];
if (child.isShared) c[i] = child = child.clone();
if (child.keys.length >= max) {
let other;
if (i > 0 && (other = c[i - 1]).keys.length < max && cmp(child.keys[0], key) < 0) {
if (other.isShared) c[i - 1] = other = other.clone();
other.takeFromRight(child);
this.keys[i - 1] = other.maxKey();
} else if ((other = c[i + 1]) !== void 0 && other.keys.length < max && cmp(child.maxKey(), key) < 0) {
if (other.isShared) c[i + 1] = other = other.clone();
other.takeFromLeft(child);
this.keys[i] = c[i].maxKey();
}
}
const result = child.set(key, value, overwrite, tree);
if (result === false) return false;
this.keys[i] = child.maxKey();
if (result === true) return true;
if (this.keys.length < max) {
this.insert(i + 1, result);
return true;
} else {
const newRightSibling = this.splitOffRightSide();
let target = this;
if (cmp(result.maxKey(), this.maxKey()) > 0) {
target = newRightSibling;
i -= this.keys.length;
}
target.insert(i + 1, result);
return newRightSibling;
}
}
/**
* Inserts `child` at index `i`.
* This does not mark `child` as shared, so it is the responsibility of the caller
* to ensure that either child is marked shared, or it is not included in another tree.
*/
insert(i, child) {
this.children.splice(i, 0, child);
this.keys.splice(i, 0, child.maxKey());
}
/**
* Split this node.
* Modifies this to remove the second half of the items, returning a separate node containing them.
*/
splitOffRightSide() {
const half = this.children.length >> 1;
return new BNodeInternal(
this.children.splice(half),
this.keys.splice(half)
);
}
takeFromRight(rhs) {
this.keys.push(rhs.keys.shift());
this.children.push(rhs.children.shift());
}
takeFromLeft(lhs) {
this.keys.unshift(lhs.keys.pop());
this.children.unshift(lhs.children.pop());
}
// ///////////////////////////////////////////////////////////////////////////
// Internal Node: scanning & deletions //////////////////////////////////////
// Note: `count` is the next value of the third argument to `onFound`.
// A leaf node's `forRange` function returns a new value for this counter,
// unless the operation is to stop early.
forRange(low, high, includeHigh, editMode, tree, count, onFound) {
const cmp = tree._compare;
const keys = this.keys, children = this.children;
let iLow = this.indexOf(low, 0, cmp), i = iLow;
const iHigh = Math.min(
high === low ? iLow : this.indexOf(high, 0, cmp),
keys.length - 1
);
if (!editMode) {
for (; i <= iHigh; i++) {
const result = children[i].forRange(
low,
high,
includeHigh,
editMode,
tree,
count,
onFound
);
if (typeof result !== `number`) return result;
count = result;
}
} else if (i <= iHigh) {
try {
for (; i <= iHigh; i++) {
if (children[i].isShared) children[i] = children[i].clone();
const result = children[i].forRange(
low,
high,
includeHigh,
editMode,
tree,
count,
onFound
);
keys[i] = children[i].maxKey();
if (typeof result !== `number`) return result;
count = result;
}
} finally {
const half = tree._maxNodeSize >> 1;
if (iLow > 0) iLow--;
for (i = iHigh; i >= iLow; i--) {
if (children[i].keys.length <= half) {
if (children[i].keys.length !== 0) {
this.tryMerge(i, tree._maxNodeSize);
} else {
keys.splice(i, 1);
children.splice(i, 1);
}
}
}
if (children.length !== 0 && children[0].keys.length === 0)
check(false, `emptiness bug`);
}
}
return count;
}
/** Merges child i with child i+1 if their combined size is not too large */
tryMerge(i, maxSize) {
const children = this.children;
if (i >= 0 && i + 1 < children.length) {
if (children[i].keys.length + children[i + 1].keys.length <= maxSize) {
if (children[i].isShared)
children[i] = children[i].clone();
children[i].mergeSibling(children[i + 1], maxSize);
children.splice(i + 1, 1);
this.keys.splice(i + 1, 1);
this.keys[i] = children[i].maxKey();
return true;
}
}
return false;
}
/**
* Move children from `rhs` into this.
* `rhs` must be part of this tree, and be removed from it after this call
* (otherwise isShared for its children could be incorrect).
*/
mergeSibling(rhs, maxNodeSize) {
const oldLength = this.keys.length;
this.keys.push.apply(this.keys, rhs.keys);
const rhsChildren = rhs.children;
this.children.push.apply(this.children, rhsChildren);
if (rhs.isShared && !this.isShared) {
for (const child of rhsChildren) child.isShared = true;
}
this.tryMerge(oldLength - 1, maxNodeSize);
}
}
const undefVals = [];
const Delete = { delete: true }, DeleteRange = () => Delete;
const EmptyLeaf = function() {
const n = new BNode();
n.isShared = true;
return n;
}();
function check(fact, ...args) {
{
args.unshift(`B+ tree`);
throw new Error(args.join(` `));
}
}
exports.BTree = BTree;
//# sourceMappingURL=btree.cjs.map