@pixagram/lacerta-db 0.3.1 → 0.5.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/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
- * LacertaDB V4 - Complete Core Library (Repaired and Enhanced)
3
- * A powerful browser-based document database with encryption, compression, and OPFS support
4
- * @version 4.0.3 (Max Compatibility)
2
+ * LacertaDB V5.0.0 - Complete Production Library
3
+ * A powerful browser-based document database with encryption, compression, OPFS support,
4
+ * custom indexes, LRU caching, and database-level security for private keys
5
+ * @version 5.0.0
5
6
  * @license MIT
6
7
  */
7
8
 
8
9
  'use strict';
9
- // Note: These imports are for browser environments using a bundler (e.g., Webpack, Vite).
10
- // For direct browser usage, you would use an ES module import from a URL or local path.
10
+
11
+ // Dependencies - for browser environments using a bundler (e.g., Webpack, Vite)
11
12
  import TurboSerial from "@pixagram/turboserial";
12
13
  import TurboBase64 from "@pixagram/turbobase64";
13
14
 
@@ -20,9 +21,10 @@ const serializer = new TurboSerial({
20
21
  });
21
22
  const base64 = new TurboBase64();
22
23
 
23
- /**
24
- * Async Mutex for managing concurrent operations
25
- */
24
+ // ========================
25
+ // Async Mutex for managing concurrent operations
26
+ // ========================
27
+
26
28
  class AsyncMutex {
27
29
  constructor() {
28
30
  this._queue = [];
@@ -60,9 +62,10 @@ class AsyncMutex {
60
62
  }
61
63
  }
62
64
 
63
- /**
64
- * Custom error class for LacertaDB
65
- */
65
+ // ========================
66
+ // Custom error class for LacertaDB
67
+ // ========================
68
+
66
69
  class LacertaDBError extends Error {
67
70
  constructor(message, code, originalError) {
68
71
  super(message);
@@ -73,6 +76,269 @@ class LacertaDBError extends Error {
73
76
  }
74
77
  }
75
78
 
79
+ // ========================
80
+ // LRU Cache Implementation
81
+ // ========================
82
+
83
+ class LRUCache {
84
+ constructor(maxSize = 100, ttl = null) {
85
+ this.maxSize = maxSize;
86
+ this.ttl = ttl;
87
+ this.cache = new Map();
88
+ this.accessOrder = [];
89
+ this.timestamps = new Map();
90
+ }
91
+
92
+ get(key) {
93
+ if (!this.cache.has(key)) {
94
+ return null;
95
+ }
96
+
97
+ if (this.ttl) {
98
+ const timestamp = this.timestamps.get(key);
99
+ if (Date.now() - timestamp > this.ttl) {
100
+ this.delete(key);
101
+ return null;
102
+ }
103
+ }
104
+
105
+ const index = this.accessOrder.indexOf(key);
106
+ if (index > -1) {
107
+ this.accessOrder.splice(index, 1);
108
+ }
109
+ this.accessOrder.push(key);
110
+
111
+ return this.cache.get(key);
112
+ }
113
+
114
+ set(key, value) {
115
+ if (this.cache.has(key)) {
116
+ const index = this.accessOrder.indexOf(key);
117
+ if (index > -1) {
118
+ this.accessOrder.splice(index, 1);
119
+ }
120
+ }
121
+
122
+ this.cache.set(key, value);
123
+ this.accessOrder.push(key);
124
+ this.timestamps.set(key, Date.now());
125
+
126
+ while (this.cache.size > this.maxSize) {
127
+ const oldest = this.accessOrder.shift();
128
+ this.cache.delete(oldest);
129
+ this.timestamps.delete(oldest);
130
+ }
131
+ }
132
+
133
+ delete(key) {
134
+ const index = this.accessOrder.indexOf(key);
135
+ if (index > -1) {
136
+ this.accessOrder.splice(index, 1);
137
+ }
138
+ this.timestamps.delete(key);
139
+ return this.cache.delete(key);
140
+ }
141
+
142
+ clear() {
143
+ this.cache.clear();
144
+ this.accessOrder = [];
145
+ this.timestamps.clear();
146
+ }
147
+
148
+ has(key) {
149
+ if (this.ttl && this.cache.has(key)) {
150
+ const timestamp = this.timestamps.get(key);
151
+ if (Date.now() - timestamp > this.ttl) {
152
+ this.delete(key);
153
+ return false;
154
+ }
155
+ }
156
+ return this.cache.has(key);
157
+ }
158
+
159
+ get size() {
160
+ return this.cache.size;
161
+ }
162
+ }
163
+
164
+ // LFU (Least Frequently Used) Cache
165
+ class LFUCache {
166
+ constructor(maxSize = 100, ttl = null) {
167
+ this.maxSize = maxSize;
168
+ this.ttl = ttl;
169
+ this.cache = new Map();
170
+ this.frequencies = new Map();
171
+ this.timestamps = new Map();
172
+ }
173
+
174
+ get(key) {
175
+ if (!this.cache.has(key)) {
176
+ return null;
177
+ }
178
+
179
+ if (this.ttl) {
180
+ const timestamp = this.timestamps.get(key);
181
+ if (Date.now() - timestamp > this.ttl) {
182
+ this.delete(key);
183
+ return null;
184
+ }
185
+ }
186
+
187
+ this.frequencies.set(key, (this.frequencies.get(key) || 0) + 1);
188
+ return this.cache.get(key);
189
+ }
190
+
191
+ set(key, value) {
192
+ if (this.cache.has(key)) {
193
+ this.cache.set(key, value);
194
+ this.frequencies.set(key, (this.frequencies.get(key) || 0) + 1);
195
+ } else {
196
+ if (this.cache.size >= this.maxSize) {
197
+ let minFreq = Infinity;
198
+ let evictKey = null;
199
+ for (const [k, freq] of this.frequencies) {
200
+ if (freq < minFreq) {
201
+ minFreq = freq;
202
+ evictKey = k;
203
+ }
204
+ }
205
+ if (evictKey) {
206
+ this.delete(evictKey);
207
+ }
208
+ }
209
+
210
+ this.cache.set(key, value);
211
+ this.frequencies.set(key, 1);
212
+ this.timestamps.set(key, Date.now());
213
+ }
214
+ }
215
+
216
+ delete(key) {
217
+ this.frequencies.delete(key);
218
+ this.timestamps.delete(key);
219
+ return this.cache.delete(key);
220
+ }
221
+
222
+ clear() {
223
+ this.cache.clear();
224
+ this.frequencies.clear();
225
+ this.timestamps.clear();
226
+ }
227
+
228
+ has(key) {
229
+ return this.cache.has(key);
230
+ }
231
+
232
+ get size() {
233
+ return this.cache.size;
234
+ }
235
+ }
236
+
237
+ // TTL (Time To Live) Only Cache
238
+ class TTLCache {
239
+ constructor(ttl = 60000) {
240
+ this.ttl = ttl;
241
+ this.cache = new Map();
242
+ this.timers = new Map();
243
+ }
244
+
245
+ get(key) {
246
+ return this.cache.get(key) || null;
247
+ }
248
+
249
+ set(key, value) {
250
+ if (this.timers.has(key)) {
251
+ clearTimeout(this.timers.get(key));
252
+ }
253
+
254
+ this.cache.set(key, value);
255
+
256
+ const timer = setTimeout(() => {
257
+ this.delete(key);
258
+ }, this.ttl);
259
+ this.timers.set(key, timer);
260
+ }
261
+
262
+ delete(key) {
263
+ if (this.timers.has(key)) {
264
+ clearTimeout(this.timers.get(key));
265
+ this.timers.delete(key);
266
+ }
267
+ return this.cache.delete(key);
268
+ }
269
+
270
+ clear() {
271
+ for (const timer of this.timers.values()) {
272
+ clearTimeout(timer);
273
+ }
274
+ this.timers.clear();
275
+ this.cache.clear();
276
+ }
277
+
278
+ has(key) {
279
+ return this.cache.has(key);
280
+ }
281
+
282
+ get size() {
283
+ return this.cache.size;
284
+ }
285
+ }
286
+
287
+ // ========================
288
+ // Cache Strategy System
289
+ // ========================
290
+
291
+ class CacheStrategy {
292
+ constructor(config = {}) {
293
+ this.type = config.type || 'lru';
294
+ this.maxSize = config.maxSize || 100;
295
+ this.ttl = config.ttl || null;
296
+ this.enabled = config.enabled !== false;
297
+
298
+ this.cache = this.createCache();
299
+ }
300
+
301
+ createCache() {
302
+ switch (this.type) {
303
+ case 'lru':
304
+ return new LRUCache(this.maxSize, this.ttl);
305
+ case 'lfu':
306
+ return new LFUCache(this.maxSize, this.ttl);
307
+ case 'ttl':
308
+ return new TTLCache(this.ttl);
309
+ case 'none':
310
+ return null;
311
+ default:
312
+ return new LRUCache(this.maxSize, this.ttl);
313
+ }
314
+ }
315
+
316
+ get(key) {
317
+ if (!this.enabled || !this.cache) return null;
318
+ return this.cache.get(key);
319
+ }
320
+
321
+ set(key, value) {
322
+ if (!this.enabled || !this.cache) return;
323
+ this.cache.set(key, value);
324
+ }
325
+
326
+ delete(key) {
327
+ if (!this.enabled || !this.cache) return;
328
+ this.cache.delete(key);
329
+ }
330
+
331
+ clear() {
332
+ if (!this.enabled || !this.cache) return;
333
+ this.cache.clear();
334
+ }
335
+
336
+ updateStrategy(newConfig) {
337
+ Object.assign(this, newConfig);
338
+ this.cache = this.createCache();
339
+ }
340
+ }
341
+
76
342
  // ========================
77
343
  // Compression Utility
78
344
  // ========================
@@ -106,7 +372,6 @@ class BrowserCompressionUtility {
106
372
  }
107
373
  }
108
374
 
109
- // Fallback sync methods are simple pass-throughs
110
375
  compressSync(input) {
111
376
  if (!(input instanceof Uint8Array)) {
112
377
  throw new TypeError('Input must be Uint8Array');
@@ -123,7 +388,7 @@ class BrowserCompressionUtility {
123
388
  }
124
389
 
125
390
  // ========================
126
- // Encryption Utility (FIXED & IMPROVED)
391
+ // Encryption Utility
127
392
  // ========================
128
393
 
129
394
  class BrowserEncryptionUtility {
@@ -165,7 +430,6 @@ class BrowserEncryptionUtility {
165
430
  data
166
431
  );
167
432
 
168
- // The checksum was removed as AES-GCM provides this via an authentication tag.
169
433
  const result = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
170
434
  result.set(salt, 0);
171
435
  result.set(iv, salt.length);
@@ -216,15 +480,984 @@ class BrowserEncryptionUtility {
216
480
  encryptedData
217
481
  );
218
482
 
219
- // Checksum verification removed. crypto.subtle.decrypt will throw on failure.
220
483
  return new Uint8Array(decrypted);
221
484
  } catch (error) {
222
- // Provide a more specific error for failed decryption, which often indicates a wrong password.
223
485
  throw new LacertaDBError('Decryption failed. This may be due to an incorrect password or corrupted data.', 'DECRYPTION_FAILED', error);
224
486
  }
225
487
  }
226
488
  }
227
489
 
490
+ // ========================
491
+ // Database-Level Encryption
492
+ // ========================
493
+
494
+ class SecureDatabaseEncryption {
495
+ constructor() {
496
+ this.masterKey = null;
497
+ this.salt = null;
498
+ this.iterations = 1000000;
499
+ this.initialized = false;
500
+ }
501
+
502
+ async initialize(pin, salt = null) {
503
+ if (this.initialized) {
504
+ throw new Error('Database encryption already initialized');
505
+ }
506
+
507
+ this.salt = salt || crypto.getRandomValues(new Uint8Array(32));
508
+
509
+ const encoder = new TextEncoder();
510
+ const pinBuffer = encoder.encode(pin);
511
+
512
+ const keyMaterial = await crypto.subtle.importKey(
513
+ 'raw',
514
+ pinBuffer,
515
+ 'PBKDF2',
516
+ false,
517
+ ['deriveBits', 'deriveKey']
518
+ );
519
+
520
+ const derivedBits = await crypto.subtle.deriveBits(
521
+ {
522
+ name: 'PBKDF2',
523
+ salt: this.salt,
524
+ iterations: this.iterations,
525
+ hash: 'SHA-512'
526
+ },
527
+ keyMaterial,
528
+ 512
529
+ );
530
+
531
+ const derivedArray = new Uint8Array(derivedBits);
532
+
533
+ const encKeyBytes = derivedArray.slice(0, 32);
534
+ const hmacKeyBytes = derivedArray.slice(32, 64);
535
+
536
+ this.encKey = await crypto.subtle.importKey(
537
+ 'raw',
538
+ encKeyBytes,
539
+ { name: 'AES-GCM', length: 256 },
540
+ false,
541
+ ['encrypt', 'decrypt']
542
+ );
543
+
544
+ this.hmacKey = await crypto.subtle.importKey(
545
+ 'raw',
546
+ hmacKeyBytes,
547
+ { name: 'HMAC', hash: 'SHA-256' },
548
+ false,
549
+ ['sign', 'verify']
550
+ );
551
+
552
+ this.initialized = true;
553
+
554
+ return base64.encode(this.salt);
555
+ }
556
+
557
+ async encrypt(data) {
558
+ if (!this.initialized) {
559
+ throw new Error('Database encryption not initialized');
560
+ }
561
+
562
+ let dataBytes;
563
+ if (typeof data === 'string') {
564
+ dataBytes = new TextEncoder().encode(data);
565
+ } else if (data instanceof Uint8Array) {
566
+ dataBytes = data;
567
+ } else {
568
+ dataBytes = serializer.serialize(data);
569
+ }
570
+
571
+ const iv = crypto.getRandomValues(new Uint8Array(12));
572
+
573
+ const encryptedData = await crypto.subtle.encrypt(
574
+ { name: 'AES-GCM', iv },
575
+ this.encKey,
576
+ dataBytes
577
+ );
578
+
579
+ const hmacData = new Uint8Array(iv.length + encryptedData.byteLength);
580
+ hmacData.set(iv, 0);
581
+ hmacData.set(new Uint8Array(encryptedData), iv.length);
582
+
583
+ const hmac = await crypto.subtle.sign(
584
+ 'HMAC',
585
+ this.hmacKey,
586
+ hmacData
587
+ );
588
+
589
+ const result = new Uint8Array(
590
+ iv.length + encryptedData.byteLength + 32
591
+ );
592
+ result.set(iv, 0);
593
+ result.set(new Uint8Array(encryptedData), iv.length);
594
+ result.set(new Uint8Array(hmac), iv.length + encryptedData.byteLength);
595
+
596
+ return result;
597
+ }
598
+
599
+ async decrypt(encryptedPackage) {
600
+ if (!this.initialized) {
601
+ throw new Error('Database encryption not initialized');
602
+ }
603
+
604
+ if (!(encryptedPackage instanceof Uint8Array)) {
605
+ throw new TypeError('Encrypted data must be Uint8Array');
606
+ }
607
+
608
+ const iv = encryptedPackage.slice(0, 12);
609
+ const hmac = encryptedPackage.slice(-32);
610
+ const encryptedData = encryptedPackage.slice(12, -32);
611
+
612
+ const hmacData = new Uint8Array(iv.length + encryptedData.length);
613
+ hmacData.set(iv, 0);
614
+ hmacData.set(encryptedData, iv.length);
615
+
616
+ const isValid = await crypto.subtle.verify(
617
+ 'HMAC',
618
+ this.hmacKey,
619
+ hmac,
620
+ hmacData
621
+ );
622
+
623
+ if (!isValid) {
624
+ throw new Error('HMAC verification failed - data may be tampered');
625
+ }
626
+
627
+ const decryptedData = await crypto.subtle.decrypt(
628
+ { name: 'AES-GCM', iv },
629
+ this.encKey,
630
+ encryptedData
631
+ );
632
+
633
+ return new Uint8Array(decryptedData);
634
+ }
635
+
636
+ async encryptPrivateKey(privateKey, additionalAuth = '') {
637
+ if (!this.initialized) {
638
+ throw new Error('Database encryption not initialized');
639
+ }
640
+
641
+ const encoder = new TextEncoder();
642
+ const authData = encoder.encode(additionalAuth);
643
+
644
+ let keyData;
645
+ if (typeof privateKey === 'string') {
646
+ keyData = encoder.encode(privateKey);
647
+ } else if (privateKey instanceof Uint8Array) {
648
+ keyData = privateKey;
649
+ } else {
650
+ keyData = encoder.encode(JSON.stringify(privateKey));
651
+ }
652
+
653
+ const iv = crypto.getRandomValues(new Uint8Array(16));
654
+
655
+ const encryptedKey = await crypto.subtle.encrypt(
656
+ {
657
+ name: 'AES-GCM',
658
+ iv,
659
+ additionalData: authData,
660
+ tagLength: 128
661
+ },
662
+ this.encKey,
663
+ keyData
664
+ );
665
+
666
+ const authLength = new Uint32Array([authData.length]);
667
+ const result = new Uint8Array(
668
+ 16 + 4 + authData.length + encryptedKey.byteLength
669
+ );
670
+
671
+ result.set(iv, 0);
672
+ result.set(new Uint8Array(authLength.buffer), 16);
673
+ result.set(authData, 20);
674
+ result.set(new Uint8Array(encryptedKey), 20 + authData.length);
675
+
676
+ return base64.encode(result);
677
+ }
678
+
679
+ async decryptPrivateKey(encryptedKeyString, additionalAuth = '') {
680
+ if (!this.initialized) {
681
+ throw new Error('Database encryption not initialized');
682
+ }
683
+
684
+ const encryptedPackage = base64.decode(encryptedKeyString);
685
+
686
+ const iv = encryptedPackage.slice(0, 16);
687
+ const authLengthBytes = encryptedPackage.slice(16, 20);
688
+ const authLength = new Uint32Array(authLengthBytes.buffer)[0];
689
+ const authData = encryptedPackage.slice(20, 20 + authLength);
690
+ const encryptedKey = encryptedPackage.slice(20 + authLength);
691
+
692
+ const encoder = new TextEncoder();
693
+ const expectedAuth = encoder.encode(additionalAuth);
694
+
695
+ if (!this.arrayEquals(authData, expectedAuth)) {
696
+ throw new Error('Additional authentication data mismatch');
697
+ }
698
+
699
+ const decryptedKey = await crypto.subtle.decrypt(
700
+ {
701
+ name: 'AES-GCM',
702
+ iv,
703
+ additionalData: authData,
704
+ tagLength: 128
705
+ },
706
+ this.encKey,
707
+ encryptedKey
708
+ );
709
+
710
+ return new TextDecoder().decode(decryptedKey);
711
+ }
712
+
713
+ static generateSecurePIN(length = 6) {
714
+ const digits = new Uint8Array(length);
715
+ crypto.getRandomValues(digits);
716
+ return Array.from(digits)
717
+ .map(b => (b % 10).toString())
718
+ .join('');
719
+ }
720
+
721
+ destroy() {
722
+ this.masterKey = null;
723
+ this.encKey = null;
724
+ this.hmacKey = null;
725
+ this.salt = null;
726
+ this.initialized = false;
727
+ }
728
+
729
+ arrayEquals(a, b) {
730
+ if (a.length !== b.length) return false;
731
+ for (let i = 0; i < a.length; i++) {
732
+ if (a[i] !== b[i]) return false;
733
+ }
734
+ return true;
735
+ }
736
+
737
+ async changePin(oldPin, newPin) {
738
+ if (!this.initialized) {
739
+ throw new Error('Database encryption not initialized');
740
+ }
741
+
742
+ const currentSalt = this.salt;
743
+
744
+ this.destroy();
745
+ await this.initialize(oldPin, currentSalt);
746
+
747
+ this.destroy();
748
+ const newSalt = await this.initialize(newPin);
749
+
750
+ return newSalt;
751
+ }
752
+
753
+ exportMetadata() {
754
+ if (!this.salt) {
755
+ throw new Error('No encryption metadata to export');
756
+ }
757
+
758
+ return {
759
+ salt: base64.encode(this.salt),
760
+ iterations: this.iterations,
761
+ algorithm: 'AES-GCM-256',
762
+ kdf: 'PBKDF2-SHA-512'
763
+ };
764
+ }
765
+
766
+ importMetadata(metadata) {
767
+ if (!metadata.salt) {
768
+ throw new Error('Invalid encryption metadata');
769
+ }
770
+
771
+ this.salt = base64.decode(metadata.salt);
772
+ this.iterations = metadata.iterations || 1000000;
773
+
774
+ return true;
775
+ }
776
+ }
777
+
778
+ // ========================
779
+ // B-Tree Index Implementation
780
+ // ========================
781
+
782
+ class BTreeNode {
783
+ constructor(order, leaf) {
784
+ this.keys = new Array(2 * order - 1);
785
+ this.values = new Array(2 * order - 1);
786
+ this.children = new Array(2 * order);
787
+ this.n = 0;
788
+ this.leaf = leaf;
789
+ this.order = order;
790
+ }
791
+
792
+ search(key) {
793
+ let i = 0;
794
+ while (i < this.n && key > this.keys[i]) {
795
+ i++;
796
+ }
797
+
798
+ if (i < this.n && key === this.keys[i]) {
799
+ return this.values[i];
800
+ }
801
+
802
+ if (this.leaf) {
803
+ return null;
804
+ }
805
+
806
+ return this.children[i].search(key);
807
+ }
808
+
809
+ rangeSearch(min, max, results) {
810
+ let i = 0;
811
+
812
+ while (i < this.n) {
813
+ if (!this.leaf && this.children[i]) {
814
+ this.children[i].rangeSearch(min, max, results);
815
+ }
816
+
817
+ if (this.keys[i] >= min && this.keys[i] <= max) {
818
+ this.values[i].forEach(v => results.push(v));
819
+ }
820
+
821
+ i++;
822
+ }
823
+
824
+ if (!this.leaf && this.children[i]) {
825
+ this.children[i].rangeSearch(min, max, results);
826
+ }
827
+ }
828
+
829
+ insertNonFull(key, value) {
830
+ let i = this.n - 1;
831
+
832
+ if (this.leaf) {
833
+ while (i >= 0 && this.keys[i] > key) {
834
+ this.keys[i + 1] = this.keys[i];
835
+ this.values[i + 1] = this.values[i];
836
+ i--;
837
+ }
838
+
839
+ if (i >= 0 && this.keys[i] === key) {
840
+ this.values[i].add(value);
841
+ } else {
842
+ this.keys[i + 1] = key;
843
+ this.values[i + 1] = new Set([value]);
844
+ this.n++;
845
+ }
846
+ } else {
847
+ while (i >= 0 && this.keys[i] > key) {
848
+ i--;
849
+ }
850
+
851
+ if (i >= 0 && this.keys[i] === key) {
852
+ this.values[i].add(value);
853
+ return;
854
+ }
855
+
856
+ i++;
857
+ if (this.children[i].n === 2 * this.order - 1) {
858
+ this.splitChild(i, this.children[i]);
859
+ if (this.keys[i] < key) {
860
+ i++;
861
+ }
862
+ }
863
+ this.children[i].insertNonFull(key, value);
864
+ }
865
+ }
866
+
867
+ splitChild(i, y) {
868
+ const z = new BTreeNode(this.order, y.leaf);
869
+ z.n = this.order - 1;
870
+
871
+ for (let j = 0; j < this.order - 1; j++) {
872
+ z.keys[j] = y.keys[j + this.order];
873
+ z.values[j] = y.values[j + this.order];
874
+ }
875
+
876
+ if (!y.leaf) {
877
+ for (let j = 0; j < this.order; j++) {
878
+ z.children[j] = y.children[j + this.order];
879
+ }
880
+ }
881
+
882
+ y.n = this.order - 1;
883
+
884
+ for (let j = this.n; j >= i + 1; j--) {
885
+ this.children[j + 1] = this.children[j];
886
+ }
887
+
888
+ this.children[i + 1] = z;
889
+
890
+ for (let j = this.n - 1; j >= i; j--) {
891
+ this.keys[j + 1] = this.keys[j];
892
+ this.values[j + 1] = this.values[j];
893
+ }
894
+
895
+ this.keys[i] = y.keys[this.order - 1];
896
+ this.values[i] = y.values[this.order - 1];
897
+ this.n++;
898
+ }
899
+
900
+ remove(key, value) {
901
+ let i = 0;
902
+ while (i < this.n && key > this.keys[i]) {
903
+ i++;
904
+ }
905
+
906
+ if (i < this.n && key === this.keys[i]) {
907
+ this.values[i].delete(value);
908
+ if (this.values[i].size === 0) {
909
+ for (let j = i; j < this.n - 1; j++) {
910
+ this.keys[j] = this.keys[j + 1];
911
+ this.values[j] = this.values[j + 1];
912
+ }
913
+ this.n--;
914
+ }
915
+ } else if (!this.leaf) {
916
+ this.children[i].remove(key, value);
917
+ }
918
+ }
919
+ }
920
+
921
+ class BTreeIndex {
922
+ constructor(order = 4) {
923
+ this.root = null;
924
+ this.order = order;
925
+ this.size = 0;
926
+ }
927
+
928
+ insert(key, value) {
929
+ if (!this.root) {
930
+ this.root = new BTreeNode(this.order, true);
931
+ this.root.keys[0] = key;
932
+ this.root.values[0] = new Set([value]);
933
+ this.root.n = 1;
934
+ } else {
935
+ if (this.root.n === 2 * this.order - 1) {
936
+ const s = new BTreeNode(this.order, false);
937
+ s.children[0] = this.root;
938
+ s.splitChild(0, this.root);
939
+
940
+ let i = 0;
941
+ if (s.keys[0] < key) i++;
942
+ s.children[i].insertNonFull(key, value);
943
+
944
+ this.root = s;
945
+ } else {
946
+ this.root.insertNonFull(key, value);
947
+ }
948
+ }
949
+ this.size++;
950
+ }
951
+
952
+ find(key) {
953
+ if (!this.root) return [];
954
+ const values = this.root.search(key);
955
+ return values ? Array.from(values) : [];
956
+ }
957
+
958
+ contains(key) {
959
+ if (!this.root) return false;
960
+ return this.root.search(key) !== null;
961
+ }
962
+
963
+ range(min, max) {
964
+ if (!this.root) return [];
965
+ const results = [];
966
+ this.root.rangeSearch(min, max, results);
967
+ return results;
968
+ }
969
+
970
+ rangeFrom(min) {
971
+ if (!this.root) return [];
972
+ const results = [];
973
+ this.root.rangeSearch(min, Infinity, results);
974
+ return results;
975
+ }
976
+
977
+ rangeTo(max) {
978
+ if (!this.root) return [];
979
+ const results = [];
980
+ this.root.rangeSearch(-Infinity, max, results);
981
+ return results;
982
+ }
983
+
984
+ remove(key, value) {
985
+ if (!this.root) return;
986
+ this.root.remove(key, value);
987
+ if (this.root.n === 0) {
988
+ if (!this.root.leaf && this.root.children[0]) {
989
+ this.root = this.root.children[0];
990
+ }
991
+ }
992
+ this.size--;
993
+ }
994
+ }
995
+
996
+ // Text Index for full-text search
997
+ class TextIndex {
998
+ constructor() {
999
+ this.invertedIndex = new Map();
1000
+ this.documentTexts = new Map();
1001
+ }
1002
+
1003
+ addDocument(text, docId) {
1004
+ if (typeof text !== 'string') return;
1005
+
1006
+ this.documentTexts.set(docId, text);
1007
+ const tokens = this.tokenize(text);
1008
+
1009
+ for (const token of tokens) {
1010
+ if (!this.invertedIndex.has(token)) {
1011
+ this.invertedIndex.set(token, new Set());
1012
+ }
1013
+ this.invertedIndex.get(token).add(docId);
1014
+ }
1015
+ }
1016
+
1017
+ removeDocument(docId) {
1018
+ const text = this.documentTexts.get(docId);
1019
+ if (!text) return;
1020
+
1021
+ const tokens = this.tokenize(text);
1022
+ for (const token of tokens) {
1023
+ const docs = this.invertedIndex.get(token);
1024
+ if (docs) {
1025
+ docs.delete(docId);
1026
+ if (docs.size === 0) {
1027
+ this.invertedIndex.delete(token);
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ this.documentTexts.delete(docId);
1033
+ }
1034
+
1035
+ search(query) {
1036
+ const tokens = this.tokenize(query);
1037
+ if (tokens.length === 0) return [];
1038
+
1039
+ let results = null;
1040
+ for (const token of tokens) {
1041
+ const docs = this.invertedIndex.get(token);
1042
+ if (!docs || docs.size === 0) {
1043
+ return [];
1044
+ }
1045
+
1046
+ if (results === null) {
1047
+ results = new Set(docs);
1048
+ } else {
1049
+ results = new Set([...results].filter(x => docs.has(x)));
1050
+ }
1051
+ }
1052
+
1053
+ return results ? Array.from(results) : [];
1054
+ }
1055
+
1056
+ tokenize(text) {
1057
+ return text.toLowerCase()
1058
+ .replace(/[^\w\s]/g, ' ')
1059
+ .split(/\s+/)
1060
+ .filter(token => token.length > 2);
1061
+ }
1062
+
1063
+ get size() {
1064
+ return this.documentTexts.size;
1065
+ }
1066
+ }
1067
+
1068
+ // Geo Index for spatial queries
1069
+ class GeoIndex {
1070
+ constructor() {
1071
+ this.points = new Map();
1072
+ }
1073
+
1074
+ addPoint(coords, docId) {
1075
+ if (!coords || typeof coords.lat !== 'number' || typeof coords.lng !== 'number') {
1076
+ return;
1077
+ }
1078
+ this.points.set(docId, coords);
1079
+ }
1080
+
1081
+ removePoint(docId) {
1082
+ this.points.delete(docId);
1083
+ }
1084
+
1085
+ findNear(center, maxDistance) {
1086
+ const results = [];
1087
+
1088
+ for (const [docId, coords] of this.points) {
1089
+ const distance = this.haversine(center, coords);
1090
+ if (distance <= maxDistance) {
1091
+ results.push({ docId, distance });
1092
+ }
1093
+ }
1094
+
1095
+ return results.sort((a, b) => a.distance - b.distance)
1096
+ .map(r => r.docId);
1097
+ }
1098
+
1099
+ findWithin(bounds) {
1100
+ const results = [];
1101
+
1102
+ for (const [docId, coords] of this.points) {
1103
+ if (this.isWithinBounds(coords, bounds)) {
1104
+ results.push(docId);
1105
+ }
1106
+ }
1107
+
1108
+ return results;
1109
+ }
1110
+
1111
+ haversine(coord1, coord2) {
1112
+ const R = 6371;
1113
+ const dLat = this.toRad(coord2.lat - coord1.lat);
1114
+ const dLng = this.toRad(coord2.lng - coord1.lng);
1115
+
1116
+ const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
1117
+ Math.cos(this.toRad(coord1.lat)) * Math.cos(this.toRad(coord2.lat)) *
1118
+ Math.sin(dLng/2) * Math.sin(dLng/2);
1119
+
1120
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
1121
+ return R * c;
1122
+ }
1123
+
1124
+ toRad(deg) {
1125
+ return deg * (Math.PI / 180);
1126
+ }
1127
+
1128
+ isWithinBounds(coords, bounds) {
1129
+ return coords.lat >= bounds.minLat && coords.lat <= bounds.maxLat &&
1130
+ coords.lng >= bounds.minLng && coords.lng <= bounds.maxLng;
1131
+ }
1132
+
1133
+ get size() {
1134
+ return this.points.size;
1135
+ }
1136
+ }
1137
+
1138
+ // ========================
1139
+ // Index Manager
1140
+ // ========================
1141
+
1142
+ class IndexManager {
1143
+ constructor(collection) {
1144
+ this.collection = collection;
1145
+ this.indexes = new Map();
1146
+ this.indexData = new Map();
1147
+ }
1148
+
1149
+ async createIndex(fieldPath, options = {}) {
1150
+ const indexName = options.name || fieldPath;
1151
+
1152
+ if (this.indexes.has(indexName)) {
1153
+ throw new Error(`Index '${indexName}' already exists`);
1154
+ }
1155
+
1156
+ const index = {
1157
+ fieldPath,
1158
+ unique: options.unique || false,
1159
+ sparse: options.sparse || false,
1160
+ type: options.type || 'btree',
1161
+ collation: options.collation || null,
1162
+ createdAt: Date.now()
1163
+ };
1164
+
1165
+ this.indexes.set(indexName, index);
1166
+
1167
+ await this.rebuildIndex(indexName);
1168
+
1169
+ this.saveIndexMetadata();
1170
+
1171
+ return indexName;
1172
+ }
1173
+
1174
+ async rebuildIndex(indexName) {
1175
+ const index = this.indexes.get(indexName);
1176
+ if (!index) {
1177
+ throw new Error(`Index '${indexName}' not found`);
1178
+ }
1179
+
1180
+ const indexData = this.createIndexStructure(index.type);
1181
+
1182
+ const allDocs = await this.collection.getAll();
1183
+
1184
+ for (const doc of allDocs) {
1185
+ const value = this.getFieldValue(doc, index.fieldPath);
1186
+
1187
+ if (index.sparse && (value === null || value === undefined)) {
1188
+ continue;
1189
+ }
1190
+
1191
+ if (index.unique && indexData.has && indexData.has(value)) {
1192
+ throw new Error(`Unique constraint violation on index '${indexName}'`);
1193
+ }
1194
+
1195
+ this.addToIndex(indexData, value, doc._id, index.type);
1196
+ }
1197
+
1198
+ this.indexData.set(indexName, indexData);
1199
+ }
1200
+
1201
+ createIndexStructure(type) {
1202
+ switch (type) {
1203
+ case 'btree':
1204
+ return new BTreeIndex();
1205
+ case 'hash':
1206
+ return new Map();
1207
+ case 'text':
1208
+ return new TextIndex();
1209
+ case 'geo':
1210
+ return new GeoIndex();
1211
+ default:
1212
+ return new Map();
1213
+ }
1214
+ }
1215
+
1216
+ addToIndex(indexData, value, docId, type) {
1217
+ switch (type) {
1218
+ case 'btree':
1219
+ indexData.insert(value, docId);
1220
+ break;
1221
+ case 'hash':
1222
+ if (!indexData.has(value)) {
1223
+ indexData.set(value, new Set());
1224
+ }
1225
+ indexData.get(value).add(docId);
1226
+ break;
1227
+ case 'text':
1228
+ indexData.addDocument(value, docId);
1229
+ break;
1230
+ case 'geo':
1231
+ indexData.addPoint(value, docId);
1232
+ break;
1233
+ }
1234
+ }
1235
+
1236
+ async updateIndex(indexName, docId, oldValue, newValue) {
1237
+ const index = this.indexes.get(indexName);
1238
+ const indexData = this.indexData.get(indexName);
1239
+
1240
+ if (!index || !indexData) return;
1241
+
1242
+ if (oldValue !== undefined && oldValue !== null) {
1243
+ this.removeFromIndex(indexData, oldValue, docId, index.type);
1244
+ }
1245
+
1246
+ if (newValue !== undefined && newValue !== null) {
1247
+ if (index.unique && this.indexContains(indexData, newValue, index.type)) {
1248
+ throw new Error(`Unique constraint violation on index '${indexName}'`);
1249
+ }
1250
+ this.addToIndex(indexData, newValue, docId, index.type);
1251
+ }
1252
+ }
1253
+
1254
+ removeFromIndex(indexData, value, docId, type) {
1255
+ switch (type) {
1256
+ case 'btree':
1257
+ indexData.remove(value, docId);
1258
+ break;
1259
+ case 'hash':
1260
+ const docs = indexData.get(value);
1261
+ if (docs) {
1262
+ docs.delete(docId);
1263
+ if (docs.size === 0) {
1264
+ indexData.delete(value);
1265
+ }
1266
+ }
1267
+ break;
1268
+ case 'text':
1269
+ indexData.removeDocument(docId);
1270
+ break;
1271
+ case 'geo':
1272
+ indexData.removePoint(docId);
1273
+ break;
1274
+ }
1275
+ }
1276
+
1277
+ indexContains(indexData, value, type) {
1278
+ switch (type) {
1279
+ case 'btree':
1280
+ return indexData.contains(value);
1281
+ case 'hash':
1282
+ return indexData.has(value);
1283
+ case 'text':
1284
+ return false;
1285
+ case 'geo':
1286
+ return false;
1287
+ default:
1288
+ return false;
1289
+ }
1290
+ }
1291
+
1292
+ async query(indexName, queryOptions) {
1293
+ const index = this.indexes.get(indexName);
1294
+ const indexData = this.indexData.get(indexName);
1295
+
1296
+ if (!index || !indexData) {
1297
+ throw new Error(`Index '${indexName}' not found`);
1298
+ }
1299
+
1300
+ return this.queryIndex(indexData, queryOptions, index.type);
1301
+ }
1302
+
1303
+ queryIndex(indexData, options, type) {
1304
+ switch (type) {
1305
+ case 'btree':
1306
+ return this.queryBTree(indexData, options);
1307
+ case 'hash':
1308
+ return this.queryHash(indexData, options);
1309
+ case 'text':
1310
+ return this.queryText(indexData, options);
1311
+ case 'geo':
1312
+ return this.queryGeo(indexData, options);
1313
+ default:
1314
+ return [];
1315
+ }
1316
+ }
1317
+
1318
+ queryBTree(indexData, options) {
1319
+ const results = new Set();
1320
+
1321
+ if (options.$eq !== undefined) {
1322
+ const docs = indexData.find(options.$eq);
1323
+ docs.forEach(doc => results.add(doc));
1324
+ }
1325
+
1326
+ if (options.$gte !== undefined && options.$lte !== undefined) {
1327
+ const docs = indexData.range(options.$gte, options.$lte);
1328
+ docs.forEach(doc => results.add(doc));
1329
+ } else if (options.$gte !== undefined) {
1330
+ const docs = indexData.rangeFrom(options.$gte);
1331
+ docs.forEach(doc => results.add(doc));
1332
+ } else if (options.$lte !== undefined) {
1333
+ const docs = indexData.rangeTo(options.$lte);
1334
+ docs.forEach(doc => results.add(doc));
1335
+ }
1336
+
1337
+ return Array.from(results);
1338
+ }
1339
+
1340
+ queryHash(indexData, options) {
1341
+ if (options.$eq !== undefined) {
1342
+ const docs = indexData.get(options.$eq);
1343
+ return docs ? Array.from(docs) : [];
1344
+ }
1345
+
1346
+ if (options.$in !== undefined) {
1347
+ const results = new Set();
1348
+ for (const value of options.$in) {
1349
+ const docs = indexData.get(value);
1350
+ if (docs) {
1351
+ docs.forEach(doc => results.add(doc));
1352
+ }
1353
+ }
1354
+ return Array.from(results);
1355
+ }
1356
+
1357
+ return [];
1358
+ }
1359
+
1360
+ queryText(indexData, options) {
1361
+ if (options.$search) {
1362
+ return indexData.search(options.$search);
1363
+ }
1364
+ return [];
1365
+ }
1366
+
1367
+ queryGeo(indexData, options) {
1368
+ if (options.$near) {
1369
+ return indexData.findNear(
1370
+ options.$near.coordinates,
1371
+ options.$near.maxDistance || 1000
1372
+ );
1373
+ }
1374
+ if (options.$within) {
1375
+ return indexData.findWithin(options.$within);
1376
+ }
1377
+ return [];
1378
+ }
1379
+
1380
+ dropIndex(indexName) {
1381
+ this.indexes.delete(indexName);
1382
+ this.indexData.delete(indexName);
1383
+ this.saveIndexMetadata();
1384
+ }
1385
+
1386
+ getFieldValue(doc, path) {
1387
+ const parts = path.split('.');
1388
+ let value = doc;
1389
+ for (const part of parts) {
1390
+ if (value === null || value === undefined) {
1391
+ return undefined;
1392
+ }
1393
+ value = value[part];
1394
+ }
1395
+ return value;
1396
+ }
1397
+
1398
+ saveIndexMetadata() {
1399
+ const metadata = {
1400
+ indexes: Array.from(this.indexes.entries()).map(([name, index]) => ({
1401
+ name,
1402
+ ...index
1403
+ }))
1404
+ };
1405
+
1406
+ const key = `lacertadb_${this.collection.database.name}_${this.collection.name}_indexes`;
1407
+ const serialized = serializer.serialize(metadata);
1408
+ const encoded = base64.encode(serialized);
1409
+ localStorage.setItem(key, encoded);
1410
+ }
1411
+
1412
+ async loadIndexMetadata() {
1413
+ const key = `lacertadb_${this.collection.database.name}_${this.collection.name}_indexes`;
1414
+ const stored = localStorage.getItem(key);
1415
+
1416
+ if (!stored) return;
1417
+
1418
+ try {
1419
+ const decoded = base64.decode(stored);
1420
+ const metadata = serializer.deserialize(decoded);
1421
+
1422
+ for (const indexDef of metadata.indexes) {
1423
+ const { name, ...index } = indexDef;
1424
+ this.indexes.set(name, index);
1425
+ }
1426
+
1427
+ for (const indexName of this.indexes.keys()) {
1428
+ await this.rebuildIndex(indexName);
1429
+ }
1430
+ } catch (error) {
1431
+ console.error('Failed to load index metadata:', error);
1432
+ }
1433
+ }
1434
+
1435
+ getIndexStats() {
1436
+ const stats = {};
1437
+ for (const [name, index] of this.indexes) {
1438
+ const indexData = this.indexData.get(name);
1439
+ stats[name] = {
1440
+ ...index,
1441
+ size: indexData ? indexData.size || indexData.length || 0 : 0,
1442
+ memoryUsage: this.estimateMemoryUsage(indexData)
1443
+ };
1444
+ }
1445
+ return stats;
1446
+ }
1447
+
1448
+ estimateMemoryUsage(indexData) {
1449
+ if (!indexData) return 0;
1450
+
1451
+ if (indexData instanceof Map) {
1452
+ return indexData.size * 100;
1453
+ }
1454
+ if (indexData instanceof BTreeIndex) {
1455
+ return indexData.size * 120;
1456
+ }
1457
+ return 0;
1458
+ }
1459
+ }
1460
+
228
1461
  // ========================
229
1462
  // OPFS (Origin Private File System) Utility
230
1463
  // ========================
@@ -298,7 +1531,6 @@ class OPFSUtility {
298
1531
  });
299
1532
  } catch (error) {
300
1533
  console.error(`Failed to get attachment: ${attachmentInfo.path}`, error);
301
- // Optionally, collect errors and return them
302
1534
  }
303
1535
  }
304
1536
  return attachments;
@@ -311,7 +1543,6 @@ class OPFSUtility {
311
1543
  const collDir = await dbDir.getDirectoryHandle(collectionName);
312
1544
  await collDir.removeEntry(documentId, { recursive: true });
313
1545
  } catch (error) {
314
- // Ignore "NotFoundError" as the directory might already be gone
315
1546
  if (error.name !== 'NotFoundError') {
316
1547
  console.error(`Failed to delete attachments for ${documentId}:`, error);
317
1548
  }
@@ -498,7 +1729,6 @@ class Document {
498
1729
  this.data = data.data || {};
499
1730
  this.packedData = data.packedData || null;
500
1731
 
501
- // Utilities can be passed in or instantiated. For simplicity, we keep instantiation here.
502
1732
  this.compression = new BrowserCompressionUtility();
503
1733
  this.encryption = new BrowserEncryptionUtility();
504
1734
  this.password = options.password || null;
@@ -743,7 +1973,6 @@ class DatabaseMetadata {
743
1973
  class Settings {
744
1974
  constructor(dbName, data = {}) {
745
1975
  this.dbName = dbName;
746
- // Replaced `??` with ternary operator for compatibility
747
1976
  this.sizeLimitKB = data.sizeLimitKB != null ? data.sizeLimitKB : Infinity;
748
1977
  const defaultBuffer = this.sizeLimitKB === Infinity ? 0 : this.sizeLimitKB * 0.8;
749
1978
  this.bufferLimitKB = data.bufferLimitKB != null ? data.bufferLimitKB : defaultBuffer;
@@ -886,7 +2115,6 @@ class QuickStore {
886
2115
  class QueryEngine {
887
2116
  constructor() {
888
2117
  this.operators = {
889
- // Comparison
890
2118
  '$eq': (a, b) => a === b,
891
2119
  '$ne': (a, b) => a !== b,
892
2120
  '$gt': (a, b) => a > b,
@@ -896,22 +2124,18 @@ class QueryEngine {
896
2124
  '$in': (a, b) => Array.isArray(b) && b.includes(a),
897
2125
  '$nin': (a, b) => Array.isArray(b) && !b.includes(a),
898
2126
 
899
- // Logical
900
2127
  '$and': (doc, conditions) => conditions.every(cond => this.evaluate(doc, cond)),
901
2128
  '$or': (doc, conditions) => conditions.some(cond => this.evaluate(doc, cond)),
902
2129
  '$not': (doc, condition) => !this.evaluate(doc, condition),
903
2130
  '$nor': (doc, conditions) => !conditions.some(cond => this.evaluate(doc, cond)),
904
2131
 
905
- // Element
906
2132
  '$exists': (value, exists) => (value !== undefined) === exists,
907
2133
  '$type': (value, type) => typeof value === type,
908
2134
 
909
- // Array
910
2135
  '$all': (arr, values) => Array.isArray(arr) && values.every(v => arr.includes(v)),
911
2136
  '$elemMatch': (arr, condition) => Array.isArray(arr) && arr.some(elem => this.evaluate({ value: elem }, { value: condition })),
912
2137
  '$size': (arr, size) => Array.isArray(arr) && arr.length === size,
913
2138
 
914
- // String
915
2139
  '$regex': (str, pattern) => {
916
2140
  if (typeof str !== 'string') return false;
917
2141
  try {
@@ -929,14 +2153,11 @@ class QueryEngine {
929
2153
  for (const key in query) {
930
2154
  const value = query[key];
931
2155
  if (key.startsWith('$')) {
932
- // Logical operator at root level
933
2156
  const operator = this.operators[key];
934
2157
  if (!operator || !operator(doc, value)) return false;
935
2158
  } else {
936
- // Field-level query
937
2159
  const fieldValue = this.getFieldValue(doc, key);
938
2160
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
939
- // Operator-based comparison
940
2161
  for (const op in value) {
941
2162
  if (op.startsWith('$')) {
942
2163
  const operatorFn = this.operators[op];
@@ -946,7 +2167,6 @@ class QueryEngine {
946
2167
  }
947
2168
  }
948
2169
  } else {
949
- // Direct equality comparison
950
2170
  if (fieldValue !== value) return false;
951
2171
  }
952
2172
  }
@@ -955,7 +2175,6 @@ class QueryEngine {
955
2175
  }
956
2176
 
957
2177
  getFieldValue(doc, path) {
958
- // Replaced optional chaining with a loop for compatibility
959
2178
  let current = doc;
960
2179
  for (const part of path.split('.')) {
961
2180
  if (current === null || current === undefined) {
@@ -968,7 +2187,6 @@ class QueryEngine {
968
2187
  }
969
2188
  const queryEngine = new QueryEngine();
970
2189
 
971
-
972
2190
  // ========================
973
2191
  // Aggregation Pipeline
974
2192
  // ========================
@@ -984,13 +2202,10 @@ class AggregationPipeline {
984
2202
  const value = projection[key];
985
2203
  if (value === 1 || value === true) {
986
2204
  projected[key] = queryEngine.getFieldValue(doc, key);
987
- } else if (typeof value === 'object') {
988
- // Handle computed fields if necessary
989
2205
  } else if (typeof value === 'string' && value.startsWith('$')) {
990
2206
  projected[key] = queryEngine.getFieldValue(doc, value.substring(1));
991
2207
  }
992
2208
  }
993
- // Handle exclusion projection
994
2209
  if (Object.values(projection).some(v => v === 0 || v === false)) {
995
2210
  const exclusions = Object.keys(projection).filter(k => projection[k] === 0 || projection[k] === false);
996
2211
  const included = { ...doc };
@@ -1002,7 +2217,7 @@ class AggregationPipeline {
1002
2217
 
1003
2218
  '$sort': (docs, sortSpec) => [...docs].sort((a, b) => {
1004
2219
  for (const key in sortSpec) {
1005
- const order = sortSpec[key]; // 1 for asc, -1 for desc
2220
+ const order = sortSpec[key];
1006
2221
  const aVal = queryEngine.getFieldValue(a, key);
1007
2222
  const bVal = queryEngine.getFieldValue(b, key);
1008
2223
  if (aVal < bVal) return -order;
@@ -1022,7 +2237,7 @@ class AggregationPipeline {
1022
2237
  for (const doc of docs) {
1023
2238
  const groupKey = typeof idField === 'string' ?
1024
2239
  queryEngine.getFieldValue(doc, idField.replace('$', '')) :
1025
- JSON.stringify(idField); // Fallback for complex IDs
2240
+ JSON.stringify(idField);
1026
2241
 
1027
2242
  if (!groups.has(groupKey)) {
1028
2243
  groups.set(groupKey, { _id: groupKey, docs: [] });
@@ -1227,7 +2442,6 @@ class PerformanceMonitor {
1227
2442
  recordCacheMiss() { this.metrics.cacheMisses++; }
1228
2443
 
1229
2444
  collectMetrics() {
1230
- // Replaced optional chaining with `&&` for compatibility
1231
2445
  if (performance && performance.memory) {
1232
2446
  this.metrics.memoryUsage.push({
1233
2447
  used: performance.memory.usedJSHeapSize,
@@ -1246,7 +2460,6 @@ class PerformanceMonitor {
1246
2460
  const totalCacheOps = this.metrics.cacheHits + this.metrics.cacheMisses;
1247
2461
  const cacheHitRate = totalCacheOps > 0 ? (this.metrics.cacheHits / totalCacheOps) * 100 : 0;
1248
2462
 
1249
- // Replaced `.at(-1)` with classic index access for compatibility
1250
2463
  const latestMemory = this.metrics.memoryUsage.length > 0 ? this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1] : null;
1251
2464
  const memoryUsageMB = latestMemory ? latestMemory.used / (1024 * 1024) : 0;
1252
2465
 
@@ -1271,7 +2484,7 @@ class PerformanceMonitor {
1271
2484
  if (this.metrics.memoryUsage.length > 10) {
1272
2485
  const recent = this.metrics.memoryUsage.slice(-10);
1273
2486
  const trend = recent[recent.length - 1].used - recent[0].used;
1274
- if (trend > 10 * 1024 * 1024) { // > 10MB increase
2487
+ if (trend > 10 * 1024 * 1024) {
1275
2488
  tips.push('Memory usage is increasing rapidly. Check for memory leaks or consider batch processing.');
1276
2489
  }
1277
2490
  }
@@ -1294,8 +2507,16 @@ class Collection {
1294
2507
  this.opfs = new OPFSUtility();
1295
2508
  this.cleanupInterval = null;
1296
2509
  this.events = new Map();
1297
- this.queryCache = new Map();
1298
- this.cacheTimeout = 60000;
2510
+
2511
+ // Enhanced features
2512
+ this.indexManager = new IndexManager(this);
2513
+ this.cacheStrategy = new CacheStrategy({
2514
+ type: 'lru',
2515
+ maxSize: 100,
2516
+ ttl: 60000,
2517
+ enabled: true
2518
+ });
2519
+
1299
2520
  this.performanceMonitor = database.performanceMonitor;
1300
2521
  }
1301
2522
 
@@ -1306,18 +2527,38 @@ class Collection {
1306
2527
  const store = db.createObjectStore('documents', { keyPath: '_id' });
1307
2528
  store.createIndex('modified', '_modified', { unique: false });
1308
2529
  }
1309
- // Future index creation logic would go here during version bumps
1310
2530
  });
1311
2531
 
1312
2532
  const metadataData = this.database.metadata.collections[this.name];
1313
2533
  this.metadata = new CollectionMetadata(this.name, metadataData);
1314
2534
 
2535
+ // Load indexes
2536
+ await this.indexManager.loadIndexMetadata();
2537
+
1315
2538
  if (this.settings.freeSpaceEvery > 0) {
1316
2539
  this.cleanupInterval = setInterval(() => this.freeSpace(), this.settings.freeSpaceEvery);
1317
2540
  }
1318
2541
  return this;
1319
2542
  }
1320
2543
 
2544
+ // Index methods
2545
+ async createIndex(fieldPath, options = {}) {
2546
+ return await this.indexManager.createIndex(fieldPath, options);
2547
+ }
2548
+
2549
+ async dropIndex(indexName) {
2550
+ return this.indexManager.dropIndex(indexName);
2551
+ }
2552
+
2553
+ async getIndexes() {
2554
+ return this.indexManager.getIndexStats();
2555
+ }
2556
+
2557
+ // Cache configuration
2558
+ configureCacheStrategy(config) {
2559
+ this.cacheStrategy.updateStrategy(config);
2560
+ }
2561
+
1321
2562
  async add(documentData, options = {}) {
1322
2563
  await this.trigger('beforeAdd', documentData);
1323
2564
 
@@ -1342,13 +2583,22 @@ class Collection {
1342
2583
  const dbOutput = doc.databaseOutput();
1343
2584
  await this.indexedDB.add(this.db, 'documents', dbOutput);
1344
2585
 
2586
+ // Update indexes
2587
+ const fullDoc = doc.objectOutput();
2588
+ for (const [indexName, index] of this.indexManager.indexes) {
2589
+ const value = this.indexManager.getFieldValue(fullDoc, index.fieldPath);
2590
+ if (value !== undefined && value !== null) {
2591
+ await this.indexManager.updateIndex(indexName, doc._id, undefined, value);
2592
+ }
2593
+ }
2594
+
1345
2595
  const sizeKB = dbOutput.packedData.byteLength / 1024;
1346
2596
  this.metadata.addDocument(doc._id, sizeKB, doc._permanent, doc._attachments.length);
1347
2597
  this.database.metadata.setCollection(this.metadata);
1348
2598
 
1349
2599
  await this.checkSpaceLimit();
1350
2600
  await this.trigger('afterAdd', doc);
1351
- this.queryCache.clear();
2601
+ this.cacheStrategy.clear();
1352
2602
  return doc._id;
1353
2603
  }
1354
2604
 
@@ -1409,9 +2659,9 @@ class Collection {
1409
2659
  const existingDoc = new Document(stored, { password: options.password });
1410
2660
  if (stored.packedData) await existingDoc.unpack();
1411
2661
 
2662
+ const oldDocOutput = existingDoc.objectOutput();
1412
2663
  const updatedData = { ...existingDoc.data, ...updates };
1413
2664
 
1414
- // Replaced `??` with ternary operator for compatibility
1415
2665
  const doc = new Document({
1416
2666
  _id: docId,
1417
2667
  _created: stored._created,
@@ -1441,21 +2691,55 @@ class Collection {
1441
2691
  const dbOutput = doc.databaseOutput();
1442
2692
  await this.indexedDB.put(this.db, 'documents', dbOutput);
1443
2693
 
2694
+ // Update indexes
2695
+ const newDocOutput = doc.objectOutput();
2696
+ for (const [indexName, index] of this.indexManager.indexes) {
2697
+ const oldValue = this.indexManager.getFieldValue(oldDocOutput, index.fieldPath);
2698
+ const newValue = this.indexManager.getFieldValue(newDocOutput, index.fieldPath);
2699
+
2700
+ if (oldValue !== newValue) {
2701
+ await this.indexManager.updateIndex(indexName, doc._id, oldValue, newValue);
2702
+ }
2703
+ }
2704
+
1444
2705
  const sizeKB = dbOutput.packedData.byteLength / 1024;
1445
2706
  this.metadata.updateDocument(doc._id, sizeKB, doc._permanent, doc._attachments.length);
1446
2707
  this.database.metadata.setCollection(this.metadata);
1447
2708
 
1448
2709
  await this.trigger('afterUpdate', doc);
1449
- this.queryCache.clear();
2710
+ this.cacheStrategy.clear();
1450
2711
  return doc._id;
1451
2712
  }
1452
2713
 
1453
- async delete(docId) {
2714
+ async delete(docId, options = {}) {
1454
2715
  await this.trigger('beforeDelete', docId);
1455
2716
 
1456
2717
  const doc = await this.indexedDB.get(this.db, 'documents', docId);
1457
- if (!doc) throw new LacertaDBError('Document not found for deletion', 'DOCUMENT_NOT_FOUND');
1458
- if (doc._permanent) throw new LacertaDBError('Cannot delete a permanent document', 'INVALID_OPERATION');
2718
+ if (!doc) {
2719
+ throw new LacertaDBError('Document not found for deletion', 'DOCUMENT_NOT_FOUND');
2720
+ }
2721
+
2722
+ if (doc._permanent && !options.force) {
2723
+ throw new LacertaDBError(
2724
+ 'Cannot delete a permanent document. Use options.force = true to force deletion.',
2725
+ 'PERMANENT_DOCUMENT_PROTECTION'
2726
+ );
2727
+ }
2728
+
2729
+ if (doc._permanent && options.force) {
2730
+ console.warn(`Force deleting permanent document: ${docId}`);
2731
+ }
2732
+
2733
+ // Get full document for index cleanup
2734
+ const fullDoc = await this.get(docId);
2735
+
2736
+ // Remove from indexes
2737
+ for (const [indexName, index] of this.indexManager.indexes) {
2738
+ const value = this.indexManager.getFieldValue(fullDoc, index.fieldPath);
2739
+ if (value !== undefined && value !== null) {
2740
+ await this.indexManager.updateIndex(indexName, docId, value, undefined);
2741
+ }
2742
+ }
1459
2743
 
1460
2744
  await this.indexedDB.delete(this.db, 'documents', docId);
1461
2745
  const attachments = doc._attachments;
@@ -1467,36 +2751,63 @@ class Collection {
1467
2751
  this.database.metadata.setCollection(this.metadata);
1468
2752
 
1469
2753
  await this.trigger('afterDelete', docId);
1470
- this.queryCache.clear();
2754
+ this.cacheStrategy.clear();
1471
2755
  }
1472
2756
 
1473
2757
  async query(filter = {}, options = {}) {
1474
2758
  const startTime = performance.now();
2759
+
2760
+ // Try cache first
1475
2761
  const cacheKey = base64.encode(serializer.serialize({ filter, options }));
1476
- const cached = this.queryCache.get(cacheKey);
1477
-
1478
- if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
2762
+ const cached = this.cacheStrategy.get(cacheKey);
2763
+
2764
+ if (cached) {
1479
2765
  if (this.performanceMonitor) this.performanceMonitor.recordCacheHit();
1480
- return cached.data;
2766
+ return cached;
1481
2767
  }
1482
2768
  if (this.performanceMonitor) this.performanceMonitor.recordCacheMiss();
1483
-
1484
- let results = await this.getAll(options);
1485
- if (Object.keys(filter).length > 0) {
1486
- results = results.filter(doc => queryEngine.evaluate(doc, filter));
2769
+
2770
+ // Check if we can use an index
2771
+ let results;
2772
+ let usedIndex = false;
2773
+
2774
+ for (const [indexName, index] of this.indexManager.indexes) {
2775
+ const fieldValue = filter[index.fieldPath];
2776
+ if (fieldValue !== undefined) {
2777
+ const docIds = await this.indexManager.query(indexName, fieldValue);
2778
+ results = await Promise.all(
2779
+ docIds.map(id => this.get(id).catch(() => null))
2780
+ );
2781
+ results = results.filter(Boolean);
2782
+ usedIndex = true;
2783
+ break;
2784
+ }
1487
2785
  }
1488
-
2786
+
2787
+ // Fall back to full scan if no index available
2788
+ if (!usedIndex) {
2789
+ results = await this.getAll(options);
2790
+ if (Object.keys(filter).length > 0) {
2791
+ results = results.filter(doc => queryEngine.evaluate(doc, filter));
2792
+ }
2793
+ }
2794
+
2795
+ // Apply sorting, pagination, projection
1489
2796
  if (options.sort) results = aggregationPipeline.stages.$sort(results, options.sort);
1490
2797
  if (options.skip) results = aggregationPipeline.stages.$skip(results, options.skip);
1491
2798
  if (options.limit) results = aggregationPipeline.stages.$limit(results, options.limit);
1492
2799
  if (options.projection) results = aggregationPipeline.stages.$project(results, options.projection);
1493
-
1494
- if (this.performanceMonitor) this.performanceMonitor.recordOperation('query', performance.now() - startTime);
1495
-
1496
- this.queryCache.set(cacheKey, { data: results, timestamp: Date.now() });
1497
- if (this.queryCache.size > 100) {
1498
- this.queryCache.delete(this.queryCache.keys().next().value);
2800
+
2801
+ if (this.performanceMonitor) {
2802
+ this.performanceMonitor.recordOperation(
2803
+ usedIndex ? 'indexed-query' : 'full-scan-query',
2804
+ performance.now() - startTime
2805
+ );
1499
2806
  }
2807
+
2808
+ // Store in cache
2809
+ this.cacheStrategy.set(cacheKey, results);
2810
+
1500
2811
  return results;
1501
2812
  }
1502
2813
 
@@ -1519,7 +2830,7 @@ class Collection {
1519
2830
  return results;
1520
2831
  }
1521
2832
 
1522
- batchUpdate(updates, options) {
2833
+ async batchUpdate(updates, options) {
1523
2834
  return Promise.all(updates.map(update =>
1524
2835
  this.update(update.id, update.data, options)
1525
2836
  .then(id => ({ success: true, id }))
@@ -1527,19 +2838,37 @@ class Collection {
1527
2838
  ));
1528
2839
  }
1529
2840
 
1530
- batchDelete(ids) {
1531
- return Promise.all(ids.map(id =>
1532
- this.delete(id)
2841
+ async batchDelete(items) {
2842
+ const normalizedItems = items.map(item => {
2843
+ if (typeof item === 'string') {
2844
+ return { id: item, options: {} };
2845
+ }
2846
+ return { id: item.id, options: item.options || {} };
2847
+ });
2848
+
2849
+ return Promise.all(normalizedItems.map(({ id, options }) =>
2850
+ this.delete(id, options)
1533
2851
  .then(() => ({ success: true, id }))
1534
2852
  .catch(error => ({ success: false, id, error: error.message }))
1535
2853
  ));
1536
2854
  }
1537
2855
 
1538
- async clear() {
1539
- await this.indexedDB.clear(this.db, 'documents');
1540
- this.metadata = new CollectionMetadata(this.name);
1541
- this.database.metadata.setCollection(this.metadata);
1542
- this.queryCache.clear();
2856
+ async clear(options = {}) {
2857
+ if (options.force) {
2858
+ await this.indexedDB.clear(this.db, 'documents');
2859
+ this.metadata = new CollectionMetadata(this.name);
2860
+ this.database.metadata.setCollection(this.metadata);
2861
+ this.cacheStrategy.clear();
2862
+
2863
+ // Clear all indexes
2864
+ for (const indexName of this.indexManager.indexes.keys()) {
2865
+ await this.indexManager.rebuildIndex(indexName);
2866
+ }
2867
+ } else {
2868
+ const allDocs = await this.getAll();
2869
+ const nonPermanentDocs = allDocs.filter(doc => !doc._permanent);
2870
+ await this.batchDelete(nonPermanentDocs.map(doc => doc._id));
2871
+ }
1543
2872
  }
1544
2873
 
1545
2874
  async checkSpaceLimit() {
@@ -1575,7 +2904,9 @@ class Collection {
1575
2904
  }
1576
2905
  }
1577
2906
 
1578
- clearCache() { this.queryCache.clear(); }
2907
+ clearCache() {
2908
+ this.cacheStrategy.clear();
2909
+ }
1579
2910
 
1580
2911
  destroy() {
1581
2912
  clearInterval(this.cleanupInterval);
@@ -1597,15 +2928,115 @@ class Database {
1597
2928
  this.settings = null;
1598
2929
  this.quickStore = null;
1599
2930
  this.performanceMonitor = performanceMonitor;
2931
+
2932
+ // Database-level encryption
2933
+ this.encryption = null;
2934
+ this.isEncrypted = false;
1600
2935
  }
1601
2936
 
1602
- async init() {
2937
+ async init(options = {}) {
1603
2938
  this.metadata = DatabaseMetadata.load(this.name);
1604
2939
  this.settings = Settings.load(this.name);
1605
2940
  this.quickStore = new QuickStore(this.name);
2941
+
2942
+ // Initialize encryption if PIN provided
2943
+ if (options.pin) {
2944
+ await this.initializeEncryption(options.pin, options.salt);
2945
+ }
2946
+
1606
2947
  return this;
1607
2948
  }
1608
2949
 
2950
+ async initializeEncryption(pin, salt = null) {
2951
+ this.encryption = new SecureDatabaseEncryption();
2952
+
2953
+ // Check if we have existing salt in metadata
2954
+ const encMetaKey = `lacertadb_${this.name}_encryption`;
2955
+ let existingSalt = null;
2956
+
2957
+ if (!salt) {
2958
+ const stored = localStorage.getItem(encMetaKey);
2959
+ if (stored) {
2960
+ const metadata = JSON.parse(stored);
2961
+ existingSalt = metadata.salt;
2962
+ }
2963
+ }
2964
+
2965
+ const saltBase64 = await this.encryption.initialize(
2966
+ pin,
2967
+ existingSalt ? base64.decode(existingSalt) : salt
2968
+ );
2969
+
2970
+ // Store salt for future sessions
2971
+ if (!existingSalt) {
2972
+ const metadata = this.encryption.exportMetadata();
2973
+ localStorage.setItem(encMetaKey, JSON.stringify(metadata));
2974
+ }
2975
+
2976
+ this.isEncrypted = true;
2977
+ return saltBase64;
2978
+ }
2979
+
2980
+ async changePin(oldPin, newPin) {
2981
+ if (!this.isEncrypted) {
2982
+ throw new Error('Database is not encrypted');
2983
+ }
2984
+
2985
+ const newSalt = await this.encryption.changePin(oldPin, newPin);
2986
+
2987
+ // Update stored metadata
2988
+ const encMetaKey = `lacertadb_${this.name}_encryption`;
2989
+ const metadata = this.encryption.exportMetadata();
2990
+ localStorage.setItem(encMetaKey, JSON.stringify(metadata));
2991
+
2992
+ return newSalt;
2993
+ }
2994
+
2995
+ async storePrivateKey(keyName, privateKey, additionalAuth = '') {
2996
+ if (!this.isEncrypted) {
2997
+ throw new Error('Database must be encrypted to store private keys');
2998
+ }
2999
+
3000
+ const encryptedKey = await this.encryption.encryptPrivateKey(
3001
+ privateKey,
3002
+ additionalAuth
3003
+ );
3004
+
3005
+ // Store in a special collection
3006
+ let keyStore = await this.getCollection('__private_keys__').catch(() => null);
3007
+ if (!keyStore) {
3008
+ keyStore = await this.createCollection('__private_keys__', {
3009
+ encrypted: false // Already encrypted at database level
3010
+ });
3011
+ }
3012
+
3013
+ await keyStore.add({
3014
+ name: keyName,
3015
+ key: encryptedKey,
3016
+ createdAt: Date.now()
3017
+ }, {
3018
+ id: keyName,
3019
+ permanent: true
3020
+ });
3021
+
3022
+ return true;
3023
+ }
3024
+
3025
+ async getPrivateKey(keyName, additionalAuth = '') {
3026
+ if (!this.isEncrypted) {
3027
+ throw new Error('Database must be encrypted to retrieve private keys');
3028
+ }
3029
+
3030
+ const keyStore = await this.getCollection('__private_keys__');
3031
+ const doc = await keyStore.get(keyName);
3032
+
3033
+ if (!doc) {
3034
+ throw new Error(`Private key '${keyName}' not found`);
3035
+ }
3036
+
3037
+ return await this.encryption.decryptPrivateKey(doc.key, additionalAuth);
3038
+ }
3039
+
1609
3040
  async createCollection(name, options) {
1610
3041
  if (this.collections.has(name)) {
1611
3042
  throw new LacertaDBError(`Collection '${name}' already exists.`, 'COLLECTION_EXISTS');
@@ -1637,7 +3068,7 @@ class Database {
1637
3068
  async dropCollection(name) {
1638
3069
  if (this.collections.has(name)) {
1639
3070
  const collection = this.collections.get(name);
1640
- await collection.clear();
3071
+ await collection.clear({ force: true });
1641
3072
  collection.destroy();
1642
3073
  this.collections.delete(name);
1643
3074
  }
@@ -1672,11 +3103,13 @@ class Database {
1672
3103
  };
1673
3104
  }
1674
3105
 
1675
- updateSettings(newSettings) { this.settings.updateSettings(newSettings); }
3106
+ updateSettings(newSettings) {
3107
+ this.settings.updateSettings(newSettings);
3108
+ }
1676
3109
 
1677
3110
  async export(format = 'json', password = null) {
1678
3111
  const data = {
1679
- version: '4.0.3',
3112
+ version: '5.0.0',
1680
3113
  database: this.name,
1681
3114
  timestamp: Date.now(),
1682
3115
  collections: {}
@@ -1739,6 +3172,9 @@ class Database {
1739
3172
  destroy() {
1740
3173
  this.collections.forEach(collection => collection.destroy());
1741
3174
  this.collections.clear();
3175
+ if (this.encryption) {
3176
+ this.encryption.destroy();
3177
+ }
1742
3178
  }
1743
3179
  }
1744
3180
 
@@ -1752,15 +3188,19 @@ export class LacertaDB {
1752
3188
  this.performanceMonitor = new PerformanceMonitor();
1753
3189
  }
1754
3190
 
1755
- async getDatabase(name) {
3191
+ async getDatabase(name, options = {}) {
1756
3192
  if (!this.databases.has(name)) {
1757
3193
  const db = new Database(name, this.performanceMonitor);
1758
- await db.init();
3194
+ await db.init(options);
1759
3195
  this.databases.set(name, db);
1760
3196
  }
1761
3197
  return this.databases.get(name);
1762
3198
  }
1763
3199
 
3200
+ async getSecureDatabase(name, pin, salt = null) {
3201
+ return this.getDatabase(name, { pin, salt });
3202
+ }
3203
+
1764
3204
  async dropDatabase(name) {
1765
3205
  if (this.databases.has(name)) {
1766
3206
  const db = this.databases.get(name);
@@ -1769,7 +3209,7 @@ export class LacertaDB {
1769
3209
  this.databases.delete(name);
1770
3210
  }
1771
3211
 
1772
- ['metadata', 'settings', 'version'].forEach(suffix => {
3212
+ ['metadata', 'settings', 'version', 'encryption'].forEach(suffix => {
1773
3213
  localStorage.removeItem(`lacertadb_${name}_${suffix}`);
1774
3214
  });
1775
3215
  const quickStore = new QuickStore(name);
@@ -1780,10 +3220,11 @@ export class LacertaDB {
1780
3220
  const dbNames = new Set();
1781
3221
  for (let i = 0; i < localStorage.length; i++) {
1782
3222
  const key = localStorage.key(i);
1783
- // Replaced optional chaining with `&&` for compatibility
1784
3223
  if (key && key.startsWith('lacertadb_')) {
1785
- const dbName = key.split('_')[1];
1786
- dbNames.add(dbName);
3224
+ const parts = key.split('_');
3225
+ if (parts.length >= 2) {
3226
+ dbNames.add(parts[1]);
3227
+ }
1787
3228
  }
1788
3229
  }
1789
3230
  return [...dbNames];
@@ -1791,7 +3232,7 @@ export class LacertaDB {
1791
3232
 
1792
3233
  async createBackup(password = null) {
1793
3234
  const backup = {
1794
- version: '4.0.3',
3235
+ version: '5.0.0',
1795
3236
  timestamp: Date.now(),
1796
3237
  databases: {}
1797
3238
  };
@@ -1841,7 +3282,10 @@ export class LacertaDB {
1841
3282
  }
1842
3283
  }
1843
3284
 
1844
- // Export all major components for advanced usage
3285
+ // ========================
3286
+ // Export all components
3287
+ // ========================
3288
+
1845
3289
  export {
1846
3290
  Database,
1847
3291
  Collection,
@@ -1849,5 +3293,17 @@ export {
1849
3293
  MigrationManager,
1850
3294
  PerformanceMonitor,
1851
3295
  LacertaDBError,
1852
- OPFSUtility
1853
- };
3296
+ OPFSUtility,
3297
+ IndexManager,
3298
+ CacheStrategy,
3299
+ LRUCache,
3300
+ LFUCache,
3301
+ TTLCache,
3302
+ BTreeIndex,
3303
+ TextIndex,
3304
+ GeoIndex,
3305
+ SecureDatabaseEncryption
3306
+ };
3307
+
3308
+ // Default export
3309
+ export default LacertaDB;