@hkdigital/lib-sveltekit 0.1.70 → 0.1.72
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/dist/classes/cache/IndexedDbCache.d.ts +212 -0
- package/dist/classes/cache/IndexedDbCache.js +673 -0
- package/dist/classes/cache/MemoryResponseCache.d.ts +101 -14
- package/dist/classes/cache/MemoryResponseCache.js +97 -12
- package/dist/classes/cache/index.d.ts +1 -1
- package/dist/classes/cache/index.js +2 -1
- package/dist/classes/events/EventEmitter.d.ts +142 -0
- package/dist/classes/events/EventEmitter.js +275 -0
- package/dist/classes/events/index.d.ts +1 -0
- package/dist/classes/events/index.js +2 -0
- package/dist/classes/logging/Logger.d.ts +74 -0
- package/dist/classes/logging/Logger.js +158 -0
- package/dist/classes/logging/constants.d.ts +14 -0
- package/dist/classes/logging/constants.js +18 -0
- package/dist/classes/logging/index.d.ts +2 -0
- package/dist/classes/logging/index.js +4 -0
- package/dist/classes/services/ServiceBase.d.ts +153 -0
- package/dist/classes/services/ServiceBase.js +409 -0
- package/dist/classes/services/ServiceManager.d.ts +350 -0
- package/dist/classes/services/ServiceManager.js +1114 -0
- package/dist/classes/services/constants.d.ts +11 -0
- package/dist/classes/services/constants.js +12 -0
- package/dist/classes/services/index.d.ts +3 -0
- package/dist/classes/services/index.js +5 -0
- package/dist/util/env/index.d.ts +1 -0
- package/dist/util/env/index.js +9 -0
- package/dist/util/http/caching.js +24 -12
- package/dist/util/http/http-request.js +12 -7
- package/package.json +2 -1
- package/dist/classes/cache/PersistentResponseCache.d.ts +0 -46
- /package/dist/classes/cache/{PersistentResponseCache.js → PersistentResponseCache.js__} +0 -0
@@ -0,0 +1,673 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview IndexedDbCache provides efficient persistent caching with
|
3
|
+
* automatic, non-blocking background cleanup.
|
4
|
+
*
|
5
|
+
* This cache automatically manages storage limits and entry expiration
|
6
|
+
* in the background using requestIdleCallback to avoid impacting application
|
7
|
+
* performance. It supports incremental cleanup, storage monitoring, and
|
8
|
+
* graceful degradation on older browsers.
|
9
|
+
*
|
10
|
+
* @example
|
11
|
+
* // Create a cache instance
|
12
|
+
* const cache = new IndexedDbCache({
|
13
|
+
* dbName: 'app-cache',
|
14
|
+
* storeName: 'http-responses',
|
15
|
+
* maxSize: 100 * 1024 * 1024, // 100MB
|
16
|
+
* maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
|
17
|
+
* });
|
18
|
+
*
|
19
|
+
* // Store a response
|
20
|
+
* const response = await fetch('https://api.example.com/data');
|
21
|
+
* await cache.set('api-data', response, {
|
22
|
+
* expiresIn: 3600000 // 1 hour
|
23
|
+
* });
|
24
|
+
*
|
25
|
+
* // Retrieve cached response
|
26
|
+
* const cached = await cache.get('api-data');
|
27
|
+
* if (cached) {
|
28
|
+
* console.log('Cache hit', cached.response);
|
29
|
+
* } else {
|
30
|
+
* console.log('Cache miss');
|
31
|
+
* }
|
32
|
+
*/
|
33
|
+
|
34
|
+
/**
|
35
|
+
* @typedef {Object} CacheEntry
|
36
|
+
* @property {Response} response - Cached Response object
|
37
|
+
* @property {Object} metadata - Cache entry metadata
|
38
|
+
* @property {string} url - Original URL
|
39
|
+
* @property {number} timestamp - When the entry was cached
|
40
|
+
* @property {number|null} expires - Expiration timestamp (null if no expiration)
|
41
|
+
* @property {string|null} etag - ETag header if present
|
42
|
+
* @property {string|null} lastModified - Last-Modified header if present
|
43
|
+
*/
|
44
|
+
|
45
|
+
/**
|
46
|
+
* IndexedDbCache with automatic background cleanup
|
47
|
+
*/
|
48
|
+
export default class IndexedDbCache {
|
49
|
+
/**
|
50
|
+
* Create a new IndexedDB cache storage
|
51
|
+
*
|
52
|
+
* @param {Object} [options] - Cache options
|
53
|
+
* @param {string} [options.dbName='http-cache'] - Database name
|
54
|
+
* @param {string} [options.storeName='responses'] - Store name
|
55
|
+
* @param {number} [options.maxSize=50000000] - Max cache size in bytes (50MB)
|
56
|
+
* @param {number} [options.maxAge=604800000] - Max age in ms (7 days)
|
57
|
+
* @param {number} [options.cleanupBatchSize=100] - Items per cleanup batch
|
58
|
+
* @param {number} [options.cleanupInterval=300000] - Time between cleanup attempts (5min)
|
59
|
+
*/
|
60
|
+
constructor(options = {}) {
|
61
|
+
this.dbName = options.dbName || 'http-cache';
|
62
|
+
this.storeName = options.storeName || 'responses';
|
63
|
+
this.maxSize = options.maxSize || 50 * 1024 * 1024; // 50MB
|
64
|
+
this.maxAge = options.maxAge || 7 * 24 * 60 * 60 * 1000; // 7 days
|
65
|
+
this.cleanupBatchSize = options.cleanupBatchSize || 100;
|
66
|
+
this.cleanupInterval = options.cleanupInterval || 5 * 60 * 1000; // 5 minutes
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Database connection promise
|
70
|
+
* @type {Promise<IDBDatabase>}
|
71
|
+
* @private
|
72
|
+
*/
|
73
|
+
this.dbPromise = this._openDatabase();
|
74
|
+
|
75
|
+
/**
|
76
|
+
* Cleanup state tracker
|
77
|
+
* @type {Object}
|
78
|
+
* @private
|
79
|
+
*/
|
80
|
+
this.cleanupState = {
|
81
|
+
inProgress: false,
|
82
|
+
lastRun: 0,
|
83
|
+
lastCursor: null,
|
84
|
+
totalRemoved: 0,
|
85
|
+
nextScheduled: false
|
86
|
+
};
|
87
|
+
|
88
|
+
// Schedule initial cleanup
|
89
|
+
this._scheduleCleanup();
|
90
|
+
}
|
91
|
+
|
92
|
+
/**
|
93
|
+
* Open the IndexedDB database
|
94
|
+
*
|
95
|
+
* @private
|
96
|
+
* @returns {Promise<IDBDatabase>}
|
97
|
+
*/
|
98
|
+
async _openDatabase() {
|
99
|
+
return new Promise((resolve, reject) => {
|
100
|
+
const request = indexedDB.open(this.dbName, 1);
|
101
|
+
|
102
|
+
request.onerror = () => reject(request.error);
|
103
|
+
request.onsuccess = () => resolve(request.result);
|
104
|
+
|
105
|
+
request.onupgradeneeded = (event) => {
|
106
|
+
const db = event.target.result;
|
107
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
108
|
+
// Create object store with indexes to help with cleanup
|
109
|
+
const store = db.createObjectStore(this.storeName, { keyPath: 'key' });
|
110
|
+
|
111
|
+
// Index for expiration-based cleanup
|
112
|
+
store.createIndex('expires', 'expires', { unique: false });
|
113
|
+
|
114
|
+
// Index for age-based cleanup
|
115
|
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
116
|
+
|
117
|
+
// Index for size-based estimate (rough approximation)
|
118
|
+
store.createIndex('size', 'size', { unique: false });
|
119
|
+
}
|
120
|
+
};
|
121
|
+
});
|
122
|
+
}
|
123
|
+
|
124
|
+
/**
|
125
|
+
* Get a cached response
|
126
|
+
*
|
127
|
+
* @param {string} key - Cache key
|
128
|
+
* @returns {Promise<CacheEntry|null>} Cache entry or null if not found/expired
|
129
|
+
*/
|
130
|
+
async get(key) {
|
131
|
+
try {
|
132
|
+
const db = await this.dbPromise;
|
133
|
+
|
134
|
+
return new Promise((resolve, reject) => {
|
135
|
+
const transaction = db.transaction(this.storeName, 'readonly');
|
136
|
+
const store = transaction.objectStore(this.storeName);
|
137
|
+
const request = store.get(key);
|
138
|
+
|
139
|
+
request.onerror = () => reject(request.error);
|
140
|
+
request.onsuccess = () => {
|
141
|
+
const entry = request.result;
|
142
|
+
|
143
|
+
if (!entry) {
|
144
|
+
resolve(null);
|
145
|
+
return;
|
146
|
+
}
|
147
|
+
|
148
|
+
// Check if expired
|
149
|
+
if (entry.expires && Date.now() > entry.expires) {
|
150
|
+
// Delete expired entry
|
151
|
+
this._deleteEntry(key).catch(console.error);
|
152
|
+
resolve(null);
|
153
|
+
return;
|
154
|
+
}
|
155
|
+
|
156
|
+
// Update access timestamp (but don't block)
|
157
|
+
this._updateAccessTime(key).catch(console.error);
|
158
|
+
|
159
|
+
// Deserialize the response
|
160
|
+
try {
|
161
|
+
// Create headers safely
|
162
|
+
let responseHeaders;
|
163
|
+
try {
|
164
|
+
responseHeaders = new Headers(entry.headers);
|
165
|
+
} catch (err) {
|
166
|
+
// Fallback for environments without Headers support
|
167
|
+
responseHeaders = {
|
168
|
+
get: (name) => {
|
169
|
+
const header = entry.headers?.find(h => h[0].toLowerCase() === name.toLowerCase());
|
170
|
+
return header ? header[1] : null;
|
171
|
+
}
|
172
|
+
};
|
173
|
+
}
|
174
|
+
|
175
|
+
// Create Response safely
|
176
|
+
let response;
|
177
|
+
try {
|
178
|
+
response = new Response(entry.body, {
|
179
|
+
status: entry.status,
|
180
|
+
statusText: entry.statusText,
|
181
|
+
headers: responseHeaders
|
182
|
+
});
|
183
|
+
} catch (err) {
|
184
|
+
// Simplified mock response for test environments
|
185
|
+
response = {
|
186
|
+
status: entry.status,
|
187
|
+
statusText: entry.statusText,
|
188
|
+
headers: responseHeaders,
|
189
|
+
body: entry.body,
|
190
|
+
url: entry.url,
|
191
|
+
clone() { return this; }
|
192
|
+
};
|
193
|
+
}
|
194
|
+
|
195
|
+
resolve({
|
196
|
+
response,
|
197
|
+
metadata: entry.metadata,
|
198
|
+
url: entry.url,
|
199
|
+
timestamp: entry.timestamp,
|
200
|
+
expires: entry.expires,
|
201
|
+
etag: entry.etag,
|
202
|
+
lastModified: entry.lastModified
|
203
|
+
});
|
204
|
+
} catch (err) {
|
205
|
+
console.error('Failed to deserialize cached response:', err);
|
206
|
+
|
207
|
+
// Delete corrupted entry
|
208
|
+
this._deleteEntry(key).catch(console.error);
|
209
|
+
resolve(null);
|
210
|
+
}
|
211
|
+
};
|
212
|
+
});
|
213
|
+
} catch (err) {
|
214
|
+
console.error('Cache get error:', err);
|
215
|
+
return null;
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
/**
|
220
|
+
* Store a response in the cache
|
221
|
+
*
|
222
|
+
* @param {string} key - Cache key
|
223
|
+
* @param {Response} response - Response to cache
|
224
|
+
* @param {Object} [metadata={}] - Cache metadata
|
225
|
+
* @returns {Promise<void>}
|
226
|
+
*/
|
227
|
+
async set(key, response, metadata = {}) {
|
228
|
+
try {
|
229
|
+
const db = await this.dbPromise;
|
230
|
+
|
231
|
+
// Clone the response to avoid consuming it
|
232
|
+
const clonedResponse = response.clone();
|
233
|
+
|
234
|
+
// Extract response data - handle both browser Response and test mocks
|
235
|
+
let body;
|
236
|
+
try {
|
237
|
+
// Try standard Response.blob() first (browser environment)
|
238
|
+
body = await clonedResponse.blob();
|
239
|
+
} catch (err) {
|
240
|
+
// Fallback for test environment
|
241
|
+
if (typeof clonedResponse.body === 'string' ||
|
242
|
+
clonedResponse.body instanceof ArrayBuffer ||
|
243
|
+
clonedResponse.body instanceof Uint8Array) {
|
244
|
+
body = new Blob([clonedResponse.body]);
|
245
|
+
} else {
|
246
|
+
// Last resort - store as-is and hope it's serializable
|
247
|
+
body = clonedResponse.body || new Blob([]);
|
248
|
+
}
|
249
|
+
}
|
250
|
+
|
251
|
+
// Extract headers safely
|
252
|
+
let headers = [];
|
253
|
+
try {
|
254
|
+
headers = Array.from(clonedResponse.headers.entries());
|
255
|
+
} catch (err) {
|
256
|
+
// Fallback for test environment - extract headers if available
|
257
|
+
if (clonedResponse._headers && typeof clonedResponse._headers.entries === 'function') {
|
258
|
+
headers = Array.from(clonedResponse._headers.entries());
|
259
|
+
}
|
260
|
+
}
|
261
|
+
|
262
|
+
// Calculate rough size estimate
|
263
|
+
const headerSize = JSON.stringify(headers).length * 2;
|
264
|
+
const size = (body.size || 0) + headerSize + key.length * 2;
|
265
|
+
|
266
|
+
const entry = {
|
267
|
+
key,
|
268
|
+
url: clonedResponse.url || '',
|
269
|
+
status: clonedResponse.status || 200,
|
270
|
+
statusText: clonedResponse.statusText || '',
|
271
|
+
headers,
|
272
|
+
body,
|
273
|
+
metadata,
|
274
|
+
timestamp: Date.now(),
|
275
|
+
lastAccessed: Date.now(),
|
276
|
+
expires: metadata.expires || (metadata.expiresIn ? Date.now() + metadata.expiresIn : null),
|
277
|
+
etag: clonedResponse.headers?.get?.('ETag') || null,
|
278
|
+
lastModified: clonedResponse.headers?.get?.('Last-Modified') || null,
|
279
|
+
size // Store estimated size for cleanup
|
280
|
+
};
|
281
|
+
|
282
|
+
return new Promise((resolve, reject) => {
|
283
|
+
const transaction = db.transaction(this.storeName, 'readwrite');
|
284
|
+
const store = transaction.objectStore(this.storeName);
|
285
|
+
const request = store.put(entry);
|
286
|
+
|
287
|
+
request.onerror = () => reject(request.error);
|
288
|
+
request.onsuccess = () => {
|
289
|
+
resolve();
|
290
|
+
|
291
|
+
// Check if we need cleanup after adding new entries
|
292
|
+
this._checkAndScheduleCleanup();
|
293
|
+
};
|
294
|
+
});
|
295
|
+
} catch (err) {
|
296
|
+
console.error('Cache set error:', err);
|
297
|
+
throw err;
|
298
|
+
}
|
299
|
+
}
|
300
|
+
|
301
|
+
/**
|
302
|
+
* Update last accessed timestamp (without blocking)
|
303
|
+
*
|
304
|
+
* @private
|
305
|
+
* @param {string} key - Cache key
|
306
|
+
* @returns {Promise<void>}
|
307
|
+
*/
|
308
|
+
async _updateAccessTime(key) {
|
309
|
+
const db = await this.dbPromise;
|
310
|
+
|
311
|
+
return new Promise((resolve) => {
|
312
|
+
const transaction = db.transaction(this.storeName, 'readwrite');
|
313
|
+
const store = transaction.objectStore(this.storeName);
|
314
|
+
const request = store.get(key);
|
315
|
+
|
316
|
+
request.onerror = () => resolve(); // Don't block on errors
|
317
|
+
|
318
|
+
request.onsuccess = () => {
|
319
|
+
const entry = request.result;
|
320
|
+
if (!entry) return resolve();
|
321
|
+
|
322
|
+
entry.lastAccessed = Date.now();
|
323
|
+
|
324
|
+
const updateRequest = store.put(entry);
|
325
|
+
updateRequest.onerror = () => resolve(); // Don't block
|
326
|
+
updateRequest.onsuccess = () => resolve();
|
327
|
+
};
|
328
|
+
});
|
329
|
+
}
|
330
|
+
|
331
|
+
/**
|
332
|
+
* Delete a cached entry
|
333
|
+
*
|
334
|
+
* @param {string} key - Cache key
|
335
|
+
* @returns {Promise<boolean>}
|
336
|
+
*/
|
337
|
+
async delete(key) {
|
338
|
+
return this._deleteEntry(key);
|
339
|
+
}
|
340
|
+
|
341
|
+
/**
|
342
|
+
* Delete a cached entry (internal implementation)
|
343
|
+
*
|
344
|
+
* @private
|
345
|
+
* @param {string} key - Cache key
|
346
|
+
* @returns {Promise<boolean>}
|
347
|
+
*/
|
348
|
+
async _deleteEntry(key) {
|
349
|
+
try {
|
350
|
+
const db = await this.dbPromise;
|
351
|
+
|
352
|
+
return new Promise((resolve, reject) => {
|
353
|
+
const transaction = db.transaction(this.storeName, 'readwrite');
|
354
|
+
const store = transaction.objectStore(this.storeName);
|
355
|
+
const request = store.delete(key);
|
356
|
+
|
357
|
+
request.onerror = () => reject(request.error);
|
358
|
+
request.onsuccess = () => resolve(true);
|
359
|
+
});
|
360
|
+
} catch (err) {
|
361
|
+
console.error('Cache delete error:', err);
|
362
|
+
return false;
|
363
|
+
}
|
364
|
+
}
|
365
|
+
|
366
|
+
/**
|
367
|
+
* Clear all cached responses
|
368
|
+
*
|
369
|
+
* @returns {Promise<void>}
|
370
|
+
*/
|
371
|
+
async clear() {
|
372
|
+
try {
|
373
|
+
const db = await this.dbPromise;
|
374
|
+
|
375
|
+
return new Promise((resolve, reject) => {
|
376
|
+
const transaction = db.transaction(this.storeName, 'readwrite');
|
377
|
+
const store = transaction.objectStore(this.storeName);
|
378
|
+
const request = store.clear();
|
379
|
+
|
380
|
+
request.onerror = () => reject(request.error);
|
381
|
+
request.onsuccess = () => {
|
382
|
+
this.cleanupState.lastCursor = null;
|
383
|
+
this.cleanupState.totalRemoved = 0;
|
384
|
+
resolve();
|
385
|
+
};
|
386
|
+
});
|
387
|
+
} catch (err) {
|
388
|
+
console.error('Cache clear error:', err);
|
389
|
+
throw err;
|
390
|
+
}
|
391
|
+
}
|
392
|
+
|
393
|
+
/**
|
394
|
+
* Check storage usage and schedule cleanup if needed
|
395
|
+
*
|
396
|
+
* @private
|
397
|
+
*/
|
398
|
+
async _checkAndScheduleCleanup() {
|
399
|
+
// Avoid multiple concurrent checks
|
400
|
+
if (this.cleanupState.inProgress || this.cleanupState.nextScheduled) {
|
401
|
+
return;
|
402
|
+
}
|
403
|
+
|
404
|
+
// Only check periodically
|
405
|
+
const now = Date.now();
|
406
|
+
if (now - this.cleanupState.lastRun < this.cleanupInterval) {
|
407
|
+
return;
|
408
|
+
}
|
409
|
+
|
410
|
+
// Use storage estimate API if available in browser environment
|
411
|
+
if (typeof navigator !== 'undefined' &&
|
412
|
+
navigator.storage &&
|
413
|
+
typeof navigator.storage.estimate === 'function') {
|
414
|
+
try {
|
415
|
+
const estimate = await navigator.storage.estimate();
|
416
|
+
const usageRatio = estimate.usage / estimate.quota;
|
417
|
+
|
418
|
+
// If using more than 80% of quota, schedule urgent cleanup
|
419
|
+
if (usageRatio > 0.8) {
|
420
|
+
this._scheduleCleanup(true);
|
421
|
+
return;
|
422
|
+
}
|
423
|
+
} catch (err) {
|
424
|
+
// Fall back to regular scheduling if estimate fails
|
425
|
+
console.error('Storage estimate error:', err);
|
426
|
+
}
|
427
|
+
}
|
428
|
+
|
429
|
+
// Schedule normal cleanup
|
430
|
+
this._scheduleCleanup();
|
431
|
+
}
|
432
|
+
|
433
|
+
/**
|
434
|
+
* Schedule a cleanup to run during idle time
|
435
|
+
*
|
436
|
+
* @private
|
437
|
+
* @param {boolean} [urgent=false] - If true, clean up sooner
|
438
|
+
*/
|
439
|
+
_scheduleCleanup(urgent = false) {
|
440
|
+
if (this.cleanupState.nextScheduled) {
|
441
|
+
return;
|
442
|
+
}
|
443
|
+
|
444
|
+
this.cleanupState.nextScheduled = true;
|
445
|
+
|
446
|
+
// Check if we're in a browser environment with requestIdleCallback
|
447
|
+
if (typeof window !== 'undefined' &&
|
448
|
+
typeof window.requestIdleCallback === 'function') {
|
449
|
+
window.requestIdleCallback(
|
450
|
+
() => {
|
451
|
+
this.cleanupState.nextScheduled = false;
|
452
|
+
this._performCleanupStep();
|
453
|
+
},
|
454
|
+
{ timeout: urgent ? 1000 : 10000 }
|
455
|
+
);
|
456
|
+
} else {
|
457
|
+
// Fallback for Node.js or browsers without requestIdleCallback
|
458
|
+
setTimeout(() => {
|
459
|
+
this.cleanupState.nextScheduled = false;
|
460
|
+
this._performCleanupStep();
|
461
|
+
}, urgent ? 100 : 1000);
|
462
|
+
}
|
463
|
+
}
|
464
|
+
|
465
|
+
/**
|
466
|
+
* Perform a single cleanup step
|
467
|
+
*
|
468
|
+
* @private
|
469
|
+
*/
|
470
|
+
async _performCleanupStep() {
|
471
|
+
if (this.cleanupState.inProgress) {
|
472
|
+
return;
|
473
|
+
}
|
474
|
+
|
475
|
+
this.cleanupState.inProgress = true;
|
476
|
+
|
477
|
+
try {
|
478
|
+
const now = Date.now();
|
479
|
+
const db = await this.dbPromise;
|
480
|
+
let removedCount = 0;
|
481
|
+
|
482
|
+
// Step 1: Remove expired entries first
|
483
|
+
const expiredRemoved = await this._removeExpiredEntries(
|
484
|
+
this.cleanupBatchSize / 2
|
485
|
+
);
|
486
|
+
removedCount += expiredRemoved;
|
487
|
+
|
488
|
+
// If we have a lot of expired entries, focus on those first
|
489
|
+
if (expiredRemoved >= this.cleanupBatchSize / 2) {
|
490
|
+
this.cleanupState.inProgress = false;
|
491
|
+
this.cleanupState.lastRun = now;
|
492
|
+
this.cleanupState.totalRemoved += removedCount;
|
493
|
+
|
494
|
+
// Schedule next cleanup step immediately
|
495
|
+
this._scheduleCleanup();
|
496
|
+
return;
|
497
|
+
}
|
498
|
+
|
499
|
+
// Step 2: Remove old entries if we're over size/age limits
|
500
|
+
const remainingBatch = this.cleanupBatchSize - expiredRemoved;
|
501
|
+
if (remainingBatch > 0) {
|
502
|
+
const oldRemoved = await this._removeOldEntries(remainingBatch);
|
503
|
+
removedCount += oldRemoved;
|
504
|
+
}
|
505
|
+
|
506
|
+
// Update cleanup state
|
507
|
+
this.cleanupState.lastRun = now;
|
508
|
+
this.cleanupState.totalRemoved += removedCount;
|
509
|
+
|
510
|
+
// If we removed entries in this batch, schedule another cleanup
|
511
|
+
if (removedCount > 0) {
|
512
|
+
this._scheduleCleanup();
|
513
|
+
} else {
|
514
|
+
// Reset cursor if we didn't find anything to clean
|
515
|
+
this.cleanupState.lastCursor = null;
|
516
|
+
|
517
|
+
// Schedule a check later
|
518
|
+
setTimeout(() => {
|
519
|
+
this._checkAndScheduleCleanup();
|
520
|
+
}, this.cleanupInterval);
|
521
|
+
}
|
522
|
+
} catch (err) {
|
523
|
+
console.error('Cache cleanup error:', err);
|
524
|
+
} finally {
|
525
|
+
this.cleanupState.inProgress = false;
|
526
|
+
}
|
527
|
+
}
|
528
|
+
|
529
|
+
/**
|
530
|
+
* Remove expired entries
|
531
|
+
*
|
532
|
+
* @private
|
533
|
+
* @param {number} limit - Maximum number of entries to remove
|
534
|
+
* @returns {Promise<number>} Number of entries removed
|
535
|
+
*/
|
536
|
+
async _removeExpiredEntries(limit) {
|
537
|
+
const now = Date.now();
|
538
|
+
const db = await this.dbPromise;
|
539
|
+
let removed = 0;
|
540
|
+
|
541
|
+
return new Promise((resolve) => {
|
542
|
+
const transaction = db.transaction(this.storeName, 'readwrite');
|
543
|
+
const store = transaction.objectStore(this.storeName);
|
544
|
+
const index = store.index('expires');
|
545
|
+
|
546
|
+
// Create range for all entries with expiration before now
|
547
|
+
const range = IDBKeyRange.upperBound(now);
|
548
|
+
|
549
|
+
// Skip non-expiring entries (null expiration)
|
550
|
+
const request = index.openCursor(range);
|
551
|
+
|
552
|
+
request.onerror = () => resolve(removed);
|
553
|
+
|
554
|
+
request.onsuccess = (event) => {
|
555
|
+
const cursor = event.target.result;
|
556
|
+
|
557
|
+
if (!cursor || removed >= limit) {
|
558
|
+
resolve(removed);
|
559
|
+
return;
|
560
|
+
}
|
561
|
+
|
562
|
+
// Delete the expired entry
|
563
|
+
const deleteRequest = cursor.delete();
|
564
|
+
deleteRequest.onsuccess = () => {
|
565
|
+
removed++;
|
566
|
+
};
|
567
|
+
|
568
|
+
// Move to next entry
|
569
|
+
cursor.continue();
|
570
|
+
};
|
571
|
+
});
|
572
|
+
}
|
573
|
+
|
574
|
+
/**
|
575
|
+
* Remove old entries based on age and size constraints
|
576
|
+
*
|
577
|
+
* @private
|
578
|
+
* @param {number} limit - Maximum number of entries to remove
|
579
|
+
* @returns {Promise<number>} Number of entries removed
|
580
|
+
*/
|
581
|
+
async _removeOldEntries(limit) {
|
582
|
+
const db = await this.dbPromise;
|
583
|
+
let removed = 0;
|
584
|
+
|
585
|
+
// Get total cache size estimate (rough)
|
586
|
+
const sizeEstimate = await this._getCacheSizeEstimate();
|
587
|
+
|
588
|
+
// If we're under limits, don't remove anything
|
589
|
+
if (sizeEstimate < this.maxSize) {
|
590
|
+
return 0;
|
591
|
+
}
|
592
|
+
|
593
|
+
return new Promise((resolve) => {
|
594
|
+
const transaction = db.transaction(this.storeName, 'readwrite');
|
595
|
+
const store = transaction.objectStore(this.storeName);
|
596
|
+
const index = store.index('timestamp');
|
597
|
+
|
598
|
+
// Start from the oldest entries
|
599
|
+
const request = index.openCursor();
|
600
|
+
|
601
|
+
request.onerror = () => resolve(removed);
|
602
|
+
|
603
|
+
request.onsuccess = (event) => {
|
604
|
+
const cursor = event.target.result;
|
605
|
+
|
606
|
+
if (!cursor || removed >= limit) {
|
607
|
+
resolve(removed);
|
608
|
+
return;
|
609
|
+
}
|
610
|
+
|
611
|
+
const entry = cursor.value;
|
612
|
+
const now = Date.now();
|
613
|
+
const age = now - entry.timestamp;
|
614
|
+
|
615
|
+
// Delete if older than max age
|
616
|
+
if (age > this.maxAge) {
|
617
|
+
const deleteRequest = cursor.delete();
|
618
|
+
deleteRequest.onsuccess = () => {
|
619
|
+
removed++;
|
620
|
+
};
|
621
|
+
}
|
622
|
+
|
623
|
+
// Move to next entry
|
624
|
+
cursor.continue();
|
625
|
+
};
|
626
|
+
});
|
627
|
+
}
|
628
|
+
|
629
|
+
/**
|
630
|
+
* Get an estimate of the total cache size
|
631
|
+
*
|
632
|
+
* @private
|
633
|
+
* @returns {Promise<number>} Size estimate in bytes
|
634
|
+
*/
|
635
|
+
async _getCacheSizeEstimate() {
|
636
|
+
const db = await this.dbPromise;
|
637
|
+
|
638
|
+
return new Promise((resolve) => {
|
639
|
+
const transaction = db.transaction(this.storeName, 'readonly');
|
640
|
+
const store = transaction.objectStore(this.storeName);
|
641
|
+
const index = store.index('size');
|
642
|
+
|
643
|
+
// Get the sum of all entry sizes
|
644
|
+
const request = index.openCursor();
|
645
|
+
let totalSize = 0;
|
646
|
+
|
647
|
+
request.onerror = () => resolve(totalSize);
|
648
|
+
|
649
|
+
request.onsuccess = (event) => {
|
650
|
+
const cursor = event.target.result;
|
651
|
+
|
652
|
+
if (!cursor) {
|
653
|
+
resolve(totalSize);
|
654
|
+
return;
|
655
|
+
}
|
656
|
+
|
657
|
+
const entry = cursor.value;
|
658
|
+
totalSize += entry.size || 0;
|
659
|
+
|
660
|
+
cursor.continue();
|
661
|
+
};
|
662
|
+
});
|
663
|
+
}
|
664
|
+
|
665
|
+
/**
|
666
|
+
* Close the database connection
|
667
|
+
*/
|
668
|
+
close() {
|
669
|
+
this.dbPromise.then(db => {
|
670
|
+
db.close();
|
671
|
+
}).catch(console.error);
|
672
|
+
}
|
673
|
+
}
|