@pixagram/lacerta-db 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,2462 +1,1853 @@
1
1
  /**
2
- * LacertaDB V4 - Complete Core Library (Refactored)
2
+ * LacertaDB V4 - Complete Core Library (Repaired and Enhanced)
3
3
  * A powerful browser-based document database with encryption, compression, and OPFS support
4
- * @version 4.0.0
4
+ * @version 4.0.3 (Max Compatibility)
5
5
  * @license MIT
6
6
  */
7
+
7
8
  'use strict';
9
+ // Note: These imports are for browser environments using a bundler (e.g., Webpack, Vite).
10
+ // For direct browser usage, you would use an ES module import from a URL or local path.
11
+ import TurboSerial from "@pixagram/turboserial";
12
+ import TurboBase64 from "@pixagram/turbobase64";
13
+
14
+ const serializer = new TurboSerial({
15
+ compression: true,
16
+ deduplication: true,
17
+ shareArrayBuffers: true,
18
+ simdOptimization: true,
19
+ detectCircular: true
20
+ });
21
+ const base64 = new TurboBase64();
8
22
 
9
- (function(global) {
10
- var TurboSerial = require("@pixagram/turboserial");
11
- var TurboBase64 = require("@pixagram/turbobase64");
12
-
13
- var serializer = new TurboSerial({
14
- compression: true,
15
- deduplication: true,
16
- shareArrayBuffers: true,
17
- simdOptimization: true,
18
- detectCircular: true
19
- });
20
- var base64 = new TurboBase64();
21
-
22
- /**
23
- * Async Mutex for managing concurrent operations
24
- */
25
- class AsyncMutex {
26
- constructor() {
27
- this._queue = [];
28
- this._locked = false;
29
- }
30
-
31
- acquire() {
32
- var self = this;
33
- return new Promise(function(resolve) {
34
- self._queue.push(resolve);
35
- self._dispatch();
36
- });
37
- }
23
+ /**
24
+ * Async Mutex for managing concurrent operations
25
+ */
26
+ class AsyncMutex {
27
+ constructor() {
28
+ this._queue = [];
29
+ this._locked = false;
30
+ }
38
31
 
39
- release() {
40
- this._locked = false;
32
+ acquire() {
33
+ return new Promise(resolve => {
34
+ this._queue.push(resolve);
41
35
  this._dispatch();
42
- }
36
+ });
37
+ }
43
38
 
44
- async runExclusive(callback) {
45
- var release = await this.acquire();
46
- try {
47
- return await callback();
48
- } finally {
49
- release();
50
- }
51
- }
39
+ release() {
40
+ this._locked = false;
41
+ this._dispatch();
42
+ }
52
43
 
53
- _dispatch() {
54
- if (this._locked || this._queue.length === 0) {
55
- return;
56
- }
57
- this._locked = true;
58
- var resolve = this._queue.shift();
59
- var self = this;
60
- resolve(function() {
61
- self.release();
62
- });
44
+ async runExclusive(callback) {
45
+ const release = await this.acquire();
46
+ try {
47
+ return await callback();
48
+ } finally {
49
+ release();
63
50
  }
64
51
  }
65
52
 
66
- /**
67
- * Custom error class for LacertaDB
68
- */
69
- class LacertaDBError extends Error {
70
- constructor(message, code, originalError) {
71
- super(message);
72
- this.name = 'LacertaDBError';
73
- this.code = code;
74
- this.originalError = originalError || null;
75
- this.timestamp = new Date().toISOString();
53
+ _dispatch() {
54
+ if (this._locked || this._queue.length === 0) {
55
+ return;
76
56
  }
57
+ this._locked = true;
58
+ const resolve = this._queue.shift();
59
+ resolve(() => this.release());
77
60
  }
61
+ }
78
62
 
79
- // ========================
80
- // Compression Utility
81
- // ========================
63
+ /**
64
+ * Custom error class for LacertaDB
65
+ */
66
+ class LacertaDBError extends Error {
67
+ constructor(message, code, originalError) {
68
+ super(message);
69
+ this.name = 'LacertaDBError';
70
+ this.code = code;
71
+ this.originalError = originalError || null;
72
+ this.timestamp = new Date().toISOString();
73
+ }
74
+ }
82
75
 
83
- class BrowserCompressionUtility {
84
- async compress(input) {
85
- try {
86
- var stream = new Response(input).body
87
- .pipeThrough(new CompressionStream('deflate'));
88
- var compressed = await new Response(stream).arrayBuffer();
89
- return new Uint8Array(compressed);
90
- } catch (error) {
91
- throw new LacertaDBError('Compression failed', 'COMPRESSION_FAILED', error);
92
- }
93
- }
76
+ // ========================
77
+ // Compression Utility
78
+ // ========================
94
79
 
95
- async decompress(compressedData) {
96
- try {
97
- var stream = new Response(compressedData).body
98
- .pipeThrough(new DecompressionStream('deflate'));
99
- var decompressed = await new Response(stream).arrayBuffer();
100
- return new Uint8Array(decompressed);
101
- } catch (error) {
102
- throw new LacertaDBError('Decompression failed', 'DECOMPRESSION_FAILED', error);
103
- }
80
+ class BrowserCompressionUtility {
81
+ async compress(input) {
82
+ if (!(input instanceof Uint8Array)) {
83
+ throw new TypeError('Input must be Uint8Array');
104
84
  }
105
-
106
- compressSync(input) {
107
- return new TextEncoder().encode(base64.encode(input));
85
+ try {
86
+ const stream = new Response(input).body
87
+ .pipeThrough(new CompressionStream('deflate'));
88
+ const compressed = await new Response(stream).arrayBuffer();
89
+ return new Uint8Array(compressed);
90
+ } catch (error) {
91
+ throw new LacertaDBError('Compression failed', 'COMPRESSION_FAILED', error);
108
92
  }
93
+ }
109
94
 
110
- decompressSync(compressedData) {
111
- return base64.decode(new TextDecoder().decode(compressedData));
95
+ async decompress(compressedData) {
96
+ if (!(compressedData instanceof Uint8Array)) {
97
+ throw new TypeError('Input must be Uint8Array');
98
+ }
99
+ try {
100
+ const stream = new Response(compressedData).body
101
+ .pipeThrough(new DecompressionStream('deflate'));
102
+ const decompressed = await new Response(stream).arrayBuffer();
103
+ return new Uint8Array(decompressed);
104
+ } catch (error) {
105
+ throw new LacertaDBError('Decompression failed', 'DECOMPRESSION_FAILED', error);
112
106
  }
113
107
  }
114
108
 
115
- // ========================
116
- // Encryption Utility
117
- // ========================
109
+ // Fallback sync methods are simple pass-throughs
110
+ compressSync(input) {
111
+ if (!(input instanceof Uint8Array)) {
112
+ throw new TypeError('Input must be Uint8Array');
113
+ }
114
+ return input;
115
+ }
118
116
 
119
- class BrowserEncryptionUtility {
120
- async encrypt(data, password) {
121
- try {
122
- var salt = crypto.getRandomValues(new Uint8Array(16));
123
- var iv = crypto.getRandomValues(new Uint8Array(12));
124
-
125
- var keyMaterial = await crypto.subtle.importKey(
126
- 'raw',
127
- new TextEncoder().encode(password),
128
- 'PBKDF2',
129
- false,
130
- ['deriveKey']
131
- );
132
-
133
- var key = await crypto.subtle.deriveKey({
134
- name: 'PBKDF2',
135
- salt: salt,
136
- iterations: 600000,
137
- hash: 'SHA-512'
138
- },
139
- keyMaterial, {
140
- name: 'AES-GCM',
141
- length: 256
142
- },
143
- false,
144
- ['encrypt']
145
- );
146
-
147
- var encrypted = await crypto.subtle.encrypt({
148
- name: 'AES-GCM',
149
- iv: iv
150
- },
151
- key,
152
- data
153
- );
154
-
155
- var checksum = await crypto.subtle.digest('SHA-256', data);
156
- var checksumArray = new Uint8Array(checksum);
157
-
158
- var result = new Uint8Array(
159
- salt.length + iv.length + checksumArray.length + encrypted.byteLength
160
- );
161
- result.set(salt, 0);
162
- result.set(iv, salt.length);
163
- result.set(checksumArray, salt.length + iv.length);
164
- result.set(new Uint8Array(encrypted), salt.length + iv.length + checksumArray.length);
165
-
166
- return result;
167
- } catch (error) {
168
- throw new LacertaDBError('Encryption failed', 'ENCRYPTION_FAILED', error);
169
- }
117
+ decompressSync(compressedData) {
118
+ if (!(compressedData instanceof Uint8Array)) {
119
+ throw new TypeError('Input must be Uint8Array');
170
120
  }
121
+ return compressedData;
122
+ }
123
+ }
124
+
125
+ // ========================
126
+ // Encryption Utility (FIXED & IMPROVED)
127
+ // ========================
128
+
129
+ class BrowserEncryptionUtility {
130
+ async encrypt(data, password) {
131
+ if (!(data instanceof Uint8Array)) {
132
+ throw new TypeError('Data must be Uint8Array');
133
+ }
134
+ try {
135
+ const salt = crypto.getRandomValues(new Uint8Array(16));
136
+ const iv = crypto.getRandomValues(new Uint8Array(12));
137
+
138
+ const keyMaterial = await crypto.subtle.importKey(
139
+ 'raw',
140
+ new TextEncoder().encode(password),
141
+ 'PBKDF2',
142
+ false,
143
+ ['deriveKey']
144
+ );
171
145
 
172
- async decrypt(wrappedData, password) {
173
- try {
174
- var salt = wrappedData.slice(0, 16);
175
- var iv = wrappedData.slice(16, 28);
176
- var checksum = wrappedData.slice(28, 60);
177
- var encryptedData = wrappedData.slice(60);
178
-
179
- var keyMaterial = await crypto.subtle.importKey(
180
- 'raw',
181
- new TextEncoder().encode(password),
182
- 'PBKDF2',
183
- false,
184
- ['deriveKey']
185
- );
186
-
187
- var key = await crypto.subtle.deriveKey({
188
- name: 'PBKDF2',
189
- salt: salt,
190
- iterations: 600000,
191
- hash: 'SHA-512'
192
- },
193
- keyMaterial, {
194
- name: 'AES-GCM',
195
- length: 256
196
- },
197
- false,
198
- ['decrypt']
199
- );
200
-
201
- var decrypted = await crypto.subtle.decrypt({
202
- name: 'AES-GCM',
203
- iv: iv
204
- },
205
- key,
206
- encryptedData
207
- );
208
-
209
- var newChecksum = await crypto.subtle.digest('SHA-256', decrypted);
210
- var newChecksumArray = new Uint8Array(newChecksum);
211
-
212
- var isValid = true;
213
- for (var i = 0; i < checksum.length; i++) {
214
- if (checksum[i] !== newChecksumArray[i]) {
215
- isValid = false;
216
- break;
217
- }
218
- }
146
+ const key = await crypto.subtle.deriveKey({
147
+ name: 'PBKDF2',
148
+ salt,
149
+ iterations: 600000,
150
+ hash: 'SHA-512'
151
+ },
152
+ keyMaterial, {
153
+ name: 'AES-GCM',
154
+ length: 256
155
+ },
156
+ false,
157
+ ['encrypt']
158
+ );
219
159
 
220
- if (!isValid) {
221
- throw new Error('Checksum verification failed');
222
- }
160
+ const encrypted = await crypto.subtle.encrypt({
161
+ name: 'AES-GCM',
162
+ iv
163
+ },
164
+ key,
165
+ data
166
+ );
223
167
 
224
- return new Uint8Array(decrypted);
225
- } catch (error) {
226
- throw new LacertaDBError('Decryption failed', 'DECRYPTION_FAILED', error);
227
- }
168
+ // The checksum was removed as AES-GCM provides this via an authentication tag.
169
+ const result = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
170
+ result.set(salt, 0);
171
+ result.set(iv, salt.length);
172
+ result.set(new Uint8Array(encrypted), salt.length + iv.length);
173
+
174
+ return result;
175
+ } catch (error) {
176
+ throw new LacertaDBError('Encryption failed', 'ENCRYPTION_FAILED', error);
228
177
  }
229
178
  }
230
179
 
231
- // ========================
232
- // OPFS (Origin Private File System) Utility
233
- // ========================
234
-
235
- class OPFSUtility {
236
- async saveAttachments(dbName, collectionName, documentId, attachments) {
237
- var attachmentPaths = [];
238
- var root = await navigator.storage.getDirectory();
180
+ async decrypt(wrappedData, password) {
181
+ if (!(wrappedData instanceof Uint8Array)) {
182
+ throw new TypeError('Data must be Uint8Array');
183
+ }
184
+ try {
185
+ const salt = wrappedData.slice(0, 16);
186
+ const iv = wrappedData.slice(16, 28);
187
+ const encryptedData = wrappedData.slice(28);
188
+
189
+ const keyMaterial = await crypto.subtle.importKey(
190
+ 'raw',
191
+ new TextEncoder().encode(password),
192
+ 'PBKDF2',
193
+ false,
194
+ ['deriveKey']
195
+ );
239
196
 
240
- try {
241
- var dbDir = await root.getDirectoryHandle(dbName, {
242
- create: true
243
- });
244
- var collDir = await dbDir.getDirectoryHandle(collectionName, {
245
- create: true
246
- });
247
- var docDir = await collDir.getDirectoryHandle(documentId, {
248
- create: true
249
- });
197
+ const key = await crypto.subtle.deriveKey({
198
+ name: 'PBKDF2',
199
+ salt,
200
+ iterations: 600000,
201
+ hash: 'SHA-512'
202
+ },
203
+ keyMaterial, {
204
+ name: 'AES-GCM',
205
+ length: 256
206
+ },
207
+ false,
208
+ ['decrypt']
209
+ );
250
210
 
251
- for (var i = 0; i < attachments.length; i++) {
252
- var attachment = attachments[i];
253
- var fileHandle = await docDir.getFileHandle(String(i), {
254
- create: true
255
- });
256
- var writable = await fileHandle.createWritable();
211
+ const decrypted = await crypto.subtle.decrypt({
212
+ name: 'AES-GCM',
213
+ iv
214
+ },
215
+ key,
216
+ encryptedData
217
+ );
257
218
 
258
- var blob = new Blob([attachment.data], {
259
- type: attachment.type
260
- });
261
- await writable.write(blob);
262
- await writable.close();
263
-
264
- var path = '/' + dbName + '/' + collectionName + '/' + documentId + '/' + i;
265
- attachmentPaths.push({
266
- path: path,
267
- name: attachment.name,
268
- type: attachment.type,
269
- size: attachment.data.byteLength
270
- });
219
+ // Checksum verification removed. crypto.subtle.decrypt will throw on failure.
220
+ return new Uint8Array(decrypted);
221
+ } catch (error) {
222
+ // Provide a more specific error for failed decryption, which often indicates a wrong password.
223
+ throw new LacertaDBError('Decryption failed. This may be due to an incorrect password or corrupted data.', 'DECRYPTION_FAILED', error);
224
+ }
225
+ }
226
+ }
227
+
228
+ // ========================
229
+ // OPFS (Origin Private File System) Utility
230
+ // ========================
231
+
232
+ class OPFSUtility {
233
+ async saveAttachments(dbName, collectionName, documentId, attachments) {
234
+ try {
235
+ const attachmentPaths = [];
236
+ const root = await navigator.storage.getDirectory();
237
+ const dbDir = await root.getDirectoryHandle(dbName, { create: true });
238
+ const collDir = await dbDir.getDirectoryHandle(collectionName, { create: true });
239
+ const docDir = await collDir.getDirectoryHandle(documentId, { create: true });
240
+
241
+ for (const [index, attachment] of attachments.entries()) {
242
+ const filename = `${index}_${attachment.name || 'file'}`;
243
+ const fileHandle = await docDir.getFileHandle(filename, { create: true });
244
+ const writable = await fileHandle.createWritable();
245
+
246
+ let dataToWrite;
247
+ if (attachment.data instanceof Uint8Array) {
248
+ dataToWrite = attachment.data;
249
+ } else if (attachment.data instanceof ArrayBuffer) {
250
+ dataToWrite = new Uint8Array(attachment.data);
251
+ } else if (attachment.data instanceof Blob) {
252
+ dataToWrite = new Uint8Array(await attachment.data.arrayBuffer());
253
+ } else {
254
+ throw new TypeError('Unsupported attachment data type');
271
255
  }
272
256
 
273
- return attachmentPaths;
274
- } catch (error) {
275
- throw new LacertaDBError('Failed to save attachments', 'ATTACHMENT_SAVE_FAILED', error);
257
+ const blob = new Blob([dataToWrite], { type: attachment.type || 'application/octet-stream' });
258
+ await writable.write(blob);
259
+ await writable.close();
260
+
261
+ const path = `/${dbName}/${collectionName}/${documentId}/${filename}`;
262
+ attachmentPaths.push({
263
+ path,
264
+ name: attachment.name,
265
+ type: attachment.type,
266
+ size: dataToWrite.byteLength,
267
+ originalName: attachment.originalName || attachment.name
268
+ });
276
269
  }
270
+ return attachmentPaths;
271
+ } catch (error) {
272
+ throw new LacertaDBError('Failed to save attachments', 'ATTACHMENT_SAVE_FAILED', error);
277
273
  }
274
+ }
278
275
 
279
- async getAttachments(attachmentPaths) {
280
- var attachments = [];
281
- var root = await navigator.storage.getDirectory();
282
-
283
- for (var i = 0; i < attachmentPaths.length; i++) {
284
- var attachmentInfo = attachmentPaths[i];
285
- try {
286
- var pathParts = attachmentInfo.path.split('/').filter(function(p) {
287
- return p;
288
- });
289
- var currentDir = root;
290
-
291
- for (var j = 0; j < pathParts.length - 1; j++) {
292
- currentDir = await currentDir.getDirectoryHandle(pathParts[j]);
293
- }
276
+ async getAttachments(attachmentPaths) {
277
+ const attachments = [];
278
+ const root = await navigator.storage.getDirectory();
294
279
 
295
- var fileHandle = await currentDir.getFileHandle(pathParts[pathParts.length - 1]);
296
- var file = await fileHandle.getFile();
297
- var data = await file.arrayBuffer();
280
+ for (const attachmentInfo of attachmentPaths) {
281
+ try {
282
+ const pathParts = attachmentInfo.path.split('/').filter(p => p);
283
+ let currentDir = root;
298
284
 
299
- attachments.push({
300
- name: attachmentInfo.name,
301
- type: attachmentInfo.type,
302
- data: new Uint8Array(data),
303
- size: attachmentInfo.size
304
- });
305
- } catch (error) {
306
- console.error('Failed to get attachment:', attachmentInfo.path, error);
285
+ for (let i = 0; i < pathParts.length - 1; i++) {
286
+ currentDir = await currentDir.getDirectoryHandle(pathParts[i]);
307
287
  }
308
- }
309
288
 
310
- return attachments;
311
- }
289
+ const fileHandle = await currentDir.getFileHandle(pathParts[pathParts.length - 1]);
290
+ const file = await fileHandle.getFile();
291
+ const data = await file.arrayBuffer();
312
292
 
313
- async deleteAttachments(dbName, collectionName, documentId) {
314
- try {
315
- var root = await navigator.storage.getDirectory();
316
- var dbDir = await root.getDirectoryHandle(dbName);
317
- var collDir = await dbDir.getDirectoryHandle(collectionName);
318
- await collDir.removeEntry(documentId, {
319
- recursive: true
293
+ attachments.push({
294
+ name: attachmentInfo.originalName || attachmentInfo.name,
295
+ type: attachmentInfo.type,
296
+ data: new Uint8Array(data),
297
+ size: attachmentInfo.size
320
298
  });
321
299
  } catch (error) {
322
- console.error('Failed to delete attachments:', error);
300
+ console.error(`Failed to get attachment: ${attachmentInfo.path}`, error);
301
+ // Optionally, collect errors and return them
323
302
  }
324
303
  }
304
+ return attachments;
325
305
  }
326
306
 
327
- // ========================
328
- // IndexedDB Utility
329
- // ========================
330
-
331
- class IndexedDBUtility {
332
- constructor() {
333
- this.transactionQueue = [];
334
- this.isProcessing = false;
335
- this.mutex = new AsyncMutex();
307
+ async deleteAttachments(dbName, collectionName, documentId) {
308
+ try {
309
+ const root = await navigator.storage.getDirectory();
310
+ const dbDir = await root.getDirectoryHandle(dbName);
311
+ const collDir = await dbDir.getDirectoryHandle(collectionName);
312
+ await collDir.removeEntry(documentId, { recursive: true });
313
+ } catch (error) {
314
+ // Ignore "NotFoundError" as the directory might already be gone
315
+ if (error.name !== 'NotFoundError') {
316
+ console.error(`Failed to delete attachments for ${documentId}:`, error);
317
+ }
336
318
  }
319
+ }
337
320
 
338
- openDatabase(dbName, version, upgradeCallback) {
339
- version = version || 1;
340
- return new Promise(function(resolve, reject) {
341
- var request = indexedDB.open(dbName, version);
321
+ static async prepareAttachment(file, name) {
322
+ let data;
323
+ if (file instanceof File || file instanceof Blob) {
324
+ const buffer = await file.arrayBuffer();
325
+ data = new Uint8Array(buffer);
326
+ } else if (file instanceof ArrayBuffer) {
327
+ data = new Uint8Array(file);
328
+ } else if (file instanceof Uint8Array) {
329
+ data = file;
330
+ } else {
331
+ throw new TypeError('Unsupported file type for attachment');
332
+ }
333
+
334
+ return {
335
+ name: name || file.name || 'unnamed',
336
+ type: file.type || 'application/octet-stream',
337
+ data,
338
+ originalName: file.name || name
339
+ };
340
+ }
341
+ }
342
342
 
343
- request.onerror = function() {
344
- reject(new LacertaDBError(
345
- 'Failed to open database',
346
- 'DATABASE_OPEN_FAILED',
347
- request.error
348
- ));
349
- };
343
+ // ========================
344
+ // IndexedDB Utility
345
+ // ========================
350
346
 
351
- request.onsuccess = function() {
352
- resolve(request.result);
353
- };
347
+ class IndexedDBUtility {
348
+ constructor() {
349
+ this.mutex = new AsyncMutex();
350
+ }
354
351
 
355
- request.onupgradeneeded = function(event) {
356
- var db = event.target.result;
357
- if (upgradeCallback) {
358
- upgradeCallback(db, event.oldVersion, event.newVersion);
359
- }
360
- };
361
- });
362
- }
352
+ openDatabase(dbName, version = 1, upgradeCallback) {
353
+ return new Promise((resolve, reject) => {
354
+ const request = indexedDB.open(dbName, version);
355
+
356
+ request.onerror = () => reject(new LacertaDBError(
357
+ 'Failed to open database', 'DATABASE_OPEN_FAILED', request.error
358
+ ));
359
+ request.onsuccess = () => resolve(request.result);
360
+ request.onupgradeneeded = event => {
361
+ if (upgradeCallback) {
362
+ upgradeCallback(event.target.result, event.oldVersion, event.newVersion);
363
+ }
364
+ };
365
+ });
366
+ }
363
367
 
364
- async performTransaction(db, storeNames, mode, callback, retries, operationId) {
365
- retries = retries || 3;
366
- operationId = operationId || null;
367
- return this.mutex.runExclusive(async function() {
368
- var lastError;
369
-
370
- for (var i = 0; i < retries; i++) {
371
- try {
372
- return await new Promise(function(resolve, reject) {
373
- var transaction = db.transaction(storeNames, mode);
374
- var result;
375
-
376
- transaction.oncomplete = function() {
377
- resolve(result);
378
- };
379
- transaction.onerror = function() {
380
- reject(transaction.error);
381
- };
382
- transaction.onabort = function() {
383
- reject(new Error('Transaction aborted'));
384
- };
385
-
386
- try {
387
- result = callback(transaction);
388
- if (result instanceof Promise) {
389
- result.then(function(res) {
390
- result = res;
391
- }).catch(reject);
392
- }
393
- } catch (error) {
394
- reject(error);
368
+ async performTransaction(db, storeNames, mode, callback, retries = 3) {
369
+ return this.mutex.runExclusive(async () => {
370
+ let lastError;
371
+ for (let i = 0; i < retries; i++) {
372
+ try {
373
+ return await new Promise((resolve, reject) => {
374
+ const transaction = db.transaction(storeNames, mode);
375
+ let result;
376
+
377
+ transaction.oncomplete = () => resolve(result);
378
+ transaction.onerror = () => reject(transaction.error);
379
+ transaction.onabort = () => reject(new Error('Transaction aborted'));
380
+
381
+ try {
382
+ const cbResult = callback(transaction);
383
+ if (cbResult instanceof Promise) {
384
+ cbResult.then(res => { result = res; }).catch(reject);
385
+ } else {
386
+ result = cbResult;
395
387
  }
396
- });
397
- } catch (error) {
398
- lastError = error;
399
- if (i < retries - 1) {
400
- await new Promise(function(resolve) {
401
- setTimeout(resolve, Math.pow(2, i) * 100);
402
- });
388
+ } catch (error) {
389
+ reject(error);
403
390
  }
391
+ });
392
+ } catch (error) {
393
+ lastError = error;
394
+ if (i < retries - 1) {
395
+ await new Promise(resolve => setTimeout(resolve, (2 ** i) * 100));
404
396
  }
405
397
  }
398
+ }
399
+ throw new LacertaDBError('Transaction failed after retries', 'TRANSACTION_FAILED', lastError);
400
+ });
401
+ }
406
402
 
407
- throw new LacertaDBError(
408
- 'Transaction failed after retries',
409
- 'TRANSACTION_FAILED',
410
- lastError
411
- );
412
- });
413
- }
403
+ _promisifyRequest(requestFactory) {
404
+ return new Promise((resolve, reject) => {
405
+ const request = requestFactory();
406
+ request.onsuccess = () => resolve(request.result);
407
+ request.onerror = () => reject(request.error);
408
+ });
409
+ }
414
410
 
415
- add(db, storeName, value, key) {
416
- key = key === undefined ? undefined : key;
417
- return this.performTransaction(db, [storeName], 'readwrite', function(tx) {
418
- return new Promise(function(resolve, reject) {
419
- var request = key ?
420
- tx.objectStore(storeName).add(value, key) :
421
- tx.objectStore(storeName).add(value);
422
- request.onsuccess = function() {
423
- resolve(request.result);
424
- };
425
- request.onerror = function() {
426
- reject(request.error);
427
- };
428
- });
429
- });
430
- }
411
+ add(db, storeName, value, key) {
412
+ return this.performTransaction(db, [storeName], 'readwrite', tx => {
413
+ const store = tx.objectStore(storeName);
414
+ return this._promisifyRequest(() => key !== undefined ? store.add(value, key) : store.add(value));
415
+ });
416
+ }
431
417
 
432
- put(db, storeName, value, key) {
433
- key = key === undefined ? undefined : key;
434
- return this.performTransaction(db, [storeName], 'readwrite', function(tx) {
435
- return new Promise(function(resolve, reject) {
436
- var request = key ?
437
- tx.objectStore(storeName).put(value, key) :
438
- tx.objectStore(storeName).put(value);
439
- request.onsuccess = function() {
440
- resolve(request.result);
441
- };
442
- request.onerror = function() {
443
- reject(request.error);
444
- };
445
- });
446
- });
447
- }
418
+ put(db, storeName, value, key) {
419
+ return this.performTransaction(db, [storeName], 'readwrite', tx => {
420
+ const store = tx.objectStore(storeName);
421
+ return this._promisifyRequest(() => key !== undefined ? store.put(value, key) : store.put(value));
422
+ });
423
+ }
448
424
 
449
- get(db, storeName, key) {
450
- return this.performTransaction(db, [storeName], 'readonly', function(tx) {
451
- return new Promise(function(resolve, reject) {
452
- var request = tx.objectStore(storeName).get(key);
453
- request.onsuccess = function() {
454
- resolve(request.result);
455
- };
456
- request.onerror = function() {
457
- reject(request.error);
458
- };
459
- });
460
- });
461
- }
425
+ get(db, storeName, key) {
426
+ return this.performTransaction(db, [storeName], 'readonly', tx => {
427
+ return this._promisifyRequest(() => tx.objectStore(storeName).get(key));
428
+ });
429
+ }
462
430
 
463
- getAll(db, storeName, query, count) {
464
- query = query === undefined ? undefined : query;
465
- count = count === undefined ? undefined : count;
466
- return this.performTransaction(db, [storeName], 'readonly', function(tx) {
467
- return new Promise(function(resolve, reject) {
468
- var request = tx.objectStore(storeName).getAll(query, count);
469
- request.onsuccess = function() {
470
- resolve(request.result);
471
- };
472
- request.onerror = function() {
473
- reject(request.error);
474
- };
475
- });
476
- });
477
- }
431
+ getAll(db, storeName, query, count) {
432
+ return this.performTransaction(db, [storeName], 'readonly', tx => {
433
+ return this._promisifyRequest(() => tx.objectStore(storeName).getAll(query, count));
434
+ });
435
+ }
478
436
 
479
- delete(db, storeName, key) {
480
- return this.performTransaction(db, [storeName], 'readwrite', function(tx) {
481
- return new Promise(function(resolve, reject) {
482
- var request = tx.objectStore(storeName).delete(key);
483
- request.onsuccess = function() {
484
- resolve();
485
- };
486
- request.onerror = function() {
487
- reject(request.error);
488
- };
489
- });
490
- });
491
- }
437
+ delete(db, storeName, key) {
438
+ return this.performTransaction(db, [storeName], 'readwrite', tx => {
439
+ return this._promisifyRequest(() => tx.objectStore(storeName).delete(key));
440
+ });
441
+ }
492
442
 
493
- clear(db, storeName) {
494
- return this.performTransaction(db, [storeName], 'readwrite', function(tx) {
495
- return new Promise(function(resolve, reject) {
496
- var request = tx.objectStore(storeName).clear();
497
- request.onsuccess = function() {
498
- resolve();
499
- };
500
- request.onerror = function() {
501
- reject(request.error);
502
- };
503
- });
504
- });
505
- }
443
+ clear(db, storeName) {
444
+ return this.performTransaction(db, [storeName], 'readwrite', tx => {
445
+ return this._promisifyRequest(() => tx.objectStore(storeName).clear());
446
+ });
447
+ }
506
448
 
507
- count(db, storeName, query) {
508
- query = query === undefined ? undefined : query;
509
- return this.performTransaction(db, [storeName], 'readonly', function(tx) {
510
- return new Promise(function(resolve, reject) {
511
- var request = tx.objectStore(storeName).count(query);
512
- request.onsuccess = function() {
513
- resolve(request.result);
514
- };
515
- request.onerror = function() {
516
- reject(request.error);
517
- };
518
- });
519
- });
520
- }
449
+ count(db, storeName, query) {
450
+ return this.performTransaction(db, [storeName], 'readonly', tx => {
451
+ return this._promisifyRequest(() => tx.objectStore(storeName).count(query));
452
+ });
453
+ }
521
454
 
522
- iterateCursorSafe(db, storeName, callback, direction, query) {
523
- direction = direction || 'next';
524
- query = query === undefined ? undefined : query;
525
- return this.performTransaction(db, [storeName], 'readonly', function(tx) {
526
- return new Promise(function(resolve, reject) {
527
- var results = [];
528
- var request = tx.objectStore(storeName).openCursor(query, direction);
529
-
530
- request.onsuccess = function(event) {
531
- var cursor = event.target.result;
532
- if (cursor) {
533
- try {
534
- var result = callback(cursor.value, cursor.key);
535
- if (result !== false) {
536
- results.push(result);
537
- cursor.continue();
538
- } else {
539
- resolve(results);
540
- }
541
- } catch (error) {
542
- reject(error);
455
+ iterateCursorSafe(db, storeName, callback, direction = 'next', query) {
456
+ return this.performTransaction(db, [storeName], 'readonly', tx => {
457
+ return new Promise((resolve, reject) => {
458
+ const results = [];
459
+ const request = tx.objectStore(storeName).openCursor(query, direction);
460
+
461
+ request.onsuccess = event => {
462
+ const cursor = event.target.result;
463
+ if (cursor) {
464
+ try {
465
+ const result = callback(cursor.value, cursor.key);
466
+ if (result !== false) {
467
+ results.push(result);
468
+ cursor.continue();
469
+ } else {
470
+ resolve(results);
543
471
  }
544
- } else {
545
- resolve(results);
472
+ } catch (error) {
473
+ reject(error);
546
474
  }
547
- };
548
-
549
- request.onerror = function() {
550
- reject(request.error);
551
- };
552
- });
475
+ } else {
476
+ resolve(results);
477
+ }
478
+ };
479
+ request.onerror = () => reject(request.error);
553
480
  });
554
- }
481
+ });
482
+ }
483
+ }
484
+
485
+ // ========================
486
+ // Document Class
487
+ // ========================
488
+
489
+ class Document {
490
+ constructor(data = {}, options = {}) {
491
+ this._id = data._id || this.generateId();
492
+ this._created = data._created || Date.now();
493
+ this._modified = data._modified || Date.now();
494
+ this._permanent = data._permanent || options.permanent || false;
495
+ this._encrypted = data._encrypted || options.encrypted || false;
496
+ this._compressed = data._compressed || options.compressed || false;
497
+ this._attachments = data._attachments || [];
498
+ this.data = data.data || {};
499
+ this.packedData = data.packedData || null;
500
+
501
+ // Utilities can be passed in or instantiated. For simplicity, we keep instantiation here.
502
+ this.compression = new BrowserCompressionUtility();
503
+ this.encryption = new BrowserEncryptionUtility();
504
+ this.password = options.password || null;
555
505
  }
556
506
 
507
+ generateId() {
508
+ return `doc_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
509
+ }
557
510
 
558
- // ========================
559
- // Document Class
560
- // ========================
561
-
562
- class Document {
563
- constructor(data, options) {
564
- data = data || {};
565
- options = options || {};
566
- this._id = data._id || this.generateId();
567
- this._created = data._created || Date.now();
568
- this._modified = data._modified || Date.now();
569
- this._permanent = data._permanent || options.permanent || false;
570
- this._encrypted = data._encrypted || options.encrypted || false;
571
- this._compressed = data._compressed || options.compressed || false;
572
- this._attachments = data._attachments || [];
573
- this.data = data.data || {};
574
- this.packedData = data.packedData || null;
575
-
576
- this.serializer = new TurboSerial({
577
- compression: true,
578
- deduplication: true,
579
- shareArrayBuffers: true,
580
- simdOptimization: true,
581
- detectCircular: true
582
- });
583
- this.compression = new BrowserCompressionUtility();
584
- this.encryption = new BrowserEncryptionUtility();
585
- this.password = options.password || null;
586
- }
587
-
588
- generateId() {
589
- return 'doc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
590
- }
591
-
592
- async pack() {
593
- try {
594
- var packed = this.serializer.serialize(this.data);
595
-
596
- if (this._compressed) {
597
- packed = await this.compression.compress(packed);
598
- }
599
-
600
- if (this._encrypted && this.password) {
601
- packed = await this.encryption.encrypt(packed, this.password);
602
- }
603
-
604
- this.packedData = packed;
605
- return packed;
606
- } catch (error) {
607
- throw new LacertaDBError('Failed to pack document', 'PACK_FAILED', error);
608
- }
609
- }
610
-
611
- async unpack() {
612
- try {
613
- var unpacked = this.packedData;
614
-
615
- if (this._encrypted && this.password) {
616
- unpacked = await this.encryption.decrypt(unpacked, this.password);
617
- }
618
-
619
- if (this._compressed) {
620
- unpacked = await this.compression.decompress(unpacked);
621
- }
622
-
623
- this.data = this.serializer.deserialize(unpacked);
624
- return this.data;
625
- } catch (error) {
626
- throw new LacertaDBError('Failed to unpack document', 'UNPACK_FAILED', error);
627
- }
628
- }
629
-
630
- packSync() {
631
- var packed = this.serializer.serialize(this.data);
632
-
511
+ async pack() {
512
+ try {
513
+ let packed = serializer.serialize(this.data);
633
514
  if (this._compressed) {
634
- packed = this.compression.compressSync(packed);
515
+ packed = await this.compression.compress(packed);
635
516
  }
636
-
637
517
  if (this._encrypted && this.password) {
638
- throw new LacertaDBError('Synchronous encryption not supported', 'SYNC_ENCRYPT_NOT_SUPPORTED');
518
+ packed = await this.encryption.encrypt(packed, this.password);
639
519
  }
640
-
641
520
  this.packedData = packed;
642
521
  return packed;
522
+ } catch (error) {
523
+ throw new LacertaDBError('Failed to pack document', 'PACK_FAILED', error);
643
524
  }
525
+ }
644
526
 
645
- unpackSync() {
646
- var unpacked = this.packedData;
647
-
527
+ async unpack() {
528
+ try {
529
+ let unpacked = this.packedData;
648
530
  if (this._encrypted && this.password) {
649
- throw new LacertaDBError('Synchronous decryption not supported', 'SYNC_DECRYPT_NOT_SUPPORTED');
531
+ unpacked = await this.encryption.decrypt(unpacked, this.password);
650
532
  }
651
-
652
533
  if (this._compressed) {
653
- unpacked = this.compression.decompressSync(unpacked);
534
+ unpacked = await this.compression.decompress(unpacked);
654
535
  }
655
-
656
- this.data = this.serializer.deserialize(unpacked);
536
+ this.data = serializer.deserialize(unpacked);
657
537
  return this.data;
538
+ } catch (error) {
539
+ throw new LacertaDBError('Failed to unpack document', 'UNPACK_FAILED', error);
658
540
  }
541
+ }
659
542
 
660
- objectOutput(includeAttachments) {
661
- includeAttachments = includeAttachments || false;
662
- var output = {
663
- _id: this._id,
664
- _created: this._created,
665
- _modified: this._modified,
666
- _permanent: this._permanent
667
- };
668
-
669
- for (var key in this.data) {
670
- if (Object.prototype.hasOwnProperty.call(this.data, key)) {
671
- output[key] = this.data[key];
672
- }
673
- }
674
-
675
- if (includeAttachments && this._attachments.length > 0) {
676
- output._attachments = this._attachments;
677
- }
678
-
679
- return output;
543
+ packSync() {
544
+ let packed = serializer.serialize(this.data);
545
+ if (this._compressed) {
546
+ packed = this.compression.compressSync(packed);
680
547
  }
681
-
682
- databaseOutput() {
683
- return {
684
- _id: this._id,
685
- _created: this._created,
686
- _modified: this._modified,
687
- _permanent: this._permanent,
688
- _encrypted: this._encrypted,
689
- _compressed: this._compressed,
690
- _attachments: this._attachments,
691
- packedData: this.packedData
692
- };
548
+ if (this._encrypted) {
549
+ throw new LacertaDBError('Synchronous encryption not supported', 'SYNC_ENCRYPT_NOT_SUPPORTED');
693
550
  }
551
+ this.packedData = packed;
552
+ return packed;
694
553
  }
695
554
 
696
-
697
- // ========================
698
- // Metadata Classes
699
- // ========================
700
-
701
- class CollectionMetadata {
702
- constructor(name, data) {
703
- data = data || {};
704
- this.name = name;
705
- this.sizeKB = data.sizeKB || 0;
706
- this.length = data.length || 0;
707
- this.createdAt = data.createdAt || Date.now();
708
- this.modifiedAt = data.modifiedAt || Date.now();
709
- this.documentSizes = data.documentSizes || {};
710
- this.documentModifiedAt = data.documentModifiedAt || {};
711
- this.documentPermanent = data.documentPermanent || {};
712
- this.documentAttachments = data.documentAttachments || {};
555
+ unpackSync() {
556
+ let unpacked = this.packedData;
557
+ if (this._encrypted) {
558
+ throw new LacertaDBError('Synchronous decryption not supported', 'SYNC_DECRYPT_NOT_SUPPORTED');
713
559
  }
714
-
715
- addDocument(docId, sizeKB, isPermanent, attachmentCount) {
716
- this.documentSizes[docId] = sizeKB;
717
- this.documentModifiedAt[docId] = Date.now();
718
- if (isPermanent) {
719
- this.documentPermanent[docId] = true;
720
- }
721
- if (attachmentCount > 0) {
722
- this.documentAttachments[docId] = attachmentCount;
723
- }
724
- this.sizeKB += sizeKB;
725
- this.length++;
726
- this.modifiedAt = Date.now();
560
+ if (this._compressed) {
561
+ unpacked = this.compression.decompressSync(unpacked);
727
562
  }
563
+ this.data = serializer.deserialize(unpacked);
564
+ return this.data;
565
+ }
728
566
 
729
- updateDocument(docId, newSizeKB, isPermanent, attachmentCount) {
730
- var oldSize = this.documentSizes[docId] || 0;
731
- this.sizeKB = this.sizeKB - oldSize + newSizeKB;
732
- this.documentSizes[docId] = newSizeKB;
733
- this.documentModifiedAt[docId] = Date.now();
734
-
735
- if (isPermanent) {
736
- this.documentPermanent[docId] = true;
737
- } else {
738
- delete this.documentPermanent[docId];
739
- }
567
+ objectOutput(includeAttachments = false) {
568
+ const output = {
569
+ _id: this._id,
570
+ _created: this._created,
571
+ _modified: this._modified,
572
+ _permanent: this._permanent,
573
+ ...this.data
574
+ };
575
+ if (includeAttachments && this._attachments.length > 0) {
576
+ output._attachments = this._attachments;
577
+ }
578
+ return output;
579
+ }
740
580
 
741
- if (attachmentCount > 0) {
742
- this.documentAttachments[docId] = attachmentCount;
743
- } else {
744
- delete this.documentAttachments[docId];
745
- }
581
+ databaseOutput() {
582
+ return {
583
+ _id: this._id,
584
+ _created: this._created,
585
+ _modified: this._modified,
586
+ _permanent: this._permanent,
587
+ _encrypted: this._encrypted,
588
+ _compressed: this._compressed,
589
+ _attachments: this._attachments,
590
+ packedData: this.packedData
591
+ };
592
+ }
593
+ }
594
+
595
+ // ========================
596
+ // Metadata Classes
597
+ // ========================
598
+
599
+ class CollectionMetadata {
600
+ constructor(name, data = {}) {
601
+ this.name = name;
602
+ this.sizeKB = data.sizeKB || 0;
603
+ this.length = data.length || 0;
604
+ this.createdAt = data.createdAt || Date.now();
605
+ this.modifiedAt = data.modifiedAt || Date.now();
606
+ this.documentSizes = data.documentSizes || {};
607
+ this.documentModifiedAt = data.documentModifiedAt || {};
608
+ this.documentPermanent = data.documentPermanent || {};
609
+ this.documentAttachments = data.documentAttachments || {};
610
+ }
746
611
 
747
- this.modifiedAt = Date.now();
748
- }
612
+ addDocument(docId, sizeKB, isPermanent, attachmentCount) {
613
+ this.documentSizes[docId] = sizeKB;
614
+ this.documentModifiedAt[docId] = Date.now();
615
+ if (isPermanent) this.documentPermanent[docId] = true;
616
+ if (attachmentCount > 0) this.documentAttachments[docId] = attachmentCount;
749
617
 
750
- removeDocument(docId) {
751
- var sizeKB = this.documentSizes[docId] || 0;
752
- delete this.documentSizes[docId];
753
- delete this.documentModifiedAt[docId];
754
- delete this.documentPermanent[docId];
755
- delete this.documentAttachments[docId];
756
- this.sizeKB -= sizeKB;
757
- this.length--;
758
- this.modifiedAt = Date.now();
759
- }
760
-
761
- getOldestNonPermanentDocuments(count) {
762
- var self = this;
763
- var docs = Object.entries(this.documentModifiedAt)
764
- .filter(function(entry) {
765
- var docId = entry[0];
766
- return !self.documentPermanent[docId];
767
- })
768
- .sort(function(a, b) {
769
- return a[1] - b[1];
770
- })
771
- .slice(0, count)
772
- .map(function(entry) {
773
- return entry[0];
774
- });
775
- return docs;
776
- }
618
+ this.sizeKB += sizeKB;
619
+ this.length++;
620
+ this.modifiedAt = Date.now();
777
621
  }
778
622
 
779
- class DatabaseMetadata {
780
- constructor(name, data) {
781
- data = data || {};
782
- this.name = name;
783
- this.collections = data.collections || {};
784
- this.totalSizeKB = data.totalSizeKB || 0;
785
- this.totalLength = data.totalLength || 0;
786
- this.modifiedAt = data.modifiedAt || Date.now();
787
- }
788
-
789
- static load(dbName) {
790
- var key = 'lacertadb_' + dbName + '_metadata';
791
- var stored = localStorage.getItem(key);
792
- if (stored) {
793
- try {
794
- var decoded = base64.decode(stored);
795
- var data = serializer.deserialize(decoded);
796
- return new DatabaseMetadata(dbName, data);
797
- } catch (e) {
798
- console.error('Failed to load metadata:', e);
799
- }
800
- }
801
- return new DatabaseMetadata(dbName);
802
- }
623
+ updateDocument(docId, newSizeKB, isPermanent, attachmentCount) {
624
+ const oldSize = this.documentSizes[docId] || 0;
625
+ this.sizeKB = this.sizeKB - oldSize + newSizeKB;
626
+ this.documentSizes[docId] = newSizeKB;
627
+ this.documentModifiedAt[docId] = Date.now();
803
628
 
804
- save() {
805
- var key = 'lacertadb_' + this.name + '_metadata';
806
- try {
807
- var dataToStore = {
808
- collections: this.collections,
809
- totalSizeKB: this.totalSizeKB,
810
- totalLength: this.totalLength,
811
- modifiedAt: this.modifiedAt
812
- };
813
- var serializedData = serializer.serialize(dataToStore);
814
- var encodedData = base64.encode(serializedData);
815
- localStorage.setItem(key, encodedData);
816
- } catch (e) {
817
- if (e.name === 'QuotaExceededError') {
818
- throw new LacertaDBError('Storage quota exceeded', 'QUOTA_EXCEEDED', e);
819
- }
820
- throw new LacertaDBError('Failed to save metadata', 'METADATA_SAVE_FAILED', e);
821
- }
629
+ if (isPermanent) {
630
+ this.documentPermanent[docId] = true;
631
+ } else {
632
+ delete this.documentPermanent[docId];
822
633
  }
823
634
 
824
- addCollection(collectionMetadata) {
825
- this.collections[collectionMetadata.name] = {
826
- sizeKB: collectionMetadata.sizeKB,
827
- length: collectionMetadata.length,
828
- createdAt: collectionMetadata.createdAt,
829
- modifiedAt: collectionMetadata.modifiedAt,
830
- documentSizes: collectionMetadata.documentSizes,
831
- documentModifiedAt: collectionMetadata.documentModifiedAt,
832
- documentPermanent: collectionMetadata.documentPermanent,
833
- documentAttachments: collectionMetadata.documentAttachments
834
- };
835
- this.recalculate();
836
- this.save();
837
- }
838
-
839
- updateCollection(collectionMetadata) {
840
- this.collections[collectionMetadata.name] = {
841
- sizeKB: collectionMetadata.sizeKB,
842
- length: collectionMetadata.length,
843
- createdAt: collectionMetadata.createdAt,
844
- modifiedAt: collectionMetadata.modifiedAt,
845
- documentSizes: collectionMetadata.documentSizes,
846
- documentModifiedAt: collectionMetadata.documentModifiedAt,
847
- documentPermanent: collectionMetadata.documentPermanent,
848
- documentAttachments: collectionMetadata.documentAttachments
849
- };
850
- this.recalculate();
851
- this.save();
635
+ if (attachmentCount > 0) {
636
+ this.documentAttachments[docId] = attachmentCount;
637
+ } else {
638
+ delete this.documentAttachments[docId];
852
639
  }
853
640
 
854
- removeCollection(collectionName) {
855
- delete this.collections[collectionName];
856
- this.recalculate();
857
- this.save();
858
- }
641
+ this.modifiedAt = Date.now();
642
+ }
859
643
 
860
- recalculate() {
861
- this.totalSizeKB = 0;
862
- this.totalLength = 0;
863
- for (var collName in this.collections) {
864
- if (Object.prototype.hasOwnProperty.call(this.collections, collName)) {
865
- var coll = this.collections[collName];
866
- this.totalSizeKB += coll.sizeKB;
867
- this.totalLength += coll.length;
868
- }
869
- }
870
- this.modifiedAt = Date.now();
644
+ removeDocument(docId) {
645
+ const sizeKB = this.documentSizes[docId] || 0;
646
+ if (this.documentSizes[docId]) {
647
+ this.sizeKB -= sizeKB;
648
+ this.length--;
871
649
  }
650
+ delete this.documentSizes[docId];
651
+ delete this.documentModifiedAt[docId];
652
+ delete this.documentPermanent[docId];
653
+ delete this.documentAttachments[docId];
654
+ this.modifiedAt = Date.now();
872
655
  }
873
656
 
874
- class Settings {
875
- constructor(dbName, data) {
876
- data = data || {};
877
- this.dbName = dbName;
878
- this.sizeLimitKB = data.sizeLimitKB || Infinity;
879
- this.bufferLimitKB = data.bufferLimitKB || (this.sizeLimitKB === Infinity ? 0 : this.sizeLimitKB * 0.8);
880
- this.freeSpaceEvery = data.freeSpaceEvery || 10000;
881
- }
657
+ getOldestNonPermanentDocuments(count) {
658
+ return Object.entries(this.documentModifiedAt)
659
+ .filter(([docId]) => !this.documentPermanent[docId])
660
+ .sort(([, timeA], [, timeB]) => timeA - timeB)
661
+ .slice(0, count)
662
+ .map(([docId]) => docId);
663
+ }
664
+ }
665
+
666
+ class DatabaseMetadata {
667
+ constructor(name, data = {}) {
668
+ this.name = name;
669
+ this.collections = data.collections || {};
670
+ this.totalSizeKB = data.totalSizeKB || 0;
671
+ this.totalLength = data.totalLength || 0;
672
+ this.modifiedAt = data.modifiedAt || Date.now();
673
+ }
882
674
 
883
- static load(dbName) {
884
- var key = 'lacertadb_' + dbName + '_settings';
885
- var stored = localStorage.getItem(key);
886
- if (stored) {
887
- try {
888
- var decoded = base64.decode(stored);
889
- var data = serializer.deserialize(decoded);
890
- return new Settings(dbName, data);
891
- } catch (e) {
892
- console.error('Failed to load settings:', e);
893
- }
675
+ static load(dbName) {
676
+ const key = `lacertadb_${dbName}_metadata`;
677
+ const stored = localStorage.getItem(key);
678
+ if (stored) {
679
+ try {
680
+ const decoded = base64.decode(stored);
681
+ const data = serializer.deserialize(decoded);
682
+ return new DatabaseMetadata(dbName, data);
683
+ } catch (e) {
684
+ console.error('Failed to load metadata:', e);
894
685
  }
895
- return new Settings(dbName);
896
686
  }
687
+ return new DatabaseMetadata(dbName);
688
+ }
897
689
 
898
- save() {
899
- var key = 'lacertadb_' + this.dbName + '_settings';
900
- var dataToStore = {
901
- sizeLimitKB: this.sizeLimitKB,
902
- bufferLimitKB: this.bufferLimitKB,
903
- freeSpaceEvery: this.freeSpaceEvery
690
+ save() {
691
+ const key = `lacertadb_${this.name}_metadata`;
692
+ try {
693
+ const dataToStore = {
694
+ collections: this.collections,
695
+ totalSizeKB: this.totalSizeKB,
696
+ totalLength: this.totalLength,
697
+ modifiedAt: this.modifiedAt
904
698
  };
905
- var serializedData = serializer.serialize(dataToStore);
906
- var encodedData = base64.encode(serializedData);
699
+ const serializedData = serializer.serialize(dataToStore);
700
+ const encodedData = base64.encode(serializedData);
907
701
  localStorage.setItem(key, encodedData);
702
+ } catch (e) {
703
+ if (e.name === 'QuotaExceededError') {
704
+ throw new LacertaDBError('Storage quota exceeded for metadata', 'QUOTA_EXCEEDED', e);
705
+ }
706
+ throw new LacertaDBError('Failed to save metadata', 'METADATA_SAVE_FAILED', e);
908
707
  }
708
+ }
909
709
 
910
- updateSettings(newSettings) {
911
- Object.assign(this, newSettings);
912
- this.save();
913
- }
710
+ setCollection(collectionMetadata) {
711
+ this.collections[collectionMetadata.name] = {
712
+ sizeKB: collectionMetadata.sizeKB,
713
+ length: collectionMetadata.length,
714
+ createdAt: collectionMetadata.createdAt,
715
+ modifiedAt: collectionMetadata.modifiedAt,
716
+ documentSizes: collectionMetadata.documentSizes,
717
+ documentModifiedAt: collectionMetadata.documentModifiedAt,
718
+ documentPermanent: collectionMetadata.documentPermanent,
719
+ documentAttachments: collectionMetadata.documentAttachments
720
+ };
721
+ this.recalculate();
722
+ this.save();
914
723
  }
915
724
 
916
- // ========================
917
- // Quick Store (localStorage based)
918
- // ========================
725
+ removeCollection(collectionName) {
726
+ delete this.collections[collectionName];
727
+ this.recalculate();
728
+ this.save();
729
+ }
919
730
 
920
- class QuickStore {
921
- constructor(dbName) {
922
- this.dbName = dbName;
923
- this.keyPrefix = 'lacertadb_' + dbName + '_quickstore_';
731
+ recalculate() {
732
+ this.totalSizeKB = 0;
733
+ this.totalLength = 0;
734
+ for (const collName in this.collections) {
735
+ const coll = this.collections[collName];
736
+ this.totalSizeKB += coll.sizeKB;
737
+ this.totalLength += coll.length;
924
738
  }
739
+ this.modifiedAt = Date.now();
740
+ }
741
+ }
742
+
743
+ class Settings {
744
+ constructor(dbName, data = {}) {
745
+ this.dbName = dbName;
746
+ // Replaced `??` with ternary operator for compatibility
747
+ this.sizeLimitKB = data.sizeLimitKB != null ? data.sizeLimitKB : Infinity;
748
+ const defaultBuffer = this.sizeLimitKB === Infinity ? 0 : this.sizeLimitKB * 0.8;
749
+ this.bufferLimitKB = data.bufferLimitKB != null ? data.bufferLimitKB : defaultBuffer;
750
+ this.freeSpaceEvery = data.freeSpaceEvery || 10000;
751
+ }
925
752
 
926
- add(docId, data) {
927
- var key = this.keyPrefix + 'data_' + docId;
753
+ static load(dbName) {
754
+ const key = `lacertadb_${dbName}_settings`;
755
+ const stored = localStorage.getItem(key);
756
+ if (stored) {
928
757
  try {
929
- var serializedData = serializer.serialize(data);
930
- var encodedData = base64.encode(serializedData);
931
- localStorage.setItem(key, encodedData);
932
- this.updateIndex(docId, 'add');
933
- return true;
758
+ const decoded = base64.decode(stored);
759
+ const data = serializer.deserialize(decoded);
760
+ return new Settings(dbName, data);
934
761
  } catch (e) {
935
- if (e.name === 'QuotaExceededError') {
936
- throw new LacertaDBError('QuickStore quota exceeded', 'QUOTA_EXCEEDED', e);
937
- }
938
- return false;
762
+ console.error('Failed to load settings:', e);
939
763
  }
940
764
  }
765
+ return new Settings(dbName);
766
+ }
941
767
 
942
- get(docId) {
943
- var key = this.keyPrefix + 'data_' + docId;
944
- var stored = localStorage.getItem(key);
945
- if (stored) {
946
- try {
947
- var decoded = base64.decode(stored);
948
- return serializer.deserialize(decoded);
949
- } catch (e) {
950
- console.error('Failed to parse QuickStore data:', e);
951
- }
952
- }
953
- return null;
954
- }
955
-
956
- update(docId, data) {
957
- return this.add(docId, data);
958
- }
959
-
960
- delete(docId) {
961
- var key = this.keyPrefix + 'data_' + docId;
962
- localStorage.removeItem(key);
963
- this.updateIndex(docId, 'delete');
964
- }
965
-
966
- getAll() {
967
- var indexKey = this.keyPrefix + 'index';
968
- var indexStr = localStorage.getItem(indexKey);
969
- var index = [];
970
- if (indexStr) {
971
- try {
972
- var decoded = base64.decode(indexStr);
973
- index = serializer.deserialize(decoded);
974
- } catch (e) {
975
- index = [];
976
- }
977
- }
768
+ save() {
769
+ const key = `lacertadb_${this.dbName}_settings`;
770
+ const dataToStore = {
771
+ sizeLimitKB: this.sizeLimitKB,
772
+ bufferLimitKB: this.bufferLimitKB,
773
+ freeSpaceEvery: this.freeSpaceEvery
774
+ };
775
+ const serializedData = serializer.serialize(dataToStore);
776
+ const encodedData = base64.encode(serializedData);
777
+ localStorage.setItem(key, encodedData);
778
+ }
978
779
 
979
- var documents = [];
980
- for (var i = 0; i < index.length; i++) {
981
- var docId = index[i];
982
- var doc = this.get(docId);
983
- if (doc) {
984
- var docWithId = {
985
- _id: docId
986
- };
987
- for (var key in doc) {
988
- if (Object.prototype.hasOwnProperty.call(doc, key)) {
989
- docWithId[key] = doc[key];
990
- }
991
- }
992
- documents.push(docWithId);
993
- }
994
- }
995
- return documents;
996
- }
780
+ updateSettings(newSettings) {
781
+ Object.assign(this, newSettings);
782
+ this.save();
783
+ }
784
+ }
997
785
 
998
- clear() {
999
- var indexKey = this.keyPrefix + 'index';
1000
- var indexStr = localStorage.getItem(indexKey);
1001
- var index = [];
1002
- if (indexStr) {
1003
- try {
1004
- var decoded = base64.decode(indexStr);
1005
- index = serializer.deserialize(decoded);
1006
- } catch (e) {
1007
- index = [];
1008
- }
1009
- }
786
+ // ========================
787
+ // Quick Store (localStorage based)
788
+ // ========================
1010
789
 
1011
- for (var i = 0; i < index.length; i++) {
1012
- this.delete(index[i]);
1013
- }
790
+ class QuickStore {
791
+ constructor(dbName) {
792
+ this.dbName = dbName;
793
+ this.keyPrefix = `lacertadb_${dbName}_quickstore_`;
794
+ this.indexKey = `${this.keyPrefix}index`;
795
+ }
1014
796
 
1015
- localStorage.removeItem(indexKey);
797
+ _readIndex() {
798
+ const indexStr = localStorage.getItem(this.indexKey);
799
+ if (!indexStr) return [];
800
+ try {
801
+ const decoded = base64.decode(indexStr);
802
+ return serializer.deserialize(decoded);
803
+ } catch {
804
+ return [];
1016
805
  }
806
+ }
1017
807
 
1018
- updateIndex(docId, operation) {
1019
- var indexKey = this.keyPrefix + 'index';
1020
- var indexStr = localStorage.getItem(indexKey);
1021
- var index = [];
1022
- if (indexStr) {
1023
- try {
1024
- var decoded = base64.decode(indexStr);
1025
- index = serializer.deserialize(decoded);
1026
- } catch (e) {
1027
- index = [];
1028
- }
1029
- }
808
+ _writeIndex(index) {
809
+ const serializedIndex = serializer.serialize(index);
810
+ const encodedIndex = base64.encode(serializedIndex);
811
+ localStorage.setItem(this.indexKey, encodedIndex);
812
+ }
1030
813
 
814
+ add(docId, data) {
815
+ const key = `${this.keyPrefix}data_${docId}`;
816
+ try {
817
+ const serializedData = serializer.serialize(data);
818
+ const encodedData = base64.encode(serializedData);
819
+ localStorage.setItem(key, encodedData);
1031
820
 
1032
- if (operation === 'add' && !index.includes(docId)) {
821
+ const index = this._readIndex();
822
+ if (!index.includes(docId)) {
1033
823
  index.push(docId);
1034
- } else if (operation === 'delete') {
1035
- index = index.filter(function(id) {
1036
- return id !== docId;
1037
- });
1038
- }
1039
-
1040
- var serializedIndex = serializer.serialize(index);
1041
- var encodedIndex = base64.encode(serializedIndex);
1042
- localStorage.setItem(indexKey, encodedIndex);
1043
- }
1044
- }
1045
-
1046
-
1047
- // ========================
1048
- // Query Operators
1049
- // ========================
1050
-
1051
- class QueryOperators {}
1052
- QueryOperators.operators = {
1053
- // Comparison
1054
- '$eq': function(a, b) {
1055
- return a === b;
1056
- },
1057
- '$ne': function(a, b) {
1058
- return a !== b;
1059
- },
1060
- '$gt': function(a, b) {
1061
- return a > b;
1062
- },
1063
- '$gte': function(a, b) {
1064
- return a >= b;
1065
- },
1066
- '$lt': function(a, b) {
1067
- return a < b;
1068
- },
1069
- '$lte': function(a, b) {
1070
- return a <= b;
1071
- },
1072
- '$in': function(a, b) {
1073
- return Array.isArray(b) && b.includes(a);
1074
- },
1075
- '$nin': function(a, b) {
1076
- return Array.isArray(b) && !b.includes(a);
1077
- },
1078
-
1079
- // Logical
1080
- '$and': function(doc, conditions) {
1081
- return conditions.every(function(cond) {
1082
- return QueryOperators.evaluate(doc, cond);
1083
- });
1084
- },
1085
- '$or': function(doc, conditions) {
1086
- return conditions.some(function(cond) {
1087
- return QueryOperators.evaluate(doc, cond);
1088
- });
1089
- },
1090
- '$not': function(doc, condition) {
1091
- return !QueryOperators.evaluate(doc, condition);
1092
- },
1093
- '$nor': function(doc, conditions) {
1094
- return !conditions.some(function(cond) {
1095
- return QueryOperators.evaluate(doc, cond);
1096
- });
1097
- },
1098
-
1099
- // Element
1100
- '$exists': function(value, exists) {
1101
- return (value !== undefined) === exists;
1102
- },
1103
- '$type': function(value, type) {
1104
- return typeof value === type;
1105
- },
1106
-
1107
- // Array
1108
- '$all': function(arr, values) {
1109
- return Array.isArray(arr) && values.every(function(v) {
1110
- return arr.includes(v);
1111
- });
1112
- },
1113
- '$elemMatch': function(arr, condition) {
1114
- return Array.isArray(arr) && arr.some(function(elem) {
1115
- return QueryOperators.evaluate({
1116
- value: elem
1117
- }, {
1118
- value: condition
1119
- });
1120
- });
1121
- },
1122
- '$size': function(arr, size) {
1123
- return Array.isArray(arr) && arr.length === size;
1124
- },
1125
-
1126
- // String
1127
- '$regex': function(str, pattern) {
1128
- if (typeof str !== 'string') return false;
1129
- var regex = new RegExp(pattern);
1130
- return regex.test(str);
1131
- },
1132
- '$text': function(str, search) {
1133
- if (typeof str !== 'string') return false;
1134
- return str.toLowerCase().includes(search.toLowerCase());
1135
- }
1136
- };
1137
-
1138
- QueryOperators.evaluate = function(doc, query) {
1139
- for (var key in query) {
1140
- if (Object.prototype.hasOwnProperty.call(query, key)) {
1141
- var value = query[key];
1142
- if (key.startsWith('$')) {
1143
- // Logical operator at root level
1144
- var operator = this.operators[key];
1145
- if (!operator) return false;
1146
- if (!operator(doc, value)) return false;
1147
- } else {
1148
- // Field-level query
1149
- var fieldValue = this.getFieldValue(doc, key);
1150
-
1151
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1152
- // Operator-based comparison
1153
- for (var op in value) {
1154
- if (Object.prototype.hasOwnProperty.call(value, op)) {
1155
- var opValue = value[op];
1156
- if (op.startsWith('$')) {
1157
- var operatorFn = this.operators[op];
1158
- if (!operatorFn || !operatorFn(fieldValue, opValue)) {
1159
- return false;
1160
- }
1161
- } else {
1162
- if (fieldValue !== value) return false;
1163
- }
1164
- }
1165
- }
1166
- } else {
1167
- // Direct equality comparison
1168
- if (fieldValue !== value) return false;
1169
- }
1170
- }
1171
- }
1172
- }
1173
- return true;
1174
- }.bind(QueryOperators);
1175
-
1176
- QueryOperators.getFieldValue = function(doc, path) {
1177
- var parts = path.split('.');
1178
- var value = doc;
1179
- for (var i = 0; i < parts.length; i++) {
1180
- var part = parts[i];
1181
- if (value == null) return undefined;
1182
- value = value[part];
1183
- }
1184
- return value;
1185
- };
1186
-
1187
-
1188
- // ========================
1189
- // Aggregation Pipeline
1190
- // ========================
1191
-
1192
- class AggregationPipeline {}
1193
- AggregationPipeline.stages = {
1194
- '$match': function(docs, condition) {
1195
- return docs.filter(function(doc) {
1196
- return QueryOperators.evaluate(doc, condition);
1197
- });
1198
- },
1199
-
1200
- '$project': function(docs, projection) {
1201
- var self = this;
1202
- return docs.map(function(doc) {
1203
- var projected = {};
1204
- for (var key in projection) {
1205
- if (Object.prototype.hasOwnProperty.call(projection, key)) {
1206
- var value = projection[key];
1207
- if (value === 1 || value === true) {
1208
- projected[key] = QueryOperators.getFieldValue(doc, key);
1209
- } else if (typeof value === 'object') {
1210
- projected[key] = self.computeField(doc, value);
1211
- }
1212
- }
1213
- }
1214
- return projected;
1215
- });
1216
- },
1217
-
1218
- '$sort': function(docs, sortSpec) {
1219
- var sorted = docs.slice(); // Create a shallow copy
1220
- sorted.sort(function(a, b) {
1221
- for (var key in sortSpec) {
1222
- if (Object.prototype.hasOwnProperty.call(sortSpec, key)) {
1223
- var order = sortSpec[key];
1224
- var aVal = QueryOperators.getFieldValue(a, key);
1225
- var bVal = QueryOperators.getFieldValue(b, key);
1226
- if (aVal < bVal) return order === 1 ? -1 : 1;
1227
- if (aVal > bVal) return order === 1 ? 1 : -1;
1228
- }
1229
- }
1230
- return 0;
1231
- });
1232
- return sorted;
1233
- },
1234
-
1235
- '$limit': function(docs, limit) {
1236
- return docs.slice(0, limit);
1237
- },
1238
-
1239
- '$skip': function(docs, skip) {
1240
- return docs.slice(skip);
1241
- },
1242
-
1243
- '$group': function(docs, groupSpec) {
1244
- var groups = {};
1245
- var idField = groupSpec._id;
1246
-
1247
- for (var i = 0; i < docs.length; i++) {
1248
- var doc = docs[i];
1249
- var groupKey = typeof idField === 'string' ?
1250
- QueryOperators.getFieldValue(doc, idField.replace('$', '')) :
1251
- JSON.stringify(idField); // JSON for key generation is acceptable here
1252
-
1253
- if (!groups[groupKey]) {
1254
- groups[groupKey] = {
1255
- _id: groupKey,
1256
- docs: []
1257
- };
1258
- }
1259
- groups[groupKey].docs.push(doc);
1260
- }
1261
-
1262
- var results = [];
1263
- for (var key in groups) {
1264
- if (Object.prototype.hasOwnProperty.call(groups, key)) {
1265
- var group = groups[key];
1266
- var result = {
1267
- _id: group._id
1268
- };
1269
-
1270
- for (var fieldKey in groupSpec) {
1271
- if (Object.prototype.hasOwnProperty.call(groupSpec, fieldKey)) {
1272
- if (fieldKey === '_id') continue;
1273
- var accumulator = groupSpec[fieldKey];
1274
-
1275
- if (accumulator.$sum) {
1276
- result[fieldKey] = group.docs.reduce(function(sum, doc) {
1277
- var val = typeof accumulator.$sum === 'string' ?
1278
- QueryOperators.getFieldValue(doc, accumulator.$sum.replace('$', '')) :
1279
- accumulator.$sum;
1280
- return sum + (val || 0);
1281
- }, 0);
1282
- } else if (accumulator.$avg) {
1283
- var sumVal = group.docs.reduce(function(sum, doc) {
1284
- var val = QueryOperators.getFieldValue(doc, accumulator.$avg.replace('$', ''));
1285
- return sum + (val || 0);
1286
- }, 0);
1287
- result[fieldKey] = sumVal / group.docs.length;
1288
- } else if (accumulator.$count) {
1289
- result[fieldKey] = group.docs.length;
1290
- } else if (accumulator.$max) {
1291
- result[fieldKey] = Math.max.apply(null, group.docs.map(function(doc) {
1292
- return QueryOperators.getFieldValue(doc, accumulator.$max.replace('$', ''));
1293
- }));
1294
- } else if (accumulator.$min) {
1295
- result[fieldKey] = Math.min.apply(null, group.docs.map(function(doc) {
1296
- return QueryOperators.getFieldValue(doc, accumulator.$min.replace('$', ''));
1297
- }));
1298
- }
1299
- }
1300
- }
1301
- results.push(result);
1302
- }
824
+ this._writeIndex(index);
1303
825
  }
1304
- return results;
1305
- },
1306
-
1307
- '$lookup': async function(docs, lookupSpec, db) {
1308
- var results = [];
1309
- var foreignCollection = await db.getCollection(lookupSpec.from);
1310
-
1311
- for (var i = 0; i < docs.length; i++) {
1312
- var doc = docs[i];
1313
- var localValue = QueryOperators.getFieldValue(doc, lookupSpec.localField);
1314
- var query = {};
1315
- query[lookupSpec.foreignField] = localValue;
1316
- var foreignDocs = await foreignCollection.query(query);
1317
-
1318
- var newDoc = {};
1319
- for (var key in doc) {
1320
- if (Object.prototype.hasOwnProperty.call(doc, key)) {
1321
- newDoc[key] = doc[key];
1322
- }
1323
- }
1324
- newDoc[lookupSpec.as] = foreignDocs;
1325
- results.push(newDoc);
826
+ return true;
827
+ } catch (e) {
828
+ if (e.name === 'QuotaExceededError') {
829
+ throw new LacertaDBError('QuickStore quota exceeded', 'QUOTA_EXCEEDED', e);
1326
830
  }
1327
-
1328
- return results;
831
+ return false;
1329
832
  }
1330
- };
1331
-
1332
- AggregationPipeline.computeField = function(doc, spec) {
1333
- if (typeof spec === 'string' && spec.startsWith('$')) {
1334
- return QueryOperators.getFieldValue(doc, spec.replace('$', ''));
1335
- }
1336
- return spec;
1337
- };
1338
-
1339
- AggregationPipeline.execute = async function(docs, pipeline, db) {
1340
- var result = docs;
1341
-
1342
- for (var i = 0; i < pipeline.length; i++) {
1343
- var stage = pipeline[i];
1344
- var stageName = Object.keys(stage)[0];
1345
- var stageSpec = stage[stageName];
1346
- var stageFunction = this.stages[stageName];
1347
-
1348
- if (!stageFunction) {
1349
- throw new Error('Unknown aggregation stage: ' + stageName);
1350
- }
833
+ }
1351
834
 
1352
- if (stageName === '$lookup') {
1353
- result = await stageFunction(result, stageSpec, db);
1354
- } else {
1355
- result = stageFunction.call(this, result, stageSpec);
835
+ get(docId) {
836
+ const key = `${this.keyPrefix}data_${docId}`;
837
+ const stored = localStorage.getItem(key);
838
+ if (stored) {
839
+ try {
840
+ const decoded = base64.decode(stored);
841
+ return serializer.deserialize(decoded);
842
+ } catch (e) {
843
+ console.error('Failed to parse QuickStore data:', e);
1356
844
  }
1357
845
  }
846
+ return null;
847
+ }
1358
848
 
1359
- return result;
1360
- }.bind(AggregationPipeline);
1361
-
1362
- // ========================
1363
- // Migration Manager
1364
- // ========================
1365
-
1366
- class MigrationManager {
1367
- constructor(database) {
1368
- this.database = database;
1369
- this.migrations = [];
1370
- this.currentVersion = this.loadVersion();
1371
- }
1372
-
1373
- loadVersion() {
1374
- return localStorage.getItem('lacertadb_' + this.database.name + '_version') || '1.0.0';
1375
- }
1376
-
1377
- saveVersion(version) {
1378
- localStorage.setItem('lacertadb_' + this.database.name + '_version', version);
1379
- this.currentVersion = version;
1380
- }
1381
-
1382
- addMigration(migration) {
1383
- this.migrations.push(migration);
1384
- }
1385
-
1386
- async runMigrations(targetVersion) {
1387
- var self = this;
1388
- var applicableMigrations = this.migrations.filter(function(m) {
1389
- return self.compareVersions(m.version, self.currentVersion) > 0 &&
1390
- self.compareVersions(m.version, targetVersion) <= 0;
1391
- });
849
+ update(docId, data) {
850
+ return this.add(docId, data);
851
+ }
1392
852
 
1393
- applicableMigrations.sort(function(a, b) {
1394
- return self.compareVersions(a.version, b.version);
1395
- });
853
+ delete(docId) {
854
+ const key = `${this.keyPrefix}data_${docId}`;
855
+ localStorage.removeItem(key);
1396
856
 
1397
- for (var i = 0; i < applicableMigrations.length; i++) {
1398
- var migration = applicableMigrations[i];
1399
- await this.runMigration(migration);
1400
- this.saveVersion(migration.version);
1401
- }
857
+ let index = this._readIndex();
858
+ const initialLength = index.length;
859
+ index = index.filter(id => id !== docId);
860
+ if (index.length < initialLength) {
861
+ this._writeIndex(index);
1402
862
  }
863
+ }
1403
864
 
1404
- async runMigration(migration) {
1405
- console.log('Running migration: ' + migration.name + ' (v' + migration.version + ')');
1406
- var collections = await this.database.listCollections();
1407
- for (var i = 0; i < collections.length; i++) {
1408
- var collectionName = collections[i];
1409
- var coll = await this.database.getCollection(collectionName);
1410
- var docs = await coll.getAll();
865
+ getAll() {
866
+ const index = this._readIndex();
867
+ return index.map(docId => {
868
+ const doc = this.get(docId);
869
+ return doc ? { _id: docId, ...doc } : null;
870
+ }).filter(Boolean);
871
+ }
1411
872
 
1412
- for (var j = 0; j < docs.length; j++) {
1413
- var doc = docs[j];
1414
- var updated = await migration.up(doc);
1415
- if (updated) {
1416
- await coll.update(doc._id, updated);
1417
- }
1418
- }
1419
- }
873
+ clear() {
874
+ const index = this._readIndex();
875
+ for (const docId of index) {
876
+ localStorage.removeItem(`${this.keyPrefix}data_${docId}`);
1420
877
  }
1421
-
1422
- async rollback(targetVersion) {
1423
- var self = this;
1424
- var applicableMigrations = this.migrations.filter(function(m) {
1425
- return self.compareVersions(m.version, targetVersion) > 0 &&
1426
- self.compareVersions(m.version, self.currentVersion) <= 0;
1427
- });
1428
-
1429
- applicableMigrations.sort(function(a, b) {
1430
- return self.compareVersions(b.version, a.version);
1431
- });
1432
-
1433
- for (var i = 0; i < applicableMigrations.length; i++) {
1434
- var migration = applicableMigrations[i];
1435
- if (migration.down) {
1436
- await this.runRollback(migration);
878
+ localStorage.removeItem(this.indexKey);
879
+ }
880
+ }
881
+
882
+ // ========================
883
+ // Query Engine
884
+ // ========================
885
+
886
+ class QueryEngine {
887
+ constructor() {
888
+ this.operators = {
889
+ // Comparison
890
+ '$eq': (a, b) => a === b,
891
+ '$ne': (a, b) => a !== b,
892
+ '$gt': (a, b) => a > b,
893
+ '$gte': (a, b) => a >= b,
894
+ '$lt': (a, b) => a < b,
895
+ '$lte': (a, b) => a <= b,
896
+ '$in': (a, b) => Array.isArray(b) && b.includes(a),
897
+ '$nin': (a, b) => Array.isArray(b) && !b.includes(a),
898
+
899
+ // Logical
900
+ '$and': (doc, conditions) => conditions.every(cond => this.evaluate(doc, cond)),
901
+ '$or': (doc, conditions) => conditions.some(cond => this.evaluate(doc, cond)),
902
+ '$not': (doc, condition) => !this.evaluate(doc, condition),
903
+ '$nor': (doc, conditions) => !conditions.some(cond => this.evaluate(doc, cond)),
904
+
905
+ // Element
906
+ '$exists': (value, exists) => (value !== undefined) === exists,
907
+ '$type': (value, type) => typeof value === type,
908
+
909
+ // Array
910
+ '$all': (arr, values) => Array.isArray(arr) && values.every(v => arr.includes(v)),
911
+ '$elemMatch': (arr, condition) => Array.isArray(arr) && arr.some(elem => this.evaluate({ value: elem }, { value: condition })),
912
+ '$size': (arr, size) => Array.isArray(arr) && arr.length === size,
913
+
914
+ // String
915
+ '$regex': (str, pattern) => {
916
+ if (typeof str !== 'string') return false;
917
+ try {
918
+ const regex = new RegExp(pattern);
919
+ return regex.test(str);
920
+ } catch {
921
+ return false;
1437
922
  }
1438
- }
1439
-
1440
- this.saveVersion(targetVersion);
1441
- }
1442
-
1443
- async runRollback(migration) {
1444
- console.log('Rolling back migration: ' + migration.name + ' (v' + migration.version + ')');
1445
- var collections = await this.database.listCollections();
1446
- for (var i = 0; i < collections.length; i++) {
1447
- var collectionName = collections[i];
1448
- var coll = await this.database.getCollection(collectionName);
1449
- var docs = await coll.getAll();
923
+ },
924
+ '$text': (str, search) => typeof str === 'string' && str.toLowerCase().includes(search.toLowerCase())
925
+ };
926
+ }
1450
927
 
1451
- for (var j = 0; j < docs.length; j++) {
1452
- var doc = docs[j];
1453
- var updated = await migration.down(doc);
1454
- if (updated) {
1455
- await coll.update(doc._id, updated);
928
+ evaluate(doc, query) {
929
+ for (const key in query) {
930
+ const value = query[key];
931
+ if (key.startsWith('$')) {
932
+ // Logical operator at root level
933
+ const operator = this.operators[key];
934
+ if (!operator || !operator(doc, value)) return false;
935
+ } else {
936
+ // Field-level query
937
+ const fieldValue = this.getFieldValue(doc, key);
938
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
939
+ // Operator-based comparison
940
+ for (const op in value) {
941
+ if (op.startsWith('$')) {
942
+ const operatorFn = this.operators[op];
943
+ if (!operatorFn || !operatorFn(fieldValue, value[op])) {
944
+ return false;
945
+ }
946
+ }
1456
947
  }
948
+ } else {
949
+ // Direct equality comparison
950
+ if (fieldValue !== value) return false;
1457
951
  }
1458
952
  }
1459
953
  }
1460
-
1461
- compareVersions(a, b) {
1462
- var partsA = a.split('.').map(Number);
1463
- var partsB = b.split('.').map(Number);
1464
-
1465
- for (var i = 0; i < Math.max(partsA.length, partsB.length); i++) {
1466
- var partA = partsA[i] || 0;
1467
- var partB = partsB[i] || 0;
1468
-
1469
- if (partA > partB) return 1;
1470
- if (partA < partB) return -1;
1471
- }
1472
-
1473
- return 0;
1474
- }
954
+ return true;
1475
955
  }
1476
956
 
1477
- // ========================
1478
- // Performance Monitor
1479
- // ========================
1480
-
1481
- class PerformanceMonitor {
1482
- constructor() {
1483
- this.metrics = {
1484
- operations: [],
1485
- latencies: [],
1486
- cacheHits: 0,
1487
- cacheMisses: 0,
1488
- memoryUsage: []
1489
- };
1490
- this.monitoring = false;
1491
- this.monitoringInterval = null;
1492
- }
1493
-
1494
- startMonitoring() {
1495
- this.monitoring = true;
1496
- this.monitoringInterval = setInterval(this.collectMetrics.bind(this), 1000);
1497
- }
1498
-
1499
- stopMonitoring() {
1500
- this.monitoring = false;
1501
- if (this.monitoringInterval) {
1502
- clearInterval(this.monitoringInterval);
1503
- this.monitoringInterval = null;
1504
- }
1505
- }
1506
-
1507
- recordOperation(type, duration) {
1508
- if (!this.monitoring) return;
1509
-
1510
- this.metrics.operations.push({
1511
- type: type,
1512
- duration: duration,
1513
- timestamp: Date.now()
1514
- });
1515
-
1516
- this.metrics.latencies.push(duration);
1517
-
1518
- if (this.metrics.operations.length > 100) {
1519
- this.metrics.operations.shift();
1520
- }
1521
- if (this.metrics.latencies.length > 100) {
1522
- this.metrics.latencies.shift();
1523
- }
1524
- }
1525
-
1526
- recordCacheHit() {
1527
- this.metrics.cacheHits++;
1528
- }
1529
-
1530
- recordCacheMiss() {
1531
- this.metrics.cacheMisses++;
1532
- }
1533
-
1534
- collectMetrics() {
1535
- if (typeof performance !== 'undefined' && performance.memory) {
1536
- this.metrics.memoryUsage.push({
1537
- used: performance.memory.usedJSHeapSize,
1538
- total: performance.memory.totalJSHeapSize,
1539
- limit: performance.memory.jsHeapSizeLimit,
1540
- timestamp: Date.now()
1541
- });
1542
-
1543
- if (this.metrics.memoryUsage.length > 60) {
1544
- this.metrics.memoryUsage.shift();
1545
- }
1546
- }
1547
- }
1548
-
1549
- getStats() {
1550
- var opsPerSec = this.metrics.operations.filter(function(op) {
1551
- return Date.now() - op.timestamp < 1000;
1552
- }).length;
1553
-
1554
- var avgLatency = this.metrics.latencies.length > 0 ?
1555
- this.metrics.latencies.reduce(function(a, b) {
1556
- return a + b;
1557
- }, 0) / this.metrics.latencies.length :
1558
- 0;
1559
-
1560
- var cacheHitRate = this.metrics.cacheHits + this.metrics.cacheMisses > 0 ?
1561
- (this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses)) * 100 :
1562
- 0;
1563
-
1564
- var memoryUsage = this.metrics.memoryUsage.length > 0 ?
1565
- this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1].used / (1024 * 1024) :
1566
- 0;
1567
-
1568
- return {
1569
- opsPerSec: opsPerSec,
1570
- avgLatency: avgLatency.toFixed(2),
1571
- cacheHitRate: cacheHitRate.toFixed(1),
1572
- memoryUsage: memoryUsage.toFixed(2)
1573
- };
1574
- }
1575
-
1576
- getOptimizationTips() {
1577
- var tips = [];
1578
-
1579
- if (this.metrics.latencies.length > 0) {
1580
- var avgLatency = this.metrics.latencies.reduce(function(a, b) {
1581
- return a + b;
1582
- }, 0) / this.metrics.latencies.length;
1583
- if (avgLatency > 100) {
1584
- tips.push('High average latency detected. Consider enabling compression and indexing frequently queried fields.');
1585
- }
1586
- }
1587
-
1588
- var cacheHitRate = this.metrics.cacheHits + this.metrics.cacheMisses > 0 ?
1589
- (this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses)) * 100 :
1590
- 0;
1591
-
1592
- if (cacheHitRate < 50) {
1593
- tips.push('Low cache hit rate. Consider increasing cache size or optimizing query patterns.');
1594
- }
1595
-
1596
- if (this.metrics.memoryUsage.length > 10) {
1597
- var recent = this.metrics.memoryUsage.slice(-10);
1598
- var trend = recent[recent.length - 1].used - recent[0].used;
1599
- if (trend > 10 * 1024 * 1024) {
1600
- tips.push('Memory usage is increasing rapidly. Check for memory leaks or consider batch processing.');
1601
- }
1602
- }
1603
-
1604
- if (tips.length === 0) {
1605
- tips.push('Performance is optimal. No issues detected.');
957
+ getFieldValue(doc, path) {
958
+ // Replaced optional chaining with a loop for compatibility
959
+ let current = doc;
960
+ for (const part of path.split('.')) {
961
+ if (current === null || current === undefined) {
962
+ return undefined;
1606
963
  }
1607
-
1608
- return tips;
1609
- }
1610
- }
1611
-
1612
-
1613
- // ========================
1614
- // Collection Class
1615
- // ========================
1616
-
1617
- class Collection {
1618
- constructor(name, database) {
1619
- this.name = name;
1620
- this.database = database;
1621
- this.db = null;
1622
- this.metadata = null;
1623
- this.settings = database.settings;
1624
- this.indexedDB = new IndexedDBUtility();
1625
- this.opfs = new OPFSUtility();
1626
- this.cleanupInterval = null;
1627
- this.events = {
1628
- beforeAdd: [],
1629
- afterAdd: [],
1630
- beforeGet: [],
1631
- afterGet: [],
1632
- beforeUpdate: [],
1633
- afterUpdate: [],
1634
- beforeDelete: [],
1635
- afterDelete: []
1636
- };
1637
- this.queryCache = new Map();
1638
- this.cacheTimeout = 60000;
964
+ current = current[part];
1639
965
  }
1640
-
1641
- async init() {
1642
- var dbName = this.database.name + '_' + this.name;
1643
- this.db = await this.indexedDB.openDatabase(dbName, 1, function(db) {
1644
- if (!db.objectStoreNames.contains('documents')) {
1645
- var store = db.createObjectStore('documents', {
1646
- keyPath: '_id'
1647
- });
1648
- store.createIndex('created', '_created', {
1649
- unique: false
1650
- });
1651
- store.createIndex('modified', '_modified', {
1652
- unique: false
1653
- });
1654
- store.createIndex('permanent', '_permanent', {
1655
- unique: false
1656
- });
966
+ return current;
967
+ }
968
+ }
969
+ const queryEngine = new QueryEngine();
970
+
971
+
972
+ // ========================
973
+ // Aggregation Pipeline
974
+ // ========================
975
+
976
+ class AggregationPipeline {
977
+ constructor() {
978
+ this.stages = {
979
+ '$match': (docs, condition) => docs.filter(doc => queryEngine.evaluate(doc, condition)),
980
+
981
+ '$project': (docs, projection) => docs.map(doc => {
982
+ const projected = {};
983
+ for (const key in projection) {
984
+ const value = projection[key];
985
+ if (value === 1 || value === true) {
986
+ projected[key] = queryEngine.getFieldValue(doc, key);
987
+ } else if (typeof value === 'object') {
988
+ // Handle computed fields if necessary
989
+ } else if (typeof value === 'string' && value.startsWith('$')) {
990
+ projected[key] = queryEngine.getFieldValue(doc, value.substring(1));
991
+ }
1657
992
  }
1658
- });
1659
-
1660
- var metadataData = this.database.metadata.collections[this.name];
1661
- this.metadata = new CollectionMetadata(this.name, metadataData);
1662
-
1663
- if (this.settings.freeSpaceEvery > 0) {
1664
- this.cleanupInterval = setInterval(this.freeSpace.bind(this), this.settings.freeSpaceEvery);
1665
- }
1666
-
1667
- return this;
1668
- }
1669
-
1670
- async add(documentData, options) {
1671
- options = options || {};
1672
- await this.trigger('beforeAdd', documentData);
1673
-
1674
- var doc = new Document({
1675
- data: documentData,
1676
- _id: options.id
1677
- }, {
1678
- encrypted: options.encrypted || false,
1679
- compressed: options.compressed !== false,
1680
- permanent: options.permanent || false,
1681
- password: options.password
1682
- });
1683
-
1684
- if (options.attachments && options.attachments.length > 0) {
1685
- var attachmentPaths = await this.opfs.saveAttachments(
1686
- this.database.name,
1687
- this.name,
1688
- doc._id,
1689
- options.attachments
1690
- );
1691
- doc._attachments = attachmentPaths;
1692
- }
1693
-
1694
- await doc.pack();
1695
- var dbOutput = doc.databaseOutput();
993
+ // Handle exclusion projection
994
+ if (Object.values(projection).some(v => v === 0 || v === false)) {
995
+ const exclusions = Object.keys(projection).filter(k => projection[k] === 0 || projection[k] === false);
996
+ const included = { ...doc };
997
+ exclusions.forEach(key => delete included[key]);
998
+ return included;
999
+ }
1000
+ return projected;
1001
+ }),
1002
+
1003
+ '$sort': (docs, sortSpec) => [...docs].sort((a, b) => {
1004
+ for (const key in sortSpec) {
1005
+ const order = sortSpec[key]; // 1 for asc, -1 for desc
1006
+ const aVal = queryEngine.getFieldValue(a, key);
1007
+ const bVal = queryEngine.getFieldValue(b, key);
1008
+ if (aVal < bVal) return -order;
1009
+ if (aVal > bVal) return order;
1010
+ }
1011
+ return 0;
1012
+ }),
1696
1013
 
1697
- await this.indexedDB.add(this.db, 'documents', dbOutput);
1014
+ '$limit': (docs, limit) => docs.slice(0, limit),
1698
1015
 
1699
- var sizeKB = new Blob([dbOutput.packedData]).size / 1024;
1700
- this.metadata.addDocument(
1701
- doc._id,
1702
- sizeKB,
1703
- doc._permanent,
1704
- doc._attachments.length
1705
- );
1706
- this.database.metadata.updateCollection(this.metadata);
1016
+ '$skip': (docs, skip) => docs.slice(skip),
1707
1017
 
1708
- await this.checkSpaceLimit();
1709
- await this.trigger('afterAdd', doc);
1018
+ '$group': (docs, groupSpec) => {
1019
+ const groups = new Map();
1020
+ const idField = groupSpec._id;
1710
1021
 
1711
- this.queryCache.clear();
1022
+ for (const doc of docs) {
1023
+ const groupKey = typeof idField === 'string' ?
1024
+ queryEngine.getFieldValue(doc, idField.replace('$', '')) :
1025
+ JSON.stringify(idField); // Fallback for complex IDs
1712
1026
 
1713
- return doc._id;
1714
- }
1027
+ if (!groups.has(groupKey)) {
1028
+ groups.set(groupKey, { _id: groupKey, docs: [] });
1029
+ }
1030
+ groups.get(groupKey).docs.push(doc);
1031
+ }
1715
1032
 
1716
- async get(docId, options) {
1717
- options = options || {};
1718
- await this.trigger('beforeGet', docId);
1033
+ const results = [];
1034
+ for (const group of groups.values()) {
1035
+ const result = { _id: group._id };
1036
+ for (const fieldKey in groupSpec) {
1037
+ if (fieldKey === '_id') continue;
1038
+ const accumulator = groupSpec[fieldKey];
1039
+ const op = Object.keys(accumulator)[0];
1040
+ const field = accumulator[op].toString().replace('$', '');
1041
+
1042
+ switch(op) {
1043
+ case '$sum':
1044
+ result[fieldKey] = group.docs.reduce((sum, d) => sum + (queryEngine.getFieldValue(d, field) || 0), 0);
1045
+ break;
1046
+ case '$avg':
1047
+ const sum = group.docs.reduce((s, d) => s + (queryEngine.getFieldValue(d, field) || 0), 0);
1048
+ result[fieldKey] = sum / group.docs.length;
1049
+ break;
1050
+ case '$count':
1051
+ result[fieldKey] = group.docs.length;
1052
+ break;
1053
+ case '$max':
1054
+ result[fieldKey] = Math.max(...group.docs.map(d => queryEngine.getFieldValue(d, field)));
1055
+ break;
1056
+ case '$min':
1057
+ result[fieldKey] = Math.min(...group.docs.map(d => queryEngine.getFieldValue(d, field)));
1058
+ break;
1059
+ }
1060
+ }
1061
+ results.push(result);
1062
+ }
1063
+ return results;
1064
+ },
1065
+
1066
+ '$lookup': async (docs, lookupSpec, db) => {
1067
+ const foreignCollection = await db.getCollection(lookupSpec.from);
1068
+ const foreignDocs = await foreignCollection.getAll();
1069
+ const foreignMap = new Map();
1070
+ foreignDocs.forEach(doc => {
1071
+ const key = queryEngine.getFieldValue(doc, lookupSpec.foreignField);
1072
+ if (!foreignMap.has(key)) foreignMap.set(key, []);
1073
+ foreignMap.get(key).push(doc);
1074
+ });
1719
1075
 
1720
- var stored = await this.indexedDB.get(this.db, 'documents', docId);
1721
- if (!stored) {
1722
- throw new LacertaDBError('Document not found', 'DOCUMENT_NOT_FOUND');
1076
+ return docs.map(doc => {
1077
+ const localValue = queryEngine.getFieldValue(doc, lookupSpec.localField);
1078
+ return {
1079
+ ...doc,
1080
+ [lookupSpec.as]: foreignMap.get(localValue) || []
1081
+ };
1082
+ });
1723
1083
  }
1084
+ };
1085
+ }
1724
1086
 
1725
- var doc = new Document(stored, {
1726
- password: options.password,
1727
- encrypted: stored._encrypted,
1728
- compressed: stored._compressed
1729
- });
1087
+ async execute(docs, pipeline, db) {
1088
+ let result = docs;
1089
+ for (const stage of pipeline) {
1090
+ const stageName = Object.keys(stage)[0];
1091
+ const stageSpec = stage[stageName];
1092
+ const stageFunction = this.stages[stageName];
1730
1093
 
1731
- if (stored.packedData) {
1732
- await doc.unpack();
1094
+ if (!stageFunction) {
1095
+ throw new Error(`Unknown aggregation stage: ${stageName}`);
1733
1096
  }
1734
1097
 
1735
- if (options.includeAttachments && doc._attachments.length > 0) {
1736
- var attachments = await this.opfs.getAttachments(doc._attachments);
1737
- doc.data._attachments = attachments;
1098
+ if (stageName === '$lookup') {
1099
+ result = await stageFunction(result, stageSpec, db);
1100
+ } else {
1101
+ result = stageFunction(result, stageSpec);
1738
1102
  }
1103
+ }
1104
+ return result;
1105
+ }
1106
+ }
1107
+ const aggregationPipeline = new AggregationPipeline();
1108
+
1109
+ // ========================
1110
+ // Migration Manager
1111
+ // ========================
1112
+
1113
+ class MigrationManager {
1114
+ constructor(database) {
1115
+ this.database = database;
1116
+ this.migrations = [];
1117
+ this.currentVersion = this.loadVersion();
1118
+ }
1739
1119
 
1740
- await this.trigger('afterGet', doc);
1120
+ loadVersion() {
1121
+ return localStorage.getItem(`lacertadb_${this.database.name}_version`) || '1.0.0';
1122
+ }
1741
1123
 
1742
- return doc.objectOutput(options.includeAttachments);
1743
- }
1124
+ saveVersion(version) {
1125
+ localStorage.setItem(`lacertadb_${this.database.name}_version`, version);
1126
+ this.currentVersion = version;
1127
+ }
1744
1128
 
1745
- async getAll(options) {
1746
- options = options || {};
1747
- var stored = await this.indexedDB.getAll(
1748
- this.db,
1749
- 'documents',
1750
- undefined,
1751
- options.limit
1752
- );
1129
+ addMigration(migration) {
1130
+ this.migrations.push(migration);
1131
+ }
1753
1132
 
1754
- var documents = [];
1755
- for (var i = 0; i < stored.length; i++) {
1756
- var docData = stored[i];
1757
- try {
1758
- var doc = new Document(docData, {
1759
- password: options.password,
1760
- encrypted: docData._encrypted,
1761
- compressed: docData._compressed
1762
- });
1133
+ compareVersions(a, b) {
1134
+ const partsA = a.split('.').map(Number);
1135
+ const partsB = b.split('.').map(Number);
1136
+ const len = Math.max(partsA.length, partsB.length);
1763
1137
 
1764
- if (docData.packedData) {
1765
- await doc.unpack();
1766
- }
1138
+ for (let i = 0; i < len; i++) {
1139
+ const partA = partsA[i] || 0;
1140
+ const partB = partsB[i] || 0;
1141
+ if (partA > partB) return 1;
1142
+ if (partA < partB) return -1;
1143
+ }
1144
+ return 0;
1145
+ }
1767
1146
 
1768
- documents.push(doc.objectOutput());
1769
- } catch (error) {
1770
- console.error('Failed to unpack document:', error);
1771
- }
1772
- }
1147
+ async runMigrations(targetVersion) {
1148
+ const applicableMigrations = this.migrations
1149
+ .filter(m => this.compareVersions(m.version, this.currentVersion) > 0 &&
1150
+ this.compareVersions(m.version, targetVersion) <= 0)
1151
+ .sort((a, b) => this.compareVersions(a.version, b.version));
1773
1152
 
1774
- return documents;
1153
+ for (const migration of applicableMigrations) {
1154
+ await this._applyMigration(migration, 'up');
1155
+ this.saveVersion(migration.version);
1775
1156
  }
1157
+ }
1776
1158
 
1777
- async update(docId, updates, options) {
1778
- options = options || {};
1779
- await this.trigger('beforeUpdate', {
1780
- docId: docId,
1781
- updates: updates
1782
- });
1159
+ async rollback(targetVersion) {
1160
+ const applicableMigrations = this.migrations
1161
+ .filter(m => m.down &&
1162
+ this.compareVersions(m.version, targetVersion) > 0 &&
1163
+ this.compareVersions(m.version, this.currentVersion) <= 0)
1164
+ .sort((a, b) => this.compareVersions(b.version, a.version));
1783
1165
 
1784
- var existing = await this.get(docId, {
1785
- password: options.password
1786
- });
1166
+ for (const migration of applicableMigrations) {
1167
+ await this._applyMigration(migration, 'down');
1168
+ }
1169
+ this.saveVersion(targetVersion);
1170
+ }
1787
1171
 
1788
- var updatedData = {};
1789
- for (var key in existing) {
1790
- if (Object.prototype.hasOwnProperty.call(existing, key)) {
1791
- updatedData[key] = existing[key];
1172
+ async _applyMigration(migration, direction) {
1173
+ console.log(`${direction === 'up' ? 'Running' : 'Rolling back'} migration: ${migration.name} (v${migration.version})`);
1174
+ const collections = await this.database.listCollections();
1175
+ for (const collectionName of collections) {
1176
+ const coll = await this.database.getCollection(collectionName);
1177
+ const docs = await coll.getAll();
1178
+ for (const doc of docs) {
1179
+ const updated = await migration[direction](doc);
1180
+ if (updated) {
1181
+ await coll.update(doc._id, updated);
1792
1182
  }
1793
1183
  }
1794
- for (var updateKey in updates) {
1795
- if (Object.prototype.hasOwnProperty.call(updates, updateKey)) {
1796
- updatedData[updateKey] = updates[updateKey];
1797
- }
1798
- }
1799
- delete updatedData._id;
1800
- delete updatedData._created;
1801
- delete updatedData._modified;
1802
- delete updatedData._permanent;
1803
-
1804
- var doc = new Document({
1805
- _id: docId,
1806
- _created: existing._created,
1807
- data: updatedData
1808
- }, {
1809
- encrypted: options.encrypted || false,
1810
- compressed: options.compressed !== false,
1811
- permanent: options.permanent || existing._permanent,
1812
- password: options.password
1813
- });
1814
-
1815
- if (options.attachments && options.attachments.length > 0) {
1816
- await this.opfs.deleteAttachments(this.database.name, this.name, docId);
1817
- var attachmentPaths = await this.opfs.saveAttachments(
1818
- this.database.name,
1819
- this.name,
1820
- doc._id,
1821
- options.attachments
1822
- );
1823
- doc._attachments = attachmentPaths;
1824
- }
1825
-
1826
- await doc.pack();
1827
- var dbOutput = doc.databaseOutput();
1184
+ }
1185
+ }
1186
+ }
1187
+
1188
+ // ========================
1189
+ // Performance Monitor
1190
+ // ========================
1191
+
1192
+ class PerformanceMonitor {
1193
+ constructor() {
1194
+ this.metrics = {
1195
+ operations: [],
1196
+ latencies: [],
1197
+ cacheHits: 0,
1198
+ cacheMisses: 0,
1199
+ memoryUsage: []
1200
+ };
1201
+ this.monitoring = false;
1202
+ this.monitoringInterval = null;
1203
+ }
1828
1204
 
1829
- await this.indexedDB.put(this.db, 'documents', dbOutput);
1205
+ startMonitoring() {
1206
+ if (this.monitoring) return;
1207
+ this.monitoring = true;
1208
+ this.monitoringInterval = setInterval(() => this.collectMetrics(), 1000);
1209
+ }
1830
1210
 
1831
- var sizeKB = new Blob([dbOutput.packedData]).size / 1024;
1832
- this.metadata.updateDocument(
1833
- doc._id,
1834
- sizeKB,
1835
- doc._permanent,
1836
- doc._attachments.length
1837
- );
1838
- this.database.metadata.updateCollection(this.metadata);
1211
+ stopMonitoring() {
1212
+ if (!this.monitoring) return;
1213
+ this.monitoring = false;
1214
+ clearInterval(this.monitoringInterval);
1215
+ this.monitoringInterval = null;
1216
+ }
1839
1217
 
1840
- await this.trigger('afterUpdate', doc);
1218
+ recordOperation(type, duration) {
1219
+ if (!this.monitoring) return;
1220
+ this.metrics.operations.push({ type, duration, timestamp: Date.now() });
1221
+ this.metrics.latencies.push(duration);
1222
+ if (this.metrics.operations.length > 100) this.metrics.operations.shift();
1223
+ if (this.metrics.latencies.length > 100) this.metrics.latencies.shift();
1224
+ }
1841
1225
 
1842
- this.queryCache.clear();
1226
+ recordCacheHit() { this.metrics.cacheHits++; }
1227
+ recordCacheMiss() { this.metrics.cacheMisses++; }
1843
1228
 
1844
- return doc._id;
1229
+ collectMetrics() {
1230
+ // Replaced optional chaining with `&&` for compatibility
1231
+ if (performance && performance.memory) {
1232
+ this.metrics.memoryUsage.push({
1233
+ used: performance.memory.usedJSHeapSize,
1234
+ total: performance.memory.totalJSHeapSize,
1235
+ limit: performance.memory.jsHeapSizeLimit,
1236
+ timestamp: Date.now()
1237
+ });
1238
+ if (this.metrics.memoryUsage.length > 60) this.metrics.memoryUsage.shift();
1845
1239
  }
1240
+ }
1846
1241
 
1847
- async delete(docId) {
1848
- await this.trigger('beforeDelete', docId);
1242
+ getStats() {
1243
+ const opsPerSec = this.metrics.operations.filter(op => Date.now() - op.timestamp < 1000).length;
1244
+ const totalLatency = this.metrics.latencies.reduce((a, b) => a + b, 0);
1245
+ const avgLatency = this.metrics.latencies.length > 0 ? totalLatency / this.metrics.latencies.length : 0;
1246
+ const totalCacheOps = this.metrics.cacheHits + this.metrics.cacheMisses;
1247
+ const cacheHitRate = totalCacheOps > 0 ? (this.metrics.cacheHits / totalCacheOps) * 100 : 0;
1248
+
1249
+ // Replaced `.at(-1)` with classic index access for compatibility
1250
+ const latestMemory = this.metrics.memoryUsage.length > 0 ? this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1] : null;
1251
+ const memoryUsageMB = latestMemory ? latestMemory.used / (1024 * 1024) : 0;
1252
+
1253
+ return {
1254
+ opsPerSec,
1255
+ avgLatency: avgLatency.toFixed(2),
1256
+ cacheHitRate: cacheHitRate.toFixed(1),
1257
+ memoryUsageMB: memoryUsageMB.toFixed(2)
1258
+ };
1259
+ }
1849
1260
 
1850
- var doc = await this.indexedDB.get(this.db, 'documents', docId);
1851
- if (!doc) {
1852
- throw new LacertaDBError('Document not found', 'DOCUMENT_NOT_FOUND');
1853
- }
1261
+ getOptimizationTips() {
1262
+ const tips = [];
1263
+ const stats = this.getStats();
1854
1264
 
1855
- if (doc._permanent) {
1856
- throw new LacertaDBError('Cannot delete permanent document', 'INVALID_OPERATION');
1265
+ if (stats.avgLatency > 100) {
1266
+ tips.push('High average latency detected. Consider enabling compression and indexing frequently queried fields.');
1267
+ }
1268
+ if (stats.cacheHitRate < 50 && (this.metrics.cacheHits + this.metrics.cacheMisses) > 20) {
1269
+ tips.push('Low cache hit rate. Consider increasing cache size or optimizing query patterns.');
1270
+ }
1271
+ if (this.metrics.memoryUsage.length > 10) {
1272
+ const recent = this.metrics.memoryUsage.slice(-10);
1273
+ const trend = recent[recent.length - 1].used - recent[0].used;
1274
+ if (trend > 10 * 1024 * 1024) { // > 10MB increase
1275
+ tips.push('Memory usage is increasing rapidly. Check for memory leaks or consider batch processing.');
1857
1276
  }
1277
+ }
1278
+ return tips.length > 0 ? tips : ['Performance is optimal. No issues detected.'];
1279
+ }
1280
+ }
1281
+
1282
+ // ========================
1283
+ // Collection Class
1284
+ // ========================
1285
+
1286
+ class Collection {
1287
+ constructor(name, database) {
1288
+ this.name = name;
1289
+ this.database = database;
1290
+ this.db = null;
1291
+ this.metadata = null;
1292
+ this.settings = database.settings;
1293
+ this.indexedDB = new IndexedDBUtility();
1294
+ this.opfs = new OPFSUtility();
1295
+ this.cleanupInterval = null;
1296
+ this.events = new Map();
1297
+ this.queryCache = new Map();
1298
+ this.cacheTimeout = 60000;
1299
+ this.performanceMonitor = database.performanceMonitor;
1300
+ }
1858
1301
 
1859
- await this.indexedDB.delete(this.db, 'documents', docId);
1860
-
1861
- if (doc._attachments && doc._attachments.length > 0) {
1862
- await this.opfs.deleteAttachments(this.database.name, this.name, docId);
1302
+ async init() {
1303
+ const dbName = `${this.database.name}_${this.name}`;
1304
+ this.db = await this.indexedDB.openDatabase(dbName, 1, (db, oldVersion) => {
1305
+ if (oldVersion < 1 && !db.objectStoreNames.contains('documents')) {
1306
+ const store = db.createObjectStore('documents', { keyPath: '_id' });
1307
+ store.createIndex('modified', '_modified', { unique: false });
1863
1308
  }
1309
+ // Future index creation logic would go here during version bumps
1310
+ });
1864
1311
 
1865
- this.metadata.removeDocument(docId);
1866
- this.database.metadata.updateCollection(this.metadata);
1312
+ const metadataData = this.database.metadata.collections[this.name];
1313
+ this.metadata = new CollectionMetadata(this.name, metadataData);
1867
1314
 
1868
- await this.trigger('afterDelete', docId);
1869
-
1870
- this.queryCache.clear();
1315
+ if (this.settings.freeSpaceEvery > 0) {
1316
+ this.cleanupInterval = setInterval(() => this.freeSpace(), this.settings.freeSpaceEvery);
1871
1317
  }
1318
+ return this;
1319
+ }
1872
1320
 
1873
- async query(filter, options) {
1874
- filter = filter || {};
1875
- options = options || {};
1876
-
1877
- var cacheKeyData = {
1878
- filter: filter,
1879
- options: options
1880
- };
1881
- var serializedKey = serializer.serialize(cacheKeyData);
1882
- var cacheKey = base64.encode(serializedKey);
1883
-
1884
- var cached = this.queryCache.get(cacheKey);
1885
-
1886
- if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
1887
- if (global.performanceMonitor) {
1888
- global.performanceMonitor.recordCacheHit();
1889
- }
1890
- return cached.data;
1891
- }
1321
+ async add(documentData, options = {}) {
1322
+ await this.trigger('beforeAdd', documentData);
1323
+
1324
+ const doc = new Document({ data: documentData, _id: options.id }, {
1325
+ encrypted: options.encrypted || false,
1326
+ compressed: options.compressed !== false,
1327
+ permanent: options.permanent || false,
1328
+ password: options.password
1329
+ });
1330
+
1331
+ const attachments = options.attachments;
1332
+ if (attachments && attachments.length > 0) {
1333
+ const preparedAttachments = await Promise.all(
1334
+ attachments.map(att => (att instanceof File || att instanceof Blob) ?
1335
+ OPFSUtility.prepareAttachment(att, att.name) :
1336
+ Promise.resolve(att))
1337
+ );
1338
+ doc._attachments = await this.opfs.saveAttachments(this.database.name, this.name, doc._id, preparedAttachments);
1339
+ }
1892
1340
 
1893
- if (global.performanceMonitor) {
1894
- global.performanceMonitor.recordCacheMiss();
1895
- }
1341
+ await doc.pack();
1342
+ const dbOutput = doc.databaseOutput();
1343
+ await this.indexedDB.add(this.db, 'documents', dbOutput);
1896
1344
 
1897
- var startTime = performance.now();
1345
+ const sizeKB = dbOutput.packedData.byteLength / 1024;
1346
+ this.metadata.addDocument(doc._id, sizeKB, doc._permanent, doc._attachments.length);
1347
+ this.database.metadata.setCollection(this.metadata);
1898
1348
 
1899
- var allDocs = await this.getAll(options);
1900
- var filtered = allDocs.filter(function(doc) {
1901
- return QueryOperators.evaluate(doc, filter);
1902
- });
1349
+ await this.checkSpaceLimit();
1350
+ await this.trigger('afterAdd', doc);
1351
+ this.queryCache.clear();
1352
+ return doc._id;
1353
+ }
1903
1354
 
1904
- var result = filtered;
1905
- if (options.projection) {
1906
- result = AggregationPipeline.stages.$project(filtered, options.projection);
1907
- }
1908
- if (options.sort) {
1909
- result = AggregationPipeline.stages.$sort(result, options.sort);
1910
- }
1911
- if (options.skip) {
1912
- result = AggregationPipeline.stages.$skip(result, options.skip);
1913
- }
1914
- if (options.limit) {
1915
- result = AggregationPipeline.stages.$limit(result, options.limit);
1916
- }
1355
+ async get(docId, options = {}) {
1356
+ await this.trigger('beforeGet', docId);
1917
1357
 
1918
- var duration = performance.now() - startTime;
1919
- if (global.performanceMonitor) {
1920
- global.performanceMonitor.recordOperation('query', duration);
1921
- }
1358
+ const stored = await this.indexedDB.get(this.db, 'documents', docId);
1359
+ if (!stored) {
1360
+ throw new LacertaDBError(`Document with id '${docId}' not found.`, 'DOCUMENT_NOT_FOUND');
1361
+ }
1922
1362
 
1923
- this.queryCache.set(cacheKey, {
1924
- data: result,
1925
- timestamp: Date.now()
1926
- });
1363
+ const doc = new Document(stored, {
1364
+ password: options.password,
1365
+ encrypted: stored._encrypted,
1366
+ compressed: stored._compressed
1367
+ });
1927
1368
 
1928
- if (this.queryCache.size > 100) {
1929
- var oldestKey = this.queryCache.keys().next().value;
1930
- this.queryCache.delete(oldestKey);
1931
- }
1369
+ if (stored.packedData) {
1370
+ await doc.unpack();
1371
+ }
1932
1372
 
1933
- return result;
1373
+ if (options.includeAttachments && doc._attachments.length > 0) {
1374
+ doc.data._attachments = await this.opfs.getAttachments(doc._attachments);
1934
1375
  }
1935
1376
 
1936
- async aggregate(pipeline) {
1937
- var startTime = performance.now();
1938
- var docs = await this.getAll();
1939
- var result = await AggregationPipeline.execute(docs, pipeline, this.database);
1377
+ await this.trigger('afterGet', doc);
1378
+ return doc.objectOutput(options.includeAttachments);
1379
+ }
1940
1380
 
1941
- var duration = performance.now() - startTime;
1942
- if (global.performanceMonitor) {
1943
- global.performanceMonitor.recordOperation('aggregate', duration);
1381
+ async getAll(options = {}) {
1382
+ const stored = await this.indexedDB.getAll(this.db, 'documents', undefined, options.limit);
1383
+ return Promise.all(stored.map(async docData => {
1384
+ try {
1385
+ const doc = new Document(docData, {
1386
+ password: options.password,
1387
+ encrypted: docData._encrypted,
1388
+ compressed: docData._compressed
1389
+ });
1390
+ if (docData.packedData) {
1391
+ await doc.unpack();
1392
+ }
1393
+ return doc.objectOutput();
1394
+ } catch (error) {
1395
+ console.error(`Failed to unpack document ${docData._id}:`, error);
1396
+ return null;
1944
1397
  }
1398
+ })).then(docs => docs.filter(Boolean));
1399
+ }
1945
1400
 
1946
- return result;
1401
+ async update(docId, updates, options = {}) {
1402
+ await this.trigger('beforeUpdate', { docId, updates });
1403
+
1404
+ const stored = await this.indexedDB.get(this.db, 'documents', docId);
1405
+ if (!stored) {
1406
+ throw new LacertaDBError(`Document with id '${docId}' not found for update.`, 'DOCUMENT_NOT_FOUND');
1407
+ }
1408
+
1409
+ const existingDoc = new Document(stored, { password: options.password });
1410
+ if (stored.packedData) await existingDoc.unpack();
1411
+
1412
+ const updatedData = { ...existingDoc.data, ...updates };
1413
+
1414
+ // Replaced `??` with ternary operator for compatibility
1415
+ const doc = new Document({
1416
+ _id: docId,
1417
+ _created: stored._created,
1418
+ data: updatedData
1419
+ }, {
1420
+ encrypted: options.encrypted !== undefined ? options.encrypted : stored._encrypted,
1421
+ compressed: options.compressed !== undefined ? options.compressed : stored._compressed,
1422
+ permanent: options.permanent !== undefined ? options.permanent : stored._permanent,
1423
+ password: options.password
1424
+ });
1425
+ doc._modified = Date.now();
1426
+
1427
+ const attachments = options.attachments;
1428
+ if (attachments && attachments.length > 0) {
1429
+ await this.opfs.deleteAttachments(this.database.name, this.name, docId);
1430
+ const preparedAttachments = await Promise.all(
1431
+ attachments.map(att => (att instanceof File || att instanceof Blob) ?
1432
+ OPFSUtility.prepareAttachment(att, att.name) :
1433
+ Promise.resolve(att))
1434
+ );
1435
+ doc._attachments = await this.opfs.saveAttachments(this.database.name, this.name, doc._id, preparedAttachments);
1436
+ } else {
1437
+ doc._attachments = stored._attachments;
1947
1438
  }
1948
1439
 
1949
- async batchAdd(documents, options) {
1950
- options = options || {};
1951
- var results = [];
1952
- var startTime = performance.now();
1953
-
1954
- for (var i = 0; i < documents.length; i++) {
1955
- var doc = documents[i];
1956
- try {
1957
- var id = await this.add(doc, options);
1958
- results.push({
1959
- success: true,
1960
- id: id
1961
- });
1962
- } catch (error) {
1963
- results.push({
1964
- success: false,
1965
- error: error.message
1966
- });
1967
- }
1968
- }
1440
+ await doc.pack();
1441
+ const dbOutput = doc.databaseOutput();
1442
+ await this.indexedDB.put(this.db, 'documents', dbOutput);
1969
1443
 
1970
- var duration = performance.now() - startTime;
1971
- if (global.performanceMonitor) {
1972
- global.performanceMonitor.recordOperation('batchAdd', duration);
1973
- }
1444
+ const sizeKB = dbOutput.packedData.byteLength / 1024;
1445
+ this.metadata.updateDocument(doc._id, sizeKB, doc._permanent, doc._attachments.length);
1446
+ this.database.metadata.setCollection(this.metadata);
1974
1447
 
1975
- return results;
1976
- }
1448
+ await this.trigger('afterUpdate', doc);
1449
+ this.queryCache.clear();
1450
+ return doc._id;
1451
+ }
1977
1452
 
1978
- async batchUpdate(updates, options) {
1979
- options = options || {};
1980
- var results = [];
1453
+ async delete(docId) {
1454
+ await this.trigger('beforeDelete', docId);
1981
1455
 
1982
- for (var i = 0; i < updates.length; i++) {
1983
- var update = updates[i];
1984
- try {
1985
- await this.update(update.id, update.data, options);
1986
- results.push({
1987
- success: true,
1988
- id: update.id
1989
- });
1990
- } catch (error) {
1991
- results.push({
1992
- success: false,
1993
- id: update.id,
1994
- error: error.message
1995
- });
1996
- }
1997
- }
1456
+ const doc = await this.indexedDB.get(this.db, 'documents', docId);
1457
+ if (!doc) throw new LacertaDBError('Document not found for deletion', 'DOCUMENT_NOT_FOUND');
1458
+ if (doc._permanent) throw new LacertaDBError('Cannot delete a permanent document', 'INVALID_OPERATION');
1998
1459
 
1999
- return results;
1460
+ await this.indexedDB.delete(this.db, 'documents', docId);
1461
+ const attachments = doc._attachments;
1462
+ if (attachments && attachments.length > 0) {
1463
+ await this.opfs.deleteAttachments(this.database.name, this.name, docId);
2000
1464
  }
2001
1465
 
2002
- async batchDelete(ids) {
2003
- var results = [];
1466
+ this.metadata.removeDocument(docId);
1467
+ this.database.metadata.setCollection(this.metadata);
2004
1468
 
2005
- for (var i = 0; i < ids.length; i++) {
2006
- var id = ids[i];
2007
- try {
2008
- await this.delete(id);
2009
- results.push({
2010
- success: true,
2011
- id: id
2012
- });
2013
- } catch (error) {
2014
- results.push({
2015
- success: false,
2016
- id: id,
2017
- error: error.message
2018
- });
2019
- }
2020
- }
1469
+ await this.trigger('afterDelete', docId);
1470
+ this.queryCache.clear();
1471
+ }
2021
1472
 
2022
- return results;
2023
- }
1473
+ async query(filter = {}, options = {}) {
1474
+ const startTime = performance.now();
1475
+ const cacheKey = base64.encode(serializer.serialize({ filter, options }));
1476
+ const cached = this.queryCache.get(cacheKey);
2024
1477
 
2025
- async clear() {
2026
- await this.indexedDB.clear(this.db, 'documents');
2027
- this.metadata = new CollectionMetadata(this.name);
2028
- this.database.metadata.updateCollection(this.metadata);
2029
- this.queryCache.clear();
1478
+ if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
1479
+ if (this.performanceMonitor) this.performanceMonitor.recordCacheHit();
1480
+ return cached.data;
2030
1481
  }
1482
+ if (this.performanceMonitor) this.performanceMonitor.recordCacheMiss();
2031
1483
 
2032
- async checkSpaceLimit() {
2033
- if (this.settings.sizeLimitKB !== Infinity &&
2034
- this.metadata.sizeKB > this.settings.bufferLimitKB) {
2035
- await this.freeSpace();
2036
- }
1484
+ let results = await this.getAll(options);
1485
+ if (Object.keys(filter).length > 0) {
1486
+ results = results.filter(doc => queryEngine.evaluate(doc, filter));
2037
1487
  }
2038
1488
 
2039
- async freeSpace() {
2040
- var targetSize = this.settings.bufferLimitKB * 0.8;
1489
+ if (options.sort) results = aggregationPipeline.stages.$sort(results, options.sort);
1490
+ if (options.skip) results = aggregationPipeline.stages.$skip(results, options.skip);
1491
+ if (options.limit) results = aggregationPipeline.stages.$limit(results, options.limit);
1492
+ if (options.projection) results = aggregationPipeline.stages.$project(results, options.projection);
2041
1493
 
2042
- while (this.metadata.sizeKB > targetSize) {
2043
- var oldestDocs = this.metadata.getOldestNonPermanentDocuments(10);
2044
- if (oldestDocs.length === 0) break;
1494
+ if (this.performanceMonitor) this.performanceMonitor.recordOperation('query', performance.now() - startTime);
2045
1495
 
2046
- for (var i = 0; i < oldestDocs.length; i++) {
2047
- var docId = oldestDocs[i];
2048
- try {
2049
- await this.delete(docId);
2050
- } catch (error) {
2051
- console.error('Failed to delete document during cleanup:', error);
2052
- }
2053
- }
2054
- }
1496
+ this.queryCache.set(cacheKey, { data: results, timestamp: Date.now() });
1497
+ if (this.queryCache.size > 100) {
1498
+ this.queryCache.delete(this.queryCache.keys().next().value);
2055
1499
  }
1500
+ return results;
1501
+ }
2056
1502
 
2057
- createIndex(fieldName, options) {
2058
- console.log('Index for ' + fieldName + ' would be created on next database upgrade');
2059
- }
1503
+ async aggregate(pipeline) {
1504
+ const startTime = performance.now();
1505
+ const docs = await this.getAll();
1506
+ const result = await aggregationPipeline.execute(docs, pipeline, this.database);
1507
+ if (this.performanceMonitor) this.performanceMonitor.recordOperation('aggregate', performance.now() - startTime);
1508
+ return result;
1509
+ }
2060
1510
 
2061
- on(event, callback) {
2062
- if (this.events[event]) {
2063
- this.events[event].push(callback);
2064
- }
2065
- }
1511
+ async batchAdd(documents, options) {
1512
+ const startTime = performance.now();
1513
+ const results = await Promise.all(documents.map(doc =>
1514
+ this.add(doc, options)
1515
+ .then(id => ({ success: true, id }))
1516
+ .catch(error => ({ success: false, error: error.message }))
1517
+ ));
1518
+ if (this.performanceMonitor) this.performanceMonitor.recordOperation('batchAdd', performance.now() - startTime);
1519
+ return results;
1520
+ }
2066
1521
 
2067
- off(event, callback) {
2068
- if (this.events[event]) {
2069
- this.events[event] = this.events[event].filter(function(cb) {
2070
- return cb !== callback;
2071
- });
2072
- }
2073
- }
1522
+ batchUpdate(updates, options) {
1523
+ return Promise.all(updates.map(update =>
1524
+ this.update(update.id, update.data, options)
1525
+ .then(id => ({ success: true, id }))
1526
+ .catch(error => ({ success: false, id: update.id, error: error.message }))
1527
+ ));
1528
+ }
2074
1529
 
2075
- async trigger(event, data) {
2076
- if (this.events[event]) {
2077
- var callbacks = this.events[event];
2078
- for (var i = 0; i < callbacks.length; i++) {
2079
- await callbacks[i](data);
2080
- }
2081
- }
2082
- }
1530
+ batchDelete(ids) {
1531
+ return Promise.all(ids.map(id =>
1532
+ this.delete(id)
1533
+ .then(() => ({ success: true, id }))
1534
+ .catch(error => ({ success: false, id, error: error.message }))
1535
+ ));
1536
+ }
1537
+
1538
+ async clear() {
1539
+ await this.indexedDB.clear(this.db, 'documents');
1540
+ this.metadata = new CollectionMetadata(this.name);
1541
+ this.database.metadata.setCollection(this.metadata);
1542
+ this.queryCache.clear();
1543
+ }
2083
1544
 
2084
- clearCache() {
2085
- this.queryCache.clear();
1545
+ async checkSpaceLimit() {
1546
+ if (this.settings.sizeLimitKB !== Infinity && this.metadata.sizeKB > this.settings.bufferLimitKB) {
1547
+ await this.freeSpace();
2086
1548
  }
1549
+ }
2087
1550
 
2088
- destroy() {
2089
- if (this.cleanupInterval) {
2090
- clearInterval(this.cleanupInterval);
2091
- this.cleanupInterval = null;
2092
- }
2093
- if (this.db) {
2094
- this.db.close();
2095
- this.db = null;
2096
- }
1551
+ async freeSpace() {
1552
+ const targetSize = this.settings.bufferLimitKB * 0.8;
1553
+ while (this.metadata.sizeKB > targetSize) {
1554
+ const oldestDocs = this.metadata.getOldestNonPermanentDocuments(10);
1555
+ if (oldestDocs.length === 0) break;
1556
+ await this.batchDelete(oldestDocs);
2097
1557
  }
2098
1558
  }
2099
1559
 
1560
+ on(event, callback) {
1561
+ if (!this.events.has(event)) this.events.set(event, []);
1562
+ this.events.get(event).push(callback);
1563
+ }
2100
1564
 
2101
- // ========================
2102
- // Database Class
2103
- // ========================
1565
+ off(event, callback) {
1566
+ if (!this.events.has(event)) return;
1567
+ const listeners = this.events.get(event).filter(cb => cb !== callback);
1568
+ this.events.set(event, listeners);
1569
+ }
2104
1570
 
2105
- class Database {
2106
- constructor(name) {
2107
- this.name = name;
2108
- this.collections = new Map();
2109
- this.metadata = null;
2110
- this.settings = null;
2111
- this.quickStore = null;
1571
+ async trigger(event, data) {
1572
+ if (!this.events.has(event)) return;
1573
+ for (const callback of this.events.get(event)) {
1574
+ await callback(data);
2112
1575
  }
1576
+ }
2113
1577
 
2114
- async init() {
2115
- this.metadata = DatabaseMetadata.load(this.name);
2116
- this.settings = Settings.load(this.name);
2117
- this.quickStore = new QuickStore(this.name);
2118
- return this;
2119
- }
1578
+ clearCache() { this.queryCache.clear(); }
2120
1579
 
2121
- async createCollection(name, options) {
2122
- if (this.collections.has(name)) {
2123
- throw new LacertaDBError('Collection already exists', 'COLLECTION_EXISTS');
2124
- }
1580
+ destroy() {
1581
+ clearInterval(this.cleanupInterval);
1582
+ if (this.db) {
1583
+ this.db.close();
1584
+ }
1585
+ }
1586
+ }
1587
+
1588
+ // ========================
1589
+ // Database Class
1590
+ // ========================
1591
+
1592
+ class Database {
1593
+ constructor(name, performanceMonitor) {
1594
+ this.name = name;
1595
+ this.collections = new Map();
1596
+ this.metadata = null;
1597
+ this.settings = null;
1598
+ this.quickStore = null;
1599
+ this.performanceMonitor = performanceMonitor;
1600
+ }
2125
1601
 
2126
- var collection = new Collection(name, this);
2127
- await collection.init();
1602
+ async init() {
1603
+ this.metadata = DatabaseMetadata.load(this.name);
1604
+ this.settings = Settings.load(this.name);
1605
+ this.quickStore = new QuickStore(this.name);
1606
+ return this;
1607
+ }
2128
1608
 
2129
- this.collections.set(name, collection);
1609
+ async createCollection(name, options) {
1610
+ if (this.collections.has(name)) {
1611
+ throw new LacertaDBError(`Collection '${name}' already exists.`, 'COLLECTION_EXISTS');
1612
+ }
2130
1613
 
2131
- if (!this.metadata.collections[name]) {
2132
- var collMetadata = new CollectionMetadata(name);
2133
- this.metadata.addCollection(collMetadata);
2134
- }
1614
+ const collection = new Collection(name, this);
1615
+ await collection.init();
1616
+ this.collections.set(name, collection);
2135
1617
 
2136
- return collection;
1618
+ if (!this.metadata.collections[name]) {
1619
+ this.metadata.setCollection(new CollectionMetadata(name));
2137
1620
  }
1621
+ return collection;
1622
+ }
2138
1623
 
2139
- async getCollection(name) {
2140
- if (!this.collections.has(name)) {
2141
- if (this.metadata.collections[name]) {
2142
- var collection = new Collection(name, this);
2143
- await collection.init();
2144
- this.collections.set(name, collection);
2145
- } else {
2146
- throw new LacertaDBError('Collection not found', 'COLLECTION_NOT_FOUND');
2147
- }
2148
- }
2149
-
1624
+ async getCollection(name) {
1625
+ if (this.collections.has(name)) {
2150
1626
  return this.collections.get(name);
2151
1627
  }
1628
+ if (this.metadata.collections[name]) {
1629
+ const collection = new Collection(name, this);
1630
+ await collection.init();
1631
+ this.collections.set(name, collection);
1632
+ return collection;
1633
+ }
1634
+ throw new LacertaDBError(`Collection '${name}' not found.`, 'COLLECTION_NOT_FOUND');
1635
+ }
2152
1636
 
2153
- async dropCollection(name) {
2154
- var collection = await this.getCollection(name);
1637
+ async dropCollection(name) {
1638
+ if (this.collections.has(name)) {
1639
+ const collection = this.collections.get(name);
2155
1640
  await collection.clear();
2156
1641
  collection.destroy();
2157
-
2158
1642
  this.collections.delete(name);
2159
- this.metadata.removeCollection(name);
2160
-
2161
- var dbName = this.name + '_' + name;
2162
- await new Promise(function(resolve, reject) {
2163
- var deleteReq = indexedDB.deleteDatabase(dbName);
2164
- deleteReq.onsuccess = resolve;
2165
- deleteReq.onerror = reject;
2166
- });
2167
- }
2168
-
2169
- async listCollections() {
2170
- return Object.keys(this.metadata.collections);
2171
1643
  }
2172
1644
 
2173
- getStats() {
2174
- var collectionStats = [];
2175
- var collectionsMeta = this.metadata.collections;
2176
- for (var name in collectionsMeta) {
2177
- if (Object.prototype.hasOwnProperty.call(collectionsMeta, name)) {
2178
- var data = collectionsMeta[name];
2179
- collectionStats.push({
2180
- name: name,
2181
- sizeKB: data.sizeKB,
2182
- documents: data.length,
2183
- createdAt: new Date(data.createdAt).toISOString(),
2184
- modifiedAt: new Date(data.modifiedAt).toISOString()
2185
- });
2186
- }
2187
- }
1645
+ this.metadata.removeCollection(name);
2188
1646
 
2189
- return {
2190
- name: this.name,
2191
- totalSizeKB: this.metadata.totalSizeKB,
2192
- totalDocuments: this.metadata.totalLength,
2193
- collections: collectionStats
2194
- };
2195
- }
1647
+ const dbName = `${this.name}_${name}`;
1648
+ await new Promise((resolve, reject) => {
1649
+ const deleteReq = indexedDB.deleteDatabase(dbName);
1650
+ deleteReq.onsuccess = resolve;
1651
+ deleteReq.onerror = reject;
1652
+ deleteReq.onblocked = () => console.warn(`Deletion of '${dbName}' is blocked.`);
1653
+ });
1654
+ }
2196
1655
 
2197
- updateSettings(newSettings) {
2198
- this.settings.updateSettings(newSettings);
2199
- }
1656
+ listCollections() {
1657
+ return Object.keys(this.metadata.collections);
1658
+ }
2200
1659
 
2201
- async export (format, password) {
2202
- format = format || 'json';
2203
- password = password || null;
2204
- var data = {
2205
- version: '4.0.0',
2206
- database: this.name,
2207
- timestamp: Date.now(),
2208
- collections: {}
2209
- };
1660
+ getStats() {
1661
+ return {
1662
+ name: this.name,
1663
+ totalSizeKB: this.metadata.totalSizeKB,
1664
+ totalDocuments: this.metadata.totalLength,
1665
+ collections: Object.entries(this.metadata.collections).map(([name, data]) => ({
1666
+ name,
1667
+ sizeKB: data.sizeKB,
1668
+ documents: data.length,
1669
+ createdAt: new Date(data.createdAt).toISOString(),
1670
+ modifiedAt: new Date(data.modifiedAt).toISOString()
1671
+ }))
1672
+ };
1673
+ }
2210
1674
 
2211
- var collectionNames = await this.listCollections();
2212
- for (var i = 0; i < collectionNames.length; i++) {
2213
- var collName = collectionNames[i];
2214
- var collection = await this.getCollection(collName);
2215
- var docs = await collection.getAll({
2216
- password: password
2217
- });
2218
- data.collections[collName] = docs;
2219
- }
1675
+ updateSettings(newSettings) { this.settings.updateSettings(newSettings); }
2220
1676
 
2221
- if (format === 'json') {
2222
- var serialized = serializer.serialize(data);
2223
- return base64.encode(serialized);
2224
- } else if (format === 'encrypted' && password) {
2225
- var encryption = new BrowserEncryptionUtility();
2226
- var serializedData = serializer.serialize(data);
2227
- var encrypted = await encryption.encrypt(serializedData, password);
2228
- return base64.encode(encrypted);
2229
- } else if (format === 'csv') {
2230
- var csv = '';
2231
- for (var collName in data.collections) {
2232
- if (Object.prototype.hasOwnProperty.call(data.collections, collName)) {
2233
- var docs = data.collections[collName];
2234
- if (docs.length > 0) {
2235
- var keys = Object.keys(docs[0]);
2236
- csv += 'Collection: ' + collName + '\n';
2237
- csv += keys.join(',') + '\n';
2238
- for (var j = 0; j < docs.length; j++) {
2239
- var doc = docs[j];
2240
- csv += keys.map(function(k) {
2241
- return JSON.stringify(doc[k] || '');
2242
- }).join(',') + '\n';
2243
- }
2244
- csv += '\n';
2245
- }
2246
- }
2247
- }
2248
- return csv;
2249
- }
1677
+ async export(format = 'json', password = null) {
1678
+ const data = {
1679
+ version: '4.0.3',
1680
+ database: this.name,
1681
+ timestamp: Date.now(),
1682
+ collections: {}
1683
+ };
2250
1684
 
2251
- return data;
1685
+ for (const collName of this.listCollections()) {
1686
+ const collection = await this.getCollection(collName);
1687
+ data.collections[collName] = await collection.getAll({ password });
2252
1688
  }
2253
1689
 
1690
+ if (format === 'json') {
1691
+ const serialized = serializer.serialize(data);
1692
+ return base64.encode(serialized);
1693
+ }
1694
+ if (format === 'encrypted' && password) {
1695
+ const encryption = new BrowserEncryptionUtility();
1696
+ const serializedData = serializer.serialize(data);
1697
+ const encrypted = await encryption.encrypt(serializedData, password);
1698
+ return base64.encode(encrypted);
1699
+ }
1700
+ throw new LacertaDBError(`Unsupported export format: ${format}`, 'INVALID_FORMAT');
1701
+ }
2254
1702
 
2255
- async import (data, format, password) {
2256
- format = format || 'json';
2257
- password = password || null;
2258
- var parsed;
2259
-
2260
- try {
2261
- if (format === 'encrypted' && password) {
2262
- var encryption = new BrowserEncryptionUtility();
2263
- var encrypted = base64.decode(data);
2264
- var decrypted = await encryption.decrypt(encrypted, password);
2265
- parsed = serializer.deserialize(decrypted);
2266
- } else {
2267
- var decoded = base64.decode(data);
2268
- parsed = serializer.deserialize(decoded);
2269
- }
2270
- } catch (e) {
2271
- throw new LacertaDBError('Failed to parse import data', 'IMPORT_PARSE_FAILED', e);
2272
- }
2273
-
2274
- var self = this;
2275
- for (var collName in parsed.collections) {
2276
- if (Object.prototype.hasOwnProperty.call(parsed.collections, collName)) {
2277
- var docs = parsed.collections[collName];
2278
- var collection = await this.createCollection(collName).catch(function() {
2279
- return self.getCollection(collName);
2280
- });
2281
-
2282
- for (var i = 0; i < docs.length; i++) {
2283
- await collection.add(docs[i]);
2284
- }
2285
- }
2286
- }
2287
- var docCount = 0;
2288
- for(var c in parsed.collections){
2289
- if (Object.prototype.hasOwnProperty.call(parsed.collections, c)) {
2290
- docCount += parsed.collections[c].length;
2291
- }
1703
+ async import(data, format = 'json', password = null) {
1704
+ let parsed;
1705
+ try {
1706
+ const decoded = base64.decode(data);
1707
+ if (format === 'encrypted' && password) {
1708
+ const encryption = new BrowserEncryptionUtility();
1709
+ const decrypted = await encryption.decrypt(decoded, password);
1710
+ parsed = serializer.deserialize(decrypted);
1711
+ } else {
1712
+ parsed = serializer.deserialize(decoded);
2292
1713
  }
2293
-
2294
- return {
2295
- collections: Object.keys(parsed.collections).length,
2296
- documents: docCount
2297
- };
1714
+ } catch (e) {
1715
+ throw new LacertaDBError('Failed to parse import data', 'IMPORT_PARSE_FAILED', e);
2298
1716
  }
2299
1717
 
2300
-
2301
- async clearAll() {
2302
- var self = this;
2303
- this.collections.forEach(async function(collection, name) {
2304
- await collection.clear();
2305
- collection.destroy();
2306
- });
2307
- this.collections.clear();
2308
- this.metadata = new DatabaseMetadata(this.name);
2309
- this.metadata.save();
2310
- this.quickStore.clear();
1718
+ for (const collName in parsed.collections) {
1719
+ const docs = parsed.collections[collName];
1720
+ const collection = await this.createCollection(collName).catch(() => this.getCollection(collName));
1721
+ await collection.batchAdd(docs);
2311
1722
  }
2312
1723
 
2313
- destroy() {
2314
- this.collections.forEach(function(collection) {
2315
- collection.destroy();
2316
- });
2317
- this.collections.clear();
2318
- }
1724
+ const docCount = Object.values(parsed.collections).reduce((sum, docs) => sum + docs.length, 0);
1725
+ return {
1726
+ collections: Object.keys(parsed.collections).length,
1727
+ documents: docCount
1728
+ };
2319
1729
  }
2320
1730
 
1731
+ async clearAll() {
1732
+ await Promise.all([...this.collections.keys()].map(name => this.dropCollection(name)));
1733
+ this.collections.clear();
1734
+ this.metadata = new DatabaseMetadata(this.name);
1735
+ this.metadata.save();
1736
+ this.quickStore.clear();
1737
+ }
2321
1738
 
2322
- // ========================
2323
- // Main LacertaDB Class
2324
- // ========================
1739
+ destroy() {
1740
+ this.collections.forEach(collection => collection.destroy());
1741
+ this.collections.clear();
1742
+ }
1743
+ }
2325
1744
 
2326
- class LacertaDB {
2327
- constructor() {
2328
- this.databases = new Map();
2329
- this.performanceMonitor = new PerformanceMonitor();
1745
+ // ========================
1746
+ // Main LacertaDB Class
1747
+ // ========================
2330
1748
 
2331
- if (typeof global !== 'undefined') {
2332
- global.performanceMonitor = this.performanceMonitor;
2333
- }
2334
- }
1749
+ export class LacertaDB {
1750
+ constructor() {
1751
+ this.databases = new Map();
1752
+ this.performanceMonitor = new PerformanceMonitor();
1753
+ }
2335
1754
 
2336
- async getDatabase(name) {
2337
- if (!this.databases.has(name)) {
2338
- var db = new Database(name);
2339
- await db.init();
2340
- this.databases.set(name, db);
2341
- }
2342
- return this.databases.get(name);
1755
+ async getDatabase(name) {
1756
+ if (!this.databases.has(name)) {
1757
+ const db = new Database(name, this.performanceMonitor);
1758
+ await db.init();
1759
+ this.databases.set(name, db);
2343
1760
  }
1761
+ return this.databases.get(name);
1762
+ }
2344
1763
 
2345
- async dropDatabase(name) {
2346
- if (this.databases.has(name)) {
2347
- var db = this.databases.get(name);
2348
- await db.clearAll();
2349
- db.destroy();
2350
- this.databases.delete(name);
1764
+ async dropDatabase(name) {
1765
+ if (this.databases.has(name)) {
1766
+ const db = this.databases.get(name);
1767
+ await db.clearAll();
1768
+ db.destroy();
1769
+ this.databases.delete(name);
1770
+ }
2351
1771
 
2352
- localStorage.removeItem('lacertadb_' + name + '_metadata');
2353
- localStorage.removeItem('lacertadb_' + name + '_settings');
2354
- localStorage.removeItem('lacertadb_' + name + '_version');
1772
+ ['metadata', 'settings', 'version'].forEach(suffix => {
1773
+ localStorage.removeItem(`lacertadb_${name}_${suffix}`);
1774
+ });
1775
+ const quickStore = new QuickStore(name);
1776
+ quickStore.clear();
1777
+ }
2355
1778
 
2356
- var quickStore = new QuickStore(name);
2357
- quickStore.clear();
1779
+ listDatabases() {
1780
+ const dbNames = new Set();
1781
+ for (let i = 0; i < localStorage.length; i++) {
1782
+ const key = localStorage.key(i);
1783
+ // Replaced optional chaining with `&&` for compatibility
1784
+ if (key && key.startsWith('lacertadb_')) {
1785
+ const dbName = key.split('_')[1];
1786
+ dbNames.add(dbName);
2358
1787
  }
2359
1788
  }
1789
+ return [...dbNames];
1790
+ }
2360
1791
 
2361
- listDatabases() {
2362
- var dbs = [];
2363
- for (var i = 0; i < localStorage.length; i++) {
2364
- var key = localStorage.key(i);
2365
- if (key && key.startsWith('lacertadb_') && key.endsWith('_metadata')) {
2366
- var dbName = key.replace('lacertadb_', '').replace('_metadata', '');
2367
- dbs.push(dbName);
2368
- }
2369
- }
2370
- return dbs;
2371
- }
1792
+ async createBackup(password = null) {
1793
+ const backup = {
1794
+ version: '4.0.3',
1795
+ timestamp: Date.now(),
1796
+ databases: {}
1797
+ };
2372
1798
 
2373
- async createBackup(password) {
2374
- password = password || null;
2375
- var backup = {
2376
- version: '4.0.0',
2377
- timestamp: Date.now(),
2378
- databases: {}
2379
- };
1799
+ for (const dbName of this.listDatabases()) {
1800
+ const db = await this.getDatabase(dbName);
1801
+ const exported = await db.export('json');
1802
+ const decoded = base64.decode(exported);
1803
+ backup.databases[dbName] = serializer.deserialize(decoded);
1804
+ }
2380
1805
 
2381
- var dbNames = this.listDatabases();
2382
- for (var i = 0; i < dbNames.length; i++) {
2383
- var dbName = dbNames[i];
2384
- var db = await this.getDatabase(dbName);
2385
- var exported = await db.export('json'); // Exports as turboserial/base64
2386
- var decoded = base64.decode(exported);
2387
- backup.databases[dbName] = serializer.deserialize(decoded);
2388
- }
1806
+ const serializedBackup = serializer.serialize(backup);
1807
+ if (password) {
1808
+ const encryption = new BrowserEncryptionUtility();
1809
+ const encrypted = await encryption.encrypt(serializedBackup, password);
1810
+ return base64.encode(encrypted);
1811
+ }
1812
+ return base64.encode(serializedBackup);
1813
+ }
2389
1814
 
2390
- var serializedBackup = serializer.serialize(backup);
1815
+ async restoreBackup(backupData, password = null) {
1816
+ let backup;
1817
+ try {
1818
+ let decodedData = base64.decode(backupData);
2391
1819
  if (password) {
2392
- var encryption = new BrowserEncryptionUtility();
2393
- var encrypted = await encryption.encrypt(serializedBackup, password);
2394
- return base64.encode(encrypted);
1820
+ const encryption = new BrowserEncryptionUtility();
1821
+ const decrypted = await encryption.decrypt(decodedData, password);
1822
+ backup = serializer.deserialize(decrypted);
1823
+ } else {
1824
+ backup = serializer.deserialize(decodedData);
2395
1825
  }
2396
-
2397
- return base64.encode(serializedBackup);
1826
+ } catch (e) {
1827
+ throw new LacertaDBError('Failed to parse backup data', 'BACKUP_PARSE_FAILED', e);
2398
1828
  }
2399
1829
 
2400
- async restoreBackup(backupData, password) {
2401
- password = password || null;
2402
- var backup;
2403
-
2404
- try {
2405
- var decodedData = base64.decode(backupData);
2406
- if (password) {
2407
- var encryption = new BrowserEncryptionUtility();
2408
- var decrypted = await encryption.decrypt(decodedData, password);
2409
- backup = serializer.deserialize(decrypted);
2410
- } else {
2411
- backup = serializer.deserialize(decodedData);
2412
- }
2413
- } catch (e) {
2414
- throw new LacertaDBError('Failed to parse backup data', 'BACKUP_PARSE_FAILED', e);
2415
- }
2416
-
2417
- var results = {
2418
- databases: 0,
2419
- collections: 0,
2420
- documents: 0
2421
- };
2422
-
2423
- for (var dbName in backup.databases) {
2424
- if (Object.prototype.hasOwnProperty.call(backup.databases, dbName)) {
2425
- var dbData = backup.databases[dbName];
2426
- var db = await this.getDatabase(dbName);
2427
-
2428
- var serializedDbData = serializer.serialize(dbData);
2429
- var encodedDbData = base64.encode(serializedDbData);
2430
-
2431
- var importResult = await db.import(encodedDbData);
1830
+ const results = { databases: 0, collections: 0, documents: 0 };
1831
+ for (const [dbName, dbData] of Object.entries(backup.databases)) {
1832
+ const db = await this.getDatabase(dbName);
1833
+ const encodedDbData = base64.encode(serializer.serialize(dbData));
1834
+ const importResult = await db.import(encodedDbData);
2432
1835
 
2433
- results.databases++;
2434
- results.collections += importResult.collections;
2435
- results.documents += importResult.documents;
2436
- }
2437
- }
2438
-
2439
- return results;
1836
+ results.databases++;
1837
+ results.collections += importResult.collections;
1838
+ results.documents += importResult.documents;
2440
1839
  }
1840
+ return results;
2441
1841
  }
2442
-
2443
-
2444
- // ========================
2445
- // Export
2446
- // ========================
2447
-
2448
- var LacertaDBExport = {
2449
- LacertaDB: LacertaDB,
2450
- Database: Database,
2451
- Collection: Collection,
2452
- Document: Document,
2453
- QueryOperators: QueryOperators,
2454
- AggregationPipeline: AggregationPipeline,
2455
- MigrationManager: MigrationManager,
2456
- PerformanceMonitor: PerformanceMonitor,
2457
- LacertaDBError: LacertaDBError
2458
- };
2459
-
2460
- module.exports = LacertaDBExport;
2461
-
2462
- })((typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this) || {});
1842
+ }
1843
+
1844
+ // Export all major components for advanced usage
1845
+ export {
1846
+ Database,
1847
+ Collection,
1848
+ Document,
1849
+ MigrationManager,
1850
+ PerformanceMonitor,
1851
+ LacertaDBError,
1852
+ OPFSUtility
1853
+ };