@pixagram/lacerta-db 0.10.2 → 0.11.1
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 +828 -240
- 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.1 - Production Library
|
|
3
3
|
* @module LacertaDB
|
|
4
|
-
* @version 0.
|
|
4
|
+
* @version 0.11.1
|
|
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: true,
|
|
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,15 +48,17 @@ 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
|
+
|
|
57
58
|
// Optimization: Keep index in memory using a Set for O(1) lookups
|
|
58
59
|
this._indexCache = new Set();
|
|
59
60
|
this._indexLoaded = false;
|
|
60
|
-
|
|
61
|
+
|
|
61
62
|
// Async persistence state
|
|
62
63
|
this._saveIndexTimer = null;
|
|
63
64
|
this._dirty = false;
|
|
@@ -87,12 +88,12 @@ class QuickStore {
|
|
|
87
88
|
|
|
88
89
|
_ensureIndexLoaded() {
|
|
89
90
|
if (this._indexLoaded) return;
|
|
90
|
-
|
|
91
|
+
|
|
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
|
}
|
|
@@ -567,7 +568,7 @@ class CacheStrategy {
|
|
|
567
568
|
const type = this._config.type || 'lru';
|
|
568
569
|
const max = this._config.maxSize || 100;
|
|
569
570
|
const ttl = this._config.ttl;
|
|
570
|
-
|
|
571
|
+
|
|
571
572
|
if (type === 'none' || this._config.enabled === false) return null;
|
|
572
573
|
if (type === 'ttl') return new TTLCache(ttl);
|
|
573
574
|
if (type === 'lfu') return new LFUCache(max, ttl);
|
|
@@ -614,9 +615,9 @@ class CacheStrategy {
|
|
|
614
615
|
// ========================
|
|
615
616
|
|
|
616
617
|
class BrowserCompressionUtility {
|
|
617
|
-
// Magic header to distinguish compressed data.
|
|
618
|
+
// Magic header to distinguish compressed data.
|
|
618
619
|
// 0x01 = Compressed (Deflate), 0x00 = Raw
|
|
619
|
-
|
|
620
|
+
|
|
620
621
|
async compress(input) {
|
|
621
622
|
if (!(input instanceof Uint8Array)) {
|
|
622
623
|
throw new TypeError('Input must be Uint8Array');
|
|
@@ -642,7 +643,7 @@ class BrowserCompressionUtility {
|
|
|
642
643
|
throw new TypeError('Input must be Uint8Array');
|
|
643
644
|
}
|
|
644
645
|
if (input.length === 0) return input;
|
|
645
|
-
|
|
646
|
+
|
|
646
647
|
const marker = input[0];
|
|
647
648
|
const data = input.slice(1);
|
|
648
649
|
|
|
@@ -768,13 +769,15 @@ 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;
|
|
777
|
-
|
|
778
|
+
this._serializer = serializer;
|
|
779
|
+
this._base64 = base64;
|
|
780
|
+
|
|
778
781
|
this._masterKey = null; // The actual key used for data encryption
|
|
779
782
|
this._hmacKey = null; // Key for HMAC operations
|
|
780
783
|
this._salt = null; // Salt for KEK derivation
|
|
@@ -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
|
|
|
@@ -813,22 +816,22 @@ class SecureDatabaseEncryption {
|
|
|
813
816
|
// New Database
|
|
814
817
|
this._salt = crypto.getRandomValues(new Uint8Array(this._saltLength));
|
|
815
818
|
const kek = await this._deriveKEK(pinBytes, this._salt);
|
|
816
|
-
|
|
819
|
+
|
|
817
820
|
// Generate Master Keys (64 bytes: 32 enc + 32 hmac)
|
|
818
821
|
const rawKeys = crypto.getRandomValues(new Uint8Array(64));
|
|
819
822
|
await this._importMasterKeys(rawKeys.buffer);
|
|
820
|
-
|
|
823
|
+
|
|
821
824
|
// Wrap Master Key
|
|
822
825
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
823
826
|
const encryptedMK = await crypto.subtle.encrypt(
|
|
824
827
|
{ name: 'AES-GCM', iv }, kek, rawKeys
|
|
825
828
|
);
|
|
826
|
-
|
|
829
|
+
|
|
827
830
|
const wrappedKey = new Uint8Array(12 + encryptedMK.byteLength);
|
|
828
831
|
wrappedKey.set(iv, 0);
|
|
829
832
|
wrappedKey.set(new Uint8Array(encryptedMK), 12);
|
|
830
|
-
|
|
831
|
-
this._wrappedKeyBlob =
|
|
833
|
+
|
|
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,8 +913,8 @@ class SecureDatabaseEncryption {
|
|
|
910
913
|
|
|
911
914
|
// Update State
|
|
912
915
|
this._salt = newSalt;
|
|
913
|
-
this._wrappedKeyBlob =
|
|
914
|
-
|
|
916
|
+
this._wrappedKeyBlob = this._base64.encode(wrappedKey);
|
|
917
|
+
|
|
915
918
|
return this.exportMetadata();
|
|
916
919
|
}
|
|
917
920
|
|
|
@@ -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',
|
|
@@ -1138,7 +1141,7 @@ class QuadTree {
|
|
|
1138
1141
|
if (!this.divided) this._subdivide();
|
|
1139
1142
|
|
|
1140
1143
|
return (this.northeast.insert(point) || this.northwest.insert(point) ||
|
|
1141
|
-
|
|
1144
|
+
this.southeast.insert(point) || this.southwest.insert(point));
|
|
1142
1145
|
}
|
|
1143
1146
|
|
|
1144
1147
|
query(range, found = []) { // range: {x, y, w, h}
|
|
@@ -1180,12 +1183,12 @@ class QuadTree {
|
|
|
1180
1183
|
|
|
1181
1184
|
_contains(b, p) {
|
|
1182
1185
|
return (p.x >= b.x - b.w && p.x <= b.x + b.w &&
|
|
1183
|
-
|
|
1186
|
+
p.y >= b.y - b.h && p.y <= b.y + b.h);
|
|
1184
1187
|
}
|
|
1185
1188
|
|
|
1186
1189
|
_intersects(a, b) {
|
|
1187
1190
|
return !(b.x - b.w > a.x + a.w || b.x + b.w < a.x - a.w ||
|
|
1188
|
-
|
|
1191
|
+
b.y - b.h > a.y + a.h || b.y + b.h < a.y - a.h);
|
|
1189
1192
|
}
|
|
1190
1193
|
}
|
|
1191
1194
|
|
|
@@ -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
|
}
|
|
@@ -1471,7 +1782,7 @@ class TextIndex {
|
|
|
1471
1782
|
constructor() {
|
|
1472
1783
|
this._invertedIndex = new Map();
|
|
1473
1784
|
this._docTokens = new Map();
|
|
1474
|
-
this._segmenter = typeof Intl !== 'undefined' && Intl.Segmenter ?
|
|
1785
|
+
this._segmenter = typeof Intl !== 'undefined' && Intl.Segmenter ?
|
|
1475
1786
|
new Intl.Segmenter(undefined, { granularity: 'word' }) : null;
|
|
1476
1787
|
}
|
|
1477
1788
|
|
|
@@ -1557,7 +1868,7 @@ class TextIndex {
|
|
|
1557
1868
|
|
|
1558
1869
|
class GeoIndex {
|
|
1559
1870
|
constructor() {
|
|
1560
|
-
this._tree = new QuadTree({x: 0, y: 0, w: 180, h: 90});
|
|
1871
|
+
this._tree = new QuadTree({x: 0, y: 0, w: 180, h: 90});
|
|
1561
1872
|
this._size = 0;
|
|
1562
1873
|
}
|
|
1563
1874
|
|
|
@@ -1580,15 +1891,15 @@ class GeoIndex {
|
|
|
1580
1891
|
}
|
|
1581
1892
|
|
|
1582
1893
|
findNear(center, maxDistance) {
|
|
1583
|
-
const rangeDeg = maxDistance / 111;
|
|
1894
|
+
const rangeDeg = maxDistance / 111;
|
|
1584
1895
|
const range = {
|
|
1585
|
-
x: center.lng, y: center.lat,
|
|
1896
|
+
x: center.lng, y: center.lat,
|
|
1586
1897
|
w: rangeDeg, h: rangeDeg
|
|
1587
1898
|
};
|
|
1588
|
-
|
|
1899
|
+
|
|
1589
1900
|
const candidates = this._tree.query(range);
|
|
1590
1901
|
const results = [];
|
|
1591
|
-
|
|
1902
|
+
|
|
1592
1903
|
for (const p of candidates) {
|
|
1593
1904
|
const distance = this._haversine(center, {lat: p.y, lng: p.x});
|
|
1594
1905
|
if (distance <= maxDistance) {
|
|
@@ -1607,7 +1918,7 @@ class GeoIndex {
|
|
|
1607
1918
|
const h = (bounds.maxLat - bounds.minLat) / 2;
|
|
1608
1919
|
const x = bounds.minLng + w;
|
|
1609
1920
|
const y = bounds.minLat + h;
|
|
1610
|
-
|
|
1921
|
+
|
|
1611
1922
|
const candidates = this._tree.query({x, y, w, h});
|
|
1612
1923
|
return candidates.map(p => p.data);
|
|
1613
1924
|
}
|
|
@@ -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 = [];
|
|
@@ -1690,30 +2003,30 @@ class IndexManager {
|
|
|
1690
2003
|
// This prevents transaction timeouts caused by async crypto operations inside the loop
|
|
1691
2004
|
let lastKey = null;
|
|
1692
2005
|
const batchSize = 100; // Keep batch small for responsiveness
|
|
1693
|
-
|
|
2006
|
+
|
|
1694
2007
|
while (true) {
|
|
1695
2008
|
// 1. Fetch Batch (Transaction opens and closes here)
|
|
1696
2009
|
const batch = await this._collection._indexedDB.getBatch(
|
|
1697
|
-
this._collection._db,
|
|
1698
|
-
'documents',
|
|
1699
|
-
lastKey,
|
|
2010
|
+
this._collection._db,
|
|
2011
|
+
'documents',
|
|
2012
|
+
lastKey,
|
|
1700
2013
|
batchSize
|
|
1701
2014
|
);
|
|
1702
|
-
|
|
2015
|
+
|
|
1703
2016
|
if (batch.length === 0) break;
|
|
1704
|
-
|
|
2017
|
+
|
|
1705
2018
|
// 2. Process Batch (Async crypto operations safe here)
|
|
1706
2019
|
for (const docData of batch) {
|
|
1707
2020
|
lastKey = docData._id; // Update for next batch
|
|
1708
2021
|
let doc = docData;
|
|
1709
|
-
|
|
2022
|
+
|
|
1710
2023
|
if (docData.packedData) {
|
|
1711
2024
|
const d = new Document(docData, {
|
|
1712
|
-
compressed: docData._compressed,
|
|
2025
|
+
compressed: docData._compressed,
|
|
1713
2026
|
encrypted: docData._encrypted
|
|
1714
|
-
});
|
|
2027
|
+
}, this._serializer);
|
|
1715
2028
|
// This await is what killed the transaction before
|
|
1716
|
-
await d.unpack(this._collection.database.encryption);
|
|
2029
|
+
await d.unpack(this._collection.database.encryption);
|
|
1717
2030
|
doc = d.objectOutput();
|
|
1718
2031
|
}
|
|
1719
2032
|
|
|
@@ -1734,7 +2047,7 @@ class IndexManager {
|
|
|
1734
2047
|
|
|
1735
2048
|
this._addToIndex(indexData, value, doc._id, index.type);
|
|
1736
2049
|
}
|
|
1737
|
-
|
|
2050
|
+
|
|
1738
2051
|
// Optional: Yield to main thread briefly to prevent UI freeze
|
|
1739
2052
|
if (window.requestIdleCallback) await new Promise(r => window.requestIdleCallback(r));
|
|
1740
2053
|
}
|
|
@@ -1774,11 +2087,11 @@ class IndexManager {
|
|
|
1774
2087
|
break;
|
|
1775
2088
|
}
|
|
1776
2089
|
}
|
|
1777
|
-
|
|
2090
|
+
|
|
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) {
|
|
@@ -1788,7 +2101,7 @@ class IndexManager {
|
|
|
1788
2101
|
|
|
1789
2102
|
let oldValue = oldDoc ? this._getFieldValue(oldDoc, index.fieldPath) : undefined;
|
|
1790
2103
|
let newValue = newDoc ? this._getFieldValue(newDoc, index.fieldPath) : undefined;
|
|
1791
|
-
|
|
2104
|
+
|
|
1792
2105
|
if (index.hashed) {
|
|
1793
2106
|
if (oldValue) oldValue = await this._hashVal(oldValue);
|
|
1794
2107
|
if (newValue) newValue = await this._hashVal(newValue);
|
|
@@ -1834,9 +2147,9 @@ class IndexManager {
|
|
|
1834
2147
|
if (!index || !indexData) {
|
|
1835
2148
|
throw new Error(`Index '${indexName}' not found`);
|
|
1836
2149
|
}
|
|
1837
|
-
|
|
2150
|
+
|
|
1838
2151
|
if (index.hashed && typeof queryOptions !== 'object') {
|
|
1839
|
-
|
|
2152
|
+
queryOptions = await this._hashVal(queryOptions);
|
|
1840
2153
|
}
|
|
1841
2154
|
|
|
1842
2155
|
return this._queryIndex(indexData, queryOptions, index.type);
|
|
@@ -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
|
|
|
@@ -2438,73 +2753,307 @@ class Document {
|
|
|
2438
2753
|
// ========================
|
|
2439
2754
|
|
|
2440
2755
|
class CollectionMetadata {
|
|
2441
|
-
constructor(name, data = {}) {
|
|
2756
|
+
constructor(name, data = {}, serializer, base64, dbName) {
|
|
2442
2757
|
this.name = name;
|
|
2758
|
+
this._serializer = serializer;
|
|
2759
|
+
this._base64 = base64;
|
|
2760
|
+
this._dbName = dbName;
|
|
2761
|
+
this._storageKey = dbName ? `lacertadb_${dbName}_${name}_collmeta` : null;
|
|
2762
|
+
|
|
2763
|
+
// Aggregate stats
|
|
2443
2764
|
this.sizeKB = data.sizeKB || 0;
|
|
2444
2765
|
this.length = data.length || 0;
|
|
2445
2766
|
this.createdAt = data.createdAt || Date.now();
|
|
2446
2767
|
this.modifiedAt = data.modifiedAt || Date.now();
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
this.
|
|
2450
|
-
this.
|
|
2451
|
-
this.
|
|
2768
|
+
|
|
2769
|
+
// Per-document tracking (in-memory Maps for O(1) ops)
|
|
2770
|
+
this._docSizes = new Map(data._docSizes || []); // docId -> sizeKB
|
|
2771
|
+
this._docModified = new Map(data._docModified || []); // docId -> timestamp
|
|
2772
|
+
this._docPermanent = new Map(data._docPermanent || []); // docId -> boolean
|
|
2773
|
+
this._docAttachments = new Map(data._docAttachments || []); // docId -> count
|
|
2774
|
+
|
|
2775
|
+
// Debounced persistence
|
|
2776
|
+
this._dirty = false;
|
|
2777
|
+
this._saveTimer = null;
|
|
2778
|
+
|
|
2779
|
+
// Safety: flush on page unload
|
|
2780
|
+
this._flushHandler = () => this._flushSync();
|
|
2781
|
+
if (typeof window !== 'undefined') {
|
|
2782
|
+
window.addEventListener('beforeunload', this._flushHandler);
|
|
2783
|
+
}
|
|
2452
2784
|
}
|
|
2453
2785
|
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2786
|
+
// ---- Mutations (in-memory only, schedule async save) ----
|
|
2787
|
+
|
|
2788
|
+
addDocument(docId, sizeKB, isPermanent = false, attachmentCount = 0) {
|
|
2789
|
+
this._docSizes.set(docId, sizeKB);
|
|
2790
|
+
this._docModified.set(docId, Date.now());
|
|
2791
|
+
this._docPermanent.set(docId, isPermanent);
|
|
2792
|
+
this._docAttachments.set(docId, attachmentCount);
|
|
2793
|
+
|
|
2457
2794
|
this.sizeKB += sizeKB;
|
|
2458
2795
|
this.length++;
|
|
2459
2796
|
this.modifiedAt = Date.now();
|
|
2797
|
+
this._scheduleSave();
|
|
2460
2798
|
}
|
|
2461
2799
|
|
|
2462
|
-
updateDocument(docId, newSizeKB, isPermanent, attachmentCount) {
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2800
|
+
updateDocument(docId, newSizeKB, isPermanent = false, attachmentCount = 0) {
|
|
2801
|
+
const oldSize = this._docSizes.get(docId) || 0;
|
|
2802
|
+
this.sizeKB = this.sizeKB - oldSize + newSizeKB;
|
|
2803
|
+
|
|
2804
|
+
this._docSizes.set(docId, newSizeKB);
|
|
2805
|
+
this._docModified.set(docId, Date.now());
|
|
2806
|
+
this._docPermanent.set(docId, isPermanent);
|
|
2807
|
+
this._docAttachments.set(docId, attachmentCount);
|
|
2808
|
+
|
|
2466
2809
|
this.modifiedAt = Date.now();
|
|
2810
|
+
this._scheduleSave();
|
|
2467
2811
|
}
|
|
2468
2812
|
|
|
2469
2813
|
removeDocument(docId) {
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2814
|
+
const sizeKB = this._docSizes.get(docId) || 0;
|
|
2815
|
+
this.sizeKB -= sizeKB;
|
|
2816
|
+
this.length--;
|
|
2817
|
+
|
|
2818
|
+
this._docSizes.delete(docId);
|
|
2819
|
+
this._docModified.delete(docId);
|
|
2820
|
+
this._docPermanent.delete(docId);
|
|
2821
|
+
this._docAttachments.delete(docId);
|
|
2822
|
+
|
|
2473
2823
|
this.modifiedAt = Date.now();
|
|
2824
|
+
this._scheduleSave();
|
|
2474
2825
|
}
|
|
2475
2826
|
|
|
2827
|
+
// ---- Queries (instant from memory) ----
|
|
2828
|
+
|
|
2476
2829
|
getOldestNonPermanentDocuments(count) {
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2830
|
+
const candidates = [];
|
|
2831
|
+
for (const [docId, modified] of this._docModified) {
|
|
2832
|
+
if (!this._docPermanent.get(docId)) {
|
|
2833
|
+
candidates.push({ id: docId, modified });
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
candidates.sort((a, b) => a.modified - b.modified);
|
|
2837
|
+
return candidates.slice(0, count).map(c => c.id);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
getDocumentSize(docId) {
|
|
2841
|
+
return this._docSizes.get(docId) || 0;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
isDocumentPermanent(docId) {
|
|
2845
|
+
return this._docPermanent.get(docId) || false;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
hasDocument(docId) {
|
|
2849
|
+
return this._docSizes.has(docId);
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// ---- Aggregate snapshot (for DatabaseMetadata) ----
|
|
2853
|
+
|
|
2854
|
+
getAggregateSnapshot() {
|
|
2855
|
+
return {
|
|
2856
|
+
sizeKB: this.sizeKB,
|
|
2857
|
+
length: this.length,
|
|
2858
|
+
createdAt: this.createdAt,
|
|
2859
|
+
modifiedAt: this.modifiedAt
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// ---- Persistence ----
|
|
2864
|
+
|
|
2865
|
+
_scheduleSave() {
|
|
2866
|
+
this._dirty = true;
|
|
2867
|
+
if (this._saveTimer) return;
|
|
2868
|
+
|
|
2869
|
+
const save = () => {
|
|
2870
|
+
this._saveTimer = null;
|
|
2871
|
+
if (!this._dirty) return;
|
|
2872
|
+
this._persistToStorage();
|
|
2873
|
+
};
|
|
2874
|
+
|
|
2875
|
+
if (typeof window !== 'undefined' && window.requestIdleCallback) {
|
|
2876
|
+
this._saveTimer = window.requestIdleCallback(save);
|
|
2877
|
+
} else {
|
|
2878
|
+
this._saveTimer = setTimeout(save, 300);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
_flushSync() {
|
|
2883
|
+
if (!this._dirty) return;
|
|
2884
|
+
this._persistToStorage();
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
_persistToStorage() {
|
|
2888
|
+
if (!this._storageKey || !this._serializer || !this._base64) return;
|
|
2889
|
+
try {
|
|
2890
|
+
const dataToStore = {
|
|
2891
|
+
sizeKB: this.sizeKB,
|
|
2892
|
+
length: this.length,
|
|
2893
|
+
createdAt: this.createdAt,
|
|
2894
|
+
modifiedAt: this.modifiedAt,
|
|
2895
|
+
_docSizes: Array.from(this._docSizes.entries()),
|
|
2896
|
+
_docModified: Array.from(this._docModified.entries()),
|
|
2897
|
+
_docPermanent: Array.from(this._docPermanent.entries()),
|
|
2898
|
+
_docAttachments: Array.from(this._docAttachments.entries())
|
|
2899
|
+
};
|
|
2900
|
+
const serialized = this._serializer.serialize(dataToStore);
|
|
2901
|
+
const encoded = this._base64.encode(serialized);
|
|
2902
|
+
localStorage.setItem(this._storageKey, encoded);
|
|
2903
|
+
this._dirty = false;
|
|
2904
|
+
} catch (e) {
|
|
2905
|
+
if (e.name === 'QuotaExceededError') {
|
|
2906
|
+
// Fallback: persist only aggregate stats (drop per-doc maps)
|
|
2907
|
+
console.warn('CollectionMetadata: quota exceeded, saving aggregates only');
|
|
2908
|
+
try {
|
|
2909
|
+
const fallback = {
|
|
2910
|
+
sizeKB: this.sizeKB,
|
|
2911
|
+
length: this.length,
|
|
2912
|
+
createdAt: this.createdAt,
|
|
2913
|
+
modifiedAt: this.modifiedAt
|
|
2914
|
+
};
|
|
2915
|
+
const serialized = this._serializer.serialize(fallback);
|
|
2916
|
+
const encoded = this._base64.encode(serialized);
|
|
2917
|
+
localStorage.setItem(this._storageKey, encoded);
|
|
2918
|
+
this._dirty = false;
|
|
2919
|
+
} catch (e2) {
|
|
2920
|
+
console.error('CollectionMetadata: fallback save also failed:', e2);
|
|
2921
|
+
}
|
|
2922
|
+
} else {
|
|
2923
|
+
console.error('CollectionMetadata save failed:', e);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
static load(dbName, collName, serializer, base64) {
|
|
2929
|
+
const key = `lacertadb_${dbName}_${collName}_collmeta`;
|
|
2930
|
+
const stored = localStorage.getItem(key);
|
|
2931
|
+
if (stored) {
|
|
2932
|
+
try {
|
|
2933
|
+
const decoded = base64.decode(stored);
|
|
2934
|
+
const data = serializer.deserialize(decoded);
|
|
2935
|
+
return new CollectionMetadata(collName, data, serializer, base64, dbName);
|
|
2936
|
+
} catch (e) {
|
|
2937
|
+
console.warn('CollectionMetadata corrupted, resetting:', e);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
return new CollectionMetadata(collName, {}, serializer, base64, dbName);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
// ---- Lifecycle ----
|
|
2944
|
+
|
|
2945
|
+
destroy() {
|
|
2946
|
+
this._flushSync();
|
|
2947
|
+
if (typeof window !== 'undefined' && this._flushHandler) {
|
|
2948
|
+
window.removeEventListener('beforeunload', this._flushHandler);
|
|
2949
|
+
this._flushHandler = null;
|
|
2950
|
+
}
|
|
2951
|
+
if (this._saveTimer) {
|
|
2952
|
+
if (typeof window !== 'undefined' && window.cancelIdleCallback) {
|
|
2953
|
+
window.cancelIdleCallback(this._saveTimer);
|
|
2954
|
+
} else {
|
|
2955
|
+
clearTimeout(this._saveTimer);
|
|
2956
|
+
}
|
|
2957
|
+
this._saveTimer = null;
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
clear() {
|
|
2962
|
+
this.sizeKB = 0;
|
|
2963
|
+
this.length = 0;
|
|
2964
|
+
this.modifiedAt = Date.now();
|
|
2965
|
+
this._docSizes.clear();
|
|
2966
|
+
this._docModified.clear();
|
|
2967
|
+
this._docPermanent.clear();
|
|
2968
|
+
this._docAttachments.clear();
|
|
2969
|
+
this._dirty = true;
|
|
2970
|
+
this._flushSync();
|
|
2480
2971
|
}
|
|
2481
2972
|
}
|
|
2482
2973
|
|
|
2483
2974
|
class DatabaseMetadata {
|
|
2484
|
-
constructor(name, data = {}) {
|
|
2975
|
+
constructor(name, data = {}, serializer, base64) {
|
|
2485
2976
|
this.name = name;
|
|
2977
|
+
this._serializer = serializer;
|
|
2978
|
+
this._base64 = base64;
|
|
2486
2979
|
this.collections = data.collections || {};
|
|
2487
2980
|
this.totalSizeKB = data.totalSizeKB || 0;
|
|
2488
2981
|
this.totalLength = data.totalLength || 0;
|
|
2489
2982
|
this.modifiedAt = data.modifiedAt || Date.now();
|
|
2983
|
+
|
|
2984
|
+
// Debounced persistence
|
|
2985
|
+
this._dirty = false;
|
|
2986
|
+
this._saveTimer = null;
|
|
2987
|
+
|
|
2988
|
+
this._flushHandler = () => this._flushSync();
|
|
2989
|
+
if (typeof window !== 'undefined') {
|
|
2990
|
+
window.addEventListener('beforeunload', this._flushHandler);
|
|
2991
|
+
}
|
|
2490
2992
|
}
|
|
2491
2993
|
|
|
2492
|
-
static load(dbName) {
|
|
2994
|
+
static load(dbName, serializer, base64) {
|
|
2493
2995
|
const key = `lacertadb_${dbName}_metadata`;
|
|
2494
2996
|
const stored = localStorage.getItem(key);
|
|
2495
2997
|
if (stored) {
|
|
2496
2998
|
try {
|
|
2497
2999
|
const decoded = base64.decode(stored);
|
|
2498
3000
|
const data = serializer.deserialize(decoded);
|
|
2499
|
-
return new DatabaseMetadata(dbName, data);
|
|
3001
|
+
return new DatabaseMetadata(dbName, data, serializer, base64);
|
|
2500
3002
|
} catch (e) {
|
|
2501
3003
|
console.error('Failed to load metadata:', e);
|
|
2502
3004
|
}
|
|
2503
3005
|
}
|
|
2504
|
-
return new DatabaseMetadata(dbName);
|
|
3006
|
+
return new DatabaseMetadata(dbName, {}, serializer, base64);
|
|
2505
3007
|
}
|
|
2506
3008
|
|
|
2507
|
-
|
|
3009
|
+
setCollection(collectionMetadata) {
|
|
3010
|
+
this.collections[collectionMetadata.name] = collectionMetadata.getAggregateSnapshot();
|
|
3011
|
+
this._recalculate();
|
|
3012
|
+
this._scheduleSave();
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
removeCollection(collectionName) {
|
|
3016
|
+
delete this.collections[collectionName];
|
|
3017
|
+
this._recalculate();
|
|
3018
|
+
this._scheduleSave();
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
_recalculate() {
|
|
3022
|
+
this.totalSizeKB = 0;
|
|
3023
|
+
this.totalLength = 0;
|
|
3024
|
+
for (const collName in this.collections) {
|
|
3025
|
+
const coll = this.collections[collName];
|
|
3026
|
+
this.totalSizeKB += coll.sizeKB;
|
|
3027
|
+
this.totalLength += coll.length;
|
|
3028
|
+
}
|
|
3029
|
+
this.modifiedAt = Date.now();
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// ---- Debounced persistence ----
|
|
3033
|
+
|
|
3034
|
+
_scheduleSave() {
|
|
3035
|
+
this._dirty = true;
|
|
3036
|
+
if (this._saveTimer) return;
|
|
3037
|
+
|
|
3038
|
+
const save = () => {
|
|
3039
|
+
this._saveTimer = null;
|
|
3040
|
+
if (!this._dirty) return;
|
|
3041
|
+
this._persistToStorage();
|
|
3042
|
+
};
|
|
3043
|
+
|
|
3044
|
+
if (typeof window !== 'undefined' && window.requestIdleCallback) {
|
|
3045
|
+
this._saveTimer = window.requestIdleCallback(save);
|
|
3046
|
+
} else {
|
|
3047
|
+
this._saveTimer = setTimeout(save, 300);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
_flushSync() {
|
|
3052
|
+
if (!this._dirty) return;
|
|
3053
|
+
this._persistToStorage();
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
_persistToStorage() {
|
|
2508
3057
|
const key = `lacertadb_${this.name}_metadata`;
|
|
2509
3058
|
try {
|
|
2510
3059
|
const dataToStore = {
|
|
@@ -2513,9 +3062,10 @@ class DatabaseMetadata {
|
|
|
2513
3062
|
totalLength: this.totalLength,
|
|
2514
3063
|
modifiedAt: this.modifiedAt
|
|
2515
3064
|
};
|
|
2516
|
-
const serializedData =
|
|
2517
|
-
const encodedData =
|
|
3065
|
+
const serializedData = this._serializer.serialize(dataToStore);
|
|
3066
|
+
const encodedData = this._base64.encode(serializedData);
|
|
2518
3067
|
localStorage.setItem(key, encodedData);
|
|
3068
|
+
this._dirty = false;
|
|
2519
3069
|
} catch (e) {
|
|
2520
3070
|
if (e.name === 'QuotaExceededError') {
|
|
2521
3071
|
console.error('CRITICAL: LocalStorage quota exceeded. Metadata not saved for db:', this.name);
|
|
@@ -2528,57 +3078,55 @@ class DatabaseMetadata {
|
|
|
2528
3078
|
}
|
|
2529
3079
|
}
|
|
2530
3080
|
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
createdAt: collectionMetadata.createdAt,
|
|
2536
|
-
modifiedAt: collectionMetadata.modifiedAt
|
|
2537
|
-
};
|
|
2538
|
-
this._recalculate();
|
|
2539
|
-
this.save();
|
|
3081
|
+
// Force immediate save (for critical operations like clearAll)
|
|
3082
|
+
save() {
|
|
3083
|
+
this._dirty = true;
|
|
3084
|
+
this._flushSync();
|
|
2540
3085
|
}
|
|
2541
3086
|
|
|
2542
|
-
|
|
2543
|
-
delete this.collections[collectionName];
|
|
2544
|
-
this._recalculate();
|
|
2545
|
-
this.save();
|
|
2546
|
-
}
|
|
3087
|
+
// ---- Lifecycle ----
|
|
2547
3088
|
|
|
2548
|
-
|
|
2549
|
-
this.
|
|
2550
|
-
this.
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
3089
|
+
destroy() {
|
|
3090
|
+
this._flushSync();
|
|
3091
|
+
if (typeof window !== 'undefined' && this._flushHandler) {
|
|
3092
|
+
window.removeEventListener('beforeunload', this._flushHandler);
|
|
3093
|
+
this._flushHandler = null;
|
|
3094
|
+
}
|
|
3095
|
+
if (this._saveTimer) {
|
|
3096
|
+
if (typeof window !== 'undefined' && window.cancelIdleCallback) {
|
|
3097
|
+
window.cancelIdleCallback(this._saveTimer);
|
|
3098
|
+
} else {
|
|
3099
|
+
clearTimeout(this._saveTimer);
|
|
3100
|
+
}
|
|
3101
|
+
this._saveTimer = null;
|
|
2555
3102
|
}
|
|
2556
|
-
this.modifiedAt = Date.now();
|
|
2557
3103
|
}
|
|
2558
3104
|
}
|
|
2559
3105
|
|
|
2560
3106
|
class Settings {
|
|
2561
|
-
constructor(dbName, data = {}) {
|
|
3107
|
+
constructor(dbName, data = {}, serializer, base64) {
|
|
2562
3108
|
this.dbName = dbName;
|
|
3109
|
+
this._serializer = serializer;
|
|
3110
|
+
this._base64 = base64;
|
|
2563
3111
|
this.sizeLimitKB = data.sizeLimitKB != null ? data.sizeLimitKB : Infinity;
|
|
2564
3112
|
const defaultBuffer = this.sizeLimitKB === Infinity ? Infinity : this.sizeLimitKB * 0.8;
|
|
2565
3113
|
this.bufferLimitKB = data.bufferLimitKB != null ? data.bufferLimitKB : defaultBuffer;
|
|
2566
3114
|
this.freeSpaceEvery = this.sizeLimitKB === Infinity ? 0 : (data.freeSpaceEvery || 10000);
|
|
2567
3115
|
}
|
|
2568
3116
|
|
|
2569
|
-
static load(dbName) {
|
|
3117
|
+
static load(dbName, serializer, base64) {
|
|
2570
3118
|
const key = `lacertadb_${dbName}_settings`;
|
|
2571
3119
|
const stored = localStorage.getItem(key);
|
|
2572
3120
|
if (stored) {
|
|
2573
3121
|
try {
|
|
2574
3122
|
const decoded = base64.decode(stored);
|
|
2575
3123
|
const data = serializer.deserialize(decoded);
|
|
2576
|
-
return new Settings(dbName, data);
|
|
3124
|
+
return new Settings(dbName, data, serializer, base64);
|
|
2577
3125
|
} catch (e) {
|
|
2578
3126
|
console.error('Failed to load settings:', e);
|
|
2579
3127
|
}
|
|
2580
3128
|
}
|
|
2581
|
-
return new Settings(dbName);
|
|
3129
|
+
return new Settings(dbName, {}, serializer, base64);
|
|
2582
3130
|
}
|
|
2583
3131
|
|
|
2584
3132
|
save() {
|
|
@@ -2589,8 +3137,8 @@ class Settings {
|
|
|
2589
3137
|
bufferLimitKB: this.bufferLimitKB,
|
|
2590
3138
|
freeSpaceEvery: this.freeSpaceEvery
|
|
2591
3139
|
};
|
|
2592
|
-
const serializedData =
|
|
2593
|
-
const encodedData =
|
|
3140
|
+
const serializedData = this._serializer.serialize(dataToStore);
|
|
3141
|
+
const encodedData = this._base64.encode(serializedData);
|
|
2594
3142
|
localStorage.setItem(key, encodedData);
|
|
2595
3143
|
} catch (e) {
|
|
2596
3144
|
if (e.name === 'QuotaExceededError') {
|
|
@@ -3039,6 +3587,8 @@ class Collection {
|
|
|
3039
3587
|
constructor(name, database) {
|
|
3040
3588
|
this.name = name;
|
|
3041
3589
|
this.database = database;
|
|
3590
|
+
this._serializer = database._serializer;
|
|
3591
|
+
this._base64 = database._base64;
|
|
3042
3592
|
this._db = null;
|
|
3043
3593
|
this._metadata = null;
|
|
3044
3594
|
this._settings = database.settings;
|
|
@@ -3082,8 +3632,10 @@ class Collection {
|
|
|
3082
3632
|
}
|
|
3083
3633
|
});
|
|
3084
3634
|
|
|
3085
|
-
|
|
3086
|
-
this._metadata =
|
|
3635
|
+
// Load per-collection metadata (with per-doc tracking) from its own localStorage key
|
|
3636
|
+
this._metadata = CollectionMetadata.load(
|
|
3637
|
+
this.database.name, this.name, this._serializer, this._base64
|
|
3638
|
+
);
|
|
3087
3639
|
|
|
3088
3640
|
await this._indexManager.loadIndexMetadata();
|
|
3089
3641
|
|
|
@@ -3131,7 +3683,7 @@ class Collection {
|
|
|
3131
3683
|
const doc = new Document({data: documentData, _id: options.id}, {
|
|
3132
3684
|
compressed: options.compressed !== false,
|
|
3133
3685
|
permanent: options.permanent || false
|
|
3134
|
-
});
|
|
3686
|
+
}, this._serializer);
|
|
3135
3687
|
|
|
3136
3688
|
const attachments = options.attachments;
|
|
3137
3689
|
if (attachments && attachments.length > 0) {
|
|
@@ -3173,7 +3725,7 @@ class Collection {
|
|
|
3173
3725
|
const doc = new Document(stored, {
|
|
3174
3726
|
encrypted: stored._encrypted,
|
|
3175
3727
|
compressed: stored._compressed
|
|
3176
|
-
});
|
|
3728
|
+
}, this._serializer);
|
|
3177
3729
|
|
|
3178
3730
|
if (stored.packedData) {
|
|
3179
3731
|
await doc.unpack(this.database.encryption);
|
|
@@ -3196,7 +3748,7 @@ class Collection {
|
|
|
3196
3748
|
const doc = new Document(docData, {
|
|
3197
3749
|
encrypted: docData._encrypted,
|
|
3198
3750
|
compressed: docData._compressed
|
|
3199
|
-
});
|
|
3751
|
+
}, this._serializer);
|
|
3200
3752
|
if (docData.packedData) {
|
|
3201
3753
|
await doc.unpack(this.database.encryption);
|
|
3202
3754
|
}
|
|
@@ -3218,7 +3770,7 @@ class Collection {
|
|
|
3218
3770
|
throw new LacertaDBError(`Document with id '${docId}' not found for update.`, 'DOCUMENT_NOT_FOUND');
|
|
3219
3771
|
}
|
|
3220
3772
|
|
|
3221
|
-
const existingDoc = new Document(stored);
|
|
3773
|
+
const existingDoc = new Document(stored, {}, this._serializer);
|
|
3222
3774
|
if (stored.packedData) await existingDoc.unpack(this.database.encryption);
|
|
3223
3775
|
|
|
3224
3776
|
const oldDocOutput = existingDoc.objectOutput();
|
|
@@ -3231,7 +3783,7 @@ class Collection {
|
|
|
3231
3783
|
}, {
|
|
3232
3784
|
compressed: options.compressed !== undefined ? options.compressed : stored._compressed,
|
|
3233
3785
|
permanent: options.permanent !== undefined ? options.permanent : stored._permanent
|
|
3234
|
-
});
|
|
3786
|
+
}, this._serializer);
|
|
3235
3787
|
doc._modified = Date.now();
|
|
3236
3788
|
|
|
3237
3789
|
const attachments = options.attachments;
|
|
@@ -3306,7 +3858,7 @@ class Collection {
|
|
|
3306
3858
|
|
|
3307
3859
|
const startTime = performance.now();
|
|
3308
3860
|
|
|
3309
|
-
const cacheKey =
|
|
3861
|
+
const cacheKey = this._base64.encode(this._serializer.serialize({filter, options}));
|
|
3310
3862
|
const cached = this._cacheStrategy.get(cacheKey);
|
|
3311
3863
|
|
|
3312
3864
|
if (cached) {
|
|
@@ -3376,7 +3928,7 @@ class Collection {
|
|
|
3376
3928
|
const doc = new Document({data: documentData}, {
|
|
3377
3929
|
compressed: options.compressed !== false,
|
|
3378
3930
|
permanent: options.permanent || false
|
|
3379
|
-
});
|
|
3931
|
+
}, this._serializer);
|
|
3380
3932
|
|
|
3381
3933
|
await doc.pack(this.database.encryption);
|
|
3382
3934
|
operations.push({
|
|
@@ -3427,7 +3979,7 @@ class Collection {
|
|
|
3427
3979
|
continue;
|
|
3428
3980
|
}
|
|
3429
3981
|
|
|
3430
|
-
const existingDoc = new Document(stored);
|
|
3982
|
+
const existingDoc = new Document(stored, {}, this._serializer);
|
|
3431
3983
|
if (stored.packedData) await existingDoc.unpack(this.database.encryption);
|
|
3432
3984
|
|
|
3433
3985
|
oldDocs.push(existingDoc.objectOutput());
|
|
@@ -3440,7 +3992,7 @@ class Collection {
|
|
|
3440
3992
|
}, {
|
|
3441
3993
|
compressed: options.compressed !== undefined ? options.compressed : stored._compressed,
|
|
3442
3994
|
permanent: options.permanent !== undefined ? options.permanent : stored._permanent
|
|
3443
|
-
});
|
|
3995
|
+
}, this._serializer);
|
|
3444
3996
|
doc._modified = Date.now();
|
|
3445
3997
|
doc._attachments = stored._attachments;
|
|
3446
3998
|
|
|
@@ -3596,7 +4148,11 @@ class Collection {
|
|
|
3596
4148
|
await this._indexedDB.clear(this._db, 'documents');
|
|
3597
4149
|
|
|
3598
4150
|
// Reset metadata
|
|
3599
|
-
this._metadata
|
|
4151
|
+
if (this._metadata) this._metadata.destroy();
|
|
4152
|
+
this._metadata = new CollectionMetadata(
|
|
4153
|
+
this.name, {}, this._serializer, this._base64, this.database.name
|
|
4154
|
+
);
|
|
4155
|
+
this._metadata._flushSync();
|
|
3600
4156
|
this.database.metadata.setCollection(this._metadata);
|
|
3601
4157
|
|
|
3602
4158
|
// Clear cache
|
|
@@ -3624,6 +4180,11 @@ class Collection {
|
|
|
3624
4180
|
}
|
|
3625
4181
|
|
|
3626
4182
|
destroy() {
|
|
4183
|
+
// Flush and destroy collection metadata
|
|
4184
|
+
if (this._metadata) {
|
|
4185
|
+
this._metadata.destroy();
|
|
4186
|
+
}
|
|
4187
|
+
|
|
3627
4188
|
// Clear the cleanup interval
|
|
3628
4189
|
if (this._cleanupInterval) {
|
|
3629
4190
|
clearInterval(this._cleanupInterval);
|
|
@@ -3652,13 +4213,15 @@ class Collection {
|
|
|
3652
4213
|
// ========================
|
|
3653
4214
|
|
|
3654
4215
|
class Database {
|
|
3655
|
-
constructor(name, performanceMonitor) {
|
|
4216
|
+
constructor(name, performanceMonitor, serializer, base64) {
|
|
3656
4217
|
this.name = name;
|
|
3657
4218
|
this._collections = new Map();
|
|
3658
4219
|
this._metadata = null;
|
|
3659
4220
|
this._settings = null;
|
|
3660
4221
|
this._quickStore = null;
|
|
3661
4222
|
this._performanceMonitor = performanceMonitor;
|
|
4223
|
+
this._serializer = serializer;
|
|
4224
|
+
this._base64 = base64;
|
|
3662
4225
|
|
|
3663
4226
|
// Database-level encryption
|
|
3664
4227
|
this._encryption = null;
|
|
@@ -3693,9 +4256,9 @@ class Database {
|
|
|
3693
4256
|
}
|
|
3694
4257
|
|
|
3695
4258
|
async init(options = {}) {
|
|
3696
|
-
this._metadata = DatabaseMetadata.load(this.name);
|
|
3697
|
-
this._settings = Settings.load(this.name);
|
|
3698
|
-
this._quickStore = new QuickStore(this.name);
|
|
4259
|
+
this._metadata = DatabaseMetadata.load(this.name, this._serializer, this._base64);
|
|
4260
|
+
this._settings = Settings.load(this.name, this._serializer, this._base64);
|
|
4261
|
+
this._quickStore = new QuickStore(this.name, this._serializer, this._base64);
|
|
3699
4262
|
|
|
3700
4263
|
if (options.pin) {
|
|
3701
4264
|
await this._initializeEncryption(options.pin, options.salt, options.encryptionConfig);
|
|
@@ -3708,18 +4271,18 @@ class Database {
|
|
|
3708
4271
|
const encMetaKey = `lacertadb_${this.name}_encryption`;
|
|
3709
4272
|
let existingMetadata = null;
|
|
3710
4273
|
const stored = localStorage.getItem(encMetaKey);
|
|
3711
|
-
|
|
4274
|
+
|
|
3712
4275
|
if (stored) {
|
|
3713
|
-
const decoded =
|
|
3714
|
-
existingMetadata =
|
|
4276
|
+
const decoded = this._base64.decode(stored);
|
|
4277
|
+
existingMetadata = this._serializer.deserialize(decoded);
|
|
3715
4278
|
}
|
|
3716
4279
|
|
|
3717
|
-
this._encryption = new SecureDatabaseEncryption(config);
|
|
4280
|
+
this._encryption = new SecureDatabaseEncryption(config, this._serializer, this._base64);
|
|
3718
4281
|
const newMeta = await this._encryption.initialize(pin, existingMetadata);
|
|
3719
4282
|
|
|
3720
4283
|
if (!existingMetadata) {
|
|
3721
|
-
const serialized =
|
|
3722
|
-
const encoded =
|
|
4284
|
+
const serialized = this._serializer.serialize(newMeta);
|
|
4285
|
+
const encoded = this._base64.encode(serialized);
|
|
3723
4286
|
localStorage.setItem(encMetaKey, encoded);
|
|
3724
4287
|
}
|
|
3725
4288
|
}
|
|
@@ -3732,8 +4295,8 @@ class Database {
|
|
|
3732
4295
|
const newMeta = await this._encryption.changePin(oldPin, newPin);
|
|
3733
4296
|
|
|
3734
4297
|
const encMetaKey = `lacertadb_${this.name}_encryption`;
|
|
3735
|
-
const serialized =
|
|
3736
|
-
const encoded =
|
|
4298
|
+
const serialized = this._serializer.serialize(newMeta);
|
|
4299
|
+
const encoded = this._base64.encode(serialized);
|
|
3737
4300
|
localStorage.setItem(encMetaKey, encoded);
|
|
3738
4301
|
|
|
3739
4302
|
return true;
|
|
@@ -3791,7 +4354,9 @@ class Database {
|
|
|
3791
4354
|
this._collections.set(name, collection);
|
|
3792
4355
|
|
|
3793
4356
|
if (!this._metadata.collections[name]) {
|
|
3794
|
-
this._metadata.setCollection(new CollectionMetadata(
|
|
4357
|
+
this._metadata.setCollection(new CollectionMetadata(
|
|
4358
|
+
name, {}, this._serializer, this._base64, this.name
|
|
4359
|
+
));
|
|
3795
4360
|
}
|
|
3796
4361
|
return collection;
|
|
3797
4362
|
}
|
|
@@ -3825,6 +4390,10 @@ class Database {
|
|
|
3825
4390
|
|
|
3826
4391
|
this._metadata.removeCollection(name);
|
|
3827
4392
|
|
|
4393
|
+
// Clean up collection-level metadata and index localStorage keys
|
|
4394
|
+
localStorage.removeItem(`lacertadb_${this.name}_${name}_collmeta`);
|
|
4395
|
+
localStorage.removeItem(`lacertadb_${this.name}_${name}_indexes`);
|
|
4396
|
+
|
|
3828
4397
|
const dbName = `${this.name}_${name}`;
|
|
3829
4398
|
await new Promise((resolve, reject) => {
|
|
3830
4399
|
const deleteReq = indexedDB.deleteDatabase(dbName);
|
|
@@ -3859,7 +4428,7 @@ class Database {
|
|
|
3859
4428
|
|
|
3860
4429
|
async export(format = 'json', password = null) {
|
|
3861
4430
|
const data = {
|
|
3862
|
-
version: '0.
|
|
4431
|
+
version: '0.10.2',
|
|
3863
4432
|
database: this.name,
|
|
3864
4433
|
timestamp: Date.now(),
|
|
3865
4434
|
collections: {}
|
|
@@ -3871,14 +4440,14 @@ class Database {
|
|
|
3871
4440
|
}
|
|
3872
4441
|
|
|
3873
4442
|
if (format === 'json') {
|
|
3874
|
-
const serialized =
|
|
3875
|
-
return
|
|
4443
|
+
const serialized = this._serializer.serialize(data);
|
|
4444
|
+
return this._base64.encode(serialized);
|
|
3876
4445
|
}
|
|
3877
4446
|
if (format === 'encrypted' && password) {
|
|
3878
4447
|
const encryption = new BrowserEncryptionUtility();
|
|
3879
|
-
const serializedData =
|
|
4448
|
+
const serializedData = this._serializer.serialize(data);
|
|
3880
4449
|
const encrypted = await encryption.encrypt(serializedData, password);
|
|
3881
|
-
return
|
|
4450
|
+
return this._base64.encode(encrypted);
|
|
3882
4451
|
}
|
|
3883
4452
|
throw new LacertaDBError(`Unsupported export format: ${format}`, 'INVALID_FORMAT');
|
|
3884
4453
|
}
|
|
@@ -3886,13 +4455,13 @@ class Database {
|
|
|
3886
4455
|
async import(data, format = 'json', password = null) {
|
|
3887
4456
|
let parsed;
|
|
3888
4457
|
try {
|
|
3889
|
-
const decoded =
|
|
4458
|
+
const decoded = this._base64.decode(data);
|
|
3890
4459
|
if (format === 'encrypted' && password) {
|
|
3891
4460
|
const encryption = new BrowserEncryptionUtility();
|
|
3892
4461
|
const decrypted = await encryption.decrypt(decoded, password);
|
|
3893
|
-
parsed =
|
|
4462
|
+
parsed = this._serializer.deserialize(decrypted);
|
|
3894
4463
|
} else {
|
|
3895
|
-
parsed =
|
|
4464
|
+
parsed = this._serializer.deserialize(decoded);
|
|
3896
4465
|
}
|
|
3897
4466
|
} catch (e) {
|
|
3898
4467
|
throw new LacertaDBError('Failed to parse import data', 'IMPORT_PARSE_FAILED', e);
|
|
@@ -3923,7 +4492,8 @@ class Database {
|
|
|
3923
4492
|
async clearAll() {
|
|
3924
4493
|
await Promise.all([...this._collections.keys()].map(name => this.dropCollection(name)));
|
|
3925
4494
|
this._collections.clear();
|
|
3926
|
-
this._metadata
|
|
4495
|
+
if (this._metadata) this._metadata.destroy();
|
|
4496
|
+
this._metadata = new DatabaseMetadata(this.name, {}, this._serializer, this._base64);
|
|
3927
4497
|
this._metadata.save();
|
|
3928
4498
|
this._quickStore.clear();
|
|
3929
4499
|
}
|
|
@@ -3943,6 +4513,11 @@ class Database {
|
|
|
3943
4513
|
this._quickStore.destroy();
|
|
3944
4514
|
}
|
|
3945
4515
|
|
|
4516
|
+
// Destroy database metadata
|
|
4517
|
+
if (this._metadata) {
|
|
4518
|
+
this._metadata.destroy();
|
|
4519
|
+
}
|
|
4520
|
+
|
|
3946
4521
|
// Destroy encryption
|
|
3947
4522
|
if (this._encryption) {
|
|
3948
4523
|
this._encryption.destroy();
|
|
@@ -3961,18 +4536,31 @@ class Database {
|
|
|
3961
4536
|
// ========================
|
|
3962
4537
|
|
|
3963
4538
|
class LacertaDB {
|
|
3964
|
-
constructor() {
|
|
4539
|
+
constructor(config = {}) {
|
|
3965
4540
|
this._databases = new Map();
|
|
3966
4541
|
this._performanceMonitor = new PerformanceMonitor();
|
|
4542
|
+
|
|
4543
|
+
// Instantiate serializer with user-overridable config merged over defaults
|
|
4544
|
+
const serialConfig = { ...TURBO_SERIAL_DEFAULTS, ...(config.turboSerial || {}) };
|
|
4545
|
+
this._serializer = new TurboSerial(serialConfig);
|
|
4546
|
+
this._base64 = new TurboBase64();
|
|
3967
4547
|
}
|
|
3968
4548
|
|
|
3969
4549
|
get performanceMonitor() {
|
|
3970
4550
|
return this._performanceMonitor;
|
|
3971
4551
|
}
|
|
3972
4552
|
|
|
4553
|
+
get serializer() {
|
|
4554
|
+
return this._serializer;
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
get base64() {
|
|
4558
|
+
return this._base64;
|
|
4559
|
+
}
|
|
4560
|
+
|
|
3973
4561
|
async getDatabase(name, options = {}) {
|
|
3974
4562
|
if (!this._databases.has(name)) {
|
|
3975
|
-
const db = new Database(name, this._performanceMonitor);
|
|
4563
|
+
const db = new Database(name, this._performanceMonitor, this._serializer, this._base64);
|
|
3976
4564
|
await db.init(options);
|
|
3977
4565
|
this._databases.set(name, db);
|
|
3978
4566
|
}
|
|
@@ -3996,7 +4584,7 @@ class LacertaDB {
|
|
|
3996
4584
|
});
|
|
3997
4585
|
|
|
3998
4586
|
// Clean up quickstore
|
|
3999
|
-
const quickStore = new QuickStore(name);
|
|
4587
|
+
const quickStore = new QuickStore(name, this._serializer, this._base64);
|
|
4000
4588
|
quickStore.clear();
|
|
4001
4589
|
|
|
4002
4590
|
// Clean up all collections and indexes
|
|
@@ -4026,7 +4614,7 @@ class LacertaDB {
|
|
|
4026
4614
|
|
|
4027
4615
|
async createBackup(password = null) {
|
|
4028
4616
|
const backup = {
|
|
4029
|
-
version: '0.
|
|
4617
|
+
version: '0.10.2',
|
|
4030
4618
|
timestamp: Date.now(),
|
|
4031
4619
|
databases: {}
|
|
4032
4620
|
};
|
|
@@ -4034,29 +4622,29 @@ class LacertaDB {
|
|
|
4034
4622
|
for (const dbName of this.listDatabases()) {
|
|
4035
4623
|
const db = await this.getDatabase(dbName);
|
|
4036
4624
|
const exported = await db.export('json');
|
|
4037
|
-
const decoded =
|
|
4038
|
-
backup.databases[dbName] =
|
|
4625
|
+
const decoded = this._base64.decode(exported);
|
|
4626
|
+
backup.databases[dbName] = this._serializer.deserialize(decoded);
|
|
4039
4627
|
}
|
|
4040
4628
|
|
|
4041
|
-
const serializedBackup =
|
|
4629
|
+
const serializedBackup = this._serializer.serialize(backup);
|
|
4042
4630
|
if (password) {
|
|
4043
4631
|
const encryption = new BrowserEncryptionUtility();
|
|
4044
4632
|
const encrypted = await encryption.encrypt(serializedBackup, password);
|
|
4045
|
-
return
|
|
4633
|
+
return this._base64.encode(encrypted);
|
|
4046
4634
|
}
|
|
4047
|
-
return
|
|
4635
|
+
return this._base64.encode(serializedBackup);
|
|
4048
4636
|
}
|
|
4049
4637
|
|
|
4050
4638
|
async restoreBackup(backupData, password = null) {
|
|
4051
4639
|
let backup;
|
|
4052
4640
|
try {
|
|
4053
|
-
let decodedData =
|
|
4641
|
+
let decodedData = this._base64.decode(backupData);
|
|
4054
4642
|
if (password) {
|
|
4055
4643
|
const encryption = new BrowserEncryptionUtility();
|
|
4056
4644
|
const decrypted = await encryption.decrypt(decodedData, password);
|
|
4057
|
-
backup =
|
|
4645
|
+
backup = this._serializer.deserialize(decrypted);
|
|
4058
4646
|
} else {
|
|
4059
|
-
backup =
|
|
4647
|
+
backup = this._serializer.deserialize(decodedData);
|
|
4060
4648
|
}
|
|
4061
4649
|
} catch (e) {
|
|
4062
4650
|
throw new LacertaDBError('Failed to parse backup data', 'BACKUP_PARSE_FAILED', e);
|
|
@@ -4065,7 +4653,7 @@ class LacertaDB {
|
|
|
4065
4653
|
const results = { databases: 0, collections: 0, documents: 0 };
|
|
4066
4654
|
for (const [dbName, dbData] of Object.entries(backup.databases)) {
|
|
4067
4655
|
const db = await this.getDatabase(dbName);
|
|
4068
|
-
const encodedDbData =
|
|
4656
|
+
const encodedDbData = this._base64.encode(this._serializer.serialize(dbData));
|
|
4069
4657
|
const importResult = await db.import(encodedDbData);
|
|
4070
4658
|
|
|
4071
4659
|
results.databases++;
|
|
@@ -4115,4 +4703,4 @@ export {
|
|
|
4115
4703
|
IndexedDBConnectionPool,
|
|
4116
4704
|
BrowserCompressionUtility,
|
|
4117
4705
|
BrowserEncryptionUtility
|
|
4118
|
-
};
|
|
4706
|
+
};
|