@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/.idea/lacerta-db.iml +8 -0
- package/.idea/modules.xml +8 -0
- package/.idea/php.xml +19 -0
- package/.idea/workspace.xml +61 -0
- package/dist/browser.min.js +17 -5
- package/dist/index.min.js +17 -5
- package/index (Copy).js +3718 -0
- package/index.js +736 -81
- package/package.json +7 -4
package/index.js
CHANGED
|
@@ -1,36 +1,105 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LacertaDB V0.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
172
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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();
|