@xiboplayer/cache 0.1.0
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/docs/CACHE_PROXY_ARCHITECTURE.md +439 -0
- package/docs/README.md +118 -0
- package/package.json +41 -0
- package/src/cache-proxy.js +493 -0
- package/src/cache-proxy.test.js +391 -0
- package/src/cache.js +739 -0
- package/src/cache.test.js +760 -0
- package/src/download-manager.js +434 -0
- package/src/download-manager.test.js +726 -0
- package/src/index.js +4 -0
- package/src/test-utils.js +133 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CacheProxy - Service Worker cache interface
|
|
3
|
+
*
|
|
4
|
+
* Provides a proxy to Service Worker for background downloads and caching.
|
|
5
|
+
* Service Worker MUST be available - no fallback (committed to SW architecture).
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* ┌─────────────────────────────────────────┐
|
|
9
|
+
* │ CacheProxy (Service Worker Only) │
|
|
10
|
+
* │ - Waits for Service Worker ready │
|
|
11
|
+
* │ - Routes all requests to SW │
|
|
12
|
+
* │ - No fallback to direct cache │
|
|
13
|
+
* └─────────────────────────────────────────┘
|
|
14
|
+
* ↓
|
|
15
|
+
* ┌──────────────────┐
|
|
16
|
+
* │ ServiceWorker │
|
|
17
|
+
* │ Backend │
|
|
18
|
+
* │ - postMessage │
|
|
19
|
+
* │ - Background DL │
|
|
20
|
+
* └──────────────────┘
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* const proxy = new CacheProxy();
|
|
24
|
+
* await proxy.init(); // Waits for SW to be ready
|
|
25
|
+
*
|
|
26
|
+
* const blob = await proxy.getFile('media', '123');
|
|
27
|
+
* await proxy.requestDownload([{ id, type, path, md5 }]);
|
|
28
|
+
* const isCached = await proxy.isCached('layout', '456');
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { EventEmitter, createLogger } from '@xiboplayer/utils';
|
|
32
|
+
|
|
33
|
+
const log = createLogger('CacheProxy');
|
|
34
|
+
|
|
35
|
+
// Dynamic base path for multi-variant deployment (pwa, pwa-xmds, pwa-xlr)
|
|
36
|
+
const BASE = (typeof window !== 'undefined')
|
|
37
|
+
? window.location.pathname.replace(/\/[^/]*$/, '').replace(/\/$/, '') || '/player/pwa'
|
|
38
|
+
: '/player/pwa';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* ServiceWorkerBackend - Uses Service Worker for downloads and caching
|
|
42
|
+
*/
|
|
43
|
+
class ServiceWorkerBackend extends EventEmitter {
|
|
44
|
+
constructor() {
|
|
45
|
+
super();
|
|
46
|
+
this.controller = null;
|
|
47
|
+
this.fetchReady = false;
|
|
48
|
+
this.fetchReadyPromise = null;
|
|
49
|
+
this.fetchReadyResolve = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async init() {
|
|
53
|
+
// Create promise for fetch readiness (resolved when SW sends SW_READY)
|
|
54
|
+
this.fetchReadyPromise = new Promise(resolve => {
|
|
55
|
+
this.fetchReadyResolve = resolve;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Listen for SW_READY message
|
|
59
|
+
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
60
|
+
if (event.data?.type === 'SW_READY') {
|
|
61
|
+
log.info('Received SW_READY signal - fetch handler is ready');
|
|
62
|
+
this.fetchReady = true;
|
|
63
|
+
this.fetchReadyResolve();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Get the active Service Worker (don't require controller)
|
|
68
|
+
if ('serviceWorker' in navigator) {
|
|
69
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
70
|
+
|
|
71
|
+
// Use active SW if it exists (controller not required!)
|
|
72
|
+
if (registration && registration.active && registration.active.state === 'activated') {
|
|
73
|
+
log.info('Using active Service Worker (controller not required)');
|
|
74
|
+
this.controller = navigator.serviceWorker.controller || registration.active;
|
|
75
|
+
|
|
76
|
+
// Request readiness signal from SW
|
|
77
|
+
this.controller.postMessage({ type: 'PING' });
|
|
78
|
+
|
|
79
|
+
log.info('Service Worker backend initialized, waiting for fetch readiness...');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fall back to waiting for ready
|
|
84
|
+
await navigator.serviceWorker.ready;
|
|
85
|
+
this.controller = navigator.serviceWorker.controller;
|
|
86
|
+
|
|
87
|
+
if (!this.controller) {
|
|
88
|
+
throw new Error('Service Worker not controlling page');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Request readiness signal from SW
|
|
92
|
+
this.controller.postMessage({ type: 'PING' });
|
|
93
|
+
|
|
94
|
+
log.info('Service Worker backend initialized, waiting for fetch readiness...');
|
|
95
|
+
} else {
|
|
96
|
+
throw new Error('Service Worker not supported');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get file from cache (via Service Worker)
|
|
102
|
+
* @param {string} type - 'media', 'layout', 'widget'
|
|
103
|
+
* @param {string} id - File ID
|
|
104
|
+
* @returns {Promise<Blob|null>}
|
|
105
|
+
*/
|
|
106
|
+
async getFile(type, id) {
|
|
107
|
+
// Wait for SW fetch handler to be ready (eliminates race condition)
|
|
108
|
+
if (!this.fetchReady) {
|
|
109
|
+
log.debug(`Waiting for SW fetch handler to be ready before fetching ${type}/${id}...`);
|
|
110
|
+
await this.fetchReadyPromise;
|
|
111
|
+
log.debug(`SW fetch handler ready, proceeding with fetch`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Service Worker serves files via fetch interception
|
|
115
|
+
// Construct cache URL and fetch it
|
|
116
|
+
const cacheUrl = `${BASE}/cache/${type}/${id}`;
|
|
117
|
+
|
|
118
|
+
log.debug(`getFile(${type}, ${id}) → fetching ${cacheUrl}`);
|
|
119
|
+
log.debug(`About to call fetch()...`);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
log.debug(`Calling fetch(${cacheUrl})...`);
|
|
123
|
+
const response = await fetch(cacheUrl);
|
|
124
|
+
log.debug(`fetch returned, status:`, response.status, response.statusText);
|
|
125
|
+
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
log.debug(`Response not OK (${response.status}), returning null`);
|
|
128
|
+
if (response.status === 404) {
|
|
129
|
+
return null; // Not cached
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Failed to get file: ${response.status}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
log.debug(`Response OK, getting blob...`);
|
|
135
|
+
const blob = await response.blob();
|
|
136
|
+
log.debug(`Got blob, size:`, blob.size);
|
|
137
|
+
return blob;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
log.error('getFile EXCEPTION:', error);
|
|
140
|
+
log.error('Error name:', error.name);
|
|
141
|
+
log.error('Error message:', error.message);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if file exists in cache (supports both whole files and chunked storage)
|
|
148
|
+
* Service Worker's CacheManager.fileExists() handles the logic internally
|
|
149
|
+
* @param {string} type - 'media', 'layout', 'widget'
|
|
150
|
+
* @param {string} id - File ID
|
|
151
|
+
* @returns {Promise<boolean>}
|
|
152
|
+
*/
|
|
153
|
+
async hasFile(type, id) {
|
|
154
|
+
// Wait for SW fetch handler to be ready
|
|
155
|
+
if (!this.fetchReady) {
|
|
156
|
+
await this.fetchReadyPromise;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const cacheUrl = `${BASE}/cache/${type}/${id}`;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// SW's handleRequest uses CacheManager.fileExists() internally
|
|
163
|
+
// Returns 200 for both whole files and chunked files (via metadata check)
|
|
164
|
+
const response = await fetch(cacheUrl, { method: 'HEAD' });
|
|
165
|
+
return response.ok;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Request downloads from Service Worker (non-blocking)
|
|
173
|
+
* @param {Array} files - Array of { id, type, path, md5 }
|
|
174
|
+
* @returns {Promise<void>}
|
|
175
|
+
*/
|
|
176
|
+
async requestDownload(files) {
|
|
177
|
+
if (!this.controller) {
|
|
178
|
+
throw new Error('Service Worker not available');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
const messageChannel = new MessageChannel();
|
|
183
|
+
|
|
184
|
+
messageChannel.port1.onmessage = (event) => {
|
|
185
|
+
const { success, error, enqueuedCount, activeCount, queuedCount } = event.data;
|
|
186
|
+
if (success) {
|
|
187
|
+
log.info('Download request acknowledged:', enqueuedCount, 'files');
|
|
188
|
+
log.info('Queue state:', activeCount, 'active,', queuedCount, 'queued');
|
|
189
|
+
resolve();
|
|
190
|
+
} else {
|
|
191
|
+
reject(new Error(error || 'Service Worker download failed'));
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
this.controller.postMessage(
|
|
196
|
+
{
|
|
197
|
+
type: 'DOWNLOAD_FILES',
|
|
198
|
+
data: { files }
|
|
199
|
+
},
|
|
200
|
+
[messageChannel.port2]
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Tell SW to prioritize downloading a specific file
|
|
207
|
+
* Moves it to the front of the download queue if still queued
|
|
208
|
+
* @param {string} fileType - 'media' or 'layout'
|
|
209
|
+
* @param {string} fileId - File ID
|
|
210
|
+
*/
|
|
211
|
+
async prioritizeDownload(fileType, fileId) {
|
|
212
|
+
if (!this.controller) return;
|
|
213
|
+
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
const messageChannel = new MessageChannel();
|
|
216
|
+
messageChannel.port1.onmessage = (event) => resolve(event.data);
|
|
217
|
+
this.controller.postMessage(
|
|
218
|
+
{ type: 'PRIORITIZE_DOWNLOAD', data: { fileType, fileId } },
|
|
219
|
+
[messageChannel.port2]
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if file is cached
|
|
226
|
+
* @param {string} type - 'media', 'layout', 'widget'
|
|
227
|
+
* @param {string} id - File ID
|
|
228
|
+
* @returns {Promise<boolean>}
|
|
229
|
+
*/
|
|
230
|
+
async isCached(type, id) {
|
|
231
|
+
const cacheUrl = `${BASE}/cache/${type}/${id}`;
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const response = await fetch(cacheUrl, { method: 'HEAD' });
|
|
235
|
+
return response.ok;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// DirectCacheBackend removed - Service Worker only architecture
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* CacheProxy - Service Worker only interface
|
|
246
|
+
*/
|
|
247
|
+
export class CacheProxy extends EventEmitter {
|
|
248
|
+
constructor() {
|
|
249
|
+
super();
|
|
250
|
+
this.backend = null;
|
|
251
|
+
this.backendType = 'service-worker';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Initialize proxy - WAITS for Service Worker to be ready
|
|
256
|
+
*/
|
|
257
|
+
async init() {
|
|
258
|
+
if (!('serviceWorker' in navigator)) {
|
|
259
|
+
throw new Error('Service Worker not supported - PWA requires Service Worker');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
log.debug('Checking Service Worker state...');
|
|
263
|
+
log.debug('controller =', navigator.serviceWorker.controller);
|
|
264
|
+
|
|
265
|
+
// Check if SW registration exists (better than checking controller)
|
|
266
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
267
|
+
log.debug('registration =', registration);
|
|
268
|
+
log.debug('active =', registration?.active);
|
|
269
|
+
log.debug('installing =', registration?.installing);
|
|
270
|
+
log.debug('waiting =', registration?.waiting);
|
|
271
|
+
|
|
272
|
+
// FAST PATH: If active SW exists AND no new SW is installing, use it immediately
|
|
273
|
+
if (registration && registration.active && !registration.installing && !registration.waiting) {
|
|
274
|
+
log.info('Active Service Worker found (no updates pending)');
|
|
275
|
+
log.debug('SW state =', registration.active.state);
|
|
276
|
+
|
|
277
|
+
// If not controlling yet, give it a moment to claim page
|
|
278
|
+
if (!navigator.serviceWorker.controller) {
|
|
279
|
+
log.debug('Not controlling yet, waiting 200ms for claim...');
|
|
280
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
281
|
+
log.debug('After wait, controller =', navigator.serviceWorker.controller);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Use the active SW (even if controller is still null - it will work)
|
|
285
|
+
this.backend = new ServiceWorkerBackend();
|
|
286
|
+
await this.backend.init();
|
|
287
|
+
log.info('Service Worker backend ready (fast path)');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// If there's a new SW installing/waiting, wait for it instead of using old one
|
|
292
|
+
if (registration && (registration.installing || registration.waiting)) {
|
|
293
|
+
log.info('New Service Worker detected, waiting for it to activate...');
|
|
294
|
+
log.debug('installing =', registration.installing?.state);
|
|
295
|
+
log.debug('waiting =', registration.waiting?.state);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// SLOW PATH: No active SW, wait for registration (fresh install)
|
|
299
|
+
log.info('No active Service Worker, waiting for registration...');
|
|
300
|
+
|
|
301
|
+
// Wait with timeout
|
|
302
|
+
const swReady = navigator.serviceWorker.ready;
|
|
303
|
+
const timeout = new Promise((_, reject) =>
|
|
304
|
+
setTimeout(() => reject(new Error('Service Worker ready timeout after 10s')), 10000)
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await Promise.race([swReady, timeout]);
|
|
309
|
+
log.debug('Service Worker ready promise resolved');
|
|
310
|
+
} catch (error) {
|
|
311
|
+
log.error('Service Worker wait failed:', error);
|
|
312
|
+
throw new Error('Service Worker not ready - please reload page');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Wait for SW to claim page
|
|
316
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
317
|
+
log.debug('After claim wait, controller =', navigator.serviceWorker.controller);
|
|
318
|
+
|
|
319
|
+
// Controller not required - we can use registration.active instead
|
|
320
|
+
// This handles the case where SW is active but hasn't set controller yet (timing issue)
|
|
321
|
+
this.backend = new ServiceWorkerBackend();
|
|
322
|
+
await this.backend.init();
|
|
323
|
+
log.info('Service Worker backend ready (slow path)');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get file from cache
|
|
328
|
+
* @param {string} type - 'media', 'layout', 'widget'
|
|
329
|
+
* @param {string} id - File ID
|
|
330
|
+
* @returns {Promise<Blob|null>}
|
|
331
|
+
*/
|
|
332
|
+
async getFile(type, id) {
|
|
333
|
+
if (!this.backend) {
|
|
334
|
+
throw new Error('CacheProxy not initialized');
|
|
335
|
+
}
|
|
336
|
+
return await this.backend.getFile(type, id);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Check if file exists in cache (for streaming - no blob creation)
|
|
341
|
+
* @param {string} type - 'media', 'layout', 'widget'
|
|
342
|
+
* @param {string} id - File ID
|
|
343
|
+
* @returns {Promise<boolean>}
|
|
344
|
+
*/
|
|
345
|
+
async hasFile(type, id) {
|
|
346
|
+
if (!this.backend) {
|
|
347
|
+
throw new Error('CacheProxy not initialized');
|
|
348
|
+
}
|
|
349
|
+
return await this.backend.hasFile(type, id);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Request file downloads
|
|
354
|
+
* Service Worker: Non-blocking (downloads in background)
|
|
355
|
+
* Direct cache: Blocking (downloads sequentially)
|
|
356
|
+
*
|
|
357
|
+
* @param {Array} files - Array of { id, type, path, md5 }
|
|
358
|
+
* @returns {Promise<void>}
|
|
359
|
+
*/
|
|
360
|
+
async requestDownload(files) {
|
|
361
|
+
if (!this.backend) {
|
|
362
|
+
throw new Error('CacheProxy not initialized');
|
|
363
|
+
}
|
|
364
|
+
return await this.backend.requestDownload(files);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Prioritize downloading a specific file (move to front of queue)
|
|
369
|
+
* @param {string} fileType - 'media' or 'layout'
|
|
370
|
+
* @param {string} fileId - File ID
|
|
371
|
+
*/
|
|
372
|
+
async prioritizeDownload(fileType, fileId) {
|
|
373
|
+
if (!this.backend?.prioritizeDownload) return;
|
|
374
|
+
return await this.backend.prioritizeDownload(fileType, fileId);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Check if file is cached
|
|
379
|
+
* @param {string} type - 'media', 'layout', 'widget'
|
|
380
|
+
* @param {string} id - File ID
|
|
381
|
+
* @returns {Promise<boolean>}
|
|
382
|
+
*/
|
|
383
|
+
async isCached(type, id) {
|
|
384
|
+
if (!this.backend) {
|
|
385
|
+
throw new Error('CacheProxy not initialized');
|
|
386
|
+
}
|
|
387
|
+
return await this.backend.isCached(type, id);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get backend type for debugging
|
|
392
|
+
* @returns {string} 'service-worker' or 'direct'
|
|
393
|
+
*/
|
|
394
|
+
getBackendType() {
|
|
395
|
+
return this.backendType;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Check if Service Worker is being used
|
|
400
|
+
* @returns {boolean}
|
|
401
|
+
*/
|
|
402
|
+
isUsingServiceWorker() {
|
|
403
|
+
return this.backendType === 'service-worker';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Delete files from cache (purge obsolete media)
|
|
408
|
+
* @param {Array<{type: string, id: string}>} files - Files to delete
|
|
409
|
+
* @returns {Promise<{deleted: number, total: number}>}
|
|
410
|
+
*/
|
|
411
|
+
async deleteFiles(files) {
|
|
412
|
+
if (!this.backend) {
|
|
413
|
+
throw new Error('CacheProxy not initialized');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return new Promise((resolve, reject) => {
|
|
417
|
+
const channel = new MessageChannel();
|
|
418
|
+
|
|
419
|
+
channel.port1.onmessage = (event) => {
|
|
420
|
+
const { success, error, deleted, total } = event.data;
|
|
421
|
+
if (success) {
|
|
422
|
+
resolve({ deleted, total });
|
|
423
|
+
} else {
|
|
424
|
+
reject(new Error(error || 'Delete failed'));
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
navigator.serviceWorker.controller.postMessage(
|
|
429
|
+
{ type: 'DELETE_FILES', data: { files } },
|
|
430
|
+
[channel.port2]
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
setTimeout(() => resolve({ deleted: 0, total: files.length }), 5000);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Pre-warm video chunks into SW BlobCache for faster playback startup.
|
|
439
|
+
* Loads first and last chunks into memory so moov atom probing is instant.
|
|
440
|
+
* @param {number[]} mediaIds - Media file IDs to pre-warm
|
|
441
|
+
* @returns {Promise<{warmed: number, total: number}>}
|
|
442
|
+
*/
|
|
443
|
+
async prewarmVideoChunks(mediaIds) {
|
|
444
|
+
if (!mediaIds || mediaIds.length === 0) return { warmed: 0, total: 0 };
|
|
445
|
+
|
|
446
|
+
const controller = navigator.serviceWorker?.controller;
|
|
447
|
+
if (!controller) return { warmed: 0, total: mediaIds.length };
|
|
448
|
+
|
|
449
|
+
return new Promise((resolve) => {
|
|
450
|
+
const channel = new MessageChannel();
|
|
451
|
+
|
|
452
|
+
channel.port1.onmessage = (event) => {
|
|
453
|
+
const { success, warmed, total } = event.data;
|
|
454
|
+
resolve(success ? { warmed, total } : { warmed: 0, total: mediaIds.length });
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
controller.postMessage(
|
|
458
|
+
{ type: 'PREWARM_VIDEO_CHUNKS', data: { mediaIds } },
|
|
459
|
+
[channel.port2]
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
// Don't block layout rendering for more than 2s
|
|
463
|
+
setTimeout(() => resolve({ warmed: 0, total: mediaIds.length }), 2000);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Get download progress from Service Worker
|
|
469
|
+
* @returns {Promise<Object>} Progress info for all active downloads
|
|
470
|
+
*/
|
|
471
|
+
async getDownloadProgress() {
|
|
472
|
+
if (!this.backend) {
|
|
473
|
+
throw new Error('CacheProxy not initialized');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return new Promise((resolve) => {
|
|
477
|
+
const channel = new MessageChannel();
|
|
478
|
+
|
|
479
|
+
channel.port1.onmessage = (event) => {
|
|
480
|
+
const { success, progress } = event.data;
|
|
481
|
+
resolve(success ? progress : {});
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
navigator.serviceWorker.controller.postMessage(
|
|
485
|
+
{ type: 'GET_DOWNLOAD_PROGRESS' },
|
|
486
|
+
[channel.port2]
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
// Timeout after 1 second
|
|
490
|
+
setTimeout(() => resolve({}), 1000);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|