sehawq.db 4.0.2 โ 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
|
@@ -1,355 +1,355 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LazyLoader - Loads data only when needed, saves memory like a boss ๐พ
|
|
3
|
-
*
|
|
4
|
-
* Why load everything when you only need some things?
|
|
5
|
-
* This made our memory usage drop faster than my grades in college ๐
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
class LazyLoader {
|
|
9
|
-
constructor(storage, options = {}) {
|
|
10
|
-
this.storage = storage;
|
|
11
|
-
this.options = {
|
|
12
|
-
chunkSize: 100, // Items per chunk
|
|
13
|
-
prefetch: true, // Load next chunk in background
|
|
14
|
-
maxLoadedChunks: 5, // Keep this many chunks in memory
|
|
15
|
-
autoUnload: true, // Unload old chunks automatically
|
|
16
|
-
...options
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// Chunk management
|
|
20
|
-
this.chunks = new Map(); // chunkIndex -> data
|
|
21
|
-
this.chunkIndex = new Map(); // key -> chunkIndex
|
|
22
|
-
this.accessHistory = []; // LRU for chunks
|
|
23
|
-
this.loadedChunksCount = 0;
|
|
24
|
-
|
|
25
|
-
// Performance tracking
|
|
26
|
-
this.stats = {
|
|
27
|
-
chunksLoaded: 0,
|
|
28
|
-
chunksUnloaded: 0,
|
|
29
|
-
keysLoaded: 0,
|
|
30
|
-
memorySaved: 0,
|
|
31
|
-
cacheHits: 0,
|
|
32
|
-
cacheMisses: 0,
|
|
33
|
-
prefetchHits: 0
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
this._initialized = false;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Initialize the lazy loader - build chunk index
|
|
41
|
-
*/
|
|
42
|
-
async initialize(allKeys) {
|
|
43
|
-
if (this._initialized) return;
|
|
44
|
-
|
|
45
|
-
// Build chunk index from all keys
|
|
46
|
-
let chunkIndex = 0;
|
|
47
|
-
let currentChunkSize = 0;
|
|
48
|
-
|
|
49
|
-
for (const key of allKeys) {
|
|
50
|
-
this.chunkIndex.set(key, chunkIndex);
|
|
51
|
-
currentChunkSize++;
|
|
52
|
-
|
|
53
|
-
if (currentChunkSize >= this.options.chunkSize) {
|
|
54
|
-
chunkIndex++;
|
|
55
|
-
currentChunkSize = 0;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
this.totalChunks = chunkIndex + 1;
|
|
60
|
-
this._initialized = true;
|
|
61
|
-
|
|
62
|
-
if (this.options.debug) {
|
|
63
|
-
console.log(`๐ LazyLoader: ${allKeys.length} keys in ${this.totalChunks} chunks`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get value by key - loads chunk if needed
|
|
69
|
-
*/
|
|
70
|
-
async get(key) {
|
|
71
|
-
if (!this._initialized) {
|
|
72
|
-
throw new Error('LazyLoader not initialized. Call initialize() first.');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const chunkIndex = this.chunkIndex.get(key);
|
|
76
|
-
|
|
77
|
-
if (chunkIndex === undefined) {
|
|
78
|
-
this.stats.cacheMisses++;
|
|
79
|
-
return undefined; // Key not found
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Check if chunk is already loaded
|
|
83
|
-
if (this.chunks.has(chunkIndex)) {
|
|
84
|
-
this.stats.cacheHits++;
|
|
85
|
-
this._updateAccessHistory(chunkIndex);
|
|
86
|
-
return this.chunks.get(chunkIndex).get(key);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
this.stats.cacheMisses++;
|
|
90
|
-
|
|
91
|
-
// Load the chunk
|
|
92
|
-
await this._loadChunk(chunkIndex);
|
|
93
|
-
|
|
94
|
-
// Prefetch adjacent chunks in background
|
|
95
|
-
if (this.options.prefetch) {
|
|
96
|
-
this._prefetchAdjacentChunks(chunkIndex);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const chunk = this.chunks.get(chunkIndex);
|
|
100
|
-
return chunk ? chunk.get(key) : undefined;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Set value - updates chunk if loaded, or defers to storage
|
|
105
|
-
*/
|
|
106
|
-
async set(key, value) {
|
|
107
|
-
const chunkIndex = this.chunkIndex.get(key);
|
|
108
|
-
|
|
109
|
-
if (chunkIndex !== undefined && this.chunks.has(chunkIndex)) {
|
|
110
|
-
// Update in loaded chunk
|
|
111
|
-
this.chunks.get(chunkIndex).set(key, value);
|
|
112
|
-
this._updateAccessHistory(chunkIndex);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Always update in storage
|
|
116
|
-
await this.storage.set(key, value);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Check if key exists (without loading chunk)
|
|
121
|
-
*/
|
|
122
|
-
has(key) {
|
|
123
|
-
return this.chunkIndex.has(key);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Get all keys (without loading data)
|
|
128
|
-
*/
|
|
129
|
-
getAllKeys() {
|
|
130
|
-
return Array.from(this.chunkIndex.keys());
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Load a specific chunk into memory
|
|
135
|
-
*/
|
|
136
|
-
async _loadChunk(chunkIndex) {
|
|
137
|
-
// Unload least recently used chunks if we're at the limit
|
|
138
|
-
if (this.loadedChunksCount >= this.options.maxLoadedChunks) {
|
|
139
|
-
await this._unloadLRUChunk();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Get all keys in this chunk
|
|
143
|
-
const chunkKeys = [];
|
|
144
|
-
for (const [key, index] of this.chunkIndex) {
|
|
145
|
-
if (index === chunkIndex) {
|
|
146
|
-
chunkKeys.push(key);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Load data for these keys
|
|
151
|
-
const chunkData = new Map();
|
|
152
|
-
for (const key of chunkKeys) {
|
|
153
|
-
const value = await this.storage.get(key);
|
|
154
|
-
if (value !== undefined) {
|
|
155
|
-
chunkData.set(key, value);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Store the chunk
|
|
160
|
-
this.chunks.set(chunkIndex, chunkData);
|
|
161
|
-
this._updateAccessHistory(chunkIndex);
|
|
162
|
-
this.loadedChunksCount++;
|
|
163
|
-
this.stats.chunksLoaded++;
|
|
164
|
-
this.stats.keysLoaded += chunkData.size;
|
|
165
|
-
|
|
166
|
-
if (this.options.debug) {
|
|
167
|
-
console.log(`๐ Loaded chunk ${chunkIndex} with ${chunkData.size} items`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return chunkData;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Unload least recently used chunk
|
|
175
|
-
*/
|
|
176
|
-
async _unloadLRUChunk() {
|
|
177
|
-
if (this.accessHistory.length === 0) return;
|
|
178
|
-
|
|
179
|
-
const lruChunkIndex = this.accessHistory[0];
|
|
180
|
-
await this._unloadChunk(lruChunkIndex);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Unload specific chunk
|
|
185
|
-
*/
|
|
186
|
-
async _unloadChunk(chunkIndex) {
|
|
187
|
-
const chunk = this.chunks.get(chunkIndex);
|
|
188
|
-
if (!chunk) return;
|
|
189
|
-
|
|
190
|
-
// Calculate memory saved (rough estimate)
|
|
191
|
-
let chunkSize = 0;
|
|
192
|
-
for (const [key, value] of chunk) {
|
|
193
|
-
chunkSize += this._estimateSize(key) + this._estimateSize(value);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
this.chunks.delete(chunkIndex);
|
|
197
|
-
this.accessHistory = this.accessHistory.filter(idx => idx !== chunkIndex);
|
|
198
|
-
this.loadedChunksCount--;
|
|
199
|
-
this.stats.chunksUnloaded++;
|
|
200
|
-
this.stats.memorySaved += chunkSize;
|
|
201
|
-
|
|
202
|
-
if (this.options.debug) {
|
|
203
|
-
console.log(`๐๏ธ Unloaded chunk ${chunkIndex} (saved ~${chunkSize} bytes)`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Prefetch chunks around the currently loaded one
|
|
209
|
-
*/
|
|
210
|
-
_prefetchAdjacentChunks(currentChunkIndex) {
|
|
211
|
-
const prefetchIndices = [
|
|
212
|
-
currentChunkIndex + 1, // Next chunk
|
|
213
|
-
currentChunkIndex - 1 // Previous chunk
|
|
214
|
-
].filter(index => index >= 0 && index < this.totalChunks && !this.chunks.has(index));
|
|
215
|
-
|
|
216
|
-
// Prefetch in background (don't await)
|
|
217
|
-
for (const index of prefetchIndices) {
|
|
218
|
-
this._loadChunk(index).then(() => {
|
|
219
|
-
this.stats.prefetchHits++;
|
|
220
|
-
}).catch(error => {
|
|
221
|
-
console.error(`Prefetch failed for chunk ${index}:`, error);
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Update access history for LRU tracking
|
|
228
|
-
*/
|
|
229
|
-
_updateAccessHistory(chunkIndex) {
|
|
230
|
-
// Remove existing entry
|
|
231
|
-
this.accessHistory = this.accessHistory.filter(idx => idx !== chunkIndex);
|
|
232
|
-
// Add to end (most recently used)
|
|
233
|
-
this.accessHistory.push(chunkIndex);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Estimate size of an object in bytes
|
|
238
|
-
*/
|
|
239
|
-
_estimateSize(obj) {
|
|
240
|
-
if (obj === null || obj === undefined) return 0;
|
|
241
|
-
|
|
242
|
-
switch (typeof obj) {
|
|
243
|
-
case 'string':
|
|
244
|
-
return obj.length * 2; // 2 bytes per character
|
|
245
|
-
case 'number':
|
|
246
|
-
return 8; // 8 bytes for number
|
|
247
|
-
case 'boolean':
|
|
248
|
-
return 4; // 4 bytes for boolean
|
|
249
|
-
case 'object':
|
|
250
|
-
if (Array.isArray(obj)) {
|
|
251
|
-
return obj.reduce((size, item) => size + this._estimateSize(item), 0);
|
|
252
|
-
} else {
|
|
253
|
-
let size = 0;
|
|
254
|
-
for (const key in obj) {
|
|
255
|
-
if (obj.hasOwnProperty(key)) {
|
|
256
|
-
size += this._estimateSize(key) + this._estimateSize(obj[key]);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return size;
|
|
260
|
-
}
|
|
261
|
-
default:
|
|
262
|
-
return 0;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Manually load a chunk (for eager loading)
|
|
268
|
-
*/
|
|
269
|
-
async loadChunk(chunkIndex) {
|
|
270
|
-
return await this._loadChunk(chunkIndex);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Manually unload a chunk
|
|
275
|
-
*/
|
|
276
|
-
async unloadChunk(chunkIndex) {
|
|
277
|
-
return await this._unloadChunk(chunkIndex);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Get currently loaded chunks
|
|
282
|
-
*/
|
|
283
|
-
getLoadedChunks() {
|
|
284
|
-
return Array.from(this.chunks.keys());
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Get chunk information for a key
|
|
289
|
-
*/
|
|
290
|
-
getChunkInfo(key) {
|
|
291
|
-
const chunkIndex = this.chunkIndex.get(key);
|
|
292
|
-
if (chunkIndex === undefined) return null;
|
|
293
|
-
|
|
294
|
-
const isLoaded = this.chunks.has(chunkIndex);
|
|
295
|
-
const keysInChunk = Array.from(this.chunkIndex.entries())
|
|
296
|
-
.filter(([k, idx]) => idx === chunkIndex)
|
|
297
|
-
.map(([k]) => k);
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
chunkIndex,
|
|
301
|
-
isLoaded,
|
|
302
|
-
keysInChunk,
|
|
303
|
-
loadedChunks: this.loadedChunksCount,
|
|
304
|
-
totalChunks: this.totalChunks
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Get performance statistics
|
|
310
|
-
*/
|
|
311
|
-
getStats() {
|
|
312
|
-
const totalAccesses = this.stats.cacheHits + this.stats.cacheMisses;
|
|
313
|
-
const hitRate = totalAccesses > 0
|
|
314
|
-
? (this.stats.cacheHits / totalAccesses * 100).toFixed(2)
|
|
315
|
-
: 0;
|
|
316
|
-
|
|
317
|
-
return {
|
|
318
|
-
...this.stats,
|
|
319
|
-
hitRate: `${hitRate}%`,
|
|
320
|
-
loadedChunks: this.loadedChunksCount,
|
|
321
|
-
totalChunks: this.totalChunks,
|
|
322
|
-
memorySaved: `${(this.stats.memorySaved / 1024 / 1024).toFixed(2)} MB`,
|
|
323
|
-
prefetchEffectiveness: this.stats.prefetchHits > 0
|
|
324
|
-
? `${((this.stats.prefetchHits / this.stats.chunksLoaded) * 100).toFixed(1)}%`
|
|
325
|
-
: '0%'
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Clear all loaded chunks
|
|
331
|
-
*/
|
|
332
|
-
clear() {
|
|
333
|
-
this.chunks.clear();
|
|
334
|
-
this.accessHistory = [];
|
|
335
|
-
this.loadedChunksCount = 0;
|
|
336
|
-
|
|
337
|
-
if (this.options.debug) {
|
|
338
|
-
console.log('๐งน Cleared all loaded chunks');
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Preload specific chunks (for startup optimization)
|
|
344
|
-
*/
|
|
345
|
-
async preloadChunks(chunkIndices) {
|
|
346
|
-
const loadPromises = chunkIndices.map(index => this._loadChunk(index));
|
|
347
|
-
await Promise.all(loadPromises);
|
|
348
|
-
|
|
349
|
-
if (this.options.debug) {
|
|
350
|
-
console.log(`๐ฅ Preloaded ${chunkIndices.length} chunks`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
1
|
+
/**
|
|
2
|
+
* LazyLoader - Loads data only when needed, saves memory like a boss ๐พ
|
|
3
|
+
*
|
|
4
|
+
* Why load everything when you only need some things?
|
|
5
|
+
* This made our memory usage drop faster than my grades in college ๐
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class LazyLoader {
|
|
9
|
+
constructor(storage, options = {}) {
|
|
10
|
+
this.storage = storage;
|
|
11
|
+
this.options = {
|
|
12
|
+
chunkSize: 100, // Items per chunk
|
|
13
|
+
prefetch: true, // Load next chunk in background
|
|
14
|
+
maxLoadedChunks: 5, // Keep this many chunks in memory
|
|
15
|
+
autoUnload: true, // Unload old chunks automatically
|
|
16
|
+
...options
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Chunk management
|
|
20
|
+
this.chunks = new Map(); // chunkIndex -> data
|
|
21
|
+
this.chunkIndex = new Map(); // key -> chunkIndex
|
|
22
|
+
this.accessHistory = []; // LRU for chunks
|
|
23
|
+
this.loadedChunksCount = 0;
|
|
24
|
+
|
|
25
|
+
// Performance tracking
|
|
26
|
+
this.stats = {
|
|
27
|
+
chunksLoaded: 0,
|
|
28
|
+
chunksUnloaded: 0,
|
|
29
|
+
keysLoaded: 0,
|
|
30
|
+
memorySaved: 0,
|
|
31
|
+
cacheHits: 0,
|
|
32
|
+
cacheMisses: 0,
|
|
33
|
+
prefetchHits: 0
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
this._initialized = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialize the lazy loader - build chunk index
|
|
41
|
+
*/
|
|
42
|
+
async initialize(allKeys) {
|
|
43
|
+
if (this._initialized) return;
|
|
44
|
+
|
|
45
|
+
// Build chunk index from all keys
|
|
46
|
+
let chunkIndex = 0;
|
|
47
|
+
let currentChunkSize = 0;
|
|
48
|
+
|
|
49
|
+
for (const key of allKeys) {
|
|
50
|
+
this.chunkIndex.set(key, chunkIndex);
|
|
51
|
+
currentChunkSize++;
|
|
52
|
+
|
|
53
|
+
if (currentChunkSize >= this.options.chunkSize) {
|
|
54
|
+
chunkIndex++;
|
|
55
|
+
currentChunkSize = 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.totalChunks = chunkIndex + 1;
|
|
60
|
+
this._initialized = true;
|
|
61
|
+
|
|
62
|
+
if (this.options.debug) {
|
|
63
|
+
console.log(`๐ LazyLoader: ${allKeys.length} keys in ${this.totalChunks} chunks`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get value by key - loads chunk if needed
|
|
69
|
+
*/
|
|
70
|
+
async get(key) {
|
|
71
|
+
if (!this._initialized) {
|
|
72
|
+
throw new Error('LazyLoader not initialized. Call initialize() first.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const chunkIndex = this.chunkIndex.get(key);
|
|
76
|
+
|
|
77
|
+
if (chunkIndex === undefined) {
|
|
78
|
+
this.stats.cacheMisses++;
|
|
79
|
+
return undefined; // Key not found
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if chunk is already loaded
|
|
83
|
+
if (this.chunks.has(chunkIndex)) {
|
|
84
|
+
this.stats.cacheHits++;
|
|
85
|
+
this._updateAccessHistory(chunkIndex);
|
|
86
|
+
return this.chunks.get(chunkIndex).get(key);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.stats.cacheMisses++;
|
|
90
|
+
|
|
91
|
+
// Load the chunk
|
|
92
|
+
await this._loadChunk(chunkIndex);
|
|
93
|
+
|
|
94
|
+
// Prefetch adjacent chunks in background
|
|
95
|
+
if (this.options.prefetch) {
|
|
96
|
+
this._prefetchAdjacentChunks(chunkIndex);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const chunk = this.chunks.get(chunkIndex);
|
|
100
|
+
return chunk ? chunk.get(key) : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Set value - updates chunk if loaded, or defers to storage
|
|
105
|
+
*/
|
|
106
|
+
async set(key, value) {
|
|
107
|
+
const chunkIndex = this.chunkIndex.get(key);
|
|
108
|
+
|
|
109
|
+
if (chunkIndex !== undefined && this.chunks.has(chunkIndex)) {
|
|
110
|
+
// Update in loaded chunk
|
|
111
|
+
this.chunks.get(chunkIndex).set(key, value);
|
|
112
|
+
this._updateAccessHistory(chunkIndex);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Always update in storage
|
|
116
|
+
await this.storage.set(key, value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if key exists (without loading chunk)
|
|
121
|
+
*/
|
|
122
|
+
has(key) {
|
|
123
|
+
return this.chunkIndex.has(key);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get all keys (without loading data)
|
|
128
|
+
*/
|
|
129
|
+
getAllKeys() {
|
|
130
|
+
return Array.from(this.chunkIndex.keys());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Load a specific chunk into memory
|
|
135
|
+
*/
|
|
136
|
+
async _loadChunk(chunkIndex) {
|
|
137
|
+
// Unload least recently used chunks if we're at the limit
|
|
138
|
+
if (this.loadedChunksCount >= this.options.maxLoadedChunks) {
|
|
139
|
+
await this._unloadLRUChunk();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get all keys in this chunk
|
|
143
|
+
const chunkKeys = [];
|
|
144
|
+
for (const [key, index] of this.chunkIndex) {
|
|
145
|
+
if (index === chunkIndex) {
|
|
146
|
+
chunkKeys.push(key);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Load data for these keys
|
|
151
|
+
const chunkData = new Map();
|
|
152
|
+
for (const key of chunkKeys) {
|
|
153
|
+
const value = await this.storage.get(key);
|
|
154
|
+
if (value !== undefined) {
|
|
155
|
+
chunkData.set(key, value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Store the chunk
|
|
160
|
+
this.chunks.set(chunkIndex, chunkData);
|
|
161
|
+
this._updateAccessHistory(chunkIndex);
|
|
162
|
+
this.loadedChunksCount++;
|
|
163
|
+
this.stats.chunksLoaded++;
|
|
164
|
+
this.stats.keysLoaded += chunkData.size;
|
|
165
|
+
|
|
166
|
+
if (this.options.debug) {
|
|
167
|
+
console.log(`๐ Loaded chunk ${chunkIndex} with ${chunkData.size} items`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return chunkData;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Unload least recently used chunk
|
|
175
|
+
*/
|
|
176
|
+
async _unloadLRUChunk() {
|
|
177
|
+
if (this.accessHistory.length === 0) return;
|
|
178
|
+
|
|
179
|
+
const lruChunkIndex = this.accessHistory[0];
|
|
180
|
+
await this._unloadChunk(lruChunkIndex);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Unload specific chunk
|
|
185
|
+
*/
|
|
186
|
+
async _unloadChunk(chunkIndex) {
|
|
187
|
+
const chunk = this.chunks.get(chunkIndex);
|
|
188
|
+
if (!chunk) return;
|
|
189
|
+
|
|
190
|
+
// Calculate memory saved (rough estimate)
|
|
191
|
+
let chunkSize = 0;
|
|
192
|
+
for (const [key, value] of chunk) {
|
|
193
|
+
chunkSize += this._estimateSize(key) + this._estimateSize(value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.chunks.delete(chunkIndex);
|
|
197
|
+
this.accessHistory = this.accessHistory.filter(idx => idx !== chunkIndex);
|
|
198
|
+
this.loadedChunksCount--;
|
|
199
|
+
this.stats.chunksUnloaded++;
|
|
200
|
+
this.stats.memorySaved += chunkSize;
|
|
201
|
+
|
|
202
|
+
if (this.options.debug) {
|
|
203
|
+
console.log(`๐๏ธ Unloaded chunk ${chunkIndex} (saved ~${chunkSize} bytes)`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Prefetch chunks around the currently loaded one
|
|
209
|
+
*/
|
|
210
|
+
_prefetchAdjacentChunks(currentChunkIndex) {
|
|
211
|
+
const prefetchIndices = [
|
|
212
|
+
currentChunkIndex + 1, // Next chunk
|
|
213
|
+
currentChunkIndex - 1 // Previous chunk
|
|
214
|
+
].filter(index => index >= 0 && index < this.totalChunks && !this.chunks.has(index));
|
|
215
|
+
|
|
216
|
+
// Prefetch in background (don't await)
|
|
217
|
+
for (const index of prefetchIndices) {
|
|
218
|
+
this._loadChunk(index).then(() => {
|
|
219
|
+
this.stats.prefetchHits++;
|
|
220
|
+
}).catch(error => {
|
|
221
|
+
console.error(`Prefetch failed for chunk ${index}:`, error);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Update access history for LRU tracking
|
|
228
|
+
*/
|
|
229
|
+
_updateAccessHistory(chunkIndex) {
|
|
230
|
+
// Remove existing entry
|
|
231
|
+
this.accessHistory = this.accessHistory.filter(idx => idx !== chunkIndex);
|
|
232
|
+
// Add to end (most recently used)
|
|
233
|
+
this.accessHistory.push(chunkIndex);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Estimate size of an object in bytes
|
|
238
|
+
*/
|
|
239
|
+
_estimateSize(obj) {
|
|
240
|
+
if (obj === null || obj === undefined) return 0;
|
|
241
|
+
|
|
242
|
+
switch (typeof obj) {
|
|
243
|
+
case 'string':
|
|
244
|
+
return obj.length * 2; // 2 bytes per character
|
|
245
|
+
case 'number':
|
|
246
|
+
return 8; // 8 bytes for number
|
|
247
|
+
case 'boolean':
|
|
248
|
+
return 4; // 4 bytes for boolean
|
|
249
|
+
case 'object':
|
|
250
|
+
if (Array.isArray(obj)) {
|
|
251
|
+
return obj.reduce((size, item) => size + this._estimateSize(item), 0);
|
|
252
|
+
} else {
|
|
253
|
+
let size = 0;
|
|
254
|
+
for (const key in obj) {
|
|
255
|
+
if (obj.hasOwnProperty(key)) {
|
|
256
|
+
size += this._estimateSize(key) + this._estimateSize(obj[key]);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return size;
|
|
260
|
+
}
|
|
261
|
+
default:
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Manually load a chunk (for eager loading)
|
|
268
|
+
*/
|
|
269
|
+
async loadChunk(chunkIndex) {
|
|
270
|
+
return await this._loadChunk(chunkIndex);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Manually unload a chunk
|
|
275
|
+
*/
|
|
276
|
+
async unloadChunk(chunkIndex) {
|
|
277
|
+
return await this._unloadChunk(chunkIndex);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get currently loaded chunks
|
|
282
|
+
*/
|
|
283
|
+
getLoadedChunks() {
|
|
284
|
+
return Array.from(this.chunks.keys());
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get chunk information for a key
|
|
289
|
+
*/
|
|
290
|
+
getChunkInfo(key) {
|
|
291
|
+
const chunkIndex = this.chunkIndex.get(key);
|
|
292
|
+
if (chunkIndex === undefined) return null;
|
|
293
|
+
|
|
294
|
+
const isLoaded = this.chunks.has(chunkIndex);
|
|
295
|
+
const keysInChunk = Array.from(this.chunkIndex.entries())
|
|
296
|
+
.filter(([k, idx]) => idx === chunkIndex)
|
|
297
|
+
.map(([k]) => k);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
chunkIndex,
|
|
301
|
+
isLoaded,
|
|
302
|
+
keysInChunk,
|
|
303
|
+
loadedChunks: this.loadedChunksCount,
|
|
304
|
+
totalChunks: this.totalChunks
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get performance statistics
|
|
310
|
+
*/
|
|
311
|
+
getStats() {
|
|
312
|
+
const totalAccesses = this.stats.cacheHits + this.stats.cacheMisses;
|
|
313
|
+
const hitRate = totalAccesses > 0
|
|
314
|
+
? (this.stats.cacheHits / totalAccesses * 100).toFixed(2)
|
|
315
|
+
: 0;
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
...this.stats,
|
|
319
|
+
hitRate: `${hitRate}%`,
|
|
320
|
+
loadedChunks: this.loadedChunksCount,
|
|
321
|
+
totalChunks: this.totalChunks,
|
|
322
|
+
memorySaved: `${(this.stats.memorySaved / 1024 / 1024).toFixed(2)} MB`,
|
|
323
|
+
prefetchEffectiveness: this.stats.prefetchHits > 0
|
|
324
|
+
? `${((this.stats.prefetchHits / this.stats.chunksLoaded) * 100).toFixed(1)}%`
|
|
325
|
+
: '0%'
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Clear all loaded chunks
|
|
331
|
+
*/
|
|
332
|
+
clear() {
|
|
333
|
+
this.chunks.clear();
|
|
334
|
+
this.accessHistory = [];
|
|
335
|
+
this.loadedChunksCount = 0;
|
|
336
|
+
|
|
337
|
+
if (this.options.debug) {
|
|
338
|
+
console.log('๐งน Cleared all loaded chunks');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Preload specific chunks (for startup optimization)
|
|
344
|
+
*/
|
|
345
|
+
async preloadChunks(chunkIndices) {
|
|
346
|
+
const loadPromises = chunkIndices.map(index => this._loadChunk(index));
|
|
347
|
+
await Promise.all(loadPromises);
|
|
348
|
+
|
|
349
|
+
if (this.options.debug) {
|
|
350
|
+
console.log(`๐ฅ Preloaded ${chunkIndices.length} chunks`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
355
|
module.exports = LazyLoader;
|