@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/cache",
3
- "version": "0.3.5",
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.5"
15
+ "@xiboplayer/utils": "0.3.7"
16
16
  },
17
17
  "devDependencies": {
18
18
  "vitest": "^2.0.0",
@@ -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').CacheManager} cache - CacheManager instance
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 evicted = [];
142
- let freedBytes = 0;
143
+ const toEvict = [];
144
+ let plannedBytes = 0;
143
145
 
144
146
  for (const file of orphanedFiles) {
145
- if (freedBytes >= targetBytes) break;
146
-
147
- try {
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
- log.info(`Evicted ${evicted.length} files, freed ${formatBytes(freedBytes)}`);
172
- return evicted;
173
- }
152
+ if (toEvict.length === 0) return [];
174
153
 
175
- /**
176
- * Delete a file record from IndexedDB.
177
- */
178
- async _deleteFileMetadata(id) {
179
- return new Promise((resolve, reject) => {
180
- const tx = this.cache.db.transaction('files', 'readwrite');
181
- const store = tx.objectStore('files');
182
- const request = store.delete(id);
183
- request.onsuccess = () => resolve();
184
- request.onerror = () => reject(request.error);
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 CacheManager with in-memory file store
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
- getCacheKey: vi.fn((type, id) => `/player/pwa/cache/${type}/${id}`),
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
- expect(deletedIds).toContain('old');
224
- // Cache API delete should also be called
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
  });
@@ -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