@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/dist/browser.min.js +4 -4
- package/dist/index.min.js +4 -4
- package/index.js +490 -154
- package/package.json +1 -1
- package/readme.md +1 -1
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LacertaDB V0.
|
|
2
|
+
* LacertaDB V0.11.0 - Production Library
|
|
3
3
|
* @module LacertaDB
|
|
4
|
-
* @version 0.
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 =
|
|
95
|
-
const list =
|
|
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 =
|
|
114
|
-
const encodedIndex =
|
|
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 =
|
|
143
|
-
const encodedIndex =
|
|
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 =
|
|
163
|
-
const encodedData =
|
|
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 =
|
|
186
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
this.
|
|
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]
|
|
1393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
const
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
const docs = indexData.
|
|
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 =
|
|
1966
|
-
const encoded =
|
|
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 =
|
|
1986
|
-
const metadata =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2517
|
-
const encodedData =
|
|
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 =
|
|
2593
|
-
const encodedData =
|
|
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 =
|
|
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 =
|
|
3714
|
-
existingMetadata =
|
|
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 =
|
|
3722
|
-
const encoded =
|
|
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 =
|
|
3736
|
-
const encoded =
|
|
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.
|
|
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 =
|
|
3875
|
-
return
|
|
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 =
|
|
4202
|
+
const serializedData = this._serializer.serialize(data);
|
|
3880
4203
|
const encrypted = await encryption.encrypt(serializedData, password);
|
|
3881
|
-
return
|
|
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 =
|
|
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 =
|
|
4216
|
+
parsed = this._serializer.deserialize(decrypted);
|
|
3894
4217
|
} else {
|
|
3895
|
-
parsed =
|
|
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.
|
|
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 =
|
|
4038
|
-
backup.databases[dbName] =
|
|
4373
|
+
const decoded = this._base64.decode(exported);
|
|
4374
|
+
backup.databases[dbName] = this._serializer.deserialize(decoded);
|
|
4039
4375
|
}
|
|
4040
4376
|
|
|
4041
|
-
const serializedBackup =
|
|
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
|
|
4381
|
+
return this._base64.encode(encrypted);
|
|
4046
4382
|
}
|
|
4047
|
-
return
|
|
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 =
|
|
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 =
|
|
4393
|
+
backup = this._serializer.deserialize(decrypted);
|
|
4058
4394
|
} else {
|
|
4059
|
-
backup =
|
|
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 =
|
|
4404
|
+
const encodedDbData = this._base64.encode(this._serializer.serialize(dbData));
|
|
4069
4405
|
const importResult = await db.import(encodedDbData);
|
|
4070
4406
|
|
|
4071
4407
|
results.databases++;
|