@pixagram/lacerta-db 0.10.2 → 0.11.0

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.10.2 - Production Library
2
+ * LacertaDB V0.11.0 - Production Library
3
3
  * @module LacertaDB
4
- * @version 0.9.2
4
+ * @version 0.11.0
5
5
  * @license MIT
6
6
  * @author Pixagram SA
7
7
  */
@@ -26,19 +26,18 @@ if (typeof window !== 'undefined' && !window.requestIdleCallback) {
26
26
  import TurboSerial from "@pixagram/turboserial";
27
27
  import TurboBase64 from "@pixagram/turbobase64";
28
28
 
29
- const serializer = new TurboSerial({
30
- compression: true, // Enable built-in compression
31
- preservePropertyDescriptors: true, // Preserve property descriptors
32
- deduplication: true, // Deduplicate repeated values
33
- simdOptimization: true, // Use SIMD instructions when available
34
- detectCircular: true, // Handle circular references
35
- shareArrayBuffers: true, // Share ArrayBuffer references
36
- allowFunction: false, // Allow function storage/retrieval (security gate)
37
- serializeFunctions: false, // Capture and reconstruct function source
38
- memoryPoolSize: 65536*16 // Initial memory pool size (1MB)
39
- });
40
-
41
- const base64 = new TurboBase64();
29
+ // Default TurboSerial configuration (overridable via LacertaDB constructor)
30
+ const TURBO_SERIAL_DEFAULTS = {
31
+ compression: false,
32
+ preservePropertyDescriptors: true,
33
+ deduplication: false,
34
+ simdOptimization: true,
35
+ detectCircular: true,
36
+ shareArrayBuffers: true,
37
+ allowFunction: false,
38
+ serializeFunctions: false,
39
+ memoryPoolSize: 65536 * 16
40
+ };
42
41
 
43
42
  // ========================
44
43
  // Quick Store (Optimized)
@@ -49,8 +48,10 @@ const base64 = new TurboBase64();
49
48
  * Keeps index in memory to avoid blocking main thread with JSON parsing.
50
49
  */
51
50
  class QuickStore {
52
- constructor(dbName) {
51
+ constructor(dbName, serializer, base64) {
53
52
  this._dbName = dbName;
53
+ this._serializer = serializer;
54
+ this._base64 = base64;
54
55
  this._keyPrefix = `lacertadb_${dbName}_quickstore_`;
55
56
  this._indexKey = `${this._keyPrefix}index`;
56
57
 
@@ -91,8 +92,8 @@ class QuickStore {
91
92
  const indexStr = localStorage.getItem(this._indexKey);
92
93
  if (indexStr) {
93
94
  try {
94
- const decoded = base64.decode(indexStr);
95
- const list = serializer.deserialize(decoded);
95
+ const decoded = this._base64.decode(indexStr);
96
+ const list = this._serializer.deserialize(decoded);
96
97
  this._indexCache = new Set(list);
97
98
  } catch (e) {
98
99
  console.warn('QuickStore index corrupted, resetting.', e);
@@ -110,8 +111,8 @@ class QuickStore {
110
111
  if (!this._dirty) return;
111
112
  try {
112
113
  const list = Array.from(this._indexCache);
113
- const serializedIndex = serializer.serialize(list);
114
- const encodedIndex = base64.encode(serializedIndex);
114
+ const serializedIndex = this._serializer.serialize(list);
115
+ const encodedIndex = this._base64.encode(serializedIndex);
115
116
  localStorage.setItem(this._indexKey, encodedIndex);
116
117
  this._dirty = false;
117
118
  } catch (e) {
@@ -128,7 +129,7 @@ class QuickStore {
128
129
  };
129
130
 
130
131
  // Debounce with idle callback to prevent UI freezing
131
- if (window.requestIdleCallback) {
132
+ if (typeof window !== 'undefined' && window.requestIdleCallback) {
132
133
  this._saveIndexTimer = window.requestIdleCallback(save);
133
134
  } else {
134
135
  this._saveIndexTimer = setTimeout(save, 200);
@@ -139,8 +140,8 @@ class QuickStore {
139
140
  if (!this._dirty) return;
140
141
  try {
141
142
  const list = Array.from(this._indexCache);
142
- const serializedIndex = serializer.serialize(list);
143
- const encodedIndex = base64.encode(serializedIndex);
143
+ const serializedIndex = this._serializer.serialize(list);
144
+ const encodedIndex = this._base64.encode(serializedIndex);
144
145
  localStorage.setItem(this._indexKey, encodedIndex);
145
146
  this._dirty = false;
146
147
  } catch (e) {
@@ -159,8 +160,8 @@ class QuickStore {
159
160
  this._ensureIndexLoaded();
160
161
  const key = `${this._keyPrefix}data_${docId}`;
161
162
  try {
162
- const serializedData = serializer.serialize(data);
163
- const encodedData = base64.encode(serializedData);
163
+ const serializedData = this._serializer.serialize(data);
164
+ const encodedData = this._base64.encode(serializedData);
164
165
  localStorage.setItem(key, encodedData);
165
166
 
166
167
  if (!this._indexCache.has(docId)) {
@@ -182,8 +183,8 @@ class QuickStore {
182
183
  const stored = localStorage.getItem(key);
183
184
  if (stored) {
184
185
  try {
185
- const decoded = base64.decode(stored);
186
- return serializer.deserialize(decoded);
186
+ const decoded = this._base64.decode(stored);
187
+ return this._serializer.deserialize(decoded);
187
188
  } catch (e) {
188
189
  console.error('Failed to parse QuickStore data:', e);
189
190
  }
@@ -768,12 +769,14 @@ class BrowserEncryptionUtility {
768
769
  // ========================
769
770
 
770
771
  class SecureDatabaseEncryption {
771
- constructor(config = {}) {
772
+ constructor(config = {}, serializer, base64) {
772
773
  this._iterations = config.iterations || 600000; // Increased to OWASP recommendation
773
774
  this._hashAlgorithm = config.hashAlgorithm || 'SHA-256';
774
775
  this._keyLength = config.keyLength || 256;
775
776
  this._saltLength = config.saltLength || 32;
776
777
  this._initialized = false;
778
+ this._serializer = serializer;
779
+ this._base64 = base64;
777
780
 
778
781
  this._masterKey = null; // The actual key used for data encryption
779
782
  this._hmacKey = null; // Key for HMAC operations
@@ -793,11 +796,11 @@ class SecureDatabaseEncryption {
793
796
 
794
797
  if (existingMetadata) {
795
798
  // Load existing
796
- this._salt = base64.decode(existingMetadata.salt);
799
+ this._salt = this._base64.decode(existingMetadata.salt);
797
800
  const kek = await this._deriveKEK(pinBytes, this._salt);
798
801
 
799
802
  // Unwrap Master Key
800
- const wrappedBytes = base64.decode(existingMetadata.wrappedKey);
803
+ const wrappedBytes = this._base64.decode(existingMetadata.wrappedKey);
801
804
  const iv = wrappedBytes.slice(0, 12);
802
805
  const encryptedMK = wrappedBytes.slice(12);
803
806
 
@@ -828,7 +831,7 @@ class SecureDatabaseEncryption {
828
831
  wrappedKey.set(iv, 0);
829
832
  wrappedKey.set(new Uint8Array(encryptedMK), 12);
830
833
 
831
- this._wrappedKeyBlob = base64.encode(wrappedKey);
834
+ this._wrappedKeyBlob = this._base64.encode(wrappedKey);
832
835
  }
833
836
 
834
837
  this._initialized = true;
@@ -882,7 +885,7 @@ class SecureDatabaseEncryption {
882
885
 
883
886
  // Verify old PIN by attempting to unwrap current master key
884
887
  const oldKek = await this._deriveKEK(new TextEncoder().encode(oldPin), this._salt);
885
- const currentWrappedBytes = base64.decode(this._wrappedKeyBlob);
888
+ const currentWrappedBytes = this._base64.decode(this._wrappedKeyBlob);
886
889
  const currentIv = currentWrappedBytes.slice(0, 12);
887
890
  const currentEncMK = currentWrappedBytes.slice(12);
888
891
  try {
@@ -910,7 +913,7 @@ class SecureDatabaseEncryption {
910
913
 
911
914
  // Update State
912
915
  this._salt = newSalt;
913
- this._wrappedKeyBlob = base64.encode(wrappedKey);
916
+ this._wrappedKeyBlob = this._base64.encode(wrappedKey);
914
917
 
915
918
  return this.exportMetadata();
916
919
  }
@@ -926,7 +929,7 @@ class SecureDatabaseEncryption {
926
929
  } else if (data instanceof Uint8Array) {
927
930
  dataBytes = data;
928
931
  } else {
929
- dataBytes = serializer.serialize(data);
932
+ dataBytes = this._serializer.serialize(data);
930
933
  }
931
934
 
932
935
  const iv = crypto.getRandomValues(new Uint8Array(12));
@@ -1007,7 +1010,7 @@ class SecureDatabaseEncryption {
1007
1010
  } else if (privateKey instanceof Uint8Array) {
1008
1011
  keyData = privateKey;
1009
1012
  } else {
1010
- keyData = serializer.serialize(privateKey);
1013
+ keyData = this._serializer.serialize(privateKey);
1011
1014
  }
1012
1015
 
1013
1016
  const iv = crypto.getRandomValues(new Uint8Array(12));
@@ -1033,7 +1036,7 @@ class SecureDatabaseEncryption {
1033
1036
  result.set(authData, 16);
1034
1037
  result.set(new Uint8Array(encryptedKey), 16 + authData.length);
1035
1038
 
1036
- return base64.encode(result);
1039
+ return this._base64.encode(result);
1037
1040
  }
1038
1041
 
1039
1042
  async decryptPrivateKey(encryptedKeyString, additionalAuth = '') {
@@ -1041,7 +1044,7 @@ class SecureDatabaseEncryption {
1041
1044
  throw new Error('Database encryption not initialized');
1042
1045
  }
1043
1046
 
1044
- const encryptedPackage = base64.decode(encryptedKeyString);
1047
+ const encryptedPackage = this._base64.decode(encryptedKeyString);
1045
1048
 
1046
1049
  const iv = encryptedPackage.slice(0, 12);
1047
1050
  const authLengthBytes = encryptedPackage.slice(12, 16);
@@ -1101,7 +1104,7 @@ class SecureDatabaseEncryption {
1101
1104
 
1102
1105
  exportMetadata() {
1103
1106
  return {
1104
- salt: base64.encode(this._salt),
1107
+ salt: this._base64.encode(this._salt),
1105
1108
  wrappedKey: this._wrappedKeyBlob,
1106
1109
  iterations: this._iterations,
1107
1110
  algorithm: 'AES-GCM-256',
@@ -1220,26 +1223,53 @@ class BTreeNode {
1220
1223
  return this.children[i] ? this.children[i].search(key) : null;
1221
1224
  }
1222
1225
 
1223
- rangeSearch(min, max, results) {
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
+ rangeSearch(min, max, results, excludeMin = false, excludeMax = false) {
1229
+ // Skip keys strictly below the min bound
1224
1230
  let i = 0;
1225
- while (i < this.n) {
1231
+ if (min !== null) {
1232
+ while (i < this.n && this.keys[i] < min) {
1233
+ i++;
1234
+ }
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
+ }
1241
+
1242
+ // Process keys from i onward
1243
+ 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
+ if (max !== null) {
1247
+ const pastMax = excludeMax ? this.keys[i] >= max : this.keys[i] > max;
1248
+ if (pastMax) {
1249
+ if (!this.leaf && this.children[i]) {
1250
+ this.children[i].rangeSearch(min, max, results, excludeMin, excludeMax);
1251
+ }
1252
+ return;
1253
+ }
1254
+ }
1255
+
1256
+ // Descend into left child of current key
1226
1257
  if (!this.leaf && this.children[i]) {
1227
- this.children[i].rangeSearch(min, max, results);
1258
+ this.children[i].rangeSearch(min, max, results, excludeMin, excludeMax);
1228
1259
  }
1229
1260
 
1230
- // Treat null as unbounded (wildcard) to avoid broken string vs Infinity comparisons
1231
- const meetsMin = min === null || this.keys[i] >= min;
1232
- const meetsMax = max === null || this.keys[i] <= max;
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);
1233
1264
 
1234
- if (meetsMin && meetsMax) {
1235
- if (this.values[i]) {
1236
- this.values[i].forEach(v => results.push(v));
1237
- }
1265
+ if (meetsMin && meetsMax && this.values[i]) {
1266
+ this.values[i].forEach(v => results.push(v));
1238
1267
  }
1239
- i++;
1240
1268
  }
1269
+
1270
+ // Descend into rightmost child
1241
1271
  if (!this.leaf && this.children[i]) {
1242
- this.children[i].rangeSearch(min, max, results);
1272
+ this.children[i].rangeSearch(min, max, results, excludeMin, excludeMax);
1243
1273
  }
1244
1274
  }
1245
1275
 
@@ -1279,6 +1309,18 @@ class BTreeNode {
1279
1309
  i++;
1280
1310
  if (this.children[i] && this.children[i].n === 2 * this.order - 1) {
1281
1311
  this.splitChild(i, this.children[i]);
1312
+
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
+ }
1320
+ this.values[i].add(value);
1321
+ return;
1322
+ }
1323
+
1282
1324
  if (this.keys[i] < key) {
1283
1325
  i++;
1284
1326
  }
@@ -1306,6 +1348,17 @@ class BTreeNode {
1306
1348
 
1307
1349
  y.n = this.order - 1;
1308
1350
 
1351
+ // Clean stale slots in y after split
1352
+ for (let j = this.order - 1; j < 2 * this.order - 1; j++) {
1353
+ y.keys[j] = undefined;
1354
+ y.values[j] = undefined;
1355
+ }
1356
+ if (!y.leaf) {
1357
+ for (let j = this.order; j < 2 * this.order; j++) {
1358
+ y.children[j] = undefined;
1359
+ }
1360
+ }
1361
+
1309
1362
  for (let j = this.n; j >= i + 1; j--) {
1310
1363
  this.children[j + 1] = this.children[j];
1311
1364
  }
@@ -1319,31 +1372,267 @@ class BTreeNode {
1319
1372
 
1320
1373
  this.keys[i] = y.keys[this.order - 1];
1321
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;
1322
1378
  this.n++;
1323
1379
  }
1324
1380
 
1325
- remove(key, value) {
1381
+ // ---- Deletion helpers (proper B-tree delete with rebalancing) ----
1382
+
1383
+ // Get the predecessor: rightmost key in the left subtree of keys[idx]
1384
+ _getPredecessor(idx) {
1385
+ let node = this.children[idx];
1386
+ while (!node.leaf) {
1387
+ node = node.children[node.n];
1388
+ }
1389
+ return { key: node.keys[node.n - 1], value: node.values[node.n - 1] };
1390
+ }
1391
+
1392
+ // Get the successor: leftmost key in the right subtree of keys[idx]
1393
+ _getSuccessor(idx) {
1394
+ let node = this.children[idx + 1];
1395
+ while (!node.leaf) {
1396
+ node = node.children[0];
1397
+ }
1398
+ return { key: node.keys[0], value: node.values[0] };
1399
+ }
1400
+
1401
+ // Merge children[idx+1] into children[idx], pulling keys[idx] down as separator
1402
+ _merge(idx) {
1403
+ const child = this.children[idx];
1404
+ const sibling = this.children[idx + 1];
1405
+ const t = this.order;
1406
+
1407
+ // Pull separator key down into child
1408
+ child.keys[t - 1] = this.keys[idx];
1409
+ child.values[t - 1] = this.values[idx];
1410
+
1411
+ // Copy keys/values from sibling into child
1412
+ for (let j = 0; j < sibling.n; j++) {
1413
+ child.keys[t + j] = sibling.keys[j];
1414
+ child.values[t + j] = sibling.values[j];
1415
+ }
1416
+
1417
+ // Copy children from sibling
1418
+ if (!child.leaf) {
1419
+ for (let j = 0; j <= sibling.n; j++) {
1420
+ child.children[t + j] = sibling.children[j];
1421
+ }
1422
+ }
1423
+
1424
+ child.n += sibling.n + 1;
1425
+
1426
+ // Shift keys/values left in this node to fill the gap
1427
+ for (let j = idx; j < this.n - 1; j++) {
1428
+ this.keys[j] = this.keys[j + 1];
1429
+ this.values[j] = this.values[j + 1];
1430
+ }
1431
+
1432
+ // Shift children left in this node
1433
+ for (let j = idx + 1; j < this.n; j++) {
1434
+ this.children[j] = this.children[j + 1];
1435
+ }
1436
+
1437
+ // Clean stale trailing slots
1438
+ this.keys[this.n - 1] = undefined;
1439
+ this.values[this.n - 1] = undefined;
1440
+ this.children[this.n] = undefined;
1441
+
1442
+ this.n--;
1443
+ }
1444
+
1445
+ // Borrow the last key from children[idx-1] through the parent
1446
+ _borrowFromPrev(idx) {
1447
+ const child = this.children[idx];
1448
+ const sibling = this.children[idx - 1];
1449
+
1450
+ // Shift everything in child right by 1
1451
+ for (let j = child.n - 1; j >= 0; j--) {
1452
+ child.keys[j + 1] = child.keys[j];
1453
+ child.values[j + 1] = child.values[j];
1454
+ }
1455
+ if (!child.leaf) {
1456
+ for (let j = child.n; j >= 0; j--) {
1457
+ child.children[j + 1] = child.children[j];
1458
+ }
1459
+ }
1460
+
1461
+ // Move separator from parent down to child[0]
1462
+ child.keys[0] = this.keys[idx - 1];
1463
+ child.values[0] = this.values[idx - 1];
1464
+
1465
+ // Move last child of sibling to child
1466
+ if (!child.leaf) {
1467
+ child.children[0] = sibling.children[sibling.n];
1468
+ sibling.children[sibling.n] = undefined;
1469
+ }
1470
+
1471
+ // Move last key of sibling up to parent
1472
+ this.keys[idx - 1] = sibling.keys[sibling.n - 1];
1473
+ this.values[idx - 1] = sibling.values[sibling.n - 1];
1474
+
1475
+ // Clean stale slots in sibling
1476
+ sibling.keys[sibling.n - 1] = undefined;
1477
+ sibling.values[sibling.n - 1] = undefined;
1478
+
1479
+ child.n++;
1480
+ sibling.n--;
1481
+ }
1482
+
1483
+ // Borrow the first key from children[idx+1] through the parent
1484
+ _borrowFromNext(idx) {
1485
+ const child = this.children[idx];
1486
+ const sibling = this.children[idx + 1];
1487
+
1488
+ // Move separator from parent down to end of child
1489
+ child.keys[child.n] = this.keys[idx];
1490
+ child.values[child.n] = this.values[idx];
1491
+
1492
+ // Move first child of sibling to child
1493
+ if (!child.leaf) {
1494
+ child.children[child.n + 1] = sibling.children[0];
1495
+ }
1496
+
1497
+ // Move first key of sibling up to parent
1498
+ this.keys[idx] = sibling.keys[0];
1499
+ this.values[idx] = sibling.values[0];
1500
+
1501
+ // Shift sibling's keys/values left
1502
+ for (let j = 0; j < sibling.n - 1; j++) {
1503
+ sibling.keys[j] = sibling.keys[j + 1];
1504
+ sibling.values[j] = sibling.values[j + 1];
1505
+ }
1506
+ if (!sibling.leaf) {
1507
+ for (let j = 0; j < sibling.n; j++) {
1508
+ sibling.children[j] = sibling.children[j + 1];
1509
+ }
1510
+ sibling.children[sibling.n] = undefined;
1511
+ }
1512
+
1513
+ // Clean stale trailing slots in sibling
1514
+ sibling.keys[sibling.n - 1] = undefined;
1515
+ sibling.values[sibling.n - 1] = undefined;
1516
+
1517
+ child.n++;
1518
+ sibling.n--;
1519
+ }
1520
+
1521
+ // Ensure children[idx] has at least `order` keys (minimum degree)
1522
+ // so we can safely descend into it during deletion
1523
+ _fill(idx) {
1524
+ const t = this.order;
1525
+ if (idx > 0 && this.children[idx - 1] && this.children[idx - 1].n >= t) {
1526
+ this._borrowFromPrev(idx);
1527
+ } else if (idx < this.n && this.children[idx + 1] && this.children[idx + 1].n >= t) {
1528
+ this._borrowFromNext(idx);
1529
+ } else {
1530
+ // Merge with a sibling
1531
+ if (idx < this.n) {
1532
+ this._merge(idx);
1533
+ } else {
1534
+ this._merge(idx - 1);
1535
+ }
1536
+ }
1537
+ }
1538
+
1539
+ // Remove a leaf-level key entry (shift keys, values left)
1540
+ _removeFromLeaf(idx) {
1541
+ for (let j = idx; j < this.n - 1; j++) {
1542
+ this.keys[j] = this.keys[j + 1];
1543
+ this.values[j] = this.values[j + 1];
1544
+ }
1545
+ this.keys[this.n - 1] = undefined;
1546
+ this.values[this.n - 1] = undefined;
1547
+ this.n--;
1548
+ }
1549
+
1550
+ // Remove an internal key entry using predecessor/successor replacement
1551
+ _removeFromInternal(idx) {
1552
+ const t = this.order;
1553
+ const key = this.keys[idx];
1554
+
1555
+ if (this.children[idx] && this.children[idx].n >= t) {
1556
+ // Left child has enough keys: replace with predecessor
1557
+ const pred = this._getPredecessor(idx);
1558
+ this.keys[idx] = pred.key;
1559
+ this.values[idx] = pred.value;
1560
+ this.children[idx]._remove(pred.key, null, true);
1561
+ } else if (this.children[idx + 1] && this.children[idx + 1].n >= t) {
1562
+ // Right child has enough keys: replace with successor
1563
+ const succ = this._getSuccessor(idx);
1564
+ this.keys[idx] = succ.key;
1565
+ this.values[idx] = succ.value;
1566
+ this.children[idx + 1]._remove(succ.key, null, true);
1567
+ } else {
1568
+ // Both children at minimum: merge them, then delete from merged child
1569
+ this._merge(idx);
1570
+ this.children[idx]._remove(key, null, true);
1571
+ }
1572
+ }
1573
+
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
+ _remove(key, value, removeEntire) {
1326
1579
  let i = 0;
1327
1580
  while (i < this.n && key > this.keys[i]) {
1328
1581
  i++;
1329
1582
  }
1330
1583
 
1331
1584
  if (i < this.n && key === this.keys[i]) {
1332
- if (this.values[i]) {
1585
+ // Key found at this node
1586
+ let shouldRemoveEntry = removeEntire;
1587
+
1588
+ if (!shouldRemoveEntry && this.values[i]) {
1333
1589
  this.values[i].delete(value);
1334
- if (this.values[i].size === 0) {
1335
- for (let j = i; j < this.n - 1; j++) {
1336
- this.keys[j] = this.keys[j + 1];
1337
- this.values[j] = this.values[j + 1];
1338
- }
1339
- this.n--;
1590
+ shouldRemoveEntry = this.values[i].size === 0;
1591
+ }
1592
+
1593
+ if (shouldRemoveEntry) {
1594
+ if (this.leaf) {
1595
+ this._removeFromLeaf(i);
1596
+ } else {
1597
+ this._removeFromInternal(i);
1340
1598
  }
1599
+ return true;
1600
+ }
1601
+ return false;
1602
+ } else {
1603
+ // Key not found at this level — descend
1604
+ if (this.leaf) return false;
1605
+
1606
+ const isLastChild = (i === this.n);
1607
+
1608
+ // Ensure the child we descend into has enough keys for safe deletion
1609
+ if (this.children[i] && this.children[i].n < this.order) {
1610
+ this._fill(i);
1611
+ }
1612
+
1613
+ // After _fill, if the last child was merged, idx shifted
1614
+ if (isLastChild && i > this.n) {
1615
+ return this.children[i - 1]
1616
+ ? this.children[i - 1]._remove(key, value, removeEntire)
1617
+ : false;
1618
+ } else {
1619
+ return this.children[i]
1620
+ ? this.children[i]._remove(key, value, removeEntire)
1621
+ : false;
1341
1622
  }
1342
- } else if (!this.leaf && this.children[i]) {
1343
- this.children[i].remove(key, value);
1344
1623
  }
1345
1624
  }
1346
1625
 
1626
+ // Public remove: remove a single (key, value) pair
1627
+ remove(key, value) {
1628
+ return this._remove(key, value, false);
1629
+ }
1630
+
1631
+ // Public removeKey: remove an entire key entry (used internally for predecessor/successor cleanup)
1632
+ removeKey(key) {
1633
+ return this._remove(key, null, true);
1634
+ }
1635
+
1347
1636
  verify() {
1348
1637
  const issues = [];
1349
1638
  for (let i = 1; i < this.n; i++) {
@@ -1377,6 +1666,14 @@ class BTreeIndex {
1377
1666
  this.verify();
1378
1667
  }
1379
1668
 
1669
+ // Check for exact duplicate (key, value) to keep _size accurate
1670
+ if (this._root) {
1671
+ const existing = this._root.search(key);
1672
+ if (existing && existing.has(value)) {
1673
+ return; // Already present, no-op
1674
+ }
1675
+ }
1676
+
1380
1677
  if (!this._root) {
1381
1678
  this._root = new BTreeNode(this._order, true);
1382
1679
  this._root.keys[0] = key;
@@ -1388,9 +1685,15 @@ class BTreeIndex {
1388
1685
  s.children[0] = this._root;
1389
1686
  s.splitChild(0, this._root);
1390
1687
 
1688
+ // FIX: Check if promoted median equals key
1391
1689
  let i = 0;
1392
- if (s.keys[0] < key) i++;
1393
- s.children[i].insertNonFull(key, value);
1690
+ if (s.keys[0] === key) {
1691
+ if (!s.values[0]) s.values[0] = new Set();
1692
+ s.values[0].add(value);
1693
+ } else {
1694
+ if (s.keys[0] < key) i++;
1695
+ s.children[i].insertNonFull(key, value);
1696
+ }
1394
1697
 
1395
1698
  this._root = s;
1396
1699
  } else {
@@ -1411,24 +1714,24 @@ class BTreeIndex {
1411
1714
  return this._root.search(key) !== null;
1412
1715
  }
1413
1716
 
1414
- range(min, max) {
1717
+ range(min, max, excludeMin = false, excludeMax = false) {
1415
1718
  if (!this._root) return [];
1416
1719
  const results = [];
1417
- this._root.rangeSearch(min, max, results);
1720
+ this._root.rangeSearch(min, max, results, excludeMin, excludeMax);
1418
1721
  return results;
1419
1722
  }
1420
1723
 
1421
- rangeFrom(min) {
1724
+ rangeFrom(min, excludeMin = false) {
1422
1725
  if (!this._root) return [];
1423
1726
  const results = [];
1424
- this._root.rangeSearch(min, null, results);
1727
+ this._root.rangeSearch(min, null, results, excludeMin, false);
1425
1728
  return results;
1426
1729
  }
1427
1730
 
1428
- rangeTo(max) {
1731
+ rangeTo(max, excludeMax = false) {
1429
1732
  if (!this._root) return [];
1430
1733
  const results = [];
1431
- this._root.rangeSearch(null, max, results);
1734
+ this._root.rangeSearch(null, max, results, false, excludeMax);
1432
1735
  return results;
1433
1736
  }
1434
1737
 
@@ -1437,6 +1740,7 @@ class BTreeIndex {
1437
1740
  const existing = this._root.search(key);
1438
1741
  if (existing && existing.has(value)) {
1439
1742
  this._root.remove(key, value);
1743
+ // Shrink root if it became empty (all keys merged down)
1440
1744
  if (this._root.n === 0 && !this._root.leaf && this._root.children[0]) {
1441
1745
  this._root = this._root.children[0];
1442
1746
  }
@@ -1449,15 +1753,22 @@ class BTreeIndex {
1449
1753
  if (!this._root) return { healthy: true, issues: [] };
1450
1754
  const issues = this._root.verify();
1451
1755
  if (issues.length > 0) {
1452
- console.warn('BTree index issues detected and fixed:', issues);
1756
+ // NOTE: verify detects structural violations but cannot auto-repair.
1757
+ // A full rebuild is required to fix a corrupted index.
1758
+ console.warn('BTree index issues detected (rebuild required):', issues);
1453
1759
  }
1454
1760
  return {
1455
1761
  healthy: issues.length === 0,
1456
1762
  issues,
1457
- repaired: issues.length
1763
+ requiresRebuild: issues.length > 0
1458
1764
  };
1459
1765
  }
1460
1766
 
1767
+ clear() {
1768
+ this._root = null;
1769
+ this._size = 0;
1770
+ }
1771
+
1461
1772
  get size() {
1462
1773
  return this._size;
1463
1774
  }
@@ -1641,6 +1952,8 @@ class GeoIndex {
1641
1952
  class IndexManager {
1642
1953
  constructor(collection) {
1643
1954
  this._collection = collection;
1955
+ this._serializer = collection.database._serializer;
1956
+ this._base64 = collection.database._base64;
1644
1957
  this._indexes = new Map();
1645
1958
  this._indexData = new Map();
1646
1959
  this._indexQueue = [];
@@ -1711,7 +2024,7 @@ class IndexManager {
1711
2024
  const d = new Document(docData, {
1712
2025
  compressed: docData._compressed,
1713
2026
  encrypted: docData._encrypted
1714
- });
2027
+ }, this._serializer);
1715
2028
  // This await is what killed the transaction before
1716
2029
  await d.unpack(this._collection.database.encryption);
1717
2030
  doc = d.objectOutput();
@@ -1778,7 +2091,7 @@ class IndexManager {
1778
2091
  async _hashVal(val) {
1779
2092
  const msg = new TextEncoder().encode(String(val));
1780
2093
  const hash = await crypto.subtle.digest('SHA-256', msg);
1781
- return base64.encode(new Uint8Array(hash));
2094
+ return this._base64.encode(new Uint8Array(hash));
1782
2095
  }
1783
2096
 
1784
2097
  async updateIndexForDocument(docId, oldDoc, newDoc) {
@@ -1870,25 +2183,20 @@ class IndexManager {
1870
2183
  docs.forEach(doc => results.add(doc));
1871
2184
  }
1872
2185
 
1873
- if (options.$gte !== undefined && options.$lte !== undefined) {
1874
- const docs = indexData.range(options.$gte, options.$lte);
1875
- docs.forEach(doc => results.add(doc));
1876
- } else if (options.$gte !== undefined) {
1877
- const docs = indexData.rangeFrom(options.$gte);
1878
- docs.forEach(doc => results.add(doc));
1879
- } else if (options.$gt !== undefined) {
1880
- const docs = indexData.rangeFrom(options.$gt);
1881
- docs.forEach(doc => {
1882
- if (doc !== options.$gt) results.add(doc);
1883
- });
1884
- } else if (options.$lte !== undefined) {
1885
- const docs = indexData.rangeTo(options.$lte);
2186
+ // Combined range queries: pick the tightest bounds and correct exclusivity
2187
+ const hasGte = options.$gte !== undefined;
2188
+ const hasGt = options.$gt !== undefined;
2189
+ const hasLte = options.$lte !== undefined;
2190
+ const hasLt = options.$lt !== undefined;
2191
+
2192
+ if (hasGte || hasGt || hasLte || hasLt) {
2193
+ const min = hasGte ? options.$gte : (hasGt ? options.$gt : null);
2194
+ const max = hasLte ? options.$lte : (hasLt ? options.$lt : null);
2195
+ const excludeMin = !hasGte && hasGt;
2196
+ const excludeMax = !hasLte && hasLt;
2197
+
2198
+ const docs = indexData.range(min, max, excludeMin, excludeMax);
1886
2199
  docs.forEach(doc => results.add(doc));
1887
- } else if (options.$lt !== undefined) {
1888
- const docs = indexData.rangeTo(options.$lt);
1889
- docs.forEach(doc => {
1890
- if (doc !== options.$lt) results.add(doc);
1891
- });
1892
2200
  }
1893
2201
 
1894
2202
  return Array.from(results);
@@ -1962,8 +2270,8 @@ class IndexManager {
1962
2270
  ...index
1963
2271
  }))
1964
2272
  };
1965
- const serialized = serializer.serialize(metadata);
1966
- const encoded = base64.encode(serialized);
2273
+ const serialized = this._serializer.serialize(metadata);
2274
+ const encoded = this._base64.encode(serialized);
1967
2275
  localStorage.setItem(key, encoded);
1968
2276
  resolve();
1969
2277
  };
@@ -1982,8 +2290,8 @@ class IndexManager {
1982
2290
  if (!stored) return;
1983
2291
 
1984
2292
  try {
1985
- const decoded = base64.decode(stored);
1986
- const metadata = serializer.deserialize(decoded);
2293
+ const decoded = this._base64.decode(stored);
2294
+ const metadata = this._serializer.deserialize(decoded);
1987
2295
 
1988
2296
  for (const indexDef of metadata.indexes) {
1989
2297
  const { name, ...index } = indexDef;
@@ -2026,7 +2334,13 @@ class IndexManager {
2026
2334
  report[name] = { status: 'missing', rebuilt: true };
2027
2335
  await this.rebuildIndex(name);
2028
2336
  } else if (indexData.verify) {
2029
- report[name] = indexData.verify();
2337
+ const result = indexData.verify();
2338
+ if (result.requiresRebuild) {
2339
+ // BTree detected structural issues — rebuild the index from source data
2340
+ await this.rebuildIndex(name);
2341
+ result.rebuilt = true;
2342
+ }
2343
+ report[name] = result;
2030
2344
  } else {
2031
2345
  report[name] = { status: 'ok' };
2032
2346
  }
@@ -2310,7 +2624,7 @@ class IndexedDBUtility {
2310
2624
  // ========================
2311
2625
 
2312
2626
  class Document {
2313
- constructor(data = {}, options = {}) {
2627
+ constructor(data = {}, options = {}, serializer) {
2314
2628
  this._id = data._id || this._generateId();
2315
2629
  this._created = data._created || Date.now();
2316
2630
  this._modified = data._modified || Date.now();
@@ -2321,6 +2635,7 @@ class Document {
2321
2635
  this._data = null;
2322
2636
  this._packedData = data.packedData || null;
2323
2637
  this._compression = new BrowserCompressionUtility();
2638
+ this._serializer = serializer;
2324
2639
 
2325
2640
  if (data.data) {
2326
2641
  this.data = data.data;
@@ -2341,7 +2656,7 @@ class Document {
2341
2656
 
2342
2657
  async pack(encryptionUtil = null) {
2343
2658
  try {
2344
- let packed = serializer.serialize(this.data);
2659
+ let packed = this._serializer.serialize(this.data);
2345
2660
  if (this._compressed) {
2346
2661
  packed = await this._compression.compress(packed);
2347
2662
  }
@@ -2370,7 +2685,7 @@ class Document {
2370
2685
  throw new Error('Empty unpacked data');
2371
2686
  }
2372
2687
 
2373
- this.data = serializer.deserialize(unpacked);
2688
+ this.data = this._serializer.deserialize(unpacked);
2374
2689
 
2375
2690
  if (typeof this.data !== 'object' || this.data === null) {
2376
2691
  throw new Error('Invalid deserialized data');
@@ -2385,7 +2700,7 @@ class Document {
2385
2700
  }
2386
2701
 
2387
2702
  packSync() {
2388
- let packed = serializer.serialize(this.data);
2703
+ let packed = this._serializer.serialize(this.data);
2389
2704
  if (this._compressed) {
2390
2705
  packed = this._compression.compressSync(packed);
2391
2706
  }
@@ -2401,7 +2716,7 @@ class Document {
2401
2716
  if (this._compressed) {
2402
2717
  unpacked = this._compression.decompressSync(unpacked);
2403
2718
  }
2404
- this.data = serializer.deserialize(unpacked);
2719
+ this.data = this._serializer.deserialize(unpacked);
2405
2720
  return this.data;
2406
2721
  }
2407
2722
 
@@ -2481,27 +2796,29 @@ class CollectionMetadata {
2481
2796
  }
2482
2797
 
2483
2798
  class DatabaseMetadata {
2484
- constructor(name, data = {}) {
2799
+ constructor(name, data = {}, serializer, base64) {
2485
2800
  this.name = name;
2801
+ this._serializer = serializer;
2802
+ this._base64 = base64;
2486
2803
  this.collections = data.collections || {};
2487
2804
  this.totalSizeKB = data.totalSizeKB || 0;
2488
2805
  this.totalLength = data.totalLength || 0;
2489
2806
  this.modifiedAt = data.modifiedAt || Date.now();
2490
2807
  }
2491
2808
 
2492
- static load(dbName) {
2809
+ static load(dbName, serializer, base64) {
2493
2810
  const key = `lacertadb_${dbName}_metadata`;
2494
2811
  const stored = localStorage.getItem(key);
2495
2812
  if (stored) {
2496
2813
  try {
2497
2814
  const decoded = base64.decode(stored);
2498
2815
  const data = serializer.deserialize(decoded);
2499
- return new DatabaseMetadata(dbName, data);
2816
+ return new DatabaseMetadata(dbName, data, serializer, base64);
2500
2817
  } catch (e) {
2501
2818
  console.error('Failed to load metadata:', e);
2502
2819
  }
2503
2820
  }
2504
- return new DatabaseMetadata(dbName);
2821
+ return new DatabaseMetadata(dbName, {}, serializer, base64);
2505
2822
  }
2506
2823
 
2507
2824
  save() {
@@ -2513,8 +2830,8 @@ class DatabaseMetadata {
2513
2830
  totalLength: this.totalLength,
2514
2831
  modifiedAt: this.modifiedAt
2515
2832
  };
2516
- const serializedData = serializer.serialize(dataToStore);
2517
- const encodedData = base64.encode(serializedData);
2833
+ const serializedData = this._serializer.serialize(dataToStore);
2834
+ const encodedData = this._base64.encode(serializedData);
2518
2835
  localStorage.setItem(key, encodedData);
2519
2836
  } catch (e) {
2520
2837
  if (e.name === 'QuotaExceededError') {
@@ -2558,27 +2875,29 @@ class DatabaseMetadata {
2558
2875
  }
2559
2876
 
2560
2877
  class Settings {
2561
- constructor(dbName, data = {}) {
2878
+ constructor(dbName, data = {}, serializer, base64) {
2562
2879
  this.dbName = dbName;
2880
+ this._serializer = serializer;
2881
+ this._base64 = base64;
2563
2882
  this.sizeLimitKB = data.sizeLimitKB != null ? data.sizeLimitKB : Infinity;
2564
2883
  const defaultBuffer = this.sizeLimitKB === Infinity ? Infinity : this.sizeLimitKB * 0.8;
2565
2884
  this.bufferLimitKB = data.bufferLimitKB != null ? data.bufferLimitKB : defaultBuffer;
2566
2885
  this.freeSpaceEvery = this.sizeLimitKB === Infinity ? 0 : (data.freeSpaceEvery || 10000);
2567
2886
  }
2568
2887
 
2569
- static load(dbName) {
2888
+ static load(dbName, serializer, base64) {
2570
2889
  const key = `lacertadb_${dbName}_settings`;
2571
2890
  const stored = localStorage.getItem(key);
2572
2891
  if (stored) {
2573
2892
  try {
2574
2893
  const decoded = base64.decode(stored);
2575
2894
  const data = serializer.deserialize(decoded);
2576
- return new Settings(dbName, data);
2895
+ return new Settings(dbName, data, serializer, base64);
2577
2896
  } catch (e) {
2578
2897
  console.error('Failed to load settings:', e);
2579
2898
  }
2580
2899
  }
2581
- return new Settings(dbName);
2900
+ return new Settings(dbName, {}, serializer, base64);
2582
2901
  }
2583
2902
 
2584
2903
  save() {
@@ -2589,8 +2908,8 @@ class Settings {
2589
2908
  bufferLimitKB: this.bufferLimitKB,
2590
2909
  freeSpaceEvery: this.freeSpaceEvery
2591
2910
  };
2592
- const serializedData = serializer.serialize(dataToStore);
2593
- const encodedData = base64.encode(serializedData);
2911
+ const serializedData = this._serializer.serialize(dataToStore);
2912
+ const encodedData = this._base64.encode(serializedData);
2594
2913
  localStorage.setItem(key, encodedData);
2595
2914
  } catch (e) {
2596
2915
  if (e.name === 'QuotaExceededError') {
@@ -3039,6 +3358,8 @@ class Collection {
3039
3358
  constructor(name, database) {
3040
3359
  this.name = name;
3041
3360
  this.database = database;
3361
+ this._serializer = database._serializer;
3362
+ this._base64 = database._base64;
3042
3363
  this._db = null;
3043
3364
  this._metadata = null;
3044
3365
  this._settings = database.settings;
@@ -3131,7 +3452,7 @@ class Collection {
3131
3452
  const doc = new Document({data: documentData, _id: options.id}, {
3132
3453
  compressed: options.compressed !== false,
3133
3454
  permanent: options.permanent || false
3134
- });
3455
+ }, this._serializer);
3135
3456
 
3136
3457
  const attachments = options.attachments;
3137
3458
  if (attachments && attachments.length > 0) {
@@ -3173,7 +3494,7 @@ class Collection {
3173
3494
  const doc = new Document(stored, {
3174
3495
  encrypted: stored._encrypted,
3175
3496
  compressed: stored._compressed
3176
- });
3497
+ }, this._serializer);
3177
3498
 
3178
3499
  if (stored.packedData) {
3179
3500
  await doc.unpack(this.database.encryption);
@@ -3196,7 +3517,7 @@ class Collection {
3196
3517
  const doc = new Document(docData, {
3197
3518
  encrypted: docData._encrypted,
3198
3519
  compressed: docData._compressed
3199
- });
3520
+ }, this._serializer);
3200
3521
  if (docData.packedData) {
3201
3522
  await doc.unpack(this.database.encryption);
3202
3523
  }
@@ -3218,7 +3539,7 @@ class Collection {
3218
3539
  throw new LacertaDBError(`Document with id '${docId}' not found for update.`, 'DOCUMENT_NOT_FOUND');
3219
3540
  }
3220
3541
 
3221
- const existingDoc = new Document(stored);
3542
+ const existingDoc = new Document(stored, {}, this._serializer);
3222
3543
  if (stored.packedData) await existingDoc.unpack(this.database.encryption);
3223
3544
 
3224
3545
  const oldDocOutput = existingDoc.objectOutput();
@@ -3231,7 +3552,7 @@ class Collection {
3231
3552
  }, {
3232
3553
  compressed: options.compressed !== undefined ? options.compressed : stored._compressed,
3233
3554
  permanent: options.permanent !== undefined ? options.permanent : stored._permanent
3234
- });
3555
+ }, this._serializer);
3235
3556
  doc._modified = Date.now();
3236
3557
 
3237
3558
  const attachments = options.attachments;
@@ -3306,7 +3627,7 @@ class Collection {
3306
3627
 
3307
3628
  const startTime = performance.now();
3308
3629
 
3309
- const cacheKey = base64.encode(serializer.serialize({filter, options}));
3630
+ const cacheKey = this._base64.encode(this._serializer.serialize({filter, options}));
3310
3631
  const cached = this._cacheStrategy.get(cacheKey);
3311
3632
 
3312
3633
  if (cached) {
@@ -3376,7 +3697,7 @@ class Collection {
3376
3697
  const doc = new Document({data: documentData}, {
3377
3698
  compressed: options.compressed !== false,
3378
3699
  permanent: options.permanent || false
3379
- });
3700
+ }, this._serializer);
3380
3701
 
3381
3702
  await doc.pack(this.database.encryption);
3382
3703
  operations.push({
@@ -3427,7 +3748,7 @@ class Collection {
3427
3748
  continue;
3428
3749
  }
3429
3750
 
3430
- const existingDoc = new Document(stored);
3751
+ const existingDoc = new Document(stored, {}, this._serializer);
3431
3752
  if (stored.packedData) await existingDoc.unpack(this.database.encryption);
3432
3753
 
3433
3754
  oldDocs.push(existingDoc.objectOutput());
@@ -3440,7 +3761,7 @@ class Collection {
3440
3761
  }, {
3441
3762
  compressed: options.compressed !== undefined ? options.compressed : stored._compressed,
3442
3763
  permanent: options.permanent !== undefined ? options.permanent : stored._permanent
3443
- });
3764
+ }, this._serializer);
3444
3765
  doc._modified = Date.now();
3445
3766
  doc._attachments = stored._attachments;
3446
3767
 
@@ -3652,13 +3973,15 @@ class Collection {
3652
3973
  // ========================
3653
3974
 
3654
3975
  class Database {
3655
- constructor(name, performanceMonitor) {
3976
+ constructor(name, performanceMonitor, serializer, base64) {
3656
3977
  this.name = name;
3657
3978
  this._collections = new Map();
3658
3979
  this._metadata = null;
3659
3980
  this._settings = null;
3660
3981
  this._quickStore = null;
3661
3982
  this._performanceMonitor = performanceMonitor;
3983
+ this._serializer = serializer;
3984
+ this._base64 = base64;
3662
3985
 
3663
3986
  // Database-level encryption
3664
3987
  this._encryption = null;
@@ -3693,9 +4016,9 @@ class Database {
3693
4016
  }
3694
4017
 
3695
4018
  async init(options = {}) {
3696
- this._metadata = DatabaseMetadata.load(this.name);
3697
- this._settings = Settings.load(this.name);
3698
- this._quickStore = new QuickStore(this.name);
4019
+ this._metadata = DatabaseMetadata.load(this.name, this._serializer, this._base64);
4020
+ this._settings = Settings.load(this.name, this._serializer, this._base64);
4021
+ this._quickStore = new QuickStore(this.name, this._serializer, this._base64);
3699
4022
 
3700
4023
  if (options.pin) {
3701
4024
  await this._initializeEncryption(options.pin, options.salt, options.encryptionConfig);
@@ -3710,16 +4033,16 @@ class Database {
3710
4033
  const stored = localStorage.getItem(encMetaKey);
3711
4034
 
3712
4035
  if (stored) {
3713
- const decoded = base64.decode(stored);
3714
- existingMetadata = serializer.deserialize(decoded);
4036
+ const decoded = this._base64.decode(stored);
4037
+ existingMetadata = this._serializer.deserialize(decoded);
3715
4038
  }
3716
4039
 
3717
- this._encryption = new SecureDatabaseEncryption(config);
4040
+ this._encryption = new SecureDatabaseEncryption(config, this._serializer, this._base64);
3718
4041
  const newMeta = await this._encryption.initialize(pin, existingMetadata);
3719
4042
 
3720
4043
  if (!existingMetadata) {
3721
- const serialized = serializer.serialize(newMeta);
3722
- const encoded = base64.encode(serialized);
4044
+ const serialized = this._serializer.serialize(newMeta);
4045
+ const encoded = this._base64.encode(serialized);
3723
4046
  localStorage.setItem(encMetaKey, encoded);
3724
4047
  }
3725
4048
  }
@@ -3732,8 +4055,8 @@ class Database {
3732
4055
  const newMeta = await this._encryption.changePin(oldPin, newPin);
3733
4056
 
3734
4057
  const encMetaKey = `lacertadb_${this.name}_encryption`;
3735
- const serialized = serializer.serialize(newMeta);
3736
- const encoded = base64.encode(serialized);
4058
+ const serialized = this._serializer.serialize(newMeta);
4059
+ const encoded = this._base64.encode(serialized);
3737
4060
  localStorage.setItem(encMetaKey, encoded);
3738
4061
 
3739
4062
  return true;
@@ -3859,7 +4182,7 @@ class Database {
3859
4182
 
3860
4183
  async export(format = 'json', password = null) {
3861
4184
  const data = {
3862
- version: '0.9.2',
4185
+ version: '0.10.2',
3863
4186
  database: this.name,
3864
4187
  timestamp: Date.now(),
3865
4188
  collections: {}
@@ -3871,14 +4194,14 @@ class Database {
3871
4194
  }
3872
4195
 
3873
4196
  if (format === 'json') {
3874
- const serialized = serializer.serialize(data);
3875
- return base64.encode(serialized);
4197
+ const serialized = this._serializer.serialize(data);
4198
+ return this._base64.encode(serialized);
3876
4199
  }
3877
4200
  if (format === 'encrypted' && password) {
3878
4201
  const encryption = new BrowserEncryptionUtility();
3879
- const serializedData = serializer.serialize(data);
4202
+ const serializedData = this._serializer.serialize(data);
3880
4203
  const encrypted = await encryption.encrypt(serializedData, password);
3881
- return base64.encode(encrypted);
4204
+ return this._base64.encode(encrypted);
3882
4205
  }
3883
4206
  throw new LacertaDBError(`Unsupported export format: ${format}`, 'INVALID_FORMAT');
3884
4207
  }
@@ -3886,13 +4209,13 @@ class Database {
3886
4209
  async import(data, format = 'json', password = null) {
3887
4210
  let parsed;
3888
4211
  try {
3889
- const decoded = base64.decode(data);
4212
+ const decoded = this._base64.decode(data);
3890
4213
  if (format === 'encrypted' && password) {
3891
4214
  const encryption = new BrowserEncryptionUtility();
3892
4215
  const decrypted = await encryption.decrypt(decoded, password);
3893
- parsed = serializer.deserialize(decrypted);
4216
+ parsed = this._serializer.deserialize(decrypted);
3894
4217
  } else {
3895
- parsed = serializer.deserialize(decoded);
4218
+ parsed = this._serializer.deserialize(decoded);
3896
4219
  }
3897
4220
  } catch (e) {
3898
4221
  throw new LacertaDBError('Failed to parse import data', 'IMPORT_PARSE_FAILED', e);
@@ -3923,7 +4246,7 @@ class Database {
3923
4246
  async clearAll() {
3924
4247
  await Promise.all([...this._collections.keys()].map(name => this.dropCollection(name)));
3925
4248
  this._collections.clear();
3926
- this._metadata = new DatabaseMetadata(this.name);
4249
+ this._metadata = new DatabaseMetadata(this.name, {}, this._serializer, this._base64);
3927
4250
  this._metadata.save();
3928
4251
  this._quickStore.clear();
3929
4252
  }
@@ -3961,18 +4284,31 @@ class Database {
3961
4284
  // ========================
3962
4285
 
3963
4286
  class LacertaDB {
3964
- constructor() {
4287
+ constructor(config = {}) {
3965
4288
  this._databases = new Map();
3966
4289
  this._performanceMonitor = new PerformanceMonitor();
4290
+
4291
+ // Instantiate serializer with user-overridable config merged over defaults
4292
+ const serialConfig = { ...TURBO_SERIAL_DEFAULTS, ...(config.turboSerial || {}) };
4293
+ this._serializer = new TurboSerial(serialConfig);
4294
+ this._base64 = new TurboBase64();
3967
4295
  }
3968
4296
 
3969
4297
  get performanceMonitor() {
3970
4298
  return this._performanceMonitor;
3971
4299
  }
3972
4300
 
4301
+ get serializer() {
4302
+ return this._serializer;
4303
+ }
4304
+
4305
+ get base64() {
4306
+ return this._base64;
4307
+ }
4308
+
3973
4309
  async getDatabase(name, options = {}) {
3974
4310
  if (!this._databases.has(name)) {
3975
- const db = new Database(name, this._performanceMonitor);
4311
+ const db = new Database(name, this._performanceMonitor, this._serializer, this._base64);
3976
4312
  await db.init(options);
3977
4313
  this._databases.set(name, db);
3978
4314
  }
@@ -3996,7 +4332,7 @@ class LacertaDB {
3996
4332
  });
3997
4333
 
3998
4334
  // Clean up quickstore
3999
- const quickStore = new QuickStore(name);
4335
+ const quickStore = new QuickStore(name, this._serializer, this._base64);
4000
4336
  quickStore.clear();
4001
4337
 
4002
4338
  // Clean up all collections and indexes
@@ -4026,7 +4362,7 @@ class LacertaDB {
4026
4362
 
4027
4363
  async createBackup(password = null) {
4028
4364
  const backup = {
4029
- version: '0.9.2',
4365
+ version: '0.10.2',
4030
4366
  timestamp: Date.now(),
4031
4367
  databases: {}
4032
4368
  };
@@ -4034,29 +4370,29 @@ class LacertaDB {
4034
4370
  for (const dbName of this.listDatabases()) {
4035
4371
  const db = await this.getDatabase(dbName);
4036
4372
  const exported = await db.export('json');
4037
- const decoded = base64.decode(exported);
4038
- backup.databases[dbName] = serializer.deserialize(decoded);
4373
+ const decoded = this._base64.decode(exported);
4374
+ backup.databases[dbName] = this._serializer.deserialize(decoded);
4039
4375
  }
4040
4376
 
4041
- const serializedBackup = serializer.serialize(backup);
4377
+ const serializedBackup = this._serializer.serialize(backup);
4042
4378
  if (password) {
4043
4379
  const encryption = new BrowserEncryptionUtility();
4044
4380
  const encrypted = await encryption.encrypt(serializedBackup, password);
4045
- return base64.encode(encrypted);
4381
+ return this._base64.encode(encrypted);
4046
4382
  }
4047
- return base64.encode(serializedBackup);
4383
+ return this._base64.encode(serializedBackup);
4048
4384
  }
4049
4385
 
4050
4386
  async restoreBackup(backupData, password = null) {
4051
4387
  let backup;
4052
4388
  try {
4053
- let decodedData = base64.decode(backupData);
4389
+ let decodedData = this._base64.decode(backupData);
4054
4390
  if (password) {
4055
4391
  const encryption = new BrowserEncryptionUtility();
4056
4392
  const decrypted = await encryption.decrypt(decodedData, password);
4057
- backup = serializer.deserialize(decrypted);
4393
+ backup = this._serializer.deserialize(decrypted);
4058
4394
  } else {
4059
- backup = serializer.deserialize(decodedData);
4395
+ backup = this._serializer.deserialize(decodedData);
4060
4396
  }
4061
4397
  } catch (e) {
4062
4398
  throw new LacertaDBError('Failed to parse backup data', 'BACKUP_PARSE_FAILED', e);
@@ -4065,7 +4401,7 @@ class LacertaDB {
4065
4401
  const results = { databases: 0, collections: 0, documents: 0 };
4066
4402
  for (const [dbName, dbData] of Object.entries(backup.databases)) {
4067
4403
  const db = await this.getDatabase(dbName);
4068
- const encodedDbData = base64.encode(serializer.serialize(dbData));
4404
+ const encodedDbData = this._base64.encode(this._serializer.serialize(dbData));
4069
4405
  const importResult = await db.import(encodedDbData);
4070
4406
 
4071
4407
  results.databases++;