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