@pixagram/lacerta-db 0.13.2 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.min.js +2 -2
- package/dist/index.min.js +1 -1
- package/index.js +77 -57
- package/package.json +4 -2
- package/readme.md +3 -5
package/index.js
CHANGED
|
@@ -1448,18 +1448,27 @@ class BTreeNode {
|
|
|
1448
1448
|
let i = this.n - 1;
|
|
1449
1449
|
|
|
1450
1450
|
if (this.leaf) {
|
|
1451
|
+
// Search first, shift only if the key is truly new.
|
|
1452
|
+
// The old code shifted while scanning, which corrupted the array
|
|
1453
|
+
// when the key already existed at a lower index: entries between
|
|
1454
|
+
// the key's position and n-1 were duplicated rightward, leaving
|
|
1455
|
+
// stale copies inside the valid range.
|
|
1451
1456
|
while (i >= 0 && _btreeCmp(this.keys[i], key) > 0) {
|
|
1452
|
-
this.keys[i + 1] = this.keys[i];
|
|
1453
|
-
this.values[i + 1] = this.values[i];
|
|
1454
1457
|
i--;
|
|
1455
1458
|
}
|
|
1456
1459
|
|
|
1457
1460
|
if (i >= 0 && _btreeCmp(this.keys[i], key) === 0) {
|
|
1461
|
+
// Key exists — merge value into existing Set (no shift needed)
|
|
1458
1462
|
if (!this.values[i]) {
|
|
1459
1463
|
this.values[i] = new Set();
|
|
1460
1464
|
}
|
|
1461
1465
|
this.values[i].add(value);
|
|
1462
1466
|
} else {
|
|
1467
|
+
// Key is new — shift entries right to open a slot at i+1
|
|
1468
|
+
for (let j = this.n - 1; j > i; j--) {
|
|
1469
|
+
this.keys[j + 1] = this.keys[j];
|
|
1470
|
+
this.values[j + 1] = this.values[j];
|
|
1471
|
+
}
|
|
1463
1472
|
this.keys[i + 1] = key;
|
|
1464
1473
|
this.values[i + 1] = new Set([value]);
|
|
1465
1474
|
this.n++;
|
|
@@ -1764,7 +1773,7 @@ class BTreeNode {
|
|
|
1764
1773
|
return this._remove(key, null, true);
|
|
1765
1774
|
}
|
|
1766
1775
|
|
|
1767
|
-
verify() {
|
|
1776
|
+
verify(isRoot = true) {
|
|
1768
1777
|
const issues = [];
|
|
1769
1778
|
for (let i = 0; i < this.n; i++) {
|
|
1770
1779
|
if (this.keys[i] === undefined || this.keys[i] === null) {
|
|
@@ -1778,8 +1787,10 @@ class BTreeNode {
|
|
|
1778
1787
|
}
|
|
1779
1788
|
if (!this.leaf) {
|
|
1780
1789
|
for (let i = 0; i <= this.n; i++) {
|
|
1781
|
-
if (this.children[i]) {
|
|
1782
|
-
|
|
1790
|
+
if (!this.children[i]) {
|
|
1791
|
+
issues.push(`Missing child at index ${i} (node has ${this.n} keys)`);
|
|
1792
|
+
} else {
|
|
1793
|
+
const childIssues = this.children[i].verify(false);
|
|
1783
1794
|
issues.push(...childIssues);
|
|
1784
1795
|
}
|
|
1785
1796
|
}
|
|
@@ -1932,7 +1943,8 @@ class BTreeIndex {
|
|
|
1932
1943
|
/**
|
|
1933
1944
|
* Restore a BTreeIndex from persisted sorted entries via O(n) bottom-up bulk-load.
|
|
1934
1945
|
* Builds leaf nodes directly from sorted data, then constructs internal levels
|
|
1935
|
-
*
|
|
1946
|
+
* using pre-extracted separators — no individual insert() calls, no comparisons,
|
|
1947
|
+
* and no child mutation (which avoids orphaning subtrees at depth ≥ 3).
|
|
1936
1948
|
* @param {Array} entries - [[key, [docId1, ...]], ...] — MUST be sorted by key
|
|
1937
1949
|
* @param {number} [order=4]
|
|
1938
1950
|
* @returns {BTreeIndex}
|
|
@@ -1943,41 +1955,50 @@ class BTreeIndex {
|
|
|
1943
1955
|
|
|
1944
1956
|
const maxKeys = 2 * order - 1;
|
|
1945
1957
|
|
|
1946
|
-
//
|
|
1947
|
-
let totalSize = 0;
|
|
1948
|
-
for (let i = 0; i < entries.length; i++) {
|
|
1949
|
-
if (entries[i][0] === undefined || entries[i][0] === null) continue;
|
|
1950
|
-
totalSize += entries[i][1].length;
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
// Filter out null/undefined keys
|
|
1958
|
+
// Filter out null/undefined keys and count total values for _size
|
|
1954
1959
|
const clean = [];
|
|
1960
|
+
let totalSize = 0;
|
|
1955
1961
|
for (let i = 0; i < entries.length; i++) {
|
|
1956
1962
|
if (entries[i][0] !== undefined && entries[i][0] !== null) {
|
|
1957
1963
|
clean.push(entries[i]);
|
|
1964
|
+
totalSize += entries[i][1].length;
|
|
1958
1965
|
}
|
|
1959
1966
|
}
|
|
1960
1967
|
|
|
1961
1968
|
if (clean.length === 0) return tree;
|
|
1962
1969
|
|
|
1963
|
-
//
|
|
1970
|
+
// ---------------------------------------------------------------
|
|
1971
|
+
// Step 1: Build leaf nodes AND pre-extract inter-leaf separators.
|
|
1972
|
+
//
|
|
1973
|
+
// In a B-tree (not B+) some keys live at internal nodes. We decide
|
|
1974
|
+
// up-front which entries become leaf data and which become separator
|
|
1975
|
+
// keys for parent nodes. This avoids the old promote-and-shift
|
|
1976
|
+
// approach which orphaned subtrees when children were non-leaf.
|
|
1977
|
+
//
|
|
1978
|
+
// Layout: L0 S0 L1 S1 ... S(n-2) L(n-1)
|
|
1979
|
+
// ↑leaf ↑sep ↑leaf ↑last leaf (no trailing sep)
|
|
1980
|
+
// ---------------------------------------------------------------
|
|
1981
|
+
|
|
1982
|
+
// How many leaves do we need?
|
|
1983
|
+
// N entries = numLeaves * leafEntries + (numLeaves - 1) separators
|
|
1984
|
+
// N = numLeaves * fill + numLeaves - 1 = numLeaves * (fill + 1) - 1
|
|
1985
|
+
// numLeaves = ceil((N + 1) / (maxKeys + 1))
|
|
1986
|
+
const numLeaves = clean.length <= maxKeys
|
|
1987
|
+
? 1
|
|
1988
|
+
: Math.ceil((clean.length + 1) / (maxKeys + 1));
|
|
1989
|
+
|
|
1990
|
+
// Distribute entries among leaves as evenly as possible
|
|
1991
|
+
const totalLeafEntries = clean.length - (numLeaves - 1); // subtract separator slots
|
|
1992
|
+
const basePerLeaf = Math.floor(totalLeafEntries / numLeaves);
|
|
1993
|
+
const extraLeaves = totalLeafEntries - basePerLeaf * numLeaves;
|
|
1994
|
+
|
|
1964
1995
|
const leaves = [];
|
|
1996
|
+
const separators = [];
|
|
1965
1997
|
let pos = 0;
|
|
1966
|
-
|
|
1998
|
+
|
|
1999
|
+
for (let li = 0; li < numLeaves; li++) {
|
|
2000
|
+
const count = basePerLeaf + (li < extraLeaves ? 1 : 0);
|
|
1967
2001
|
const node = new BTreeNode(order, true);
|
|
1968
|
-
const remaining = clean.length - pos;
|
|
1969
|
-
|
|
1970
|
-
// How many entries for this leaf?
|
|
1971
|
-
let count;
|
|
1972
|
-
if (remaining <= maxKeys) {
|
|
1973
|
-
// Last chunk — take everything
|
|
1974
|
-
count = remaining;
|
|
1975
|
-
} else if (remaining < maxKeys + order) {
|
|
1976
|
-
// Would leave a tiny next leaf — split evenly
|
|
1977
|
-
count = Math.ceil(remaining / 2);
|
|
1978
|
-
} else {
|
|
1979
|
-
count = maxKeys;
|
|
1980
|
-
}
|
|
1981
2002
|
|
|
1982
2003
|
for (let j = 0; j < count; j++) {
|
|
1983
2004
|
const [key, values] = clean[pos++];
|
|
@@ -1986,50 +2007,49 @@ class BTreeIndex {
|
|
|
1986
2007
|
node.n++;
|
|
1987
2008
|
}
|
|
1988
2009
|
leaves.push(node);
|
|
2010
|
+
|
|
2011
|
+
// Extract separator between this leaf and the next (not after the last)
|
|
2012
|
+
if (li < numLeaves - 1) {
|
|
2013
|
+
separators.push(clean[pos++]);
|
|
2014
|
+
}
|
|
1989
2015
|
}
|
|
1990
2016
|
|
|
1991
|
-
//
|
|
2017
|
+
// ---------------------------------------------------------------
|
|
2018
|
+
// Step 2: Build internal levels bottom-up using pre-extracted
|
|
2019
|
+
// separators. Children are never mutated, so no subtrees are lost.
|
|
2020
|
+
// ---------------------------------------------------------------
|
|
1992
2021
|
let level = leaves;
|
|
2022
|
+
let seps = separators;
|
|
2023
|
+
|
|
1993
2024
|
while (level.length > 1) {
|
|
1994
2025
|
const parents = [];
|
|
1995
|
-
|
|
2026
|
+
const nextSeps = [];
|
|
2027
|
+
let ci = 0; // child index into level
|
|
2028
|
+
let si = 0; // separator index into seps
|
|
1996
2029
|
|
|
1997
2030
|
while (ci < level.length) {
|
|
1998
2031
|
const parent = new BTreeNode(order, false);
|
|
1999
2032
|
parent.children[0] = level[ci++];
|
|
2000
2033
|
|
|
2001
|
-
//
|
|
2002
|
-
while (parent.n < maxKeys && ci < level.length) {
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
parent.
|
|
2007
|
-
parent.values[parent.n] = child.values[0];
|
|
2008
|
-
|
|
2009
|
-
// Shift child entries left to remove promoted key
|
|
2010
|
-
for (let j = 0; j < child.n - 1; j++) {
|
|
2011
|
-
child.keys[j] = child.keys[j + 1];
|
|
2012
|
-
child.values[j] = child.values[j + 1];
|
|
2013
|
-
}
|
|
2014
|
-
if (!child.leaf) {
|
|
2015
|
-
for (let j = 0; j < child.n; j++) {
|
|
2016
|
-
child.children[j] = child.children[j + 1];
|
|
2017
|
-
}
|
|
2018
|
-
child.children[child.n] = undefined;
|
|
2019
|
-
}
|
|
2020
|
-
child.keys[child.n - 1] = undefined;
|
|
2021
|
-
child.values[child.n - 1] = undefined;
|
|
2022
|
-
child.n--;
|
|
2023
|
-
|
|
2024
|
-
parent.children[parent.n + 1] = child;
|
|
2034
|
+
// Attach children with their pre-extracted separators
|
|
2035
|
+
while (parent.n < maxKeys && si < seps.length && ci < level.length) {
|
|
2036
|
+
const [sepKey, sepValues] = seps[si++];
|
|
2037
|
+
parent.keys[parent.n] = sepKey;
|
|
2038
|
+
parent.values[parent.n] = new Set(sepValues);
|
|
2039
|
+
parent.children[parent.n + 1] = level[ci++];
|
|
2025
2040
|
parent.n++;
|
|
2026
|
-
ci++;
|
|
2027
2041
|
}
|
|
2028
2042
|
|
|
2029
2043
|
parents.push(parent);
|
|
2044
|
+
|
|
2045
|
+
// Extract separator between this parent and the next
|
|
2046
|
+
if (ci < level.length && si < seps.length) {
|
|
2047
|
+
nextSeps.push(seps[si++]);
|
|
2048
|
+
}
|
|
2030
2049
|
}
|
|
2031
2050
|
|
|
2032
2051
|
level = parents;
|
|
2052
|
+
seps = nextSeps;
|
|
2033
2053
|
}
|
|
2034
2054
|
|
|
2035
2055
|
tree._root = level[0];
|
|
@@ -6515,4 +6535,4 @@ export {
|
|
|
6515
6535
|
IndexedDBConnectionPool,
|
|
6516
6536
|
BrowserCompressionUtility,
|
|
6517
6537
|
BrowserEncryptionUtility
|
|
6518
|
-
};
|
|
6538
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixagram/lacerta-db",
|
|
3
|
-
"version": "0.13.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.13.3",
|
|
4
|
+
"description": "LacertaDB is a JS Database for Web Browsers. Up to 10x faster than others. Simple, Fast, Secure.",
|
|
5
5
|
"devDependencies": {
|
|
6
6
|
"@babel/core": "^7.23.6",
|
|
7
7
|
"@babel/plugin-transform-optional-chaining": "^7.28.6",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"author": "Affolter Matias",
|
|
23
23
|
"keywords": [
|
|
24
24
|
"indexeddb",
|
|
25
|
+
"pouchdb",
|
|
26
|
+
"rxdb",
|
|
25
27
|
"database",
|
|
26
28
|
"db",
|
|
27
29
|
"store",
|
package/readme.md
CHANGED
|
@@ -96,9 +96,10 @@ LacertaDB is a **browser-native** document database built for Web3 applications,
|
|
|
96
96
|
- **Hash** indexes for O(1) exact-match lookups
|
|
97
97
|
- **Full-text search** with CJK support via `Intl.Segmenter`
|
|
98
98
|
- **Geospatial queries** with QuadTree-backed `$near` and `$within`
|
|
99
|
-
|
|
100
99
|
</td>
|
|
101
|
-
|
|
100
|
+
</tr>
|
|
101
|
+
</table>
|
|
102
|
+
|
|
102
103
|
|
|
103
104
|
### Performance
|
|
104
105
|
|
|
@@ -120,9 +121,6 @@ LacertaDB is a **browser-native** document database built for Web3 applications,
|
|
|
120
121
|
| **Medium Data** (array of objects) | 334 ops/s · 4,093B | 3,460 ops/s · 4,168B | **5,587 ops/s** · 4,809B |
|
|
121
122
|
| **Large Data** (TypedArray 0.2MB) | 711 ops/s · 200,005B | 341 ops/s · 200,005B | **2,703 ops/s** · 200,023B |
|
|
122
123
|
|
|
123
|
-
</td>
|
|
124
|
-
</tr>
|
|
125
|
-
</table>
|
|
126
124
|
|
|
127
125
|
---
|
|
128
126
|
|