@xiboplayer/cache 0.5.7 → 0.5.9

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.
@@ -1,451 +1,248 @@
1
- # CacheProxy Architecture - Unified Cache Interface
1
+ # StoreClient + DownloadClient Architecture
2
2
 
3
3
  ## Overview
4
4
 
5
- The CacheProxy provides a unified interface for file caching and downloading that works seamlessly with both Service Worker and direct cache implementations. This abstraction enables platform-independent code and automatic backend selection.
5
+ The cache package provides two client classes that separate storage concerns from download concerns:
6
+
7
+ - **StoreClient** — pure REST client for reading/writing content in the ContentStore (filesystem)
8
+ - **DownloadClient** — Service Worker postMessage client for managing background downloads
9
+
10
+ No Cache API is used anywhere. All content is stored on the filesystem via the proxy's ContentStore.
6
11
 
7
12
  ## Architecture
8
13
 
9
14
  ```
10
15
  ┌─────────────────────────────────────────────────────┐
11
- │ Client Code (PWA, XLR, Mobile, etc.)
12
- │ - Uses CacheProxy for all file operations
13
- │ - No knowledge of backend implementation
14
- │ - Minimal integration code │
16
+ │ Client Code (PWA, Electron, Chromium)
17
+ │ - StoreClient for storage operations
18
+ │ - DownloadClient for download management
15
19
  └─────────────────────────────────────────────────────┘
16
-
17
- ┌─────────────────────────────────────────────────────┐
18
- │ CacheProxy Module (Shared Interface) │
19
- │ - Detects environment (Service Worker vs Direct) │
20
- │ - Provides unified API: get/download/cache files │
21
- │ - No client code changes needed │
22
- └─────────────────────────────────────────────────────┘
23
-
24
- ┌───────────────┴───────────────┐
25
- ↓ ↓
20
+ │ │
21
+ ▼ ▼
26
22
  ┌─────────────────┐ ┌─────────────────┐
27
- Service Worker │ │ Direct Cache
28
- Backend │ │ Backend
23
+ StoreClient │ │ DownloadClient
24
+ (REST) │ │ (SW postMessage)
29
25
  │ │ │ │
30
- - Downloads in │ │ - cache.js
31
- background │ │ - IndexedDB
32
- - Caches files │ │ - Blocking
33
- - Serves via │ │ downloads
34
- fetch │ │ │
35
- └─────────────────┘ └─────────────────┘
26
+ has(type, id) │ │ download(files)
27
+ get(type, id) │ │ prioritize()
28
+ put(type, id) │ │ getProgress()
29
+ remove(files) │ │
30
+ list() │ │ │
31
+ └────────┬────────┘ └────────┬────────┘
32
+ │ │
33
+ ▼ ▼
34
+ ┌─────────────────┐ ┌─────────────────┐
35
+ │ Proxy REST API │ │ Service Worker │
36
+ │ /store/:type/* │ │ DownloadManager │
37
+ │ │ │ RequestHandler │
38
+ │ GET, HEAD, PUT │ │ MessageHandler │
39
+ │ POST /delete │ │ │
40
+ │ GET /list │ │ │
41
+ └────────┬────────┘ └─────────────────┘
42
+
43
+
44
+ ┌─────────────────┐
45
+ │ ContentStore │
46
+ │ (filesystem) │
47
+ │ │
48
+ │ media/*.bin │
49
+ │ layout/*.bin │
50
+ │ widget/*.bin │
51
+ │ static/*.bin │
52
+ └─────────────────┘
36
53
  ```
37
54
 
38
55
  ## Components
39
56
 
40
- ### 1. CacheProxy (Main Interface)
57
+ ### 1. StoreClient (REST)
41
58
 
42
- **Location**: `packages/core/src/cache-proxy.js`
59
+ **Location**: `packages/cache/src/store-client.js`
43
60
 
44
- **Purpose**: Auto-detects backend and provides unified API
61
+ **Purpose**: Pure REST client for content storage operations. No Service Worker dependency — works immediately with just `fetch()`.
45
62
 
46
63
  **API**:
47
64
  ```javascript
48
- class CacheProxy {
49
- async init()
50
- async getFile(type: string, id: string): Promise<Blob|null>
51
- async requestDownload(files: FileInfo[]): Promise<void>
52
- async isCached(type: string, id: string): Promise<boolean>
53
- getBackendType(): string // 'service-worker' | 'direct'
54
- isUsingServiceWorker(): boolean
65
+ class StoreClient {
66
+ async has(type, id) // HEAD /store/:type/:id → { exists, size }
67
+ async get(type, id) // GET /store/:type/:id → Blob | null
68
+ async put(type, id, body, contentType) // PUT /store/:type/:id
69
+ async remove(files) // POST /store/delete
70
+ async list() // GET /store/list Array<FileInfo>
55
71
  }
56
72
  ```
57
73
 
58
74
  **Usage**:
59
75
  ```javascript
60
- import { CacheProxy } from '@core/cache-proxy.js';
76
+ import { StoreClient } from '@xiboplayer/cache';
61
77
 
62
- // Initialize
63
- const proxy = new CacheProxy(cacheManager);
64
- await proxy.init();
65
-
66
- // Get file (works with both backends)
67
- const blob = await proxy.getFile('media', '123');
68
-
69
- // Request downloads
70
- await proxy.requestDownload([
71
- { id: '1', type: 'media', path: 'https://...', md5: '...' }
72
- ]);
73
-
74
- // Check cache
75
- const cached = await proxy.isCached('layout', '456');
76
- ```
78
+ const store = new StoreClient();
77
79
 
78
- ### 2. ServiceWorkerBackend
80
+ // Check if file exists
81
+ const { exists, size } = await store.has('media', '123');
79
82
 
80
- **Purpose**: Routes requests to Service Worker
83
+ // Store widget HTML
84
+ await store.put('widget', '472/221/190', htmlBlob, 'text/html');
81
85
 
82
- **Responsibilities**:
83
- - Detects Service Worker availability
84
- - Uses postMessage for downloads
85
- - Uses fetch for file retrieval
86
- - Non-blocking downloads (background)
86
+ // Get file
87
+ const blob = await store.get('media', '123');
87
88
 
88
- **Implementation Details**:
89
- ```javascript
90
- class ServiceWorkerBackend {
91
- async getFile(type, id) {
92
- // Fetch from /player/cache/{type}/{id}
93
- // Service Worker intercepts and serves from cache
94
- }
95
-
96
- async requestDownload(files) {
97
- // postMessage to Service Worker
98
- // Downloads happen in background
99
- // Returns immediately
100
- }
101
- }
89
+ // List all cached files
90
+ const files = await store.list();
102
91
  ```
103
92
 
104
- ### 3. DirectCacheBackend
93
+ ### 2. DownloadClient (SW postMessage)
105
94
 
106
- **Purpose**: Fallback when Service Worker unavailable
95
+ **Location**: `packages/cache/src/download-client.js`
107
96
 
108
- **Responsibilities**:
109
- - Uses cache.js directly
110
- - Blocking downloads
111
- - IndexedDB metadata storage
112
- - Browser Cache API for files
97
+ **Purpose**: Communicates with the Service Worker's DownloadManager via postMessage to manage background downloads.
113
98
 
114
- **Implementation Details**:
99
+ **API**:
115
100
  ```javascript
116
- class DirectCacheBackend {
117
- async getFile(type, id) {
118
- // Call cacheManager.getCachedFile()
119
- // Direct Cache API access
120
- }
121
-
122
- async requestDownload(files) {
123
- // Sequential downloads
124
- // Blocks until complete
125
- // Fallback for non-Service Worker environments
126
- }
101
+ class DownloadClient {
102
+ async init() // Wait for SW ready
103
+ async download(files) // SW DOWNLOAD_FILES
104
+ async prioritize(type, id) // SW PRIORITIZE_DOWNLOAD
105
+ async prioritizeLayout(mediaIds) // SW PRIORITIZE_LAYOUT_FILES
106
+ async getProgress() // SW GET_DOWNLOAD_PROGRESS
127
107
  }
128
108
  ```
129
109
 
130
- ## Backend Selection
131
-
132
- CacheProxy automatically selects the best backend:
133
-
110
+ **Usage**:
134
111
  ```javascript
135
- async init() {
136
- // Try Service Worker first
137
- if (navigator.serviceWorker?.controller) {
138
- this.backend = new ServiceWorkerBackend();
139
- this.backendType = 'service-worker';
140
- } else {
141
- // Fallback to direct cache
142
- this.backend = new DirectCacheBackend(cacheManager);
143
- this.backendType = 'direct';
144
- }
145
- }
146
- ```
147
-
148
- ### Selection Criteria
112
+ import { DownloadClient } from '@xiboplayer/cache';
149
113
 
150
- | Condition | Backend | Reason |
151
- |-----------|---------|--------|
152
- | Service Worker active | ServiceWorkerBackend | Better performance, non-blocking |
153
- | Service Worker not active | DirectCacheBackend | Compatibility, works everywhere |
154
- | Service Worker failed | DirectCacheBackend | Graceful degradation |
114
+ const downloads = new DownloadClient();
115
+ await downloads.init();
155
116
 
156
- ## Integration Example
117
+ // Request background downloads
118
+ await downloads.download(files);
157
119
 
158
- ### Before (Without CacheProxy)
120
+ // Prioritize a file needed for the current layout
121
+ await downloads.prioritize('media', '12');
159
122
 
160
- ```javascript
161
- // Complex logic with manual fallback
162
- const serviceWorkerActive = navigator.serviceWorker?.controller;
163
-
164
- if (serviceWorkerActive) {
165
- try {
166
- await sendFilesToServiceWorker(files);
167
- } catch (error) {
168
- for (const file of files) {
169
- await cacheManager.downloadFile(file);
170
- }
171
- }
172
- } else {
173
- for (const file of files) {
174
- await cacheManager.downloadFile(file);
175
- }
176
- }
177
-
178
- // Separate media retrieval
179
- const response = await cacheManager.getCachedResponse('media', fileId);
180
- const blob = await response.blob();
123
+ // Check download progress
124
+ const progress = await downloads.getProgress();
181
125
  ```
182
126
 
183
- ### After (With CacheProxy)
127
+ ## REST API Routes (Proxy)
184
128
 
185
- ```javascript
186
- // Simple, unified interface
187
- await cacheProxy.requestDownload(files);
129
+ The proxy server (`packages/proxy/src/proxy.js`) exposes these endpoints backed by ContentStore:
188
130
 
189
- // Consistent file retrieval
190
- const blob = await cacheProxy.getFile('media', fileId);
191
- ```
131
+ | Method | Route | Purpose |
132
+ |--------|-------|---------|
133
+ | `GET` | `/store/:type/:id` | Serve file (Range support) |
134
+ | `HEAD` | `/store/:type/:id` | Existence + size check |
135
+ | `PUT` | `/store/:type/:id` | Store file |
136
+ | `POST` | `/store/delete` | Delete files |
137
+ | `POST` | `/store/mark-complete` | Mark chunked download complete |
138
+ | `GET` | `/store/list` | List all cached files |
192
139
 
193
- **Benefits**:
194
- - 75% less code
195
- - Automatic backend selection
196
- - No error handling duplication
197
- - Platform-independent
140
+ ### ContentStore (Filesystem)
198
141
 
199
- ## Performance Characteristics
142
+ **Location**: `packages/proxy/src/content-store.js`
200
143
 
201
- ### Service Worker Backend
144
+ The ContentStore manages files on disk with metadata:
202
145
 
203
- | Metric | Value | Notes |
204
- |--------|-------|-------|
205
- | Download latency | ~100ms | postMessage + enqueue |
206
- | File retrieval | ~10ms | Fetch intercept |
207
- | Blocking | No | Downloads in background |
208
- | Concurrency | 4 files | Configurable |
209
-
210
- ### Direct Cache Backend
146
+ ```
147
+ ~/.config/xiboplayer/{electron,chromium}/content-store/
148
+ ├── media/
149
+ │ ├── 12.bin # Video file
150
+ │ ├── 12.meta.json # { contentType, size, cachedAt, md5 }
151
+ │ ├── 34.bin # Image file
152
+ │ └── 34.meta.json
153
+ ├── layout/
154
+ │ ├── 472.bin # XLF layout XML
155
+ │ └── 472.meta.json
156
+ ├── widget/
157
+ │ ├── 472/221/190.bin # Widget HTML
158
+ │ └── 472/221/190.meta.json
159
+ └── static/
160
+ ├── bundle.min.js.bin
161
+ ├── fonts.css.bin
162
+ ├── Aileron-Heavy.otf.bin
163
+ └── ...
164
+ ```
211
165
 
212
- | Metric | Value | Notes |
213
- |--------|-------|-------|
214
- | Download latency | Varies | Depends on file size |
215
- | File retrieval | ~5ms | Direct cache access |
216
- | Blocking | Yes | Sequential downloads |
217
- | Concurrency | 1 file | No parallelism |
166
+ ## Download Flow
218
167
 
219
- ## Service Worker Download Flow
168
+ ### Service Worker Download
220
169
 
221
170
  ```
222
- Client CacheProxy Service Worker
171
+ Client DownloadClient Service Worker
223
172
  │ │ │
224
- requestDownload() │ │
225
- │─────────────────────> │
173
+ download(files) │ │
174
+ │──────────────────────>│
226
175
  │ │ postMessage │
227
176
  │ │ (DOWNLOAD_FILES) │
228
- │─────────────────────> │
177
+ │──────────────────────>│
229
178
  │ │ │
230
179
  │ │ │ enqueue files
231
- │ │ │ start downloads
180
+ │ │ │ download from CMS
181
+ │ │ │ PUT /store/:type/:id
232
182
  │ │ │
233
183
  │ │ acknowledge │
234
- │ <─────────────────────│
184
+ │<──────────────────────│
235
185
  │ return (immediate) │ │
236
- │ <─────────────────────│
237
- │ │ │
238
- │ ... continue work ... │ │ ... downloading ...
186
+ │<──────────────────────│
239
187
  │ │ │
240
- getFile('media', '1') │ │
241
- │─────────────────────> │ │
242
- │ │ fetch /cache/media/1 │
243
- │ │─────────────────────> │
188
+ ... continue work ... │ │
244
189
  │ │ │
245
- │ │ if cached: serve
246
- │ if downloading: wait
247
- │ │ if not found: 404
248
- │ │
249
- │ <─────────────────────│
250
- │ <─────────────────────│
251
- blob │ │
190
+ store.has('media','1')│ │
191
+ │──────────────────────>│
192
+ │ │ HEAD /store/media/1 │
193
+ │──────────────────────>│
194
+ │<──────────────────────│
195
+ │<──────────────────────│
196
+ { exists: true } │ │
252
197
  ```
253
198
 
254
- ## Direct Cache Download Flow
199
+ ### Widget HTML Storage
255
200
 
256
- ```
257
- Client CacheProxy Cache.js
258
- │ │ │
259
- │ requestDownload() │ │
260
- │─────────────────────> │ │
261
- │ │ downloadFile() x N │
262
- │ │─────────────────────> │
263
- │ │ │ fetch file
264
- │ │ │ verify MD5
265
- │ │ │ cache in Cache API
266
- │ │ │ save metadata to IDB
267
- │ │ │
268
- │ │ <─────────────────────│
269
- │ return (after all │ │
270
- │ downloads complete) │ │
271
- │ <─────────────────────│ │
272
- ```
201
+ Widget HTML is processed on the main thread by `cacheWidgetHtml()`:
273
202
 
274
- ## Error Handling
203
+ 1. Fetch widget HTML from CMS (`getResource`)
204
+ 2. Inject `<base>` tag for relative path resolution
205
+ 3. Rewrite CMS signed URLs → local `/player/cache/static/*` paths
206
+ 4. Fetch and store static dependencies (bundle.min.js, fonts.css, fonts)
207
+ 5. Store widget HTML via `PUT /store/widget/{layoutId}/{regionId}/{mediaId}`
275
208
 
276
- CacheProxy handles errors gracefully:
209
+ Static resources are stored before widget HTML to prevent race conditions when the iframe loads.
277
210
 
278
- ```javascript
279
- try {
280
- await cacheProxy.requestDownload(files);
281
- } catch (error) {
282
- // Backend-specific error
283
- // Already logged by backend
284
- // Fallback handled automatically
285
- }
286
- ```
287
-
288
- ### Error Scenarios
289
-
290
- | Scenario | Service Worker | Direct Cache |
291
- |----------|----------------|--------------|
292
- | Network failure | Retry in SW | Throw error |
293
- | File not found | 404 on fetch | null on get |
294
- | MD5 mismatch | Log warning, continue | Log warning, continue |
295
- | SW not available | Auto-switch to Direct | N/A |
296
-
297
- ## Testing
298
-
299
- ### Unit Tests
300
-
301
- ```javascript
302
- // Test backend detection
303
- it('should use Service Worker when available', async () => {
304
- const proxy = new CacheProxy(cacheManager);
305
- await proxy.init();
306
- expect(proxy.getBackendType()).toBe('service-worker');
307
- });
308
-
309
- // Test fallback
310
- it('should fallback to direct cache', async () => {
311
- // Mock SW not available
312
- const proxy = new CacheProxy(cacheManager);
313
- await proxy.init();
314
- expect(proxy.getBackendType()).toBe('direct');
315
- });
316
- ```
317
-
318
- ### Integration Tests
319
-
320
- ```javascript
321
- // Test file download
322
- it('should download and cache files', async () => {
323
- const files = [{ id: '1', type: 'media', path: 'https://...' }];
324
- await proxy.requestDownload(files);
325
-
326
- const blob = await proxy.getFile('media', '1');
327
- expect(blob).toBeTruthy();
328
- expect(blob.size).toBeGreaterThan(0);
329
- });
330
- ```
331
-
332
- ## Migration Guide
333
-
334
- ### For Existing Platforms
335
-
336
- 1. **Import CacheProxy**:
337
- ```javascript
338
- import { CacheProxy } from '@core/cache-proxy.js';
339
- ```
340
-
341
- 2. **Initialize**:
342
- ```javascript
343
- const cacheProxy = new CacheProxy(cacheManager);
344
- await cacheProxy.init();
345
- ```
346
-
347
- 3. **Replace download code**:
348
- ```javascript
349
- // Old:
350
- await cacheManager.downloadFile(file);
351
-
352
- // New:
353
- await cacheProxy.requestDownload([file]);
354
- ```
355
-
356
- 4. **Replace file retrieval**:
357
- ```javascript
358
- // Old:
359
- const response = await cacheManager.getCachedResponse('media', id);
360
- const blob = await response.blob();
361
-
362
- // New:
363
- const blob = await cacheProxy.getFile('media', id);
364
- ```
365
-
366
- ## Widget Data Download Flow
367
-
368
- Widget data for RSS feeds and dataset widgets is handled server-side. The CMS enriches
369
- the RequiredFiles response with absolute download URLs for widget data files. These are
370
- downloaded through the normal CacheProxy/Service Worker pipeline alongside regular media,
371
- rather than being fetched client-side by the player. This ensures widget data is available
372
- offline and benefits from the same parallel chunk download and caching infrastructure.
373
-
374
- Widget HTML served from cache uses a dynamic `<base>` tag pointing to the Service Worker
375
- scope path, ensuring relative URLs within widget HTML resolve correctly regardless of the
376
- player's deployment path.
377
-
378
- ### For New Platforms
379
-
380
- Simply use CacheProxy from the start:
381
-
382
- ```javascript
383
- class MyPlayer {
384
- async init() {
385
- // Initialize CacheProxy
386
- this.cache = new CacheProxy(cacheManager);
387
- await this.cache.init();
388
-
389
- // Use unified API
390
- const files = await this.xmds.requiredFiles();
391
- await this.cache.requestDownload(files);
392
-
393
- const blob = await this.cache.getFile('media', '123');
394
- }
395
- }
396
- ```
397
-
398
- ## Future Enhancements
399
-
400
- ### Planned Features
401
-
402
- 1. **Smart Backend Switching**:
403
- - Monitor Service Worker health
404
- - Auto-switch if SW becomes unresponsive
405
- - Fallback to direct cache on errors
406
-
407
- 2. **Progress Tracking**:
408
- ```javascript
409
- proxy.on('download-progress', (progress) => {
410
- console.log(`Downloaded ${progress.loaded}/${progress.total}`);
411
- });
412
- ```
211
+ ## Error Handling
413
212
 
414
- 3. **Cache Invalidation**:
415
- ```javascript
416
- await proxy.invalidate('media', '123');
417
- await proxy.invalidateAll();
418
- ```
213
+ | Scenario | StoreClient | DownloadClient |
214
+ |----------|-------------|----------------|
215
+ | Network failure | `fetch()` throws | SW retries internally |
216
+ | File not found | `has()` → `{ exists: false }` | N/A |
217
+ | Proxy down | `fetch()` throws | SW queues for retry |
218
+ | SW not ready | N/A | `init()` waits for activation |
419
219
 
420
- 4. **Prefetching**:
421
- ```javascript
422
- await proxy.prefetch(['media/1', 'layout/2']);
423
- ```
220
+ ## Performance
424
221
 
425
- ## Benefits
222
+ ### StoreClient
426
223
 
427
- ### For Developers
224
+ | Operation | Latency | Notes |
225
+ |-----------|---------|-------|
226
+ | `has()` | ~2ms | HEAD request to local proxy |
227
+ | `get()` | ~5ms | GET from filesystem |
228
+ | `put()` | ~10ms | Write to filesystem |
229
+ | `list()` | ~20ms | Scan all type directories |
428
230
 
429
- 1. **Simpler Code**: 75% reduction in cache-related code
430
- 2. **Automatic Optimization**: Best backend selected automatically
431
- 3. **Platform Independence**: Same code works everywhere
432
- 4. **Better Testing**: Mock backends for unit tests
231
+ ### DownloadClient
433
232
 
434
- ### For Users
233
+ | Operation | Latency | Notes |
234
+ |-----------|---------|-------|
235
+ | `download()` | ~100ms | postMessage + enqueue |
236
+ | `prioritize()` | ~50ms | Reorder queue |
237
+ | `getProgress()` | ~50ms | Query active downloads |
435
238
 
436
- 1. **Better Performance**: Service Worker when available
437
- 2. **Better Compatibility**: Fallback when SW unavailable
438
- 3. **Transparent**: No difference in functionality
439
- 4. **Reliable**: Graceful degradation on errors
239
+ Downloads run with configurable concurrency (default 6 on high-RAM devices).
440
240
 
441
241
  ## Summary
442
242
 
443
- CacheProxy provides a clean abstraction layer that:
444
- - Automatically selects best backend
445
- - Provides unified API across platforms
446
- - Enables non-blocking downloads with Service Worker
447
- - ✅ Gracefully falls back to direct cache
448
- - ✅ Simplifies platform-specific code
449
- - ✅ Makes testing easier
243
+ - **StoreClient**: Pure REST, no SW dependency, works immediately
244
+ - **DownloadClient**: SW postMessage, non-blocking background downloads
245
+ - **ContentStore**: Filesystem storage, no Cache API anywhere
246
+ - **Types**: media/, layout/, widget/, static/
450
247
 
451
- **Result**: Platform-independent cache code that just works.
248
+ **Result**: Single storage backend (filesystem), two clean client interfaces, zero Cache API usage.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/cache",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
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.5.7"
15
+ "@xiboplayer/utils": "0.5.9"
16
16
  },
17
17
  "devDependencies": {
18
18
  "vitest": "^2.0.0",