@pixagram/lacerta-db 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.min.js +23 -5
- package/dist/index.min.js +23 -5
- package/index.js +174 -97
- package/package.json +1 -1
- package/readme.md +763 -420
- package/.idea/lacerta-db.iml +0 -8
- package/.idea/modules.xml +0 -8
- package/.idea/php.xml +0 -19
- package/.idea/workspace.xml +0 -61
- package/index (Copy).js +0 -3718
package/index (Copy).js
DELETED
|
@@ -1,3718 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LacertaDB V0.7.3 - Production Library with QuickStore
|
|
3
|
-
* Added: QuickStore feature, private property/method conventions
|
|
4
|
-
* @version 0.7.3
|
|
5
|
-
* @license MIT
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
'use strict';
|
|
9
|
-
|
|
10
|
-
// Add polyfill at the top of the file:
|
|
11
|
-
if (!window.requestIdleCallback) {
|
|
12
|
-
window.requestIdleCallback = function(callback) {
|
|
13
|
-
return setTimeout(callback, 0);
|
|
14
|
-
};
|
|
15
|
-
window.cancelIdleCallback = clearTimeout;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Dependencies - for browser environments using a bundler (e.g., Webpack, Vite)
|
|
19
|
-
import TurboSerial from "@pixagram/turboserial";
|
|
20
|
-
import TurboBase64 from "@pixagram/turbobase64";
|
|
21
|
-
|
|
22
|
-
const serializer = new TurboSerial({
|
|
23
|
-
compression: true,
|
|
24
|
-
deduplication: true,
|
|
25
|
-
simdOptimization: true,
|
|
26
|
-
detectCircular: true
|
|
27
|
-
});
|
|
28
|
-
const base64 = new TurboBase64();
|
|
29
|
-
|
|
30
|
-
// ========================
|
|
31
|
-
// Quick Store (localStorage based)
|
|
32
|
-
// ========================
|
|
33
|
-
|
|
34
|
-
class QuickStore {
|
|
35
|
-
constructor(dbName) {
|
|
36
|
-
this._dbName = dbName;
|
|
37
|
-
this._keyPrefix = `lacertadb_${dbName}_quickstore_`;
|
|
38
|
-
this._indexKey = `${this._keyPrefix}index`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
_readIndex() {
|
|
42
|
-
const indexStr = localStorage.getItem(this._indexKey);
|
|
43
|
-
if (!indexStr) return [];
|
|
44
|
-
try {
|
|
45
|
-
const decoded = base64.decode(indexStr);
|
|
46
|
-
return serializer.deserialize(decoded);
|
|
47
|
-
} catch {
|
|
48
|
-
return [];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
_writeIndex(index) {
|
|
53
|
-
const serializedIndex = serializer.serialize(index);
|
|
54
|
-
const encodedIndex = base64.encode(serializedIndex);
|
|
55
|
-
localStorage.setItem(this._indexKey, encodedIndex);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
add(docId, data) {
|
|
59
|
-
const key = `${this._keyPrefix}data_${docId}`;
|
|
60
|
-
try {
|
|
61
|
-
const serializedData = serializer.serialize(data);
|
|
62
|
-
const encodedData = base64.encode(serializedData);
|
|
63
|
-
localStorage.setItem(key, encodedData);
|
|
64
|
-
|
|
65
|
-
const index = this._readIndex();
|
|
66
|
-
if (!index.includes(docId)) {
|
|
67
|
-
index.push(docId);
|
|
68
|
-
this._writeIndex(index);
|
|
69
|
-
}
|
|
70
|
-
return true;
|
|
71
|
-
} catch (e) {
|
|
72
|
-
if (e.name === 'QuotaExceededError') {
|
|
73
|
-
throw new LacertaDBError('QuickStore quota exceeded', 'QUOTA_EXCEEDED', e);
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
get(docId) {
|
|
80
|
-
const key = `${this._keyPrefix}data_${docId}`;
|
|
81
|
-
const stored = localStorage.getItem(key);
|
|
82
|
-
if (stored) {
|
|
83
|
-
try {
|
|
84
|
-
const decoded = base64.decode(stored);
|
|
85
|
-
return serializer.deserialize(decoded);
|
|
86
|
-
} catch (e) {
|
|
87
|
-
console.error('Failed to parse QuickStore data:', e);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
update(docId, data) {
|
|
94
|
-
return this.add(docId, data);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
delete(docId) {
|
|
98
|
-
const key = `${this._keyPrefix}data_${docId}`;
|
|
99
|
-
localStorage.removeItem(key);
|
|
100
|
-
|
|
101
|
-
let index = this._readIndex();
|
|
102
|
-
const initialLength = index.length;
|
|
103
|
-
index = index.filter(id => id !== docId);
|
|
104
|
-
if (index.length < initialLength) {
|
|
105
|
-
this._writeIndex(index);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
getAll() {
|
|
110
|
-
const index = this._readIndex();
|
|
111
|
-
return index.map(docId => {
|
|
112
|
-
const doc = this.get(docId);
|
|
113
|
-
return doc ? { _id: docId, ...doc } : null;
|
|
114
|
-
}).filter(Boolean);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
query(filter = {}) {
|
|
118
|
-
const allDocs = this.getAll();
|
|
119
|
-
if (Object.keys(filter).length === 0) return allDocs;
|
|
120
|
-
|
|
121
|
-
// Use the same query engine as main database
|
|
122
|
-
return allDocs.filter(doc => queryEngine.evaluate(doc, filter));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
clear() {
|
|
126
|
-
const index = this._readIndex();
|
|
127
|
-
for (const docId of index) {
|
|
128
|
-
localStorage.removeItem(`${this._keyPrefix}data_${docId}`);
|
|
129
|
-
}
|
|
130
|
-
localStorage.removeItem(this._indexKey);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
get size() {
|
|
134
|
-
return this._readIndex().length;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ========================
|
|
139
|
-
// Global IndexedDB Connection Pool
|
|
140
|
-
// ========================
|
|
141
|
-
|
|
142
|
-
class IndexedDBConnectionPool {
|
|
143
|
-
constructor() {
|
|
144
|
-
this._connections = new Map();
|
|
145
|
-
this._refCounts = new Map();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async getConnection(dbName, version = 1, upgradeCallback) {
|
|
149
|
-
const key = `${dbName}_v${version}`;
|
|
150
|
-
|
|
151
|
-
if (this._connections.has(key)) {
|
|
152
|
-
this._refCounts.set(key, (this._refCounts.get(key) || 0) + 1);
|
|
153
|
-
return this._connections.get(key);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const db = await new Promise((resolve, reject) => {
|
|
157
|
-
const request = indexedDB.open(dbName, version);
|
|
158
|
-
request.onerror = () => reject(new LacertaDBError(
|
|
159
|
-
'Failed to open database', 'DATABASE_OPEN_FAILED', request.error
|
|
160
|
-
));
|
|
161
|
-
request.onsuccess = () => resolve(request.result);
|
|
162
|
-
request.onupgradeneeded = event => {
|
|
163
|
-
if (upgradeCallback) {
|
|
164
|
-
upgradeCallback(event.target.result, event.oldVersion, event.newVersion);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
this._connections.set(key, db);
|
|
170
|
-
this._refCounts.set(key, 1);
|
|
171
|
-
return db;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
releaseConnection(dbName, version = 1) {
|
|
175
|
-
const key = `${dbName}_v${version}`;
|
|
176
|
-
const refCount = this._refCounts.get(key) || 0;
|
|
177
|
-
|
|
178
|
-
// Force close if refCount is 1 or less
|
|
179
|
-
if (refCount <= 1) {
|
|
180
|
-
const db = this._connections.get(key);
|
|
181
|
-
if (db) {
|
|
182
|
-
db.close();
|
|
183
|
-
this._connections.delete(key);
|
|
184
|
-
this._refCounts.delete(key);
|
|
185
|
-
}
|
|
186
|
-
} else {
|
|
187
|
-
this._refCounts.set(key, refCount - 1);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
closeAll() {
|
|
192
|
-
for (const db of this._connections.values()) {
|
|
193
|
-
db.close();
|
|
194
|
-
}
|
|
195
|
-
this._connections.clear();
|
|
196
|
-
this._refCounts.clear();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const connectionPool = new IndexedDBConnectionPool();
|
|
201
|
-
|
|
202
|
-
// ========================
|
|
203
|
-
// Async Mutex for managing concurrent operations
|
|
204
|
-
// ========================
|
|
205
|
-
|
|
206
|
-
class AsyncMutex {
|
|
207
|
-
constructor() {
|
|
208
|
-
this._queue = [];
|
|
209
|
-
this._locked = false;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
acquire() {
|
|
213
|
-
return new Promise(resolve => {
|
|
214
|
-
this._queue.push(resolve);
|
|
215
|
-
this._dispatch();
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
release() {
|
|
220
|
-
this._locked = false;
|
|
221
|
-
this._dispatch();
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async runExclusive(callback) {
|
|
225
|
-
const release = await this.acquire();
|
|
226
|
-
try {
|
|
227
|
-
return await callback();
|
|
228
|
-
} finally {
|
|
229
|
-
release();
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
_dispatch() {
|
|
234
|
-
if (this._locked || this._queue.length === 0) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
this._locked = true;
|
|
238
|
-
const resolve = this._queue.shift();
|
|
239
|
-
resolve(() => this.release());
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// ========================
|
|
244
|
-
// Custom error class for LacertaDB
|
|
245
|
-
// ========================
|
|
246
|
-
|
|
247
|
-
class LacertaDBError extends Error {
|
|
248
|
-
constructor(message, code, originalError) {
|
|
249
|
-
super(message);
|
|
250
|
-
this.name = 'LacertaDBError';
|
|
251
|
-
this.code = code;
|
|
252
|
-
this.originalError = originalError || null;
|
|
253
|
-
this.timestamp = new Date().toISOString();
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ========================
|
|
258
|
-
// LRU Cache Implementation
|
|
259
|
-
// ========================
|
|
260
|
-
|
|
261
|
-
class LRUCache {
|
|
262
|
-
constructor(maxSize = 100, ttl = null) {
|
|
263
|
-
this._maxSize = maxSize;
|
|
264
|
-
this._ttl = ttl;
|
|
265
|
-
this._cache = new Map();
|
|
266
|
-
this._accessOrder = [];
|
|
267
|
-
this._timestamps = new Map();
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
get(key) {
|
|
271
|
-
if (!this._cache.has(key)) {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (this._ttl) {
|
|
276
|
-
const timestamp = this._timestamps.get(key);
|
|
277
|
-
if (Date.now() - timestamp > this._ttl) {
|
|
278
|
-
this.delete(key);
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const index = this._accessOrder.indexOf(key);
|
|
284
|
-
if (index > -1) {
|
|
285
|
-
this._accessOrder.splice(index, 1);
|
|
286
|
-
}
|
|
287
|
-
this._accessOrder.push(key);
|
|
288
|
-
|
|
289
|
-
return this._cache.get(key);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
set(key, value) {
|
|
293
|
-
if (this._cache.has(key)) {
|
|
294
|
-
const index = this._accessOrder.indexOf(key);
|
|
295
|
-
if (index > -1) {
|
|
296
|
-
this._accessOrder.splice(index, 1);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
this._cache.set(key, value);
|
|
301
|
-
this._accessOrder.push(key);
|
|
302
|
-
this._timestamps.set(key, Date.now());
|
|
303
|
-
|
|
304
|
-
while (this._cache.size > this._maxSize) {
|
|
305
|
-
const oldest = this._accessOrder.shift();
|
|
306
|
-
this._cache.delete(oldest);
|
|
307
|
-
this._timestamps.delete(oldest);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
delete(key) {
|
|
312
|
-
const index = this._accessOrder.indexOf(key);
|
|
313
|
-
if (index > -1) {
|
|
314
|
-
this._accessOrder.splice(index, 1);
|
|
315
|
-
}
|
|
316
|
-
this._timestamps.delete(key);
|
|
317
|
-
return this._cache.delete(key);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
clear() {
|
|
321
|
-
this._cache.clear();
|
|
322
|
-
this._accessOrder = [];
|
|
323
|
-
this._timestamps.clear();
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
has(key) {
|
|
327
|
-
if (this._ttl && this._cache.has(key)) {
|
|
328
|
-
const timestamp = this._timestamps.get(key);
|
|
329
|
-
if (Date.now() - timestamp > this._ttl) {
|
|
330
|
-
this.delete(key);
|
|
331
|
-
return false;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
return this._cache.has(key);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
get size() {
|
|
338
|
-
return this._cache.size;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// LFU (Least Frequently Used) Cache
|
|
343
|
-
class LFUCache {
|
|
344
|
-
constructor(maxSize = 100, ttl = null) {
|
|
345
|
-
this._maxSize = maxSize;
|
|
346
|
-
this._ttl = ttl;
|
|
347
|
-
this._cache = new Map();
|
|
348
|
-
this._frequencies = new Map();
|
|
349
|
-
this._timestamps = new Map();
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
get(key) {
|
|
353
|
-
if (!this._cache.has(key)) {
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (this._ttl) {
|
|
358
|
-
const timestamp = this._timestamps.get(key);
|
|
359
|
-
if (Date.now() - timestamp > this._ttl) {
|
|
360
|
-
this.delete(key);
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
this._frequencies.set(key, (this._frequencies.get(key) || 0) + 1);
|
|
366
|
-
return this._cache.get(key);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
set(key, value) {
|
|
370
|
-
if (this._cache.has(key)) {
|
|
371
|
-
this._cache.set(key, value);
|
|
372
|
-
this._frequencies.set(key, (this._frequencies.get(key) || 0) + 1);
|
|
373
|
-
} else {
|
|
374
|
-
if (this._cache.size >= this._maxSize) {
|
|
375
|
-
let minFreq = Infinity;
|
|
376
|
-
let evictKey = null;
|
|
377
|
-
for (const [k, freq] of this._frequencies) {
|
|
378
|
-
if (freq < minFreq) {
|
|
379
|
-
minFreq = freq;
|
|
380
|
-
evictKey = k;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
if (evictKey) {
|
|
384
|
-
this.delete(evictKey);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
this._cache.set(key, value);
|
|
389
|
-
this._frequencies.set(key, 1);
|
|
390
|
-
this._timestamps.set(key, Date.now());
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
delete(key) {
|
|
395
|
-
this._frequencies.delete(key);
|
|
396
|
-
this._timestamps.delete(key);
|
|
397
|
-
return this._cache.delete(key);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
clear() {
|
|
401
|
-
this._cache.clear();
|
|
402
|
-
this._frequencies.clear();
|
|
403
|
-
this._timestamps.clear();
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
has(key) {
|
|
407
|
-
return this._cache.has(key);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
get size() {
|
|
411
|
-
return this._cache.size;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// TTL (Time To Live) Only Cache
|
|
416
|
-
class TTLCache {
|
|
417
|
-
constructor(ttl = 60000) {
|
|
418
|
-
this._ttl = ttl;
|
|
419
|
-
this._cache = new Map();
|
|
420
|
-
this._timers = new Map();
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
get(key) {
|
|
424
|
-
return this._cache.get(key) || null;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
set(key, value) {
|
|
428
|
-
if (this._timers.has(key)) {
|
|
429
|
-
clearTimeout(this._timers.get(key));
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
this._cache.set(key, value);
|
|
433
|
-
|
|
434
|
-
const timer = setTimeout(() => {
|
|
435
|
-
this.delete(key);
|
|
436
|
-
}, this._ttl);
|
|
437
|
-
this._timers.set(key, timer);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
delete(key) {
|
|
441
|
-
if (this._timers.has(key)) {
|
|
442
|
-
clearTimeout(this._timers.get(key));
|
|
443
|
-
this._timers.delete(key);
|
|
444
|
-
}
|
|
445
|
-
return this._cache.delete(key);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
clear() {
|
|
449
|
-
for (const timer of this._timers.values()) {
|
|
450
|
-
clearTimeout(timer);
|
|
451
|
-
}
|
|
452
|
-
this._timers.clear();
|
|
453
|
-
this._cache.clear();
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
has(key) {
|
|
457
|
-
return this._cache.has(key);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
get size() {
|
|
461
|
-
return this._cache.size;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Add a destroy method:
|
|
465
|
-
destroy() {
|
|
466
|
-
for (const timer of this._timers.values()) {
|
|
467
|
-
clearTimeout(timer);
|
|
468
|
-
}
|
|
469
|
-
this._timers.clear();
|
|
470
|
-
this._cache.clear();
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// ========================
|
|
475
|
-
// Cache Strategy System
|
|
476
|
-
// ========================
|
|
477
|
-
|
|
478
|
-
class CacheStrategy {
|
|
479
|
-
constructor(config = {}) {
|
|
480
|
-
this._type = config.type || 'lru';
|
|
481
|
-
this._maxSize = config.maxSize || 100;
|
|
482
|
-
this._ttl = config.ttl || null;
|
|
483
|
-
this._enabled = config.enabled !== false;
|
|
484
|
-
this._cache = null;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Lazy initialization with getter
|
|
488
|
-
get cache() {
|
|
489
|
-
if (!this._cache) {
|
|
490
|
-
this._cache = this._createCache();
|
|
491
|
-
}
|
|
492
|
-
return this._cache;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
_createCache() {
|
|
496
|
-
switch (this._type) {
|
|
497
|
-
case 'lru':
|
|
498
|
-
return new LRUCache(this._maxSize, this._ttl);
|
|
499
|
-
case 'lfu':
|
|
500
|
-
return new LFUCache(this._maxSize, this._ttl);
|
|
501
|
-
case 'ttl':
|
|
502
|
-
return new TTLCache(this._ttl);
|
|
503
|
-
case 'none':
|
|
504
|
-
return null;
|
|
505
|
-
default:
|
|
506
|
-
return new LRUCache(this._maxSize, this._ttl);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
get(key) {
|
|
511
|
-
if (!this._enabled || !this.cache) return null;
|
|
512
|
-
return this.cache.get(key);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
set(key, value) {
|
|
516
|
-
if (!this._enabled || !this.cache) return;
|
|
517
|
-
this.cache.set(key, value);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
delete(key) {
|
|
521
|
-
if (!this._enabled || !this.cache) return;
|
|
522
|
-
this.cache.delete(key);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
clear() {
|
|
526
|
-
if (!this._enabled || !this.cache) return;
|
|
527
|
-
this.cache.clear();
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
updateStrategy(newConfig) {
|
|
531
|
-
Object.assign(this, newConfig);
|
|
532
|
-
this._cache = null; // Reset cache for lazy reinitialization
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
destroy() {
|
|
536
|
-
if (this._cache && this._cache.destroy) {
|
|
537
|
-
this._cache.destroy();
|
|
538
|
-
} else if (this._cache && this._cache.clear) {
|
|
539
|
-
this._cache.clear();
|
|
540
|
-
}
|
|
541
|
-
this._cache = null;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// ========================
|
|
546
|
-
// Compression Utility (Fixed)
|
|
547
|
-
// ========================
|
|
548
|
-
|
|
549
|
-
class BrowserCompressionUtility {
|
|
550
|
-
async compress(input) {
|
|
551
|
-
if (!(input instanceof Uint8Array)) {
|
|
552
|
-
throw new TypeError('Input must be Uint8Array');
|
|
553
|
-
}
|
|
554
|
-
try {
|
|
555
|
-
const stream = new Response(input).body
|
|
556
|
-
.pipeThrough(new CompressionStream('deflate'));
|
|
557
|
-
const compressed = await new Response(stream).arrayBuffer();
|
|
558
|
-
return new Uint8Array(compressed);
|
|
559
|
-
} catch (error) {
|
|
560
|
-
console.warn('CompressionStream not available, returning uncompressed data');
|
|
561
|
-
return input;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
async decompress(compressedData) {
|
|
566
|
-
if (!(compressedData instanceof Uint8Array)) {
|
|
567
|
-
throw new TypeError('Input must be Uint8Array');
|
|
568
|
-
}
|
|
569
|
-
try {
|
|
570
|
-
const stream = new Response(compressedData).body
|
|
571
|
-
.pipeThrough(new DecompressionStream('deflate'));
|
|
572
|
-
const decompressed = await new Response(stream).arrayBuffer();
|
|
573
|
-
return new Uint8Array(decompressed);
|
|
574
|
-
} catch (error) {
|
|
575
|
-
console.warn('DecompressionStream failed, returning original data');
|
|
576
|
-
return compressedData;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
compressSync(input) {
|
|
581
|
-
if (!(input instanceof Uint8Array)) {
|
|
582
|
-
throw new TypeError('Input must be Uint8Array');
|
|
583
|
-
}
|
|
584
|
-
return input;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
decompressSync(compressedData) {
|
|
588
|
-
if (!(compressedData instanceof Uint8Array)) {
|
|
589
|
-
throw new TypeError('Input must be Uint8Array');
|
|
590
|
-
}
|
|
591
|
-
return compressedData;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// ========================
|
|
596
|
-
// Browser Encryption Utility
|
|
597
|
-
// ========================
|
|
598
|
-
|
|
599
|
-
class BrowserEncryptionUtility {
|
|
600
|
-
async encrypt(data, password) {
|
|
601
|
-
const encoder = new TextEncoder();
|
|
602
|
-
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
603
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
604
|
-
|
|
605
|
-
const passwordBuffer = encoder.encode(password);
|
|
606
|
-
const keyMaterial = await crypto.subtle.importKey(
|
|
607
|
-
'raw',
|
|
608
|
-
passwordBuffer,
|
|
609
|
-
'PBKDF2',
|
|
610
|
-
false,
|
|
611
|
-
['deriveBits', 'deriveKey']
|
|
612
|
-
);
|
|
613
|
-
|
|
614
|
-
const key = await crypto.subtle.deriveKey(
|
|
615
|
-
{
|
|
616
|
-
name: 'PBKDF2',
|
|
617
|
-
salt: salt,
|
|
618
|
-
iterations: 100000,
|
|
619
|
-
hash: 'SHA-256'
|
|
620
|
-
},
|
|
621
|
-
keyMaterial,
|
|
622
|
-
{ name: 'AES-GCM', length: 256 },
|
|
623
|
-
false,
|
|
624
|
-
['encrypt', 'decrypt']
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
const encrypted = await crypto.subtle.encrypt(
|
|
628
|
-
{ name: 'AES-GCM', iv },
|
|
629
|
-
key,
|
|
630
|
-
data
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
const result = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
|
|
634
|
-
result.set(salt, 0);
|
|
635
|
-
result.set(iv, salt.length);
|
|
636
|
-
result.set(new Uint8Array(encrypted), salt.length + iv.length);
|
|
637
|
-
|
|
638
|
-
return result;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
async decrypt(encryptedData, password) {
|
|
642
|
-
const encoder = new TextEncoder();
|
|
643
|
-
const salt = encryptedData.slice(0, 16);
|
|
644
|
-
const iv = encryptedData.slice(16, 28);
|
|
645
|
-
const data = encryptedData.slice(28);
|
|
646
|
-
|
|
647
|
-
const passwordBuffer = encoder.encode(password);
|
|
648
|
-
const keyMaterial = await crypto.subtle.importKey(
|
|
649
|
-
'raw',
|
|
650
|
-
passwordBuffer,
|
|
651
|
-
'PBKDF2',
|
|
652
|
-
false,
|
|
653
|
-
['deriveBits', 'deriveKey']
|
|
654
|
-
);
|
|
655
|
-
|
|
656
|
-
const key = await crypto.subtle.deriveKey(
|
|
657
|
-
{
|
|
658
|
-
name: 'PBKDF2',
|
|
659
|
-
salt: salt,
|
|
660
|
-
iterations: 100000,
|
|
661
|
-
hash: 'SHA-256'
|
|
662
|
-
},
|
|
663
|
-
keyMaterial,
|
|
664
|
-
{ name: 'AES-GCM', length: 256 },
|
|
665
|
-
false,
|
|
666
|
-
['encrypt', 'decrypt']
|
|
667
|
-
);
|
|
668
|
-
|
|
669
|
-
const decrypted = await crypto.subtle.decrypt(
|
|
670
|
-
{ name: 'AES-GCM', iv },
|
|
671
|
-
key,
|
|
672
|
-
data
|
|
673
|
-
);
|
|
674
|
-
|
|
675
|
-
return new Uint8Array(decrypted);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// ========================
|
|
680
|
-
// Database-Level Encryption
|
|
681
|
-
// ========================
|
|
682
|
-
|
|
683
|
-
class SecureDatabaseEncryption {
|
|
684
|
-
constructor(config = {}) {
|
|
685
|
-
this._masterKey = null;
|
|
686
|
-
this._salt = null;
|
|
687
|
-
this._iterations = config.iterations || 100000;
|
|
688
|
-
this._hashAlgorithm = config.hashAlgorithm || 'SHA-256';
|
|
689
|
-
this._keyLength = config.keyLength || 256;
|
|
690
|
-
this._saltLength = config.saltLength || 32;
|
|
691
|
-
this._initialized = false;
|
|
692
|
-
this._encKey = null;
|
|
693
|
-
this._hmacKey = null;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
get initialized() {
|
|
697
|
-
return this._initialized;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
async initialize(pin, salt = null) {
|
|
701
|
-
if (this._initialized) {
|
|
702
|
-
throw new Error('Database encryption already initialized');
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
this._salt = salt || crypto.getRandomValues(new Uint8Array(this._saltLength));
|
|
706
|
-
|
|
707
|
-
const encoder = new TextEncoder();
|
|
708
|
-
const pinBuffer = encoder.encode(pin);
|
|
709
|
-
|
|
710
|
-
const keyMaterial = await crypto.subtle.importKey(
|
|
711
|
-
'raw',
|
|
712
|
-
pinBuffer,
|
|
713
|
-
'PBKDF2',
|
|
714
|
-
false,
|
|
715
|
-
['deriveBits', 'deriveKey']
|
|
716
|
-
);
|
|
717
|
-
|
|
718
|
-
const derivedBits = await crypto.subtle.deriveBits(
|
|
719
|
-
{
|
|
720
|
-
name: 'PBKDF2',
|
|
721
|
-
salt: this._salt,
|
|
722
|
-
iterations: this._iterations,
|
|
723
|
-
hash: this._hashAlgorithm
|
|
724
|
-
},
|
|
725
|
-
keyMaterial,
|
|
726
|
-
512
|
|
727
|
-
);
|
|
728
|
-
|
|
729
|
-
const derivedArray = new Uint8Array(derivedBits);
|
|
730
|
-
|
|
731
|
-
const encKeyBytes = derivedArray.slice(0, 32);
|
|
732
|
-
const hmacKeyBytes = derivedArray.slice(32, 64);
|
|
733
|
-
|
|
734
|
-
this._encKey = await crypto.subtle.importKey(
|
|
735
|
-
'raw',
|
|
736
|
-
encKeyBytes,
|
|
737
|
-
{ name: 'AES-GCM', length: this._keyLength },
|
|
738
|
-
false,
|
|
739
|
-
['encrypt', 'decrypt']
|
|
740
|
-
);
|
|
741
|
-
|
|
742
|
-
this._hmacKey = await crypto.subtle.importKey(
|
|
743
|
-
'raw',
|
|
744
|
-
hmacKeyBytes,
|
|
745
|
-
{ name: 'HMAC', hash: 'SHA-256' },
|
|
746
|
-
false,
|
|
747
|
-
['sign', 'verify']
|
|
748
|
-
);
|
|
749
|
-
|
|
750
|
-
this._initialized = true;
|
|
751
|
-
|
|
752
|
-
return base64.encode(this._salt);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
async encrypt(data) {
|
|
756
|
-
if (!this._initialized) {
|
|
757
|
-
throw new Error('Database encryption not initialized');
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
let dataBytes;
|
|
761
|
-
if (typeof data === 'string') {
|
|
762
|
-
dataBytes = new TextEncoder().encode(data);
|
|
763
|
-
} else if (data instanceof Uint8Array) {
|
|
764
|
-
dataBytes = data;
|
|
765
|
-
} else {
|
|
766
|
-
dataBytes = serializer.serialize(data);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
770
|
-
|
|
771
|
-
const encryptedData = await crypto.subtle.encrypt(
|
|
772
|
-
{ name: 'AES-GCM', iv },
|
|
773
|
-
this._encKey,
|
|
774
|
-
dataBytes
|
|
775
|
-
);
|
|
776
|
-
|
|
777
|
-
const hmacData = new Uint8Array(iv.length + encryptedData.byteLength);
|
|
778
|
-
hmacData.set(iv, 0);
|
|
779
|
-
hmacData.set(new Uint8Array(encryptedData), iv.length);
|
|
780
|
-
|
|
781
|
-
const hmac = await crypto.subtle.sign(
|
|
782
|
-
'HMAC',
|
|
783
|
-
this._hmacKey,
|
|
784
|
-
hmacData
|
|
785
|
-
);
|
|
786
|
-
|
|
787
|
-
const result = new Uint8Array(
|
|
788
|
-
iv.length + encryptedData.byteLength + 32
|
|
789
|
-
);
|
|
790
|
-
result.set(iv, 0);
|
|
791
|
-
result.set(new Uint8Array(encryptedData), iv.length);
|
|
792
|
-
result.set(new Uint8Array(hmac), iv.length + encryptedData.byteLength);
|
|
793
|
-
|
|
794
|
-
return result;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
async decrypt(encryptedPackage) {
|
|
798
|
-
if (!this._initialized) {
|
|
799
|
-
throw new Error('Database encryption not initialized');
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
if (!(encryptedPackage instanceof Uint8Array)) {
|
|
803
|
-
throw new TypeError('Encrypted data must be Uint8Array');
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
const iv = encryptedPackage.slice(0, 12);
|
|
807
|
-
const hmac = encryptedPackage.slice(-32);
|
|
808
|
-
const encryptedData = encryptedPackage.slice(12, -32);
|
|
809
|
-
|
|
810
|
-
const hmacData = new Uint8Array(iv.length + encryptedData.length);
|
|
811
|
-
hmacData.set(iv, 0);
|
|
812
|
-
hmacData.set(encryptedData, iv.length);
|
|
813
|
-
|
|
814
|
-
const isValid = await crypto.subtle.verify(
|
|
815
|
-
'HMAC',
|
|
816
|
-
this._hmacKey,
|
|
817
|
-
hmac,
|
|
818
|
-
hmacData
|
|
819
|
-
);
|
|
820
|
-
|
|
821
|
-
if (!isValid) {
|
|
822
|
-
throw new Error('HMAC verification failed - data may be tampered');
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
const decryptedData = await crypto.subtle.decrypt(
|
|
826
|
-
{ name: 'AES-GCM', iv },
|
|
827
|
-
this._encKey,
|
|
828
|
-
encryptedData
|
|
829
|
-
);
|
|
830
|
-
|
|
831
|
-
return new Uint8Array(decryptedData);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
async encryptPrivateKey(privateKey, additionalAuth = '') {
|
|
835
|
-
if (!this._initialized) {
|
|
836
|
-
throw new Error('Database encryption not initialized');
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const encoder = new TextEncoder();
|
|
840
|
-
const authData = encoder.encode(additionalAuth);
|
|
841
|
-
|
|
842
|
-
let keyData;
|
|
843
|
-
if (typeof privateKey === 'string') {
|
|
844
|
-
keyData = encoder.encode(privateKey);
|
|
845
|
-
} else if (privateKey instanceof Uint8Array) {
|
|
846
|
-
keyData = privateKey;
|
|
847
|
-
} else {
|
|
848
|
-
keyData = serializer.serialize(privateKey);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
852
|
-
|
|
853
|
-
const encryptedKey = await crypto.subtle.encrypt(
|
|
854
|
-
{
|
|
855
|
-
name: 'AES-GCM',
|
|
856
|
-
iv,
|
|
857
|
-
additionalData: authData,
|
|
858
|
-
tagLength: 128
|
|
859
|
-
},
|
|
860
|
-
this._encKey,
|
|
861
|
-
keyData
|
|
862
|
-
);
|
|
863
|
-
|
|
864
|
-
const authLength = new Uint32Array([authData.length]);
|
|
865
|
-
const result = new Uint8Array(
|
|
866
|
-
16 + 4 + authData.length + encryptedKey.byteLength
|
|
867
|
-
);
|
|
868
|
-
|
|
869
|
-
result.set(iv, 0);
|
|
870
|
-
result.set(new Uint8Array(authLength.buffer), 16);
|
|
871
|
-
result.set(authData, 20);
|
|
872
|
-
result.set(new Uint8Array(encryptedKey), 20 + authData.length);
|
|
873
|
-
|
|
874
|
-
return base64.encode(result);
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
async decryptPrivateKey(encryptedKeyString, additionalAuth = '') {
|
|
878
|
-
if (!this._initialized) {
|
|
879
|
-
throw new Error('Database encryption not initialized');
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
const encryptedPackage = base64.decode(encryptedKeyString);
|
|
883
|
-
|
|
884
|
-
const iv = encryptedPackage.slice(0, 16);
|
|
885
|
-
const authLengthBytes = encryptedPackage.slice(16, 20);
|
|
886
|
-
const authLength = new Uint32Array(authLengthBytes.buffer)[0];
|
|
887
|
-
const authData = encryptedPackage.slice(20, 20 + authLength);
|
|
888
|
-
const encryptedKey = encryptedPackage.slice(20 + authLength);
|
|
889
|
-
|
|
890
|
-
const encoder = new TextEncoder();
|
|
891
|
-
const expectedAuth = encoder.encode(additionalAuth);
|
|
892
|
-
|
|
893
|
-
if (!this._arrayEquals(authData, expectedAuth)) {
|
|
894
|
-
throw new Error('Additional authentication data mismatch');
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
const decryptedKey = await crypto.subtle.decrypt(
|
|
898
|
-
{
|
|
899
|
-
name: 'AES-GCM',
|
|
900
|
-
iv,
|
|
901
|
-
additionalData: authData,
|
|
902
|
-
tagLength: 128
|
|
903
|
-
},
|
|
904
|
-
this._encKey,
|
|
905
|
-
encryptedKey
|
|
906
|
-
);
|
|
907
|
-
|
|
908
|
-
return new TextDecoder().decode(decryptedKey);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
static generateSecurePIN(length = 6) {
|
|
912
|
-
const digits = new Uint8Array(length);
|
|
913
|
-
crypto.getRandomValues(digits);
|
|
914
|
-
return Array.from(digits)
|
|
915
|
-
.map(b => (b % 10).toString())
|
|
916
|
-
.join('');
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
destroy() {
|
|
920
|
-
this._masterKey = null;
|
|
921
|
-
this._encKey = null;
|
|
922
|
-
this._hmacKey = null;
|
|
923
|
-
this._salt = null;
|
|
924
|
-
this._initialized = false;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
_arrayEquals(a, b) {
|
|
928
|
-
if (a.length !== b.length) return false;
|
|
929
|
-
for (let i = 0; i < a.length; i++) {
|
|
930
|
-
if (a[i] !== b[i]) return false;
|
|
931
|
-
}
|
|
932
|
-
return true;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
async changePin(oldPin, newPin) {
|
|
936
|
-
if (!this._initialized) {
|
|
937
|
-
throw new Error('Database encryption not initialized');
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
const currentSalt = this._salt;
|
|
941
|
-
const currentConfig = {
|
|
942
|
-
iterations: this._iterations,
|
|
943
|
-
hashAlgorithm: this._hashAlgorithm,
|
|
944
|
-
keyLength: this._keyLength,
|
|
945
|
-
saltLength: this._saltLength
|
|
946
|
-
};
|
|
947
|
-
|
|
948
|
-
this.destroy();
|
|
949
|
-
Object.assign(this, currentConfig);
|
|
950
|
-
await this.initialize(oldPin, currentSalt);
|
|
951
|
-
|
|
952
|
-
this.destroy();
|
|
953
|
-
Object.assign(this, currentConfig);
|
|
954
|
-
const newSalt = await this.initialize(newPin);
|
|
955
|
-
|
|
956
|
-
return newSalt;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
exportMetadata() {
|
|
960
|
-
if (!this._salt) {
|
|
961
|
-
throw new Error('No encryption metadata to export');
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
return {
|
|
965
|
-
salt: base64.encode(this._salt),
|
|
966
|
-
iterations: this._iterations,
|
|
967
|
-
algorithm: 'AES-GCM-256',
|
|
968
|
-
kdf: 'PBKDF2',
|
|
969
|
-
hashAlgorithm: this._hashAlgorithm,
|
|
970
|
-
keyLength: this._keyLength,
|
|
971
|
-
saltLength: this._saltLength
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
importMetadata(metadata) {
|
|
976
|
-
if (!metadata.salt) {
|
|
977
|
-
throw new Error('Invalid encryption metadata');
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
this._salt = base64.decode(metadata.salt);
|
|
981
|
-
this._iterations = metadata.iterations || 100000;
|
|
982
|
-
this._hashAlgorithm = metadata.hashAlgorithm || 'SHA-256';
|
|
983
|
-
this._keyLength = metadata.keyLength || 256;
|
|
984
|
-
this._saltLength = metadata.saltLength || 32;
|
|
985
|
-
|
|
986
|
-
return true;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
// ========================
|
|
991
|
-
// B-Tree Index Implementation with Self-Healing
|
|
992
|
-
// ========================
|
|
993
|
-
|
|
994
|
-
class BTreeNode {
|
|
995
|
-
constructor(order, leaf) {
|
|
996
|
-
this.keys = new Array(2 * order - 1);
|
|
997
|
-
this.values = new Array(2 * order - 1);
|
|
998
|
-
this.children = new Array(2 * order);
|
|
999
|
-
this.n = 0;
|
|
1000
|
-
this.leaf = leaf;
|
|
1001
|
-
this.order = order;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
search(key) {
|
|
1005
|
-
let i = 0;
|
|
1006
|
-
while (i < this.n && key > this.keys[i]) {
|
|
1007
|
-
i++;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
if (i < this.n && key === this.keys[i]) {
|
|
1011
|
-
return this.values[i];
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
if (this.leaf) {
|
|
1015
|
-
return null;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
return this.children[i] ? this.children[i].search(key) : null;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
rangeSearch(min, max, results) {
|
|
1022
|
-
let i = 0;
|
|
1023
|
-
|
|
1024
|
-
while (i < this.n) {
|
|
1025
|
-
if (!this.leaf && this.children[i]) {
|
|
1026
|
-
this.children[i].rangeSearch(min, max, results);
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
if (this.keys[i] >= min && this.keys[i] <= max) {
|
|
1030
|
-
if (this.values[i]) {
|
|
1031
|
-
this.values[i].forEach(v => results.push(v));
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
i++;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
if (!this.leaf && this.children[i]) {
|
|
1039
|
-
this.children[i].rangeSearch(min, max, results);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
insertNonFull(key, value) {
|
|
1044
|
-
let i = this.n - 1;
|
|
1045
|
-
|
|
1046
|
-
if (this.leaf) {
|
|
1047
|
-
while (i >= 0 && this.keys[i] > key) {
|
|
1048
|
-
this.keys[i + 1] = this.keys[i];
|
|
1049
|
-
this.values[i + 1] = this.values[i];
|
|
1050
|
-
i--;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
if (i >= 0 && this.keys[i] === key) {
|
|
1054
|
-
if (!this.values[i]) {
|
|
1055
|
-
this.values[i] = new Set();
|
|
1056
|
-
}
|
|
1057
|
-
this.values[i].add(value);
|
|
1058
|
-
} else {
|
|
1059
|
-
this.keys[i + 1] = key;
|
|
1060
|
-
this.values[i + 1] = new Set([value]);
|
|
1061
|
-
this.n++;
|
|
1062
|
-
}
|
|
1063
|
-
} else {
|
|
1064
|
-
while (i >= 0 && this.keys[i] > key) {
|
|
1065
|
-
i--;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
if (i >= 0 && this.keys[i] === key) {
|
|
1069
|
-
if (!this.values[i]) {
|
|
1070
|
-
this.values[i] = new Set();
|
|
1071
|
-
}
|
|
1072
|
-
this.values[i].add(value);
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
i++;
|
|
1077
|
-
if (this.children[i] && this.children[i].n === 2 * this.order - 1) {
|
|
1078
|
-
this.splitChild(i, this.children[i]);
|
|
1079
|
-
if (this.keys[i] < key) {
|
|
1080
|
-
i++;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
if (this.children[i]) {
|
|
1084
|
-
this.children[i].insertNonFull(key, value);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
splitChild(i, y) {
|
|
1090
|
-
const z = new BTreeNode(this.order, y.leaf);
|
|
1091
|
-
z.n = this.order - 1;
|
|
1092
|
-
|
|
1093
|
-
for (let j = 0; j < this.order - 1; j++) {
|
|
1094
|
-
z.keys[j] = y.keys[j + this.order];
|
|
1095
|
-
z.values[j] = y.values[j + this.order];
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
if (!y.leaf) {
|
|
1099
|
-
for (let j = 0; j < this.order; j++) {
|
|
1100
|
-
z.children[j] = y.children[j + this.order];
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
y.n = this.order - 1;
|
|
1105
|
-
|
|
1106
|
-
for (let j = this.n; j >= i + 1; j--) {
|
|
1107
|
-
this.children[j + 1] = this.children[j];
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
this.children[i + 1] = z;
|
|
1111
|
-
|
|
1112
|
-
for (let j = this.n - 1; j >= i; j--) {
|
|
1113
|
-
this.keys[j + 1] = this.keys[j];
|
|
1114
|
-
this.values[j + 1] = this.values[j];
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
this.keys[i] = y.keys[this.order - 1];
|
|
1118
|
-
this.values[i] = y.values[this.order - 1];
|
|
1119
|
-
this.n++;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
remove(key, value) {
|
|
1123
|
-
let i = 0;
|
|
1124
|
-
while (i < this.n && key > this.keys[i]) {
|
|
1125
|
-
i++;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
if (i < this.n && key === this.keys[i]) {
|
|
1129
|
-
if (this.values[i]) {
|
|
1130
|
-
this.values[i].delete(value);
|
|
1131
|
-
if (this.values[i].size === 0) {
|
|
1132
|
-
for (let j = i; j < this.n - 1; j++) {
|
|
1133
|
-
this.keys[j] = this.keys[j + 1];
|
|
1134
|
-
this.values[j] = this.values[j + 1];
|
|
1135
|
-
}
|
|
1136
|
-
this.n--;
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
} else if (!this.leaf && this.children[i]) {
|
|
1140
|
-
this.children[i].remove(key, value);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
verify() {
|
|
1145
|
-
const issues = [];
|
|
1146
|
-
|
|
1147
|
-
for (let i = 1; i < this.n; i++) {
|
|
1148
|
-
if (this.keys[i] <= this.keys[i - 1]) {
|
|
1149
|
-
issues.push(`Key order violation at index ${i}`);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
for (let i = 0; i < this.n; i++) {
|
|
1154
|
-
if (this.values[i] && !(this.values[i] instanceof Set)) {
|
|
1155
|
-
this.values[i] = new Set(Array.isArray(this.values[i]) ? this.values[i] : [this.values[i]]);
|
|
1156
|
-
issues.push(`Fixed non-Set value at index ${i}`);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
if (!this.leaf) {
|
|
1161
|
-
for (let i = 0; i <= this.n; i++) {
|
|
1162
|
-
if (this.children[i]) {
|
|
1163
|
-
const childIssues = this.children[i].verify();
|
|
1164
|
-
issues.push(...childIssues);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
return issues;
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
class BTreeIndex {
|
|
1174
|
-
constructor(order = 4) {
|
|
1175
|
-
this._root = null;
|
|
1176
|
-
this._order = order;
|
|
1177
|
-
this._size = 0;
|
|
1178
|
-
this._lastVerification = Date.now();
|
|
1179
|
-
this._verificationInterval = 60000;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
insert(key, value) {
|
|
1183
|
-
if (Date.now() - this._lastVerification > this._verificationInterval) {
|
|
1184
|
-
this.verify();
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
if (!this._root) {
|
|
1188
|
-
this._root = new BTreeNode(this._order, true);
|
|
1189
|
-
this._root.keys[0] = key;
|
|
1190
|
-
this._root.values[0] = new Set([value]);
|
|
1191
|
-
this._root.n = 1;
|
|
1192
|
-
} else {
|
|
1193
|
-
if (this._root.n === 2 * this._order - 1) {
|
|
1194
|
-
const s = new BTreeNode(this._order, false);
|
|
1195
|
-
s.children[0] = this._root;
|
|
1196
|
-
s.splitChild(0, this._root);
|
|
1197
|
-
|
|
1198
|
-
let i = 0;
|
|
1199
|
-
if (s.keys[0] < key) i++;
|
|
1200
|
-
s.children[i].insertNonFull(key, value);
|
|
1201
|
-
|
|
1202
|
-
this._root = s;
|
|
1203
|
-
} else {
|
|
1204
|
-
this._root.insertNonFull(key, value);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
this._size++;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
find(key) {
|
|
1211
|
-
if (!this._root) return [];
|
|
1212
|
-
const values = this._root.search(key);
|
|
1213
|
-
return values ? Array.from(values) : [];
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
contains(key) {
|
|
1217
|
-
if (!this._root) return false;
|
|
1218
|
-
return this._root.search(key) !== null;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
range(min, max) {
|
|
1222
|
-
if (!this._root) return [];
|
|
1223
|
-
const results = [];
|
|
1224
|
-
this._root.rangeSearch(min, max, results);
|
|
1225
|
-
return results;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
rangeFrom(min) {
|
|
1229
|
-
if (!this._root) return [];
|
|
1230
|
-
const results = [];
|
|
1231
|
-
this._root.rangeSearch(min, Infinity, results);
|
|
1232
|
-
return results;
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
rangeTo(max) {
|
|
1236
|
-
if (!this._root) return [];
|
|
1237
|
-
const results = [];
|
|
1238
|
-
this._root.rangeSearch(-Infinity, max, results);
|
|
1239
|
-
return results;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
remove(key, value) {
|
|
1243
|
-
if (!this._root) return;
|
|
1244
|
-
this._root.remove(key, value);
|
|
1245
|
-
if (this._root.n === 0) {
|
|
1246
|
-
if (!this._root.leaf && this._root.children[0]) {
|
|
1247
|
-
this._root = this._root.children[0];
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
this._size--;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
verify() {
|
|
1254
|
-
this._lastVerification = Date.now();
|
|
1255
|
-
if (!this._root) return { healthy: true, issues: [] };
|
|
1256
|
-
|
|
1257
|
-
const issues = this._root.verify();
|
|
1258
|
-
|
|
1259
|
-
if (issues.length > 0) {
|
|
1260
|
-
console.warn('BTree index issues detected and fixed:', issues);
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
return {
|
|
1264
|
-
healthy: issues.length === 0,
|
|
1265
|
-
issues,
|
|
1266
|
-
repaired: issues.length
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
get size() {
|
|
1271
|
-
return this._size;
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// Text Index for full-text search
|
|
1276
|
-
class TextIndex {
|
|
1277
|
-
constructor() {
|
|
1278
|
-
this._invertedIndex = new Map();
|
|
1279
|
-
this._documentTexts = new Map();
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
addDocument(text, docId) {
|
|
1283
|
-
if (typeof text !== 'string') return;
|
|
1284
|
-
|
|
1285
|
-
this._documentTexts.set(docId, text);
|
|
1286
|
-
const tokens = this._tokenize(text);
|
|
1287
|
-
|
|
1288
|
-
for (const token of tokens) {
|
|
1289
|
-
if (!this._invertedIndex.has(token)) {
|
|
1290
|
-
this._invertedIndex.set(token, new Set());
|
|
1291
|
-
}
|
|
1292
|
-
this._invertedIndex.get(token).add(docId);
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
removeDocument(docId) {
|
|
1297
|
-
const text = this._documentTexts.get(docId);
|
|
1298
|
-
if (!text) return;
|
|
1299
|
-
|
|
1300
|
-
const tokens = this._tokenize(text);
|
|
1301
|
-
for (const token of tokens) {
|
|
1302
|
-
const docs = this._invertedIndex.get(token);
|
|
1303
|
-
if (docs) {
|
|
1304
|
-
docs.delete(docId);
|
|
1305
|
-
if (docs.size === 0) {
|
|
1306
|
-
this._invertedIndex.delete(token);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
this._documentTexts.delete(docId);
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
updateDocument(docId, newText) {
|
|
1315
|
-
this.removeDocument(docId);
|
|
1316
|
-
this.addDocument(newText, docId);
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
search(query) {
|
|
1320
|
-
const tokens = this._tokenize(query);
|
|
1321
|
-
if (tokens.length === 0) return [];
|
|
1322
|
-
|
|
1323
|
-
let results = null;
|
|
1324
|
-
for (const token of tokens) {
|
|
1325
|
-
const docs = this._invertedIndex.get(token);
|
|
1326
|
-
if (!docs || docs.size === 0) {
|
|
1327
|
-
return [];
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
if (results === null) {
|
|
1331
|
-
results = new Set(docs);
|
|
1332
|
-
} else {
|
|
1333
|
-
results = new Set([...results].filter(x => docs.has(x)));
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
return results ? Array.from(results) : [];
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
_tokenize(text) {
|
|
1341
|
-
return text.toLowerCase()
|
|
1342
|
-
.replace(/[^\w\s]/g, ' ')
|
|
1343
|
-
.split(/\s+/)
|
|
1344
|
-
.filter(token => token.length > 2);
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
get size() {
|
|
1348
|
-
return this._documentTexts.size;
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
// Geo Index for spatial queries
|
|
1353
|
-
class GeoIndex {
|
|
1354
|
-
constructor() {
|
|
1355
|
-
this._points = new Map();
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
addPoint(coords, docId) {
|
|
1359
|
-
if (!coords || typeof coords.lat !== 'number' || typeof coords.lng !== 'number') {
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
this._points.set(docId, coords);
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
removePoint(docId) {
|
|
1366
|
-
this._points.delete(docId);
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
updatePoint(docId, newCoords) {
|
|
1370
|
-
this._points.set(docId, newCoords);
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
findNear(center, maxDistance) {
|
|
1374
|
-
const results = [];
|
|
1375
|
-
|
|
1376
|
-
for (const [docId, coords] of this._points) {
|
|
1377
|
-
const distance = this._haversine(center, coords);
|
|
1378
|
-
if (distance <= maxDistance) {
|
|
1379
|
-
results.push({ docId, distance });
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
return results.sort((a, b) => a.distance - b.distance)
|
|
1384
|
-
.map(r => r.docId);
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
findWithin(bounds) {
|
|
1388
|
-
const results = [];
|
|
1389
|
-
|
|
1390
|
-
for (const [docId, coords] of this._points) {
|
|
1391
|
-
if (this._isWithinBounds(coords, bounds)) {
|
|
1392
|
-
results.push(docId);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
return results;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
_haversine(coord1, coord2) {
|
|
1400
|
-
const R = 6371;
|
|
1401
|
-
const dLat = this._toRad(coord2.lat - coord1.lat);
|
|
1402
|
-
const dLng = this._toRad(coord2.lng - coord1.lng);
|
|
1403
|
-
|
|
1404
|
-
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
|
1405
|
-
Math.cos(this._toRad(coord1.lat)) * Math.cos(this._toRad(coord2.lat)) *
|
|
1406
|
-
Math.sin(dLng/2) * Math.sin(dLng/2);
|
|
1407
|
-
|
|
1408
|
-
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
1409
|
-
return R * c;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
_toRad(deg) {
|
|
1413
|
-
return deg * (Math.PI / 180);
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
_isWithinBounds(coords, bounds) {
|
|
1417
|
-
return coords.lat >= bounds.minLat && coords.lat <= bounds.maxLat &&
|
|
1418
|
-
coords.lng >= bounds.minLng && coords.lng <= bounds.maxLng;
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
get size() {
|
|
1422
|
-
return this._points.size;
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
// ========================
|
|
1427
|
-
// Index Manager (Optimized)
|
|
1428
|
-
// ========================
|
|
1429
|
-
|
|
1430
|
-
class IndexManager {
|
|
1431
|
-
constructor(collection) {
|
|
1432
|
-
this._collection = collection;
|
|
1433
|
-
this._indexes = new Map();
|
|
1434
|
-
this._indexData = new Map();
|
|
1435
|
-
this._indexQueue = [];
|
|
1436
|
-
this._processing = false;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
get indexes() {
|
|
1440
|
-
return this._indexes;
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
async createIndex(fieldPath, options = {}) {
|
|
1444
|
-
const indexName = options.name || fieldPath;
|
|
1445
|
-
|
|
1446
|
-
if (this._indexes.has(indexName)) {
|
|
1447
|
-
throw new Error(`Index '${indexName}' already exists`);
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
const index = {
|
|
1451
|
-
fieldPath,
|
|
1452
|
-
unique: options.unique || false,
|
|
1453
|
-
sparse: options.sparse || false,
|
|
1454
|
-
type: options.type || 'btree',
|
|
1455
|
-
collation: options.collation || null,
|
|
1456
|
-
createdAt: Date.now()
|
|
1457
|
-
};
|
|
1458
|
-
|
|
1459
|
-
this._indexes.set(indexName, index);
|
|
1460
|
-
|
|
1461
|
-
await this.rebuildIndex(indexName);
|
|
1462
|
-
|
|
1463
|
-
this._saveIndexMetadata();
|
|
1464
|
-
|
|
1465
|
-
return indexName;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
async rebuildIndex(indexName) {
|
|
1469
|
-
const index = this._indexes.get(indexName);
|
|
1470
|
-
if (!index) {
|
|
1471
|
-
throw new Error(`Index '${indexName}' not found`);
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
const indexData = this._createIndexStructure(index.type);
|
|
1475
|
-
|
|
1476
|
-
const allDocs = await this._collection.getAll();
|
|
1477
|
-
|
|
1478
|
-
for (const doc of allDocs) {
|
|
1479
|
-
const value = this._getFieldValue(doc, index.fieldPath);
|
|
1480
|
-
|
|
1481
|
-
if (index.sparse && (value === null || value === undefined)) {
|
|
1482
|
-
continue;
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
if (index.unique && indexData.has && indexData.has(value)) {
|
|
1486
|
-
throw new Error(`Unique constraint violation on index '${indexName}'`);
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
this._addToIndex(indexData, value, doc._id, index.type);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
this._indexData.set(indexName, indexData);
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
_createIndexStructure(type) {
|
|
1496
|
-
switch (type) {
|
|
1497
|
-
case 'btree':
|
|
1498
|
-
return new BTreeIndex();
|
|
1499
|
-
case 'hash':
|
|
1500
|
-
return new Map();
|
|
1501
|
-
case 'text':
|
|
1502
|
-
return new TextIndex();
|
|
1503
|
-
case 'geo':
|
|
1504
|
-
return new GeoIndex();
|
|
1505
|
-
default:
|
|
1506
|
-
return new Map();
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
_addToIndex(indexData, value, docId, type) {
|
|
1511
|
-
switch (type) {
|
|
1512
|
-
case 'btree':
|
|
1513
|
-
indexData.insert(value, docId);
|
|
1514
|
-
break;
|
|
1515
|
-
case 'hash':
|
|
1516
|
-
if (!indexData.has(value)) {
|
|
1517
|
-
indexData.set(value, new Set());
|
|
1518
|
-
}
|
|
1519
|
-
indexData.get(value).add(docId);
|
|
1520
|
-
break;
|
|
1521
|
-
case 'text':
|
|
1522
|
-
indexData.addDocument(value, docId);
|
|
1523
|
-
break;
|
|
1524
|
-
case 'geo':
|
|
1525
|
-
indexData.addPoint(value, docId);
|
|
1526
|
-
break;
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
async updateIndexForDocument(docId, oldDoc, newDoc) {
|
|
1531
|
-
for (const [indexName, index] of this._indexes) {
|
|
1532
|
-
const indexData = this._indexData.get(indexName);
|
|
1533
|
-
if (!indexData) continue;
|
|
1534
|
-
|
|
1535
|
-
const oldValue = oldDoc ? this._getFieldValue(oldDoc, index.fieldPath) : undefined;
|
|
1536
|
-
const newValue = newDoc ? this._getFieldValue(newDoc, index.fieldPath) : undefined;
|
|
1537
|
-
|
|
1538
|
-
if (oldValue === newValue) continue;
|
|
1539
|
-
|
|
1540
|
-
switch (index.type) {
|
|
1541
|
-
case 'btree':
|
|
1542
|
-
if (oldValue !== undefined) indexData.remove(oldValue, docId);
|
|
1543
|
-
if (newValue !== undefined) indexData.insert(newValue, docId);
|
|
1544
|
-
break;
|
|
1545
|
-
case 'hash':
|
|
1546
|
-
if (oldValue !== undefined) {
|
|
1547
|
-
const oldSet = indexData.get(oldValue);
|
|
1548
|
-
if (oldSet) {
|
|
1549
|
-
oldSet.delete(docId);
|
|
1550
|
-
if (oldSet.size === 0) indexData.delete(oldValue);
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
if (newValue !== undefined) {
|
|
1554
|
-
if (!indexData.has(newValue)) indexData.set(newValue, new Set());
|
|
1555
|
-
indexData.get(newValue).add(docId);
|
|
1556
|
-
}
|
|
1557
|
-
break;
|
|
1558
|
-
case 'text':
|
|
1559
|
-
if (oldValue || newValue) {
|
|
1560
|
-
indexData.updateDocument(docId, newValue || '');
|
|
1561
|
-
}
|
|
1562
|
-
break;
|
|
1563
|
-
case 'geo':
|
|
1564
|
-
if (oldValue) indexData.removePoint(docId);
|
|
1565
|
-
if (newValue) indexData.addPoint(newValue, docId);
|
|
1566
|
-
break;
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
async query(indexName, queryOptions) {
|
|
1572
|
-
const index = this._indexes.get(indexName);
|
|
1573
|
-
const indexData = this._indexData.get(indexName);
|
|
1574
|
-
|
|
1575
|
-
if (!index || !indexData) {
|
|
1576
|
-
throw new Error(`Index '${indexName}' not found`);
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
return this._queryIndex(indexData, queryOptions, index.type);
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
_queryIndex(indexData, options, type) {
|
|
1583
|
-
switch (type) {
|
|
1584
|
-
case 'btree':
|
|
1585
|
-
return this._queryBTree(indexData, options);
|
|
1586
|
-
case 'hash':
|
|
1587
|
-
return this._queryHash(indexData, options);
|
|
1588
|
-
case 'text':
|
|
1589
|
-
return this._queryText(indexData, options);
|
|
1590
|
-
case 'geo':
|
|
1591
|
-
return this._queryGeo(indexData, options);
|
|
1592
|
-
default:
|
|
1593
|
-
return [];
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
_queryBTree(indexData, options) {
|
|
1598
|
-
const results = new Set();
|
|
1599
|
-
|
|
1600
|
-
if (options.$eq !== undefined) {
|
|
1601
|
-
const docs = indexData.find(options.$eq);
|
|
1602
|
-
docs.forEach(doc => results.add(doc));
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
if (options.$gte !== undefined && options.$lte !== undefined) {
|
|
1606
|
-
const docs = indexData.range(options.$gte, options.$lte);
|
|
1607
|
-
docs.forEach(doc => results.add(doc));
|
|
1608
|
-
} else if (options.$gte !== undefined) {
|
|
1609
|
-
const docs = indexData.rangeFrom(options.$gte);
|
|
1610
|
-
docs.forEach(doc => results.add(doc));
|
|
1611
|
-
} else if (options.$gt !== undefined) {
|
|
1612
|
-
const docs = indexData.rangeFrom(options.$gt);
|
|
1613
|
-
docs.forEach(doc => {
|
|
1614
|
-
if (doc !== options.$gt) results.add(doc);
|
|
1615
|
-
});
|
|
1616
|
-
} else if (options.$lte !== undefined) {
|
|
1617
|
-
const docs = indexData.rangeTo(options.$lte);
|
|
1618
|
-
docs.forEach(doc => results.add(doc));
|
|
1619
|
-
} else if (options.$lt !== undefined) {
|
|
1620
|
-
const docs = indexData.rangeTo(options.$lt);
|
|
1621
|
-
docs.forEach(doc => {
|
|
1622
|
-
if (doc !== options.$lt) results.add(doc);
|
|
1623
|
-
});
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
return Array.from(results);
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
_queryHash(indexData, options) {
|
|
1630
|
-
if (options.$eq !== undefined) {
|
|
1631
|
-
const docs = indexData.get(options.$eq);
|
|
1632
|
-
return docs ? Array.from(docs) : [];
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
if (options.$in !== undefined) {
|
|
1636
|
-
const results = new Set();
|
|
1637
|
-
for (const value of options.$in) {
|
|
1638
|
-
const docs = indexData.get(value);
|
|
1639
|
-
if (docs) {
|
|
1640
|
-
docs.forEach(doc => results.add(doc));
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
return Array.from(results);
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
return [];
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
_queryText(indexData, options) {
|
|
1650
|
-
if (options.$search) {
|
|
1651
|
-
return indexData.search(options.$search);
|
|
1652
|
-
}
|
|
1653
|
-
return [];
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
_queryGeo(indexData, options) {
|
|
1657
|
-
if (options.$near) {
|
|
1658
|
-
return indexData.findNear(
|
|
1659
|
-
options.$near.coordinates,
|
|
1660
|
-
options.$near.maxDistance || 1000
|
|
1661
|
-
);
|
|
1662
|
-
}
|
|
1663
|
-
if (options.$within) {
|
|
1664
|
-
return indexData.findWithin(options.$within);
|
|
1665
|
-
}
|
|
1666
|
-
return [];
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
dropIndex(indexName) {
|
|
1670
|
-
this._indexes.delete(indexName);
|
|
1671
|
-
this._indexData.delete(indexName);
|
|
1672
|
-
this._saveIndexMetadata();
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
_getFieldValue(doc, path) {
|
|
1676
|
-
const parts = path.split('.');
|
|
1677
|
-
let value = doc;
|
|
1678
|
-
for (const part of parts) {
|
|
1679
|
-
if (value === null || value === undefined) {
|
|
1680
|
-
return undefined;
|
|
1681
|
-
}
|
|
1682
|
-
value = value[part];
|
|
1683
|
-
}
|
|
1684
|
-
return value;
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
async _saveIndexMetadata() {
|
|
1688
|
-
const key = `lacertadb_${this._collection.database.name}_${this._collection.name}_indexes`;
|
|
1689
|
-
return new Promise((resolve) => {
|
|
1690
|
-
requestIdleCallback(() => {
|
|
1691
|
-
const metadata = {
|
|
1692
|
-
indexes: Array.from(this._indexes.entries()).map(([name, index]) => ({
|
|
1693
|
-
name,
|
|
1694
|
-
...index
|
|
1695
|
-
}))
|
|
1696
|
-
};
|
|
1697
|
-
const serialized = serializer.serialize(metadata);
|
|
1698
|
-
const encoded = base64.encode(serialized);
|
|
1699
|
-
localStorage.setItem(key, encoded);
|
|
1700
|
-
resolve();
|
|
1701
|
-
});
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
async loadIndexMetadata() {
|
|
1706
|
-
const key = `lacertadb_${this._collection.database.name}_${this._collection.name}_indexes`;
|
|
1707
|
-
const stored = localStorage.getItem(key);
|
|
1708
|
-
|
|
1709
|
-
if (!stored) return;
|
|
1710
|
-
|
|
1711
|
-
try {
|
|
1712
|
-
const decoded = base64.decode(stored);
|
|
1713
|
-
const metadata = serializer.deserialize(decoded);
|
|
1714
|
-
|
|
1715
|
-
for (const indexDef of metadata.indexes) {
|
|
1716
|
-
const { name, ...index } = indexDef;
|
|
1717
|
-
this._indexes.set(name, index);
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
for (const indexName of this._indexes.keys()) {
|
|
1721
|
-
await this.rebuildIndex(indexName);
|
|
1722
|
-
}
|
|
1723
|
-
} catch (error) {
|
|
1724
|
-
console.error('Failed to load index metadata:', error);
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
getIndexStats() {
|
|
1729
|
-
const stats = {};
|
|
1730
|
-
for (const [name, index] of this._indexes) {
|
|
1731
|
-
const indexData = this._indexData.get(name);
|
|
1732
|
-
stats[name] = {
|
|
1733
|
-
...index,
|
|
1734
|
-
size: indexData ? indexData.size || indexData.length || 0 : 0,
|
|
1735
|
-
memoryUsage: this._estimateMemoryUsage(indexData)
|
|
1736
|
-
};
|
|
1737
|
-
}
|
|
1738
|
-
return stats;
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
_estimateMemoryUsage(indexData) {
|
|
1742
|
-
if (!indexData) return 0;
|
|
1743
|
-
|
|
1744
|
-
if (indexData instanceof Map) {
|
|
1745
|
-
return indexData.size * 100;
|
|
1746
|
-
}
|
|
1747
|
-
if (indexData instanceof BTreeIndex) {
|
|
1748
|
-
return indexData.size * 120;
|
|
1749
|
-
}
|
|
1750
|
-
return 0;
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
async verifyIndexes() {
|
|
1754
|
-
const report = {};
|
|
1755
|
-
|
|
1756
|
-
for (const [name, index] of this._indexes) {
|
|
1757
|
-
const indexData = this._indexData.get(name);
|
|
1758
|
-
|
|
1759
|
-
if (!indexData) {
|
|
1760
|
-
report[name] = { status: 'missing', rebuilt: true };
|
|
1761
|
-
await this.rebuildIndex(name);
|
|
1762
|
-
} else if (indexData.verify) {
|
|
1763
|
-
const verification = indexData.verify();
|
|
1764
|
-
report[name] = verification;
|
|
1765
|
-
} else {
|
|
1766
|
-
report[name] = { status: 'ok' };
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
return report;
|
|
1771
|
-
}
|
|
1772
|
-
destroy() {
|
|
1773
|
-
// Clear all index data
|
|
1774
|
-
for (const [name, indexData] of this._indexData) {
|
|
1775
|
-
if (indexData && indexData.clear) {
|
|
1776
|
-
indexData.clear();
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
this._indexData.clear();
|
|
1780
|
-
this._indexes.clear();
|
|
1781
|
-
this._indexQueue = [];
|
|
1782
|
-
this._processing = false;
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
class OPFSUtility {
|
|
1788
|
-
async saveAttachments(dbName, collectionName, documentId, attachments) {
|
|
1789
|
-
try {
|
|
1790
|
-
const attachmentPaths = [];
|
|
1791
|
-
const root = await navigator.storage.getDirectory();
|
|
1792
|
-
const dbDir = await root.getDirectoryHandle(dbName, { create: true });
|
|
1793
|
-
const collDir = await dbDir.getDirectoryHandle(collectionName, { create: true });
|
|
1794
|
-
const docDir = await collDir.getDirectoryHandle(documentId, { create: true });
|
|
1795
|
-
|
|
1796
|
-
for (const [index, attachment] of attachments.entries()) {
|
|
1797
|
-
const filename = `${index}_${attachment.name || 'file'}`;
|
|
1798
|
-
const fileHandle = await docDir.getFileHandle(filename, { create: true });
|
|
1799
|
-
const writable = await fileHandle.createWritable();
|
|
1800
|
-
|
|
1801
|
-
let dataToWrite;
|
|
1802
|
-
if (attachment.data instanceof Uint8Array) {
|
|
1803
|
-
dataToWrite = attachment.data;
|
|
1804
|
-
} else if (attachment.data instanceof ArrayBuffer) {
|
|
1805
|
-
dataToWrite = new Uint8Array(attachment.data);
|
|
1806
|
-
} else if (attachment.data instanceof Blob) {
|
|
1807
|
-
dataToWrite = new Uint8Array(await attachment.data.arrayBuffer());
|
|
1808
|
-
} else {
|
|
1809
|
-
throw new TypeError('Unsupported attachment data type');
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
const blob = new Blob([dataToWrite], { type: attachment.type || 'application/octet-stream' });
|
|
1813
|
-
await writable.write(blob);
|
|
1814
|
-
await writable.close();
|
|
1815
|
-
|
|
1816
|
-
const path = `/${dbName}/${collectionName}/${documentId}/${filename}`;
|
|
1817
|
-
attachmentPaths.push({
|
|
1818
|
-
path,
|
|
1819
|
-
name: attachment.name,
|
|
1820
|
-
type: attachment.type,
|
|
1821
|
-
size: dataToWrite.byteLength,
|
|
1822
|
-
originalName: attachment.originalName || attachment.name
|
|
1823
|
-
});
|
|
1824
|
-
}
|
|
1825
|
-
return attachmentPaths;
|
|
1826
|
-
} catch (error) {
|
|
1827
|
-
throw new LacertaDBError('Failed to save attachments', 'ATTACHMENT_SAVE_FAILED', error);
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
async getAttachments(attachmentPaths) {
|
|
1832
|
-
const attachments = [];
|
|
1833
|
-
const root = await navigator.storage.getDirectory();
|
|
1834
|
-
|
|
1835
|
-
for (const attachmentInfo of attachmentPaths) {
|
|
1836
|
-
try {
|
|
1837
|
-
const pathParts = attachmentInfo.path.split('/').filter(p => p);
|
|
1838
|
-
let currentDir = root;
|
|
1839
|
-
|
|
1840
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
1841
|
-
currentDir = await currentDir.getDirectoryHandle(pathParts[i]);
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
const fileHandle = await currentDir.getFileHandle(pathParts[pathParts.length - 1]);
|
|
1845
|
-
const file = await fileHandle.getFile();
|
|
1846
|
-
const data = await file.arrayBuffer();
|
|
1847
|
-
|
|
1848
|
-
attachments.push({
|
|
1849
|
-
name: attachmentInfo.originalName || attachmentInfo.name,
|
|
1850
|
-
type: attachmentInfo.type,
|
|
1851
|
-
data: new Uint8Array(data),
|
|
1852
|
-
size: attachmentInfo.size
|
|
1853
|
-
});
|
|
1854
|
-
} catch (error) {
|
|
1855
|
-
console.error(`Failed to get attachment: ${attachmentInfo.path}`, error);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
return attachments;
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
async deleteAttachments(dbName, collectionName, documentId) {
|
|
1862
|
-
try {
|
|
1863
|
-
const root = await navigator.storage.getDirectory();
|
|
1864
|
-
const dbDir = await root.getDirectoryHandle(dbName);
|
|
1865
|
-
const collDir = await dbDir.getDirectoryHandle(collectionName);
|
|
1866
|
-
await collDir.removeEntry(documentId, { recursive: true });
|
|
1867
|
-
} catch (error) {
|
|
1868
|
-
if (error.name !== 'NotFoundError') {
|
|
1869
|
-
console.error(`Failed to delete attachments for ${documentId}:`, error);
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
static async prepareAttachment(file, name) {
|
|
1875
|
-
let data;
|
|
1876
|
-
if (file instanceof File || file instanceof Blob) {
|
|
1877
|
-
const buffer = await file.arrayBuffer();
|
|
1878
|
-
data = new Uint8Array(buffer);
|
|
1879
|
-
} else if (file instanceof ArrayBuffer) {
|
|
1880
|
-
data = new Uint8Array(file);
|
|
1881
|
-
} else if (file instanceof Uint8Array) {
|
|
1882
|
-
data = file;
|
|
1883
|
-
} else {
|
|
1884
|
-
throw new TypeError('Unsupported file type for attachment');
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
return {
|
|
1888
|
-
name: name || file.name || 'unnamed',
|
|
1889
|
-
type: file.type || 'application/octet-stream',
|
|
1890
|
-
data,
|
|
1891
|
-
originalName: file.name || name
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
// ========================
|
|
1897
|
-
// IndexedDB Utility (Optimized)
|
|
1898
|
-
// ========================
|
|
1899
|
-
|
|
1900
|
-
class IndexedDBUtility {
|
|
1901
|
-
constructor() {
|
|
1902
|
-
this._mutex = new AsyncMutex();
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
async performTransaction(db, storeNames, mode, callback, retries = 3) {
|
|
1906
|
-
return this._mutex.runExclusive(async () => {
|
|
1907
|
-
let lastError;
|
|
1908
|
-
for (let i = 0; i < retries; i++) {
|
|
1909
|
-
try {
|
|
1910
|
-
return await new Promise((resolve, reject) => {
|
|
1911
|
-
const transaction = db.transaction(storeNames, mode);
|
|
1912
|
-
let result;
|
|
1913
|
-
|
|
1914
|
-
transaction.oncomplete = () => resolve(result);
|
|
1915
|
-
transaction.onerror = () => reject(transaction.error);
|
|
1916
|
-
transaction.onabort = () => reject(new Error('Transaction aborted'));
|
|
1917
|
-
|
|
1918
|
-
try {
|
|
1919
|
-
const cbResult = callback(transaction);
|
|
1920
|
-
if (cbResult instanceof Promise) {
|
|
1921
|
-
cbResult.then(res => { result = res; }).catch(reject);
|
|
1922
|
-
} else {
|
|
1923
|
-
result = cbResult;
|
|
1924
|
-
}
|
|
1925
|
-
} catch (error) {
|
|
1926
|
-
reject(error);
|
|
1927
|
-
}
|
|
1928
|
-
});
|
|
1929
|
-
} catch (error) {
|
|
1930
|
-
lastError = error;
|
|
1931
|
-
if (i < retries - 1) {
|
|
1932
|
-
await new Promise(resolve => setTimeout(resolve, (2 ** i) * 100));
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
throw new LacertaDBError('Transaction failed after retries', 'TRANSACTION_FAILED', lastError);
|
|
1937
|
-
});
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
_promisifyRequest(requestFactory) {
|
|
1941
|
-
return new Promise((resolve, reject) => {
|
|
1942
|
-
const request = requestFactory();
|
|
1943
|
-
request.onsuccess = () => resolve(request.result);
|
|
1944
|
-
request.onerror = () => reject(request.error);
|
|
1945
|
-
});
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
add(db, storeName, value, key) {
|
|
1949
|
-
return this.performTransaction(db, [storeName], 'readwrite', tx => {
|
|
1950
|
-
const store = tx.objectStore(storeName);
|
|
1951
|
-
return this._promisifyRequest(() => key !== undefined ? store.add(value, key) : store.add(value));
|
|
1952
|
-
});
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
put(db, storeName, value, key) {
|
|
1956
|
-
return this.performTransaction(db, [storeName], 'readwrite', tx => {
|
|
1957
|
-
const store = tx.objectStore(storeName);
|
|
1958
|
-
return this._promisifyRequest(() => key !== undefined ? store.put(value, key) : store.put(value));
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
get(db, storeName, key) {
|
|
1963
|
-
return this.performTransaction(db, [storeName], 'readonly', tx => {
|
|
1964
|
-
return this._promisifyRequest(() => tx.objectStore(storeName).get(key));
|
|
1965
|
-
});
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
getAll(db, storeName, query, count) {
|
|
1969
|
-
return this.performTransaction(db, [storeName], 'readonly', tx => {
|
|
1970
|
-
return this._promisifyRequest(() => tx.objectStore(storeName).getAll(query, count));
|
|
1971
|
-
});
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
delete(db, storeName, key) {
|
|
1975
|
-
return this.performTransaction(db, [storeName], 'readwrite', tx => {
|
|
1976
|
-
return this._promisifyRequest(() => tx.objectStore(storeName).delete(key));
|
|
1977
|
-
});
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
clear(db, storeName) {
|
|
1981
|
-
return this.performTransaction(db, [storeName], 'readwrite', tx => {
|
|
1982
|
-
return this._promisifyRequest(() => tx.objectStore(storeName).clear());
|
|
1983
|
-
});
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
count(db, storeName, query) {
|
|
1987
|
-
return this.performTransaction(db, [storeName], 'readonly', tx => {
|
|
1988
|
-
return this._promisifyRequest(() => tx.objectStore(storeName).count(query));
|
|
1989
|
-
});
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
async batchOperation(db, operations) {
|
|
1993
|
-
return this.performTransaction(db, ['documents'], 'readwrite', async tx => {
|
|
1994
|
-
const store = tx.objectStore('documents');
|
|
1995
|
-
const results = [];
|
|
1996
|
-
|
|
1997
|
-
for (const op of operations) {
|
|
1998
|
-
try {
|
|
1999
|
-
let result;
|
|
2000
|
-
switch (op.type) {
|
|
2001
|
-
case 'add':
|
|
2002
|
-
result = await this._promisifyRequest(() => store.add(op.data));
|
|
2003
|
-
break;
|
|
2004
|
-
case 'put':
|
|
2005
|
-
result = await this._promisifyRequest(() => store.put(op.data));
|
|
2006
|
-
break;
|
|
2007
|
-
case 'delete':
|
|
2008
|
-
result = await this._promisifyRequest(() => store.delete(op.key));
|
|
2009
|
-
break;
|
|
2010
|
-
default:
|
|
2011
|
-
throw new Error(`Unknown operation type: ${op.type}`);
|
|
2012
|
-
}
|
|
2013
|
-
results.push({ success: true, result });
|
|
2014
|
-
} catch (error) {
|
|
2015
|
-
results.push({ success: false, error: error.message });
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
return results;
|
|
2020
|
-
});
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
// ========================
|
|
2025
|
-
// Document Class
|
|
2026
|
-
// ========================
|
|
2027
|
-
|
|
2028
|
-
class Document {
|
|
2029
|
-
constructor(data = {}, options = {}) {
|
|
2030
|
-
this._id = data._id || this._generateId();
|
|
2031
|
-
this._created = data._created || Date.now();
|
|
2032
|
-
this._modified = data._modified || Date.now();
|
|
2033
|
-
this._permanent = data._permanent || options.permanent || false;
|
|
2034
|
-
this._encrypted = false;
|
|
2035
|
-
this._compressed = data._compressed || options.compressed || false;
|
|
2036
|
-
this._attachments = data._attachments || [];
|
|
2037
|
-
this._data = null;
|
|
2038
|
-
this._packedData = data.packedData || null;
|
|
2039
|
-
this._compression = new BrowserCompressionUtility();
|
|
2040
|
-
|
|
2041
|
-
// Use getter/setter for memory optimization
|
|
2042
|
-
if (data.data) {
|
|
2043
|
-
this.data = data.data;
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
get data() {
|
|
2048
|
-
return this._data || {};
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
set data(value) {
|
|
2052
|
-
this._data = value;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
_generateId() {
|
|
2056
|
-
return `doc_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
async pack(encryptionUtil = null) {
|
|
2060
|
-
try {
|
|
2061
|
-
let packed = serializer.serialize(this.data);
|
|
2062
|
-
if (this._compressed) {
|
|
2063
|
-
packed = await this._compression.compress(packed);
|
|
2064
|
-
}
|
|
2065
|
-
if (encryptionUtil) {
|
|
2066
|
-
packed = await encryptionUtil.encrypt(packed);
|
|
2067
|
-
this._encrypted = true;
|
|
2068
|
-
}
|
|
2069
|
-
this._packedData = packed;
|
|
2070
|
-
return packed;
|
|
2071
|
-
} catch (error) {
|
|
2072
|
-
throw new LacertaDBError('Failed to pack document', 'PACK_FAILED', error);
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
async unpack(encryptionUtil = null) {
|
|
2077
|
-
try {
|
|
2078
|
-
let unpacked = this._packedData;
|
|
2079
|
-
if (this._encrypted && encryptionUtil) {
|
|
2080
|
-
unpacked = await encryptionUtil.decrypt(unpacked);
|
|
2081
|
-
}
|
|
2082
|
-
if (this._compressed) {
|
|
2083
|
-
unpacked = await this._compression.decompress(unpacked);
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
// Validate unpacked data
|
|
2087
|
-
if (!unpacked || unpacked.length === 0) {
|
|
2088
|
-
throw new Error('Empty unpacked data');
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
this.data = serializer.deserialize(unpacked);
|
|
2092
|
-
|
|
2093
|
-
// Validate deserialized data
|
|
2094
|
-
if (typeof this.data !== 'object' || this.data === null) {
|
|
2095
|
-
throw new Error('Invalid deserialized data');
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
return this.data;
|
|
2099
|
-
} catch (error) {
|
|
2100
|
-
console.error('Document unpack failed:', error);
|
|
2101
|
-
// Return empty object instead of throwing
|
|
2102
|
-
this.data = {};
|
|
2103
|
-
return this.data;
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
packSync() {
|
|
2108
|
-
let packed = serializer.serialize(this.data);
|
|
2109
|
-
if (this._compressed) {
|
|
2110
|
-
packed = this._compression.compressSync(packed);
|
|
2111
|
-
}
|
|
2112
|
-
this._packedData = packed;
|
|
2113
|
-
return packed;
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
unpackSync() {
|
|
2117
|
-
if (this._encrypted) {
|
|
2118
|
-
throw new LacertaDBError('Synchronous decryption not supported', 'SYNC_DECRYPT_NOT_SUPPORTED');
|
|
2119
|
-
}
|
|
2120
|
-
let unpacked = this._packedData;
|
|
2121
|
-
if (this._compressed) {
|
|
2122
|
-
unpacked = this._compression.decompressSync(unpacked);
|
|
2123
|
-
}
|
|
2124
|
-
this.data = serializer.deserialize(unpacked);
|
|
2125
|
-
return this.data;
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
objectOutput(includeAttachments = false) {
|
|
2129
|
-
const output = {
|
|
2130
|
-
_id: this._id,
|
|
2131
|
-
_created: this._created,
|
|
2132
|
-
_modified: this._modified,
|
|
2133
|
-
_permanent: this._permanent,
|
|
2134
|
-
...this.data
|
|
2135
|
-
};
|
|
2136
|
-
if (includeAttachments && this._attachments.length > 0) {
|
|
2137
|
-
output._attachments = this._attachments;
|
|
2138
|
-
}
|
|
2139
|
-
return output;
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
databaseOutput() {
|
|
2143
|
-
return {
|
|
2144
|
-
_id: this._id,
|
|
2145
|
-
_created: this._created,
|
|
2146
|
-
_modified: this._modified,
|
|
2147
|
-
_permanent: this._permanent,
|
|
2148
|
-
_encrypted: this._encrypted,
|
|
2149
|
-
_compressed: this._compressed,
|
|
2150
|
-
_attachments: this._attachments,
|
|
2151
|
-
packedData: this._packedData
|
|
2152
|
-
};
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
// ========================
|
|
2157
|
-
// Metadata Classes
|
|
2158
|
-
// ========================
|
|
2159
|
-
|
|
2160
|
-
class CollectionMetadata {
|
|
2161
|
-
constructor(name, data = {}) {
|
|
2162
|
-
this.name = name;
|
|
2163
|
-
this.sizeKB = data.sizeKB || 0;
|
|
2164
|
-
this.length = data.length || 0;
|
|
2165
|
-
this.createdAt = data.createdAt || Date.now();
|
|
2166
|
-
this.modifiedAt = data.modifiedAt || Date.now();
|
|
2167
|
-
this.documentSizes = data.documentSizes || {};
|
|
2168
|
-
this.documentModifiedAt = data.documentModifiedAt || {};
|
|
2169
|
-
this.documentPermanent = data.documentPermanent || {};
|
|
2170
|
-
this.documentAttachments = data.documentAttachments || {};
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
addDocument(docId, sizeKB, isPermanent, attachmentCount) {
|
|
2174
|
-
this.documentSizes[docId] = sizeKB;
|
|
2175
|
-
this.documentModifiedAt[docId] = Date.now();
|
|
2176
|
-
if (isPermanent) this.documentPermanent[docId] = true;
|
|
2177
|
-
if (attachmentCount > 0) this.documentAttachments[docId] = attachmentCount;
|
|
2178
|
-
|
|
2179
|
-
this.sizeKB += sizeKB;
|
|
2180
|
-
this.length++;
|
|
2181
|
-
this.modifiedAt = Date.now();
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
updateDocument(docId, newSizeKB, isPermanent, attachmentCount) {
|
|
2185
|
-
const oldSize = this.documentSizes[docId] || 0;
|
|
2186
|
-
this.sizeKB = this.sizeKB - oldSize + newSizeKB;
|
|
2187
|
-
this.documentSizes[docId] = newSizeKB;
|
|
2188
|
-
this.documentModifiedAt[docId] = Date.now();
|
|
2189
|
-
|
|
2190
|
-
if (isPermanent) {
|
|
2191
|
-
this.documentPermanent[docId] = true;
|
|
2192
|
-
} else {
|
|
2193
|
-
delete this.documentPermanent[docId];
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
if (attachmentCount > 0) {
|
|
2197
|
-
this.documentAttachments[docId] = attachmentCount;
|
|
2198
|
-
} else {
|
|
2199
|
-
delete this.documentAttachments[docId];
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
this.modifiedAt = Date.now();
|
|
2203
|
-
}
|
|
2204
|
-
|
|
2205
|
-
removeDocument(docId) {
|
|
2206
|
-
const sizeKB = this.documentSizes[docId] || 0;
|
|
2207
|
-
if (this.documentSizes[docId]) {
|
|
2208
|
-
this.sizeKB -= sizeKB;
|
|
2209
|
-
this.length--;
|
|
2210
|
-
}
|
|
2211
|
-
delete this.documentSizes[docId];
|
|
2212
|
-
delete this.documentModifiedAt[docId];
|
|
2213
|
-
delete this.documentPermanent[docId];
|
|
2214
|
-
delete this.documentAttachments[docId];
|
|
2215
|
-
this.modifiedAt = Date.now();
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
getOldestNonPermanentDocuments(count) {
|
|
2219
|
-
return Object.entries(this.documentModifiedAt)
|
|
2220
|
-
.filter(([docId]) => !this.documentPermanent[docId])
|
|
2221
|
-
.sort(([, timeA], [, timeB]) => timeA - timeB)
|
|
2222
|
-
.slice(0, count)
|
|
2223
|
-
.map(([docId]) => docId);
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
class DatabaseMetadata {
|
|
2228
|
-
constructor(name, data = {}) {
|
|
2229
|
-
this.name = name;
|
|
2230
|
-
this.collections = data.collections || {};
|
|
2231
|
-
this.totalSizeKB = data.totalSizeKB || 0;
|
|
2232
|
-
this.totalLength = data.totalLength || 0;
|
|
2233
|
-
this.modifiedAt = data.modifiedAt || Date.now();
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
static load(dbName) {
|
|
2237
|
-
const key = `lacertadb_${dbName}_metadata`;
|
|
2238
|
-
const stored = localStorage.getItem(key);
|
|
2239
|
-
if (stored) {
|
|
2240
|
-
try {
|
|
2241
|
-
const decoded = base64.decode(stored);
|
|
2242
|
-
const data = serializer.deserialize(decoded);
|
|
2243
|
-
return new DatabaseMetadata(dbName, data);
|
|
2244
|
-
} catch (e) {
|
|
2245
|
-
console.error('Failed to load metadata:', e);
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
return new DatabaseMetadata(dbName);
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
save() {
|
|
2252
|
-
const key = `lacertadb_${this.name}_metadata`;
|
|
2253
|
-
try {
|
|
2254
|
-
const dataToStore = {
|
|
2255
|
-
collections: this.collections,
|
|
2256
|
-
totalSizeKB: this.totalSizeKB,
|
|
2257
|
-
totalLength: this.totalLength,
|
|
2258
|
-
modifiedAt: this.modifiedAt
|
|
2259
|
-
};
|
|
2260
|
-
const serializedData = serializer.serialize(dataToStore);
|
|
2261
|
-
const encodedData = base64.encode(serializedData);
|
|
2262
|
-
localStorage.setItem(key, encodedData);
|
|
2263
|
-
} catch (e) {
|
|
2264
|
-
if (e.name === 'QuotaExceededError') {
|
|
2265
|
-
throw new LacertaDBError('Storage quota exceeded for metadata', 'QUOTA_EXCEEDED', e);
|
|
2266
|
-
}
|
|
2267
|
-
throw new LacertaDBError('Failed to save metadata', 'METADATA_SAVE_FAILED', e);
|
|
2268
|
-
}
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
setCollection(collectionMetadata) {
|
|
2272
|
-
this.collections[collectionMetadata.name] = {
|
|
2273
|
-
sizeKB: collectionMetadata.sizeKB,
|
|
2274
|
-
length: collectionMetadata.length,
|
|
2275
|
-
createdAt: collectionMetadata.createdAt,
|
|
2276
|
-
modifiedAt: collectionMetadata.modifiedAt,
|
|
2277
|
-
documentSizes: collectionMetadata.documentSizes,
|
|
2278
|
-
documentModifiedAt: collectionMetadata.documentModifiedAt,
|
|
2279
|
-
documentPermanent: collectionMetadata.documentPermanent,
|
|
2280
|
-
documentAttachments: collectionMetadata.documentAttachments
|
|
2281
|
-
};
|
|
2282
|
-
this._recalculate();
|
|
2283
|
-
this.save();
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
removeCollection(collectionName) {
|
|
2287
|
-
delete this.collections[collectionName];
|
|
2288
|
-
this._recalculate();
|
|
2289
|
-
this.save();
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
_recalculate() {
|
|
2293
|
-
this.totalSizeKB = 0;
|
|
2294
|
-
this.totalLength = 0;
|
|
2295
|
-
for (const collName in this.collections) {
|
|
2296
|
-
const coll = this.collections[collName];
|
|
2297
|
-
this.totalSizeKB += coll.sizeKB;
|
|
2298
|
-
this.totalLength += coll.length;
|
|
2299
|
-
}
|
|
2300
|
-
this.modifiedAt = Date.now();
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
class Settings {
|
|
2305
|
-
constructor(dbName, data = {}) {
|
|
2306
|
-
this.dbName = dbName;
|
|
2307
|
-
this.sizeLimitKB = data.sizeLimitKB != null ? data.sizeLimitKB : Infinity;
|
|
2308
|
-
const defaultBuffer = this.sizeLimitKB === Infinity ? Infinity : this.sizeLimitKB * 0.8;
|
|
2309
|
-
this.bufferLimitKB = data.bufferLimitKB != null ? data.bufferLimitKB : defaultBuffer;
|
|
2310
|
-
this.freeSpaceEvery = this.sizeLimitKB === Infinity ? 0 : (data.freeSpaceEvery || 10000);
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
static load(dbName) {
|
|
2314
|
-
const key = `lacertadb_${dbName}_settings`;
|
|
2315
|
-
const stored = localStorage.getItem(key);
|
|
2316
|
-
if (stored) {
|
|
2317
|
-
try {
|
|
2318
|
-
const decoded = base64.decode(stored);
|
|
2319
|
-
const data = serializer.deserialize(decoded);
|
|
2320
|
-
return new Settings(dbName, data);
|
|
2321
|
-
} catch (e) {
|
|
2322
|
-
console.error('Failed to load settings:', e);
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
return new Settings(dbName);
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
save() {
|
|
2329
|
-
const key = `lacertadb_${this.dbName}_settings`;
|
|
2330
|
-
const dataToStore = {
|
|
2331
|
-
sizeLimitKB: this.sizeLimitKB,
|
|
2332
|
-
bufferLimitKB: this.bufferLimitKB,
|
|
2333
|
-
freeSpaceEvery: this.freeSpaceEvery
|
|
2334
|
-
};
|
|
2335
|
-
const serializedData = serializer.serialize(dataToStore);
|
|
2336
|
-
const encodedData = base64.encode(serializedData);
|
|
2337
|
-
localStorage.setItem(key, encodedData);
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
updateSettings(newSettings) {
|
|
2341
|
-
Object.assign(this, newSettings);
|
|
2342
|
-
if (newSettings.sizeLimitKB !== undefined && newSettings.bufferLimitKB === undefined) {
|
|
2343
|
-
this.bufferLimitKB = this.sizeLimitKB === Infinity ? Infinity : this.sizeLimitKB * 0.8;
|
|
2344
|
-
}
|
|
2345
|
-
if (this.sizeLimitKB === Infinity) {
|
|
2346
|
-
this.freeSpaceEvery = 0;
|
|
2347
|
-
}
|
|
2348
|
-
this.save();
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
// ========================
|
|
2353
|
-
// Query Engine
|
|
2354
|
-
// ========================
|
|
2355
|
-
|
|
2356
|
-
class QueryEngine {
|
|
2357
|
-
constructor() {
|
|
2358
|
-
this.operators = {
|
|
2359
|
-
'$eq': (a, b) => a === b,
|
|
2360
|
-
'$ne': (a, b) => a !== b,
|
|
2361
|
-
'$gt': (a, b) => a > b,
|
|
2362
|
-
'$gte': (a, b) => a >= b,
|
|
2363
|
-
'$lt': (a, b) => a < b,
|
|
2364
|
-
'$lte': (a, b) => a <= b,
|
|
2365
|
-
'$in': (a, b) => Array.isArray(b) && b.includes(a),
|
|
2366
|
-
'$nin': (a, b) => Array.isArray(b) && !b.includes(a),
|
|
2367
|
-
|
|
2368
|
-
'$and': (doc, conditions) => conditions.every(cond => this.evaluate(doc, cond)),
|
|
2369
|
-
'$or': (doc, conditions) => conditions.some(cond => this.evaluate(doc, cond)),
|
|
2370
|
-
'$not': (doc, condition) => !this.evaluate(doc, condition),
|
|
2371
|
-
'$nor': (doc, conditions) => !conditions.some(cond => this.evaluate(doc, cond)),
|
|
2372
|
-
|
|
2373
|
-
'$exists': (value, exists) => (value !== undefined) === exists,
|
|
2374
|
-
'$type': (value, type) => typeof value === type,
|
|
2375
|
-
|
|
2376
|
-
'$all': (arr, values) => Array.isArray(arr) && values.every(v => arr.includes(v)),
|
|
2377
|
-
'$elemMatch': (arr, condition) => Array.isArray(arr) && arr.some(elem => this.evaluate({ value: elem }, { value: condition })),
|
|
2378
|
-
'$size': (arr, size) => Array.isArray(arr) && arr.length === size,
|
|
2379
|
-
|
|
2380
|
-
'$regex': (str, pattern) => {
|
|
2381
|
-
if (typeof str !== 'string') return false;
|
|
2382
|
-
try {
|
|
2383
|
-
const regex = new RegExp(pattern);
|
|
2384
|
-
return regex.test(str);
|
|
2385
|
-
} catch {
|
|
2386
|
-
return false;
|
|
2387
|
-
}
|
|
2388
|
-
},
|
|
2389
|
-
'$text': (str, search) => typeof str === 'string' && str.toLowerCase().includes(search.toLowerCase())
|
|
2390
|
-
};
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
evaluate(doc, query) {
|
|
2394
|
-
for (const key in query) {
|
|
2395
|
-
const value = query[key];
|
|
2396
|
-
if (key.startsWith('$')) {
|
|
2397
|
-
const operator = this.operators[key];
|
|
2398
|
-
if (!operator || !operator(doc, value)) return false;
|
|
2399
|
-
} else {
|
|
2400
|
-
const fieldValue = this.getFieldValue(doc, key);
|
|
2401
|
-
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
2402
|
-
for (const op in value) {
|
|
2403
|
-
if (op.startsWith('$')) {
|
|
2404
|
-
const operatorFn = this.operators[op];
|
|
2405
|
-
if (!operatorFn || !operatorFn(fieldValue, value[op])) {
|
|
2406
|
-
return false;
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
}
|
|
2410
|
-
} else {
|
|
2411
|
-
if (fieldValue !== value) return false;
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
return true;
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
getFieldValue(doc, path) {
|
|
2419
|
-
let current = doc;
|
|
2420
|
-
for (const part of path.split('.')) {
|
|
2421
|
-
if (current === null || current === undefined) {
|
|
2422
|
-
return undefined;
|
|
2423
|
-
}
|
|
2424
|
-
current = current[part];
|
|
2425
|
-
}
|
|
2426
|
-
return current;
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
const queryEngine = new QueryEngine();
|
|
2430
|
-
|
|
2431
|
-
// ========================
|
|
2432
|
-
// Aggregation Pipeline
|
|
2433
|
-
// ========================
|
|
2434
|
-
|
|
2435
|
-
class AggregationPipeline {
|
|
2436
|
-
constructor() {
|
|
2437
|
-
this.stages = {
|
|
2438
|
-
'$match': (docs, condition) => docs.filter(doc => queryEngine.evaluate(doc, condition)),
|
|
2439
|
-
|
|
2440
|
-
'$project': (docs, projection) => docs.map(doc => {
|
|
2441
|
-
const projected = {};
|
|
2442
|
-
for (const key in projection) {
|
|
2443
|
-
const value = projection[key];
|
|
2444
|
-
if (value === 1 || value === true) {
|
|
2445
|
-
projected[key] = queryEngine.getFieldValue(doc, key);
|
|
2446
|
-
} else if (typeof value === 'string' && value.startsWith('$')) {
|
|
2447
|
-
projected[key] = queryEngine.getFieldValue(doc, value.substring(1));
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
if (Object.values(projection).some(v => v === 0 || v === false)) {
|
|
2451
|
-
const exclusions = Object.keys(projection).filter(k => projection[k] === 0 || projection[k] === false);
|
|
2452
|
-
const included = { ...doc };
|
|
2453
|
-
exclusions.forEach(key => delete included[key]);
|
|
2454
|
-
return included;
|
|
2455
|
-
}
|
|
2456
|
-
return projected;
|
|
2457
|
-
}),
|
|
2458
|
-
|
|
2459
|
-
'$sort': (docs, sortSpec) => [...docs].sort((a, b) => {
|
|
2460
|
-
for (const key in sortSpec) {
|
|
2461
|
-
const order = sortSpec[key];
|
|
2462
|
-
const aVal = queryEngine.getFieldValue(a, key);
|
|
2463
|
-
const bVal = queryEngine.getFieldValue(b, key);
|
|
2464
|
-
if (aVal < bVal) return -order;
|
|
2465
|
-
if (aVal > bVal) return order;
|
|
2466
|
-
}
|
|
2467
|
-
return 0;
|
|
2468
|
-
}),
|
|
2469
|
-
|
|
2470
|
-
'$limit': (docs, limit) => docs.slice(0, limit),
|
|
2471
|
-
|
|
2472
|
-
'$skip': (docs, skip) => docs.slice(skip),
|
|
2473
|
-
|
|
2474
|
-
'$group': (docs, groupSpec) => {
|
|
2475
|
-
const groups = new Map();
|
|
2476
|
-
const idField = groupSpec._id;
|
|
2477
|
-
|
|
2478
|
-
for (const doc of docs) {
|
|
2479
|
-
const groupKey = typeof idField === 'string' ?
|
|
2480
|
-
queryEngine.getFieldValue(doc, idField.replace('$', '')) :
|
|
2481
|
-
serializer.serialize(idField);
|
|
2482
|
-
|
|
2483
|
-
if (!groups.has(groupKey)) {
|
|
2484
|
-
groups.set(groupKey, { _id: groupKey, docs: [] });
|
|
2485
|
-
}
|
|
2486
|
-
groups.get(groupKey).docs.push(doc);
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
const results = [];
|
|
2490
|
-
for (const group of groups.values()) {
|
|
2491
|
-
const result = { _id: group._id };
|
|
2492
|
-
for (const fieldKey in groupSpec) {
|
|
2493
|
-
if (fieldKey === '_id') continue;
|
|
2494
|
-
const accumulator = groupSpec[fieldKey];
|
|
2495
|
-
const op = Object.keys(accumulator)[0];
|
|
2496
|
-
const field = accumulator[op].toString().replace('$', '');
|
|
2497
|
-
|
|
2498
|
-
switch(op) {
|
|
2499
|
-
case '$sum':
|
|
2500
|
-
result[fieldKey] = group.docs.reduce((sum, d) => sum + (queryEngine.getFieldValue(d, field) || 0), 0);
|
|
2501
|
-
break;
|
|
2502
|
-
case '$avg':
|
|
2503
|
-
const sum = group.docs.reduce((s, d) => s + (queryEngine.getFieldValue(d, field) || 0), 0);
|
|
2504
|
-
result[fieldKey] = sum / group.docs.length;
|
|
2505
|
-
break;
|
|
2506
|
-
case '$count':
|
|
2507
|
-
result[fieldKey] = group.docs.length;
|
|
2508
|
-
break;
|
|
2509
|
-
case '$max':
|
|
2510
|
-
result[fieldKey] = Math.max(...group.docs.map(d => queryEngine.getFieldValue(d, field)));
|
|
2511
|
-
break;
|
|
2512
|
-
case '$min':
|
|
2513
|
-
result[fieldKey] = Math.min(...group.docs.map(d => queryEngine.getFieldValue(d, field)));
|
|
2514
|
-
break;
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
results.push(result);
|
|
2518
|
-
}
|
|
2519
|
-
return results;
|
|
2520
|
-
},
|
|
2521
|
-
|
|
2522
|
-
'$lookup': async (docs, lookupSpec, db) => {
|
|
2523
|
-
const foreignCollection = await db.getCollection(lookupSpec.from);
|
|
2524
|
-
const foreignDocs = await foreignCollection.getAll();
|
|
2525
|
-
const foreignMap = new Map();
|
|
2526
|
-
foreignDocs.forEach(doc => {
|
|
2527
|
-
const key = queryEngine.getFieldValue(doc, lookupSpec.foreignField);
|
|
2528
|
-
if (!foreignMap.has(key)) foreignMap.set(key, []);
|
|
2529
|
-
foreignMap.get(key).push(doc);
|
|
2530
|
-
});
|
|
2531
|
-
|
|
2532
|
-
return docs.map(doc => {
|
|
2533
|
-
const localValue = queryEngine.getFieldValue(doc, lookupSpec.localField);
|
|
2534
|
-
return {
|
|
2535
|
-
...doc,
|
|
2536
|
-
[lookupSpec.as]: foreignMap.get(localValue) || []
|
|
2537
|
-
};
|
|
2538
|
-
});
|
|
2539
|
-
}
|
|
2540
|
-
};
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
async execute(docs, pipeline, db) {
|
|
2544
|
-
let result = docs;
|
|
2545
|
-
for (const stage of pipeline) {
|
|
2546
|
-
const stageName = Object.keys(stage)[0];
|
|
2547
|
-
const stageSpec = stage[stageName];
|
|
2548
|
-
const stageFunction = this.stages[stageName];
|
|
2549
|
-
|
|
2550
|
-
if (!stageFunction) {
|
|
2551
|
-
throw new Error(`Unknown aggregation stage: ${stageName}`);
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
if (stageName === '$lookup') {
|
|
2555
|
-
result = await stageFunction(result, stageSpec, db);
|
|
2556
|
-
} else {
|
|
2557
|
-
result = stageFunction(result, stageSpec);
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
return result;
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
const aggregationPipeline = new AggregationPipeline();
|
|
2564
|
-
|
|
2565
|
-
// ========================
|
|
2566
|
-
// Migration Manager
|
|
2567
|
-
// ========================
|
|
2568
|
-
|
|
2569
|
-
class MigrationManager {
|
|
2570
|
-
constructor(database) {
|
|
2571
|
-
this.database = database;
|
|
2572
|
-
this.migrations = [];
|
|
2573
|
-
this.currentVersion = this._loadVersion();
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
|
-
_loadVersion() {
|
|
2577
|
-
return localStorage.getItem(`lacertadb_${this.database.name}_version`) || '1.0.0';
|
|
2578
|
-
}
|
|
2579
|
-
|
|
2580
|
-
_saveVersion(version) {
|
|
2581
|
-
localStorage.setItem(`lacertadb_${this.database.name}_version`, version);
|
|
2582
|
-
this.currentVersion = version;
|
|
2583
|
-
}
|
|
2584
|
-
|
|
2585
|
-
addMigration(migration) {
|
|
2586
|
-
this.migrations.push(migration);
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
|
-
_compareVersions(a, b) {
|
|
2590
|
-
const partsA = a.split('.').map(Number);
|
|
2591
|
-
const partsB = b.split('.').map(Number);
|
|
2592
|
-
const len = Math.max(partsA.length, partsB.length);
|
|
2593
|
-
|
|
2594
|
-
for (let i = 0; i < len; i++) {
|
|
2595
|
-
const partA = partsA[i] || 0;
|
|
2596
|
-
const partB = partsB[i] || 0;
|
|
2597
|
-
if (partA > partB) return 1;
|
|
2598
|
-
if (partA < partB) return -1;
|
|
2599
|
-
}
|
|
2600
|
-
return 0;
|
|
2601
|
-
}
|
|
2602
|
-
|
|
2603
|
-
async runMigrations(targetVersion) {
|
|
2604
|
-
const applicableMigrations = this.migrations
|
|
2605
|
-
.filter(m => this._compareVersions(m.version, this.currentVersion) > 0 &&
|
|
2606
|
-
this._compareVersions(m.version, targetVersion) <= 0)
|
|
2607
|
-
.sort((a, b) => this._compareVersions(a.version, b.version));
|
|
2608
|
-
|
|
2609
|
-
for (const migration of applicableMigrations) {
|
|
2610
|
-
await this._applyMigration(migration, 'up');
|
|
2611
|
-
this._saveVersion(migration.version);
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
async rollback(targetVersion) {
|
|
2616
|
-
const applicableMigrations = this.migrations
|
|
2617
|
-
.filter(m => m.down &&
|
|
2618
|
-
this._compareVersions(m.version, targetVersion) > 0 &&
|
|
2619
|
-
this._compareVersions(m.version, this.currentVersion) <= 0)
|
|
2620
|
-
.sort((a, b) => this._compareVersions(b.version, a.version));
|
|
2621
|
-
|
|
2622
|
-
for (const migration of applicableMigrations) {
|
|
2623
|
-
await this._applyMigration(migration, 'down');
|
|
2624
|
-
}
|
|
2625
|
-
this._saveVersion(targetVersion);
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
|
-
async _applyMigration(migration, direction) {
|
|
2629
|
-
console.log(`${direction === 'up' ? 'Running' : 'Rolling back'} migration: ${migration.name} (v${migration.version})`);
|
|
2630
|
-
const collections = await this.database.listCollections();
|
|
2631
|
-
for (const collectionName of collections) {
|
|
2632
|
-
const coll = await this.database.getCollection(collectionName);
|
|
2633
|
-
const docs = await coll.getAll();
|
|
2634
|
-
for (const doc of docs) {
|
|
2635
|
-
const updated = await migration[direction](doc);
|
|
2636
|
-
if (updated) {
|
|
2637
|
-
await coll.update(doc._id, updated);
|
|
2638
|
-
}
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
|
-
// ========================
|
|
2645
|
-
// Performance Monitor
|
|
2646
|
-
// ========================
|
|
2647
|
-
|
|
2648
|
-
class PerformanceMonitor {
|
|
2649
|
-
constructor() {
|
|
2650
|
-
this._metrics = {
|
|
2651
|
-
operations: [],
|
|
2652
|
-
latencies: [],
|
|
2653
|
-
cacheHits: 0,
|
|
2654
|
-
cacheMisses: 0,
|
|
2655
|
-
memoryUsage: []
|
|
2656
|
-
};
|
|
2657
|
-
this._monitoring = false;
|
|
2658
|
-
this._monitoringInterval = null;
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
startMonitoring() {
|
|
2662
|
-
if (this._monitoring) return;
|
|
2663
|
-
this._monitoring = true;
|
|
2664
|
-
this._monitoringInterval = setInterval(() => this._collectMetrics(), 1000);
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
stopMonitoring() {
|
|
2668
|
-
if (!this._monitoring) return;
|
|
2669
|
-
this._monitoring = false;
|
|
2670
|
-
clearInterval(this._monitoringInterval);
|
|
2671
|
-
this._monitoringInterval = null;
|
|
2672
|
-
}
|
|
2673
|
-
|
|
2674
|
-
recordOperation(type, duration) {
|
|
2675
|
-
if (!this._monitoring) return;
|
|
2676
|
-
this._metrics.operations.push({ type, duration, timestamp: Date.now() });
|
|
2677
|
-
this._metrics.latencies.push(duration);
|
|
2678
|
-
if (this._metrics.operations.length > 100) this._metrics.operations.shift();
|
|
2679
|
-
if (this._metrics.latencies.length > 100) this._metrics.latencies.shift();
|
|
2680
|
-
}
|
|
2681
|
-
|
|
2682
|
-
recordCacheHit() { this._metrics.cacheHits++; }
|
|
2683
|
-
recordCacheMiss() { this._metrics.cacheMisses++; }
|
|
2684
|
-
|
|
2685
|
-
_collectMetrics() {
|
|
2686
|
-
if (performance && performance.memory) {
|
|
2687
|
-
this._metrics.memoryUsage.push({
|
|
2688
|
-
used: performance.memory.usedJSHeapSize,
|
|
2689
|
-
total: performance.memory.totalJSHeapSize,
|
|
2690
|
-
limit: performance.memory.jsHeapSizeLimit,
|
|
2691
|
-
timestamp: Date.now()
|
|
2692
|
-
});
|
|
2693
|
-
if (this._metrics.memoryUsage.length > 60) this._metrics.memoryUsage.shift();
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
|
|
2697
|
-
getStats() {
|
|
2698
|
-
const opsPerSec = this._metrics.operations.filter(op => Date.now() - op.timestamp < 1000).length;
|
|
2699
|
-
const totalLatency = this._metrics.latencies.reduce((a, b) => a + b, 0);
|
|
2700
|
-
const avgLatency = this._metrics.latencies.length > 0 ? totalLatency / this._metrics.latencies.length : 0;
|
|
2701
|
-
const totalCacheOps = this._metrics.cacheHits + this._metrics.cacheMisses;
|
|
2702
|
-
const cacheHitRate = totalCacheOps > 0 ? (this._metrics.cacheHits / totalCacheOps) * 100 : 0;
|
|
2703
|
-
|
|
2704
|
-
const latestMemory = this._metrics.memoryUsage.length > 0 ? this._metrics.memoryUsage[this._metrics.memoryUsage.length - 1] : null;
|
|
2705
|
-
const memoryUsageMB = latestMemory ? latestMemory.used / (1024 * 1024) : 0;
|
|
2706
|
-
|
|
2707
|
-
return {
|
|
2708
|
-
opsPerSec,
|
|
2709
|
-
avgLatency: avgLatency.toFixed(2),
|
|
2710
|
-
cacheHitRate: cacheHitRate.toFixed(1),
|
|
2711
|
-
memoryUsageMB: memoryUsageMB.toFixed(2)
|
|
2712
|
-
};
|
|
2713
|
-
}
|
|
2714
|
-
|
|
2715
|
-
getOptimizationTips() {
|
|
2716
|
-
const tips = [];
|
|
2717
|
-
const stats = this.getStats();
|
|
2718
|
-
|
|
2719
|
-
if (stats.avgLatency > 100) {
|
|
2720
|
-
tips.push('High average latency detected. Consider enabling compression and indexing frequently queried fields.');
|
|
2721
|
-
}
|
|
2722
|
-
if (stats.cacheHitRate < 50 && (this._metrics.cacheHits + this._metrics.cacheMisses) > 20) {
|
|
2723
|
-
tips.push('Low cache hit rate. Consider increasing cache size or optimizing query patterns.');
|
|
2724
|
-
}
|
|
2725
|
-
if (this._metrics.memoryUsage.length > 10) {
|
|
2726
|
-
const recent = this._metrics.memoryUsage.slice(-10);
|
|
2727
|
-
const trend = recent[recent.length - 1].used - recent[0].used;
|
|
2728
|
-
if (trend > 10 * 1024 * 1024) {
|
|
2729
|
-
tips.push('Memory usage is increasing rapidly. Check for memory leaks or consider batch processing.');
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
return tips.length > 0 ? tips : ['Performance is optimal. No issues detected.'];
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
|
|
2736
|
-
// ========================
|
|
2737
|
-
// Collection Class (Optimized)
|
|
2738
|
-
// ========================
|
|
2739
|
-
|
|
2740
|
-
class Collection {
|
|
2741
|
-
constructor(name, database) {
|
|
2742
|
-
this.name = name;
|
|
2743
|
-
this.database = database;
|
|
2744
|
-
this._db = null;
|
|
2745
|
-
this._metadata = null;
|
|
2746
|
-
this._settings = database.settings;
|
|
2747
|
-
this._indexedDB = new IndexedDBUtility();
|
|
2748
|
-
this._opfs = new OPFSUtility();
|
|
2749
|
-
this._cleanupInterval = null;
|
|
2750
|
-
this._events = new Map();
|
|
2751
|
-
|
|
2752
|
-
this._indexManager = new IndexManager(this);
|
|
2753
|
-
this._cacheStrategy = new CacheStrategy({
|
|
2754
|
-
type: 'lru',
|
|
2755
|
-
maxSize: 100,
|
|
2756
|
-
ttl: 60000,
|
|
2757
|
-
enabled: true
|
|
2758
|
-
});
|
|
2759
|
-
|
|
2760
|
-
this._performanceMonitor = database.performanceMonitor;
|
|
2761
|
-
this._initialized = false;
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
get settings() {
|
|
2765
|
-
return this._settings;
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
get metadata() {
|
|
2769
|
-
return this._metadata;
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
get initialized() {
|
|
2773
|
-
return this._initialized;
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
async init() {
|
|
2777
|
-
if (this._initialized) return this;
|
|
2778
|
-
|
|
2779
|
-
const dbName = `${this.database.name}_${this.name}`;
|
|
2780
|
-
this._db = await connectionPool.getConnection(dbName, 1, (db, oldVersion) => {
|
|
2781
|
-
if (oldVersion < 1 && !db.objectStoreNames.contains('documents')) {
|
|
2782
|
-
const store = db.createObjectStore('documents', {keyPath: '_id'});
|
|
2783
|
-
store.createIndex('modified', '_modified', {unique: false});
|
|
2784
|
-
}
|
|
2785
|
-
});
|
|
2786
|
-
|
|
2787
|
-
const metadataData = this.database.metadata.collections[this.name];
|
|
2788
|
-
this._metadata = new CollectionMetadata(this.name, metadataData);
|
|
2789
|
-
|
|
2790
|
-
await this._indexManager.loadIndexMetadata();
|
|
2791
|
-
|
|
2792
|
-
if (this._settings.freeSpaceEvery > 0 && this._settings.sizeLimitKB !== Infinity) {
|
|
2793
|
-
this._cleanupInterval = setInterval(() => this._freeSpace(), this._settings.freeSpaceEvery);
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
this._initialized = true;
|
|
2797
|
-
return this;
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
// Index methods
|
|
2801
|
-
async createIndex(fieldPath, options = {}) {
|
|
2802
|
-
return await this._indexManager.createIndex(fieldPath, options);
|
|
2803
|
-
}
|
|
2804
|
-
|
|
2805
|
-
async dropIndex(indexName) {
|
|
2806
|
-
return this._indexManager.dropIndex(indexName);
|
|
2807
|
-
}
|
|
2808
|
-
|
|
2809
|
-
async getIndexes() {
|
|
2810
|
-
return this._indexManager.getIndexStats();
|
|
2811
|
-
}
|
|
2812
|
-
|
|
2813
|
-
async verifyIndexes() {
|
|
2814
|
-
return await this._indexManager.verifyIndexes();
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
configureCacheStrategy(config) {
|
|
2818
|
-
this._cacheStrategy.updateStrategy(config);
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
async add(documentData, options = {}) {
|
|
2822
|
-
if (!this._initialized) await this.init();
|
|
2823
|
-
|
|
2824
|
-
if (options.encrypted && !this.database.isEncrypted) {
|
|
2825
|
-
throw new LacertaDBError(
|
|
2826
|
-
'Document-level encryption requires database-level encryption. Use getSecureDatabase() to create an encrypted database.',
|
|
2827
|
-
'ENCRYPTION_NOT_INITIALIZED'
|
|
2828
|
-
);
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
await this._trigger('beforeAdd', documentData);
|
|
2832
|
-
|
|
2833
|
-
const doc = new Document({data: documentData, _id: options.id}, {
|
|
2834
|
-
compressed: options.compressed !== false,
|
|
2835
|
-
permanent: options.permanent || false
|
|
2836
|
-
});
|
|
2837
|
-
|
|
2838
|
-
const attachments = options.attachments;
|
|
2839
|
-
if (attachments && attachments.length > 0) {
|
|
2840
|
-
const preparedAttachments = await Promise.all(
|
|
2841
|
-
attachments.map(att => (att instanceof File || att instanceof Blob) ?
|
|
2842
|
-
OPFSUtility.prepareAttachment(att, att.name) :
|
|
2843
|
-
Promise.resolve(att))
|
|
2844
|
-
);
|
|
2845
|
-
doc._attachments = await this._opfs.saveAttachments(this.database.name, this.name, doc._id, preparedAttachments);
|
|
2846
|
-
}
|
|
2847
|
-
|
|
2848
|
-
await doc.pack(this.database.encryption);
|
|
2849
|
-
const dbOutput = doc.databaseOutput();
|
|
2850
|
-
await this._indexedDB.add(this._db, 'documents', dbOutput);
|
|
2851
|
-
|
|
2852
|
-
const fullDoc = doc.objectOutput();
|
|
2853
|
-
await this._indexManager.updateIndexForDocument(doc._id, null, fullDoc);
|
|
2854
|
-
|
|
2855
|
-
const sizeKB = dbOutput.packedData.byteLength / 1024;
|
|
2856
|
-
this._metadata.addDocument(doc._id, sizeKB, doc._permanent, doc._attachments.length);
|
|
2857
|
-
this.database.metadata.setCollection(this._metadata);
|
|
2858
|
-
|
|
2859
|
-
await this._checkSpaceLimit();
|
|
2860
|
-
await this._trigger('afterAdd', doc);
|
|
2861
|
-
this._cacheStrategy.clear();
|
|
2862
|
-
return doc._id;
|
|
2863
|
-
}
|
|
2864
|
-
|
|
2865
|
-
async get(docId, options = {}) {
|
|
2866
|
-
if (!this._initialized) await this.init();
|
|
2867
|
-
|
|
2868
|
-
await this._trigger('beforeGet', docId);
|
|
2869
|
-
|
|
2870
|
-
const stored = await this._indexedDB.get(this._db, 'documents', docId);
|
|
2871
|
-
if (!stored) {
|
|
2872
|
-
throw new LacertaDBError(`Document with id '${docId}' not found.`, 'DOCUMENT_NOT_FOUND');
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
const doc = new Document(stored, {
|
|
2876
|
-
encrypted: stored._encrypted,
|
|
2877
|
-
compressed: stored._compressed
|
|
2878
|
-
});
|
|
2879
|
-
|
|
2880
|
-
if (stored.packedData) {
|
|
2881
|
-
await doc.unpack(this.database.encryption);
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
if (options.includeAttachments && doc._attachments.length > 0) {
|
|
2885
|
-
doc.data._attachments = await this._opfs.getAttachments(doc._attachments);
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
await this._trigger('afterGet', doc);
|
|
2889
|
-
return doc.objectOutput(options.includeAttachments);
|
|
2890
|
-
}
|
|
2891
|
-
|
|
2892
|
-
async getAll(options = {}) {
|
|
2893
|
-
if (!this._initialized) await this.init();
|
|
2894
|
-
|
|
2895
|
-
const stored = await this._indexedDB.getAll(this._db, 'documents', undefined, options.limit);
|
|
2896
|
-
return Promise.all(stored.map(async docData => {
|
|
2897
|
-
try {
|
|
2898
|
-
const doc = new Document(docData, {
|
|
2899
|
-
encrypted: docData._encrypted,
|
|
2900
|
-
compressed: docData._compressed
|
|
2901
|
-
});
|
|
2902
|
-
if (docData.packedData) {
|
|
2903
|
-
await doc.unpack(this.database.encryption);
|
|
2904
|
-
}
|
|
2905
|
-
return doc.objectOutput();
|
|
2906
|
-
} catch (error) {
|
|
2907
|
-
console.error(`Failed to unpack document ${docData._id}:`, error);
|
|
2908
|
-
return null;
|
|
2909
|
-
}
|
|
2910
|
-
})).then(docs => docs.filter(Boolean));
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2913
|
-
async update(docId, updates, options = {}) {
|
|
2914
|
-
if (!this._initialized) await this.init();
|
|
2915
|
-
|
|
2916
|
-
await this._trigger('beforeUpdate', {docId, updates});
|
|
2917
|
-
|
|
2918
|
-
const stored = await this._indexedDB.get(this._db, 'documents', docId);
|
|
2919
|
-
if (!stored) {
|
|
2920
|
-
throw new LacertaDBError(`Document with id '${docId}' not found for update.`, 'DOCUMENT_NOT_FOUND');
|
|
2921
|
-
}
|
|
2922
|
-
|
|
2923
|
-
const existingDoc = new Document(stored);
|
|
2924
|
-
if (stored.packedData) await existingDoc.unpack(this.database.encryption);
|
|
2925
|
-
|
|
2926
|
-
const oldDocOutput = existingDoc.objectOutput();
|
|
2927
|
-
const updatedData = {...existingDoc.data, ...updates};
|
|
2928
|
-
|
|
2929
|
-
const doc = new Document({
|
|
2930
|
-
_id: docId,
|
|
2931
|
-
_created: stored._created,
|
|
2932
|
-
data: updatedData
|
|
2933
|
-
}, {
|
|
2934
|
-
compressed: options.compressed !== undefined ? options.compressed : stored._compressed,
|
|
2935
|
-
permanent: options.permanent !== undefined ? options.permanent : stored._permanent
|
|
2936
|
-
});
|
|
2937
|
-
doc._modified = Date.now();
|
|
2938
|
-
|
|
2939
|
-
const attachments = options.attachments;
|
|
2940
|
-
if (attachments && attachments.length > 0) {
|
|
2941
|
-
await this._opfs.deleteAttachments(this.database.name, this.name, docId);
|
|
2942
|
-
const preparedAttachments = await Promise.all(
|
|
2943
|
-
attachments.map(att => (att instanceof File || att instanceof Blob) ?
|
|
2944
|
-
OPFSUtility.prepareAttachment(att, att.name) :
|
|
2945
|
-
Promise.resolve(att))
|
|
2946
|
-
);
|
|
2947
|
-
doc._attachments = await this._opfs.saveAttachments(this.database.name, this.name, doc._id, preparedAttachments);
|
|
2948
|
-
} else {
|
|
2949
|
-
doc._attachments = stored._attachments;
|
|
2950
|
-
}
|
|
2951
|
-
|
|
2952
|
-
await doc.pack(this.database.encryption);
|
|
2953
|
-
const dbOutput = doc.databaseOutput();
|
|
2954
|
-
await this._indexedDB.put(this._db, 'documents', dbOutput);
|
|
2955
|
-
|
|
2956
|
-
const newDocOutput = doc.objectOutput();
|
|
2957
|
-
await this._indexManager.updateIndexForDocument(doc._id, oldDocOutput, newDocOutput);
|
|
2958
|
-
|
|
2959
|
-
const sizeKB = dbOutput.packedData.byteLength / 1024;
|
|
2960
|
-
this._metadata.updateDocument(doc._id, sizeKB, doc._permanent, doc._attachments.length);
|
|
2961
|
-
this.database.metadata.setCollection(this._metadata);
|
|
2962
|
-
|
|
2963
|
-
await this._trigger('afterUpdate', doc);
|
|
2964
|
-
this._cacheStrategy.clear();
|
|
2965
|
-
return doc._id;
|
|
2966
|
-
}
|
|
2967
|
-
|
|
2968
|
-
async delete(docId, options = {}) {
|
|
2969
|
-
if (!this._initialized) await this.init();
|
|
2970
|
-
|
|
2971
|
-
await this._trigger('beforeDelete', docId);
|
|
2972
|
-
|
|
2973
|
-
const doc = await this._indexedDB.get(this._db, 'documents', docId);
|
|
2974
|
-
if (!doc) {
|
|
2975
|
-
throw new LacertaDBError('Document not found for deletion', 'DOCUMENT_NOT_FOUND');
|
|
2976
|
-
}
|
|
2977
|
-
|
|
2978
|
-
if (doc._permanent && !options.force) {
|
|
2979
|
-
throw new LacertaDBError(
|
|
2980
|
-
'Cannot delete a permanent document. Use options.force = true to force deletion.',
|
|
2981
|
-
'PERMANENT_DOCUMENT_PROTECTION'
|
|
2982
|
-
);
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
if (doc._permanent && options.force) {
|
|
2986
|
-
console.warn(`Force deleting permanent document: ${docId}`);
|
|
2987
|
-
}
|
|
2988
|
-
|
|
2989
|
-
const fullDoc = await this.get(docId);
|
|
2990
|
-
|
|
2991
|
-
await this._indexManager.updateIndexForDocument(docId, fullDoc, null);
|
|
2992
|
-
|
|
2993
|
-
await this._indexedDB.delete(this._db, 'documents', docId);
|
|
2994
|
-
const attachments = doc._attachments;
|
|
2995
|
-
if (attachments && attachments.length > 0) {
|
|
2996
|
-
await this._opfs.deleteAttachments(this.database.name, this.name, docId);
|
|
2997
|
-
}
|
|
2998
|
-
|
|
2999
|
-
this._metadata.removeDocument(docId);
|
|
3000
|
-
this.database.metadata.setCollection(this._metadata);
|
|
3001
|
-
|
|
3002
|
-
await this._trigger('afterDelete', docId);
|
|
3003
|
-
this._cacheStrategy.clear();
|
|
3004
|
-
}
|
|
3005
|
-
|
|
3006
|
-
async query(filter = {}, options = {}) {
|
|
3007
|
-
if (!this._initialized) await this.init();
|
|
3008
|
-
|
|
3009
|
-
const startTime = performance.now();
|
|
3010
|
-
|
|
3011
|
-
const cacheKey = base64.encode(serializer.serialize({filter, options}));
|
|
3012
|
-
const cached = this._cacheStrategy.get(cacheKey);
|
|
3013
|
-
|
|
3014
|
-
if (cached) {
|
|
3015
|
-
if (this._performanceMonitor) this._performanceMonitor.recordCacheHit();
|
|
3016
|
-
return cached;
|
|
3017
|
-
}
|
|
3018
|
-
if (this._performanceMonitor) this._performanceMonitor.recordCacheMiss();
|
|
3019
|
-
|
|
3020
|
-
let results;
|
|
3021
|
-
let usedIndex = false;
|
|
3022
|
-
|
|
3023
|
-
for (const [indexName, index] of this._indexManager.indexes) {
|
|
3024
|
-
const fieldValue = filter[index.fieldPath];
|
|
3025
|
-
if (fieldValue !== undefined) {
|
|
3026
|
-
const docIds = await this._indexManager.query(indexName, fieldValue);
|
|
3027
|
-
results = await Promise.all(
|
|
3028
|
-
docIds.map(id => this.get(id).catch(() => null))
|
|
3029
|
-
);
|
|
3030
|
-
results = results.filter(Boolean);
|
|
3031
|
-
usedIndex = true;
|
|
3032
|
-
break;
|
|
3033
|
-
}
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
if (!usedIndex) {
|
|
3037
|
-
results = await this.getAll(options);
|
|
3038
|
-
if (Object.keys(filter).length > 0) {
|
|
3039
|
-
results = results.filter(doc => queryEngine.evaluate(doc, filter));
|
|
3040
|
-
}
|
|
3041
|
-
}
|
|
3042
|
-
|
|
3043
|
-
if (options.sort) results = aggregationPipeline.stages.$sort(results, options.sort);
|
|
3044
|
-
if (options.skip) results = aggregationPipeline.stages.$skip(results, options.skip);
|
|
3045
|
-
if (options.limit) results = aggregationPipeline.stages.$limit(results, options.limit);
|
|
3046
|
-
if (options.projection) results = aggregationPipeline.stages.$project(results, options.projection);
|
|
3047
|
-
|
|
3048
|
-
if (this._performanceMonitor) {
|
|
3049
|
-
this._performanceMonitor.recordOperation(
|
|
3050
|
-
usedIndex ? 'indexed-query' : 'full-scan-query',
|
|
3051
|
-
performance.now() - startTime
|
|
3052
|
-
);
|
|
3053
|
-
}
|
|
3054
|
-
|
|
3055
|
-
this._cacheStrategy.set(cacheKey, results);
|
|
3056
|
-
|
|
3057
|
-
return results;
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
async aggregate(pipeline) {
|
|
3061
|
-
if (!this._initialized) await this.init();
|
|
3062
|
-
|
|
3063
|
-
const startTime = performance.now();
|
|
3064
|
-
const docs = await this.getAll();
|
|
3065
|
-
const result = await aggregationPipeline.execute(docs, pipeline, this.database);
|
|
3066
|
-
if (this._performanceMonitor) this._performanceMonitor.recordOperation('aggregate', performance.now() - startTime);
|
|
3067
|
-
return result;
|
|
3068
|
-
}
|
|
3069
|
-
|
|
3070
|
-
async batchAdd(documents, options) {
|
|
3071
|
-
if (!this._initialized) await this.init();
|
|
3072
|
-
|
|
3073
|
-
const startTime = performance.now();
|
|
3074
|
-
const operations = [];
|
|
3075
|
-
const results = [];
|
|
3076
|
-
|
|
3077
|
-
for (const documentData of documents) {
|
|
3078
|
-
const doc = new Document({data: documentData}, {
|
|
3079
|
-
compressed: options.compressed !== false,
|
|
3080
|
-
permanent: options.permanent || false
|
|
3081
|
-
});
|
|
3082
|
-
|
|
3083
|
-
await doc.pack(this.database.encryption);
|
|
3084
|
-
operations.push({
|
|
3085
|
-
type: 'add',
|
|
3086
|
-
data: doc.databaseOutput()
|
|
3087
|
-
});
|
|
3088
|
-
results.push(doc);
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
|
-
const dbResults = await this._indexedDB.batchOperation(this._db, operations);
|
|
3092
|
-
|
|
3093
|
-
for (let i = 0; i < results.length; i++) {
|
|
3094
|
-
if (dbResults[i].success) {
|
|
3095
|
-
const doc = results[i];
|
|
3096
|
-
const fullDoc = doc.objectOutput();
|
|
3097
|
-
await this._indexManager.updateIndexForDocument(doc._id, null, fullDoc);
|
|
3098
|
-
|
|
3099
|
-
const sizeKB = doc._packedData.byteLength / 1024;
|
|
3100
|
-
this._metadata.addDocument(doc._id, sizeKB, doc._permanent, 0);
|
|
3101
|
-
}
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
|
-
this.database.metadata.setCollection(this._metadata);
|
|
3105
|
-
if (this._performanceMonitor) {
|
|
3106
|
-
this._performanceMonitor.recordOperation('batchAdd', performance.now() - startTime);
|
|
3107
|
-
}
|
|
3108
|
-
|
|
3109
|
-
return dbResults.map((r, i) => ({
|
|
3110
|
-
...r,
|
|
3111
|
-
id: results[i]._id
|
|
3112
|
-
}));
|
|
3113
|
-
}
|
|
3114
|
-
|
|
3115
|
-
async batchUpdate(updates, options) {
|
|
3116
|
-
if (!this._initialized) await this.init();
|
|
3117
|
-
|
|
3118
|
-
return Promise.all(updates.map(update =>
|
|
3119
|
-
this.update(update.id, update.data, options)
|
|
3120
|
-
.then(id => ({success: true, id}))
|
|
3121
|
-
.catch(error => ({success: false, id: update.id, error: error.message}))
|
|
3122
|
-
));
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
async batchDelete(items) {
|
|
3126
|
-
if (!this._initialized) await this.init();
|
|
3127
|
-
|
|
3128
|
-
const normalizedItems = items.map(item => {
|
|
3129
|
-
if (typeof item === 'string') {
|
|
3130
|
-
return {id: item, options: {}};
|
|
3131
|
-
}
|
|
3132
|
-
return {id: item.id, options: item.options || {}};
|
|
3133
|
-
});
|
|
3134
|
-
|
|
3135
|
-
return Promise.all(normalizedItems.map(({id, options}) =>
|
|
3136
|
-
this.delete(id, options)
|
|
3137
|
-
.then(() => ({success: true, id}))
|
|
3138
|
-
.catch(error => ({success: false, id, error: error.message}))
|
|
3139
|
-
));
|
|
3140
|
-
}
|
|
3141
|
-
|
|
3142
|
-
async _checkSpaceLimit() {
|
|
3143
|
-
if (this._settings.sizeLimitKB !== Infinity && this._metadata.sizeKB > this._settings.bufferLimitKB) {
|
|
3144
|
-
await this._freeSpace();
|
|
3145
|
-
}
|
|
3146
|
-
}
|
|
3147
|
-
|
|
3148
|
-
async _freeSpace() {
|
|
3149
|
-
const targetSize = this._settings.bufferLimitKB * 0.8;
|
|
3150
|
-
while (this._metadata.sizeKB > targetSize) {
|
|
3151
|
-
const oldestDocs = this._metadata.getOldestNonPermanentDocuments(10);
|
|
3152
|
-
if (oldestDocs.length === 0) break;
|
|
3153
|
-
await this.batchDelete(oldestDocs);
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
|
-
on(event, callback) {
|
|
3158
|
-
if (!this._events.has(event)) this._events.set(event, []);
|
|
3159
|
-
this._events.get(event).push(callback);
|
|
3160
|
-
}
|
|
3161
|
-
|
|
3162
|
-
off(event, callback) {
|
|
3163
|
-
if (!this._events.has(event)) return;
|
|
3164
|
-
const listeners = this._events.get(event).filter(cb => cb !== callback);
|
|
3165
|
-
this._events.set(event, listeners);
|
|
3166
|
-
}
|
|
3167
|
-
|
|
3168
|
-
async _trigger(event, data) {
|
|
3169
|
-
if (!this._events.has(event)) return;
|
|
3170
|
-
for (const callback of this._events.get(event)) {
|
|
3171
|
-
await callback(data);
|
|
3172
|
-
}
|
|
3173
|
-
}
|
|
3174
|
-
|
|
3175
|
-
clearCache() {
|
|
3176
|
-
this._cacheStrategy.clear();
|
|
3177
|
-
}
|
|
3178
|
-
|
|
3179
|
-
async clear(options = {}) {
|
|
3180
|
-
if (!this._initialized) await this.init();
|
|
3181
|
-
|
|
3182
|
-
if (options.force) {
|
|
3183
|
-
// Clear documents first
|
|
3184
|
-
await this._indexedDB.clear(this._db, 'documents');
|
|
3185
|
-
|
|
3186
|
-
// Reset metadata
|
|
3187
|
-
this._metadata = new CollectionMetadata(this.name);
|
|
3188
|
-
this.database.metadata.setCollection(this._metadata);
|
|
3189
|
-
|
|
3190
|
-
// Clear cache
|
|
3191
|
-
this._cacheStrategy.clear();
|
|
3192
|
-
|
|
3193
|
-
// Rebuild indexes after clearing
|
|
3194
|
-
for (const indexName of this._indexManager.indexes.keys()) {
|
|
3195
|
-
await this._indexManager.rebuildIndex(indexName);
|
|
3196
|
-
}
|
|
3197
|
-
} else {
|
|
3198
|
-
const allDocs = await this.getAll();
|
|
3199
|
-
const nonPermanentDocs = allDocs.filter(doc => !doc._permanent);
|
|
3200
|
-
await this.batchDelete(nonPermanentDocs.map(doc => doc._id));
|
|
3201
|
-
}
|
|
3202
|
-
|
|
3203
|
-
// Reset cleanup interval if needed
|
|
3204
|
-
if (this._cleanupInterval) {
|
|
3205
|
-
clearInterval(this._cleanupInterval);
|
|
3206
|
-
this._cleanupInterval = null;
|
|
3207
|
-
|
|
3208
|
-
if (this._settings.freeSpaceEvery > 0 && this._settings.sizeLimitKB !== Infinity) {
|
|
3209
|
-
this._cleanupInterval = setInterval(() => this._freeSpace(), this._settings.freeSpaceEvery);
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
|
-
}
|
|
3213
|
-
|
|
3214
|
-
destroy() {
|
|
3215
|
-
// Clear the cleanup interval
|
|
3216
|
-
if (this._cleanupInterval) {
|
|
3217
|
-
clearInterval(this._cleanupInterval);
|
|
3218
|
-
this._cleanupInterval = null;
|
|
3219
|
-
}
|
|
3220
|
-
|
|
3221
|
-
// Destroy cache strategy
|
|
3222
|
-
if (this._cacheStrategy) {
|
|
3223
|
-
this._cacheStrategy.destroy();
|
|
3224
|
-
}
|
|
3225
|
-
|
|
3226
|
-
// Release the connection
|
|
3227
|
-
if (this._db) {
|
|
3228
|
-
const dbName = `${this.database.name}_${this.name}`;
|
|
3229
|
-
connectionPool.releaseConnection(dbName);
|
|
3230
|
-
this._db = null;
|
|
3231
|
-
}
|
|
3232
|
-
|
|
3233
|
-
// Clear event listeners
|
|
3234
|
-
this._events.clear();
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
// ========================
|
|
3238
|
-
// Database Class (Optimized with QuickStore)
|
|
3239
|
-
// ========================
|
|
3240
|
-
|
|
3241
|
-
class Database {
|
|
3242
|
-
constructor(name, performanceMonitor) {
|
|
3243
|
-
this.name = name;
|
|
3244
|
-
this._collections = new Map();
|
|
3245
|
-
this._metadata = null;
|
|
3246
|
-
this._settings = null;
|
|
3247
|
-
this._quickStore = null;
|
|
3248
|
-
this._performanceMonitor = performanceMonitor;
|
|
3249
|
-
|
|
3250
|
-
// Database-level encryption
|
|
3251
|
-
this._encryption = null;
|
|
3252
|
-
this._isEncrypted = false;
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
get collections() {
|
|
3256
|
-
return this._collections;
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
|
-
get metadata() {
|
|
3260
|
-
return this._metadata;
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
|
-
get settings() {
|
|
3264
|
-
return this._settings;
|
|
3265
|
-
}
|
|
3266
|
-
|
|
3267
|
-
get quickStore() {
|
|
3268
|
-
return this._quickStore;
|
|
3269
|
-
}
|
|
3270
|
-
|
|
3271
|
-
get performanceMonitor() {
|
|
3272
|
-
return this._performanceMonitor;
|
|
3273
|
-
}
|
|
3274
|
-
|
|
3275
|
-
get encryption() {
|
|
3276
|
-
return this._encryption;
|
|
3277
|
-
}
|
|
3278
|
-
|
|
3279
|
-
get isEncrypted() {
|
|
3280
|
-
return this._isEncrypted;
|
|
3281
|
-
}
|
|
3282
|
-
|
|
3283
|
-
async init(options = {}) {
|
|
3284
|
-
this._metadata = DatabaseMetadata.load(this.name);
|
|
3285
|
-
this._settings = Settings.load(this.name);
|
|
3286
|
-
this._quickStore = new QuickStore(this.name);
|
|
3287
|
-
|
|
3288
|
-
if (options.pin) {
|
|
3289
|
-
await this._initializeEncryption(options.pin, options.salt, options.encryptionConfig);
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
|
-
return this;
|
|
3293
|
-
}
|
|
3294
|
-
|
|
3295
|
-
async _initializeEncryption(pin, salt = null, config = {}) {
|
|
3296
|
-
this._encryption = new SecureDatabaseEncryption(config);
|
|
3297
|
-
|
|
3298
|
-
const encMetaKey = `lacertadb_${this.name}_encryption`;
|
|
3299
|
-
let existingMetadata = null;
|
|
3300
|
-
|
|
3301
|
-
if (!salt) {
|
|
3302
|
-
const stored = localStorage.getItem(encMetaKey);
|
|
3303
|
-
if (stored) {
|
|
3304
|
-
const decoded = base64.decode(stored);
|
|
3305
|
-
existingMetadata = serializer.deserialize(decoded);
|
|
3306
|
-
if (existingMetadata.salt) {
|
|
3307
|
-
this._encryption.importMetadata(existingMetadata);
|
|
3308
|
-
salt = base64.decode(existingMetadata.salt);
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
}
|
|
3312
|
-
|
|
3313
|
-
const saltBase64 = await this._encryption.initialize(pin, salt);
|
|
3314
|
-
|
|
3315
|
-
if (!existingMetadata) {
|
|
3316
|
-
const metadata = this._encryption.exportMetadata();
|
|
3317
|
-
const serialized = serializer.serialize(metadata);
|
|
3318
|
-
const encoded = base64.encode(serialized);
|
|
3319
|
-
localStorage.setItem(encMetaKey, encoded);
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
this._isEncrypted = true;
|
|
3323
|
-
return saltBase64;
|
|
3324
|
-
}
|
|
3325
|
-
|
|
3326
|
-
async changePin(oldPin, newPin) {
|
|
3327
|
-
if (!this._isEncrypted) {
|
|
3328
|
-
throw new Error('Database is not encrypted');
|
|
3329
|
-
}
|
|
3330
|
-
|
|
3331
|
-
const newSalt = await this._encryption.changePin(oldPin, newPin);
|
|
3332
|
-
|
|
3333
|
-
const encMetaKey = `lacertadb_${this.name}_encryption`;
|
|
3334
|
-
const metadata = this._encryption.exportMetadata();
|
|
3335
|
-
const serialized = serializer.serialize(metadata);
|
|
3336
|
-
const encoded = base64.encode(serialized);
|
|
3337
|
-
localStorage.setItem(encMetaKey, encoded);
|
|
3338
|
-
|
|
3339
|
-
return newSalt;
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
|
-
async storePrivateKey(keyName, privateKey, additionalAuth = '') {
|
|
3343
|
-
if (!this._isEncrypted) {
|
|
3344
|
-
throw new Error('Database must be encrypted to store private keys');
|
|
3345
|
-
}
|
|
3346
|
-
|
|
3347
|
-
const encryptedKey = await this._encryption.encryptPrivateKey(
|
|
3348
|
-
privateKey,
|
|
3349
|
-
additionalAuth
|
|
3350
|
-
);
|
|
3351
|
-
|
|
3352
|
-
let keyStore = await this.getCollection('__private_keys__').catch(() => null);
|
|
3353
|
-
if (!keyStore) {
|
|
3354
|
-
keyStore = await this.createCollection('__private_keys__');
|
|
3355
|
-
}
|
|
3356
|
-
|
|
3357
|
-
await keyStore.add({
|
|
3358
|
-
name: keyName,
|
|
3359
|
-
key: encryptedKey,
|
|
3360
|
-
createdAt: Date.now()
|
|
3361
|
-
}, {
|
|
3362
|
-
id: keyName,
|
|
3363
|
-
permanent: true
|
|
3364
|
-
});
|
|
3365
|
-
|
|
3366
|
-
return true;
|
|
3367
|
-
}
|
|
3368
|
-
|
|
3369
|
-
async getPrivateKey(keyName, additionalAuth = '') {
|
|
3370
|
-
if (!this._isEncrypted) {
|
|
3371
|
-
throw new Error('Database must be encrypted to retrieve private keys');
|
|
3372
|
-
}
|
|
3373
|
-
|
|
3374
|
-
const keyStore = await this.getCollection('__private_keys__');
|
|
3375
|
-
const doc = await keyStore.get(keyName);
|
|
3376
|
-
|
|
3377
|
-
if (!doc) {
|
|
3378
|
-
throw new Error(`Private key '${keyName}' not found`);
|
|
3379
|
-
}
|
|
3380
|
-
|
|
3381
|
-
return await this._encryption.decryptPrivateKey(doc.key, additionalAuth);
|
|
3382
|
-
}
|
|
3383
|
-
|
|
3384
|
-
async createCollection(name, options) {
|
|
3385
|
-
if (this._collections.has(name)) {
|
|
3386
|
-
throw new LacertaDBError(`Collection '${name}' already exists.`, 'COLLECTION_EXISTS');
|
|
3387
|
-
}
|
|
3388
|
-
|
|
3389
|
-
const collection = new Collection(name, this);
|
|
3390
|
-
// Lazy initialization - don't init here
|
|
3391
|
-
this._collections.set(name, collection);
|
|
3392
|
-
|
|
3393
|
-
if (!this._metadata.collections[name]) {
|
|
3394
|
-
this._metadata.setCollection(new CollectionMetadata(name));
|
|
3395
|
-
}
|
|
3396
|
-
return collection;
|
|
3397
|
-
}
|
|
3398
|
-
|
|
3399
|
-
async getCollection(name) {
|
|
3400
|
-
if (this._collections.has(name)) {
|
|
3401
|
-
const collection = this._collections.get(name);
|
|
3402
|
-
if (!collection.initialized) {
|
|
3403
|
-
await collection.init();
|
|
3404
|
-
}
|
|
3405
|
-
return collection;
|
|
3406
|
-
}
|
|
3407
|
-
if (this._metadata.collections[name]) {
|
|
3408
|
-
const collection = new Collection(name, this);
|
|
3409
|
-
this._collections.set(name, collection);
|
|
3410
|
-
await collection.init();
|
|
3411
|
-
return collection;
|
|
3412
|
-
}
|
|
3413
|
-
throw new LacertaDBError(`Collection '${name}' not found.`, 'COLLECTION_NOT_FOUND');
|
|
3414
|
-
}
|
|
3415
|
-
|
|
3416
|
-
async dropCollection(name) {
|
|
3417
|
-
if (this._collections.has(name)) {
|
|
3418
|
-
const collection = this._collections.get(name);
|
|
3419
|
-
if (collection.initialized) {
|
|
3420
|
-
await collection.clear({ force: true });
|
|
3421
|
-
collection.destroy();
|
|
3422
|
-
}
|
|
3423
|
-
this._collections.delete(name);
|
|
3424
|
-
}
|
|
3425
|
-
|
|
3426
|
-
this._metadata.removeCollection(name);
|
|
3427
|
-
|
|
3428
|
-
const dbName = `${this.name}_${name}`;
|
|
3429
|
-
await new Promise((resolve, reject) => {
|
|
3430
|
-
const deleteReq = indexedDB.deleteDatabase(dbName);
|
|
3431
|
-
deleteReq.onsuccess = resolve;
|
|
3432
|
-
deleteReq.onerror = reject;
|
|
3433
|
-
deleteReq.onblocked = () => console.warn(`Deletion of '${dbName}' is blocked.`);
|
|
3434
|
-
});
|
|
3435
|
-
}
|
|
3436
|
-
|
|
3437
|
-
listCollections() {
|
|
3438
|
-
return Object.keys(this._metadata.collections);
|
|
3439
|
-
}
|
|
3440
|
-
|
|
3441
|
-
getStats() {
|
|
3442
|
-
return {
|
|
3443
|
-
name: this.name,
|
|
3444
|
-
totalSizeKB: this._metadata.totalSizeKB,
|
|
3445
|
-
totalDocuments: this._metadata.totalLength,
|
|
3446
|
-
collections: Object.entries(this._metadata.collections).map(([name, data]) => ({
|
|
3447
|
-
name,
|
|
3448
|
-
sizeKB: data.sizeKB,
|
|
3449
|
-
documents: data.length,
|
|
3450
|
-
createdAt: new Date(data.createdAt).toISOString(),
|
|
3451
|
-
modifiedAt: new Date(data.modifiedAt).toISOString()
|
|
3452
|
-
}))
|
|
3453
|
-
};
|
|
3454
|
-
}
|
|
3455
|
-
|
|
3456
|
-
updateSettings(newSettings) {
|
|
3457
|
-
this._settings.updateSettings(newSettings);
|
|
3458
|
-
}
|
|
3459
|
-
|
|
3460
|
-
async export(format = 'json', password = null) {
|
|
3461
|
-
const data = {
|
|
3462
|
-
version: '0.7.0',
|
|
3463
|
-
database: this.name,
|
|
3464
|
-
timestamp: Date.now(),
|
|
3465
|
-
collections: {}
|
|
3466
|
-
};
|
|
3467
|
-
|
|
3468
|
-
for (const collName of this.listCollections()) {
|
|
3469
|
-
const collection = await this.getCollection(collName);
|
|
3470
|
-
data.collections[collName] = await collection.getAll();
|
|
3471
|
-
}
|
|
3472
|
-
|
|
3473
|
-
if (format === 'json') {
|
|
3474
|
-
const serialized = serializer.serialize(data);
|
|
3475
|
-
return base64.encode(serialized);
|
|
3476
|
-
}
|
|
3477
|
-
if (format === 'encrypted' && password) {
|
|
3478
|
-
const encryption = new BrowserEncryptionUtility();
|
|
3479
|
-
const serializedData = serializer.serialize(data);
|
|
3480
|
-
const encrypted = await encryption.encrypt(serializedData, password);
|
|
3481
|
-
return base64.encode(encrypted);
|
|
3482
|
-
}
|
|
3483
|
-
throw new LacertaDBError(`Unsupported export format: ${format}`, 'INVALID_FORMAT');
|
|
3484
|
-
}
|
|
3485
|
-
|
|
3486
|
-
async import(data, format = 'json', password = null) {
|
|
3487
|
-
let parsed;
|
|
3488
|
-
try {
|
|
3489
|
-
const decoded = base64.decode(data);
|
|
3490
|
-
if (format === 'encrypted' && password) {
|
|
3491
|
-
const encryption = new BrowserEncryptionUtility();
|
|
3492
|
-
const decrypted = await encryption.decrypt(decoded, password);
|
|
3493
|
-
parsed = serializer.deserialize(decrypted);
|
|
3494
|
-
} else {
|
|
3495
|
-
parsed = serializer.deserialize(decoded);
|
|
3496
|
-
}
|
|
3497
|
-
} catch (e) {
|
|
3498
|
-
throw new LacertaDBError('Failed to parse import data', 'IMPORT_PARSE_FAILED', e);
|
|
3499
|
-
}
|
|
3500
|
-
|
|
3501
|
-
for (const collName in parsed.collections) {
|
|
3502
|
-
const docs = parsed.collections[collName];
|
|
3503
|
-
let collection;
|
|
3504
|
-
try {
|
|
3505
|
-
collection = await this.createCollection(collName);
|
|
3506
|
-
} catch (e) {
|
|
3507
|
-
if (e.code === 'COLLECTION_EXISTS') {
|
|
3508
|
-
collection = await this.getCollection(collName);
|
|
3509
|
-
} else {
|
|
3510
|
-
throw e;
|
|
3511
|
-
}
|
|
3512
|
-
}
|
|
3513
|
-
await collection.batchAdd(docs);
|
|
3514
|
-
}
|
|
3515
|
-
|
|
3516
|
-
const docCount = Object.values(parsed.collections).reduce((sum, docs) => sum + docs.length, 0);
|
|
3517
|
-
return {
|
|
3518
|
-
collections: Object.keys(parsed.collections).length,
|
|
3519
|
-
documents: docCount
|
|
3520
|
-
};
|
|
3521
|
-
}
|
|
3522
|
-
|
|
3523
|
-
async clearAll() {
|
|
3524
|
-
await Promise.all([...this._collections.keys()].map(name => this.dropCollection(name)));
|
|
3525
|
-
this._collections.clear();
|
|
3526
|
-
this._metadata = new DatabaseMetadata(this.name);
|
|
3527
|
-
this._metadata.save();
|
|
3528
|
-
this._quickStore.clear();
|
|
3529
|
-
}
|
|
3530
|
-
|
|
3531
|
-
async destroy() {
|
|
3532
|
-
// Destroy all collections first
|
|
3533
|
-
for (const collection of this._collections.values()) {
|
|
3534
|
-
if (collection.initialized) {
|
|
3535
|
-
await collection.clear({ force: true });
|
|
3536
|
-
collection.destroy();
|
|
3537
|
-
}
|
|
3538
|
-
}
|
|
3539
|
-
this._collections.clear();
|
|
3540
|
-
|
|
3541
|
-
// Clear quickstore
|
|
3542
|
-
if (this._quickStore) {
|
|
3543
|
-
this._quickStore.clear();
|
|
3544
|
-
}
|
|
3545
|
-
|
|
3546
|
-
// Destroy encryption
|
|
3547
|
-
if (this._encryption) {
|
|
3548
|
-
this._encryption.destroy();
|
|
3549
|
-
}
|
|
3550
|
-
|
|
3551
|
-
// Clear references
|
|
3552
|
-
this._metadata = null;
|
|
3553
|
-
this._settings = null;
|
|
3554
|
-
this._quickStore = null;
|
|
3555
|
-
this._performanceMonitor = null;
|
|
3556
|
-
}
|
|
3557
|
-
}
|
|
3558
|
-
|
|
3559
|
-
// ========================
|
|
3560
|
-
// Main LacertaDB Class
|
|
3561
|
-
// ========================
|
|
3562
|
-
|
|
3563
|
-
class LacertaDB {
|
|
3564
|
-
constructor() {
|
|
3565
|
-
this._databases = new Map();
|
|
3566
|
-
this._performanceMonitor = new PerformanceMonitor();
|
|
3567
|
-
}
|
|
3568
|
-
|
|
3569
|
-
get performanceMonitor() {
|
|
3570
|
-
return this._performanceMonitor;
|
|
3571
|
-
}
|
|
3572
|
-
|
|
3573
|
-
async getDatabase(name, options = {}) {
|
|
3574
|
-
if (!this._databases.has(name)) {
|
|
3575
|
-
const db = new Database(name, this._performanceMonitor);
|
|
3576
|
-
await db.init(options);
|
|
3577
|
-
this._databases.set(name, db);
|
|
3578
|
-
}
|
|
3579
|
-
return this._databases.get(name);
|
|
3580
|
-
}
|
|
3581
|
-
|
|
3582
|
-
async getSecureDatabase(name, pin, salt = null, encryptionConfig = {}) {
|
|
3583
|
-
return this.getDatabase(name, { pin, salt, encryptionConfig });
|
|
3584
|
-
}
|
|
3585
|
-
|
|
3586
|
-
async dropDatabase(name) {
|
|
3587
|
-
if (this._databases.has(name)) {
|
|
3588
|
-
const db = this._databases.get(name);
|
|
3589
|
-
await db.clearAll();
|
|
3590
|
-
db.destroy();
|
|
3591
|
-
this._databases.delete(name);
|
|
3592
|
-
}
|
|
3593
|
-
|
|
3594
|
-
['metadata', 'settings', 'version', 'encryption'].forEach(suffix => {
|
|
3595
|
-
localStorage.removeItem(`lacertadb_${name}_${suffix}`);
|
|
3596
|
-
});
|
|
3597
|
-
|
|
3598
|
-
// Clean up quickstore
|
|
3599
|
-
const quickStore = new QuickStore(name);
|
|
3600
|
-
quickStore.clear();
|
|
3601
|
-
|
|
3602
|
-
// Clean up all collections and indexes
|
|
3603
|
-
const keysToRemove = [];
|
|
3604
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
3605
|
-
const key = localStorage.key(i);
|
|
3606
|
-
if (key && key.startsWith(`lacertadb_${name}_`)) {
|
|
3607
|
-
keysToRemove.push(key);
|
|
3608
|
-
}
|
|
3609
|
-
}
|
|
3610
|
-
keysToRemove.forEach(key => localStorage.removeItem(key));
|
|
3611
|
-
}
|
|
3612
|
-
|
|
3613
|
-
listDatabases() {
|
|
3614
|
-
const dbNames = new Set();
|
|
3615
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
3616
|
-
const key = localStorage.key(i);
|
|
3617
|
-
if (key && key.startsWith('lacertadb_')) {
|
|
3618
|
-
const match = key.match(/^lacertadb_([^_]+)_(metadata|settings|version|encryption|quickstore)$/);
|
|
3619
|
-
if (match) {
|
|
3620
|
-
dbNames.add(match[1]);
|
|
3621
|
-
}
|
|
3622
|
-
}
|
|
3623
|
-
}
|
|
3624
|
-
return [...dbNames];
|
|
3625
|
-
}
|
|
3626
|
-
|
|
3627
|
-
async createBackup(password = null) {
|
|
3628
|
-
const backup = {
|
|
3629
|
-
version: '0.7.0',
|
|
3630
|
-
timestamp: Date.now(),
|
|
3631
|
-
databases: {}
|
|
3632
|
-
};
|
|
3633
|
-
|
|
3634
|
-
for (const dbName of this.listDatabases()) {
|
|
3635
|
-
const db = await this.getDatabase(dbName);
|
|
3636
|
-
const exported = await db.export('json');
|
|
3637
|
-
const decoded = base64.decode(exported);
|
|
3638
|
-
backup.databases[dbName] = serializer.deserialize(decoded);
|
|
3639
|
-
}
|
|
3640
|
-
|
|
3641
|
-
const serializedBackup = serializer.serialize(backup);
|
|
3642
|
-
if (password) {
|
|
3643
|
-
const encryption = new BrowserEncryptionUtility();
|
|
3644
|
-
const encrypted = await encryption.encrypt(serializedBackup, password);
|
|
3645
|
-
return base64.encode(encrypted);
|
|
3646
|
-
}
|
|
3647
|
-
return base64.encode(serializedBackup);
|
|
3648
|
-
}
|
|
3649
|
-
|
|
3650
|
-
async restoreBackup(backupData, password = null) {
|
|
3651
|
-
let backup;
|
|
3652
|
-
try {
|
|
3653
|
-
let decodedData = base64.decode(backupData);
|
|
3654
|
-
if (password) {
|
|
3655
|
-
const encryption = new BrowserEncryptionUtility();
|
|
3656
|
-
const decrypted = await encryption.decrypt(decodedData, password);
|
|
3657
|
-
backup = serializer.deserialize(decrypted);
|
|
3658
|
-
} else {
|
|
3659
|
-
backup = serializer.deserialize(decodedData);
|
|
3660
|
-
}
|
|
3661
|
-
} catch (e) {
|
|
3662
|
-
throw new LacertaDBError('Failed to parse backup data', 'BACKUP_PARSE_FAILED', e);
|
|
3663
|
-
}
|
|
3664
|
-
|
|
3665
|
-
const results = { databases: 0, collections: 0, documents: 0 };
|
|
3666
|
-
for (const [dbName, dbData] of Object.entries(backup.databases)) {
|
|
3667
|
-
const db = await this.getDatabase(dbName);
|
|
3668
|
-
const encodedDbData = base64.encode(serializer.serialize(dbData));
|
|
3669
|
-
const importResult = await db.import(encodedDbData);
|
|
3670
|
-
|
|
3671
|
-
results.databases++;
|
|
3672
|
-
results.collections += importResult.collections;
|
|
3673
|
-
results.documents += importResult.documents;
|
|
3674
|
-
}
|
|
3675
|
-
return results;
|
|
3676
|
-
}
|
|
3677
|
-
|
|
3678
|
-
// Add this method to LacertaDB class:
|
|
3679
|
-
close() {
|
|
3680
|
-
connectionPool.closeAll();
|
|
3681
|
-
}
|
|
3682
|
-
|
|
3683
|
-
// Then fix destroy to not call close twice:
|
|
3684
|
-
destroy() {
|
|
3685
|
-
for (const db of this._databases.values()) {
|
|
3686
|
-
db.destroy();
|
|
3687
|
-
}
|
|
3688
|
-
this._databases.clear();
|
|
3689
|
-
connectionPool.closeAll();
|
|
3690
|
-
}
|
|
3691
|
-
}
|
|
3692
|
-
|
|
3693
|
-
// ========================
|
|
3694
|
-
// Export all components
|
|
3695
|
-
// ========================
|
|
3696
|
-
|
|
3697
|
-
export {
|
|
3698
|
-
LacertaDB,
|
|
3699
|
-
Database,
|
|
3700
|
-
Collection,
|
|
3701
|
-
Document,
|
|
3702
|
-
MigrationManager,
|
|
3703
|
-
PerformanceMonitor,
|
|
3704
|
-
LacertaDBError,
|
|
3705
|
-
OPFSUtility,
|
|
3706
|
-
IndexManager,
|
|
3707
|
-
CacheStrategy,
|
|
3708
|
-
LRUCache,
|
|
3709
|
-
LFUCache,
|
|
3710
|
-
TTLCache,
|
|
3711
|
-
BTreeIndex,
|
|
3712
|
-
TextIndex,
|
|
3713
|
-
GeoIndex,
|
|
3714
|
-
SecureDatabaseEncryption,
|
|
3715
|
-
QuickStore,
|
|
3716
|
-
AsyncMutex,
|
|
3717
|
-
IndexedDBConnectionPool
|
|
3718
|
-
};
|