UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

1,011 lines (936 loc) 36.5 kB
// This file was copied from https://github.com/qwertie/btree-typescript/tree/master and adapted to our needs. // We removed methods that we don't need. // B+ tree by David Piepgrass. License: MIT type EditRangeResult<V, R = number> = { value?: V break?: R delete?: boolean } type index = number // Informative microbenchmarks & stuff: // http://www.jayconrod.com/posts/52/a-tour-of-v8-object-representation (very educational) // https://blog.mozilla.org/luke/2012/10/02/optimizing-javascript-variable-access/ (local vars are faster than properties) // http://benediktmeurer.de/2017/12/13/an-introduction-to-speculative-optimization-in-v8/ (other stuff) // https://jsperf.com/js-in-operator-vs-alternatives (avoid 'in' operator; `.p!==undefined` faster than `hasOwnProperty('p')` in all browsers) // https://jsperf.com/instanceof-vs-typeof-vs-constructor-vs-member (speed of type tests varies wildly across browsers) // https://jsperf.com/detecting-arrays-new (a.constructor===Array is best across browsers, assuming a is an object) // https://jsperf.com/shallow-cloning-methods (a constructor is faster than Object.create; hand-written clone faster than Object.assign) // https://jsperf.com/ways-to-fill-an-array (slice-and-replace is fastest) // https://jsperf.com/math-min-max-vs-ternary-vs-if (Math.min/max is slow on Edge) // https://jsperf.com/array-vs-property-access-speed (v.x/v.y is faster than a[0]/a[1] in major browsers IF hidden class is constant) // https://jsperf.com/detect-not-null-or-undefined (`x==null` slightly slower than `x===null||x===undefined` on all browsers) // Overall, microbenchmarks suggest Firefox is the fastest browser for JavaScript and Edge is the slowest. // Lessons from https://v8project.blogspot.com/2017/09/elements-kinds-in-v8.html: // - Avoid holes in arrays. Avoid `new Array(N)`, it will be "holey" permanently. // - Don't read outside bounds of an array (it scans prototype chain). // - Small integer arrays are stored differently from doubles // - Adding non-numbers to an array deoptimizes it permanently into a general array // - Objects can be used like arrays (e.g. have length property) but are slower // - V8 source (NewElementsCapacity in src/objects.h): arrays grow by 50% + 16 elements /** * A reasonably fast collection of key-value pairs with a powerful API. * Largely compatible with the standard Map. BTree is a B+ tree data structure, * so the collection is sorted by key. * * B+ trees tend to use memory more efficiently than hashtables such as the * standard Map, especially when the collection contains a large number of * items. However, maintaining the sort order makes them modestly slower: * O(log size) rather than O(1). This B+ tree implementation supports O(1) * fast cloning. It also supports freeze(), which can be used to ensure that * a BTree is not changed accidentally. * * Confusingly, the ES6 Map.forEach(c) method calls c(value,key) instead of * c(key,value), in contrast to other methods such as set() and entries() * which put the key first. I can only assume that the order was reversed on * the theory that users would usually want to examine values and ignore keys. * BTree's forEach() therefore works the same way, but a second method * `.forEachPair((key,value)=>{...})` is provided which sends you the key * first and the value second; this method is slightly faster because it is * the "native" for-each method for this class. * * Out of the box, BTree supports keys that are numbers, strings, arrays of * numbers/strings, Date, and objects that have a valueOf() method returning a * number or string. Other data types, such as arrays of Date or custom * objects, require a custom comparator, which you must pass as the second * argument to the constructor (the first argument is an optional list of * initial items). Symbols cannot be used as keys because they are unordered * (one Symbol is never "greater" or "less" than another). * * @example * Given a {name: string, age: number} object, you can create a tree sorted by * name and then by age like this: * * var tree = new BTree(undefined, (a, b) => { * if (a.name > b.name) * return 1; // Return a number >0 when a > b * else if (a.name < b.name) * return -1; // Return a number <0 when a < b * else // names are equal (or incomparable) * return a.age - b.age; // Return >0 when a.age > b.age * }); * * tree.set({name:"Bill", age:17}, "happy"); * tree.set({name:"Fran", age:40}, "busy & stressed"); * tree.set({name:"Bill", age:55}, "recently laid off"); * tree.forEachPair((k, v) => { * console.log(`Name: ${k.name} Age: ${k.age} Status: ${v}`); * }); * * @description * The "range" methods (`forEach, forRange, editRange`) will return the number * of elements that were scanned. In addition, the callback can return {break:R} * to stop early and return R from the outer function. * * - TODO: Test performance of preallocating values array at max size * - TODO: Add fast initialization when a sorted array is provided to constructor * * For more documentation see https://github.com/qwertie/btree-typescript * * Are you a C# developer? You might like the similar data structures I made for C#: * BDictionary, BList, etc. See http://core.loyc.net/collections/ * * @author David Piepgrass */ export class BTree<K = any, V = any> { private _root: BNode<K, V> = EmptyLeaf as BNode<K, V> _size = 0 _maxNodeSize: number /** * provides a total order over keys (and a strict partial order over the type K) * @returns a negative value if a < b, 0 if a === b and a positive value if a > b */ _compare: (a: K, b: K) => number /** * 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. */ public constructor( compare: (a: K, b: K) => number, entries?: Array<[K, V]>, maxNodeSize?: number ) { 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 as BNode<K, V> 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: K, defaultValue?: V): V | undefined { 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: K, value: V, overwrite?: boolean): boolean { 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 // Root node has split, so create a new root node. this._root = new BNodeInternal<K, V>([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: K): boolean { return this.forRange(key, key, true, undefined) !== 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: K): boolean { 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(): K | undefined { return this._root.minKey() } /** Gets the highest key in the tree. Complexity: O(1) */ maxKey(): K | undefined { return this._root.maxKey() } /** Gets an array of all keys, sorted */ keysArray() { const results: Array<K> = [] 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: K | undefined, reusedArray?: [K, V]): [K, V] | undefined { reusedArray = reusedArray || ([] as unknown as [K, V]) if (key === undefined) { 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: K | undefined, reusedArray?: [K, V]): [K, V] | undefined { reusedArray = reusedArray || ([] as unknown as [K, V]) if (key === undefined) { 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: Array<[K, V]>, overwrite?: boolean): number { let added = 0 for (const pair of pairs) { if (this.set(pair[0], pair[1], overwrite)) added++ } return added } forRange( low: K, high: K, includeHigh: boolean, onFound?: (k: K, v: V, counter: number) => void, initialCounter?: number ): number /** * 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<R = number>( low: K, high: K, includeHigh: boolean, onFound?: (k: K, v: V, counter: number) => { break?: R } | void, initialCounter?: number ): R | number { 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<R = V>( low: K, high: K, includeHigh: boolean, onFound: (k: K, v: V, counter: number) => EditRangeResult<V, R> | void, initialCounter?: number ): R | number { 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 as any as BNodeInternal<K, V>).children[0]! } // If any ancestor of the new root was shared, the new root must also be shared if (isShared) { root.isShared = true } } } } /** Leaf node / base class. **************************************************/ class BNode<K, V> { // If this is an internal node, _keys[i] is the highest key in children[i]. keys: Array<K> values: Array<V> // True if this node might be within multiple `BTree`s (or have multiple parents). // If so, it must be cloned before being mutated to avoid changing an unrelated tree. // This is transitive: if it's true, children are also shared even if `isShared!=true` // in those children. (Certain operations will propagate isShared=true to children.) isShared: true | undefined get isLeaf() { return (this as any).children === undefined } constructor(keys: Array<K> = [], values?: Array<V>) { this.keys = keys this.values = values || undefVals this.isShared = undefined } // ///////////////////////////////////////////////////////////////////////// // 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: K, failXor: number, cmp: (a: K, b: K) => number): index { 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) // key < keys[mid] hi = mid else if (c === 0) return mid else { // c is NaN or otherwise invalid if (key === key) // at least the search key is not NaN 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(): K | undefined { return this.keys[0] } minPair(reusedArray: [K, V]): [K, V] | undefined { if (this.keys.length === 0) return undefined reusedArray[0] = this.keys[0]! reusedArray[1] = this.values[0]! return reusedArray } maxPair(reusedArray: [K, V]): [K, V] | undefined { if (this.keys.length === 0) return undefined const lastIndex = this.keys.length - 1 reusedArray[0] = this.keys[lastIndex]! reusedArray[1] = this.values[lastIndex]! return reusedArray } clone(): BNode<K, V> { const v = this.values return new BNode<K, V>(this.keys.slice(0), v === undefVals ? v : v.slice(0)) } get(key: K, defaultValue: V | undefined, tree: BTree<K, V>): V | undefined { const i = this.indexOf(key, -1, tree._compare) return i < 0 ? defaultValue : this.values[i] } getPairOrNextLower( key: K, compare: (a: K, b: K) => number, inclusive: boolean, reusedArray: [K, V] ): [K, V] | undefined { 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 undefined } getPairOrNextHigher( key: K, compare: (a: K, b: K) => number, inclusive: boolean, reusedArray: [K, V] ): [K, V] | undefined { 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 undefined } // /////////////////////////////////////////////////////////////////////////// // Leaf Node: set & node splitting ////////////////////////////////////////// set( key: K, value: V, overwrite: boolean | undefined, tree: BTree<K, V> ): boolean | BNode<K, V> { let i = this.indexOf(key, -1, tree._compare) if (i < 0) { // key does not exist yet i = ~i tree._size++ if (this.keys.length < tree._maxNodeSize) { return this.insertInLeaf(i, key, value, tree) } else { // This leaf node is full and must split const newRightSibling = this.splitOffRightSide() let target: BNode<K, V> = this if (i > this.keys.length) { i -= this.keys.length target = newRightSibling } target.insertInLeaf(i, key, value, tree) return newRightSibling } } else { // Key already exists if (overwrite !== false) { if (value !== undefined) this.reifyValues() // usually this is a no-op, but some users may wish to edit the key 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: index, key: K, value: V, tree: BTree<K, V>) { this.keys.splice(i, 0, key) if (this.values === undefVals) { while (undefVals.length < tree._maxNodeSize) undefVals.push(undefined) if (value === undefined) { return true } else { this.values = undefVals.slice(0, this.keys.length - 1) } } this.values.splice(i, 0, value) return true } takeFromRight(rhs: BNode<K, V>) { // Reminder: parent node must update its copy of key for this node // assert: neither node is shared // assert rhs.keys.length > (maxNodeSize/2 && this.keys.length<maxNodeSize) let v = this.values if (rhs.values === undefVals) { if (v !== undefVals) v.push(undefined as any) } else { v = this.reifyValues() v.push(rhs.values.shift()!) } this.keys.push(rhs.keys.shift()!) } takeFromLeft(lhs: BNode<K, V>) { // Reminder: parent node must update its copy of key for this node // assert: neither node is shared // assert rhs.keys.length > (maxNodeSize/2 && this.keys.length<maxNodeSize) let v = this.values if (lhs.values === undefVals) { if (v !== undefVals) v.unshift(undefined as any) } else { v = this.reifyValues() v.unshift(lhs.values.pop()!) } this.keys.unshift(lhs.keys.pop()!) } splitOffRightSide(): BNode<K, V> { // Reminder: parent node must update its copy of key for this node const half = this.keys.length >> 1, keys = this.keys.splice(half) const values = this.values === undefVals ? undefVals : this.values.splice(half) return new BNode<K, V>(keys, values) } // /////////////////////////////////////////////////////////////////////////// // Leaf Node: scanning & deletions ////////////////////////////////////////// forRange<R>( low: K, high: K, includeHigh: boolean | undefined, editMode: boolean, tree: BTree<K, V>, count: number, onFound?: (k: K, v: V, counter: number) => EditRangeResult<V, R> | void ): EditRangeResult<V, R> | number { 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 !== undefined) { for (let i = iLow; i < iHigh; i++) { const key = keys[i]! const result = onFound(key, values[i]!, count++) if (result !== undefined) { 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 !== undefined) return result } } } else count += iHigh - iLow return count } /** Adds entire contents of right-hand sibling (rhs is left unchanged) */ mergeSibling(rhs: BNode<K, V>, _: number) { 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()) } } /** Internal node (non-leaf node) ********************************************/ class BNodeInternal<K, V> extends BNode<K, V> { // Note: conventionally B+ trees have one fewer key than the number of // children, but I find it easier to keep the array lengths equal: each // keys[i] caches the value of children[i].maxKey(). children: Array<BNode<K, V>> /** * 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: Array<BNode<K, V>>, keys?: Array<K>) { 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: [K, V]): [K, V] | undefined { return this.children[0]!.minPair(reusedArray) } maxPair(reusedArray: [K, V]): [K, V] | undefined { return this.children[this.children.length - 1]!.maxPair(reusedArray) } get(key: K, defaultValue: V | undefined, tree: BTree<K, V>): V | undefined { const i = this.indexOf(key, 0, tree._compare), children = this.children return i < children.length ? children[i]!.get(key, defaultValue, tree) : undefined } getPairOrNextLower( key: K, compare: (a: K, b: K) => number, inclusive: boolean, reusedArray: [K, V] ): [K, V] | undefined { 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 === undefined && i > 0) { return children[i - 1]!.maxPair(reusedArray) } return result } getPairOrNextHigher( key: K, compare: (a: K, b: K) => number, inclusive: boolean, reusedArray: [K, V] ): [K, V] | undefined { const i = this.indexOf(key, 0, compare), children = this.children, length = children.length if (i >= length) return undefined const result = children[i]!.getPairOrNextHigher( key, compare, inclusive, reusedArray ) if (result === undefined && i < length - 1) { return children[i + 1]!.minPair(reusedArray) } return result } // /////////////////////////////////////////////////////////////////////////// // Internal Node: set & node splitting ////////////////////////////////////// set( key: K, value: V, overwrite: boolean | undefined, tree: BTree<K, V> ): boolean | BNodeInternal<K, V> { 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) { // child is full; inserting anything else will cause a split. // Shifting an item to the left or right sibling may avoid a split. // We can do a shift if the adjacent node is not full and if the // current key can still be placed in the same node after the shift. let other: BNode<K, V> | undefined 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]) !== undefined && 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 // The child has split and `result` is a new right child... does it fit? if (this.keys.length < max) { // yes this.insert(i + 1, result) return true } else { // no, we must split also const newRightSibling = this.splitOffRightSide() let target: BNodeInternal<K, V> = 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: index, child: BNode<K, V>) { 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() { // assert !this.isShared; const half = this.children.length >> 1 return new BNodeInternal<K, V>( this.children.splice(half), this.keys.splice(half) ) } takeFromRight(rhs: BNode<K, V>) { // Reminder: parent node must update its copy of key for this node // assert: neither node is shared // assert rhs.keys.length > (maxNodeSize/2 && this.keys.length<maxNodeSize) this.keys.push(rhs.keys.shift()!) this.children.push((rhs as BNodeInternal<K, V>).children.shift()!) } takeFromLeft(lhs: BNode<K, V>) { // Reminder: parent node must update its copy of key for this node // assert: neither node is shared // assert rhs.keys.length > (maxNodeSize/2 && this.keys.length<maxNodeSize) this.keys.unshift(lhs.keys.pop()!) this.children.unshift((lhs as BNodeInternal<K, V>).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<R>( low: K, high: K, includeHigh: boolean | undefined, editMode: boolean, tree: BTree<K, V>, count: number, onFound?: (k: K, v: V, counter: number) => EditRangeResult<V, R> | void ): EditRangeResult<V, R> | number { 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) { // Simple case 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 ) // Note: if children[i] is empty then keys[i]=undefined. // This is an invalid state, but it is fixed below. keys[i] = children[i]!.maxKey()! if (typeof result !== `number`) return result count = result } } finally { // Deletions may have occurred, so look for opportunities to merge nodes. 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 { // child is empty! delete it! 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: index, maxSize: number): boolean { 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) // cloned already UNLESS i is outside scan range 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: BNode<K, V>, maxNodeSize: number) { // assert !this.isShared; const oldLength = this.keys.length this.keys.push.apply(this.keys, rhs.keys) const rhsChildren = (rhs as any as BNodeInternal<K, V>).children this.children.push.apply(this.children, rhsChildren) if (rhs.isShared && !this.isShared) { // All children of a shared node are implicitly shared, and since their new // parent is not shared, they must now be explicitly marked as shared. for (const child of rhsChildren) child.isShared = true } // If our children are themselves almost empty due to a mass-delete, // they may need to be merged too (but only the oldLength-1 and its // right sibling should need this). this.tryMerge(oldLength - 1, maxNodeSize) } } // Optimization: this array of `undefined`s is used instead of a normal // array of values in nodes where `undefined` is the only value. // Its length is extended to max node size on first use; since it can // be shared between trees with different maximums, its length can only // increase, never decrease. Its type should be undefined[] but strangely // TypeScript won't allow the comparison V[] === undefined[]. To prevent // users from making this array too large, BTree has a maximum node size. // // FAQ: undefVals[i] is already undefined, so why increase the array size? // Reading outside the bounds of an array is relatively slow because it // has the side effect of scanning the prototype chain. const undefVals: Array<any> = [] const Delete = { delete: true }, DeleteRange = () => Delete const EmptyLeaf = (function () { const n = new BNode<any, any>() n.isShared = true return n })() function check(fact: boolean, ...args: Array<any>) { if (!fact) { args.unshift(`B+ tree`) // at beginning of message throw new Error(args.join(` `)) } }