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