@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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * LacertaDB V0.11.3 - Production Library
2
+ * LacertaDB V0.11.1 - Production Library
3
3
  * @module LacertaDB
4
- * @version 0.11.3
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: false,
32
- preservePropertyDescriptors: false,
31
+ compression: true,
32
+ preservePropertyDescriptors: true,
33
33
  deduplication: false,
34
34
  simdOptimization: true,
35
- detectCircular: false,
36
- shareArrayBuffers: false,
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
- this._frequencies.set(key, (this._frequencies.get(key) || 0) + 1);
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._frequencies.set(key, (this._frequencies.get(key) || 0) + 1);
447
- } else {
448
- if (this._cache.size >= this._maxSize) {
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
- this._cache.set(key, value);
463
- this._frequencies.set(key, 1);
464
- this._timestamps.set(key, Date.now());
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._timers = new Map();
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
- return this._cache.get(key) || null;
502
- }
538
+ const entry = this._cache.get(key);
539
+ if (!entry) return null;
503
540
 
504
- set(key, value) {
505
- if (this._timers.has(key)) {
506
- clearTimeout(this._timers.get(key));
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
- this._cache.set(key, value);
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._cache.has(key);
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
- for (const timer of this._timers.values()) {
543
- clearTimeout(timer);
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 > this.keys[i]) {
1283
+ while (i < this.n && _btreeCmp(key, this.keys[i]) > 0) {
1212
1284
  i++;
1213
1285
  }
1214
1286
 
1215
- if (i < this.n && key === this.keys[i]) {
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] < min) {
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 pastMax = excludeMax ? this.keys[i] >= max : this.keys[i] > max;
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
- // Check current key against bounds
1262
- const meetsMin = min === null || (excludeMin ? this.keys[i] > min : this.keys[i] >= min);
1263
- const meetsMax = max === null || (excludeMax ? this.keys[i] < max : this.keys[i] <= max);
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] > key) {
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] === key) {
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] > key) {
1358
+ while (i >= 0 && _btreeCmp(this.keys[i], key) > 0) {
1298
1359
  i--;
1299
1360
  }
1300
1361
 
1301
- if (i >= 0 && this.keys[i] === key) {
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
- // FIX: After split, the promoted median may equal key.
1314
- // If so, add value to it directly instead of descending
1315
- // (which would create a duplicate entry in the child).
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 after split
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
- this.keys[i] = y.keys[this.order - 1];
1374
- this.values[i] = y.values[this.order - 1];
1375
- // Clear the promoted median from y (it's now in the parent)
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 (proper B-tree delete with rebalancing) ----
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 > this.keys[i]) {
1607
+ while (i < this.n && _btreeCmp(key, this.keys[i]) > 0) {
1581
1608
  i++;
1582
1609
  }
1583
1610
 
1584
- if (i < this.n && key === this.keys[i]) {
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] <= this.keys[i - 1]) {
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
- if (Date.now() - this._lastVerification > this._verificationInterval) {
1666
- this.verify();
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
- if (s.keys[0] === key) {
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 (s.keys[0] < key) i++;
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 = new BrowserCompressionUtility();
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 = this._base64.encode(this._serializer.serialize({filter, options}));
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.10.2',
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.10.2',
4660
+ version: '0.11.1',
4618
4661
  timestamp: Date.now(),
4619
4662
  databases: {}
4620
4663
  };