@xiboplayer/cache 0.5.20 → 0.6.1
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 +42 -3
- package/package.json +2 -2
- package/src/cache-proxy.test.js +1 -206
- package/src/download-manager.js +37 -78
- package/src/file-types.js +26 -0
- package/src/index.d.ts +15 -16
- package/src/index.js +2 -2
- package/src/widget-html.js +24 -146
- package/src/widget-html.test.js +40 -119
- package/src/download-client.js +0 -222
package/src/download-client.js
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DownloadClient — Service Worker postMessage interface for download orchestration
|
|
3
|
-
*
|
|
4
|
-
* Communicates with the Service Worker via postMessage for:
|
|
5
|
-
* - Background file downloads (DOWNLOAD_FILES)
|
|
6
|
-
* - Download prioritization (PRIORITIZE_DOWNLOAD, PRIORITIZE_LAYOUT_FILES)
|
|
7
|
-
* - Progress reporting (GET_DOWNLOAD_PROGRESS)
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* const downloads = new DownloadClient();
|
|
11
|
-
* await downloads.init(); // Waits for SW to be ready
|
|
12
|
-
* await downloads.download(files);
|
|
13
|
-
* downloads.prioritize('media', '123');
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { createLogger } from '@xiboplayer/utils';
|
|
17
|
-
|
|
18
|
-
const log = createLogger('DownloadClient');
|
|
19
|
-
|
|
20
|
-
export class DownloadClient {
|
|
21
|
-
constructor() {
|
|
22
|
-
this.controller = null;
|
|
23
|
-
this.fetchReady = false;
|
|
24
|
-
this._fetchReadyPromise = null;
|
|
25
|
-
this._fetchReadyResolve = null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Initialize — waits for Service Worker to be ready and controlling the page.
|
|
30
|
-
*/
|
|
31
|
-
async init() {
|
|
32
|
-
if (!('serviceWorker' in navigator)) {
|
|
33
|
-
throw new Error('Service Worker not supported — PWA requires Service Worker');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Guard against double-initialization (would add duplicate listeners)
|
|
37
|
-
if (this._swReadyHandler) return;
|
|
38
|
-
|
|
39
|
-
// Create promise for fetch readiness (resolved when SW sends SW_READY)
|
|
40
|
-
this._fetchReadyPromise = new Promise(resolve => {
|
|
41
|
-
this._fetchReadyResolve = resolve;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Listen for SW_READY message (store handler for cleanup)
|
|
45
|
-
this._swReadyHandler = (event) => {
|
|
46
|
-
if (event.data?.type === 'SW_READY') {
|
|
47
|
-
log.info('Received SW_READY signal — fetch handler is ready');
|
|
48
|
-
this.fetchReady = true;
|
|
49
|
-
this._fetchReadyResolve();
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
navigator.serviceWorker.addEventListener('message', this._swReadyHandler);
|
|
53
|
-
|
|
54
|
-
const registration = await navigator.serviceWorker.getRegistration();
|
|
55
|
-
|
|
56
|
-
// FAST PATH: Active SW, no updates pending
|
|
57
|
-
if (registration && registration.active && !registration.installing && !registration.waiting) {
|
|
58
|
-
log.info('Active Service Worker found (no updates pending)');
|
|
59
|
-
this.controller = navigator.serviceWorker.controller || registration.active;
|
|
60
|
-
|
|
61
|
-
// If not controlling yet, give it a moment to claim page
|
|
62
|
-
if (!navigator.serviceWorker.controller) {
|
|
63
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
this.controller.postMessage({ type: 'PING' });
|
|
67
|
-
log.info('DownloadClient initialized, waiting for fetch readiness...');
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// If there's a new SW installing/waiting, wait for it
|
|
72
|
-
if (registration && (registration.installing || registration.waiting)) {
|
|
73
|
-
log.info('New Service Worker detected, waiting for it to activate...');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// SLOW PATH: No active SW, wait for registration (fresh install)
|
|
77
|
-
log.info('No active Service Worker, waiting for registration...');
|
|
78
|
-
|
|
79
|
-
const swReady = navigator.serviceWorker.ready;
|
|
80
|
-
const timeout = new Promise((_, reject) =>
|
|
81
|
-
setTimeout(() => reject(new Error('Service Worker ready timeout after 10s')), 10000)
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
await Promise.race([swReady, timeout]);
|
|
86
|
-
} catch (error) {
|
|
87
|
-
log.error('Service Worker wait failed:', error);
|
|
88
|
-
throw new Error('Service Worker not ready — please reload page');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Wait for SW to claim page
|
|
92
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
93
|
-
|
|
94
|
-
this.controller = navigator.serviceWorker.controller;
|
|
95
|
-
if (!this.controller) {
|
|
96
|
-
const reg = await navigator.serviceWorker.getRegistration();
|
|
97
|
-
this.controller = reg?.active;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (this.controller) {
|
|
101
|
-
this.controller.postMessage({ type: 'PING' });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
log.info('DownloadClient initialized (slow path)');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Wait for fetch readiness before operations that need it.
|
|
109
|
-
*/
|
|
110
|
-
async _ensureReady() {
|
|
111
|
-
if (!this.fetchReady && this._fetchReadyPromise) {
|
|
112
|
-
await this._fetchReadyPromise;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Request file downloads from Service Worker (non-blocking).
|
|
118
|
-
* @param {Object|Array} payload - { layoutOrder, files, layoutDependants } or flat Array
|
|
119
|
-
* @returns {Promise<void>}
|
|
120
|
-
*/
|
|
121
|
-
async download(payload) {
|
|
122
|
-
if (!this.controller) {
|
|
123
|
-
throw new Error('Service Worker not available');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const data = Array.isArray(payload)
|
|
127
|
-
? { files: payload }
|
|
128
|
-
: payload;
|
|
129
|
-
|
|
130
|
-
return new Promise((resolve, reject) => {
|
|
131
|
-
const messageChannel = new MessageChannel();
|
|
132
|
-
|
|
133
|
-
messageChannel.port1.onmessage = (event) => {
|
|
134
|
-
const { success, error, enqueuedCount, activeCount, queuedCount } = event.data;
|
|
135
|
-
if (success) {
|
|
136
|
-
log.info('Download request acknowledged:', enqueuedCount, 'files');
|
|
137
|
-
log.info('Queue state:', activeCount, 'active,', queuedCount, 'queued');
|
|
138
|
-
resolve();
|
|
139
|
-
} else {
|
|
140
|
-
reject(new Error(error || 'Service Worker download failed'));
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
this.controller.postMessage(
|
|
145
|
-
{ type: 'DOWNLOAD_FILES', data },
|
|
146
|
-
[messageChannel.port2]
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Prioritize downloading a specific file (move to front of queue).
|
|
153
|
-
* @param {string} fileType - 'media' or 'layout'
|
|
154
|
-
* @param {string} fileId - File ID
|
|
155
|
-
*/
|
|
156
|
-
async prioritize(fileType, fileId) {
|
|
157
|
-
if (!this.controller) return;
|
|
158
|
-
|
|
159
|
-
return new Promise((resolve) => {
|
|
160
|
-
const messageChannel = new MessageChannel();
|
|
161
|
-
messageChannel.port1.onmessage = (event) => resolve(event.data);
|
|
162
|
-
this.controller.postMessage(
|
|
163
|
-
{ type: 'PRIORITIZE_DOWNLOAD', data: { fileType, fileId } },
|
|
164
|
-
[messageChannel.port2]
|
|
165
|
-
);
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Prioritize layout files — reorder queue and hold other downloads until done.
|
|
171
|
-
* @param {string[]} mediaIds - Media IDs needed by the current layout
|
|
172
|
-
*/
|
|
173
|
-
async prioritizeLayout(mediaIds) {
|
|
174
|
-
if (!this.controller) return;
|
|
175
|
-
this.controller.postMessage({ type: 'PRIORITIZE_LAYOUT_FILES', data: { mediaIds } });
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Get download progress from Service Worker.
|
|
180
|
-
* @returns {Promise<Object>} Progress info for all active downloads
|
|
181
|
-
*/
|
|
182
|
-
async getProgress() {
|
|
183
|
-
if (!this.controller) return {};
|
|
184
|
-
|
|
185
|
-
return new Promise((resolve) => {
|
|
186
|
-
const channel = new MessageChannel();
|
|
187
|
-
let settled = false;
|
|
188
|
-
|
|
189
|
-
const timer = setTimeout(() => {
|
|
190
|
-
if (!settled) {
|
|
191
|
-
settled = true;
|
|
192
|
-
channel.port1.onmessage = null;
|
|
193
|
-
resolve({});
|
|
194
|
-
}
|
|
195
|
-
}, 1000);
|
|
196
|
-
|
|
197
|
-
channel.port1.onmessage = (event) => {
|
|
198
|
-
if (!settled) {
|
|
199
|
-
settled = true;
|
|
200
|
-
clearTimeout(timer);
|
|
201
|
-
const { success, progress } = event.data;
|
|
202
|
-
resolve(success ? progress : {});
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
this.controller.postMessage(
|
|
207
|
-
{ type: 'GET_DOWNLOAD_PROGRESS' },
|
|
208
|
-
[channel.port2]
|
|
209
|
-
);
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Remove event listeners added during init().
|
|
215
|
-
*/
|
|
216
|
-
cleanup() {
|
|
217
|
-
if (this._swReadyHandler && 'serviceWorker' in navigator) {
|
|
218
|
-
navigator.serviceWorker.removeEventListener('message', this._swReadyHandler);
|
|
219
|
-
this._swReadyHandler = null;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|