@pixagram/lacerta-db 0.11.0 → 0.11.2
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/.idea/lacerta-db.iml +8 -0
- package/.idea/modules.xml +8 -0
- package/.idea/php.xml +19 -0
- package/dist/browser.min.js +4 -4
- package/dist/index.min.js +4 -4
- package/index.js +343 -91
- package/logo.webp +0 -0
- package/package.json +1 -1
- package/readme.md +1 -1
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LacertaDB V0.11.
|
|
2
|
+
* LacertaDB V0.11.2 - Production Library
|
|
3
3
|
* @module LacertaDB
|
|
4
|
-
* @version 0.11.
|
|
4
|
+
* @version 0.11.2
|
|
5
5
|
* @license MIT
|
|
6
6
|
* @author Pixagram SA
|
|
7
7
|
*/
|
|
@@ -29,14 +29,14 @@ import TurboBase64 from "@pixagram/turbobase64";
|
|
|
29
29
|
// Default TurboSerial configuration (overridable via LacertaDB constructor)
|
|
30
30
|
const TURBO_SERIAL_DEFAULTS = {
|
|
31
31
|
compression: false,
|
|
32
|
-
preservePropertyDescriptors:
|
|
32
|
+
preservePropertyDescriptors: false,
|
|
33
33
|
deduplication: false,
|
|
34
34
|
simdOptimization: true,
|
|
35
|
-
detectCircular:
|
|
36
|
-
shareArrayBuffers:
|
|
35
|
+
detectCircular: false,
|
|
36
|
+
shareArrayBuffers: false,
|
|
37
37
|
allowFunction: false,
|
|
38
38
|
serializeFunctions: false,
|
|
39
|
-
memoryPoolSize: 65536
|
|
39
|
+
memoryPoolSize: 65536
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
// ========================
|
|
@@ -54,11 +54,11 @@ class QuickStore {
|
|
|
54
54
|
this._base64 = base64;
|
|
55
55
|
this._keyPrefix = `lacertadb_${dbName}_quickstore_`;
|
|
56
56
|
this._indexKey = `${this._keyPrefix}index`;
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
// Optimization: Keep index in memory using a Set for O(1) lookups
|
|
59
59
|
this._indexCache = new Set();
|
|
60
60
|
this._indexLoaded = false;
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
// Async persistence state
|
|
63
63
|
this._saveIndexTimer = null;
|
|
64
64
|
this._dirty = false;
|
|
@@ -88,7 +88,7 @@ class QuickStore {
|
|
|
88
88
|
|
|
89
89
|
_ensureIndexLoaded() {
|
|
90
90
|
if (this._indexLoaded) return;
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
const indexStr = localStorage.getItem(this._indexKey);
|
|
93
93
|
if (indexStr) {
|
|
94
94
|
try {
|
|
@@ -568,7 +568,7 @@ class CacheStrategy {
|
|
|
568
568
|
const type = this._config.type || 'lru';
|
|
569
569
|
const max = this._config.maxSize || 100;
|
|
570
570
|
const ttl = this._config.ttl;
|
|
571
|
-
|
|
571
|
+
|
|
572
572
|
if (type === 'none' || this._config.enabled === false) return null;
|
|
573
573
|
if (type === 'ttl') return new TTLCache(ttl);
|
|
574
574
|
if (type === 'lfu') return new LFUCache(max, ttl);
|
|
@@ -615,9 +615,9 @@ class CacheStrategy {
|
|
|
615
615
|
// ========================
|
|
616
616
|
|
|
617
617
|
class BrowserCompressionUtility {
|
|
618
|
-
// Magic header to distinguish compressed data.
|
|
618
|
+
// Magic header to distinguish compressed data.
|
|
619
619
|
// 0x01 = Compressed (Deflate), 0x00 = Raw
|
|
620
|
-
|
|
620
|
+
|
|
621
621
|
async compress(input) {
|
|
622
622
|
if (!(input instanceof Uint8Array)) {
|
|
623
623
|
throw new TypeError('Input must be Uint8Array');
|
|
@@ -643,7 +643,7 @@ class BrowserCompressionUtility {
|
|
|
643
643
|
throw new TypeError('Input must be Uint8Array');
|
|
644
644
|
}
|
|
645
645
|
if (input.length === 0) return input;
|
|
646
|
-
|
|
646
|
+
|
|
647
647
|
const marker = input[0];
|
|
648
648
|
const data = input.slice(1);
|
|
649
649
|
|
|
@@ -777,7 +777,7 @@ class SecureDatabaseEncryption {
|
|
|
777
777
|
this._initialized = false;
|
|
778
778
|
this._serializer = serializer;
|
|
779
779
|
this._base64 = base64;
|
|
780
|
-
|
|
780
|
+
|
|
781
781
|
this._masterKey = null; // The actual key used for data encryption
|
|
782
782
|
this._hmacKey = null; // Key for HMAC operations
|
|
783
783
|
this._salt = null; // Salt for KEK derivation
|
|
@@ -798,7 +798,7 @@ class SecureDatabaseEncryption {
|
|
|
798
798
|
// Load existing
|
|
799
799
|
this._salt = this._base64.decode(existingMetadata.salt);
|
|
800
800
|
const kek = await this._deriveKEK(pinBytes, this._salt);
|
|
801
|
-
|
|
801
|
+
|
|
802
802
|
// Unwrap Master Key
|
|
803
803
|
const wrappedBytes = this._base64.decode(existingMetadata.wrappedKey);
|
|
804
804
|
const iv = wrappedBytes.slice(0, 12);
|
|
@@ -816,21 +816,21 @@ class SecureDatabaseEncryption {
|
|
|
816
816
|
// New Database
|
|
817
817
|
this._salt = crypto.getRandomValues(new Uint8Array(this._saltLength));
|
|
818
818
|
const kek = await this._deriveKEK(pinBytes, this._salt);
|
|
819
|
-
|
|
819
|
+
|
|
820
820
|
// Generate Master Keys (64 bytes: 32 enc + 32 hmac)
|
|
821
821
|
const rawKeys = crypto.getRandomValues(new Uint8Array(64));
|
|
822
822
|
await this._importMasterKeys(rawKeys.buffer);
|
|
823
|
-
|
|
823
|
+
|
|
824
824
|
// Wrap Master Key
|
|
825
825
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
826
826
|
const encryptedMK = await crypto.subtle.encrypt(
|
|
827
827
|
{ name: 'AES-GCM', iv }, kek, rawKeys
|
|
828
828
|
);
|
|
829
|
-
|
|
829
|
+
|
|
830
830
|
const wrappedKey = new Uint8Array(12 + encryptedMK.byteLength);
|
|
831
831
|
wrappedKey.set(iv, 0);
|
|
832
832
|
wrappedKey.set(new Uint8Array(encryptedMK), 12);
|
|
833
|
-
|
|
833
|
+
|
|
834
834
|
this._wrappedKeyBlob = this._base64.encode(wrappedKey);
|
|
835
835
|
}
|
|
836
836
|
|
|
@@ -914,7 +914,7 @@ class SecureDatabaseEncryption {
|
|
|
914
914
|
// Update State
|
|
915
915
|
this._salt = newSalt;
|
|
916
916
|
this._wrappedKeyBlob = this._base64.encode(wrappedKey);
|
|
917
|
-
|
|
917
|
+
|
|
918
918
|
return this.exportMetadata();
|
|
919
919
|
}
|
|
920
920
|
|
|
@@ -1141,7 +1141,7 @@ class QuadTree {
|
|
|
1141
1141
|
if (!this.divided) this._subdivide();
|
|
1142
1142
|
|
|
1143
1143
|
return (this.northeast.insert(point) || this.northwest.insert(point) ||
|
|
1144
|
-
|
|
1144
|
+
this.southeast.insert(point) || this.southwest.insert(point));
|
|
1145
1145
|
}
|
|
1146
1146
|
|
|
1147
1147
|
query(range, found = []) { // range: {x, y, w, h}
|
|
@@ -1183,12 +1183,12 @@ class QuadTree {
|
|
|
1183
1183
|
|
|
1184
1184
|
_contains(b, p) {
|
|
1185
1185
|
return (p.x >= b.x - b.w && p.x <= b.x + b.w &&
|
|
1186
|
-
|
|
1186
|
+
p.y >= b.y - b.h && p.y <= b.y + b.h);
|
|
1187
1187
|
}
|
|
1188
1188
|
|
|
1189
1189
|
_intersects(a, b) {
|
|
1190
1190
|
return !(b.x - b.w > a.x + a.w || b.x + b.w < a.x - a.w ||
|
|
1191
|
-
|
|
1191
|
+
b.y - b.h > a.y + a.h || b.y + b.h < a.y - a.h);
|
|
1192
1192
|
}
|
|
1193
1193
|
}
|
|
1194
1194
|
|
|
@@ -1782,7 +1782,7 @@ class TextIndex {
|
|
|
1782
1782
|
constructor() {
|
|
1783
1783
|
this._invertedIndex = new Map();
|
|
1784
1784
|
this._docTokens = new Map();
|
|
1785
|
-
this._segmenter = typeof Intl !== 'undefined' && Intl.Segmenter ?
|
|
1785
|
+
this._segmenter = typeof Intl !== 'undefined' && Intl.Segmenter ?
|
|
1786
1786
|
new Intl.Segmenter(undefined, { granularity: 'word' }) : null;
|
|
1787
1787
|
}
|
|
1788
1788
|
|
|
@@ -1868,7 +1868,7 @@ class TextIndex {
|
|
|
1868
1868
|
|
|
1869
1869
|
class GeoIndex {
|
|
1870
1870
|
constructor() {
|
|
1871
|
-
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});
|
|
1872
1872
|
this._size = 0;
|
|
1873
1873
|
}
|
|
1874
1874
|
|
|
@@ -1891,15 +1891,15 @@ class GeoIndex {
|
|
|
1891
1891
|
}
|
|
1892
1892
|
|
|
1893
1893
|
findNear(center, maxDistance) {
|
|
1894
|
-
const rangeDeg = maxDistance / 111;
|
|
1894
|
+
const rangeDeg = maxDistance / 111;
|
|
1895
1895
|
const range = {
|
|
1896
|
-
x: center.lng, y: center.lat,
|
|
1896
|
+
x: center.lng, y: center.lat,
|
|
1897
1897
|
w: rangeDeg, h: rangeDeg
|
|
1898
1898
|
};
|
|
1899
|
-
|
|
1899
|
+
|
|
1900
1900
|
const candidates = this._tree.query(range);
|
|
1901
1901
|
const results = [];
|
|
1902
|
-
|
|
1902
|
+
|
|
1903
1903
|
for (const p of candidates) {
|
|
1904
1904
|
const distance = this._haversine(center, {lat: p.y, lng: p.x});
|
|
1905
1905
|
if (distance <= maxDistance) {
|
|
@@ -1918,7 +1918,7 @@ class GeoIndex {
|
|
|
1918
1918
|
const h = (bounds.maxLat - bounds.minLat) / 2;
|
|
1919
1919
|
const x = bounds.minLng + w;
|
|
1920
1920
|
const y = bounds.minLat + h;
|
|
1921
|
-
|
|
1921
|
+
|
|
1922
1922
|
const candidates = this._tree.query({x, y, w, h});
|
|
1923
1923
|
return candidates.map(p => p.data);
|
|
1924
1924
|
}
|
|
@@ -2003,30 +2003,30 @@ class IndexManager {
|
|
|
2003
2003
|
// This prevents transaction timeouts caused by async crypto operations inside the loop
|
|
2004
2004
|
let lastKey = null;
|
|
2005
2005
|
const batchSize = 100; // Keep batch small for responsiveness
|
|
2006
|
-
|
|
2006
|
+
|
|
2007
2007
|
while (true) {
|
|
2008
2008
|
// 1. Fetch Batch (Transaction opens and closes here)
|
|
2009
2009
|
const batch = await this._collection._indexedDB.getBatch(
|
|
2010
|
-
this._collection._db,
|
|
2011
|
-
'documents',
|
|
2012
|
-
lastKey,
|
|
2010
|
+
this._collection._db,
|
|
2011
|
+
'documents',
|
|
2012
|
+
lastKey,
|
|
2013
2013
|
batchSize
|
|
2014
2014
|
);
|
|
2015
|
-
|
|
2015
|
+
|
|
2016
2016
|
if (batch.length === 0) break;
|
|
2017
|
-
|
|
2017
|
+
|
|
2018
2018
|
// 2. Process Batch (Async crypto operations safe here)
|
|
2019
2019
|
for (const docData of batch) {
|
|
2020
2020
|
lastKey = docData._id; // Update for next batch
|
|
2021
2021
|
let doc = docData;
|
|
2022
|
-
|
|
2022
|
+
|
|
2023
2023
|
if (docData.packedData) {
|
|
2024
2024
|
const d = new Document(docData, {
|
|
2025
|
-
compressed: docData._compressed,
|
|
2025
|
+
compressed: docData._compressed,
|
|
2026
2026
|
encrypted: docData._encrypted
|
|
2027
2027
|
}, this._serializer);
|
|
2028
2028
|
// This await is what killed the transaction before
|
|
2029
|
-
await d.unpack(this._collection.database.encryption);
|
|
2029
|
+
await d.unpack(this._collection.database.encryption);
|
|
2030
2030
|
doc = d.objectOutput();
|
|
2031
2031
|
}
|
|
2032
2032
|
|
|
@@ -2047,7 +2047,7 @@ class IndexManager {
|
|
|
2047
2047
|
|
|
2048
2048
|
this._addToIndex(indexData, value, doc._id, index.type);
|
|
2049
2049
|
}
|
|
2050
|
-
|
|
2050
|
+
|
|
2051
2051
|
// Optional: Yield to main thread briefly to prevent UI freeze
|
|
2052
2052
|
if (window.requestIdleCallback) await new Promise(r => window.requestIdleCallback(r));
|
|
2053
2053
|
}
|
|
@@ -2087,7 +2087,7 @@ class IndexManager {
|
|
|
2087
2087
|
break;
|
|
2088
2088
|
}
|
|
2089
2089
|
}
|
|
2090
|
-
|
|
2090
|
+
|
|
2091
2091
|
async _hashVal(val) {
|
|
2092
2092
|
const msg = new TextEncoder().encode(String(val));
|
|
2093
2093
|
const hash = await crypto.subtle.digest('SHA-256', msg);
|
|
@@ -2101,7 +2101,7 @@ class IndexManager {
|
|
|
2101
2101
|
|
|
2102
2102
|
let oldValue = oldDoc ? this._getFieldValue(oldDoc, index.fieldPath) : undefined;
|
|
2103
2103
|
let newValue = newDoc ? this._getFieldValue(newDoc, index.fieldPath) : undefined;
|
|
2104
|
-
|
|
2104
|
+
|
|
2105
2105
|
if (index.hashed) {
|
|
2106
2106
|
if (oldValue) oldValue = await this._hashVal(oldValue);
|
|
2107
2107
|
if (newValue) newValue = await this._hashVal(newValue);
|
|
@@ -2147,9 +2147,9 @@ class IndexManager {
|
|
|
2147
2147
|
if (!index || !indexData) {
|
|
2148
2148
|
throw new Error(`Index '${indexName}' not found`);
|
|
2149
2149
|
}
|
|
2150
|
-
|
|
2150
|
+
|
|
2151
2151
|
if (index.hashed && typeof queryOptions !== 'object') {
|
|
2152
|
-
|
|
2152
|
+
queryOptions = await this._hashVal(queryOptions);
|
|
2153
2153
|
}
|
|
2154
2154
|
|
|
2155
2155
|
return this._queryIndex(indexData, queryOptions, index.type);
|
|
@@ -2753,45 +2753,221 @@ class Document {
|
|
|
2753
2753
|
// ========================
|
|
2754
2754
|
|
|
2755
2755
|
class CollectionMetadata {
|
|
2756
|
-
constructor(name, data = {}) {
|
|
2756
|
+
constructor(name, data = {}, serializer, base64, dbName) {
|
|
2757
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
|
|
2758
2764
|
this.sizeKB = data.sizeKB || 0;
|
|
2759
2765
|
this.length = data.length || 0;
|
|
2760
2766
|
this.createdAt = data.createdAt || Date.now();
|
|
2761
2767
|
this.modifiedAt = data.modifiedAt || Date.now();
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
this.
|
|
2765
|
-
this.
|
|
2766
|
-
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
|
+
}
|
|
2767
2784
|
}
|
|
2768
2785
|
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
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
|
+
|
|
2772
2794
|
this.sizeKB += sizeKB;
|
|
2773
2795
|
this.length++;
|
|
2774
2796
|
this.modifiedAt = Date.now();
|
|
2797
|
+
this._scheduleSave();
|
|
2775
2798
|
}
|
|
2776
2799
|
|
|
2777
|
-
updateDocument(docId, newSizeKB, isPermanent, attachmentCount) {
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
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
|
+
|
|
2781
2809
|
this.modifiedAt = Date.now();
|
|
2810
|
+
this._scheduleSave();
|
|
2782
2811
|
}
|
|
2783
2812
|
|
|
2784
2813
|
removeDocument(docId) {
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
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
|
+
|
|
2788
2823
|
this.modifiedAt = Date.now();
|
|
2824
|
+
this._scheduleSave();
|
|
2789
2825
|
}
|
|
2790
2826
|
|
|
2827
|
+
// ---- Queries (instant from memory) ----
|
|
2828
|
+
|
|
2791
2829
|
getOldestNonPermanentDocuments(count) {
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
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();
|
|
2795
2971
|
}
|
|
2796
2972
|
}
|
|
2797
2973
|
|
|
@@ -2804,6 +2980,15 @@ class DatabaseMetadata {
|
|
|
2804
2980
|
this.totalSizeKB = data.totalSizeKB || 0;
|
|
2805
2981
|
this.totalLength = data.totalLength || 0;
|
|
2806
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
|
+
}
|
|
2807
2992
|
}
|
|
2808
2993
|
|
|
2809
2994
|
static load(dbName, serializer, base64) {
|
|
@@ -2821,7 +3006,54 @@ class DatabaseMetadata {
|
|
|
2821
3006
|
return new DatabaseMetadata(dbName, {}, serializer, base64);
|
|
2822
3007
|
}
|
|
2823
3008
|
|
|
2824
|
-
|
|
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() {
|
|
2825
3057
|
const key = `lacertadb_${this.name}_metadata`;
|
|
2826
3058
|
try {
|
|
2827
3059
|
const dataToStore = {
|
|
@@ -2833,6 +3065,7 @@ class DatabaseMetadata {
|
|
|
2833
3065
|
const serializedData = this._serializer.serialize(dataToStore);
|
|
2834
3066
|
const encodedData = this._base64.encode(serializedData);
|
|
2835
3067
|
localStorage.setItem(key, encodedData);
|
|
3068
|
+
this._dirty = false;
|
|
2836
3069
|
} catch (e) {
|
|
2837
3070
|
if (e.name === 'QuotaExceededError') {
|
|
2838
3071
|
console.error('CRITICAL: LocalStorage quota exceeded. Metadata not saved for db:', this.name);
|
|
@@ -2845,32 +3078,28 @@ class DatabaseMetadata {
|
|
|
2845
3078
|
}
|
|
2846
3079
|
}
|
|
2847
3080
|
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
createdAt: collectionMetadata.createdAt,
|
|
2853
|
-
modifiedAt: collectionMetadata.modifiedAt
|
|
2854
|
-
};
|
|
2855
|
-
this._recalculate();
|
|
2856
|
-
this.save();
|
|
3081
|
+
// Force immediate save (for critical operations like clearAll)
|
|
3082
|
+
save() {
|
|
3083
|
+
this._dirty = true;
|
|
3084
|
+
this._flushSync();
|
|
2857
3085
|
}
|
|
2858
3086
|
|
|
2859
|
-
|
|
2860
|
-
delete this.collections[collectionName];
|
|
2861
|
-
this._recalculate();
|
|
2862
|
-
this.save();
|
|
2863
|
-
}
|
|
3087
|
+
// ---- Lifecycle ----
|
|
2864
3088
|
|
|
2865
|
-
|
|
2866
|
-
this.
|
|
2867
|
-
this.
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
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;
|
|
2872
3102
|
}
|
|
2873
|
-
this.modifiedAt = Date.now();
|
|
2874
3103
|
}
|
|
2875
3104
|
}
|
|
2876
3105
|
|
|
@@ -3403,8 +3632,10 @@ class Collection {
|
|
|
3403
3632
|
}
|
|
3404
3633
|
});
|
|
3405
3634
|
|
|
3406
|
-
|
|
3407
|
-
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
|
+
);
|
|
3408
3639
|
|
|
3409
3640
|
await this._indexManager.loadIndexMetadata();
|
|
3410
3641
|
|
|
@@ -3917,7 +4148,11 @@ class Collection {
|
|
|
3917
4148
|
await this._indexedDB.clear(this._db, 'documents');
|
|
3918
4149
|
|
|
3919
4150
|
// Reset metadata
|
|
3920
|
-
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();
|
|
3921
4156
|
this.database.metadata.setCollection(this._metadata);
|
|
3922
4157
|
|
|
3923
4158
|
// Clear cache
|
|
@@ -3945,6 +4180,11 @@ class Collection {
|
|
|
3945
4180
|
}
|
|
3946
4181
|
|
|
3947
4182
|
destroy() {
|
|
4183
|
+
// Flush and destroy collection metadata
|
|
4184
|
+
if (this._metadata) {
|
|
4185
|
+
this._metadata.destroy();
|
|
4186
|
+
}
|
|
4187
|
+
|
|
3948
4188
|
// Clear the cleanup interval
|
|
3949
4189
|
if (this._cleanupInterval) {
|
|
3950
4190
|
clearInterval(this._cleanupInterval);
|
|
@@ -4031,7 +4271,7 @@ class Database {
|
|
|
4031
4271
|
const encMetaKey = `lacertadb_${this.name}_encryption`;
|
|
4032
4272
|
let existingMetadata = null;
|
|
4033
4273
|
const stored = localStorage.getItem(encMetaKey);
|
|
4034
|
-
|
|
4274
|
+
|
|
4035
4275
|
if (stored) {
|
|
4036
4276
|
const decoded = this._base64.decode(stored);
|
|
4037
4277
|
existingMetadata = this._serializer.deserialize(decoded);
|
|
@@ -4114,7 +4354,9 @@ class Database {
|
|
|
4114
4354
|
this._collections.set(name, collection);
|
|
4115
4355
|
|
|
4116
4356
|
if (!this._metadata.collections[name]) {
|
|
4117
|
-
this._metadata.setCollection(new CollectionMetadata(
|
|
4357
|
+
this._metadata.setCollection(new CollectionMetadata(
|
|
4358
|
+
name, {}, this._serializer, this._base64, this.name
|
|
4359
|
+
));
|
|
4118
4360
|
}
|
|
4119
4361
|
return collection;
|
|
4120
4362
|
}
|
|
@@ -4148,6 +4390,10 @@ class Database {
|
|
|
4148
4390
|
|
|
4149
4391
|
this._metadata.removeCollection(name);
|
|
4150
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
|
+
|
|
4151
4397
|
const dbName = `${this.name}_${name}`;
|
|
4152
4398
|
await new Promise((resolve, reject) => {
|
|
4153
4399
|
const deleteReq = indexedDB.deleteDatabase(dbName);
|
|
@@ -4246,6 +4492,7 @@ class Database {
|
|
|
4246
4492
|
async clearAll() {
|
|
4247
4493
|
await Promise.all([...this._collections.keys()].map(name => this.dropCollection(name)));
|
|
4248
4494
|
this._collections.clear();
|
|
4495
|
+
if (this._metadata) this._metadata.destroy();
|
|
4249
4496
|
this._metadata = new DatabaseMetadata(this.name, {}, this._serializer, this._base64);
|
|
4250
4497
|
this._metadata.save();
|
|
4251
4498
|
this._quickStore.clear();
|
|
@@ -4266,6 +4513,11 @@ class Database {
|
|
|
4266
4513
|
this._quickStore.destroy();
|
|
4267
4514
|
}
|
|
4268
4515
|
|
|
4516
|
+
// Destroy database metadata
|
|
4517
|
+
if (this._metadata) {
|
|
4518
|
+
this._metadata.destroy();
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4269
4521
|
// Destroy encryption
|
|
4270
4522
|
if (this._encryption) {
|
|
4271
4523
|
this._encryption.destroy();
|