@pixagram/lacerta-db 0.11.3 → 0.11.4
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 +4 -4
- package/dist/index.min.js +4 -4
- package/index.js +197 -154
- package/package.json +1 -1
- package/readme.md +1 -1
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LacertaDB V0.11.
|
|
2
|
+
* LacertaDB V0.11.1 - Production Library
|
|
3
3
|
* @module LacertaDB
|
|
4
|
-
* @version 0.11.
|
|
4
|
+
* @version 0.11.1
|
|
5
5
|
* @license MIT
|
|
6
6
|
* @author Pixagram SA
|
|
7
7
|
*/
|
|
@@ -28,15 +28,15 @@ import TurboBase64 from "@pixagram/turbobase64";
|
|
|
28
28
|
|
|
29
29
|
// Default TurboSerial configuration (overridable via LacertaDB constructor)
|
|
30
30
|
const TURBO_SERIAL_DEFAULTS = {
|
|
31
|
-
compression:
|
|
32
|
-
preservePropertyDescriptors:
|
|
31
|
+
compression: true,
|
|
32
|
+
preservePropertyDescriptors: true,
|
|
33
33
|
deduplication: false,
|
|
34
34
|
simdOptimization: true,
|
|
35
|
-
detectCircular:
|
|
36
|
-
shareArrayBuffers:
|
|
35
|
+
detectCircular: true,
|
|
36
|
+
shareArrayBuffers: true,
|
|
37
37
|
allowFunction: false,
|
|
38
38
|
serializeFunctions: false,
|
|
39
|
-
memoryPoolSize: 65536
|
|
39
|
+
memoryPoolSize: 65536 * 16
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
// ========================
|
|
@@ -418,9 +418,11 @@ class LFUCache {
|
|
|
418
418
|
constructor(maxSize = 100, ttl = null) {
|
|
419
419
|
this._maxSize = maxSize;
|
|
420
420
|
this._ttl = ttl;
|
|
421
|
-
this._cache = new Map();
|
|
422
|
-
this._frequencies = new Map();
|
|
423
|
-
this._timestamps = new Map();
|
|
421
|
+
this._cache = new Map(); // key → value
|
|
422
|
+
this._frequencies = new Map(); // key → frequency
|
|
423
|
+
this._timestamps = new Map(); // key → insertion timestamp
|
|
424
|
+
this._buckets = new Map(); // frequency → Set<key>
|
|
425
|
+
this._minFreq = 0;
|
|
424
426
|
}
|
|
425
427
|
|
|
426
428
|
get(key) {
|
|
@@ -436,36 +438,63 @@ class LFUCache {
|
|
|
436
438
|
}
|
|
437
439
|
}
|
|
438
440
|
|
|
439
|
-
|
|
441
|
+
// Promote: remove from old bucket, add to new bucket
|
|
442
|
+
const oldFreq = this._frequencies.get(key) || 1;
|
|
443
|
+
const newFreq = oldFreq + 1;
|
|
444
|
+
this._frequencies.set(key, newFreq);
|
|
445
|
+
|
|
446
|
+
const oldBucket = this._buckets.get(oldFreq);
|
|
447
|
+
if (oldBucket) {
|
|
448
|
+
oldBucket.delete(key);
|
|
449
|
+
if (oldBucket.size === 0) {
|
|
450
|
+
this._buckets.delete(oldFreq);
|
|
451
|
+
if (this._minFreq === oldFreq) this._minFreq = newFreq;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (!this._buckets.has(newFreq)) this._buckets.set(newFreq, new Set());
|
|
456
|
+
this._buckets.get(newFreq).add(key);
|
|
457
|
+
|
|
440
458
|
return this._cache.get(key);
|
|
441
459
|
}
|
|
442
460
|
|
|
443
461
|
set(key, value) {
|
|
462
|
+
if (this._maxSize <= 0) return;
|
|
463
|
+
|
|
444
464
|
if (this._cache.has(key)) {
|
|
445
465
|
this._cache.set(key, value);
|
|
446
|
-
this.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
let minFreq = Infinity;
|
|
450
|
-
let evictKey = null;
|
|
451
|
-
for (const [k, freq] of this._frequencies) {
|
|
452
|
-
if (freq < minFreq) {
|
|
453
|
-
minFreq = freq;
|
|
454
|
-
evictKey = k;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
if (evictKey) {
|
|
458
|
-
this.delete(evictKey);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
466
|
+
this.get(key); // triggers frequency promotion
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
461
469
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
this.
|
|
470
|
+
if (this._cache.size >= this._maxSize) {
|
|
471
|
+
// O(1) eviction: grab any key from the lowest-frequency bucket
|
|
472
|
+
const minBucket = this._buckets.get(this._minFreq);
|
|
473
|
+
if (minBucket && minBucket.size > 0) {
|
|
474
|
+
const evictKey = minBucket.values().next().value;
|
|
475
|
+
this.delete(evictKey);
|
|
476
|
+
}
|
|
465
477
|
}
|
|
478
|
+
|
|
479
|
+
this._cache.set(key, value);
|
|
480
|
+
this._frequencies.set(key, 1);
|
|
481
|
+
this._timestamps.set(key, Date.now());
|
|
482
|
+
|
|
483
|
+
if (!this._buckets.has(1)) this._buckets.set(1, new Set());
|
|
484
|
+
this._buckets.get(1).add(key);
|
|
485
|
+
this._minFreq = 1;
|
|
466
486
|
}
|
|
467
487
|
|
|
468
488
|
delete(key) {
|
|
489
|
+
if (!this._cache.has(key)) return false;
|
|
490
|
+
|
|
491
|
+
const freq = this._frequencies.get(key) || 1;
|
|
492
|
+
const bucket = this._buckets.get(freq);
|
|
493
|
+
if (bucket) {
|
|
494
|
+
bucket.delete(key);
|
|
495
|
+
if (bucket.size === 0) this._buckets.delete(freq);
|
|
496
|
+
}
|
|
497
|
+
|
|
469
498
|
this._frequencies.delete(key);
|
|
470
499
|
this._timestamps.delete(key);
|
|
471
500
|
return this._cache.delete(key);
|
|
@@ -475,6 +504,8 @@ class LFUCache {
|
|
|
475
504
|
this._cache.clear();
|
|
476
505
|
this._frequencies.clear();
|
|
477
506
|
this._timestamps.clear();
|
|
507
|
+
this._buckets.clear();
|
|
508
|
+
this._minFreq = 0;
|
|
478
509
|
}
|
|
479
510
|
|
|
480
511
|
has(key) {
|
|
@@ -493,56 +524,63 @@ class LFUCache {
|
|
|
493
524
|
class TTLCache {
|
|
494
525
|
constructor(ttl = 60000) {
|
|
495
526
|
this._ttl = ttl;
|
|
496
|
-
this._cache = new Map();
|
|
497
|
-
this.
|
|
527
|
+
this._cache = new Map(); // key → { value, ts }
|
|
528
|
+
this._sweepTimer = null;
|
|
529
|
+
this._sweepInterval = Math.min(ttl, 30000); // sweep at most every 30s
|
|
530
|
+
|
|
531
|
+
// Start periodic sweep
|
|
532
|
+
if (typeof globalThis !== 'undefined') {
|
|
533
|
+
this._sweepTimer = setInterval(() => this._sweep(), this._sweepInterval);
|
|
534
|
+
}
|
|
498
535
|
}
|
|
499
536
|
|
|
500
537
|
get(key) {
|
|
501
|
-
|
|
502
|
-
|
|
538
|
+
const entry = this._cache.get(key);
|
|
539
|
+
if (!entry) return null;
|
|
503
540
|
|
|
504
|
-
|
|
505
|
-
if (
|
|
506
|
-
|
|
541
|
+
// Lazy eviction: check TTL on read
|
|
542
|
+
if (Date.now() - entry.ts > this._ttl) {
|
|
543
|
+
this._cache.delete(key);
|
|
544
|
+
return null;
|
|
507
545
|
}
|
|
546
|
+
return entry.value;
|
|
547
|
+
}
|
|
508
548
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const timer = setTimeout(() => {
|
|
512
|
-
this.delete(key);
|
|
513
|
-
}, this._ttl);
|
|
514
|
-
this._timers.set(key, timer);
|
|
549
|
+
set(key, value) {
|
|
550
|
+
this._cache.set(key, { value, ts: Date.now() });
|
|
515
551
|
}
|
|
516
552
|
|
|
517
553
|
delete(key) {
|
|
518
|
-
if (this._timers.has(key)) {
|
|
519
|
-
clearTimeout(this._timers.get(key));
|
|
520
|
-
this._timers.delete(key);
|
|
521
|
-
}
|
|
522
554
|
return this._cache.delete(key);
|
|
523
555
|
}
|
|
524
556
|
|
|
525
557
|
clear() {
|
|
526
|
-
for (const timer of this._timers.values()) {
|
|
527
|
-
clearTimeout(timer);
|
|
528
|
-
}
|
|
529
|
-
this._timers.clear();
|
|
530
558
|
this._cache.clear();
|
|
531
559
|
}
|
|
532
560
|
|
|
533
561
|
has(key) {
|
|
534
|
-
return this.
|
|
562
|
+
return this.get(key) !== null;
|
|
535
563
|
}
|
|
536
564
|
|
|
537
565
|
get size() {
|
|
538
566
|
return this._cache.size;
|
|
539
567
|
}
|
|
540
568
|
|
|
569
|
+
/** Periodic sweep: remove all expired entries in one pass */
|
|
570
|
+
_sweep() {
|
|
571
|
+
const now = Date.now();
|
|
572
|
+
for (const [key, entry] of this._cache) {
|
|
573
|
+
if (now - entry.ts > this._ttl) {
|
|
574
|
+
this._cache.delete(key);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
541
579
|
destroy() {
|
|
542
|
-
|
|
543
|
-
|
|
580
|
+
if (this._sweepTimer) {
|
|
581
|
+
clearInterval(this._sweepTimer);
|
|
582
|
+
this._sweepTimer = null;
|
|
544
583
|
}
|
|
545
|
-
this._timers.clear();
|
|
546
584
|
this._cache.clear();
|
|
547
585
|
}
|
|
548
586
|
}
|
|
@@ -680,6 +718,9 @@ class BrowserCompressionUtility {
|
|
|
680
718
|
}
|
|
681
719
|
}
|
|
682
720
|
|
|
721
|
+
// Shared singleton — BrowserCompressionUtility is stateless
|
|
722
|
+
const _sharedCompression = new BrowserCompressionUtility();
|
|
723
|
+
|
|
683
724
|
// ========================
|
|
684
725
|
// Browser Encryption Utility
|
|
685
726
|
// ========================
|
|
@@ -1193,9 +1234,40 @@ class QuadTree {
|
|
|
1193
1234
|
}
|
|
1194
1235
|
|
|
1195
1236
|
// ========================
|
|
1196
|
-
// B-Tree Index Implementation
|
|
1237
|
+
// B-Tree Index Implementation (Hardened)
|
|
1197
1238
|
// ========================
|
|
1198
1239
|
|
|
1240
|
+
/**
|
|
1241
|
+
* Safe total-order comparison for B-Tree keys.
|
|
1242
|
+
* JavaScript's >, <, === do NOT provide a total order when
|
|
1243
|
+
* types are mixed or special values (undefined, null, NaN) appear.
|
|
1244
|
+
* This function guarantees a consistent -1/0/+1 for ANY input.
|
|
1245
|
+
*
|
|
1246
|
+
* Ordering: numbers < strings (within same type, natural order)
|
|
1247
|
+
* @param {*} a
|
|
1248
|
+
* @param {*} b
|
|
1249
|
+
* @returns {number} -1 if a<b, 0 if a===b, 1 if a>b
|
|
1250
|
+
*/
|
|
1251
|
+
function _btreeCmp(a, b) {
|
|
1252
|
+
// Identical references (covers same-value primitives and same object)
|
|
1253
|
+
if (a === b) return 0;
|
|
1254
|
+
|
|
1255
|
+
const ta = typeof a;
|
|
1256
|
+
const tb = typeof b;
|
|
1257
|
+
|
|
1258
|
+
// Same type — fast path (99% of real usage)
|
|
1259
|
+
if (ta === tb) {
|
|
1260
|
+
if (ta === 'number') return a < b ? -1 : 1;
|
|
1261
|
+
if (ta === 'string') return a < b ? -1 : (a > b ? 1 : 0);
|
|
1262
|
+
// Fallback: coerce to string for other types
|
|
1263
|
+
const sa = String(a), sb = String(b);
|
|
1264
|
+
return sa < sb ? -1 : (sa > sb ? 1 : 0);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Different types — sort by type name for a stable total order
|
|
1268
|
+
return ta < tb ? -1 : 1;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1199
1271
|
class BTreeNode {
|
|
1200
1272
|
constructor(order, leaf) {
|
|
1201
1273
|
this.keys = new Array(2 * order - 1);
|
|
@@ -1208,11 +1280,11 @@ class BTreeNode {
|
|
|
1208
1280
|
|
|
1209
1281
|
search(key) {
|
|
1210
1282
|
let i = 0;
|
|
1211
|
-
while (i < this.n && key
|
|
1283
|
+
while (i < this.n && _btreeCmp(key, this.keys[i]) > 0) {
|
|
1212
1284
|
i++;
|
|
1213
1285
|
}
|
|
1214
1286
|
|
|
1215
|
-
if (i < this.n && key
|
|
1287
|
+
if (i < this.n && _btreeCmp(key, this.keys[i]) === 0) {
|
|
1216
1288
|
return this.values[i];
|
|
1217
1289
|
}
|
|
1218
1290
|
|
|
@@ -1223,28 +1295,18 @@ class BTreeNode {
|
|
|
1223
1295
|
return this.children[i] ? this.children[i].search(key) : null;
|
|
1224
1296
|
}
|
|
1225
1297
|
|
|
1226
|
-
// Optimized range search with subtree pruning (O(log n + k) instead of O(n))
|
|
1227
|
-
// excludeMin/excludeMax: when true, boundary values are excluded from results
|
|
1228
1298
|
rangeSearch(min, max, results, excludeMin = false, excludeMax = false) {
|
|
1229
|
-
// Skip keys strictly below the min bound
|
|
1230
1299
|
let i = 0;
|
|
1231
1300
|
if (min !== null) {
|
|
1232
|
-
while (i < this.n && this.keys[i] <
|
|
1301
|
+
while (i < this.n && _btreeCmp(this.keys[i], min) < 0) {
|
|
1233
1302
|
i++;
|
|
1234
1303
|
}
|
|
1235
|
-
// If min is exclusive, also skip keys equal to min
|
|
1236
|
-
if (excludeMin) {
|
|
1237
|
-
// But first descend into the child at boundary — it may have keys > min
|
|
1238
|
-
// that are relevant. We handle this below in the loop.
|
|
1239
|
-
}
|
|
1240
1304
|
}
|
|
1241
1305
|
|
|
1242
|
-
// Process keys from i onward
|
|
1243
1306
|
for (; i < this.n; i++) {
|
|
1244
|
-
// Early exit: if current key exceeds max (or equals max when exclusive),
|
|
1245
|
-
// descend into left child then stop — no further keys can match
|
|
1246
1307
|
if (max !== null) {
|
|
1247
|
-
const
|
|
1308
|
+
const cmpMax = _btreeCmp(this.keys[i], max);
|
|
1309
|
+
const pastMax = excludeMax ? cmpMax >= 0 : cmpMax > 0;
|
|
1248
1310
|
if (pastMax) {
|
|
1249
1311
|
if (!this.leaf && this.children[i]) {
|
|
1250
1312
|
this.children[i].rangeSearch(min, max, results, excludeMin, excludeMax);
|
|
@@ -1253,21 +1315,20 @@ class BTreeNode {
|
|
|
1253
1315
|
}
|
|
1254
1316
|
}
|
|
1255
1317
|
|
|
1256
|
-
// Descend into left child of current key
|
|
1257
1318
|
if (!this.leaf && this.children[i]) {
|
|
1258
1319
|
this.children[i].rangeSearch(min, max, results, excludeMin, excludeMax);
|
|
1259
1320
|
}
|
|
1260
1321
|
|
|
1261
|
-
|
|
1262
|
-
const
|
|
1263
|
-
const
|
|
1322
|
+
const cmpMin = min === null ? 1 : _btreeCmp(this.keys[i], min);
|
|
1323
|
+
const cmpMaxCheck = max === null ? -1 : _btreeCmp(this.keys[i], max);
|
|
1324
|
+
const meetsMin = min === null || (excludeMin ? cmpMin > 0 : cmpMin >= 0);
|
|
1325
|
+
const meetsMax = max === null || (excludeMax ? cmpMaxCheck < 0 : cmpMaxCheck <= 0);
|
|
1264
1326
|
|
|
1265
1327
|
if (meetsMin && meetsMax && this.values[i]) {
|
|
1266
1328
|
this.values[i].forEach(v => results.push(v));
|
|
1267
1329
|
}
|
|
1268
1330
|
}
|
|
1269
1331
|
|
|
1270
|
-
// Descend into rightmost child
|
|
1271
1332
|
if (!this.leaf && this.children[i]) {
|
|
1272
1333
|
this.children[i].rangeSearch(min, max, results, excludeMin, excludeMax);
|
|
1273
1334
|
}
|
|
@@ -1277,13 +1338,13 @@ class BTreeNode {
|
|
|
1277
1338
|
let i = this.n - 1;
|
|
1278
1339
|
|
|
1279
1340
|
if (this.leaf) {
|
|
1280
|
-
while (i >= 0 && this.keys[i] >
|
|
1341
|
+
while (i >= 0 && _btreeCmp(this.keys[i], key) > 0) {
|
|
1281
1342
|
this.keys[i + 1] = this.keys[i];
|
|
1282
1343
|
this.values[i + 1] = this.values[i];
|
|
1283
1344
|
i--;
|
|
1284
1345
|
}
|
|
1285
1346
|
|
|
1286
|
-
if (i >= 0 && this.keys[i] ===
|
|
1347
|
+
if (i >= 0 && _btreeCmp(this.keys[i], key) === 0) {
|
|
1287
1348
|
if (!this.values[i]) {
|
|
1288
1349
|
this.values[i] = new Set();
|
|
1289
1350
|
}
|
|
@@ -1294,11 +1355,11 @@ class BTreeNode {
|
|
|
1294
1355
|
this.n++;
|
|
1295
1356
|
}
|
|
1296
1357
|
} else {
|
|
1297
|
-
while (i >= 0 && this.keys[i] >
|
|
1358
|
+
while (i >= 0 && _btreeCmp(this.keys[i], key) > 0) {
|
|
1298
1359
|
i--;
|
|
1299
1360
|
}
|
|
1300
1361
|
|
|
1301
|
-
if (i >= 0 && this.keys[i] ===
|
|
1362
|
+
if (i >= 0 && _btreeCmp(this.keys[i], key) === 0) {
|
|
1302
1363
|
if (!this.values[i]) {
|
|
1303
1364
|
this.values[i] = new Set();
|
|
1304
1365
|
}
|
|
@@ -1310,18 +1371,13 @@ class BTreeNode {
|
|
|
1310
1371
|
if (this.children[i] && this.children[i].n === 2 * this.order - 1) {
|
|
1311
1372
|
this.splitChild(i, this.children[i]);
|
|
1312
1373
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
if (this.keys[i] === key) {
|
|
1317
|
-
if (!this.values[i]) {
|
|
1318
|
-
this.values[i] = new Set();
|
|
1319
|
-
}
|
|
1374
|
+
const cmp = _btreeCmp(this.keys[i], key);
|
|
1375
|
+
if (cmp === 0) {
|
|
1376
|
+
if (!this.values[i]) this.values[i] = new Set();
|
|
1320
1377
|
this.values[i].add(value);
|
|
1321
1378
|
return;
|
|
1322
1379
|
}
|
|
1323
|
-
|
|
1324
|
-
if (this.keys[i] < key) {
|
|
1380
|
+
if (cmp < 0) {
|
|
1325
1381
|
i++;
|
|
1326
1382
|
}
|
|
1327
1383
|
}
|
|
@@ -1346,9 +1402,14 @@ class BTreeNode {
|
|
|
1346
1402
|
}
|
|
1347
1403
|
}
|
|
1348
1404
|
|
|
1405
|
+
// CRITICAL: Save the median BEFORE cleaning stale slots.
|
|
1406
|
+
// The clean loop covers index (order-1) which IS the median position.
|
|
1407
|
+
const medianKey = y.keys[this.order - 1];
|
|
1408
|
+
const medianValue = y.values[this.order - 1];
|
|
1409
|
+
|
|
1349
1410
|
y.n = this.order - 1;
|
|
1350
1411
|
|
|
1351
|
-
// Clean stale slots in y
|
|
1412
|
+
// Clean all stale slots in y (median + right half)
|
|
1352
1413
|
for (let j = this.order - 1; j < 2 * this.order - 1; j++) {
|
|
1353
1414
|
y.keys[j] = undefined;
|
|
1354
1415
|
y.values[j] = undefined;
|
|
@@ -1359,28 +1420,26 @@ class BTreeNode {
|
|
|
1359
1420
|
}
|
|
1360
1421
|
}
|
|
1361
1422
|
|
|
1423
|
+
// Shift parent's children right to make room
|
|
1362
1424
|
for (let j = this.n; j >= i + 1; j--) {
|
|
1363
1425
|
this.children[j + 1] = this.children[j];
|
|
1364
1426
|
}
|
|
1365
|
-
|
|
1366
1427
|
this.children[i + 1] = z;
|
|
1367
1428
|
|
|
1429
|
+
// Shift parent's keys/values right to make room
|
|
1368
1430
|
for (let j = this.n - 1; j >= i; j--) {
|
|
1369
1431
|
this.keys[j + 1] = this.keys[j];
|
|
1370
1432
|
this.values[j + 1] = this.values[j];
|
|
1371
1433
|
}
|
|
1372
1434
|
|
|
1373
|
-
|
|
1374
|
-
this.
|
|
1375
|
-
|
|
1376
|
-
y.keys[this.order - 1] = undefined;
|
|
1377
|
-
y.values[this.order - 1] = undefined;
|
|
1435
|
+
// Promote the saved median
|
|
1436
|
+
this.keys[i] = medianKey;
|
|
1437
|
+
this.values[i] = medianValue;
|
|
1378
1438
|
this.n++;
|
|
1379
1439
|
}
|
|
1380
1440
|
|
|
1381
|
-
// ---- Deletion helpers
|
|
1441
|
+
// ---- Deletion helpers ----
|
|
1382
1442
|
|
|
1383
|
-
// Get the predecessor: rightmost key in the left subtree of keys[idx]
|
|
1384
1443
|
_getPredecessor(idx) {
|
|
1385
1444
|
let node = this.children[idx];
|
|
1386
1445
|
while (!node.leaf) {
|
|
@@ -1389,7 +1448,6 @@ class BTreeNode {
|
|
|
1389
1448
|
return { key: node.keys[node.n - 1], value: node.values[node.n - 1] };
|
|
1390
1449
|
}
|
|
1391
1450
|
|
|
1392
|
-
// Get the successor: leftmost key in the right subtree of keys[idx]
|
|
1393
1451
|
_getSuccessor(idx) {
|
|
1394
1452
|
let node = this.children[idx + 1];
|
|
1395
1453
|
while (!node.leaf) {
|
|
@@ -1398,23 +1456,19 @@ class BTreeNode {
|
|
|
1398
1456
|
return { key: node.keys[0], value: node.values[0] };
|
|
1399
1457
|
}
|
|
1400
1458
|
|
|
1401
|
-
// Merge children[idx+1] into children[idx], pulling keys[idx] down as separator
|
|
1402
1459
|
_merge(idx) {
|
|
1403
1460
|
const child = this.children[idx];
|
|
1404
1461
|
const sibling = this.children[idx + 1];
|
|
1405
1462
|
const t = this.order;
|
|
1406
1463
|
|
|
1407
|
-
// Pull separator key down into child
|
|
1408
1464
|
child.keys[t - 1] = this.keys[idx];
|
|
1409
1465
|
child.values[t - 1] = this.values[idx];
|
|
1410
1466
|
|
|
1411
|
-
// Copy keys/values from sibling into child
|
|
1412
1467
|
for (let j = 0; j < sibling.n; j++) {
|
|
1413
1468
|
child.keys[t + j] = sibling.keys[j];
|
|
1414
1469
|
child.values[t + j] = sibling.values[j];
|
|
1415
1470
|
}
|
|
1416
1471
|
|
|
1417
|
-
// Copy children from sibling
|
|
1418
1472
|
if (!child.leaf) {
|
|
1419
1473
|
for (let j = 0; j <= sibling.n; j++) {
|
|
1420
1474
|
child.children[t + j] = sibling.children[j];
|
|
@@ -1423,18 +1477,15 @@ class BTreeNode {
|
|
|
1423
1477
|
|
|
1424
1478
|
child.n += sibling.n + 1;
|
|
1425
1479
|
|
|
1426
|
-
// Shift keys/values left in this node to fill the gap
|
|
1427
1480
|
for (let j = idx; j < this.n - 1; j++) {
|
|
1428
1481
|
this.keys[j] = this.keys[j + 1];
|
|
1429
1482
|
this.values[j] = this.values[j + 1];
|
|
1430
1483
|
}
|
|
1431
1484
|
|
|
1432
|
-
// Shift children left in this node
|
|
1433
1485
|
for (let j = idx + 1; j < this.n; j++) {
|
|
1434
1486
|
this.children[j] = this.children[j + 1];
|
|
1435
1487
|
}
|
|
1436
1488
|
|
|
1437
|
-
// Clean stale trailing slots
|
|
1438
1489
|
this.keys[this.n - 1] = undefined;
|
|
1439
1490
|
this.values[this.n - 1] = undefined;
|
|
1440
1491
|
this.children[this.n] = undefined;
|
|
@@ -1442,12 +1493,10 @@ class BTreeNode {
|
|
|
1442
1493
|
this.n--;
|
|
1443
1494
|
}
|
|
1444
1495
|
|
|
1445
|
-
// Borrow the last key from children[idx-1] through the parent
|
|
1446
1496
|
_borrowFromPrev(idx) {
|
|
1447
1497
|
const child = this.children[idx];
|
|
1448
1498
|
const sibling = this.children[idx - 1];
|
|
1449
1499
|
|
|
1450
|
-
// Shift everything in child right by 1
|
|
1451
1500
|
for (let j = child.n - 1; j >= 0; j--) {
|
|
1452
1501
|
child.keys[j + 1] = child.keys[j];
|
|
1453
1502
|
child.values[j + 1] = child.values[j];
|
|
@@ -1458,21 +1507,17 @@ class BTreeNode {
|
|
|
1458
1507
|
}
|
|
1459
1508
|
}
|
|
1460
1509
|
|
|
1461
|
-
// Move separator from parent down to child[0]
|
|
1462
1510
|
child.keys[0] = this.keys[idx - 1];
|
|
1463
1511
|
child.values[0] = this.values[idx - 1];
|
|
1464
1512
|
|
|
1465
|
-
// Move last child of sibling to child
|
|
1466
1513
|
if (!child.leaf) {
|
|
1467
1514
|
child.children[0] = sibling.children[sibling.n];
|
|
1468
1515
|
sibling.children[sibling.n] = undefined;
|
|
1469
1516
|
}
|
|
1470
1517
|
|
|
1471
|
-
// Move last key of sibling up to parent
|
|
1472
1518
|
this.keys[idx - 1] = sibling.keys[sibling.n - 1];
|
|
1473
1519
|
this.values[idx - 1] = sibling.values[sibling.n - 1];
|
|
1474
1520
|
|
|
1475
|
-
// Clean stale slots in sibling
|
|
1476
1521
|
sibling.keys[sibling.n - 1] = undefined;
|
|
1477
1522
|
sibling.values[sibling.n - 1] = undefined;
|
|
1478
1523
|
|
|
@@ -1480,25 +1525,20 @@ class BTreeNode {
|
|
|
1480
1525
|
sibling.n--;
|
|
1481
1526
|
}
|
|
1482
1527
|
|
|
1483
|
-
// Borrow the first key from children[idx+1] through the parent
|
|
1484
1528
|
_borrowFromNext(idx) {
|
|
1485
1529
|
const child = this.children[idx];
|
|
1486
1530
|
const sibling = this.children[idx + 1];
|
|
1487
1531
|
|
|
1488
|
-
// Move separator from parent down to end of child
|
|
1489
1532
|
child.keys[child.n] = this.keys[idx];
|
|
1490
1533
|
child.values[child.n] = this.values[idx];
|
|
1491
1534
|
|
|
1492
|
-
// Move first child of sibling to child
|
|
1493
1535
|
if (!child.leaf) {
|
|
1494
1536
|
child.children[child.n + 1] = sibling.children[0];
|
|
1495
1537
|
}
|
|
1496
1538
|
|
|
1497
|
-
// Move first key of sibling up to parent
|
|
1498
1539
|
this.keys[idx] = sibling.keys[0];
|
|
1499
1540
|
this.values[idx] = sibling.values[0];
|
|
1500
1541
|
|
|
1501
|
-
// Shift sibling's keys/values left
|
|
1502
1542
|
for (let j = 0; j < sibling.n - 1; j++) {
|
|
1503
1543
|
sibling.keys[j] = sibling.keys[j + 1];
|
|
1504
1544
|
sibling.values[j] = sibling.values[j + 1];
|
|
@@ -1510,7 +1550,6 @@ class BTreeNode {
|
|
|
1510
1550
|
sibling.children[sibling.n] = undefined;
|
|
1511
1551
|
}
|
|
1512
1552
|
|
|
1513
|
-
// Clean stale trailing slots in sibling
|
|
1514
1553
|
sibling.keys[sibling.n - 1] = undefined;
|
|
1515
1554
|
sibling.values[sibling.n - 1] = undefined;
|
|
1516
1555
|
|
|
@@ -1518,8 +1557,6 @@ class BTreeNode {
|
|
|
1518
1557
|
sibling.n--;
|
|
1519
1558
|
}
|
|
1520
1559
|
|
|
1521
|
-
// Ensure children[idx] has at least `order` keys (minimum degree)
|
|
1522
|
-
// so we can safely descend into it during deletion
|
|
1523
1560
|
_fill(idx) {
|
|
1524
1561
|
const t = this.order;
|
|
1525
1562
|
if (idx > 0 && this.children[idx - 1] && this.children[idx - 1].n >= t) {
|
|
@@ -1527,7 +1564,6 @@ class BTreeNode {
|
|
|
1527
1564
|
} else if (idx < this.n && this.children[idx + 1] && this.children[idx + 1].n >= t) {
|
|
1528
1565
|
this._borrowFromNext(idx);
|
|
1529
1566
|
} else {
|
|
1530
|
-
// Merge with a sibling
|
|
1531
1567
|
if (idx < this.n) {
|
|
1532
1568
|
this._merge(idx);
|
|
1533
1569
|
} else {
|
|
@@ -1536,7 +1572,6 @@ class BTreeNode {
|
|
|
1536
1572
|
}
|
|
1537
1573
|
}
|
|
1538
1574
|
|
|
1539
|
-
// Remove a leaf-level key entry (shift keys, values left)
|
|
1540
1575
|
_removeFromLeaf(idx) {
|
|
1541
1576
|
for (let j = idx; j < this.n - 1; j++) {
|
|
1542
1577
|
this.keys[j] = this.keys[j + 1];
|
|
@@ -1547,42 +1582,33 @@ class BTreeNode {
|
|
|
1547
1582
|
this.n--;
|
|
1548
1583
|
}
|
|
1549
1584
|
|
|
1550
|
-
// Remove an internal key entry using predecessor/successor replacement
|
|
1551
1585
|
_removeFromInternal(idx) {
|
|
1552
1586
|
const t = this.order;
|
|
1553
1587
|
const key = this.keys[idx];
|
|
1554
1588
|
|
|
1555
1589
|
if (this.children[idx] && this.children[idx].n >= t) {
|
|
1556
|
-
// Left child has enough keys: replace with predecessor
|
|
1557
1590
|
const pred = this._getPredecessor(idx);
|
|
1558
1591
|
this.keys[idx] = pred.key;
|
|
1559
1592
|
this.values[idx] = pred.value;
|
|
1560
1593
|
this.children[idx]._remove(pred.key, null, true);
|
|
1561
1594
|
} else if (this.children[idx + 1] && this.children[idx + 1].n >= t) {
|
|
1562
|
-
// Right child has enough keys: replace with successor
|
|
1563
1595
|
const succ = this._getSuccessor(idx);
|
|
1564
1596
|
this.keys[idx] = succ.key;
|
|
1565
1597
|
this.values[idx] = succ.value;
|
|
1566
1598
|
this.children[idx + 1]._remove(succ.key, null, true);
|
|
1567
1599
|
} else {
|
|
1568
|
-
// Both children at minimum: merge them, then delete from merged child
|
|
1569
1600
|
this._merge(idx);
|
|
1570
1601
|
this.children[idx]._remove(key, null, true);
|
|
1571
1602
|
}
|
|
1572
1603
|
}
|
|
1573
1604
|
|
|
1574
|
-
// Core removal engine.
|
|
1575
|
-
// removeEntire=false: remove one value from the Set; delete key entry if Set empties
|
|
1576
|
-
// removeEntire=true: delete the entire key entry regardless of Set contents
|
|
1577
|
-
// Returns true if a key entry was fully removed, false otherwise
|
|
1578
1605
|
_remove(key, value, removeEntire) {
|
|
1579
1606
|
let i = 0;
|
|
1580
|
-
while (i < this.n && key
|
|
1607
|
+
while (i < this.n && _btreeCmp(key, this.keys[i]) > 0) {
|
|
1581
1608
|
i++;
|
|
1582
1609
|
}
|
|
1583
1610
|
|
|
1584
|
-
if (i < this.n && key
|
|
1585
|
-
// Key found at this node
|
|
1611
|
+
if (i < this.n && _btreeCmp(key, this.keys[i]) === 0) {
|
|
1586
1612
|
let shouldRemoveEntry = removeEntire;
|
|
1587
1613
|
|
|
1588
1614
|
if (!shouldRemoveEntry && this.values[i]) {
|
|
@@ -1600,17 +1626,14 @@ class BTreeNode {
|
|
|
1600
1626
|
}
|
|
1601
1627
|
return false;
|
|
1602
1628
|
} else {
|
|
1603
|
-
// Key not found at this level — descend
|
|
1604
1629
|
if (this.leaf) return false;
|
|
1605
1630
|
|
|
1606
1631
|
const isLastChild = (i === this.n);
|
|
1607
1632
|
|
|
1608
|
-
// Ensure the child we descend into has enough keys for safe deletion
|
|
1609
1633
|
if (this.children[i] && this.children[i].n < this.order) {
|
|
1610
1634
|
this._fill(i);
|
|
1611
1635
|
}
|
|
1612
1636
|
|
|
1613
|
-
// After _fill, if the last child was merged, idx shifted
|
|
1614
1637
|
if (isLastChild && i > this.n) {
|
|
1615
1638
|
return this.children[i - 1]
|
|
1616
1639
|
? this.children[i - 1]._remove(key, value, removeEntire)
|
|
@@ -1623,20 +1646,23 @@ class BTreeNode {
|
|
|
1623
1646
|
}
|
|
1624
1647
|
}
|
|
1625
1648
|
|
|
1626
|
-
// Public remove: remove a single (key, value) pair
|
|
1627
1649
|
remove(key, value) {
|
|
1628
1650
|
return this._remove(key, value, false);
|
|
1629
1651
|
}
|
|
1630
1652
|
|
|
1631
|
-
// Public removeKey: remove an entire key entry (used internally for predecessor/successor cleanup)
|
|
1632
1653
|
removeKey(key) {
|
|
1633
1654
|
return this._remove(key, null, true);
|
|
1634
1655
|
}
|
|
1635
1656
|
|
|
1636
1657
|
verify() {
|
|
1637
1658
|
const issues = [];
|
|
1659
|
+
for (let i = 0; i < this.n; i++) {
|
|
1660
|
+
if (this.keys[i] === undefined || this.keys[i] === null) {
|
|
1661
|
+
issues.push(`Invalid key (${this.keys[i]}) at index ${i}`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1638
1664
|
for (let i = 1; i < this.n; i++) {
|
|
1639
|
-
if (this.keys[i]
|
|
1665
|
+
if (_btreeCmp(this.keys[i], this.keys[i - 1]) <= 0) {
|
|
1640
1666
|
issues.push(`Key order violation at index ${i}`);
|
|
1641
1667
|
}
|
|
1642
1668
|
}
|
|
@@ -1657,14 +1683,11 @@ class BTreeIndex {
|
|
|
1657
1683
|
this._root = null;
|
|
1658
1684
|
this._order = order;
|
|
1659
1685
|
this._size = 0;
|
|
1660
|
-
this._lastVerification = Date.now();
|
|
1661
|
-
this._verificationInterval = 60000;
|
|
1662
1686
|
}
|
|
1663
1687
|
|
|
1664
1688
|
insert(key, value) {
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
}
|
|
1689
|
+
// Reject keys that break comparison semantics
|
|
1690
|
+
if (key === undefined || key === null || (typeof key === 'number' && isNaN(key))) return;
|
|
1668
1691
|
|
|
1669
1692
|
// Check for exact duplicate (key, value) to keep _size accurate
|
|
1670
1693
|
if (this._root) {
|
|
@@ -1685,13 +1708,13 @@ class BTreeIndex {
|
|
|
1685
1708
|
s.children[0] = this._root;
|
|
1686
1709
|
s.splitChild(0, this._root);
|
|
1687
1710
|
|
|
1688
|
-
// FIX: Check if promoted median equals key
|
|
1689
1711
|
let i = 0;
|
|
1690
|
-
|
|
1712
|
+
const cmp = _btreeCmp(s.keys[0], key);
|
|
1713
|
+
if (cmp === 0) {
|
|
1691
1714
|
if (!s.values[0]) s.values[0] = new Set();
|
|
1692
1715
|
s.values[0].add(value);
|
|
1693
1716
|
} else {
|
|
1694
|
-
if (
|
|
1717
|
+
if (cmp < 0) i++;
|
|
1695
1718
|
s.children[i].insertNonFull(key, value);
|
|
1696
1719
|
}
|
|
1697
1720
|
|
|
@@ -1740,7 +1763,6 @@ class BTreeIndex {
|
|
|
1740
1763
|
const existing = this._root.search(key);
|
|
1741
1764
|
if (existing && existing.has(value)) {
|
|
1742
1765
|
this._root.remove(key, value);
|
|
1743
|
-
// Shrink root if it became empty (all keys merged down)
|
|
1744
1766
|
if (this._root.n === 0 && !this._root.leaf && this._root.children[0]) {
|
|
1745
1767
|
this._root = this._root.children[0];
|
|
1746
1768
|
}
|
|
@@ -1749,12 +1771,9 @@ class BTreeIndex {
|
|
|
1749
1771
|
}
|
|
1750
1772
|
|
|
1751
1773
|
verify() {
|
|
1752
|
-
this._lastVerification = Date.now();
|
|
1753
1774
|
if (!this._root) return { healthy: true, issues: [] };
|
|
1754
1775
|
const issues = this._root.verify();
|
|
1755
1776
|
if (issues.length > 0) {
|
|
1756
|
-
// NOTE: verify detects structural violations but cannot auto-repair.
|
|
1757
|
-
// A full rebuild is required to fix a corrupted index.
|
|
1758
1777
|
console.warn('BTree index issues detected (rebuild required):', issues);
|
|
1759
1778
|
}
|
|
1760
1779
|
return {
|
|
@@ -2634,7 +2653,7 @@ class Document {
|
|
|
2634
2653
|
this._attachments = data._attachments || [];
|
|
2635
2654
|
this._data = null;
|
|
2636
2655
|
this._packedData = data.packedData || null;
|
|
2637
|
-
this._compression =
|
|
2656
|
+
this._compression = _sharedCompression;
|
|
2638
2657
|
this._serializer = serializer;
|
|
2639
2658
|
|
|
2640
2659
|
if (data.data) {
|
|
@@ -3579,6 +3598,30 @@ class PerformanceMonitor {
|
|
|
3579
3598
|
}
|
|
3580
3599
|
}
|
|
3581
3600
|
|
|
3601
|
+
// ========================
|
|
3602
|
+
// Stable Cache Key Utility
|
|
3603
|
+
// ========================
|
|
3604
|
+
|
|
3605
|
+
/**
|
|
3606
|
+
* Generate a deterministic cache key from query filter + options.
|
|
3607
|
+
* Uses sorted-keys JSON.stringify for stability, avoiding the overhead
|
|
3608
|
+
* of full TurboSerial serialize + Base64 encode on every query call.
|
|
3609
|
+
* @param {object} filter
|
|
3610
|
+
* @param {object} options
|
|
3611
|
+
* @returns {string}
|
|
3612
|
+
*/
|
|
3613
|
+
function _stableCacheKey(filter, options) {
|
|
3614
|
+
const replacer = (_, v) => {
|
|
3615
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
3616
|
+
const sorted = {};
|
|
3617
|
+
for (const k of Object.keys(v).sort()) sorted[k] = v[k];
|
|
3618
|
+
return sorted;
|
|
3619
|
+
}
|
|
3620
|
+
return v;
|
|
3621
|
+
};
|
|
3622
|
+
return JSON.stringify({ f: filter, o: options }, replacer);
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3582
3625
|
// ========================
|
|
3583
3626
|
// Collection Class (Optimized)
|
|
3584
3627
|
// ========================
|
|
@@ -3858,7 +3901,7 @@ class Collection {
|
|
|
3858
3901
|
|
|
3859
3902
|
const startTime = performance.now();
|
|
3860
3903
|
|
|
3861
|
-
const cacheKey =
|
|
3904
|
+
const cacheKey = _stableCacheKey(filter, options);
|
|
3862
3905
|
const cached = this._cacheStrategy.get(cacheKey);
|
|
3863
3906
|
|
|
3864
3907
|
if (cached) {
|
|
@@ -4428,7 +4471,7 @@ class Database {
|
|
|
4428
4471
|
|
|
4429
4472
|
async export(format = 'json', password = null) {
|
|
4430
4473
|
const data = {
|
|
4431
|
-
version: '0.
|
|
4474
|
+
version: '0.11.1',
|
|
4432
4475
|
database: this.name,
|
|
4433
4476
|
timestamp: Date.now(),
|
|
4434
4477
|
collections: {}
|
|
@@ -4614,7 +4657,7 @@ class LacertaDB {
|
|
|
4614
4657
|
|
|
4615
4658
|
async createBackup(password = null) {
|
|
4616
4659
|
const backup = {
|
|
4617
|
-
version: '0.
|
|
4660
|
+
version: '0.11.1',
|
|
4618
4661
|
timestamp: Date.now(),
|
|
4619
4662
|
databases: {}
|
|
4620
4663
|
};
|