@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.
- package/README.md +22 -13
- package/docs/CACHE_PROXY_ARCHITECTURE.md +165 -368
- package/package.json +2 -2
- package/src/cache-analyzer.js +9 -5
- package/src/cache-analyzer.test.js +6 -6
- package/src/cache-proxy.test.js +239 -237
- package/src/cache.js +3 -6
- package/src/cache.test.js +2 -30
- package/src/download-client.js +222 -0
- package/src/download-manager.js +48 -5
- package/src/index.js +3 -2
- package/src/store-client.js +114 -0
- package/src/widget-html.js +70 -54
- package/src/widget-html.test.js +71 -62
- package/src/cache-proxy.js +0 -532
|
@@ -1,451 +1,248 @@
|
|
|
1
|
-
#
|
|
1
|
+
# StoreClient + DownloadClient Architecture
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
The
|
|
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,
|
|
12
|
-
│ -
|
|
13
|
-
│ -
|
|
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
|
-
│
|
|
28
|
-
│
|
|
23
|
+
│ StoreClient │ │ DownloadClient │
|
|
24
|
+
│ (REST) │ │ (SW postMessage)│
|
|
29
25
|
│ │ │ │
|
|
30
|
-
│
|
|
31
|
-
│
|
|
32
|
-
│
|
|
33
|
-
│
|
|
34
|
-
│
|
|
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.
|
|
57
|
+
### 1. StoreClient (REST)
|
|
41
58
|
|
|
42
|
-
**Location**: `packages/
|
|
59
|
+
**Location**: `packages/cache/src/store-client.js`
|
|
43
60
|
|
|
44
|
-
**Purpose**:
|
|
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
|
|
49
|
-
async
|
|
50
|
-
async
|
|
51
|
-
async
|
|
52
|
-
async
|
|
53
|
-
|
|
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 {
|
|
76
|
+
import { StoreClient } from '@xiboplayer/cache';
|
|
61
77
|
|
|
62
|
-
|
|
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
|
-
|
|
80
|
+
// Check if file exists
|
|
81
|
+
const { exists, size } = await store.has('media', '123');
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
// Store widget HTML
|
|
84
|
+
await store.put('widget', '472/221/190', htmlBlob, 'text/html');
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
###
|
|
93
|
+
### 2. DownloadClient (SW postMessage)
|
|
105
94
|
|
|
106
|
-
**
|
|
95
|
+
**Location**: `packages/cache/src/download-client.js`
|
|
107
96
|
|
|
108
|
-
**
|
|
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
|
-
**
|
|
99
|
+
**API**:
|
|
115
100
|
```javascript
|
|
116
|
-
class
|
|
117
|
-
async
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
CacheProxy automatically selects the best backend:
|
|
133
|
-
|
|
110
|
+
**Usage**:
|
|
134
111
|
```javascript
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
+
// Request background downloads
|
|
118
|
+
await downloads.download(files);
|
|
157
119
|
|
|
158
|
-
|
|
120
|
+
// Prioritize a file needed for the current layout
|
|
121
|
+
await downloads.prioritize('media', '12');
|
|
159
122
|
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
127
|
+
## REST API Routes (Proxy)
|
|
184
128
|
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
- 75% less code
|
|
195
|
-
- Automatic backend selection
|
|
196
|
-
- No error handling duplication
|
|
197
|
-
- Platform-independent
|
|
140
|
+
### ContentStore (Filesystem)
|
|
198
141
|
|
|
199
|
-
|
|
142
|
+
**Location**: `packages/proxy/src/content-store.js`
|
|
200
143
|
|
|
201
|
-
|
|
144
|
+
The ContentStore manages files on disk with metadata:
|
|
202
145
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
+
### Service Worker Download
|
|
220
169
|
|
|
221
170
|
```
|
|
222
|
-
Client
|
|
171
|
+
Client DownloadClient Service Worker
|
|
223
172
|
│ │ │
|
|
224
|
-
│
|
|
225
|
-
|
|
173
|
+
│ download(files) │ │
|
|
174
|
+
│──────────────────────>│ │
|
|
226
175
|
│ │ postMessage │
|
|
227
176
|
│ │ (DOWNLOAD_FILES) │
|
|
228
|
-
│
|
|
177
|
+
│ │──────────────────────>│
|
|
229
178
|
│ │ │
|
|
230
179
|
│ │ │ enqueue files
|
|
231
|
-
│ │ │
|
|
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
|
-
│
|
|
241
|
-
│─────────────────────> │ │
|
|
242
|
-
│ │ fetch /cache/media/1 │
|
|
243
|
-
│ │─────────────────────> │
|
|
188
|
+
│ ... continue work ... │ │
|
|
244
189
|
│ │ │
|
|
245
|
-
│
|
|
246
|
-
|
|
247
|
-
│ │
|
|
248
|
-
│
|
|
249
|
-
│
|
|
250
|
-
|
|
251
|
-
│
|
|
190
|
+
│ store.has('media','1')│ │
|
|
191
|
+
│──────────────────────>│ │
|
|
192
|
+
│ │ HEAD /store/media/1 │
|
|
193
|
+
│ │──────────────────────>│
|
|
194
|
+
│ │<──────────────────────│
|
|
195
|
+
│<──────────────────────│ │
|
|
196
|
+
│ { exists: true } │ │
|
|
252
197
|
```
|
|
253
198
|
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
+
Static resources are stored before widget HTML to prevent race conditions when the iframe loads.
|
|
277
210
|
|
|
278
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
421
|
-
```javascript
|
|
422
|
-
await proxy.prefetch(['media/1', 'layout/2']);
|
|
423
|
-
```
|
|
220
|
+
## Performance
|
|
424
221
|
|
|
425
|
-
|
|
222
|
+
### StoreClient
|
|
426
223
|
|
|
427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
+
| Operation | Latency | Notes |
|
|
234
|
+
|-----------|---------|-------|
|
|
235
|
+
| `download()` | ~100ms | postMessage + enqueue |
|
|
236
|
+
| `prioritize()` | ~50ms | Reorder queue |
|
|
237
|
+
| `getProgress()` | ~50ms | Query active downloads |
|
|
435
238
|
|
|
436
|
-
|
|
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
|
-
|
|
444
|
-
-
|
|
445
|
-
-
|
|
446
|
-
-
|
|
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**:
|
|
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.
|
|
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.
|
|
15
|
+
"@xiboplayer/utils": "0.5.9"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"vitest": "^2.0.0",
|