sehawq.db 4.0.3 → 4.0.5
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/.github/workflows/npm-publish.yml +30 -30
- package/LICENSE +21 -21
- package/index.js +1 -1
- package/package.json +36 -36
- package/readme.md +413 -413
- package/src/core/Database.js +294 -294
- package/src/core/Events.js +285 -285
- package/src/core/IndexManager.js +813 -813
- package/src/core/Persistence.js +375 -375
- package/src/core/QueryEngine.js +447 -447
- package/src/core/Storage.js +321 -321
- package/src/core/Validator.js +324 -324
- package/src/index.js +115 -115
- package/src/performance/Cache.js +338 -338
- package/src/performance/LazyLoader.js +354 -354
- package/src/performance/MemoryManager.js +495 -495
- package/src/server/api.js +687 -687
- package/src/server/websocket.js +527 -527
- package/src/utils/benchmark.js +51 -51
- package/src/utils/dot-notation.js +247 -247
- package/src/utils/helpers.js +275 -275
- package/src/utils/profiler.js +70 -70
- package/src/version.js +37 -37
package/src/core/Persistence.js
CHANGED
|
@@ -1,376 +1,376 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Persistence Layer - Handles data storage and retrieval 💾
|
|
3
|
-
*
|
|
4
|
-
* The bridge between memory and permanent storage
|
|
5
|
-
* Because RAM is great, but it forgets everything when you blink
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs').promises;
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const { performance } = require('perf_hooks');
|
|
11
|
-
|
|
12
|
-
class Persistence {
|
|
13
|
-
constructor(filePath, options = {}) {
|
|
14
|
-
this.filePath = filePath;
|
|
15
|
-
this.options = {
|
|
16
|
-
autoSave: true,
|
|
17
|
-
saveInterval: 5000,
|
|
18
|
-
compression: false,
|
|
19
|
-
encryption: false,
|
|
20
|
-
encryptionKey: null,
|
|
21
|
-
...options
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
this.data = new Map();
|
|
25
|
-
this.isSaving = false;
|
|
26
|
-
this.saveQueue = [];
|
|
27
|
-
this.stats = {
|
|
28
|
-
reads: 0,
|
|
29
|
-
writes: 0,
|
|
30
|
-
saves: 0,
|
|
31
|
-
loads: 0,
|
|
32
|
-
errors: 0
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
this._ensureDirectory();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Ensure data directory exists
|
|
40
|
-
*/
|
|
41
|
-
async _ensureDirectory() {
|
|
42
|
-
try {
|
|
43
|
-
const dir = path.dirname(this.filePath);
|
|
44
|
-
await fs.access(dir);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Load data from storage
|
|
52
|
-
*/
|
|
53
|
-
async load() {
|
|
54
|
-
const startTime = performance.now();
|
|
55
|
-
this.stats.loads++;
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
// Check if file exists
|
|
59
|
-
try {
|
|
60
|
-
await fs.access(this.filePath);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
// File doesn't exist - start with empty data
|
|
63
|
-
this.data.clear();
|
|
64
|
-
return new Map();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Read and parse data
|
|
68
|
-
const fileData = await fs.readFile(this.filePath, 'utf8');
|
|
69
|
-
|
|
70
|
-
let parsedData;
|
|
71
|
-
if (this.options.compression) {
|
|
72
|
-
parsedData = await this._decompress(fileData);
|
|
73
|
-
} else if (this.options.encryption) {
|
|
74
|
-
parsedData = await this._decrypt(fileData);
|
|
75
|
-
} else {
|
|
76
|
-
parsedData = JSON.parse(fileData);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Convert to Map
|
|
80
|
-
this.data.clear();
|
|
81
|
-
for (const [key, value] of Object.entries(parsedData)) {
|
|
82
|
-
this.data.set(key, value);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const loadTime = performance.now() - startTime;
|
|
86
|
-
|
|
87
|
-
if (this.options.debug) {
|
|
88
|
-
console.log(`📁 Loaded ${this.data.size} records in ${loadTime.toFixed(2)}ms`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return this.data;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
this.stats.errors++;
|
|
94
|
-
console.error('🚨 Persistence load error:', error);
|
|
95
|
-
|
|
96
|
-
// Try to recover from backup
|
|
97
|
-
return await this._recoverFromBackup();
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Save data to storage
|
|
103
|
-
*/
|
|
104
|
-
async save(data = null) {
|
|
105
|
-
if (this.isSaving) {
|
|
106
|
-
// Queue the save request
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
this.saveQueue.push({ data, resolve, reject });
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
this.isSaving = true;
|
|
113
|
-
const startTime = performance.now();
|
|
114
|
-
this.stats.saves++;
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
const saveData = data || this.data;
|
|
118
|
-
|
|
119
|
-
// Convert Map to object for JSON serialization
|
|
120
|
-
const serializableData = Object.fromEntries(saveData);
|
|
121
|
-
|
|
122
|
-
let dataToSave;
|
|
123
|
-
if (this.options.compression) {
|
|
124
|
-
dataToSave = await this._compress(serializableData);
|
|
125
|
-
} else if (this.options.encryption) {
|
|
126
|
-
dataToSave = await this._encrypt(serializableData);
|
|
127
|
-
} else {
|
|
128
|
-
dataToSave = JSON.stringify(serializableData, null, 2);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Atomic write: write to temp file then rename
|
|
132
|
-
const tempPath = this.filePath + '.tmp';
|
|
133
|
-
await fs.writeFile(tempPath, dataToSave, 'utf8');
|
|
134
|
-
await fs.rename(tempPath, this.filePath);
|
|
135
|
-
|
|
136
|
-
const saveTime = performance.now() - startTime;
|
|
137
|
-
|
|
138
|
-
if (this.options.debug) {
|
|
139
|
-
console.log(`💾 Saved ${saveData.size} records in ${saveTime.toFixed(2)}ms`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
this.stats.writes++;
|
|
143
|
-
return true;
|
|
144
|
-
} catch (error) {
|
|
145
|
-
this.stats.errors++;
|
|
146
|
-
console.error('🚨 Persistence save error:', error);
|
|
147
|
-
throw error;
|
|
148
|
-
} finally {
|
|
149
|
-
this.isSaving = false;
|
|
150
|
-
|
|
151
|
-
// Process next item in queue
|
|
152
|
-
if (this.saveQueue.length > 0) {
|
|
153
|
-
const next = this.saveQueue.shift();
|
|
154
|
-
this.save(next.data)
|
|
155
|
-
.then(next.resolve)
|
|
156
|
-
.catch(next.reject);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Set value in persistence
|
|
163
|
-
*/
|
|
164
|
-
async set(key, value) {
|
|
165
|
-
this.data.set(key, value);
|
|
166
|
-
|
|
167
|
-
if (this.options.autoSave) {
|
|
168
|
-
await this.save();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Get value from persistence
|
|
176
|
-
*/
|
|
177
|
-
async get(key) {
|
|
178
|
-
this.stats.reads++;
|
|
179
|
-
return this.data.get(key);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Delete value from persistence
|
|
184
|
-
*/
|
|
185
|
-
async delete(key) {
|
|
186
|
-
const deleted = this.data.delete(key);
|
|
187
|
-
|
|
188
|
-
if (deleted && this.options.autoSave) {
|
|
189
|
-
await this.save();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return deleted;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Check if key exists
|
|
197
|
-
*/
|
|
198
|
-
async has(key) {
|
|
199
|
-
return this.data.has(key);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Get all data
|
|
204
|
-
*/
|
|
205
|
-
async getAll() {
|
|
206
|
-
return new Map(this.data);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Clear all data
|
|
211
|
-
*/
|
|
212
|
-
async clear() {
|
|
213
|
-
this.data.clear();
|
|
214
|
-
|
|
215
|
-
if (this.options.autoSave) {
|
|
216
|
-
await this.save();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Compression methods (placeholder - would use zlib in real implementation)
|
|
224
|
-
*/
|
|
225
|
-
async _compress(data) {
|
|
226
|
-
// In a real implementation, this would use zlib or similar
|
|
227
|
-
// For now, just return stringified data
|
|
228
|
-
return JSON.stringify(data);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async _decompress(data) {
|
|
232
|
-
// In a real implementation, this would decompress
|
|
233
|
-
// For now, just parse JSON
|
|
234
|
-
return JSON.parse(data);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Encryption methods (placeholder)
|
|
239
|
-
*/
|
|
240
|
-
async _encrypt(data) {
|
|
241
|
-
if (!this.options.encryptionKey) {
|
|
242
|
-
throw new Error('Encryption key required for encryption');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// In a real implementation, this would use crypto
|
|
246
|
-
// For now, just return stringified data
|
|
247
|
-
return JSON.stringify(data);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async _decrypt(data) {
|
|
251
|
-
if (!this.options.encryptionKey) {
|
|
252
|
-
throw new Error('Encryption key required for decryption');
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// In a real implementation, this would decrypt
|
|
256
|
-
// For now, just parse JSON
|
|
257
|
-
return JSON.parse(data);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Backup and recovery
|
|
262
|
-
*/
|
|
263
|
-
async backup(backupPath = null) {
|
|
264
|
-
const path = backupPath || `${this.filePath}.backup_${Date.now()}`;
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
await this.save();
|
|
268
|
-
await fs.copyFile(this.filePath, path);
|
|
269
|
-
|
|
270
|
-
if (this.options.debug) {
|
|
271
|
-
console.log(`💾 Backup created: ${path}`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return path;
|
|
275
|
-
} catch (error) {
|
|
276
|
-
console.error('🚨 Backup failed:', error);
|
|
277
|
-
throw error;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async _recoverFromBackup() {
|
|
282
|
-
try {
|
|
283
|
-
const dir = path.dirname(this.filePath);
|
|
284
|
-
const files = await fs.readdir(dir);
|
|
285
|
-
const backupFiles = files
|
|
286
|
-
.filter(file => file.startsWith(path.basename(this.filePath) + '.backup_'))
|
|
287
|
-
.sort()
|
|
288
|
-
.reverse();
|
|
289
|
-
|
|
290
|
-
for (const backupFile of backupFiles) {
|
|
291
|
-
try {
|
|
292
|
-
const backupPath = path.join(dir, backupFile);
|
|
293
|
-
await fs.copyFile(backupPath, this.filePath);
|
|
294
|
-
|
|
295
|
-
console.log(`🔧 Recovered from backup: ${backupFile}`);
|
|
296
|
-
return await this.load();
|
|
297
|
-
} catch (error) {
|
|
298
|
-
// Try next backup
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
throw new Error('No valid backup found');
|
|
304
|
-
} catch (error) {
|
|
305
|
-
console.error('🚨 Recovery failed:', error);
|
|
306
|
-
// Return empty data as last resort
|
|
307
|
-
this.data.clear();
|
|
308
|
-
return new Map();
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Get persistence statistics
|
|
314
|
-
*/
|
|
315
|
-
getStats() {
|
|
316
|
-
return {
|
|
317
|
-
...this.stats,
|
|
318
|
-
dataSize: this.data.size,
|
|
319
|
-
filePath: this.filePath,
|
|
320
|
-
isSaving: this.isSaving,
|
|
321
|
-
queueLength: this.saveQueue.length
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Start auto-save interval
|
|
327
|
-
*/
|
|
328
|
-
startAutoSave() {
|
|
329
|
-
if (this.autoSaveInterval) {
|
|
330
|
-
clearInterval(this.autoSaveInterval);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
this.autoSaveInterval = setInterval(() => {
|
|
334
|
-
if (this.data.size > 0) {
|
|
335
|
-
this.save().catch(error => {
|
|
336
|
-
console.error('🚨 Auto-save failed:', error);
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
}, this.options.saveInterval);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Stop auto-save
|
|
344
|
-
*/
|
|
345
|
-
stopAutoSave() {
|
|
346
|
-
if (this.autoSaveInterval) {
|
|
347
|
-
clearInterval(this.autoSaveInterval);
|
|
348
|
-
this.autoSaveInterval = null;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Close persistence (cleanup)
|
|
354
|
-
*/
|
|
355
|
-
async close() {
|
|
356
|
-
this.stopAutoSave();
|
|
357
|
-
|
|
358
|
-
// Process remaining save queue
|
|
359
|
-
while (this.saveQueue.length > 0) {
|
|
360
|
-
const { data, resolve, reject } = this.saveQueue.shift();
|
|
361
|
-
try {
|
|
362
|
-
await this.save(data);
|
|
363
|
-
resolve();
|
|
364
|
-
} catch (error) {
|
|
365
|
-
reject(error);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Final save
|
|
370
|
-
if (this.data.size > 0) {
|
|
371
|
-
await this.save();
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Persistence Layer - Handles data storage and retrieval 💾
|
|
3
|
+
*
|
|
4
|
+
* The bridge between memory and permanent storage
|
|
5
|
+
* Because RAM is great, but it forgets everything when you blink
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs').promises;
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { performance } = require('perf_hooks');
|
|
11
|
+
|
|
12
|
+
class Persistence {
|
|
13
|
+
constructor(filePath, options = {}) {
|
|
14
|
+
this.filePath = filePath;
|
|
15
|
+
this.options = {
|
|
16
|
+
autoSave: true,
|
|
17
|
+
saveInterval: 5000,
|
|
18
|
+
compression: false,
|
|
19
|
+
encryption: false,
|
|
20
|
+
encryptionKey: null,
|
|
21
|
+
...options
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.data = new Map();
|
|
25
|
+
this.isSaving = false;
|
|
26
|
+
this.saveQueue = [];
|
|
27
|
+
this.stats = {
|
|
28
|
+
reads: 0,
|
|
29
|
+
writes: 0,
|
|
30
|
+
saves: 0,
|
|
31
|
+
loads: 0,
|
|
32
|
+
errors: 0
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
this._ensureDirectory();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ensure data directory exists
|
|
40
|
+
*/
|
|
41
|
+
async _ensureDirectory() {
|
|
42
|
+
try {
|
|
43
|
+
const dir = path.dirname(this.filePath);
|
|
44
|
+
await fs.access(dir);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load data from storage
|
|
52
|
+
*/
|
|
53
|
+
async load() {
|
|
54
|
+
const startTime = performance.now();
|
|
55
|
+
this.stats.loads++;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Check if file exists
|
|
59
|
+
try {
|
|
60
|
+
await fs.access(this.filePath);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// File doesn't exist - start with empty data
|
|
63
|
+
this.data.clear();
|
|
64
|
+
return new Map();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Read and parse data
|
|
68
|
+
const fileData = await fs.readFile(this.filePath, 'utf8');
|
|
69
|
+
|
|
70
|
+
let parsedData;
|
|
71
|
+
if (this.options.compression) {
|
|
72
|
+
parsedData = await this._decompress(fileData);
|
|
73
|
+
} else if (this.options.encryption) {
|
|
74
|
+
parsedData = await this._decrypt(fileData);
|
|
75
|
+
} else {
|
|
76
|
+
parsedData = JSON.parse(fileData);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Convert to Map
|
|
80
|
+
this.data.clear();
|
|
81
|
+
for (const [key, value] of Object.entries(parsedData)) {
|
|
82
|
+
this.data.set(key, value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const loadTime = performance.now() - startTime;
|
|
86
|
+
|
|
87
|
+
if (this.options.debug) {
|
|
88
|
+
console.log(`📁 Loaded ${this.data.size} records in ${loadTime.toFixed(2)}ms`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return this.data;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.stats.errors++;
|
|
94
|
+
console.error('🚨 Persistence load error:', error);
|
|
95
|
+
|
|
96
|
+
// Try to recover from backup
|
|
97
|
+
return await this._recoverFromBackup();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Save data to storage
|
|
103
|
+
*/
|
|
104
|
+
async save(data = null) {
|
|
105
|
+
if (this.isSaving) {
|
|
106
|
+
// Queue the save request
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
this.saveQueue.push({ data, resolve, reject });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.isSaving = true;
|
|
113
|
+
const startTime = performance.now();
|
|
114
|
+
this.stats.saves++;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const saveData = data || this.data;
|
|
118
|
+
|
|
119
|
+
// Convert Map to object for JSON serialization
|
|
120
|
+
const serializableData = Object.fromEntries(saveData);
|
|
121
|
+
|
|
122
|
+
let dataToSave;
|
|
123
|
+
if (this.options.compression) {
|
|
124
|
+
dataToSave = await this._compress(serializableData);
|
|
125
|
+
} else if (this.options.encryption) {
|
|
126
|
+
dataToSave = await this._encrypt(serializableData);
|
|
127
|
+
} else {
|
|
128
|
+
dataToSave = JSON.stringify(serializableData, null, 2);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Atomic write: write to temp file then rename
|
|
132
|
+
const tempPath = this.filePath + '.tmp';
|
|
133
|
+
await fs.writeFile(tempPath, dataToSave, 'utf8');
|
|
134
|
+
await fs.rename(tempPath, this.filePath);
|
|
135
|
+
|
|
136
|
+
const saveTime = performance.now() - startTime;
|
|
137
|
+
|
|
138
|
+
if (this.options.debug) {
|
|
139
|
+
console.log(`💾 Saved ${saveData.size} records in ${saveTime.toFixed(2)}ms`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.stats.writes++;
|
|
143
|
+
return true;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
this.stats.errors++;
|
|
146
|
+
console.error('🚨 Persistence save error:', error);
|
|
147
|
+
throw error;
|
|
148
|
+
} finally {
|
|
149
|
+
this.isSaving = false;
|
|
150
|
+
|
|
151
|
+
// Process next item in queue
|
|
152
|
+
if (this.saveQueue.length > 0) {
|
|
153
|
+
const next = this.saveQueue.shift();
|
|
154
|
+
this.save(next.data)
|
|
155
|
+
.then(next.resolve)
|
|
156
|
+
.catch(next.reject);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Set value in persistence
|
|
163
|
+
*/
|
|
164
|
+
async set(key, value) {
|
|
165
|
+
this.data.set(key, value);
|
|
166
|
+
|
|
167
|
+
if (this.options.autoSave) {
|
|
168
|
+
await this.save();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get value from persistence
|
|
176
|
+
*/
|
|
177
|
+
async get(key) {
|
|
178
|
+
this.stats.reads++;
|
|
179
|
+
return this.data.get(key);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Delete value from persistence
|
|
184
|
+
*/
|
|
185
|
+
async delete(key) {
|
|
186
|
+
const deleted = this.data.delete(key);
|
|
187
|
+
|
|
188
|
+
if (deleted && this.options.autoSave) {
|
|
189
|
+
await this.save();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return deleted;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check if key exists
|
|
197
|
+
*/
|
|
198
|
+
async has(key) {
|
|
199
|
+
return this.data.has(key);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get all data
|
|
204
|
+
*/
|
|
205
|
+
async getAll() {
|
|
206
|
+
return new Map(this.data);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Clear all data
|
|
211
|
+
*/
|
|
212
|
+
async clear() {
|
|
213
|
+
this.data.clear();
|
|
214
|
+
|
|
215
|
+
if (this.options.autoSave) {
|
|
216
|
+
await this.save();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Compression methods (placeholder - would use zlib in real implementation)
|
|
224
|
+
*/
|
|
225
|
+
async _compress(data) {
|
|
226
|
+
// In a real implementation, this would use zlib or similar
|
|
227
|
+
// For now, just return stringified data
|
|
228
|
+
return JSON.stringify(data);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async _decompress(data) {
|
|
232
|
+
// In a real implementation, this would decompress
|
|
233
|
+
// For now, just parse JSON
|
|
234
|
+
return JSON.parse(data);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Encryption methods (placeholder)
|
|
239
|
+
*/
|
|
240
|
+
async _encrypt(data) {
|
|
241
|
+
if (!this.options.encryptionKey) {
|
|
242
|
+
throw new Error('Encryption key required for encryption');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// In a real implementation, this would use crypto
|
|
246
|
+
// For now, just return stringified data
|
|
247
|
+
return JSON.stringify(data);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async _decrypt(data) {
|
|
251
|
+
if (!this.options.encryptionKey) {
|
|
252
|
+
throw new Error('Encryption key required for decryption');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// In a real implementation, this would decrypt
|
|
256
|
+
// For now, just parse JSON
|
|
257
|
+
return JSON.parse(data);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Backup and recovery
|
|
262
|
+
*/
|
|
263
|
+
async backup(backupPath = null) {
|
|
264
|
+
const path = backupPath || `${this.filePath}.backup_${Date.now()}`;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await this.save();
|
|
268
|
+
await fs.copyFile(this.filePath, path);
|
|
269
|
+
|
|
270
|
+
if (this.options.debug) {
|
|
271
|
+
console.log(`💾 Backup created: ${path}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return path;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error('🚨 Backup failed:', error);
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async _recoverFromBackup() {
|
|
282
|
+
try {
|
|
283
|
+
const dir = path.dirname(this.filePath);
|
|
284
|
+
const files = await fs.readdir(dir);
|
|
285
|
+
const backupFiles = files
|
|
286
|
+
.filter(file => file.startsWith(path.basename(this.filePath) + '.backup_'))
|
|
287
|
+
.sort()
|
|
288
|
+
.reverse();
|
|
289
|
+
|
|
290
|
+
for (const backupFile of backupFiles) {
|
|
291
|
+
try {
|
|
292
|
+
const backupPath = path.join(dir, backupFile);
|
|
293
|
+
await fs.copyFile(backupPath, this.filePath);
|
|
294
|
+
|
|
295
|
+
console.log(`🔧 Recovered from backup: ${backupFile}`);
|
|
296
|
+
return await this.load();
|
|
297
|
+
} catch (error) {
|
|
298
|
+
// Try next backup
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
throw new Error('No valid backup found');
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('🚨 Recovery failed:', error);
|
|
306
|
+
// Return empty data as last resort
|
|
307
|
+
this.data.clear();
|
|
308
|
+
return new Map();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get persistence statistics
|
|
314
|
+
*/
|
|
315
|
+
getStats() {
|
|
316
|
+
return {
|
|
317
|
+
...this.stats,
|
|
318
|
+
dataSize: this.data.size,
|
|
319
|
+
filePath: this.filePath,
|
|
320
|
+
isSaving: this.isSaving,
|
|
321
|
+
queueLength: this.saveQueue.length
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Start auto-save interval
|
|
327
|
+
*/
|
|
328
|
+
startAutoSave() {
|
|
329
|
+
if (this.autoSaveInterval) {
|
|
330
|
+
clearInterval(this.autoSaveInterval);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.autoSaveInterval = setInterval(() => {
|
|
334
|
+
if (this.data.size > 0) {
|
|
335
|
+
this.save().catch(error => {
|
|
336
|
+
console.error('🚨 Auto-save failed:', error);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}, this.options.saveInterval);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Stop auto-save
|
|
344
|
+
*/
|
|
345
|
+
stopAutoSave() {
|
|
346
|
+
if (this.autoSaveInterval) {
|
|
347
|
+
clearInterval(this.autoSaveInterval);
|
|
348
|
+
this.autoSaveInterval = null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Close persistence (cleanup)
|
|
354
|
+
*/
|
|
355
|
+
async close() {
|
|
356
|
+
this.stopAutoSave();
|
|
357
|
+
|
|
358
|
+
// Process remaining save queue
|
|
359
|
+
while (this.saveQueue.length > 0) {
|
|
360
|
+
const { data, resolve, reject } = this.saveQueue.shift();
|
|
361
|
+
try {
|
|
362
|
+
await this.save(data);
|
|
363
|
+
resolve();
|
|
364
|
+
} catch (error) {
|
|
365
|
+
reject(error);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Final save
|
|
370
|
+
if (this.data.size > 0) {
|
|
371
|
+
await this.save();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
376
|
module.exports = Persistence;
|