@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/browser.js +24 -3
- package/dist/browser.min.js +8 -1
- package/dist/index.min.js +8 -1
- package/index.js +2086 -1715
- package/package.json +3 -2
- package/readme.md +132 -923
package/index.js
CHANGED
|
@@ -1,2091 +1,2462 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
release() {
|
|
40
|
+
this._locked = false;
|
|
41
|
+
this._dispatch();
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
79
|
+
// ========================
|
|
80
|
+
// Compression Utility
|
|
81
|
+
// ========================
|
|
49
82
|
|
|
50
|
-
|
|
83
|
+
class BrowserCompressionUtility {
|
|
84
|
+
async compress(input) {
|
|
51
85
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
91
|
+
throw new LacertaDBError('Compression failed', 'COMPRESSION_FAILED', error);
|
|
66
92
|
}
|
|
67
93
|
}
|
|
68
94
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
89
|
-
|
|
110
|
+
decompressSync(compressedData) {
|
|
111
|
+
return base64.decode(new TextDecoder().decode(compressedData));
|
|
112
|
+
}
|
|
90
113
|
}
|
|
91
|
-
}
|
|
92
114
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const stream = new CompressionStream('deflate');
|
|
97
|
-
const writer = stream.writable.getWriter();
|
|
115
|
+
// ========================
|
|
116
|
+
// Encryption Utility
|
|
117
|
+
// ========================
|
|
98
118
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
125
|
+
var keyMaterial = await crypto.subtle.importKey(
|
|
126
|
+
'raw',
|
|
127
|
+
new TextEncoder().encode(password),
|
|
128
|
+
'PBKDF2',
|
|
129
|
+
false,
|
|
130
|
+
['deriveKey']
|
|
131
|
+
);
|
|
108
132
|
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
147
|
+
var encrypted = await crypto.subtle.encrypt({
|
|
148
|
+
name: 'AES-GCM',
|
|
149
|
+
iv: iv
|
|
150
|
+
},
|
|
151
|
+
key,
|
|
152
|
+
data
|
|
153
|
+
);
|
|
114
154
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
201
|
+
var decrypted = await crypto.subtle.decrypt({
|
|
202
|
+
name: 'AES-GCM',
|
|
203
|
+
iv: iv
|
|
204
|
+
},
|
|
205
|
+
key,
|
|
206
|
+
encryptedData
|
|
207
|
+
);
|
|
171
208
|
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
);
|
|
211
|
-
}
|
|
220
|
+
if (!isValid) {
|
|
221
|
+
throw new Error('Checksum verification failed');
|
|
222
|
+
}
|
|
212
223
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
235
|
+
class OPFSUtility {
|
|
236
|
+
async saveAttachments(dbName, collectionName, documentId, attachments) {
|
|
237
|
+
var attachmentPaths = [];
|
|
238
|
+
var root = await navigator.storage.getDirectory();
|
|
267
239
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
273
|
+
return attachmentPaths;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw new LacertaDBError('Failed to save attachments', 'ATTACHMENT_SAVE_FAILED', error);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
285
278
|
|
|
286
|
-
|
|
287
|
-
|
|
279
|
+
async getAttachments(attachmentPaths) {
|
|
280
|
+
var attachments = [];
|
|
281
|
+
var root = await navigator.storage.getDirectory();
|
|
288
282
|
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
291
|
+
for (var j = 0; j < pathParts.length - 1; j++) {
|
|
292
|
+
currentDir = await currentDir.getDirectoryHandle(pathParts[j]);
|
|
293
|
+
}
|
|
298
294
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
306
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
}
|
|
331
|
+
class IndexedDBUtility {
|
|
332
|
+
constructor() {
|
|
333
|
+
this.transactionQueue = [];
|
|
334
|
+
this.isProcessing = false;
|
|
335
|
+
this.mutex = new AsyncMutex();
|
|
336
|
+
}
|
|
370
337
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
this.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
726
|
-
|
|
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
|
-
|
|
730
|
-
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
558
|
+
// ========================
|
|
559
|
+
// Document Class
|
|
560
|
+
// ========================
|
|
740
561
|
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
588
|
+
generateId() {
|
|
589
|
+
return 'doc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
590
|
+
}
|
|
748
591
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
592
|
+
async pack() {
|
|
593
|
+
try {
|
|
594
|
+
var packed = this.serializer.serialize(this.data);
|
|
752
595
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
596
|
+
if (this._compressed) {
|
|
597
|
+
packed = await this.compression.compress(packed);
|
|
598
|
+
}
|
|
756
599
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
600
|
+
if (this._encrypted && this.password) {
|
|
601
|
+
packed = await this.encryption.encrypt(packed, this.password);
|
|
602
|
+
}
|
|
760
603
|
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
611
|
+
async unpack() {
|
|
612
|
+
try {
|
|
613
|
+
var unpacked = this.packedData;
|
|
768
614
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
set documentAttachments(v){ this.metadata.documentAttachments = v;}
|
|
619
|
+
if (this._compressed) {
|
|
620
|
+
unpacked = await this.compression.decompress(unpacked);
|
|
621
|
+
}
|
|
778
622
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
786
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
633
|
+
if (this._compressed) {
|
|
634
|
+
packed = this.compression.compressSync(packed);
|
|
635
|
+
}
|
|
793
636
|
|
|
794
|
-
|
|
795
|
-
|
|
637
|
+
if (this._encrypted && this.password) {
|
|
638
|
+
throw new LacertaDBError('Synchronous encryption not supported', 'SYNC_ENCRYPT_NOT_SUPPORTED');
|
|
639
|
+
}
|
|
796
640
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
if (!docExists) {
|
|
800
|
-
return false;
|
|
641
|
+
this.packedData = packed;
|
|
642
|
+
return packed;
|
|
801
643
|
}
|
|
802
644
|
|
|
803
|
-
|
|
645
|
+
unpackSync() {
|
|
646
|
+
var unpacked = this.packedData;
|
|
804
647
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
817
|
-
|
|
652
|
+
if (this._compressed) {
|
|
653
|
+
unpacked = this.compression.decompressSync(unpacked);
|
|
654
|
+
}
|
|
818
655
|
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
834
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
846
|
-
|
|
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
|
-
|
|
859
|
-
const metadata = LocalStorageUtility.getItem(this._metadataKey);
|
|
860
|
-
if (metadata) {
|
|
861
|
-
return metadata;
|
|
862
|
-
} else {
|
|
682
|
+
databaseOutput() {
|
|
863
683
|
return {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
this.
|
|
902
|
-
|
|
903
|
-
this.
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
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
|
-
|
|
735
|
+
if (isPermanent) {
|
|
736
|
+
this.documentPermanent[docId] = true;
|
|
737
|
+
} else {
|
|
738
|
+
delete this.documentPermanent[docId];
|
|
739
|
+
}
|
|
913
740
|
|
|
914
|
-
|
|
915
|
-
|
|
741
|
+
if (attachmentCount > 0) {
|
|
742
|
+
this.documentAttachments[docId] = attachmentCount;
|
|
743
|
+
} else {
|
|
744
|
+
delete this.documentAttachments[docId];
|
|
745
|
+
}
|
|
916
746
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
854
|
+
removeCollection(collectionName) {
|
|
855
|
+
delete this.collections[collectionName];
|
|
856
|
+
this.recalculate();
|
|
857
|
+
this.save();
|
|
941
858
|
}
|
|
942
|
-
}
|
|
943
859
|
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
962
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
this.
|
|
975
|
-
|
|
976
|
-
});
|
|
910
|
+
updateSettings(newSettings) {
|
|
911
|
+
Object.assign(this, newSettings);
|
|
912
|
+
this.save();
|
|
913
|
+
}
|
|
977
914
|
}
|
|
978
915
|
|
|
979
|
-
|
|
980
|
-
|
|
916
|
+
// ========================
|
|
917
|
+
// Quick Store (localStorage based)
|
|
918
|
+
// ========================
|
|
981
919
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
this._listeners[event] = [];
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1011
|
+
for (var i = 0; i < index.length; i++) {
|
|
1012
|
+
this.delete(index[i]);
|
|
1013
|
+
}
|
|
1046
1014
|
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
1067
|
-
|
|
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
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
1153
|
-
|
|
1154
|
-
});
|
|
1339
|
+
AggregationPipeline.execute = async function(docs, pipeline, db) {
|
|
1340
|
+
var result = docs;
|
|
1155
1341
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1348
|
+
if (!stageFunction) {
|
|
1349
|
+
throw new Error('Unknown aggregation stage: ' + stageName);
|
|
1350
|
+
}
|
|
1162
1351
|
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1186
|
-
|
|
1359
|
+
return result;
|
|
1360
|
+
}.bind(AggregationPipeline);
|
|
1187
1361
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1362
|
+
// ========================
|
|
1363
|
+
// Migration Manager
|
|
1364
|
+
// ========================
|
|
1191
1365
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
this.
|
|
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
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1373
|
+
loadVersion() {
|
|
1374
|
+
return localStorage.getItem('lacertadb_' + this.database.name + '_version') || '1.0.0';
|
|
1375
|
+
}
|
|
1209
1376
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1377
|
+
saveVersion(version) {
|
|
1378
|
+
localStorage.setItem('lacertadb_' + this.database.name + '_version', version);
|
|
1379
|
+
this.currentVersion = version;
|
|
1380
|
+
}
|
|
1213
1381
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1382
|
+
addMigration(migration) {
|
|
1383
|
+
this.migrations.push(migration);
|
|
1384
|
+
}
|
|
1217
1385
|
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1393
|
+
applicableMigrations.sort(function(a, b) {
|
|
1394
|
+
return self.compareVersions(a.version, b.version);
|
|
1395
|
+
});
|
|
1225
1396
|
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
1235
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1429
|
+
applicableMigrations.sort(function(a, b) {
|
|
1430
|
+
return self.compareVersions(b.version, a.version);
|
|
1431
|
+
});
|
|
1245
1432
|
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
1251
|
-
|
|
1252
|
-
}
|
|
1440
|
+
this.saveVersion(targetVersion);
|
|
1441
|
+
}
|
|
1253
1442
|
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
-
|
|
1259
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
}
|
|
1461
|
+
compareVersions(a, b) {
|
|
1462
|
+
var partsA = a.split('.').map(Number);
|
|
1463
|
+
var partsB = b.split('.').map(Number);
|
|
1266
1464
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
-
|
|
1469
|
+
if (partA > partB) return 1;
|
|
1470
|
+
if (partA < partB) return -1;
|
|
1471
|
+
}
|
|
1276
1472
|
|
|
1277
|
-
|
|
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
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1477
|
+
// ========================
|
|
1478
|
+
// Performance Monitor
|
|
1479
|
+
// ========================
|
|
1293
1480
|
|
|
1294
|
-
|
|
1295
|
-
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1494
|
+
startMonitoring() {
|
|
1495
|
+
this.monitoring = true;
|
|
1496
|
+
this.monitoringInterval = setInterval(this.collectMetrics.bind(this), 1000);
|
|
1497
|
+
}
|
|
1301
1498
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1499
|
+
stopMonitoring() {
|
|
1500
|
+
this.monitoring = false;
|
|
1501
|
+
if (this.monitoringInterval) {
|
|
1502
|
+
clearInterval(this.monitoringInterval);
|
|
1503
|
+
this.monitoringInterval = null;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1305
1506
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1507
|
+
recordOperation(type, duration) {
|
|
1508
|
+
if (!this.monitoring) return;
|
|
1309
1509
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1510
|
+
this.metrics.operations.push({
|
|
1511
|
+
type: type,
|
|
1512
|
+
duration: duration,
|
|
1513
|
+
timestamp: Date.now()
|
|
1514
|
+
});
|
|
1313
1515
|
|
|
1314
|
-
|
|
1315
|
-
this._packedData = d;
|
|
1316
|
-
}
|
|
1516
|
+
this.metrics.latencies.push(duration);
|
|
1317
1517
|
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1526
|
+
recordCacheHit() {
|
|
1527
|
+
this.metrics.cacheHits++;
|
|
1528
|
+
}
|
|
1326
1529
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
return [];
|
|
1530
|
+
recordCacheMiss() {
|
|
1531
|
+
this.metrics.cacheMisses++;
|
|
1330
1532
|
}
|
|
1331
1533
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1543
|
+
if (this.metrics.memoryUsage.length > 60) {
|
|
1544
|
+
this.metrics.memoryUsage.shift();
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1340
1548
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
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
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
-
|
|
1389
|
-
|
|
1390
|
-
return packedData;
|
|
1391
|
-
}
|
|
1576
|
+
getOptimizationTips() {
|
|
1577
|
+
var tips = [];
|
|
1392
1578
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
1400
|
-
|
|
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
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1603
|
+
|
|
1604
|
+
if (tips.length === 0) {
|
|
1605
|
+
tips.push('Performance is optimal. No issues detected.');
|
|
1414
1606
|
}
|
|
1415
|
-
|
|
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
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1613
|
+
// ========================
|
|
1614
|
+
// Collection Class
|
|
1615
|
+
// ========================
|
|
1429
1616
|
|
|
1430
|
-
|
|
1431
|
-
|
|
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
|
-
|
|
1435
|
-
|
|
1436
|
-
}
|
|
1660
|
+
var metadataData = this.database.metadata.collections[this.name];
|
|
1661
|
+
this.metadata = new CollectionMetadata(this.name, metadataData);
|
|
1437
1662
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1663
|
+
if (this.settings.freeSpaceEvery > 0) {
|
|
1664
|
+
this.cleanupInterval = setInterval(this.freeSpace.bind(this), this.settings.freeSpaceEvery);
|
|
1665
|
+
}
|
|
1441
1666
|
|
|
1442
|
-
|
|
1443
|
-
if (!this.data) {
|
|
1444
|
-
await this.unpack();
|
|
1667
|
+
return this;
|
|
1445
1668
|
}
|
|
1446
1669
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
-
|
|
1501
|
-
|
|
1502
|
-
}
|
|
1708
|
+
await this.checkSpaceLimit();
|
|
1709
|
+
await this.trigger('afterAdd', doc);
|
|
1503
1710
|
|
|
1504
|
-
|
|
1505
|
-
}
|
|
1711
|
+
this.queryCache.clear();
|
|
1506
1712
|
|
|
1507
|
-
|
|
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
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
}
|
|
1716
|
+
async get(docId, options) {
|
|
1717
|
+
options = options || {};
|
|
1718
|
+
await this.trigger('beforeGet', docId);
|
|
1524
1719
|
|
|
1525
|
-
|
|
1526
|
-
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1725
|
+
var doc = new Document(stored, {
|
|
1726
|
+
password: options.password,
|
|
1727
|
+
encrypted: stored._encrypted,
|
|
1728
|
+
compressed: stored._compressed
|
|
1729
|
+
});
|
|
1533
1730
|
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1731
|
+
if (stored.packedData) {
|
|
1732
|
+
await doc.unpack();
|
|
1733
|
+
}
|
|
1537
1734
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
1544
|
-
delete this.data[key];
|
|
1545
|
-
this._saveSettings();
|
|
1546
|
-
}
|
|
1740
|
+
await this.trigger('afterGet', doc);
|
|
1547
1741
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
this._saveSettings();
|
|
1551
|
-
}
|
|
1742
|
+
return doc.objectOutput(options.includeAttachments);
|
|
1743
|
+
}
|
|
1552
1744
|
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1768
|
+
documents.push(doc.objectOutput());
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
console.error('Failed to unpack document:', error);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1564
1773
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
}
|
|
1774
|
+
return documents;
|
|
1775
|
+
}
|
|
1568
1776
|
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1784
|
+
var existing = await this.get(docId, {
|
|
1785
|
+
password: options.password
|
|
1786
|
+
});
|
|
1590
1787
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1619
|
-
this.
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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
|
-
|
|
1628
|
-
return this._collectionName;
|
|
1629
|
-
}
|
|
1840
|
+
await this.trigger('afterUpdate', doc);
|
|
1630
1841
|
|
|
1631
|
-
|
|
1632
|
-
return this.metadataData.documentSizes || {};
|
|
1633
|
-
}
|
|
1842
|
+
this.queryCache.clear();
|
|
1634
1843
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
}
|
|
1844
|
+
return doc._id;
|
|
1845
|
+
}
|
|
1638
1846
|
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
}
|
|
1847
|
+
async delete(docId) {
|
|
1848
|
+
await this.trigger('beforeDelete', docId);
|
|
1642
1849
|
|
|
1643
|
-
|
|
1644
|
-
|
|
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
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1855
|
+
if (doc._permanent) {
|
|
1856
|
+
throw new LacertaDBError('Cannot delete permanent document', 'INVALID_OPERATION');
|
|
1857
|
+
}
|
|
1650
1858
|
|
|
1651
|
-
|
|
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
|
-
|
|
1670
|
-
|
|
1861
|
+
if (doc._attachments && doc._attachments.length > 0) {
|
|
1862
|
+
await this.opfs.deleteAttachments(this.database.name, this.name, docId);
|
|
1863
|
+
}
|
|
1671
1864
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
}
|
|
1865
|
+
this.metadata.removeDocument(docId);
|
|
1866
|
+
this.database.metadata.updateCollection(this.metadata);
|
|
1675
1867
|
|
|
1676
|
-
|
|
1677
|
-
return this.settings.data;
|
|
1678
|
-
}
|
|
1868
|
+
await this.trigger('afterDelete', docId);
|
|
1679
1869
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
}
|
|
1870
|
+
this.queryCache.clear();
|
|
1871
|
+
}
|
|
1683
1872
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1873
|
+
async query(filter, options) {
|
|
1874
|
+
filter = filter || {};
|
|
1875
|
+
options = options || {};
|
|
1687
1876
|
|
|
1688
|
-
|
|
1689
|
-
|
|
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
|
-
|
|
1693
|
-
return this._database;
|
|
1694
|
-
}
|
|
1884
|
+
var cached = this.queryCache.get(cacheKey);
|
|
1695
1885
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1893
|
+
if (global.performanceMonitor) {
|
|
1894
|
+
global.performanceMonitor.recordCacheMiss();
|
|
1895
|
+
}
|
|
1704
1896
|
|
|
1705
|
-
|
|
1706
|
-
this._metadata = m;
|
|
1707
|
-
}
|
|
1897
|
+
var startTime = performance.now();
|
|
1708
1898
|
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
-
|
|
1714
|
-
|
|
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
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1918
|
+
var duration = performance.now() - startTime;
|
|
1919
|
+
if (global.performanceMonitor) {
|
|
1920
|
+
global.performanceMonitor.recordOperation('query', duration);
|
|
1921
|
+
}
|
|
1720
1922
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1923
|
+
this.queryCache.set(cacheKey, {
|
|
1924
|
+
data: result,
|
|
1925
|
+
timestamp: Date.now()
|
|
1926
|
+
});
|
|
1724
1927
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1928
|
+
if (this.queryCache.size > 100) {
|
|
1929
|
+
var oldestKey = this.queryCache.keys().next().value;
|
|
1930
|
+
this.queryCache.delete(oldestKey);
|
|
1931
|
+
}
|
|
1728
1932
|
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
}
|
|
1933
|
+
return result;
|
|
1934
|
+
}
|
|
1732
1935
|
|
|
1733
|
-
|
|
1734
|
-
|
|
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
|
-
|
|
1738
|
-
|
|
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
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
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
|
-
|
|
1999
|
+
return results;
|
|
2000
|
+
}
|
|
1752
2001
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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
|
-
|
|
1765
|
-
|
|
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
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
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
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
1795
|
-
|
|
1796
|
-
docSizeKB,
|
|
1797
|
-
isPermanent,
|
|
1798
|
-
attachmentPaths.length
|
|
1799
|
-
);
|
|
2039
|
+
async freeSpace() {
|
|
2040
|
+
var targetSize = this.settings.bufferLimitKB * 0.8;
|
|
1800
2041
|
|
|
1801
|
-
|
|
2042
|
+
while (this.metadata.sizeKB > targetSize) {
|
|
2043
|
+
var oldestDocs = this.metadata.getOldestNonPermanentDocuments(10);
|
|
2044
|
+
if (oldestDocs.length === 0) break;
|
|
1802
2045
|
|
|
1803
|
-
|
|
1804
|
-
|
|
2046
|
+
for (var i = 0; i < oldestDocs.length; i++) {
|
|
2047
|
+
var docId = oldestDocs[i];
|
|
1805
2048
|
try {
|
|
1806
|
-
await
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
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
|
-
|
|
1830
|
-
|
|
2061
|
+
on(event, callback) {
|
|
2062
|
+
if (this.events[event]) {
|
|
2063
|
+
this.events[event].push(callback);
|
|
2064
|
+
}
|
|
1831
2065
|
}
|
|
1832
2066
|
|
|
1833
|
-
|
|
1834
|
-
this.
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
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
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2084
|
+
clearCache() {
|
|
2085
|
+
this.queryCache.clear();
|
|
2086
|
+
}
|
|
1857
2087
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
1873
|
-
this.
|
|
1874
|
-
this.name
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
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
|
-
|
|
1894
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1904
|
-
|
|
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
|
-
|
|
1907
|
-
if (isPermanent && !force) {
|
|
1908
|
-
return false;
|
|
2150
|
+
return this.collections.get(name);
|
|
1909
2151
|
}
|
|
1910
2152
|
|
|
1911
|
-
|
|
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
|
-
|
|
1914
|
-
|
|
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
|
-
|
|
1918
|
-
|
|
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
|
-
|
|
1927
|
-
|
|
1928
|
-
this.
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
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
|
-
|
|
2189
|
+
return {
|
|
2190
|
+
name: this.name,
|
|
2191
|
+
totalSizeKB: this.metadata.totalSizeKB,
|
|
2192
|
+
totalDocuments: this.metadata.totalLength,
|
|
2193
|
+
collections: collectionStats
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
1938
2196
|
|
|
1939
|
-
|
|
2197
|
+
updateSettings(newSettings) {
|
|
2198
|
+
this.settings.updateSettings(newSettings);
|
|
2199
|
+
}
|
|
1940
2200
|
|
|
1941
|
-
|
|
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
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
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
|
-
|
|
1950
|
-
|
|
1951
|
-
return
|
|
1952
|
-
} else {
|
|
1953
|
-
|
|
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
|
-
|
|
1956
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2322
|
+
// ========================
|
|
2323
|
+
// Main LacertaDB Class
|
|
2324
|
+
// ========================
|
|
2006
2325
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2326
|
+
class LacertaDB {
|
|
2327
|
+
constructor() {
|
|
2328
|
+
this.databases = new Map();
|
|
2329
|
+
this.performanceMonitor = new PerformanceMonitor();
|
|
2010
2330
|
|
|
2011
|
-
|
|
2012
|
-
|
|
2331
|
+
if (typeof global !== 'undefined') {
|
|
2332
|
+
global.performanceMonitor = this.performanceMonitor;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2013
2335
|
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
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
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
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
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
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
|
-
|
|
2034
|
-
|
|
2356
|
+
var quickStore = new QuickStore(name);
|
|
2357
|
+
quickStore.clear();
|
|
2035
2358
|
}
|
|
2036
|
-
|
|
2359
|
+
}
|
|
2037
2360
|
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
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
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
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
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
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
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
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
|
-
|
|
2069
|
-
|
|
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
|
|
2074
|
-
|
|
2417
|
+
var results = {
|
|
2418
|
+
databases: 0,
|
|
2419
|
+
collections: 0,
|
|
2420
|
+
documents: 0
|
|
2421
|
+
};
|
|
2075
2422
|
|
|
2076
|
-
|
|
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
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
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) || {});
|