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