@xiboplayer/cache 0.3.5 → 0.3.7
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/package.json +2 -2
- package/src/cache-analyzer.js +30 -43
- package/src/cache-analyzer.test.js +8 -29
- package/src/cache-proxy.js +26 -0
- package/src/cache.js +8 -659
- package/src/cache.test.js +15 -616
- package/src/index.js +1 -0
- package/src/widget-html.js +181 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/cache",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Offline caching and download management with parallel chunk downloads",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"spark-md5": "^3.0.2",
|
|
15
|
-
"@xiboplayer/utils": "0.3.
|
|
15
|
+
"@xiboplayer/utils": "0.3.7"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"vitest": "^2.0.0",
|
package/src/cache-analyzer.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Compares cached files against RequiredFiles from the CMS to identify
|
|
5
5
|
* orphaned media that is no longer needed. Logs a summary every collection
|
|
6
6
|
* cycle. Only evicts when storage pressure exceeds a configurable threshold.
|
|
7
|
+
*
|
|
8
|
+
* Works entirely through CacheProxy (postMessage to SW) — no IndexedDB,
|
|
9
|
+
* no direct Cache API access.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import { createLogger } from '@xiboplayer/utils';
|
|
@@ -24,7 +27,7 @@ function formatBytes(bytes) {
|
|
|
24
27
|
|
|
25
28
|
export class CacheAnalyzer {
|
|
26
29
|
/**
|
|
27
|
-
* @param {import('./cache.js').
|
|
30
|
+
* @param {import('./cache-proxy.js').CacheProxy} cache - CacheProxy instance
|
|
28
31
|
* @param {object} [options]
|
|
29
32
|
* @param {number} [options.threshold=80] - Storage usage % above which eviction triggers
|
|
30
33
|
*/
|
|
@@ -130,59 +133,43 @@ export class CacheAnalyzer {
|
|
|
130
133
|
|
|
131
134
|
/**
|
|
132
135
|
* Evict orphaned files (oldest first) until targetBytes are freed.
|
|
133
|
-
*
|
|
134
|
-
* Deletes from both IndexedDB metadata and Cache API.
|
|
136
|
+
* Delegates deletion to CacheProxy.deleteFiles() which routes to SW.
|
|
135
137
|
*
|
|
136
138
|
* @param {Array} orphanedFiles - Files to evict, sorted oldest-first
|
|
137
139
|
* @param {number} targetBytes - Bytes to free
|
|
138
140
|
* @returns {Promise<Array>} Evicted file records
|
|
139
141
|
*/
|
|
140
142
|
async _evict(orphanedFiles, targetBytes) {
|
|
141
|
-
const
|
|
142
|
-
let
|
|
143
|
+
const toEvict = [];
|
|
144
|
+
let plannedBytes = 0;
|
|
143
145
|
|
|
144
146
|
for (const file of orphanedFiles) {
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// Delete from IndexedDB metadata
|
|
149
|
-
await this._deleteFileMetadata(file.id);
|
|
150
|
-
|
|
151
|
-
// Delete from Cache API
|
|
152
|
-
const cacheKey = this.cache.getCacheKey(file.type, file.id);
|
|
153
|
-
if (this.cache.cache) {
|
|
154
|
-
await this.cache.cache.delete(cacheKey);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
freedBytes += file.size || 0;
|
|
158
|
-
evicted.push({
|
|
159
|
-
id: file.id,
|
|
160
|
-
type: file.type,
|
|
161
|
-
size: file.size || 0,
|
|
162
|
-
cachedAt: file.cachedAt || 0,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
log.info(` Evicted: ${file.type}/${file.id} (${formatBytes(file.size || 0)})`);
|
|
166
|
-
} catch (err) {
|
|
167
|
-
log.warn(` Failed to evict ${file.type}/${file.id}:`, err.message);
|
|
168
|
-
}
|
|
147
|
+
if (plannedBytes >= targetBytes) break;
|
|
148
|
+
toEvict.push(file);
|
|
149
|
+
plannedBytes += file.size || 0;
|
|
169
150
|
}
|
|
170
151
|
|
|
171
|
-
|
|
172
|
-
return evicted;
|
|
173
|
-
}
|
|
152
|
+
if (toEvict.length === 0) return [];
|
|
174
153
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
154
|
+
try {
|
|
155
|
+
const filesToDelete = toEvict.map(f => ({ type: f.type, id: f.id }));
|
|
156
|
+
await this.cache.deleteFiles(filesToDelete);
|
|
157
|
+
|
|
158
|
+
for (const f of toEvict) {
|
|
159
|
+
log.info(` Evicted: ${f.type}/${f.id} (${formatBytes(f.size || 0)})`);
|
|
160
|
+
}
|
|
161
|
+
log.info(`Evicted ${toEvict.length} files, freed ${formatBytes(plannedBytes)}`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
log.warn('Eviction failed:', err.message);
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return toEvict.map(f => ({
|
|
168
|
+
id: f.id,
|
|
169
|
+
type: f.type,
|
|
170
|
+
size: f.size || 0,
|
|
171
|
+
cachedAt: f.cachedAt || 0,
|
|
172
|
+
}));
|
|
186
173
|
}
|
|
187
174
|
}
|
|
188
175
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* CacheAnalyzer Tests
|
|
3
3
|
*
|
|
4
4
|
* Tests for stale media detection, storage health reporting, and eviction logic.
|
|
5
|
+
* Mock follows the CacheProxy interface (getAllFiles, deleteFiles).
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
@@ -12,17 +13,15 @@ describe('CacheAnalyzer', () => {
|
|
|
12
13
|
let mockCache;
|
|
13
14
|
|
|
14
15
|
beforeEach(() => {
|
|
15
|
-
// Mock
|
|
16
|
+
// Mock CacheProxy with in-memory file store
|
|
16
17
|
const files = new Map();
|
|
17
|
-
const cacheStore = new Map();
|
|
18
18
|
|
|
19
19
|
mockCache = {
|
|
20
|
-
db: null, // Will be set up per-test if eviction is tested
|
|
21
|
-
cache: {
|
|
22
|
-
delete: vi.fn(async (key) => cacheStore.delete(key)),
|
|
23
|
-
},
|
|
24
20
|
getAllFiles: vi.fn(async () => [...files.values()]),
|
|
25
|
-
|
|
21
|
+
deleteFiles: vi.fn(async (filesToDelete) => ({
|
|
22
|
+
deleted: filesToDelete.length,
|
|
23
|
+
total: filesToDelete.length,
|
|
24
|
+
})),
|
|
26
25
|
// Helper to add test files
|
|
27
26
|
_files: files,
|
|
28
27
|
_addFile(record) {
|
|
@@ -184,25 +183,6 @@ describe('CacheAnalyzer', () => {
|
|
|
184
183
|
});
|
|
185
184
|
|
|
186
185
|
describe('eviction', () => {
|
|
187
|
-
let deletedIds;
|
|
188
|
-
|
|
189
|
-
beforeEach(() => {
|
|
190
|
-
deletedIds = [];
|
|
191
|
-
|
|
192
|
-
// Mock IndexedDB transaction for metadata deletion
|
|
193
|
-
const mockStore = {
|
|
194
|
-
delete: vi.fn((id) => {
|
|
195
|
-
deletedIds.push(id);
|
|
196
|
-
return { set onsuccess(fn) { fn(); }, set onerror(_) {} };
|
|
197
|
-
}),
|
|
198
|
-
};
|
|
199
|
-
mockCache.db = {
|
|
200
|
-
transaction: vi.fn(() => ({
|
|
201
|
-
objectStore: vi.fn(() => mockStore),
|
|
202
|
-
})),
|
|
203
|
-
};
|
|
204
|
-
});
|
|
205
|
-
|
|
206
186
|
it('should evict orphaned files when storage exceeds threshold', async () => {
|
|
207
187
|
mockCache._addFile({ id: 'old', type: 'media', size: 500, cachedAt: 1000 });
|
|
208
188
|
mockCache._addFile({ id: 'newer', type: 'media', size: 300, cachedAt: 2000 });
|
|
@@ -220,9 +200,8 @@ describe('CacheAnalyzer', () => {
|
|
|
220
200
|
expect(report.evicted.length).toBeGreaterThan(0);
|
|
221
201
|
// Should evict oldest first
|
|
222
202
|
expect(report.evicted[0].id).toBe('old');
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
expect(mockCache.cache.delete).toHaveBeenCalled();
|
|
203
|
+
// deleteFiles should be called on the cache proxy
|
|
204
|
+
expect(mockCache.deleteFiles).toHaveBeenCalled();
|
|
226
205
|
|
|
227
206
|
vi.unstubAllGlobals();
|
|
228
207
|
});
|
package/src/cache-proxy.js
CHANGED
|
@@ -416,6 +416,32 @@ export class CacheProxy extends EventEmitter {
|
|
|
416
416
|
return this.backendType === 'service-worker';
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Get all cached files from Service Worker
|
|
421
|
+
* @returns {Promise<Array<{id: string, type: string, size: number, cachedAt: number}>>}
|
|
422
|
+
*/
|
|
423
|
+
async getAllFiles() {
|
|
424
|
+
if (!this.backend) {
|
|
425
|
+
throw new Error('CacheProxy not initialized');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return new Promise((resolve) => {
|
|
429
|
+
const channel = new MessageChannel();
|
|
430
|
+
|
|
431
|
+
channel.port1.onmessage = (event) => {
|
|
432
|
+
const { success, files } = event.data;
|
|
433
|
+
resolve(success ? files : []);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
navigator.serviceWorker.controller.postMessage(
|
|
437
|
+
{ type: 'GET_ALL_FILES' },
|
|
438
|
+
[channel.port2]
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
setTimeout(() => resolve([]), 5000);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
419
445
|
/**
|
|
420
446
|
* Delete files from cache (purge obsolete media)
|
|
421
447
|
* @param {Array<{type: string, id: string}>} files - Files to delete
|