UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

693 lines (692 loc) 24.7 kB
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 key larger than the specified key, or undefined if there is none. * Also, nextHigherKey(undefined) returns the lowest key. */ nextHigherKey(key) { const p = this.nextHigherPair(key, ReusedArray); return p && p[0]; } /** 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); } /** Returns the next key smaller than the specified key, or undefined if there is none. * Also, nextLowerKey(undefined) returns the highest key. */ nextLowerKey(key) { const p = this.nextLowerPair(key, ReusedArray); return p && p[0]; } /** 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 ||= 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; })(); const ReusedArray = []; function check(fact, ...args) { { args.unshift(`B+ tree`); throw new Error(args.join(` `)); } } export { BTree }; //# sourceMappingURL=btree.js.map