@pixagram/lacerta-db 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,2091 +1,1853 @@
1
- /*
2
- The MIT License (MIT)
3
- Copyright (c) 2024 Matias Affolter
4
- */
5
- "use strict";
1
+ /**
2
+ * LacertaDB V4 - Complete Core Library (Repaired and Enhanced)
3
+ * A powerful browser-based document database with encryption, compression, and OPFS support
4
+ * @version 4.0.3 (Max Compatibility)
5
+ * @license MIT
6
+ */
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.
6
11
  import TurboSerial from "@pixagram/turboserial";
7
- const TURBO = new TurboSerial({
8
- compression: false, // Enable compression
9
- deduplication: true, // Enable reference deduplication
10
- shareArrayBuffers: true, // Share ArrayBuffer references
11
- simdOptimization: true, // Enable SIMD optimizations
12
- detectCircular: true, // Detect circular references
13
- memoryPoolSize: 65536 // Initial memory pool size
14
- });
15
-
16
- // Utility function for nested object access
17
- function getNestedValue(obj, path) {
18
- return path.split('.').reduce((curr, prop) => (curr || {})[prop], obj);
19
- }
20
-
21
- class OPFSUtility {
22
- static async saveAttachments(dbName, collectionName, documentId, attachments) {
23
- const attachmentPaths = [];
24
- const rootHandle = await navigator.storage.getDirectory();
25
- const pathParts = [dbName, collectionName, documentId];
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();
22
+
23
+ /**
24
+ * Async Mutex for managing concurrent operations
25
+ */
26
+ class AsyncMutex {
27
+ constructor() {
28
+ this._queue = [];
29
+ this._locked = false;
30
+ }
26
31
 
27
- let dirHandle = rootHandle;
28
- for (const part of pathParts) {
29
- dirHandle = await dirHandle.getDirectoryHandle(part, { create: true });
30
- }
32
+ acquire() {
33
+ return new Promise(resolve => {
34
+ this._queue.push(resolve);
35
+ this._dispatch();
36
+ });
37
+ }
31
38
 
32
- for (let i = 0; i < attachments.length; i++) {
33
- const fileId = i.toString();
34
- const fileHandle = await dirHandle.getFileHandle(fileId, { create: true });
35
- const writable = await fileHandle.createWritable();
36
- await writable.write(attachments[i].data);
37
- await writable.close();
39
+ release() {
40
+ this._locked = false;
41
+ this._dispatch();
42
+ }
38
43
 
39
- const filePath = `${dbName}/${collectionName}/${documentId}/${fileId}`;
40
- attachmentPaths.push(filePath);
44
+ async runExclusive(callback) {
45
+ const release = await this.acquire();
46
+ try {
47
+ return await callback();
48
+ } finally {
49
+ release();
41
50
  }
42
-
43
- return attachmentPaths;
44
51
  }
45
52
 
46
- static async getAttachments(attachmentPaths) {
47
- const attachments = [];
48
- const rootHandle = await navigator.storage.getDirectory();
49
-
50
- for (const path of attachmentPaths) {
51
- try {
52
- const pathParts = path.split('/');
53
- let dirHandle = rootHandle;
54
- for (let i = 0; i < pathParts.length - 1; i++) {
55
- dirHandle = await dirHandle.getDirectoryHandle(pathParts[i]);
56
- }
57
- const fileHandle = await dirHandle.getFileHandle(pathParts[pathParts.length - 1]);
58
- const file = await fileHandle.getFile();
59
-
60
- attachments.push({
61
- path: path,
62
- data: file,
63
- });
64
- } catch (error) {
65
- console.error(`Error retrieving attachment at "${path}": ${error.message}`);
66
- }
53
+ _dispatch() {
54
+ if (this._locked || this._queue.length === 0) {
55
+ return;
67
56
  }
57
+ this._locked = true;
58
+ const resolve = this._queue.shift();
59
+ resolve(() => this.release());
60
+ }
61
+ }
68
62
 
69
- return attachments;
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();
70
73
  }
74
+ }
71
75
 
72
- static async deleteAttachments(dbName, collectionName, documentId) {
73
- const rootHandle = await navigator.storage.getDirectory();
74
- const pathParts = [dbName, collectionName, documentId];
76
+ // ========================
77
+ // Compression Utility
78
+ // ========================
75
79
 
76
- let dirHandle = rootHandle;
77
- for (let i = 0; i < pathParts.length - 1; i++) {
78
- dirHandle = await dirHandle.getDirectoryHandle(pathParts[i]);
80
+ class BrowserCompressionUtility {
81
+ async compress(input) {
82
+ if (!(input instanceof Uint8Array)) {
83
+ throw new TypeError('Input must be Uint8Array');
84
+ }
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);
79
92
  }
93
+ }
80
94
 
95
+ async decompress(compressedData) {
96
+ if (!(compressedData instanceof Uint8Array)) {
97
+ throw new TypeError('Input must be Uint8Array');
98
+ }
81
99
  try {
82
- await dirHandle.removeEntry(pathParts[pathParts.length - 1], { recursive: true });
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);
83
104
  } catch (error) {
84
- console.error(`Error deleting attachments for document "${documentId}": ${error.message}`);
105
+ throw new LacertaDBError('Decompression failed', 'DECOMPRESSION_FAILED', error);
85
106
  }
86
107
  }
87
108
 
88
- toString() {
89
- return '[OPFSUtility]';
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;
90
115
  }
91
- }
92
116
 
93
- class BrowserCompressionUtility {
94
- static async compress(input) {
95
- const data = typeof input === "string" ? new TextEncoder().encode(input) : input;
96
- const stream = new CompressionStream('deflate');
97
- const writer = stream.writable.getWriter();
117
+ decompressSync(compressedData) {
118
+ if (!(compressedData instanceof Uint8Array)) {
119
+ throw new TypeError('Input must be Uint8Array');
120
+ }
121
+ return compressedData;
122
+ }
123
+ }
98
124
 
99
- writer.write(data);
100
- writer.close();
125
+ // ========================
126
+ // Encryption Utility (FIXED & IMPROVED)
127
+ // ========================
101
128
 
102
- return await this._streamToUint8Array(stream.readable);
103
- }
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));
104
137
 
105
- static async decompress(compressedData) {
106
- const stream = new DecompressionStream('deflate');
107
- const writer = stream.writable.getWriter();
138
+ const keyMaterial = await crypto.subtle.importKey(
139
+ 'raw',
140
+ new TextEncoder().encode(password),
141
+ 'PBKDF2',
142
+ false,
143
+ ['deriveKey']
144
+ );
108
145
 
109
- writer.write(compressedData);
110
- writer.close();
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
+ );
111
159
 
112
- return await this._streamToUint8Array(stream.readable);
113
- }
160
+ const encrypted = await crypto.subtle.encrypt({
161
+ name: 'AES-GCM',
162
+ iv
163
+ },
164
+ key,
165
+ data
166
+ );
114
167
 
115
- static async _streamToUint8Array(readableStream) {
116
- const reader = readableStream.getReader();
117
- const chunks = [];
118
- let totalLength = 0;
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);
119
173
 
120
- while (true) {
121
- const { value, done } = await reader.read();
122
- if (done) break;
123
- chunks.push(value);
124
- totalLength += value.length;
174
+ return result;
175
+ } catch (error) {
176
+ throw new LacertaDBError('Encryption failed', 'ENCRYPTION_FAILED', error);
125
177
  }
178
+ }
126
179
 
127
- const result = new Uint8Array(totalLength);
128
- let offset = 0;
129
- for (const chunk of chunks) {
130
- result.set(chunk, offset);
131
- offset += chunk.length;
180
+ async decrypt(wrappedData, password) {
181
+ if (!(wrappedData instanceof Uint8Array)) {
182
+ throw new TypeError('Data must be Uint8Array');
132
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
+ );
133
196
 
134
- return result;
135
- }
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
+ );
136
210
 
137
- toString() {
138
- return '[BrowserCompressionUtility]';
211
+ const decrypted = await crypto.subtle.decrypt({
212
+ name: 'AES-GCM',
213
+ iv
214
+ },
215
+ key,
216
+ encryptedData
217
+ );
218
+
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
+ }
139
225
  }
140
226
  }
141
227
 
142
- class BrowserEncryptionUtility {
143
- static async encrypt(data, password) {
144
- const salt = crypto.getRandomValues(new Uint8Array(16));
145
- const key = await this._deriveKey(password, salt);
146
- const iv = crypto.getRandomValues(new Uint8Array(12));
147
- const checksum = await this._generateChecksum(data);
148
- const combinedData = this._combineDataAndChecksum(data, checksum);
149
-
150
- const encryptedData = await crypto.subtle.encrypt(
151
- { name: "AES-GCM", iv: iv },
152
- key,
153
- combinedData
154
- );
155
-
156
- return this._wrapIntoUint8Array(salt, iv, new Uint8Array(encryptedData));
157
- }
158
-
159
- static async decrypt(wrappedData, password) {
160
- const { salt, iv, encryptedData } = this._unwrapUint8Array(wrappedData);
161
- const key = await this._deriveKey(password, salt);
162
-
163
- const decryptedData = await crypto.subtle.decrypt(
164
- { name: "AES-GCM", iv: iv },
165
- key,
166
- encryptedData
167
- );
168
-
169
- const decryptedUint8Array = new Uint8Array(decryptedData);
170
- const { data, checksum } = this._separateDataAndChecksum(decryptedUint8Array);
171
-
172
- const validChecksum = await this._generateChecksum(data);
173
- if (!this._verifyChecksum(validChecksum, checksum)) {
174
- throw new Error("Data integrity check failed. The data has been tampered with.");
175
- }
176
-
177
- return data;
178
- }
179
-
180
- static async _deriveKey(password, salt) {
181
- const encoder = new TextEncoder();
182
- const keyMaterial = await crypto.subtle.importKey(
183
- "raw",
184
- encoder.encode(password),
185
- { name: "PBKDF2" },
186
- false,
187
- ["deriveKey"]
188
- );
189
-
190
- return await crypto.subtle.deriveKey(
191
- {
192
- name: "PBKDF2",
193
- salt: salt,
194
- iterations: 600000,
195
- hash: "SHA-512"
196
- },
197
- keyMaterial,
198
- {
199
- name: "AES-GCM",
200
- length: 256
201
- },
202
- false,
203
- ["encrypt", "decrypt"]
204
- );
205
- }
228
+ // ========================
229
+ // OPFS (Origin Private File System) Utility
230
+ // ========================
206
231
 
207
- static async _generateChecksum(data) {
208
- return new Uint8Array(
209
- await crypto.subtle.digest("SHA-256", data)
210
- );
211
- }
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');
255
+ }
212
256
 
213
- static _verifyChecksum(generatedChecksum, originalChecksum) {
214
- if (generatedChecksum.length !== originalChecksum.length) return false;
215
- for (let i = 0; i < generatedChecksum.length; i++) {
216
- if (generatedChecksum[i] !== originalChecksum[i]) {
217
- return false;
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
+ });
218
269
  }
270
+ return attachmentPaths;
271
+ } catch (error) {
272
+ throw new LacertaDBError('Failed to save attachments', 'ATTACHMENT_SAVE_FAILED', error);
219
273
  }
220
- return true;
221
274
  }
222
275
 
223
- static _combineDataAndChecksum(data, checksum) {
224
- const combined = new Uint8Array(data.length + checksum.length);
225
- combined.set(data);
226
- combined.set(checksum, data.length);
227
- return combined;
228
- }
276
+ async getAttachments(attachmentPaths) {
277
+ const attachments = [];
278
+ const root = await navigator.storage.getDirectory();
229
279
 
230
- static _separateDataAndChecksum(combinedData) {
231
- const dataLength = combinedData.length - 32;
232
- const data = combinedData.slice(0, dataLength);
233
- const checksum = combinedData.slice(dataLength);
234
- return { data, checksum };
235
- }
280
+ for (const attachmentInfo of attachmentPaths) {
281
+ try {
282
+ const pathParts = attachmentInfo.path.split('/').filter(p => p);
283
+ let currentDir = root;
236
284
 
237
- static _wrapIntoUint8Array(salt, iv, encryptedData) {
238
- const result = new Uint8Array(salt.length + iv.length + encryptedData.length);
239
- result.set(salt, 0);
240
- result.set(iv, salt.length);
241
- result.set(encryptedData, salt.length + iv.length);
242
- return result;
285
+ for (let i = 0; i < pathParts.length - 1; i++) {
286
+ currentDir = await currentDir.getDirectoryHandle(pathParts[i]);
287
+ }
288
+
289
+ const fileHandle = await currentDir.getFileHandle(pathParts[pathParts.length - 1]);
290
+ const file = await fileHandle.getFile();
291
+ const data = await file.arrayBuffer();
292
+
293
+ attachments.push({
294
+ name: attachmentInfo.originalName || attachmentInfo.name,
295
+ type: attachmentInfo.type,
296
+ data: new Uint8Array(data),
297
+ size: attachmentInfo.size
298
+ });
299
+ } catch (error) {
300
+ console.error(`Failed to get attachment: ${attachmentInfo.path}`, error);
301
+ // Optionally, collect errors and return them
302
+ }
303
+ }
304
+ return attachments;
243
305
  }
244
306
 
245
- static _unwrapUint8Array(wrappedData) {
246
- const salt = wrappedData.slice(0, 16);
247
- const iv = wrappedData.slice(16, 28);
248
- const encryptedData = wrappedData.slice(28);
249
- return { salt, iv, encryptedData };
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
+ }
318
+ }
250
319
  }
251
320
 
252
- toString() {
253
- return '[BrowserEncryptionUtility]';
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
+ };
254
340
  }
255
341
  }
256
342
 
257
- class IndexedDBUtility {
258
- static openDatabase(dbName, version = null, upgradeCallback = null) {
259
- return new Promise((resolve, reject) => {
260
- let request;
343
+ // ========================
344
+ // IndexedDB Utility
345
+ // ========================
261
346
 
262
- if (version) {
263
- request = indexedDB.open(dbName, version);
264
- } else {
265
- request = indexedDB.open(dbName);
266
- }
347
+ class IndexedDBUtility {
348
+ constructor() {
349
+ this.mutex = new AsyncMutex();
350
+ }
267
351
 
268
- request.onupgradeneeded = (event) => {
269
- const db = event.target.result;
270
- const oldVersion = event.oldVersion;
271
- const newVersion = event.newVersion;
272
- console.log(`Upgrading database "${dbName}" from version ${oldVersion} to ${newVersion}`);
352
+ openDatabase(dbName, version = 1, upgradeCallback) {
353
+ return new Promise((resolve, reject) => {
354
+ const request = indexedDB.open(dbName, version);
273
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 => {
274
361
  if (upgradeCallback) {
275
- upgradeCallback(db, oldVersion, newVersion);
362
+ upgradeCallback(event.target.result, event.oldVersion, event.newVersion);
276
363
  }
277
364
  };
278
-
279
- request.onsuccess = (event) => {
280
- const db = event.target.result;
281
-
282
- db.onclose = () => {
283
- console.log(`Database "${dbName}" connection is closing.`);
284
- };
285
-
286
- resolve(db);
287
- };
288
-
289
- request.onerror = (event) => {
290
- reject(new Error(`Failed to open database "${dbName}": ${event.target.error.message}`));
291
- };
292
365
  });
293
366
  }
294
367
 
295
- // Improved transaction handling with better retry logic
296
- static async performTransaction(db, storeNames, mode, callback, retries = 3, operationId = null) {
297
- let lastError = null;
298
-
299
- for (let attempt = 0; attempt <= retries; attempt++) {
300
- try {
301
- if (!db || db.readyState !== 'done') {
302
- throw new Error('Database connection is not available or ready.');
303
- }
304
-
305
- const tx = db.transaction(Array.isArray(storeNames) ? storeNames : [storeNames], mode);
306
- const stores = Array.isArray(storeNames)
307
- ? storeNames.map(name => tx.objectStore(name))
308
- : [tx.objectStore(storeNames)];
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;
309
376
 
310
- // Add transaction timeout
311
- const timeoutId = setTimeout(() => {
312
- tx.abort();
313
- }, 30000); // 30 second timeout
377
+ transaction.oncomplete = () => resolve(result);
378
+ transaction.onerror = () => reject(transaction.error);
379
+ transaction.onabort = () => reject(new Error('Transaction aborted'));
314
380
 
315
- const result = await callback(...stores, operationId);
316
-
317
- return new Promise((resolve, reject) => {
318
- tx.oncomplete = () => {
319
- clearTimeout(timeoutId);
320
- resolve(result);
321
- };
322
- tx.onerror = () => {
323
- clearTimeout(timeoutId);
324
- lastError = new Error(`Transaction failed: ${tx.error ? tx.error.message : 'unknown error'}`);
325
- reject(lastError);
326
- };
327
- tx.onabort = () => {
328
- clearTimeout(timeoutId);
329
- lastError = new Error(`Transaction aborted: ${tx.error ? tx.error.message : 'timeout or abort'}`);
330
- reject(lastError);
331
- };
332
- });
333
- } catch (error) {
334
- lastError = error;
335
- if (attempt < retries) {
336
- console.warn(`Transaction failed, retrying... (${retries - attempt} attempts left): ${error.message}`);
337
- // Exponential backoff with jitter
338
- const delay = Math.pow(2, attempt) * 100 + Math.random() * 100;
339
- await new Promise(resolve => setTimeout(resolve, delay));
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;
387
+ }
388
+ } catch (error) {
389
+ reject(error);
390
+ }
391
+ });
392
+ } catch (error) {
393
+ lastError = error;
394
+ if (i < retries - 1) {
395
+ await new Promise(resolve => setTimeout(resolve, (2 ** i) * 100));
396
+ }
340
397
  }
341
398
  }
342
- }
343
-
344
- throw new Error(`Transaction ultimately failed after ${retries + 1} attempts: ${lastError.message}`);
345
- }
346
-
347
- static add(store, record) {
348
- return new Promise((resolve, reject) => {
349
- const request = store.add(record);
350
- request.onsuccess = () => resolve();
351
- request.onerror = event => reject(`Failed to insert record: ${event.target.error.message}`);
399
+ throw new LacertaDBError('Transaction failed after retries', 'TRANSACTION_FAILED', lastError);
352
400
  });
353
401
  }
354
402
 
355
- static put(store, record) {
403
+ _promisifyRequest(requestFactory) {
356
404
  return new Promise((resolve, reject) => {
357
- const request = store.put(record);
358
- request.onsuccess = () => resolve();
359
- request.onerror = event => reject(`Failed to put record: ${event.target.error.message}`);
405
+ const request = requestFactory();
406
+ request.onsuccess = () => resolve(request.result);
407
+ request.onerror = () => reject(request.error);
360
408
  });
361
409
  }
362
410
 
363
- static delete(store, key) {
364
- return new Promise((resolve, reject) => {
365
- const request = store.delete(key);
366
- request.onsuccess = () => resolve();
367
- request.onerror = event => reject(`Failed to delete record: ${event.target.error.message}`);
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));
368
415
  });
369
416
  }
370
417
 
371
- static get(store, key) {
372
- return new Promise((resolve, reject) => {
373
- const request = store.get(key);
374
- request.onsuccess = event => resolve(event.target.result);
375
- request.onerror = event => reject(`Failed to retrieve record with key ${key}: ${event.target.error.message}`);
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));
376
422
  });
377
423
  }
378
424
 
379
- static getAll(store) {
380
- return new Promise((resolve, reject) => {
381
- const request = store.getAll();
382
- request.onsuccess = event => resolve(event.target.result);
383
- request.onerror = event => reject(`Failed to retrieve records: ${event.target.error.message}`);
425
+ get(db, storeName, key) {
426
+ return this.performTransaction(db, [storeName], 'readonly', tx => {
427
+ return this._promisifyRequest(() => tx.objectStore(storeName).get(key));
384
428
  });
385
429
  }
386
430
 
387
- static count(store) {
388
- return new Promise((resolve, reject) => {
389
- const request = store.count();
390
- request.onsuccess = event => resolve(event.target.result);
391
- request.onerror = event => reject(`Failed to count records: ${event.target.error.message}`);
431
+ getAll(db, storeName, query, count) {
432
+ return this.performTransaction(db, [storeName], 'readonly', tx => {
433
+ return this._promisifyRequest(() => tx.objectStore(storeName).getAll(query, count));
392
434
  });
393
435
  }
394
436
 
395
- static clear(store) {
396
- return new Promise((resolve, reject) => {
397
- const request = store.clear();
398
- request.onsuccess = () => resolve();
399
- request.onerror = event => reject(`Failed to clear store: ${event.target.error.message}`);
437
+ delete(db, storeName, key) {
438
+ return this.performTransaction(db, [storeName], 'readwrite', tx => {
439
+ return this._promisifyRequest(() => tx.objectStore(storeName).delete(key));
400
440
  });
401
441
  }
402
442
 
403
- static deleteDatabase(dbName) {
404
- return new Promise((resolve, reject) => {
405
- const request = indexedDB.deleteDatabase(dbName);
406
- request.onsuccess = () => resolve();
407
- request.onerror = event => reject(`Failed to delete database: ${event.target.error.message}`);
443
+ clear(db, storeName) {
444
+ return this.performTransaction(db, [storeName], 'readwrite', tx => {
445
+ return this._promisifyRequest(() => tx.objectStore(storeName).clear());
408
446
  });
409
447
  }
410
448
 
411
- static iterateCursor(store, processCallback) {
412
- return new Promise((resolve, reject) => {
413
- const request = store.openCursor();
414
- request.onsuccess = event => {
415
- const cursor = event.target.result;
416
- if (cursor) {
417
- processCallback(cursor.value, cursor.key);
418
- cursor.continue();
419
- } else {
420
- resolve();
421
- }
422
- };
423
- request.onerror = event => reject(`Cursor iteration failed: ${event.target.error.message}`);
449
+ count(db, storeName, query) {
450
+ return this.performTransaction(db, [storeName], 'readonly', tx => {
451
+ return this._promisifyRequest(() => tx.objectStore(storeName).count(query));
424
452
  });
425
453
  }
426
-
427
- static iterateCursorSafe(store, processCallback, options = {}) {
428
- return new Promise((resolve, reject) => {
429
- const {
430
- index = null,
431
- direction = 'next',
432
- range = null
433
- } = options;
434
-
435
- let source = store;
436
- if (index && store.indexNames.contains(index)) {
437
- source = store.index(index);
438
- }
439
454
 
440
- const request = source.openCursor(range, direction);
441
- const results = [];
442
-
443
- request.onsuccess = (event) => {
444
- const cursor = event.target.result;
445
-
446
- if (cursor) {
447
- // FIXED: No async operations in cursor callback
448
- const shouldContinue = processCallback(cursor.value, cursor.key, results);
449
-
450
- if (shouldContinue !== false) {
451
- cursor.continue();
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);
471
+ }
472
+ } catch (error) {
473
+ reject(error);
474
+ }
452
475
  } else {
453
476
  resolve(results);
454
477
  }
455
- } else {
456
- resolve(results);
457
- }
458
- };
459
-
460
- request.onerror = () => reject(request.error);
478
+ };
479
+ request.onerror = () => reject(request.error);
480
+ });
461
481
  });
462
482
  }
463
-
464
- toString() {
465
- return '[IndexedDBUtility]';
466
- }
467
483
  }
468
484
 
469
- class LocalStorageUtility {
470
- static getItem(key) {
471
- const value = localStorage.getItem(key);
472
- return value ? TURBO.serialize(value) : null;
473
- }
474
-
475
- static setItem(key, value) {
476
- localStorage.setItem(key, TURBO.serialize(value));
477
- }
478
-
479
- static removeItem(key) {
480
- localStorage.removeItem(key);
481
- }
485
+ // ========================
486
+ // Document Class
487
+ // ========================
482
488
 
483
- static clear() {
484
- localStorage.clear();
485
- }
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;
486
500
 
487
- toString() {
488
- return '[LocalStorageUtility]';
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;
489
505
  }
490
- }
491
506
 
492
- class AsyncMutex {
493
- constructor() {
494
- this.queue = [];
495
- this.locked = false;
507
+ generateId() {
508
+ return `doc_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
496
509
  }
497
510
 
498
- async acquire() {
499
- return new Promise((resolve) => {
500
- if (!this.locked) {
501
- this.locked = true;
502
- resolve();
503
- } else {
504
- this.queue.push(resolve);
511
+ async pack() {
512
+ try {
513
+ let packed = serializer.serialize(this.data);
514
+ if (this._compressed) {
515
+ packed = await this.compression.compress(packed);
505
516
  }
506
- });
507
- }
508
-
509
- release() {
510
- if (this.queue.length > 0) {
511
- const next = this.queue.shift();
512
- next();
513
- } else {
514
- this.locked = false;
517
+ if (this._encrypted && this.password) {
518
+ packed = await this.encryption.encrypt(packed, this.password);
519
+ }
520
+ this.packedData = packed;
521
+ return packed;
522
+ } catch (error) {
523
+ throw new LacertaDBError('Failed to pack document', 'PACK_FAILED', error);
515
524
  }
516
525
  }
517
526
 
518
- async runExclusive(callback) {
519
- await this.acquire();
527
+ async unpack() {
520
528
  try {
521
- return await callback();
522
- } finally {
523
- this.release();
524
- }
525
- }
526
-
527
- toString() {
528
- return `[AsyncMutex locked:${this.locked} queue:${this.queue.length}]`;
529
- }
530
- }
531
-
532
- class DatabaseMetadata {
533
- constructor(dbName) {
534
- this._dbName = dbName;
535
- this._metadataKey = `lacertadb_${this._dbName}_metadata`;
536
- this._collections = new Map();
537
- this._metadata = this._loadMetadata();
538
- this._mutex = new AsyncMutex();
539
- }
540
-
541
- async saveMetadata() {
542
- return await this._mutex.runExclusive(() => {
543
- LocalStorageUtility.setItem(this.metadataKey, this.getRawMetadata());
544
- });
545
- }
546
-
547
- async adjustTotals(sizeKBChange, lengthChange) {
548
- return await this._mutex.runExclusive(() => {
549
- this.data.totalSizeKB += sizeKBChange;
550
- this.data.totalLength += lengthChange;
551
- this.data.modifiedAt = Date.now();
552
- });
553
- }
554
-
555
- _loadMetadata() {
556
- const metadata = LocalStorageUtility.getItem(this.metadataKey);
557
- if (metadata) {
558
- for (const collectionName in metadata.collections) {
559
- const collectionData = metadata.collections[collectionName];
560
- const collectionMetadata = new CollectionMetadata(collectionName, this, collectionData);
561
- this.collections.set(collectionName, collectionMetadata);
529
+ let unpacked = this.packedData;
530
+ if (this._encrypted && this.password) {
531
+ unpacked = await this.encryption.decrypt(unpacked, this.password);
562
532
  }
563
- return metadata;
564
- } else {
565
- return {
566
- name: this._dbName,
567
- collections: {},
568
- totalSizeKB: 0,
569
- totalLength: 0,
570
- modifiedAt: Date.now(),
571
- };
533
+ if (this._compressed) {
534
+ unpacked = await this.compression.decompress(unpacked);
535
+ }
536
+ this.data = serializer.deserialize(unpacked);
537
+ return this.data;
538
+ } catch (error) {
539
+ throw new LacertaDBError('Failed to unpack document', 'UNPACK_FAILED', error);
572
540
  }
573
541
  }
574
542
 
575
- get data() {
576
- return this._metadata;
577
- }
578
-
579
- set data(d) {
580
- this._metadata = d;
581
- }
582
-
583
- get name() {
584
- return this.data.name;
585
- }
586
-
587
- get metadataKey() {
588
- return this._metadataKey;
589
- }
590
-
591
- get collections() {
592
- return this._collections;
593
- }
594
-
595
- set collections(c) {
596
- this._collections = c;
597
- }
598
-
599
- get totalSizeKB() {
600
- return this.data.totalSizeKB;
601
- }
602
-
603
- get totalLength() {
604
- return this.data.totalLength;
605
- }
606
-
607
- get modifiedAt() {
608
- return this.data.modifiedAt;
609
- }
610
-
611
- getCollectionMetadata(collectionName) {
612
- if (!this.collections.has(collectionName)) {
613
- const collectionMetadata = new CollectionMetadata(collectionName, this);
614
- this.collections.set(collectionName, collectionMetadata);
615
- this.data.collections[collectionName] = collectionMetadata.getRawMetadata();
616
- this.data.modifiedAt = Date.now();
543
+ packSync() {
544
+ let packed = serializer.serialize(this.data);
545
+ if (this._compressed) {
546
+ packed = this.compression.compressSync(packed);
617
547
  }
618
- return this.collections.get(collectionName);
619
- }
620
-
621
- getCollectionMetadataData(collectionName) {
622
- const metadata = this.getCollectionMetadata(collectionName);
623
- return metadata ? metadata.getRawMetadata(): {}
624
- }
625
-
626
- removeCollectionMetadata(collectionName) {
627
- const collectionMetadata = this.collections.get(collectionName);
628
- if (collectionMetadata) {
629
- this.data.totalSizeKB -= collectionMetadata.sizeKB;
630
- this.data.totalLength -= collectionMetadata.length;
631
- this.collections.delete(collectionName);
632
- delete this.data.collections[collectionName];
633
- this.data.modifiedAt = Date.now();
548
+ if (this._encrypted) {
549
+ throw new LacertaDBError('Synchronous encryption not supported', 'SYNC_ENCRYPT_NOT_SUPPORTED');
634
550
  }
551
+ this.packedData = packed;
552
+ return packed;
635
553
  }
636
554
 
637
- getCollectionNames() {
638
- return Array.from(this.collections.keys());
639
- }
640
-
641
- getRawMetadata() {
642
- for (const [collectionName, collectionMetadata] of this._collections.entries()) {
643
- this.data.collections[collectionName] = collectionMetadata.getRawMetadata();
555
+ unpackSync() {
556
+ let unpacked = this.packedData;
557
+ if (this._encrypted) {
558
+ throw new LacertaDBError('Synchronous decryption not supported', 'SYNC_DECRYPT_NOT_SUPPORTED');
559
+ }
560
+ if (this._compressed) {
561
+ unpacked = this.compression.decompressSync(unpacked);
644
562
  }
563
+ this.data = serializer.deserialize(unpacked);
645
564
  return this.data;
646
565
  }
647
566
 
648
- setRawMetadata(metadata) {
649
- this._metadata = metadata;
650
- this._collections.clear();
651
- for (const collectionName in metadata.collections) {
652
- const collectionData = metadata.collections[collectionName];
653
- const collectionMetadata = new CollectionMetadata(collectionName, this, collectionData);
654
- this._collections.set(collectionName, collectionMetadata);
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;
655
577
  }
578
+ return output;
656
579
  }
657
580
 
658
- get dbName() {
659
- return this._dbName;
660
- }
661
-
662
- get key() {
663
- return this._metadataKey;
664
- }
665
-
666
- toString() {
667
- return `[DatabaseMetadata: ${this.name} | Collections: ${this.collections.size} | Size: ${this.totalSizeKB.toFixed(2)}KB | Documents: ${this.totalLength}]`;
668
- }
669
- }
670
-
671
- class LacertaDBError extends Error {
672
- constructor(message, code, originalError = null) {
673
- super(message);
674
- this.name = 'LacertaDBError';
675
- this.code = code;
676
- this.originalError = originalError;
677
- this.timestamp = Date.now();
678
- }
679
-
680
- toString() {
681
- return `[LacertaDBError ${this.code}: ${this.message} at ${new Date(this.timestamp).toISOString()}]`;
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
+ };
682
592
  }
683
593
  }
684
594
 
685
- const ErrorCodes = {
686
- ATTACHMENT_DELETE_FAILED: 'ATTACHMENT_DELETE_FAILED',
687
- METADATA_SYNC_FAILED: 'METADATA_SYNC_FAILED',
688
- TRANSACTION_FAILED: 'TRANSACTION_FAILED',
689
- ENCRYPTION_FAILED: 'ENCRYPTION_FAILED',
690
- QUOTA_EXCEEDED: 'QUOTA_EXCEEDED',
691
- INVALID_OPERATION: 'INVALID_OPERATION',
692
- DOCUMENT_NOT_FOUND: 'DOCUMENT_NOT_FOUND',
693
- COLLECTION_NOT_FOUND: 'COLLECTION_NOT_FOUND',
694
- COLLECTION_EXISTS: 'COLLECTION_EXISTS'
695
- };
595
+ // ========================
596
+ // Metadata Classes
597
+ // ========================
696
598
 
697
599
  class CollectionMetadata {
698
- constructor(collectionName, databaseMetadata, existingMetadata = null) {
699
- this._collectionName = collectionName;
700
- this._databaseMetadata = databaseMetadata;
701
-
702
- if (existingMetadata) {
703
- this._metadata = existingMetadata;
704
- } else {
705
- this._metadata = {
706
- name: collectionName,
707
- sizeKB: 0,
708
- length: 0,
709
- createdAt: Date.now(),
710
- modifiedAt: Date.now(),
711
- documentSizes: {},
712
- documentModifiedAt: {},
713
- documentPermanent: {},
714
- documentAttachments: {},
715
- };
716
- this._databaseMetadata.data.collections[collectionName] = this._metadata;
717
- this._databaseMetadata.data.modifiedAt = Date.now();
718
- }
719
- }
720
-
721
- get name() {
722
- return this._collectionName;
723
- }
724
-
725
- get keys() {
726
- return Object.keys(this.documentSizes);
727
- }
728
-
729
- get collectionName() {
730
- return this.name;
731
- }
732
-
733
- get sizeKB() {
734
- return this._metadata.sizeKB;
735
- }
736
-
737
- get length() {
738
- return this._metadata.length;
739
- }
740
-
741
- get modifiedAt() {
742
- return this._metadata.modifiedAt;
743
- }
744
-
745
- get metadata() {
746
- return this._metadata;
747
- }
748
-
749
- set metadata(m) {
750
- return this._metadata = m;
751
- }
752
-
753
- get data() {
754
- return this.metadata;
755
- }
756
-
757
- set data(m) {
758
- this.metadata = m;
759
- }
760
-
761
- get databaseMetadata() {
762
- return this._databaseMetadata;
763
- }
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
+ }
611
+
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;
764
617
 
765
- set databaseMetadata(m) {
766
- this._databaseMetadata = m;
618
+ this.sizeKB += sizeKB;
619
+ this.length++;
620
+ this.modifiedAt = Date.now();
767
621
  }
768
622
 
769
- get documentSizes(){return this.metadata.documentSizes;}
770
- get documentModifiedAt(){return this.metadata.documentModifiedAt;}
771
- get documentPermanent(){return this.metadata.documentPermanent;}
772
- get documentAttachments(){return this.metadata.documentAttachments;}
773
-
774
- set documentSizes(v){ this.metadata.documentSizes = v;}
775
- set documentModifiedAt(v){ this.metadata.documentModifiedAt = v;}
776
- set documentPermanent(v){ this.metadata.documentPermanent = v;}
777
- set documentAttachments(v){ this.metadata.documentAttachments = v;}
778
-
779
- updateDocument(docId, docSizeKB, isPermanent = false, attachmentCount = 0) {
780
- const isNewDocument = !this.keys.includes(docId);
781
- const previousDocSizeKB = this.documentSizes[docId] || 0;
782
- const sizeKBChange = docSizeKB - previousDocSizeKB;
783
- const lengthChange = isNewDocument ? 1 : 0;
784
-
785
- this.documentSizes[docId] = docSizeKB;
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;
786
627
  this.documentModifiedAt[docId] = Date.now();
787
- this.documentPermanent[docId] = isPermanent ? 1 : 0;
788
- this.documentAttachments[docId] = attachmentCount;
789
628
 
790
- this.metadata.sizeKB += sizeKBChange;
791
- this.metadata.length += lengthChange;
792
- this.metadata.modifiedAt = Date.now();
793
-
794
- this.databaseMetadata.adjustTotals(sizeKBChange, lengthChange);
795
- }
629
+ if (isPermanent) {
630
+ this.documentPermanent[docId] = true;
631
+ } else {
632
+ delete this.documentPermanent[docId];
633
+ }
796
634
 
797
- deleteDocument(docId) {
798
- const docExists = docId in this.documentSizes;
799
- if (!docExists) {
800
- return false;
635
+ if (attachmentCount > 0) {
636
+ this.documentAttachments[docId] = attachmentCount;
637
+ } else {
638
+ delete this.documentAttachments[docId];
801
639
  }
802
640
 
803
- const docSizeKB = this.documentSizes[docId];
641
+ this.modifiedAt = Date.now();
642
+ }
804
643
 
644
+ removeDocument(docId) {
645
+ const sizeKB = this.documentSizes[docId] || 0;
646
+ if (this.documentSizes[docId]) {
647
+ this.sizeKB -= sizeKB;
648
+ this.length--;
649
+ }
805
650
  delete this.documentSizes[docId];
806
651
  delete this.documentModifiedAt[docId];
807
652
  delete this.documentPermanent[docId];
808
653
  delete this.documentAttachments[docId];
809
-
810
- this.metadata.sizeKB -= docSizeKB;
811
- this.metadata.length -= 1;
812
- this.metadata.modifiedAt = Date.now();
813
-
814
- this.databaseMetadata.adjustTotals(-docSizeKB, -1);
815
-
816
- return true;
817
- }
818
-
819
- updateDocuments(updates) {
820
- for (const { docId, docSizeKB, isPermanent, attachmentCount } of updates) {
821
- this.updateDocument(docId, docSizeKB, isPermanent, attachmentCount || 0);
822
- }
823
- }
824
-
825
- getRawMetadata() {
826
- return this.metadata;
827
- }
828
-
829
- setRawMetadata(metadata) {
830
- this.metadata = metadata;
831
- }
832
-
833
- toString() {
834
- return `[CollectionMetadata: ${this.name} | Size: ${this.sizeKB.toFixed(2)}KB | Documents: ${this.length}]`;
835
- }
836
- }
837
-
838
- class LacertaDBResult {
839
- constructor(success, data = null, error = null) {
840
- this.success = success;
841
- this.data = data;
842
- this.error = error;
654
+ this.modifiedAt = Date.now();
843
655
  }
844
656
 
845
- toString() {
846
- return `[LacertaDBResult success:${this.success} ${this.error ? 'error:' + this.error : ''}]`;
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);
847
663
  }
848
664
  }
849
665
 
850
- class QuickStore {
851
- constructor(dbName) {
852
- this._dbName = dbName;
853
- this._metadataKey = `lacertadb_${this._dbName}_quickstore_metadata`;
854
- this._documentKeyPrefix = `lacertadb_${this._dbName}_quickstore_data_`;
855
- this._metadata = this._loadMetadata();
856
- }
857
-
858
- _loadMetadata() {
859
- const metadata = LocalStorageUtility.getItem(this._metadataKey);
860
- if (metadata) {
861
- return metadata;
862
- } else {
863
- return {
864
- totalSizeKB: 0,
865
- totalLength: 0,
866
- documentSizesKB: {},
867
- documentModificationTime: {},
868
- documentPermanent: {},
869
- };
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
+ }
674
+
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);
685
+ }
870
686
  }
687
+ return new DatabaseMetadata(dbName);
871
688
  }
872
689
 
873
- _saveMetadata() {
874
- LocalStorageUtility.setItem(this._metadataKey, this._metadata);
875
- }
876
-
877
- setDocumentSync(documentData, encryptionKey = null) {
878
- const document = new Document(documentData, encryptionKey);
879
- const packedData = document.packSync();
880
- const docId = document._id;
881
- const isPermanent = document._permanent || false;
882
-
883
- const dataToStore = TURBO.serialize({
884
- _id: document._id,
885
- _created: document._created,
886
- _modified: document._modified,
887
- _permanent: isPermanent,
888
- _encrypted: document._encrypted,
889
- _compressed: document._compressed,
890
- packedData: packedData,
891
- });
892
-
893
- const key = this._documentKeyPrefix + docId;
894
- localStorage.setItem(key, dataToStore);
895
-
896
- const dataSizeKB = dataToStore.length / 1024;
897
-
898
- const isNewDocument = !(docId in this._metadata.documentSizesKB);
899
-
900
- if (isNewDocument) {
901
- this._metadata.totalLength += 1;
902
- } else {
903
- this._metadata.totalSizeKB -= this._metadata.documentSizesKB[docId];
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
698
+ };
699
+ const serializedData = serializer.serialize(dataToStore);
700
+ const encodedData = base64.encode(serializedData);
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);
904
707
  }
905
-
906
- this._metadata.documentSizesKB[docId] = dataSizeKB;
907
- this._metadata.documentModificationTime[docId] = document._modified;
908
- this._metadata.documentPermanent[docId] = isPermanent;
909
-
910
- this._metadata.totalSizeKB += dataSizeKB;
911
-
912
- this._saveMetadata();
913
-
914
- return isNewDocument;
915
708
  }
916
709
 
917
- deleteDocumentSync(docId, force = false) {
918
- const isPermanent = this._metadata.documentPermanent[docId] || false;
919
- if (isPermanent && !force) {
920
- return false;
921
- }
922
-
923
- const key = this._documentKeyPrefix + docId;
924
- const docSizeKB = this._metadata.documentSizesKB[docId] || 0;
925
-
926
- if (localStorage.getItem(key)) {
927
- localStorage.removeItem(key);
928
-
929
- delete this._metadata.documentSizesKB[docId];
930
- delete this._metadata.documentModificationTime[docId];
931
- delete this._metadata.documentPermanent[docId];
932
-
933
- this._metadata.totalSizeKB -= docSizeKB;
934
- this._metadata.totalLength -= 1;
935
-
936
- this._saveMetadata();
937
-
938
- return true;
939
- } else {
940
- return false;
941
- }
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();
942
723
  }
943
724
 
944
- getAllKeys() {
945
- return Object.keys(this._metadata.documentSizesKB);
725
+ removeCollection(collectionName) {
726
+ delete this.collections[collectionName];
727
+ this.recalculate();
728
+ this.save();
946
729
  }
947
730
 
948
- getDocumentSync(docId, encryptionKey = null) {
949
- const key = this._documentKeyPrefix + docId;
950
- const dataString = localStorage.getItem(key);
951
-
952
- if (dataString) {
953
- const docData = TURBO.serialize(dataString);
954
- const document = new Document(docData, encryptionKey);
955
- return document.unpackSync();
956
- } else {
957
- return null;
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;
958
738
  }
959
- }
960
-
961
- toString() {
962
- return `[QuickStore: ${this._dbName} | Size: ${this._metadata.totalSizeKB.toFixed(2)}KB | Documents: ${this._metadata.totalLength}]`;
739
+ this.modifiedAt = Date.now();
963
740
  }
964
741
  }
965
742
 
966
- class TransactionManager {
967
- constructor() {
968
- this.queue = [];
969
- this.processing = false;
970
- }
971
-
972
- async execute(operation) {
973
- return new Promise((resolve, reject) => {
974
- this.queue.push({ operation, resolve, reject });
975
- this.process();
976
- });
977
- }
978
-
979
- async process() {
980
- if (this.processing || this.queue.length === 0) return;
981
-
982
- this.processing = true;
983
-
984
- while (this.queue.length > 0) {
985
- const { operation, resolve, reject } = this.queue.shift();
986
-
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
+ }
752
+
753
+ static load(dbName) {
754
+ const key = `lacertadb_${dbName}_settings`;
755
+ const stored = localStorage.getItem(key);
756
+ if (stored) {
987
757
  try {
988
- const result = await operation();
989
- resolve(result);
990
- } catch (error) {
991
- reject(error);
758
+ const decoded = base64.decode(stored);
759
+ const data = serializer.deserialize(decoded);
760
+ return new Settings(dbName, data);
761
+ } catch (e) {
762
+ console.error('Failed to load settings:', e);
992
763
  }
993
764
  }
765
+ return new Settings(dbName);
766
+ }
994
767
 
995
- this.processing = false;
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);
996
778
  }
997
779
 
998
- toString() {
999
- return `[TransactionManager processing:${this.processing} queue:${this.queue.length}]`;
780
+ updateSettings(newSettings) {
781
+ Object.assign(this, newSettings);
782
+ this.save();
1000
783
  }
1001
784
  }
1002
785
 
1003
- class Observer {
1004
- constructor() {
1005
- this._listeners = {
1006
- 'beforeAdd': [],
1007
- 'afterAdd': [],
1008
- 'beforeDelete': [],
1009
- 'afterDelete': [],
1010
- 'beforeGet': [],
1011
- 'afterGet': [],
1012
- };
786
+ // ========================
787
+ // Quick Store (localStorage based)
788
+ // ========================
789
+
790
+ class QuickStore {
791
+ constructor(dbName) {
792
+ this.dbName = dbName;
793
+ this.keyPrefix = `lacertadb_${dbName}_quickstore_`;
794
+ this.indexKey = `${this.keyPrefix}index`;
1013
795
  }
1014
796
 
1015
- on(event, callback) {
1016
- if (this._listeners[event]) {
1017
- this._listeners[event].push(callback);
1018
- } else {
1019
- throw new Error(`Event "${event}" is not supported.`);
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 [];
1020
805
  }
1021
806
  }
1022
807
 
1023
- off(event, callback) {
1024
- if (this._listeners[event]) {
1025
- const index = this._listeners[event].indexOf(callback);
1026
- if (index > -1) {
1027
- this._listeners[event].splice(index, 1);
1028
- }
1029
- }
808
+ _writeIndex(index) {
809
+ const serializedIndex = serializer.serialize(index);
810
+ const encodedIndex = base64.encode(serializedIndex);
811
+ localStorage.setItem(this.indexKey, encodedIndex);
1030
812
  }
1031
813
 
1032
- _emit(event, ...args) {
1033
- if (this._listeners[event]) {
1034
- for (const callback of this._listeners[event]) {
1035
- callback(...args);
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);
820
+
821
+ const index = this._readIndex();
822
+ if (!index.includes(docId)) {
823
+ index.push(docId);
824
+ this._writeIndex(index);
1036
825
  }
826
+ return true;
827
+ } catch (e) {
828
+ if (e.name === 'QuotaExceededError') {
829
+ throw new LacertaDBError('QuickStore quota exceeded', 'QUOTA_EXCEEDED', e);
830
+ }
831
+ return false;
1037
832
  }
1038
833
  }
1039
834
 
1040
- // FIXED: Add cleanup method
1041
- _cleanup() {
1042
- for (const event in this._listeners) {
1043
- this._listeners[event] = [];
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);
844
+ }
1044
845
  }
846
+ return null;
1045
847
  }
1046
848
 
1047
- toString() {
1048
- const counts = Object.entries(this._listeners)
1049
- .map(([event, listeners]) => `${event}:${listeners.length}`)
1050
- .join(' ');
1051
- return `[Observer listeners:{${counts}}]`;
1052
- }
1053
- }
1054
-
1055
- export class Database {
1056
- constructor(dbName, settings = {}) {
1057
- this._dbName = dbName;
1058
- this._db = null;
1059
- this._collections = new Map();
1060
- this._metadata = new DatabaseMetadata(dbName);
1061
- this._settings = new Settings(dbName, settings);
1062
- this._quickStore = new QuickStore(this._dbName);
1063
- this._settings.init();
1064
- }
1065
-
1066
- get quickStore() {
1067
- return this._quickStore;
849
+ update(docId, data) {
850
+ return this.add(docId, data);
1068
851
  }
1069
852
 
1070
- async init() {
1071
- this.db = await IndexedDBUtility.openDatabase(this.name, undefined, (db, oldVersion, newVersion) => {
1072
- this._upgradeDatabase(db, oldVersion, newVersion);
1073
- });
1074
-
1075
- const collectionNames = this.data.getCollectionNames();
1076
- for (const collectionName of collectionNames) {
1077
- const collection = new Collection(this, collectionName, this.settings);
1078
- await collection.init();
1079
- this.collections.set(collectionName, collection);
1080
- }
1081
- }
853
+ delete(docId) {
854
+ const key = `${this.keyPrefix}data_${docId}`;
855
+ localStorage.removeItem(key);
1082
856
 
1083
- _createDataStores(db) {
1084
- for (const collectionName of this.collections.keys()) {
1085
- this._createDataStore(db, collectionName);
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);
1086
862
  }
1087
863
  }
1088
864
 
1089
- _createDataStore(db, collectionName) {
1090
- if (!db.objectStoreNames.contains(collectionName)) {
1091
- db.createObjectStore(collectionName, { keyPath: '_id' });
1092
- }
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);
1093
871
  }
1094
872
 
1095
- _upgradeDatabase(db, oldVersion, newVersion) {
1096
- console.log(`Upgrading database "${this.name}" from version ${oldVersion} to ${newVersion}`);
1097
- if (!db.objectStoreNames.contains('_metadata')) {
1098
- db.createObjectStore('_metadata', { keyPath: '_id' });
873
+ clear() {
874
+ const index = this._readIndex();
875
+ for (const docId of index) {
876
+ localStorage.removeItem(`${this.keyPrefix}data_${docId}`);
1099
877
  }
878
+ localStorage.removeItem(this.indexKey);
1100
879
  }
880
+ }
1101
881
 
1102
- async createCollection(collectionName) {
1103
- if (this.collections.has(collectionName)) {
1104
- console.log(`Collection "${collectionName}" already exists.`);
1105
- return new LacertaDBResult(
1106
- false,
1107
- this.collections.get(collectionName),
1108
- new LacertaDBError(
1109
- `Collection "${collectionName}" already exists`,
1110
- ErrorCodes.COLLECTION_EXISTS
1111
- )
1112
- );
1113
- }
1114
-
1115
- if (!this.db.objectStoreNames.contains(collectionName)) {
1116
- const newVersion = this.db.version + 1;
1117
- this.db.close();
1118
- this.db = await IndexedDBUtility.openDatabase(this.name, newVersion, (db, oldVersion, newVersion) => {
1119
- this._createDataStore(db, collectionName);
1120
- });
1121
- }
1122
-
1123
- const collection = new Collection(this, collectionName, this.settings);
1124
- await collection.init();
1125
- this.collections.set(collectionName, collection);
1126
-
1127
- this.data.getCollectionMetadata(collectionName);
1128
- await this.data.saveMetadata();
1129
-
1130
- return new LacertaDBResult(true, collection);
1131
- }
1132
-
1133
- // FIXED: Add proper cleanup for observers
1134
- async deleteCollection(collectionName) {
1135
- if (!this.collections.has(collectionName)) {
1136
- return new LacertaDBResult(
1137
- false,
1138
- null,
1139
- new LacertaDBError(
1140
- `Collection "${collectionName}" does not exist`,
1141
- ErrorCodes.COLLECTION_NOT_FOUND
1142
- )
1143
- );
1144
- }
1145
-
1146
- const collection = this.collections.get(collectionName);
1147
-
1148
- // Clean up collection resources
1149
- await collection.close();
1150
- collection.observer._cleanup();
1151
-
1152
- await IndexedDBUtility.performTransaction(this.db, collectionName, 'readwrite', (store) => {
1153
- return IndexedDBUtility.clear(store);
1154
- });
1155
-
1156
- this.collections.delete(collectionName);
1157
- this.data.removeCollectionMetadata(collectionName);
1158
- await this.data.saveMetadata();
882
+ // ========================
883
+ // Query Engine
884
+ // ========================
1159
885
 
1160
- return new LacertaDBResult(true, null);
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;
922
+ }
923
+ },
924
+ '$text': (str, search) => typeof str === 'string' && str.toLowerCase().includes(search.toLowerCase())
925
+ };
1161
926
  }
1162
927
 
1163
- async getCollection(collectionName) {
1164
- if (this.collections.has(collectionName)) {
1165
- return new LacertaDBResult(true, this.collections.get(collectionName));
1166
- } else {
1167
- if (this.db.objectStoreNames.contains(collectionName)) {
1168
- const collection = new Collection(this, collectionName, this.settings);
1169
- await collection.init();
1170
- this.collections.set(collectionName, collection);
1171
- return new LacertaDBResult(true, collection);
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;
1172
935
  } else {
1173
- return new LacertaDBResult(
1174
- false,
1175
- null,
1176
- new LacertaDBError(
1177
- `Collection "${collectionName}" does not exist`,
1178
- ErrorCodes.COLLECTION_NOT_FOUND
1179
- )
1180
- );
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
+ }
947
+ }
948
+ } else {
949
+ // Direct equality comparison
950
+ if (fieldValue !== value) return false;
951
+ }
1181
952
  }
1182
953
  }
954
+ return true;
1183
955
  }
1184
956
 
1185
- async close() {
1186
- const collections = this.collectionsArray;
1187
-
1188
- for (const collection of collections) {
1189
- await collection.close();
1190
- }
1191
-
1192
- if (this.db) {
1193
- this.db.close();
1194
- this.db = null;
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;
963
+ }
964
+ current = current[part];
1195
965
  }
966
+ return current;
1196
967
  }
968
+ }
969
+ const queryEngine = new QueryEngine();
1197
970
 
1198
- async deleteDatabase() {
1199
- await this.close();
1200
- await IndexedDBUtility.deleteDatabase(this.name);
1201
- LocalStorageUtility.removeItem(this.data.metadataKey);
1202
- this.settings.clear();
1203
- this.data = null;
1204
- }
1205
-
1206
- get name() {
1207
- return this._dbName;
1208
- }
1209
-
1210
- get db() {
1211
- return this._db;
1212
- }
1213
-
1214
- set db(db) {
1215
- this._db = db;
1216
- }
1217
-
1218
- get data() {
1219
- return this.metadata;
1220
- }
1221
-
1222
- set data(d) {
1223
- this.metadata = d;
1224
- }
1225
-
1226
- get collectionsArray() {
1227
- return Array.from(this.collections.values());
1228
- }
1229
-
1230
- get collections() {
1231
- return this._collections;
1232
- }
1233
-
1234
- get metadata() {
1235
- return this._metadata;
1236
- }
1237
-
1238
- set metadata(d) {
1239
- this._metadata = d;
1240
- }
1241
-
1242
- get totalSizeKB() {
1243
- return this.data.totalSizeKB;
1244
- }
1245
-
1246
- get totalLength() {
1247
- return this.data.totalLength;
1248
- }
1249
-
1250
- get modifiedAt() {
1251
- return this.data.modifiedAt;
1252
- }
1253
-
1254
- get settings() {
1255
- return this._settings;
1256
- }
1257
971
 
1258
- get settingsData() {
1259
- return this.settings.data;
1260
- }
972
+ // ========================
973
+ // Aggregation Pipeline
974
+ // ========================
1261
975
 
1262
- toString() {
1263
- return `[Database: ${this.name} | Collections: ${this.collections.size} | Size: ${this.totalSizeKB.toFixed(2)}KB | Documents: ${this.totalLength}]`;
1264
- }
1265
- }
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
+ }
992
+ }
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
+ }),
1266
1013
 
1267
- export class Document {
1268
- constructor(data, encryptionKey = null) {
1269
- this._id = data._id || this._generateId();
1270
- this._created = data._created || Date.now();
1271
- this._permanent = data._permanent ? true : false;
1272
- this._encrypted = data._encrypted || (encryptionKey ? true : false);
1273
- this._compressed = data._compressed || false;
1014
+ '$limit': (docs, limit) => docs.slice(0, limit),
1274
1015
 
1275
- this._attachments = data._attachments || data.attachments || [];
1016
+ '$skip': (docs, skip) => docs.slice(skip),
1276
1017
 
1277
- if (data.packedData) {
1278
- this._packedData = data.packedData;
1279
- this._modified = data._modified || Date.now();
1280
- this._data = null;
1281
- } else {
1282
- this._data = data.data || {};
1283
- this._modified = Date.now();
1284
- this._packedData = new Uint8Array(0);
1285
- }
1018
+ '$group': (docs, groupSpec) => {
1019
+ const groups = new Map();
1020
+ const idField = groupSpec._id;
1286
1021
 
1287
- this._encryptionKey = encryptionKey || "";
1288
- }
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
1289
1026
 
1290
- get attachments() {
1291
- return this._attachments;
1292
- }
1027
+ if (!groups.has(groupKey)) {
1028
+ groups.set(groupKey, { _id: groupKey, docs: [] });
1029
+ }
1030
+ groups.get(groupKey).docs.push(doc);
1031
+ }
1293
1032
 
1294
- set attachments(value) {
1295
- this._attachments = value;
1296
- }
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
+ },
1297
1065
 
1298
- get data() {
1299
- return this._data;
1300
- }
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
+ });
1301
1075
 
1302
- get packedData() {
1303
- return this._packedData;
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
+ });
1083
+ }
1084
+ };
1304
1085
  }
1305
1086
 
1306
- get encryptionKey() {
1307
- return this._encryptionKey;
1308
- }
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];
1309
1093
 
1310
- set data(d) {
1311
- this._data = d;
1312
- }
1094
+ if (!stageFunction) {
1095
+ throw new Error(`Unknown aggregation stage: ${stageName}`);
1096
+ }
1313
1097
 
1314
- set packedData(d) {
1315
- this._packedData = d;
1098
+ if (stageName === '$lookup') {
1099
+ result = await stageFunction(result, stageSpec, db);
1100
+ } else {
1101
+ result = stageFunction(result, stageSpec);
1102
+ }
1103
+ }
1104
+ return result;
1316
1105
  }
1106
+ }
1107
+ const aggregationPipeline = new AggregationPipeline();
1317
1108
 
1318
- set encryptionKey(d) {
1319
- this._encryptionKey = d;
1320
- }
1109
+ // ========================
1110
+ // Migration Manager
1111
+ // ========================
1321
1112
 
1322
- static hasAttachments(documentData) {
1323
- return (documentData._attachments && documentData._attachments.length > 0) ||
1324
- (documentData.attachments && documentData.attachments.length > 0);
1113
+ class MigrationManager {
1114
+ constructor(database) {
1115
+ this.database = database;
1116
+ this.migrations = [];
1117
+ this.currentVersion = this.loadVersion();
1325
1118
  }
1326
1119
 
1327
- static async getAttachments(documentData, dbName, collectionName) {
1328
- if (!Document.hasAttachments(documentData)) {
1329
- return [];
1330
- }
1331
-
1332
- const attachmentPaths = documentData._attachments || documentData.attachments;
1333
- documentData.attachments = await OPFSUtility.getAttachments(attachmentPaths);
1334
- return Promise.resolve(documentData);
1120
+ loadVersion() {
1121
+ return localStorage.getItem(`lacertadb_${this.database.name}_version`) || '1.0.0';
1335
1122
  }
1336
1123
 
1337
- static isEncrypted(documentData) {
1338
- return documentData._encrypted && documentData.packedData;
1124
+ saveVersion(version) {
1125
+ localStorage.setItem(`lacertadb_${this.database.name}_version`, version);
1126
+ this.currentVersion = version;
1339
1127
  }
1340
1128
 
1341
- static async decryptDocument(documentData, encryptionKey) {
1342
- if (!Document.isEncrypted(documentData)) {
1343
- throw new Error('Document is not encrypted.');
1344
- }
1345
- const decryptedData = await BrowserEncryptionUtility.decrypt(documentData.packedData, encryptionKey);
1346
- const unpackedData = TURBO.deserialize(decryptedData);
1347
-
1348
- return {
1349
- _id: documentData._id,
1350
- _created: documentData._created,
1351
- _modified: documentData._modified,
1352
- _encrypted: true,
1353
- _compressed: documentData._compressed,
1354
- _permanent: documentData._permanent ? true: false,
1355
- attachments: documentData.attachments,
1356
- data: unpackedData
1357
- };
1129
+ addMigration(migration) {
1130
+ this.migrations.push(migration);
1358
1131
  }
1359
1132
 
1360
- async pack() {
1361
- if (!this.data) {
1362
- throw new Error('No data to pack');
1363
- }
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);
1364
1137
 
1365
- let packedData = TURBO.serialize(this.data);
1366
- if (this._compressed) {
1367
- packedData = await this._compressData(packedData);
1368
- }
1369
- if (this._encrypted) {
1370
- packedData = await this._encryptData(packedData);
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;
1371
1143
  }
1372
- this.packedData = packedData;
1373
- return packedData;
1144
+ return 0;
1374
1145
  }
1375
1146
 
1376
- packSync() {
1377
- if (!this.data) {
1378
- throw new Error('No data to pack');
1379
- }
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));
1380
1152
 
1381
- if (this._encrypted) {
1382
- throw new Error("Packing synchronously a document being encrypted is impossible.")
1383
- }
1384
- if (this._compressed) {
1385
- throw new Error("Packing synchronously a document being compressed is impossible.")
1153
+ for (const migration of applicableMigrations) {
1154
+ await this._applyMigration(migration, 'up');
1155
+ this.saveVersion(migration.version);
1386
1156
  }
1387
-
1388
- let packedData = TURBO.serialize(this.data);
1389
- this.packedData = packedData;
1390
- return packedData;
1391
1157
  }
1392
1158
 
1393
- async unpack() {
1394
- if (!this.data && this.packedData.length > 0) {
1395
- let unpackedData = this.packedData;
1396
- if (this._encrypted) {
1397
- unpackedData = await this._decryptData(unpackedData);
1398
- }
1399
- if (this._compressed) {
1400
- unpackedData = await this._decompressData(unpackedData);
1401
- }
1402
- this.data = TURBO.deserialize(unpackedData);
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));
1165
+
1166
+ for (const migration of applicableMigrations) {
1167
+ await this._applyMigration(migration, 'down');
1403
1168
  }
1404
- return this.data;
1169
+ this.saveVersion(targetVersion);
1405
1170
  }
1406
1171
 
1407
- unpackSync() {
1408
- if (!this.data && this.packedData.length > 0) {
1409
- if (this._encrypted) {
1410
- throw new Error("Unpacking synchronously a document being encrypted is impossible.")
1411
- }
1412
- if (this._compressed) {
1413
- throw new Error("Unpacking synchronously a document being compressed is impossible.")
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);
1182
+ }
1414
1183
  }
1415
- this.data = TURBO.deserialize(this.packedData);
1416
1184
  }
1417
- return this.data;
1418
1185
  }
1186
+ }
1419
1187
 
1420
- async _encryptData(data) {
1421
- const encryptionKey = this.encryptionKey;
1422
- return await BrowserEncryptionUtility.encrypt(data, encryptionKey);
1423
- }
1188
+ // ========================
1189
+ // Performance Monitor
1190
+ // ========================
1424
1191
 
1425
- async _decryptData(data) {
1426
- const encryptionKey = this.encryptionKey;
1427
- return await BrowserEncryptionUtility.decrypt(data, encryptionKey);
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;
1428
1203
  }
1429
1204
 
1430
- async _compressData(data) {
1431
- return await BrowserCompressionUtility.compress(data);
1205
+ startMonitoring() {
1206
+ if (this.monitoring) return;
1207
+ this.monitoring = true;
1208
+ this.monitoringInterval = setInterval(() => this.collectMetrics(), 1000);
1432
1209
  }
1433
1210
 
1434
- async _decompressData(data) {
1435
- return await BrowserCompressionUtility.decompress(data);
1211
+ stopMonitoring() {
1212
+ if (!this.monitoring) return;
1213
+ this.monitoring = false;
1214
+ clearInterval(this.monitoringInterval);
1215
+ this.monitoringInterval = null;
1436
1216
  }
1437
1217
 
1438
- _generateId() {
1439
- return 'xxxx-xxxx-xxxx'.replace(/[x]/g, () => (Math.random() * 16 | 0).toString(16));
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();
1440
1224
  }
1441
1225
 
1442
- async objectOutput(includeAttachments = false) {
1443
- if (!this.data) {
1444
- await this.unpack();
1445
- }
1446
-
1447
- const output = {
1448
- _id: this._id,
1449
- _created: this._created,
1450
- _modified: this._modified,
1451
- _permanent: this._permanent,
1452
- _encrypted: this._encrypted,
1453
- _compressed: this._compressed,
1454
- attachments: this.attachments,
1455
- data: this.data,
1456
- };
1226
+ recordCacheHit() { this.metrics.cacheHits++; }
1227
+ recordCacheMiss() { this.metrics.cacheMisses++; }
1457
1228
 
1458
- if (includeAttachments && this.attachments.length > 0) {
1459
- const attachments = await OPFSUtility.getAttachments(this.attachments);
1460
- output.attachments = attachments;
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();
1461
1239
  }
1462
-
1463
- return output;
1464
1240
  }
1465
1241
 
1466
- async databaseOutput() {
1467
- if (!this.packedData || this.packedData.length === 0) {
1468
- await this.pack();
1469
- }
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;
1470
1252
 
1471
1253
  return {
1472
- _id: this._id,
1473
- _created: this._created,
1474
- _modified: this._modified,
1475
- _permanent: this._permanent ? true : false,
1476
- _compressed: this._compressed,
1477
- _encrypted: this._encrypted,
1478
- attachments: this.attachments,
1479
- packedData: this.packedData,
1254
+ opsPerSec,
1255
+ avgLatency: avgLatency.toFixed(2),
1256
+ cacheHitRate: cacheHitRate.toFixed(1),
1257
+ memoryUsageMB: memoryUsageMB.toFixed(2)
1480
1258
  };
1481
1259
  }
1482
1260
 
1483
- toString() {
1484
- return `[Document: ${this._id} | Created: ${new Date(this._created).toISOString()} | Encrypted: ${this._encrypted} | Compressed: ${this._compressed} | Permanent: ${this._permanent}]`;
1261
+ getOptimizationTips() {
1262
+ const tips = [];
1263
+ const stats = this.getStats();
1264
+
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.');
1276
+ }
1277
+ }
1278
+ return tips.length > 0 ? tips : ['Performance is optimal. No issues detected.'];
1485
1279
  }
1486
1280
  }
1487
1281
 
1488
- class Settings {
1489
- constructor(dbName, newSettings = {}) {
1490
- this._dbName = dbName;
1491
- this._settingsKey = `lacertadb_${this._dbName}_settings`;
1492
- this._data = this._loadSettings();
1493
- this._mergeSettings(newSettings);
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;
1494
1300
  }
1495
1301
 
1496
- init() {
1497
- this.set('sizeLimitKB', this.get('sizeLimitKB') || Infinity);
1498
- this.set('bufferLimitKB', this.get('bufferLimitKB') || -(this.get('sizeLimitKB') * 0.2));
1499
-
1500
- if (this.get('bufferLimitKB') < -0.8 * this.get('sizeLimitKB')) {
1501
- throw new Error("Buffer limit cannot be below -80% of the size limit.");
1502
- }
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 });
1308
+ }
1309
+ // Future index creation logic would go here during version bumps
1310
+ });
1503
1311
 
1504
- this.set('freeSpaceEvery', this._validateFreeSpaceSetting(this.get('freeSpaceEvery')));
1505
- }
1312
+ const metadataData = this.database.metadata.collections[this.name];
1313
+ this.metadata = new CollectionMetadata(this.name, metadataData);
1506
1314
 
1507
- _validateFreeSpaceSetting(value = 10000) {
1508
- if (value === undefined || value === false || value === 0) {
1509
- return Infinity;
1315
+ if (this.settings.freeSpaceEvery > 0) {
1316
+ this.cleanupInterval = setInterval(() => this.freeSpace(), this.settings.freeSpaceEvery);
1510
1317
  }
1511
- if (value < 1000 && value !== 0) {
1512
- throw new Error("Invalid freeSpaceEvery value. It must be 0, Infinity, or above 1000.");
1513
- }
1514
- if (value >= 1000 && value < 10000) {
1515
- console.warn("Warning: freeSpaceEvery value is between 1000 and 10000, which may lead to frequent freeing.");
1516
- }
1517
- return value;
1318
+ return this;
1518
1319
  }
1519
1320
 
1520
- _loadSettings() {
1521
- const settings = LocalStorageUtility.getItem(this.settingsKey);
1522
- return settings ? settings : {};
1523
- }
1321
+ async add(documentData, options = {}) {
1322
+ await this.trigger('beforeAdd', documentData);
1524
1323
 
1525
- _saveSettings() {
1526
- LocalStorageUtility.setItem(this.settingsKey, this.data);
1527
- }
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
+ });
1528
1330
 
1529
- _mergeSettings(newSettings) {
1530
- this.data = Object.assign(this.data, newSettings);
1531
- this._saveSettings();
1532
- }
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
+ }
1533
1340
 
1534
- get(key) {
1535
- return this.data[key];
1536
- }
1341
+ await doc.pack();
1342
+ const dbOutput = doc.databaseOutput();
1343
+ await this.indexedDB.add(this.db, 'documents', dbOutput);
1537
1344
 
1538
- set(key, value) {
1539
- this.data[key] = value;
1540
- this._saveSettings();
1541
- }
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);
1542
1348
 
1543
- remove(key) {
1544
- delete this.data[key];
1545
- this._saveSettings();
1349
+ await this.checkSpaceLimit();
1350
+ await this.trigger('afterAdd', doc);
1351
+ this.queryCache.clear();
1352
+ return doc._id;
1546
1353
  }
1547
1354
 
1548
- clear() {
1549
- this.data = {};
1550
- this._saveSettings();
1551
- }
1355
+ async get(docId, options = {}) {
1356
+ await this.trigger('beforeGet', docId);
1552
1357
 
1553
- get dbName() {
1554
- return this._dbName;
1555
- }
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
+ }
1556
1362
 
1557
- get data() {
1558
- return this._data;
1559
- }
1363
+ const doc = new Document(stored, {
1364
+ password: options.password,
1365
+ encrypted: stored._encrypted,
1366
+ compressed: stored._compressed
1367
+ });
1560
1368
 
1561
- set data(s) {
1562
- this._data = s;
1563
- }
1369
+ if (stored.packedData) {
1370
+ await doc.unpack();
1371
+ }
1564
1372
 
1565
- get settingsKey() {
1566
- return this._settingsKey;
1567
- }
1373
+ if (options.includeAttachments && doc._attachments.length > 0) {
1374
+ doc.data._attachments = await this.opfs.getAttachments(doc._attachments);
1375
+ }
1568
1376
 
1569
- toString() {
1570
- return `[Settings: ${this.dbName} | SizeLimit: ${this.get('sizeLimitKB')}KB | BufferLimit: ${this.get('bufferLimitKB')}KB]`;
1377
+ await this.trigger('afterGet', doc);
1378
+ return doc.objectOutput(options.includeAttachments);
1571
1379
  }
1572
- }
1573
1380
 
1574
- export class Collection {
1575
- constructor(database, collectionName, settings) {
1576
- this._database = database;
1577
- this._collectionName = collectionName;
1578
- this._settings = settings;
1579
- this._metadata = null;
1580
- this._lastFreeSpaceTime = 0;
1581
- this._observer = new Observer();
1582
- this._transactionManager = new TransactionManager();
1583
- this._freeSpaceInterval = null;
1584
- this._freeSpaceHandler = null;
1585
- }
1586
-
1587
- get observer() {
1588
- return this._observer;
1589
- }
1590
-
1591
- async createIndex(fieldPath, options = {}) {
1592
- const indexName = options.name || fieldPath.replace(/\./g, '_');
1593
- const newVersion = this.database.db.version + 1;
1594
- this.database.db.close();
1595
-
1596
- this.database.db = await IndexedDBUtility.openDatabase(
1597
- this.database.name,
1598
- newVersion,
1599
- (db) => {
1600
- if (db.objectStoreNames.contains(this.name)) {
1601
- const tx = db.transaction([this.name]);
1602
- const store = tx.objectStore(this.name);
1603
- if (!store.indexNames.contains(indexName)) {
1604
- store.createIndex(indexName, fieldPath, {
1605
- unique: options.unique || false,
1606
- multiEntry: options.multiEntry || false
1607
- });
1608
- }
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();
1609
1392
  }
1393
+ return doc.objectOutput();
1394
+ } catch (error) {
1395
+ console.error(`Failed to unpack document ${docData._id}:`, error);
1396
+ return null;
1610
1397
  }
1611
- );
1398
+ })).then(docs => docs.filter(Boolean));
1612
1399
  }
1613
1400
 
1614
- async init() {
1615
- // FIXED: Ensure metadata is always accessed through database metadata
1616
- this.metadata = this.database.metadata.getCollectionMetadata(this.name);
1617
-
1618
- if (this.settingsData.freeSpaceEvery !== Infinity) {
1619
- this._freeSpaceHandler = () => this._maybeFreeSpace();
1620
- this._freeSpaceInterval = setInterval(
1621
- this._freeSpaceHandler,
1622
- this.settingsData.freeSpaceEvery
1623
- );
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');
1624
1407
  }
1625
- }
1626
1408
 
1627
- get name() {
1628
- return this._collectionName;
1629
- }
1409
+ const existingDoc = new Document(stored, { password: options.password });
1410
+ if (stored.packedData) await existingDoc.unpack();
1630
1411
 
1631
- get sizes() {
1632
- return this.metadataData.documentSizes || {};
1633
- }
1412
+ const updatedData = { ...existingDoc.data, ...updates };
1634
1413
 
1635
- get modifications() {
1636
- return this.metadataData.documentModifiedAt || {};
1637
- }
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;
1438
+ }
1638
1439
 
1639
- get attachments() {
1640
- return this.metadataData.documentAttachments || {};
1641
- }
1440
+ await doc.pack();
1441
+ const dbOutput = doc.databaseOutput();
1442
+ await this.indexedDB.put(this.db, 'documents', dbOutput);
1642
1443
 
1643
- get permanents() {
1644
- return this.metadataData.documentPermanent || {};
1645
- }
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);
1646
1447
 
1647
- get keys() {
1648
- return Object.keys(this.sizes);
1448
+ await this.trigger('afterUpdate', doc);
1449
+ this.queryCache.clear();
1450
+ return doc._id;
1649
1451
  }
1650
1452
 
1651
- get documentsMetadata() {
1652
- var keys = this.keys;
1653
- var sizes = this.sizes;
1654
- var modifications = this.modifications;
1655
- var permanents = this.permanents;
1656
- var attachments = this.attachments;
1657
- var metadata = new Array(keys.length);
1658
- var i = 0;
1659
- for(const key of keys){
1660
- metadata[i++] = {
1661
- id: key,
1662
- size: sizes[key],
1663
- modified: modifications[key],
1664
- permanent: permanents[key],
1665
- attachment: attachments[key]
1666
- };
1667
- }
1453
+ async delete(docId) {
1454
+ await this.trigger('beforeDelete', docId);
1668
1455
 
1669
- return metadata;
1670
- }
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');
1671
1459
 
1672
- get settings() {
1673
- return this._settings;
1674
- }
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);
1464
+ }
1675
1465
 
1676
- get settingsData() {
1677
- return this.settings.data;
1678
- }
1466
+ this.metadata.removeDocument(docId);
1467
+ this.database.metadata.setCollection(this.metadata);
1679
1468
 
1680
- set settings(d) {
1681
- this._settings = d;
1469
+ await this.trigger('afterDelete', docId);
1470
+ this.queryCache.clear();
1682
1471
  }
1683
1472
 
1684
- get lastFreeSpaceTime() {
1685
- return this._lastFreeSpaceTime;
1686
- }
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);
1687
1477
 
1688
- set lastFreeSpaceTime(t) {
1689
- this._lastFreeSpaceTime = t;
1690
- }
1478
+ if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
1479
+ if (this.performanceMonitor) this.performanceMonitor.recordCacheHit();
1480
+ return cached.data;
1481
+ }
1482
+ if (this.performanceMonitor) this.performanceMonitor.recordCacheMiss();
1691
1483
 
1692
- get database() {
1693
- return this._database;
1694
- }
1484
+ let results = await this.getAll(options);
1485
+ if (Object.keys(filter).length > 0) {
1486
+ results = results.filter(doc => queryEngine.evaluate(doc, filter));
1487
+ }
1695
1488
 
1696
- get metadata() {
1697
- // FIXED: Always access through database metadata for consistency
1698
- return this.database.metadata.getCollectionMetadata(this.name);
1699
- }
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);
1493
+
1494
+ if (this.performanceMonitor) this.performanceMonitor.recordOperation('query', performance.now() - startTime);
1700
1495
 
1701
- get metadataData() {
1702
- return this.metadata.getRawMetadata();
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);
1499
+ }
1500
+ return results;
1703
1501
  }
1704
1502
 
1705
- set metadata(m) {
1706
- this._metadata = m;
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;
1707
1509
  }
1708
1510
 
1709
- get sizeKB() {
1710
- return this.metadataData.sizeKB;
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;
1711
1520
  }
1712
1521
 
1713
- get length() {
1714
- return this.metadataData.length;
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
+ ));
1715
1528
  }
1716
1529
 
1717
- get totalSizeKB() {
1718
- return this.sizeKB;
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
+ ));
1719
1536
  }
1720
1537
 
1721
- get totalLength() {
1722
- return this.length;
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();
1723
1543
  }
1724
1544
 
1725
- get modifiedAt() {
1726
- return this.metadataData.modifiedAt;
1545
+ async checkSpaceLimit() {
1546
+ if (this.settings.sizeLimitKB !== Infinity && this.metadata.sizeKB > this.settings.bufferLimitKB) {
1547
+ await this.freeSpace();
1548
+ }
1727
1549
  }
1728
1550
 
1729
- get isFreeSpaceEnabled() {
1730
- return this.settingsData.freeSpaceEvery !== Infinity;
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);
1557
+ }
1731
1558
  }
1732
1559
 
1733
- get shouldRunFreeSpaceSize() {
1734
- return (this.totalSizeKB > this.settingsData.sizeLimitKB + this.settingsData.bufferLimitKB);
1560
+ on(event, callback) {
1561
+ if (!this.events.has(event)) this.events.set(event, []);
1562
+ this.events.get(event).push(callback);
1735
1563
  }
1736
1564
 
1737
- get shouldRunFreeSpaceTime() {
1738
- return (this.isFreeSpaceEnabled && (Date.now() - this.lastFreeSpaceTime >= this.settingsData.freeSpaceEvery));
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);
1739
1569
  }
1740
1570
 
1741
- async _maybeFreeSpace() {
1742
- if(this.shouldRunFreeSpaceSize || this.shouldRunFreeSpaceTime){
1743
- return this.freeSpace(this.settingsData.sizeLimitKB);
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);
1744
1575
  }
1745
1576
  }
1746
1577
 
1747
- async addDocument(documentData, encryptionKey = null) {
1748
- return await this._transactionManager.execute(async () => {
1749
- this.observer._emit('beforeAdd', documentData);
1750
-
1751
- const document = new Document(documentData, encryptionKey);
1752
-
1753
- const attachmentPaths = [];
1754
- if (Document.hasAttachments(documentData)) {
1755
- const attachments = documentData._attachments || documentData.attachments;
1756
- for (let i = 0; i < attachments.length; i++) {
1757
- attachmentPaths.push(
1758
- `${this.database.name}/${this.name}/${document._id}/${i}`
1759
- );
1760
- }
1761
- document._attachments = attachmentPaths;
1762
- }
1763
-
1764
- const docData = await document.databaseOutput();
1765
- const docId = docData._id;
1766
- const isPermanent = docData._permanent || false;
1767
- const docSizeKB = docData.packedData.byteLength / 1024;
1768
- let isNewDocument = !(docId in this.metadataData.documentSizes);
1769
-
1770
- try {
1771
- await IndexedDBUtility.performTransaction(
1772
- this.database.db,
1773
- this.name,
1774
- 'readwrite',
1775
- (store) => {
1776
- if (isNewDocument) {
1777
- return IndexedDBUtility.add(store, docData);
1778
- } else {
1779
- return IndexedDBUtility.put(store, docData);
1780
- }
1781
- }
1782
- );
1783
-
1784
- if (attachmentPaths.length > 0) {
1785
- const attachments = documentData._attachments || documentData.attachments;
1786
- await OPFSUtility.saveAttachments(
1787
- this.database.name,
1788
- this.name,
1789
- document._id,
1790
- attachments
1791
- );
1792
- }
1793
-
1794
- this.metadata.updateDocument(
1795
- docId,
1796
- docSizeKB,
1797
- isPermanent,
1798
- attachmentPaths.length
1799
- );
1578
+ clearCache() { this.queryCache.clear(); }
1800
1579
 
1801
- await this.database.metadata.saveMetadata();
1802
-
1803
- } catch (error) {
1804
- if (attachmentPaths.length > 0) {
1805
- try {
1806
- await OPFSUtility.deleteAttachments(
1807
- this.database.name,
1808
- this.name,
1809
- document._id
1810
- );
1811
- } catch (e) {
1812
- // Ignore cleanup errors
1813
- }
1814
- }
1815
- throw error;
1816
- }
1817
-
1818
- await this._maybeFreeSpace();
1580
+ destroy() {
1581
+ clearInterval(this.cleanupInterval);
1582
+ if (this.db) {
1583
+ this.db.close();
1584
+ }
1585
+ }
1586
+ }
1819
1587
 
1820
- this.observer._emit('afterAdd', documentData);
1588
+ // ========================
1589
+ // Database Class
1590
+ // ========================
1821
1591
 
1822
- return isNewDocument;
1823
- });
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;
1824
1600
  }
1825
1601
 
1826
- async getDocument(docId, encryptionKey = null, includeAttachments = false) {
1827
- this.observer._emit('beforeGet', docId);
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
+ }
1828
1608
 
1829
- if (!(docId in this.metadataData.documentSizes)) {
1830
- return false;
1609
+ async createCollection(name, options) {
1610
+ if (this.collections.has(name)) {
1611
+ throw new LacertaDBError(`Collection '${name}' already exists.`, 'COLLECTION_EXISTS');
1831
1612
  }
1832
1613
 
1833
- const docData = await IndexedDBUtility.performTransaction(
1834
- this.database.db,
1835
- this.name,
1836
- 'readonly',
1837
- (store) => {
1838
- return IndexedDBUtility.get(store, docId);
1839
- }
1840
- );
1841
-
1842
- if (docData) {
1843
- let document;
1844
- if (Document.isEncrypted(docData)) {
1845
- if (encryptionKey) {
1846
- document = new Document(docData, encryptionKey);
1847
- } else {
1848
- return false;
1849
- }
1850
- } else {
1851
- document = new Document(docData);
1852
- }
1853
-
1854
- const output = await document.objectOutput(includeAttachments);
1855
-
1856
- this.observer._emit('afterGet', output);
1614
+ const collection = new Collection(name, this);
1615
+ await collection.init();
1616
+ this.collections.set(name, collection);
1857
1617
 
1858
- return output;
1859
- } else {
1860
- return false;
1618
+ if (!this.metadata.collections[name]) {
1619
+ this.metadata.setCollection(new CollectionMetadata(name));
1861
1620
  }
1621
+ return collection;
1862
1622
  }
1863
1623
 
1864
- async getDocuments(ids, encryptionKey = null, withAttachments = false) {
1865
- const results = [];
1866
- const existingIds = ids.filter(id => id in this.metadataData.documentSizes);
1867
-
1868
- if (existingIds.length === 0) {
1869
- return results;
1624
+ async getCollection(name) {
1625
+ if (this.collections.has(name)) {
1626
+ return this.collections.get(name);
1870
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
+ }
1871
1636
 
1872
- await IndexedDBUtility.performTransaction(
1873
- this.database.db,
1874
- this.name,
1875
- 'readonly',
1876
- async (store) => {
1877
- const getPromises = existingIds.map(id => IndexedDBUtility.get(store, id));
1878
- const docsData = await Promise.all(getPromises);
1637
+ async dropCollection(name) {
1638
+ if (this.collections.has(name)) {
1639
+ const collection = this.collections.get(name);
1640
+ await collection.clear();
1641
+ collection.destroy();
1642
+ this.collections.delete(name);
1643
+ }
1879
1644
 
1880
- for (const docData of docsData) {
1881
- if (docData) {
1882
- let document;
1883
- if (Document.isEncrypted(docData)) {
1884
- if (encryptionKey) {
1885
- document = new Document(docData, encryptionKey);
1886
- } else {
1887
- continue;
1888
- }
1889
- } else {
1890
- document = new Document(docData);
1891
- }
1645
+ this.metadata.removeCollection(name);
1892
1646
 
1893
- const output = await document.objectOutput(withAttachments);
1894
- results.push(output);
1895
- }
1896
- }
1897
- }
1898
- );
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
+ }
1899
1655
 
1900
- return results;
1656
+ listCollections() {
1657
+ return Object.keys(this.metadata.collections);
1901
1658
  }
1902
1659
 
1903
- async deleteDocument(docId, force = false) {
1904
- this.observer._emit('beforeDelete', docId);
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
+ }
1905
1674
 
1906
- const isPermanent = this.permanents[docId] || false;
1907
- if (isPermanent && !force) {
1908
- return false;
1909
- }
1675
+ updateSettings(newSettings) { this.settings.updateSettings(newSettings); }
1910
1676
 
1911
- const docExists = docId in this.sizes;
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
+ };
1912
1684
 
1913
- if (!docExists) {
1914
- return false;
1685
+ for (const collName of this.listCollections()) {
1686
+ const collection = await this.getCollection(collName);
1687
+ data.collections[collName] = await collection.getAll({ password });
1915
1688
  }
1916
1689
 
1917
- const attachmentCount = this.metadata.documentAttachments[docId] || 0;
1918
- if (attachmentCount > 0) {
1919
- await OPFSUtility.deleteAttachments(
1920
- this.database.name,
1921
- this.name,
1922
- docId
1923
- );
1690
+ if (format === 'json') {
1691
+ const serialized = serializer.serialize(data);
1692
+ return base64.encode(serialized);
1924
1693
  }
1925
-
1926
- await IndexedDBUtility.performTransaction(
1927
- this.database.db,
1928
- this.name,
1929
- 'readwrite',
1930
- (store) => {
1931
- return IndexedDBUtility.delete(store, docId);
1932
- }
1933
- );
1934
-
1935
- this.metadata.deleteDocument(docId);
1936
-
1937
- await this.database.metadata.saveMetadata();
1938
-
1939
- this.observer._emit('afterDelete', docId);
1940
-
1941
- return true;
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');
1942
1701
  }
1943
1702
 
1944
- async freeSpace(size) {
1945
- let spaceToFree;
1946
- this.lastFreeSpaceTime = Date.now();
1947
- const currentSize = this.sizeKB;
1948
-
1949
- if (size >= 0) {
1950
- if (currentSize <= size) {
1951
- return 0;
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);
1952
1711
  } else {
1953
- spaceToFree = currentSize - size;
1712
+ parsed = serializer.deserialize(decoded);
1954
1713
  }
1955
- } else {
1956
- spaceToFree = -size;
1957
- size = currentSize - spaceToFree;
1714
+ } catch (e) {
1715
+ throw new LacertaDBError('Failed to parse import data', 'IMPORT_PARSE_FAILED', e);
1958
1716
  }
1959
1717
 
1960
- const docEntries = Object.entries(this.metadataData.documentModifiedAt)
1961
- .filter(([docId]) => !this.metadataData.documentPermanent[docId])
1962
- .sort((a, b) => a[1] - b[1]);
1963
-
1964
- let totalFreed = 0;
1965
-
1966
- for (const [docId] of docEntries) {
1967
- if(this.sizeKB > size){
1968
- const docSize = this.metadataData.documentSizes[docId];
1969
- totalFreed += docSize;
1970
- await this.deleteDocument(docId, true);
1971
- if (totalFreed >= spaceToFree) {
1972
- break;
1973
- }
1974
- }
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);
1975
1722
  }
1976
1723
 
1977
- return totalFreed;
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
+ };
1978
1729
  }
1979
1730
 
1980
- async query(filter = {}, options = {}) {
1981
- const {
1982
- encryptionKey = null,
1983
- limit = Infinity,
1984
- offset = 0,
1985
- orderBy = null,
1986
- index = null
1987
- } = options;
1988
-
1989
- // FIXED: Collect raw document data first, then process async operations after transaction
1990
- const rawDocuments = [];
1991
-
1992
- await IndexedDBUtility.performTransaction(
1993
- this.database.db,
1994
- this.name,
1995
- 'readonly',
1996
- async (store) => {
1997
- let source = store;
1998
-
1999
- if (index && store.indexNames.contains(index)) {
2000
- source = store.index(index);
2001
- }
2002
-
2003
- const request = orderBy
2004
- ? source.openCursor(null, orderBy === 'asc' ? 'next' : 'prev')
2005
- : source.openCursor();
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
+ }
2006
1738
 
2007
- return new Promise((resolve, reject) => {
2008
- let count = 0;
2009
- let skipped = 0;
1739
+ destroy() {
1740
+ this.collections.forEach(collection => collection.destroy());
1741
+ this.collections.clear();
1742
+ }
1743
+ }
2010
1744
 
2011
- request.onsuccess = (event) => {
2012
- const cursor = event.target.result;
1745
+ // ========================
1746
+ // Main LacertaDB Class
1747
+ // ========================
2013
1748
 
2014
- if (!cursor || count >= limit) {
2015
- resolve();
2016
- return;
2017
- }
1749
+ export class LacertaDB {
1750
+ constructor() {
1751
+ this.databases = new Map();
1752
+ this.performanceMonitor = new PerformanceMonitor();
1753
+ }
2018
1754
 
2019
- if (skipped < offset) {
2020
- skipped++;
2021
- cursor.continue();
2022
- return;
2023
- }
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);
1760
+ }
1761
+ return this.databases.get(name);
1762
+ }
2024
1763
 
2025
- // FIXED: Just collect raw data, no async operations here
2026
- const docData = cursor.value;
2027
- rawDocuments.push(docData);
2028
- count++;
2029
-
2030
- cursor.continue();
2031
- };
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
+ }
2032
1771
 
2033
- request.onerror = () => reject(request.error);
2034
- });
2035
- }
2036
- );
2037
-
2038
- // FIXED: Process documents after transaction is complete
2039
- const results = [];
2040
-
2041
- for (const docData of rawDocuments) {
2042
- let document;
2043
- if (Document.isEncrypted(docData)) {
2044
- if (!encryptionKey) {
2045
- continue; // Skip encrypted documents without key
2046
- }
2047
- document = new Document(docData, encryptionKey);
2048
- } else {
2049
- document = new Document(docData);
1772
+ ['metadata', 'settings', 'version'].forEach(suffix => {
1773
+ localStorage.removeItem(`lacertadb_${name}_${suffix}`);
1774
+ });
1775
+ const quickStore = new QuickStore(name);
1776
+ quickStore.clear();
1777
+ }
1778
+
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);
2050
1787
  }
1788
+ }
1789
+ return [...dbNames];
1790
+ }
2051
1791
 
2052
- // Apply filters if any
2053
- if (Object.keys(filter).length > 0) {
2054
- const object = await document.objectOutput();
2055
- let match = true;
1792
+ async createBackup(password = null) {
1793
+ const backup = {
1794
+ version: '4.0.3',
1795
+ timestamp: Date.now(),
1796
+ databases: {}
1797
+ };
2056
1798
 
2057
- for (const key in filter) {
2058
- const value = key.includes('.')
2059
- ? getNestedValue(object.data, key)
2060
- : object.data[key];
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
+ }
2061
1805
 
2062
- if (value !== filter[key]) {
2063
- match = false;
2064
- break;
2065
- }
2066
- }
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
+ }
2067
1814
 
2068
- if (!match) {
2069
- continue;
2070
- }
1815
+ async restoreBackup(backupData, password = null) {
1816
+ let backup;
1817
+ try {
1818
+ let decodedData = base64.decode(backupData);
1819
+ if (password) {
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);
2071
1825
  }
2072
-
2073
- results.push(await document.objectOutput());
1826
+ } catch (e) {
1827
+ throw new LacertaDBError('Failed to parse backup data', 'BACKUP_PARSE_FAILED', e);
2074
1828
  }
2075
1829
 
2076
- return results;
2077
- }
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);
2078
1835
 
2079
- async close() {
2080
- if (this._freeSpaceInterval) {
2081
- clearInterval(this._freeSpaceInterval);
2082
- this._freeSpaceInterval = null;
2083
- this._freeSpaceHandler = null;
1836
+ results.databases++;
1837
+ results.collections += importResult.collections;
1838
+ results.documents += importResult.documents;
2084
1839
  }
2085
- }
2086
-
2087
- toString() {
2088
- return `[Collection: ${this.name} | Database: ${this.database.name} | Size: ${this.sizeKB.toFixed(2)}KB | Documents: ${this.length}]`;
1840
+ return results;
2089
1841
  }
2090
1842
  }
2091
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
+ };