@pixagram/lacerta-db 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,36 +1,105 @@
1
1
  /**
2
- * LacertaDB V0.7.0 - Production Library with QuickStore
3
- * Added: QuickStore feature, private property/method conventions
4
- * @version 0.7.0
2
+ * LacertaDB V0.8.0 - Production Library with QuickStore
3
+ *
4
+ * A high-performance, browser-based document database with support for:
5
+ * - IndexedDB storage with connection pooling
6
+ * - Multiple caching strategies (LRU, LFU, TTL)
7
+ * - Full-text search and geospatial indexing
8
+ * - Document encryption and compression
9
+ * - Binary attachments via OPFS (Origin Private File System)
10
+ * - MongoDB-like query syntax and aggregation pipeline
11
+ * - Schema migrations and versioning
12
+ * - QuickStore for fast localStorage-based operations
13
+ *
14
+ * @module LacertaDB
15
+ * @version 0.8.0
5
16
  * @license MIT
17
+ * @author Pixagram SA
6
18
  */
7
19
 
8
20
  'use strict';
9
21
 
10
- // Dependencies - for browser environments using a bundler (e.g., Webpack, Vite)
22
+ // ========================
23
+ // Polyfills
24
+ // ========================
25
+
26
+ /**
27
+ * Polyfill for requestIdleCallback if not available in the browser.
28
+ * Falls back to setTimeout with 0ms delay for environments that don't support it.
29
+ */
30
+ if (!window.requestIdleCallback) {
31
+ window.requestIdleCallback = function(callback) {
32
+ return setTimeout(callback, 0);
33
+ };
34
+ window.cancelIdleCallback = clearTimeout;
35
+ }
36
+
37
+ // ========================
38
+ // Dependencies
39
+ // ========================
40
+
41
+ /**
42
+ * Import external serialization and encoding utilities.
43
+ * TurboSerial provides efficient binary serialization with compression and deduplication.
44
+ * TurboBase64 provides fast base64 encoding/decoding.
45
+ */
11
46
  import TurboSerial from "@pixagram/turboserial";
12
47
  import TurboBase64 from "@pixagram/turbobase64";
13
48
 
49
+ /**
50
+ * Global serializer instance configured for optimal performance.
51
+ * @type {TurboSerial}
52
+ */
14
53
  const serializer = new TurboSerial({
15
- compression: true,
16
- deduplication: true,
17
- shareArrayBuffers: true,
18
- simdOptimization: true,
19
- detectCircular: true
54
+ compression: true, // Enable built-in compression
55
+ deduplication: true, // Deduplicate repeated values
56
+ simdOptimization: true, // Use SIMD instructions when available
57
+ detectCircular: true // Handle circular references
20
58
  });
59
+
60
+ /**
61
+ * Global Base64 encoder/decoder instance.
62
+ * @type {TurboBase64}
63
+ */
21
64
  const base64 = new TurboBase64();
22
65
 
23
66
  // ========================
24
67
  // Quick Store (localStorage based)
25
68
  // ========================
26
69
 
70
+ /**
71
+ * QuickStore provides fast, synchronous access to small amounts of data using localStorage.
72
+ * Ideal for frequently accessed metadata, user preferences, or small documents
73
+ * where the overhead of IndexedDB is not justified.
74
+ *
75
+ * Data is serialized using TurboSerial and encoded as Base64 for localStorage storage.
76
+ *
77
+ * @class QuickStore
78
+ * @example
79
+ * const store = new QuickStore('myDatabase');
80
+ * store.add('user1', { name: 'John', age: 30 });
81
+ * const user = store.get('user1');
82
+ * console.log(user.name); // 'John'
83
+ */
27
84
  class QuickStore {
85
+ /**
86
+ * Creates a new QuickStore instance.
87
+ * @param {string} dbName - The database name used to namespace localStorage keys
88
+ */
28
89
  constructor(dbName) {
90
+ /** @private @type {string} Database name */
29
91
  this._dbName = dbName;
92
+ /** @private @type {string} Prefix for all localStorage keys */
30
93
  this._keyPrefix = `lacertadb_${dbName}_quickstore_`;
94
+ /** @private @type {string} Key for storing the document index */
31
95
  this._indexKey = `${this._keyPrefix}index`;
32
96
  }
33
97
 
98
+ /**
99
+ * Reads the index of all stored document IDs from localStorage.
100
+ * @private
101
+ * @returns {Array<string>} Array of document IDs
102
+ */
34
103
  _readIndex() {
35
104
  const indexStr = localStorage.getItem(this._indexKey);
36
105
  if (!indexStr) return [];
@@ -42,12 +111,24 @@ class QuickStore {
42
111
  }
43
112
  }
44
113
 
114
+ /**
115
+ * Writes the document ID index to localStorage.
116
+ * @private
117
+ * @param {Array<string>} index - Array of document IDs to store
118
+ */
45
119
  _writeIndex(index) {
46
120
  const serializedIndex = serializer.serialize(index);
47
121
  const encodedIndex = base64.encode(serializedIndex);
48
122
  localStorage.setItem(this._indexKey, encodedIndex);
49
123
  }
50
124
 
125
+ /**
126
+ * Adds or replaces a document in the QuickStore.
127
+ * @param {string} docId - Unique identifier for the document
128
+ * @param {Object} data - Document data to store
129
+ * @returns {boolean} True if successful, false otherwise
130
+ * @throws {LacertaDBError} If localStorage quota is exceeded
131
+ */
51
132
  add(docId, data) {
52
133
  const key = `${this._keyPrefix}data_${docId}`;
53
134
  try {
@@ -69,6 +150,11 @@ class QuickStore {
69
150
  }
70
151
  }
71
152
 
153
+ /**
154
+ * Retrieves a document from the QuickStore.
155
+ * @param {string} docId - Document ID to retrieve
156
+ * @returns {Object|null} The document data, or null if not found
157
+ */
72
158
  get(docId) {
73
159
  const key = `${this._keyPrefix}data_${docId}`;
74
160
  const stored = localStorage.getItem(key);
@@ -83,10 +169,20 @@ class QuickStore {
83
169
  return null;
84
170
  }
85
171
 
172
+ /**
173
+ * Updates an existing document (alias for add).
174
+ * @param {string} docId - Document ID to update
175
+ * @param {Object} data - New document data
176
+ * @returns {boolean} True if successful
177
+ */
86
178
  update(docId, data) {
87
179
  return this.add(docId, data);
88
180
  }
89
181
 
182
+ /**
183
+ * Deletes a document from the QuickStore.
184
+ * @param {string} docId - Document ID to delete
185
+ */
90
186
  delete(docId) {
91
187
  const key = `${this._keyPrefix}data_${docId}`;
92
188
  localStorage.removeItem(key);
@@ -99,6 +195,10 @@ class QuickStore {
99
195
  }
100
196
  }
101
197
 
198
+ /**
199
+ * Retrieves all documents from the QuickStore.
200
+ * @returns {Array<Object>} Array of all documents with their _id fields
201
+ */
102
202
  getAll() {
103
203
  const index = this._readIndex();
104
204
  return index.map(docId => {
@@ -107,6 +207,12 @@ class QuickStore {
107
207
  }).filter(Boolean);
108
208
  }
109
209
 
210
+ /**
211
+ * Queries documents using MongoDB-like filter syntax.
212
+ * Uses the global queryEngine for evaluation.
213
+ * @param {Object} [filter={}] - Query filter object
214
+ * @returns {Array<Object>} Array of matching documents
215
+ */
110
216
  query(filter = {}) {
111
217
  const allDocs = this.getAll();
112
218
  if (Object.keys(filter).length === 0) return allDocs;
@@ -115,6 +221,9 @@ class QuickStore {
115
221
  return allDocs.filter(doc => queryEngine.evaluate(doc, filter));
116
222
  }
117
223
 
224
+ /**
225
+ * Clears all documents from the QuickStore.
226
+ */
118
227
  clear() {
119
228
  const index = this._readIndex();
120
229
  for (const docId of index) {
@@ -123,6 +232,10 @@ class QuickStore {
123
232
  localStorage.removeItem(this._indexKey);
124
233
  }
125
234
 
235
+ /**
236
+ * Gets the number of documents in the QuickStore.
237
+ * @type {number}
238
+ */
126
239
  get size() {
127
240
  return this._readIndex().length;
128
241
  }
@@ -132,12 +245,34 @@ class QuickStore {
132
245
  // Global IndexedDB Connection Pool
133
246
  // ========================
134
247
 
248
+ /**
249
+ * Manages a pool of IndexedDB connections to optimize database access.
250
+ * Connections are reused across multiple operations and reference-counted
251
+ * to ensure proper cleanup.
252
+ *
253
+ * @class IndexedDBConnectionPool
254
+ */
135
255
  class IndexedDBConnectionPool {
256
+ /**
257
+ * Creates a new connection pool instance.
258
+ */
136
259
  constructor() {
260
+ /** @private @type {Map<string, IDBDatabase>} Active database connections */
137
261
  this._connections = new Map();
262
+ /** @private @type {Map<string, number>} Reference counts for each connection */
138
263
  this._refCounts = new Map();
139
264
  }
140
265
 
266
+ /**
267
+ * Gets or creates a connection to an IndexedDB database.
268
+ * If a connection already exists, increments its reference count.
269
+ *
270
+ * @param {string} dbName - Database name
271
+ * @param {number} [version=1] - Database version
272
+ * @param {Function} [upgradeCallback] - Callback for onupgradeneeded event
273
+ * @returns {Promise<IDBDatabase>} The database connection
274
+ * @throws {LacertaDBError} If database cannot be opened
275
+ */
141
276
  async getConnection(dbName, version = 1, upgradeCallback) {
142
277
  const key = `${dbName}_v${version}`;
143
278
 
@@ -164,22 +299,34 @@ class IndexedDBConnectionPool {
164
299
  return db;
165
300
  }
166
301
 
302
+ /**
303
+ * Releases a database connection. If reference count reaches zero,
304
+ * the connection is closed.
305
+ *
306
+ * @param {string} dbName - Database name
307
+ * @param {number} [version=1] - Database version
308
+ */
167
309
  releaseConnection(dbName, version = 1) {
168
310
  const key = `${dbName}_v${version}`;
169
311
  const refCount = this._refCounts.get(key) || 0;
170
312
 
171
- if (refCount > 1) {
172
- this._refCounts.set(key, refCount - 1);
173
- } else {
313
+ // Force close if refCount is 1 or less
314
+ if (refCount <= 1) {
174
315
  const db = this._connections.get(key);
175
316
  if (db) {
176
317
  db.close();
177
318
  this._connections.delete(key);
178
319
  this._refCounts.delete(key);
179
320
  }
321
+ } else {
322
+ this._refCounts.set(key, refCount - 1);
180
323
  }
181
324
  }
182
325
 
326
+ /**
327
+ * Closes all connections in the pool.
328
+ * Should be called during application cleanup.
329
+ */
183
330
  closeAll() {
184
331
  for (const db of this._connections.values()) {
185
332
  db.close();
@@ -189,18 +336,34 @@ class IndexedDBConnectionPool {
189
336
  }
190
337
  }
191
338
 
339
+ /** @type {IndexedDBConnectionPool} Global connection pool instance */
192
340
  const connectionPool = new IndexedDBConnectionPool();
193
341
 
194
342
  // ========================
195
343
  // Async Mutex for managing concurrent operations
196
344
  // ========================
197
345
 
346
+ /**
347
+ * Provides mutual exclusion for asynchronous operations.
348
+ * Ensures that only one async operation can access a critical section at a time.
349
+ *
350
+ * @class AsyncMutex
351
+ */
198
352
  class AsyncMutex {
353
+ /**
354
+ * Creates a new mutex instance.
355
+ */
199
356
  constructor() {
357
+ /** @private @type {Array<Function>} Queue of waiting operations */
200
358
  this._queue = [];
359
+ /** @private @type {boolean} Whether the mutex is currently locked */
201
360
  this._locked = false;
202
361
  }
203
362
 
363
+ /**
364
+ * Acquires the mutex lock. Returns a release function when the lock is acquired.
365
+ * @returns {Promise<Function>} A function to release the lock
366
+ */
204
367
  acquire() {
205
368
  return new Promise(resolve => {
206
369
  this._queue.push(resolve);
@@ -208,11 +371,21 @@ class AsyncMutex {
208
371
  });
209
372
  }
210
373
 
374
+ /**
375
+ * Releases the mutex lock.
376
+ */
211
377
  release() {
212
378
  this._locked = false;
213
379
  this._dispatch();
214
380
  }
215
381
 
382
+ /**
383
+ * Runs a callback with exclusive access to the mutex.
384
+ * Automatically acquires and releases the lock.
385
+ *
386
+ * @param {Function} callback - Async function to run exclusively
387
+ * @returns {Promise<*>} Result of the callback
388
+ */
216
389
  async runExclusive(callback) {
217
390
  const release = await this.acquire();
218
391
  try {
@@ -222,6 +395,10 @@ class AsyncMutex {
222
395
  }
223
396
  }
224
397
 
398
+ /**
399
+ * Dispatches the next waiting operation if the mutex is unlocked.
400
+ * @private
401
+ */
225
402
  _dispatch() {
226
403
  if (this._locked || this._queue.length === 0) {
227
404
  return;
@@ -236,12 +413,29 @@ class AsyncMutex {
236
413
  // Custom error class for LacertaDB
237
414
  // ========================
238
415
 
416
+ /**
417
+ * Custom error class for LacertaDB operations.
418
+ * Includes error codes for programmatic error handling.
419
+ *
420
+ * @class LacertaDBError
421
+ * @extends Error
422
+ */
239
423
  class LacertaDBError extends Error {
424
+ /**
425
+ * Creates a new LacertaDBError.
426
+ * @param {string} message - Human-readable error message
427
+ * @param {string} code - Machine-readable error code
428
+ * @param {Error} [originalError] - Original error that caused this error
429
+ */
240
430
  constructor(message, code, originalError) {
241
431
  super(message);
432
+ /** @type {string} Error name identifier */
242
433
  this.name = 'LacertaDBError';
434
+ /** @type {string} Machine-readable error code */
243
435
  this.code = code;
436
+ /** @type {Error|null} Original error if this is a wrapped error */
244
437
  this.originalError = originalError || null;
438
+ /** @type {string} ISO timestamp when the error occurred */
245
439
  this.timestamp = new Date().toISOString();
246
440
  }
247
441
  }
@@ -250,15 +444,38 @@ class LacertaDBError extends Error {
250
444
  // LRU Cache Implementation
251
445
  // ========================
252
446
 
447
+ /**
448
+ * Least Recently Used (LRU) cache implementation.
449
+ * Evicts the least recently accessed items when the cache reaches its maximum size.
450
+ * Optionally supports TTL (time-to-live) for cache entries.
451
+ *
452
+ * @class LRUCache
453
+ */
253
454
  class LRUCache {
455
+ /**
456
+ * Creates a new LRU cache.
457
+ * @param {number} [maxSize=100] - Maximum number of items to store
458
+ * @param {number|null} [ttl=null] - Time-to-live in milliseconds (null for no expiration)
459
+ */
254
460
  constructor(maxSize = 100, ttl = null) {
461
+ /** @private @type {number} Maximum cache size */
255
462
  this._maxSize = maxSize;
463
+ /** @private @type {number|null} TTL in milliseconds */
256
464
  this._ttl = ttl;
465
+ /** @private @type {Map<string, *>} Cache storage */
257
466
  this._cache = new Map();
467
+ /** @private @type {Array<string>} Access order tracking (most recent at end) */
258
468
  this._accessOrder = [];
469
+ /** @private @type {Map<string, number>} Timestamps for TTL */
259
470
  this._timestamps = new Map();
260
471
  }
261
472
 
473
+ /**
474
+ * Retrieves a value from the cache.
475
+ * Updates the access order and checks TTL expiration.
476
+ * @param {string} key - Cache key
477
+ * @returns {*|null} Cached value or null if not found/expired
478
+ */
262
479
  get(key) {
263
480
  if (!this._cache.has(key)) {
264
481
  return null;
@@ -281,6 +498,12 @@ class LRUCache {
281
498
  return this._cache.get(key);
282
499
  }
283
500
 
501
+ /**
502
+ * Stores a value in the cache.
503
+ * Evicts the oldest items if the cache exceeds maxSize.
504
+ * @param {string} key - Cache key
505
+ * @param {*} value - Value to store
506
+ */
284
507
  set(key, value) {
285
508
  if (this._cache.has(key)) {
286
509
  const index = this._accessOrder.indexOf(key);
@@ -300,6 +523,11 @@ class LRUCache {
300
523
  }
301
524
  }
302
525
 
526
+ /**
527
+ * Deletes an item from the cache.
528
+ * @param {string} key - Cache key to delete
529
+ * @returns {boolean} True if item was deleted
530
+ */
303
531
  delete(key) {
304
532
  const index = this._accessOrder.indexOf(key);
305
533
  if (index > -1) {
@@ -309,12 +537,20 @@ class LRUCache {
309
537
  return this._cache.delete(key);
310
538
  }
311
539
 
540
+ /**
541
+ * Clears all items from the cache.
542
+ */
312
543
  clear() {
313
544
  this._cache.clear();
314
545
  this._accessOrder = [];
315
546
  this._timestamps.clear();
316
547
  }
317
548
 
549
+ /**
550
+ * Checks if a key exists in the cache (respects TTL).
551
+ * @param {string} key - Cache key
552
+ * @returns {boolean} True if key exists and is not expired
553
+ */
318
554
  has(key) {
319
555
  if (this._ttl && this._cache.has(key)) {
320
556
  const timestamp = this._timestamps.get(key);
@@ -326,13 +562,30 @@ class LRUCache {
326
562
  return this._cache.has(key);
327
563
  }
328
564
 
565
+ /**
566
+ * Gets the current number of items in the cache.
567
+ * @type {number}
568
+ */
329
569
  get size() {
330
570
  return this._cache.size;
331
571
  }
332
572
  }
333
573
 
334
- // LFU (Least Frequently Used) Cache
574
+ // ========================
575
+ // LFU Cache Implementation
576
+ // ========================
577
+
578
+ /**
579
+ * Least Frequently Used (LFU) cache implementation.
580
+ * Evicts the least frequently accessed items when the cache reaches its maximum size.
581
+ * @class LFUCache
582
+ */
335
583
  class LFUCache {
584
+ /**
585
+ * Creates a new LFU cache.
586
+ * @param {number} [maxSize=100] - Maximum number of items to store
587
+ * @param {number|null} [ttl=null] - Time-to-live in milliseconds
588
+ */
336
589
  constructor(maxSize = 100, ttl = null) {
337
590
  this._maxSize = maxSize;
338
591
  this._ttl = ttl;
@@ -341,6 +594,11 @@ class LFUCache {
341
594
  this._timestamps = new Map();
342
595
  }
343
596
 
597
+ /**
598
+ * Retrieves a value from the cache and increments its frequency.
599
+ * @param {string} key - Cache key
600
+ * @returns {*|null} Cached value or null if not found/expired
601
+ */
344
602
  get(key) {
345
603
  if (!this._cache.has(key)) {
346
604
  return null;
@@ -358,6 +616,11 @@ class LFUCache {
358
616
  return this._cache.get(key);
359
617
  }
360
618
 
619
+ /**
620
+ * Stores a value in the cache.
621
+ * @param {string} key - Cache key
622
+ * @param {*} value - Value to store
623
+ */
361
624
  set(key, value) {
362
625
  if (this._cache.has(key)) {
363
626
  this._cache.set(key, value);
@@ -383,39 +646,73 @@ class LFUCache {
383
646
  }
384
647
  }
385
648
 
649
+ /**
650
+ * Deletes an item from the cache.
651
+ * @param {string} key - Cache key to delete
652
+ * @returns {boolean} True if item was deleted
653
+ */
386
654
  delete(key) {
387
655
  this._frequencies.delete(key);
388
656
  this._timestamps.delete(key);
389
657
  return this._cache.delete(key);
390
658
  }
391
659
 
660
+ /** Clears all items from the cache. */
392
661
  clear() {
393
662
  this._cache.clear();
394
663
  this._frequencies.clear();
395
664
  this._timestamps.clear();
396
665
  }
397
666
 
667
+ /**
668
+ * Checks if a key exists in the cache.
669
+ * @param {string} key - Cache key
670
+ * @returns {boolean} True if key exists
671
+ */
398
672
  has(key) {
399
673
  return this._cache.has(key);
400
674
  }
401
675
 
676
+ /** @type {number} Current number of items in the cache */
402
677
  get size() {
403
678
  return this._cache.size;
404
679
  }
405
680
  }
406
681
 
407
- // TTL (Time To Live) Only Cache
682
+ // ========================
683
+ // TTL Cache Implementation
684
+ // ========================
685
+
686
+ /**
687
+ * Time-To-Live (TTL) only cache implementation.
688
+ * Items automatically expire after a specified duration.
689
+ * @class TTLCache
690
+ */
408
691
  class TTLCache {
692
+ /**
693
+ * Creates a new TTL cache.
694
+ * @param {number} [ttl=60000] - Time-to-live in milliseconds (default 1 minute)
695
+ */
409
696
  constructor(ttl = 60000) {
410
697
  this._ttl = ttl;
411
698
  this._cache = new Map();
412
699
  this._timers = new Map();
413
700
  }
414
701
 
702
+ /**
703
+ * Retrieves a value from the cache.
704
+ * @param {string} key - Cache key
705
+ * @returns {*|null} Cached value or null if not found
706
+ */
415
707
  get(key) {
416
708
  return this._cache.get(key) || null;
417
709
  }
418
710
 
711
+ /**
712
+ * Stores a value in the cache with automatic expiration.
713
+ * @param {string} key - Cache key
714
+ * @param {*} value - Value to store
715
+ */
419
716
  set(key, value) {
420
717
  if (this._timers.has(key)) {
421
718
  clearTimeout(this._timers.get(key));
@@ -429,6 +726,11 @@ class TTLCache {
429
726
  this._timers.set(key, timer);
430
727
  }
431
728
 
729
+ /**
730
+ * Deletes an item from the cache.
731
+ * @param {string} key - Cache key to delete
732
+ * @returns {boolean} True if item was deleted
733
+ */
432
734
  delete(key) {
433
735
  if (this._timers.has(key)) {
434
736
  clearTimeout(this._timers.get(key));
@@ -437,6 +739,7 @@ class TTLCache {
437
739
  return this._cache.delete(key);
438
740
  }
439
741
 
742
+ /** Clears all items and timers from the cache. */
440
743
  clear() {
441
744
  for (const timer of this._timers.values()) {
442
745
  clearTimeout(timer);
@@ -445,20 +748,48 @@ class TTLCache {
445
748
  this._cache.clear();
446
749
  }
447
750
 
751
+ /**
752
+ * Checks if a key exists in the cache.
753
+ * @param {string} key - Cache key
754
+ * @returns {boolean} True if key exists
755
+ */
448
756
  has(key) {
449
757
  return this._cache.has(key);
450
758
  }
451
759
 
760
+ /** @type {number} Current number of items in the cache */
452
761
  get size() {
453
762
  return this._cache.size;
454
763
  }
764
+
765
+ /** Destroys the cache, clearing all timers and data. */
766
+ destroy() {
767
+ for (const timer of this._timers.values()) {
768
+ clearTimeout(timer);
769
+ }
770
+ this._timers.clear();
771
+ this._cache.clear();
772
+ }
455
773
  }
456
774
 
457
775
  // ========================
458
776
  // Cache Strategy System
459
777
  // ========================
460
778
 
779
+ /**
780
+ * Unified cache strategy manager that supports multiple caching algorithms.
781
+ * Provides a consistent interface regardless of the underlying strategy.
782
+ * @class CacheStrategy
783
+ */
461
784
  class CacheStrategy {
785
+ /**
786
+ * Creates a new cache strategy.
787
+ * @param {Object} [config={}] - Configuration options
788
+ * @param {string} [config.type='lru'] - Cache type: 'lru', 'lfu', 'ttl', or 'none'
789
+ * @param {number} [config.maxSize=100] - Maximum cache size
790
+ * @param {number|null} [config.ttl=null] - Time-to-live in milliseconds
791
+ * @param {boolean} [config.enabled=true] - Whether caching is enabled
792
+ */
462
793
  constructor(config = {}) {
463
794
  this._type = config.type || 'lru';
464
795
  this._maxSize = config.maxSize || 100;
@@ -467,7 +798,7 @@ class CacheStrategy {
467
798
  this._cache = null;
468
799
  }
469
800
 
470
- // Lazy initialization with getter
801
+ /** Gets the cache instance, creating it lazily if needed. */
471
802
  get cache() {
472
803
  if (!this._cache) {
473
804
  this._cache = this._createCache();
@@ -475,6 +806,7 @@ class CacheStrategy {
475
806
  return this._cache;
476
807
  }
477
808
 
809
+ /** @private Creates the appropriate cache instance based on config type. */
478
810
  _createCache() {
479
811
  switch (this._type) {
480
812
  case 'lru':
@@ -490,37 +822,77 @@ class CacheStrategy {
490
822
  }
491
823
  }
492
824
 
825
+ /**
826
+ * Retrieves a value from the cache.
827
+ * @param {string} key - Cache key
828
+ * @returns {*|null} Cached value or null
829
+ */
493
830
  get(key) {
494
831
  if (!this._enabled || !this.cache) return null;
495
832
  return this.cache.get(key);
496
833
  }
497
834
 
835
+ /**
836
+ * Stores a value in the cache.
837
+ * @param {string} key - Cache key
838
+ * @param {*} value - Value to store
839
+ */
498
840
  set(key, value) {
499
841
  if (!this._enabled || !this.cache) return;
500
842
  this.cache.set(key, value);
501
843
  }
502
844
 
845
+ /**
846
+ * Deletes an item from the cache.
847
+ * @param {string} key - Cache key to delete
848
+ */
503
849
  delete(key) {
504
850
  if (!this._enabled || !this.cache) return;
505
851
  this.cache.delete(key);
506
852
  }
507
853
 
854
+ /** Clears all items from the cache. */
508
855
  clear() {
509
856
  if (!this._enabled || !this.cache) return;
510
857
  this.cache.clear();
511
858
  }
512
859
 
860
+ /**
861
+ * Updates the cache strategy configuration.
862
+ * @param {Object} newConfig - New configuration options
863
+ */
513
864
  updateStrategy(newConfig) {
514
865
  Object.assign(this, newConfig);
515
866
  this._cache = null; // Reset cache for lazy reinitialization
516
867
  }
868
+
869
+ /** Destroys the cache and releases resources. */
870
+ destroy() {
871
+ if (this._cache && this._cache.destroy) {
872
+ this._cache.destroy();
873
+ } else if (this._cache && this._cache.clear) {
874
+ this._cache.clear();
875
+ }
876
+ this._cache = null;
877
+ }
517
878
  }
518
879
 
519
880
  // ========================
520
- // Compression Utility (Fixed)
881
+ // Compression Utility
521
882
  // ========================
522
883
 
884
+ /**
885
+ * Browser-based compression utility using the Compression Streams API.
886
+ * Provides deflate compression and decompression for data storage optimization.
887
+ * @class BrowserCompressionUtility
888
+ */
523
889
  class BrowserCompressionUtility {
890
+ /**
891
+ * Compresses data using the deflate algorithm.
892
+ * @param {Uint8Array} input - Data to compress
893
+ * @returns {Promise<Uint8Array>} Compressed data
894
+ * @throws {TypeError} If input is not a Uint8Array
895
+ */
524
896
  async compress(input) {
525
897
  if (!(input instanceof Uint8Array)) {
526
898
  throw new TypeError('Input must be Uint8Array');
@@ -536,6 +908,11 @@ class BrowserCompressionUtility {
536
908
  }
537
909
  }
538
910
 
911
+ /**
912
+ * Decompresses deflate-compressed data.
913
+ * @param {Uint8Array} compressedData - Compressed data to decompress
914
+ * @returns {Promise<Uint8Array>} Decompressed data
915
+ */
539
916
  async decompress(compressedData) {
540
917
  if (!(compressedData instanceof Uint8Array)) {
541
918
  throw new TypeError('Input must be Uint8Array');
@@ -551,6 +928,11 @@ class BrowserCompressionUtility {
551
928
  }
552
929
  }
553
930
 
931
+ /**
932
+ * Synchronous compression (pass-through, no actual compression).
933
+ * @param {Uint8Array} input - Data to compress
934
+ * @returns {Uint8Array} Original data (uncompressed)
935
+ */
554
936
  compressSync(input) {
555
937
  if (!(input instanceof Uint8Array)) {
556
938
  throw new TypeError('Input must be Uint8Array');
@@ -558,6 +940,11 @@ class BrowserCompressionUtility {
558
940
  return input;
559
941
  }
560
942
 
943
+ /**
944
+ * Synchronous decompression (pass-through).
945
+ * @param {Uint8Array} compressedData - Data to decompress
946
+ * @returns {Uint8Array} Original data
947
+ */
561
948
  decompressSync(compressedData) {
562
949
  if (!(compressedData instanceof Uint8Array)) {
563
950
  throw new TypeError('Input must be Uint8Array');
@@ -570,7 +957,18 @@ class BrowserCompressionUtility {
570
957
  // Browser Encryption Utility
571
958
  // ========================
572
959
 
960
+ /**
961
+ * Utility for encrypting and decrypting data using Web Crypto API.
962
+ * Uses AES-GCM for authenticated encryption with PBKDF2 for key derivation.
963
+ * @class BrowserEncryptionUtility
964
+ */
573
965
  class BrowserEncryptionUtility {
966
+ /**
967
+ * Encrypts data using AES-256-GCM with PBKDF2 key derivation.
968
+ * @param {Uint8Array} data - Data to encrypt
969
+ * @param {string} password - Password for encryption
970
+ * @returns {Promise<Uint8Array>} Encrypted data (salt + IV + ciphertext)
971
+ */
574
972
  async encrypt(data, password) {
575
973
  const encoder = new TextEncoder();
576
974
  const salt = crypto.getRandomValues(new Uint8Array(16));
@@ -612,6 +1010,12 @@ class BrowserEncryptionUtility {
612
1010
  return result;
613
1011
  }
614
1012
 
1013
+ /**
1014
+ * Decrypts AES-256-GCM encrypted data.
1015
+ * @param {Uint8Array} encryptedData - Encrypted data (salt + IV + ciphertext)
1016
+ * @param {string} password - Password for decryption
1017
+ * @returns {Promise<Uint8Array>} Decrypted data
1018
+ */
615
1019
  async decrypt(encryptedData, password) {
616
1020
  const encoder = new TextEncoder();
617
1021
  const salt = encryptedData.slice(0, 16);
@@ -654,7 +1058,21 @@ class BrowserEncryptionUtility {
654
1058
  // Database-Level Encryption
655
1059
  // ========================
656
1060
 
1061
+ /**
1062
+ * Advanced encryption system for database-level security.
1063
+ * Provides PIN-based encryption with HMAC verification for data integrity.
1064
+ * Suitable for securing entire databases or sensitive documents like private keys.
1065
+ * @class SecureDatabaseEncryption
1066
+ */
657
1067
  class SecureDatabaseEncryption {
1068
+ /**
1069
+ * Creates a new secure encryption instance.
1070
+ * @param {Object} [config={}] - Encryption configuration
1071
+ * @param {number} [config.iterations=100000] - PBKDF2 iteration count
1072
+ * @param {string} [config.hashAlgorithm='SHA-256'] - Hash algorithm for key derivation
1073
+ * @param {number} [config.keyLength=256] - AES key length in bits
1074
+ * @param {number} [config.saltLength=32] - Salt length in bytes
1075
+ */
658
1076
  constructor(config = {}) {
659
1077
  this._masterKey = null;
660
1078
  this._salt = null;
@@ -667,10 +1085,17 @@ class SecureDatabaseEncryption {
667
1085
  this._hmacKey = null;
668
1086
  }
669
1087
 
1088
+ /** @type {boolean} Whether encryption has been initialized */
670
1089
  get initialized() {
671
1090
  return this._initialized;
672
1091
  }
673
1092
 
1093
+ /**
1094
+ * Initializes encryption with a PIN code.
1095
+ * @param {string} pin - PIN code (typically 4-8 digits)
1096
+ * @param {Uint8Array|null} [salt=null] - Salt for key derivation (generated if null)
1097
+ * @returns {Promise<string>} Base64-encoded salt
1098
+ */
674
1099
  async initialize(pin, salt = null) {
675
1100
  if (this._initialized) {
676
1101
  throw new Error('Database encryption already initialized');
@@ -726,6 +1151,11 @@ class SecureDatabaseEncryption {
726
1151
  return base64.encode(this._salt);
727
1152
  }
728
1153
 
1154
+ /**
1155
+ * Encrypts data with AES-GCM and adds HMAC for integrity.
1156
+ * @param {string|Object|Uint8Array} data - Data to encrypt
1157
+ * @returns {Promise<Uint8Array>} Encrypted data with IV and HMAC
1158
+ */
729
1159
  async encrypt(data) {
730
1160
  if (!this._initialized) {
731
1161
  throw new Error('Database encryption not initialized');
@@ -768,6 +1198,12 @@ class SecureDatabaseEncryption {
768
1198
  return result;
769
1199
  }
770
1200
 
1201
+ /**
1202
+ * Decrypts data and verifies HMAC integrity.
1203
+ * @param {Uint8Array} encryptedPackage - Encrypted data (IV + ciphertext + HMAC)
1204
+ * @returns {Promise<Uint8Array>} Decrypted data
1205
+ * @throws {Error} If HMAC verification fails
1206
+ */
771
1207
  async decrypt(encryptedPackage) {
772
1208
  if (!this._initialized) {
773
1209
  throw new Error('Database encryption not initialized');
@@ -805,6 +1241,12 @@ class SecureDatabaseEncryption {
805
1241
  return new Uint8Array(decryptedData);
806
1242
  }
807
1243
 
1244
+ /**
1245
+ * Encrypts a private key with additional authentication data.
1246
+ * @param {string|Uint8Array|Object} privateKey - Key to encrypt
1247
+ * @param {string} [additionalAuth=''] - Additional authentication data
1248
+ * @returns {Promise<string>} Base64-encoded encrypted key
1249
+ */
808
1250
  async encryptPrivateKey(privateKey, additionalAuth = '') {
809
1251
  if (!this._initialized) {
810
1252
  throw new Error('Database encryption not initialized');
@@ -848,6 +1290,12 @@ class SecureDatabaseEncryption {
848
1290
  return base64.encode(result);
849
1291
  }
850
1292
 
1293
+ /**
1294
+ * Decrypts a private key, verifying additional authentication data.
1295
+ * @param {string} encryptedKeyString - Base64-encoded encrypted key
1296
+ * @param {string} [additionalAuth=''] - Additional authentication data to verify
1297
+ * @returns {Promise<string>} Decrypted private key
1298
+ */
851
1299
  async decryptPrivateKey(encryptedKeyString, additionalAuth = '') {
852
1300
  if (!this._initialized) {
853
1301
  throw new Error('Database encryption not initialized');
@@ -882,6 +1330,12 @@ class SecureDatabaseEncryption {
882
1330
  return new TextDecoder().decode(decryptedKey);
883
1331
  }
884
1332
 
1333
+ /**
1334
+ * Generates a cryptographically secure random PIN.
1335
+ * @static
1336
+ * @param {number} [length=6] - PIN length (number of digits)
1337
+ * @returns {string} Random PIN
1338
+ */
885
1339
  static generateSecurePIN(length = 6) {
886
1340
  const digits = new Uint8Array(length);
887
1341
  crypto.getRandomValues(digits);
@@ -890,6 +1344,7 @@ class SecureDatabaseEncryption {
890
1344
  .join('');
891
1345
  }
892
1346
 
1347
+ /** Destroys the encryption instance and clears all keys from memory. */
893
1348
  destroy() {
894
1349
  this._masterKey = null;
895
1350
  this._encKey = null;
@@ -898,6 +1353,7 @@ class SecureDatabaseEncryption {
898
1353
  this._initialized = false;
899
1354
  }
900
1355
 
1356
+ /** @private Compares two Uint8Arrays for equality. */
901
1357
  _arrayEquals(a, b) {
902
1358
  if (a.length !== b.length) return false;
903
1359
  for (let i = 0; i < a.length; i++) {
@@ -906,6 +1362,12 @@ class SecureDatabaseEncryption {
906
1362
  return true;
907
1363
  }
908
1364
 
1365
+ /**
1366
+ * Changes the encryption PIN.
1367
+ * @param {string} oldPin - Current PIN
1368
+ * @param {string} newPin - New PIN to set
1369
+ * @returns {Promise<string>} Base64-encoded new salt
1370
+ */
909
1371
  async changePin(oldPin, newPin) {
910
1372
  if (!this._initialized) {
911
1373
  throw new Error('Database encryption not initialized');
@@ -930,6 +1392,10 @@ class SecureDatabaseEncryption {
930
1392
  return newSalt;
931
1393
  }
932
1394
 
1395
+ /**
1396
+ * Exports encryption metadata for persistence (does NOT include keys).
1397
+ * @returns {Object} Encryption metadata
1398
+ */
933
1399
  exportMetadata() {
934
1400
  if (!this._salt) {
935
1401
  throw new Error('No encryption metadata to export');
@@ -946,6 +1412,11 @@ class SecureDatabaseEncryption {
946
1412
  };
947
1413
  }
948
1414
 
1415
+ /**
1416
+ * Imports encryption metadata from persistence.
1417
+ * @param {Object} metadata - Encryption metadata
1418
+ * @returns {boolean} True if successful
1419
+ */
949
1420
  importMetadata(metadata) {
950
1421
  if (!metadata.salt) {
951
1422
  throw new Error('Invalid encryption metadata');
@@ -965,7 +1436,17 @@ class SecureDatabaseEncryption {
965
1436
  // B-Tree Index Implementation with Self-Healing
966
1437
  // ========================
967
1438
 
1439
+ /**
1440
+ * Node in a B-Tree index structure.
1441
+ * @class BTreeNode
1442
+ * @private
1443
+ */
968
1444
  class BTreeNode {
1445
+ /**
1446
+ * Creates a new B-Tree node.
1447
+ * @param {number} order - B-Tree order (determines branching factor)
1448
+ * @param {boolean} leaf - Whether this is a leaf node
1449
+ */
969
1450
  constructor(order, leaf) {
970
1451
  this.keys = new Array(2 * order - 1);
971
1452
  this.values = new Array(2 * order - 1);
@@ -975,6 +1456,7 @@ class BTreeNode {
975
1456
  this.order = order;
976
1457
  }
977
1458
 
1459
+ /** Searches for a key in this node and its subtree. */
978
1460
  search(key) {
979
1461
  let i = 0;
980
1462
  while (i < this.n && key > this.keys[i]) {
@@ -992,6 +1474,7 @@ class BTreeNode {
992
1474
  return this.children[i] ? this.children[i].search(key) : null;
993
1475
  }
994
1476
 
1477
+ /** Performs a range search within this node and its subtree. */
995
1478
  rangeSearch(min, max, results) {
996
1479
  let i = 0;
997
1480
 
@@ -1014,6 +1497,7 @@ class BTreeNode {
1014
1497
  }
1015
1498
  }
1016
1499
 
1500
+ /** Inserts a key-value pair into a non-full node. */
1017
1501
  insertNonFull(key, value) {
1018
1502
  let i = this.n - 1;
1019
1503
 
@@ -1060,6 +1544,7 @@ class BTreeNode {
1060
1544
  }
1061
1545
  }
1062
1546
 
1547
+ /** Splits a full child node. */
1063
1548
  splitChild(i, y) {
1064
1549
  const z = new BTreeNode(this.order, y.leaf);
1065
1550
  z.n = this.order - 1;
@@ -1093,6 +1578,7 @@ class BTreeNode {
1093
1578
  this.n++;
1094
1579
  }
1095
1580
 
1581
+ /** Removes a value from the key's value set. */
1096
1582
  remove(key, value) {
1097
1583
  let i = 0;
1098
1584
  while (i < this.n && key > this.keys[i]) {
@@ -1115,6 +1601,7 @@ class BTreeNode {
1115
1601
  }
1116
1602
  }
1117
1603
 
1604
+ /** Verifies and repairs the node structure (self-healing). */
1118
1605
  verify() {
1119
1606
  const issues = [];
1120
1607
 
@@ -1144,7 +1631,16 @@ class BTreeNode {
1144
1631
  }
1145
1632
  }
1146
1633
 
1634
+ /**
1635
+ * B-Tree index for efficient ordered data access and range queries.
1636
+ * Supports multiple document IDs per key and includes self-healing capabilities.
1637
+ * @class BTreeIndex
1638
+ */
1147
1639
  class BTreeIndex {
1640
+ /**
1641
+ * Creates a new B-Tree index.
1642
+ * @param {number} [order=4] - B-Tree order (minimum degree)
1643
+ */
1148
1644
  constructor(order = 4) {
1149
1645
  this._root = null;
1150
1646
  this._order = order;
@@ -1153,6 +1649,11 @@ class BTreeIndex {
1153
1649
  this._verificationInterval = 60000;
1154
1650
  }
1155
1651
 
1652
+ /**
1653
+ * Inserts a key-value pair into the index.
1654
+ * @param {*} key - Index key
1655
+ * @param {string} value - Document ID
1656
+ */
1156
1657
  insert(key, value) {
1157
1658
  if (Date.now() - this._lastVerification > this._verificationInterval) {
1158
1659
  this.verify();
@@ -1181,17 +1682,33 @@ class BTreeIndex {
1181
1682
  this._size++;
1182
1683
  }
1183
1684
 
1685
+ /**
1686
+ * Finds all document IDs associated with a key.
1687
+ * @param {*} key - Key to search for
1688
+ * @returns {Array<string>} Array of document IDs
1689
+ */
1184
1690
  find(key) {
1185
1691
  if (!this._root) return [];
1186
1692
  const values = this._root.search(key);
1187
1693
  return values ? Array.from(values) : [];
1188
1694
  }
1189
1695
 
1696
+ /**
1697
+ * Checks if a key exists in the index.
1698
+ * @param {*} key - Key to check
1699
+ * @returns {boolean} True if key exists
1700
+ */
1190
1701
  contains(key) {
1191
1702
  if (!this._root) return false;
1192
1703
  return this._root.search(key) !== null;
1193
1704
  }
1194
1705
 
1706
+ /**
1707
+ * Finds all document IDs within a key range.
1708
+ * @param {*} min - Minimum key (inclusive)
1709
+ * @param {*} max - Maximum key (inclusive)
1710
+ * @returns {Array<string>} Array of document IDs
1711
+ */
1195
1712
  range(min, max) {
1196
1713
  if (!this._root) return [];
1197
1714
  const results = [];
@@ -1199,6 +1716,7 @@ class BTreeIndex {
1199
1716
  return results;
1200
1717
  }
1201
1718
 
1719
+ /** Finds all document IDs with keys >= min. */
1202
1720
  rangeFrom(min) {
1203
1721
  if (!this._root) return [];
1204
1722
  const results = [];
@@ -1206,6 +1724,7 @@ class BTreeIndex {
1206
1724
  return results;
1207
1725
  }
1208
1726
 
1727
+ /** Finds all document IDs with keys <= max. */
1209
1728
  rangeTo(max) {
1210
1729
  if (!this._root) return [];
1211
1730
  const results = [];
@@ -1213,6 +1732,7 @@ class BTreeIndex {
1213
1732
  return results;
1214
1733
  }
1215
1734
 
1735
+ /** Removes a document ID from a key's value set. */
1216
1736
  remove(key, value) {
1217
1737
  if (!this._root) return;
1218
1738
  this._root.remove(key, value);
@@ -1224,6 +1744,7 @@ class BTreeIndex {
1224
1744
  this._size--;
1225
1745
  }
1226
1746
 
1747
+ /** Verifies index integrity and performs self-healing repairs. */
1227
1748
  verify() {
1228
1749
  this._lastVerification = Date.now();
1229
1750
  if (!this._root) return { healthy: true, issues: [] };
@@ -1241,18 +1762,34 @@ class BTreeIndex {
1241
1762
  };
1242
1763
  }
1243
1764
 
1765
+ /** @type {number} Total number of entries in the index */
1244
1766
  get size() {
1245
1767
  return this._size;
1246
1768
  }
1247
1769
  }
1248
1770
 
1249
- // Text Index for full-text search
1771
+ // ========================
1772
+ // Text Index for Full-Text Search
1773
+ // ========================
1774
+
1775
+ /**
1776
+ * Inverted index for full-text search capabilities.
1777
+ * Tokenizes text and builds an index for fast keyword lookups.
1778
+ * @class TextIndex
1779
+ */
1250
1780
  class TextIndex {
1251
1781
  constructor() {
1782
+ /** @private Token to document IDs mapping */
1252
1783
  this._invertedIndex = new Map();
1784
+ /** @private Document ID to text mapping */
1253
1785
  this._documentTexts = new Map();
1254
1786
  }
1255
1787
 
1788
+ /**
1789
+ * Adds a document to the text index.
1790
+ * @param {string} text - Document text content
1791
+ * @param {string} docId - Document ID
1792
+ */
1256
1793
  addDocument(text, docId) {
1257
1794
  if (typeof text !== 'string') return;
1258
1795
 
@@ -1267,6 +1804,7 @@ class TextIndex {
1267
1804
  }
1268
1805
  }
1269
1806
 
1807
+ /** Removes a document from the text index. */
1270
1808
  removeDocument(docId) {
1271
1809
  const text = this._documentTexts.get(docId);
1272
1810
  if (!text) return;
@@ -1285,11 +1823,17 @@ class TextIndex {
1285
1823
  this._documentTexts.delete(docId);
1286
1824
  }
1287
1825
 
1826
+ /** Updates a document's text in the index. */
1288
1827
  updateDocument(docId, newText) {
1289
1828
  this.removeDocument(docId);
1290
1829
  this.addDocument(newText, docId);
1291
1830
  }
1292
1831
 
1832
+ /**
1833
+ * Searches for documents containing all query terms.
1834
+ * @param {string} query - Search query
1835
+ * @returns {Array<string>} Array of matching document IDs
1836
+ */
1293
1837
  search(query) {
1294
1838
  const tokens = this._tokenize(query);
1295
1839
  if (tokens.length === 0) return [];
@@ -1311,6 +1855,7 @@ class TextIndex {
1311
1855
  return results ? Array.from(results) : [];
1312
1856
  }
1313
1857
 
1858
+ /** @private Tokenizes text into searchable terms. */
1314
1859
  _tokenize(text) {
1315
1860
  return text.toLowerCase()
1316
1861
  .replace(/[^\w\s]/g, ' ')
@@ -1318,17 +1863,32 @@ class TextIndex {
1318
1863
  .filter(token => token.length > 2);
1319
1864
  }
1320
1865
 
1866
+ /** @type {number} Number of indexed documents */
1321
1867
  get size() {
1322
1868
  return this._documentTexts.size;
1323
1869
  }
1324
1870
  }
1325
1871
 
1326
- // Geo Index for spatial queries
1872
+ // ========================
1873
+ // Geo Index for Spatial Queries
1874
+ // ========================
1875
+
1876
+ /**
1877
+ * Geospatial index for location-based queries.
1878
+ * Supports proximity searches and bounding box queries.
1879
+ * @class GeoIndex
1880
+ */
1327
1881
  class GeoIndex {
1328
1882
  constructor() {
1883
+ /** @private Document ID to coordinates mapping */
1329
1884
  this._points = new Map();
1330
1885
  }
1331
1886
 
1887
+ /**
1888
+ * Adds a geographic point to the index.
1889
+ * @param {Object} coords - Coordinates object with lat/lng properties
1890
+ * @param {string} docId - Document ID
1891
+ */
1332
1892
  addPoint(coords, docId) {
1333
1893
  if (!coords || typeof coords.lat !== 'number' || typeof coords.lng !== 'number') {
1334
1894
  return;
@@ -1336,14 +1896,22 @@ class GeoIndex {
1336
1896
  this._points.set(docId, coords);
1337
1897
  }
1338
1898
 
1899
+ /** Removes a point from the index. */
1339
1900
  removePoint(docId) {
1340
1901
  this._points.delete(docId);
1341
1902
  }
1342
1903
 
1904
+ /** Updates a point's coordinates. */
1343
1905
  updatePoint(docId, newCoords) {
1344
1906
  this._points.set(docId, newCoords);
1345
1907
  }
1346
1908
 
1909
+ /**
1910
+ * Finds all points within a distance from a center point.
1911
+ * @param {Object} center - Center coordinates
1912
+ * @param {number} maxDistance - Maximum distance in kilometers
1913
+ * @returns {Array<string>} Document IDs sorted by distance
1914
+ */
1347
1915
  findNear(center, maxDistance) {
1348
1916
  const results = [];
1349
1917
 
@@ -1358,6 +1926,11 @@ class GeoIndex {
1358
1926
  .map(r => r.docId);
1359
1927
  }
1360
1928
 
1929
+ /**
1930
+ * Finds all points within a bounding box.
1931
+ * @param {Object} bounds - Bounding box with minLat, maxLat, minLng, maxLng
1932
+ * @returns {Array<string>} Document IDs within bounds
1933
+ */
1361
1934
  findWithin(bounds) {
1362
1935
  const results = [];
1363
1936
 
@@ -1370,8 +1943,9 @@ class GeoIndex {
1370
1943
  return results;
1371
1944
  }
1372
1945
 
1946
+ /** @private Calculates distance using the Haversine formula. */
1373
1947
  _haversine(coord1, coord2) {
1374
- const R = 6371;
1948
+ const R = 6371; // Earth's radius in km
1375
1949
  const dLat = this._toRad(coord2.lat - coord1.lat);
1376
1950
  const dLng = this._toRad(coord2.lng - coord1.lng);
1377
1951
 
@@ -1383,24 +1957,25 @@ class GeoIndex {
1383
1957
  return R * c;
1384
1958
  }
1385
1959
 
1960
+ /** @private Converts degrees to radians. */
1386
1961
  _toRad(deg) {
1387
1962
  return deg * (Math.PI / 180);
1388
1963
  }
1389
1964
 
1965
+ /** @private Checks if coordinates are within bounds. */
1390
1966
  _isWithinBounds(coords, bounds) {
1391
1967
  return coords.lat >= bounds.minLat && coords.lat <= bounds.maxLat &&
1392
1968
  coords.lng >= bounds.minLng && coords.lng <= bounds.maxLng;
1393
1969
  }
1394
1970
 
1971
+ /** @type {number} Number of indexed points */
1395
1972
  get size() {
1396
1973
  return this._points.size;
1397
1974
  }
1398
1975
  }
1399
-
1400
1976
  // ========================
1401
- // Index Manager (Optimized)
1977
+ // B-Tree Index Implementation with Self-Healing
1402
1978
  // ========================
1403
-
1404
1979
  class IndexManager {
1405
1980
  constructor(collection) {
1406
1981
  this._collection = collection;
@@ -1658,18 +2233,22 @@ class IndexManager {
1658
2233
  return value;
1659
2234
  }
1660
2235
 
1661
- _saveIndexMetadata() {
1662
- const metadata = {
1663
- indexes: Array.from(this._indexes.entries()).map(([name, index]) => ({
1664
- name,
1665
- ...index
1666
- }))
1667
- };
1668
-
2236
+ async _saveIndexMetadata() {
1669
2237
  const key = `lacertadb_${this._collection.database.name}_${this._collection.name}_indexes`;
1670
- const serialized = serializer.serialize(metadata);
1671
- const encoded = base64.encode(serialized);
1672
- localStorage.setItem(key, encoded);
2238
+ return new Promise((resolve) => {
2239
+ requestIdleCallback(() => {
2240
+ const metadata = {
2241
+ indexes: Array.from(this._indexes.entries()).map(([name, index]) => ({
2242
+ name,
2243
+ ...index
2244
+ }))
2245
+ };
2246
+ const serialized = serializer.serialize(metadata);
2247
+ const encoded = base64.encode(serialized);
2248
+ localStorage.setItem(key, encoded);
2249
+ resolve();
2250
+ });
2251
+ });
1673
2252
  }
1674
2253
 
1675
2254
  async loadIndexMetadata() {
@@ -1739,11 +2318,20 @@ class IndexManager {
1739
2318
 
1740
2319
  return report;
1741
2320
  }
2321
+ destroy() {
2322
+ // Clear all index data
2323
+ for (const [name, indexData] of this._indexData) {
2324
+ if (indexData && indexData.clear) {
2325
+ indexData.clear();
2326
+ }
2327
+ }
2328
+ this._indexData.clear();
2329
+ this._indexes.clear();
2330
+ this._indexQueue = [];
2331
+ this._processing = false;
2332
+ }
1742
2333
  }
1743
2334
 
1744
- // ========================
1745
- // OPFS (Origin Private File System) Utility
1746
- // ========================
1747
2335
 
1748
2336
  class OPFSUtility {
1749
2337
  async saveAttachments(dbName, collectionName, documentId, attachments) {
@@ -2043,10 +2631,25 @@ class Document {
2043
2631
  if (this._compressed) {
2044
2632
  unpacked = await this._compression.decompress(unpacked);
2045
2633
  }
2634
+
2635
+ // Validate unpacked data
2636
+ if (!unpacked || unpacked.length === 0) {
2637
+ throw new Error('Empty unpacked data');
2638
+ }
2639
+
2046
2640
  this.data = serializer.deserialize(unpacked);
2641
+
2642
+ // Validate deserialized data
2643
+ if (typeof this.data !== 'object' || this.data === null) {
2644
+ throw new Error('Invalid deserialized data');
2645
+ }
2646
+
2047
2647
  return this.data;
2048
2648
  } catch (error) {
2049
- throw new LacertaDBError('Failed to unpack document', 'UNPACK_FAILED', error);
2649
+ console.error('Document unpack failed:', error);
2650
+ // Return empty object instead of throwing
2651
+ this.data = {};
2652
+ return this.data;
2050
2653
  }
2051
2654
  }
2052
2655
 
@@ -3085,25 +3688,6 @@ class Collection {
3085
3688
  ));
3086
3689
  }
3087
3690
 
3088
- async clear(options = {}) {
3089
- if (!this._initialized) await this.init();
3090
-
3091
- if (options.force) {
3092
- await this._indexedDB.clear(this._db, 'documents');
3093
- this._metadata = new CollectionMetadata(this.name);
3094
- this.database.metadata.setCollection(this._metadata);
3095
- this._cacheStrategy.clear();
3096
-
3097
- for (const indexName of this._indexManager.indexes.keys()) {
3098
- await this._indexManager.rebuildIndex(indexName);
3099
- }
3100
- } else {
3101
- const allDocs = await this.getAll();
3102
- const nonPermanentDocs = allDocs.filter(doc => !doc._permanent);
3103
- await this.batchDelete(nonPermanentDocs.map(doc => doc._id));
3104
- }
3105
- }
3106
-
3107
3691
  async _checkSpaceLimit() {
3108
3692
  if (this._settings.sizeLimitKB !== Infinity && this._metadata.sizeKB > this._settings.bufferLimitKB) {
3109
3693
  await this._freeSpace();
@@ -3141,12 +3725,62 @@ class Collection {
3141
3725
  this._cacheStrategy.clear();
3142
3726
  }
3143
3727
 
3728
+ async clear(options = {}) {
3729
+ if (!this._initialized) await this.init();
3730
+
3731
+ if (options.force) {
3732
+ // Clear documents first
3733
+ await this._indexedDB.clear(this._db, 'documents');
3734
+
3735
+ // Reset metadata
3736
+ this._metadata = new CollectionMetadata(this.name);
3737
+ this.database.metadata.setCollection(this._metadata);
3738
+
3739
+ // Clear cache
3740
+ this._cacheStrategy.clear();
3741
+
3742
+ // Rebuild indexes after clearing
3743
+ for (const indexName of this._indexManager.indexes.keys()) {
3744
+ await this._indexManager.rebuildIndex(indexName);
3745
+ }
3746
+ } else {
3747
+ const allDocs = await this.getAll();
3748
+ const nonPermanentDocs = allDocs.filter(doc => !doc._permanent);
3749
+ await this.batchDelete(nonPermanentDocs.map(doc => doc._id));
3750
+ }
3751
+
3752
+ // Reset cleanup interval if needed
3753
+ if (this._cleanupInterval) {
3754
+ clearInterval(this._cleanupInterval);
3755
+ this._cleanupInterval = null;
3756
+
3757
+ if (this._settings.freeSpaceEvery > 0 && this._settings.sizeLimitKB !== Infinity) {
3758
+ this._cleanupInterval = setInterval(() => this._freeSpace(), this._settings.freeSpaceEvery);
3759
+ }
3760
+ }
3761
+ }
3762
+
3144
3763
  destroy() {
3145
- clearInterval(this._cleanupInterval);
3764
+ // Clear the cleanup interval
3765
+ if (this._cleanupInterval) {
3766
+ clearInterval(this._cleanupInterval);
3767
+ this._cleanupInterval = null;
3768
+ }
3769
+
3770
+ // Destroy cache strategy
3771
+ if (this._cacheStrategy) {
3772
+ this._cacheStrategy.destroy();
3773
+ }
3774
+
3775
+ // Release the connection
3146
3776
  if (this._db) {
3147
3777
  const dbName = `${this.database.name}_${this.name}`;
3148
3778
  connectionPool.releaseConnection(dbName);
3779
+ this._db = null;
3149
3780
  }
3781
+
3782
+ // Clear event listeners
3783
+ this._events.clear();
3150
3784
  }
3151
3785
  }
3152
3786
  // ========================
@@ -3161,7 +3795,7 @@ class Database {
3161
3795
  this._settings = null;
3162
3796
  this._quickStore = null;
3163
3797
  this._performanceMonitor = performanceMonitor;
3164
-
3798
+
3165
3799
  // Database-level encryption
3166
3800
  this._encryption = null;
3167
3801
  this._isEncrypted = false;
@@ -3199,20 +3833,20 @@ class Database {
3199
3833
  this._metadata = DatabaseMetadata.load(this.name);
3200
3834
  this._settings = Settings.load(this.name);
3201
3835
  this._quickStore = new QuickStore(this.name);
3202
-
3836
+
3203
3837
  if (options.pin) {
3204
3838
  await this._initializeEncryption(options.pin, options.salt, options.encryptionConfig);
3205
3839
  }
3206
-
3840
+
3207
3841
  return this;
3208
3842
  }
3209
3843
 
3210
3844
  async _initializeEncryption(pin, salt = null, config = {}) {
3211
3845
  this._encryption = new SecureDatabaseEncryption(config);
3212
-
3846
+
3213
3847
  const encMetaKey = `lacertadb_${this.name}_encryption`;
3214
3848
  let existingMetadata = null;
3215
-
3849
+
3216
3850
  if (!salt) {
3217
3851
  const stored = localStorage.getItem(encMetaKey);
3218
3852
  if (stored) {
@@ -3224,16 +3858,16 @@ class Database {
3224
3858
  }
3225
3859
  }
3226
3860
  }
3227
-
3861
+
3228
3862
  const saltBase64 = await this._encryption.initialize(pin, salt);
3229
-
3863
+
3230
3864
  if (!existingMetadata) {
3231
3865
  const metadata = this._encryption.exportMetadata();
3232
3866
  const serialized = serializer.serialize(metadata);
3233
3867
  const encoded = base64.encode(serialized);
3234
3868
  localStorage.setItem(encMetaKey, encoded);
3235
3869
  }
3236
-
3870
+
3237
3871
  this._isEncrypted = true;
3238
3872
  return saltBase64;
3239
3873
  }
@@ -3242,15 +3876,15 @@ class Database {
3242
3876
  if (!this._isEncrypted) {
3243
3877
  throw new Error('Database is not encrypted');
3244
3878
  }
3245
-
3879
+
3246
3880
  const newSalt = await this._encryption.changePin(oldPin, newPin);
3247
-
3881
+
3248
3882
  const encMetaKey = `lacertadb_${this.name}_encryption`;
3249
3883
  const metadata = this._encryption.exportMetadata();
3250
3884
  const serialized = serializer.serialize(metadata);
3251
3885
  const encoded = base64.encode(serialized);
3252
3886
  localStorage.setItem(encMetaKey, encoded);
3253
-
3887
+
3254
3888
  return newSalt;
3255
3889
  }
3256
3890
 
@@ -3258,17 +3892,17 @@ class Database {
3258
3892
  if (!this._isEncrypted) {
3259
3893
  throw new Error('Database must be encrypted to store private keys');
3260
3894
  }
3261
-
3895
+
3262
3896
  const encryptedKey = await this._encryption.encryptPrivateKey(
3263
- privateKey,
3897
+ privateKey,
3264
3898
  additionalAuth
3265
3899
  );
3266
-
3900
+
3267
3901
  let keyStore = await this.getCollection('__private_keys__').catch(() => null);
3268
3902
  if (!keyStore) {
3269
3903
  keyStore = await this.createCollection('__private_keys__');
3270
3904
  }
3271
-
3905
+
3272
3906
  await keyStore.add({
3273
3907
  name: keyName,
3274
3908
  key: encryptedKey,
@@ -3277,7 +3911,7 @@ class Database {
3277
3911
  id: keyName,
3278
3912
  permanent: true
3279
3913
  });
3280
-
3914
+
3281
3915
  return true;
3282
3916
  }
3283
3917
 
@@ -3285,14 +3919,14 @@ class Database {
3285
3919
  if (!this._isEncrypted) {
3286
3920
  throw new Error('Database must be encrypted to retrieve private keys');
3287
3921
  }
3288
-
3922
+
3289
3923
  const keyStore = await this.getCollection('__private_keys__');
3290
3924
  const doc = await keyStore.get(keyName);
3291
-
3925
+
3292
3926
  if (!doc) {
3293
3927
  throw new Error(`Private key '${keyName}' not found`);
3294
3928
  }
3295
-
3929
+
3296
3930
  return await this._encryption.decryptPrivateKey(doc.key, additionalAuth);
3297
3931
  }
3298
3932
 
@@ -3374,7 +4008,7 @@ class Database {
3374
4008
 
3375
4009
  async export(format = 'json', password = null) {
3376
4010
  const data = {
3377
- version: '0.7.0',
4011
+ version: '0.8.0',
3378
4012
  database: this.name,
3379
4013
  timestamp: Date.now(),
3380
4014
  collections: {}
@@ -3443,16 +4077,31 @@ class Database {
3443
4077
  this._quickStore.clear();
3444
4078
  }
3445
4079
 
3446
- destroy() {
3447
- this._collections.forEach(collection => {
4080
+ async destroy() {
4081
+ // Destroy all collections first
4082
+ for (const collection of this._collections.values()) {
3448
4083
  if (collection.initialized) {
4084
+ await collection.clear({ force: true });
3449
4085
  collection.destroy();
3450
4086
  }
3451
- });
4087
+ }
3452
4088
  this._collections.clear();
4089
+
4090
+ // Clear quickstore
4091
+ if (this._quickStore) {
4092
+ this._quickStore.clear();
4093
+ }
4094
+
4095
+ // Destroy encryption
3453
4096
  if (this._encryption) {
3454
4097
  this._encryption.destroy();
3455
4098
  }
4099
+
4100
+ // Clear references
4101
+ this._metadata = null;
4102
+ this._settings = null;
4103
+ this._quickStore = null;
4104
+ this._performanceMonitor = null;
3456
4105
  }
3457
4106
  }
3458
4107
 
@@ -3526,7 +4175,7 @@ class LacertaDB {
3526
4175
 
3527
4176
  async createBackup(password = null) {
3528
4177
  const backup = {
3529
- version: '0.7.0',
4178
+ version: '0.8.0',
3530
4179
  timestamp: Date.now(),
3531
4180
  databases: {}
3532
4181
  };
@@ -3575,6 +4224,12 @@ class LacertaDB {
3575
4224
  return results;
3576
4225
  }
3577
4226
 
4227
+ // Add this method to LacertaDB class:
4228
+ close() {
4229
+ connectionPool.closeAll();
4230
+ }
4231
+
4232
+ // Then fix destroy to not call close twice:
3578
4233
  destroy() {
3579
4234
  for (const db of this._databases.values()) {
3580
4235
  db.destroy();