@xiboplayer/cache 0.3.6 → 0.4.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/package.json +2 -2
- package/src/cache.js +4 -1
- package/src/download-manager.js +31 -28
- package/src/widget-html.js +14 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/cache",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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.
|
|
15
|
+
"@xiboplayer/utils": "0.4.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"vitest": "^2.0.0",
|
package/src/cache.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* - Full cache clearing
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { createLogger } from '@xiboplayer/utils';
|
|
12
|
+
|
|
13
|
+
const log = createLogger('Cache');
|
|
11
14
|
const CACHE_NAME = 'xibo-media-v1';
|
|
12
15
|
|
|
13
16
|
// Dynamic base path for multi-variant deployment (pwa, pwa-xmds, pwa-xlr)
|
|
@@ -61,7 +64,7 @@ export class CacheManager {
|
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
if (orphaned.length > 0) {
|
|
64
|
-
|
|
67
|
+
log.info(`${orphaned.length} media files orphaned after layout ${layoutId} removed:`, orphaned);
|
|
65
68
|
}
|
|
66
69
|
return orphaned;
|
|
67
70
|
}
|
package/src/download-manager.js
CHANGED
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
* const blob = await file.wait();
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { createLogger } from '@xiboplayer/utils';
|
|
29
|
+
|
|
30
|
+
const log = createLogger('Download');
|
|
28
31
|
const DEFAULT_CONCURRENCY = 6; // Max concurrent HTTP connections (matches Chromium per-host limit)
|
|
29
32
|
const DEFAULT_CHUNK_SIZE = 50 * 1024 * 1024; // 50MB chunks
|
|
30
33
|
const DEFAULT_MAX_CHUNKS_PER_FILE = 3; // Max parallel chunk downloads per file
|
|
@@ -149,7 +152,7 @@ export class DownloadTask {
|
|
|
149
152
|
if (attempt < MAX_RETRIES) {
|
|
150
153
|
const delay = RETRY_DELAY_MS * attempt;
|
|
151
154
|
const chunkLabel = this.chunkIndex != null ? ` chunk ${this.chunkIndex}` : '';
|
|
152
|
-
|
|
155
|
+
log.warn(`[DownloadTask] ${this.fileInfo.type}/${this.fileInfo.id}${chunkLabel} attempt ${attempt}/${MAX_RETRIES} failed: ${msg}. Retrying in ${delay / 1000}s...`);
|
|
153
156
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
154
157
|
} else {
|
|
155
158
|
this.state = 'failed';
|
|
@@ -220,7 +223,7 @@ export class FileDownload {
|
|
|
220
223
|
try {
|
|
221
224
|
this.state = 'preparing';
|
|
222
225
|
const { id, type, size } = this.fileInfo;
|
|
223
|
-
|
|
226
|
+
log.info('[FileDownload] Starting:', `${type}/${id}`);
|
|
224
227
|
|
|
225
228
|
// Use declared size from RequiredFiles — no HEAD needed for queue building
|
|
226
229
|
this.totalBytes = (size && size > 0) ? parseInt(size) : 0;
|
|
@@ -242,7 +245,7 @@ export class FileDownload {
|
|
|
242
245
|
}
|
|
243
246
|
}
|
|
244
247
|
|
|
245
|
-
|
|
248
|
+
log.info('[FileDownload] File size:', (this.totalBytes / 1024 / 1024).toFixed(1), 'MB');
|
|
246
249
|
|
|
247
250
|
const chunkSize = this.options.chunkSize || DEFAULT_CHUNK_SIZE;
|
|
248
251
|
|
|
@@ -267,14 +270,14 @@ export class FileDownload {
|
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
if (needed.length === 0) {
|
|
270
|
-
|
|
273
|
+
log.info('[FileDownload] All chunks already cached, nothing to download');
|
|
271
274
|
this.state = 'complete';
|
|
272
275
|
this._resolve(new Blob([], { type: this._contentType }));
|
|
273
276
|
return;
|
|
274
277
|
}
|
|
275
278
|
|
|
276
279
|
if (skippedCount > 0) {
|
|
277
|
-
|
|
280
|
+
log.info(`[FileDownload] Resuming: ${skippedCount} chunks cached, ${needed.length} to download`);
|
|
278
281
|
}
|
|
279
282
|
|
|
280
283
|
const isResume = skippedCount > 0;
|
|
@@ -301,7 +304,7 @@ export class FileDownload {
|
|
|
301
304
|
}
|
|
302
305
|
|
|
303
306
|
const highCount = this.tasks.filter(t => t._priority >= PRIORITY.high).length;
|
|
304
|
-
|
|
307
|
+
log.info(`[FileDownload] ${type}/${id}: ${this.tasks.length} chunks` +
|
|
305
308
|
(highCount > 0 ? ` (${highCount} priority)` : '') +
|
|
306
309
|
(isResume ? ' (resume)' : ''));
|
|
307
310
|
|
|
@@ -316,7 +319,7 @@ export class FileDownload {
|
|
|
316
319
|
this.state = 'downloading';
|
|
317
320
|
|
|
318
321
|
} catch (error) {
|
|
319
|
-
|
|
322
|
+
log.error('[FileDownload] Prepare failed:', `${this.fileInfo.type}/${this.fileInfo.id}`, error);
|
|
320
323
|
this.state = 'failed';
|
|
321
324
|
this._reject(error);
|
|
322
325
|
}
|
|
@@ -339,7 +342,7 @@ export class FileDownload {
|
|
|
339
342
|
try {
|
|
340
343
|
await this.onChunkDownloaded(task.chunkIndex, task.blob, this.totalChunks);
|
|
341
344
|
} catch (e) {
|
|
342
|
-
|
|
345
|
+
log.warn('[FileDownload] onChunkDownloaded callback error:', e);
|
|
343
346
|
}
|
|
344
347
|
}
|
|
345
348
|
|
|
@@ -348,10 +351,10 @@ export class FileDownload {
|
|
|
348
351
|
const { type, id } = this.fileInfo;
|
|
349
352
|
|
|
350
353
|
if (task.chunkIndex == null) {
|
|
351
|
-
|
|
354
|
+
log.info('[FileDownload] Complete:', `${type}/${id}`, `(${task.blob.size} bytes)`);
|
|
352
355
|
this._resolve(task.blob);
|
|
353
356
|
} else if (this.onChunkDownloaded) {
|
|
354
|
-
|
|
357
|
+
log.info('[FileDownload] Complete:', `${type}/${id}`, `(progressive, ${this.totalChunks} chunks)`);
|
|
355
358
|
this._resolve(new Blob([], { type: this._contentType }));
|
|
356
359
|
} else {
|
|
357
360
|
const ordered = [];
|
|
@@ -360,7 +363,7 @@ export class FileDownload {
|
|
|
360
363
|
if (blob) ordered.push(blob);
|
|
361
364
|
}
|
|
362
365
|
const assembled = new Blob(ordered, { type: this._contentType });
|
|
363
|
-
|
|
366
|
+
log.info('[FileDownload] Complete:', `${type}/${id}`, `(${assembled.size} bytes, reassembled)`);
|
|
364
367
|
this._resolve(assembled);
|
|
365
368
|
}
|
|
366
369
|
|
|
@@ -376,7 +379,7 @@ export class FileDownload {
|
|
|
376
379
|
// provides fresh URLs and the resume logic (skipChunks) fills the gaps.
|
|
377
380
|
if (error.message?.includes('URL expired')) {
|
|
378
381
|
const chunkLabel = task.chunkIndex != null ? ` chunk ${task.chunkIndex}` : '';
|
|
379
|
-
|
|
382
|
+
log.warn(`[FileDownload] URL expired, dropping${chunkLabel}:`, `${this.fileInfo.type}/${this.fileInfo.id}`);
|
|
380
383
|
this.tasks = this.tasks.filter(t => t !== task);
|
|
381
384
|
// If all remaining tasks completed, resolve as partial
|
|
382
385
|
if (this.tasks.length === 0 || this.completedChunks >= this.tasks.length) {
|
|
@@ -387,7 +390,7 @@ export class FileDownload {
|
|
|
387
390
|
return;
|
|
388
391
|
}
|
|
389
392
|
|
|
390
|
-
|
|
393
|
+
log.error('[FileDownload] Failed:', `${this.fileInfo.type}/${this.fileInfo.id}`, error);
|
|
391
394
|
this.state = 'failed';
|
|
392
395
|
this._reject(error);
|
|
393
396
|
}
|
|
@@ -564,7 +567,7 @@ export class DownloadQueue {
|
|
|
564
567
|
const oldExpiry = getUrlExpiry(existing.fileInfo.path);
|
|
565
568
|
const newExpiry = getUrlExpiry(fileInfo.path);
|
|
566
569
|
if (newExpiry > oldExpiry) {
|
|
567
|
-
|
|
570
|
+
log.info('[DownloadQueue] Refreshing URL for', key);
|
|
568
571
|
existing.fileInfo.path = fileInfo.path;
|
|
569
572
|
}
|
|
570
573
|
}
|
|
@@ -578,7 +581,7 @@ export class DownloadQueue {
|
|
|
578
581
|
});
|
|
579
582
|
|
|
580
583
|
this.active.set(key, file);
|
|
581
|
-
|
|
584
|
+
log.info('[DownloadQueue] Enqueued:', key);
|
|
582
585
|
|
|
583
586
|
// Throttled prepare: HEAD requests are limited to avoid flooding connections
|
|
584
587
|
this._schedulePrepare(file);
|
|
@@ -612,7 +615,7 @@ export class DownloadQueue {
|
|
|
612
615
|
}
|
|
613
616
|
this._sortQueue();
|
|
614
617
|
|
|
615
|
-
|
|
618
|
+
log.info(`[DownloadQueue] ${tasks.length} tasks added (${this.queue.length} pending, ${this.running} active)`);
|
|
616
619
|
this.processQueue();
|
|
617
620
|
}
|
|
618
621
|
|
|
@@ -638,7 +641,7 @@ export class DownloadQueue {
|
|
|
638
641
|
}
|
|
639
642
|
}
|
|
640
643
|
|
|
641
|
-
|
|
644
|
+
log.info(`[DownloadQueue] Ordered queue: ${taskCount} tasks, ${barrierCount} barriers (${this.queue.length} pending, ${this.running} active)`);
|
|
642
645
|
this.processQueue();
|
|
643
646
|
}
|
|
644
647
|
|
|
@@ -652,7 +655,7 @@ export class DownloadQueue {
|
|
|
652
655
|
const file = this.active.get(key);
|
|
653
656
|
|
|
654
657
|
if (!file) {
|
|
655
|
-
|
|
658
|
+
log.info('[DownloadQueue] Not found:', key);
|
|
656
659
|
return false;
|
|
657
660
|
}
|
|
658
661
|
|
|
@@ -665,7 +668,7 @@ export class DownloadQueue {
|
|
|
665
668
|
}
|
|
666
669
|
this._sortQueue();
|
|
667
670
|
|
|
668
|
-
|
|
671
|
+
log.info('[DownloadQueue] Prioritized:', key, `(${boosted} tasks boosted)`);
|
|
669
672
|
return true;
|
|
670
673
|
}
|
|
671
674
|
|
|
@@ -691,7 +694,7 @@ export class DownloadQueue {
|
|
|
691
694
|
}
|
|
692
695
|
this._sortQueue();
|
|
693
696
|
|
|
694
|
-
|
|
697
|
+
log.info('[DownloadQueue] Layout files prioritized:', idSet.size, 'files,', boosted, 'tasks boosted to', priority);
|
|
695
698
|
}
|
|
696
699
|
|
|
697
700
|
/**
|
|
@@ -704,7 +707,7 @@ export class DownloadQueue {
|
|
|
704
707
|
const file = this.active.get(key);
|
|
705
708
|
|
|
706
709
|
if (!file) {
|
|
707
|
-
|
|
710
|
+
log.info('[DownloadQueue] urgentChunk: file not active:', key, 'chunk', chunkIndex);
|
|
708
711
|
return false;
|
|
709
712
|
}
|
|
710
713
|
|
|
@@ -719,13 +722,13 @@ export class DownloadQueue {
|
|
|
719
722
|
);
|
|
720
723
|
if (activeTask && activeTask._priority < PRIORITY.urgent) {
|
|
721
724
|
activeTask._priority = PRIORITY.urgent;
|
|
722
|
-
|
|
725
|
+
log.info(`[DownloadQueue] URGENT: ${key} chunk ${chunkIndex} (already in-flight, limiting slots)`);
|
|
723
726
|
// Don't call processQueue() — can't stop in-flight tasks, but next
|
|
724
727
|
// processQueue() call (when any task completes) will see hasUrgent
|
|
725
728
|
// and limit new starts to URGENT_CONCURRENCY.
|
|
726
729
|
return true;
|
|
727
730
|
}
|
|
728
|
-
|
|
731
|
+
log.info('[DownloadQueue] urgentChunk: already urgent:', key, 'chunk', chunkIndex);
|
|
729
732
|
return false;
|
|
730
733
|
}
|
|
731
734
|
|
|
@@ -735,7 +738,7 @@ export class DownloadQueue {
|
|
|
735
738
|
);
|
|
736
739
|
|
|
737
740
|
if (idx === -1) {
|
|
738
|
-
|
|
741
|
+
log.info('[DownloadQueue] urgentChunk: chunk not in queue:', key, 'chunk', chunkIndex);
|
|
739
742
|
return false;
|
|
740
743
|
}
|
|
741
744
|
|
|
@@ -744,7 +747,7 @@ export class DownloadQueue {
|
|
|
744
747
|
// Move to front of queue (past any barriers)
|
|
745
748
|
this.queue.unshift(task);
|
|
746
749
|
|
|
747
|
-
|
|
750
|
+
log.info(`[DownloadQueue] URGENT: ${key} chunk ${chunkIndex} (moved to front)`);
|
|
748
751
|
this.processQueue();
|
|
749
752
|
return true;
|
|
750
753
|
}
|
|
@@ -809,7 +812,7 @@ export class DownloadQueue {
|
|
|
809
812
|
}
|
|
810
813
|
|
|
811
814
|
if (this.queue.length === 0 && this.running === 0) {
|
|
812
|
-
|
|
815
|
+
log.info('[DownloadQueue] All downloads complete');
|
|
813
816
|
}
|
|
814
817
|
}
|
|
815
818
|
|
|
@@ -827,14 +830,14 @@ export class DownloadQueue {
|
|
|
827
830
|
this._activeTasks.push(task);
|
|
828
831
|
const key = `${task.fileInfo.type}/${task.fileInfo.id}`;
|
|
829
832
|
const chunkLabel = task.chunkIndex != null ? ` chunk ${task.chunkIndex}` : '';
|
|
830
|
-
|
|
833
|
+
log.info(`[DownloadQueue] Starting: ${key}${chunkLabel} (${this.running}/${this.concurrency} active)`);
|
|
831
834
|
|
|
832
835
|
task.start()
|
|
833
836
|
.then(() => {
|
|
834
837
|
this.running--;
|
|
835
838
|
task._parentFile._runningCount--;
|
|
836
839
|
this._activeTasks = this._activeTasks.filter(t => t !== task);
|
|
837
|
-
|
|
840
|
+
log.info(`[DownloadQueue] Fetched: ${key}${chunkLabel} (${this.running} active, ${this.queue.length} pending)`);
|
|
838
841
|
this.processQueue();
|
|
839
842
|
return task._parentFile.onTaskComplete(task);
|
|
840
843
|
})
|
package/src/widget-html.js
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* Uses Cache API directly — the SW also serves from the same cache.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { createLogger } from '@xiboplayer/utils';
|
|
16
|
+
|
|
17
|
+
const log = createLogger('Cache');
|
|
15
18
|
const CACHE_NAME = 'xibo-media-v1';
|
|
16
19
|
|
|
17
20
|
// Dynamic base path for multi-variant deployment (pwa, pwa-xmds, pwa-xlr)
|
|
@@ -54,7 +57,7 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
54
57
|
modifiedHtml = modifiedHtml.replace(cmsUrlRegex, (match, filename) => {
|
|
55
58
|
const localPath = `${BASE}/cache/static/${filename}`;
|
|
56
59
|
staticResources.push({ filename, originalUrl: match });
|
|
57
|
-
|
|
60
|
+
log.info(`Rewrote widget URL: ${filename} → ${localPath}`);
|
|
58
61
|
return localPath;
|
|
59
62
|
});
|
|
60
63
|
|
|
@@ -77,7 +80,7 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
77
80
|
`hostAddress: "${BASE}/ic"`
|
|
78
81
|
);
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
log.info('Injected base tag and rewrote CMS URLs in widget HTML');
|
|
81
84
|
|
|
82
85
|
// Construct full URL for cache storage
|
|
83
86
|
const cacheUrl = new URL(cacheKey, window.location.origin);
|
|
@@ -90,7 +93,7 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
90
93
|
});
|
|
91
94
|
|
|
92
95
|
await cache.put(cacheUrl, response);
|
|
93
|
-
|
|
96
|
+
log.info(`Stored widget HTML at ${cacheKey} (${modifiedHtml.length} bytes)`);
|
|
94
97
|
|
|
95
98
|
// Fetch and cache static resources (shared Cache API - accessible from main thread and SW)
|
|
96
99
|
if (staticResources.length > 0) {
|
|
@@ -105,7 +108,7 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
105
108
|
try {
|
|
106
109
|
const resp = await fetch(originalUrl);
|
|
107
110
|
if (!resp.ok) {
|
|
108
|
-
|
|
111
|
+
log.warn(`Failed to fetch static resource: ${filename} (HTTP ${resp.status})`);
|
|
109
112
|
return;
|
|
110
113
|
}
|
|
111
114
|
|
|
@@ -126,14 +129,14 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
126
129
|
const fontUrlRegex = /url\((['"]?)(https?:\/\/[^'")\s]+\?[^'")\s]*file=([^&'")\s]+\.(?:woff2?|ttf|otf|eot|svg))[^'")\s]*)\1\)/gi;
|
|
127
130
|
cssText = cssText.replace(fontUrlRegex, (_match, quote, fullUrl, fontFilename) => {
|
|
128
131
|
fontResources.push({ filename: fontFilename, originalUrl: fullUrl });
|
|
129
|
-
|
|
132
|
+
log.info(`Rewrote font URL in CSS: ${fontFilename}`);
|
|
130
133
|
return `url(${quote}${BASE}/cache/static/${encodeURIComponent(fontFilename)}${quote})`;
|
|
131
134
|
});
|
|
132
135
|
|
|
133
136
|
await staticCache.put(staticKey, new Response(cssText, {
|
|
134
137
|
headers: { 'Content-Type': 'text/css' }
|
|
135
138
|
}));
|
|
136
|
-
|
|
139
|
+
log.info(`Cached CSS with ${fontResources.length} rewritten font URLs: ${filename}`);
|
|
137
140
|
|
|
138
141
|
// Fetch and cache referenced font files
|
|
139
142
|
await Promise.all(fontResources.map(async ({ filename: fontFile, originalUrl: fontUrl }) => {
|
|
@@ -144,7 +147,7 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
144
147
|
try {
|
|
145
148
|
const fontResp = await fetch(fontUrl);
|
|
146
149
|
if (!fontResp.ok) {
|
|
147
|
-
|
|
150
|
+
log.warn(`Failed to fetch font: ${fontFile} (HTTP ${fontResp.status})`);
|
|
148
151
|
return;
|
|
149
152
|
}
|
|
150
153
|
const fontBlob = await fontResp.blob();
|
|
@@ -159,9 +162,9 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
159
162
|
await staticCache.put(fontKey, new Response(fontBlob, {
|
|
160
163
|
headers: { 'Content-Type': fontContentType }
|
|
161
164
|
}));
|
|
162
|
-
|
|
165
|
+
log.info(`Cached font: ${fontFile} (${fontContentType}, ${fontBlob.size} bytes)`);
|
|
163
166
|
} catch (fontErr) {
|
|
164
|
-
|
|
167
|
+
log.warn(`Failed to cache font: ${fontFile}`, fontErr);
|
|
165
168
|
}
|
|
166
169
|
}));
|
|
167
170
|
} else {
|
|
@@ -169,10 +172,10 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
169
172
|
await staticCache.put(staticKey, new Response(blob, {
|
|
170
173
|
headers: { 'Content-Type': contentType }
|
|
171
174
|
}));
|
|
172
|
-
|
|
175
|
+
log.info(`Cached static resource: ${filename} (${contentType}, ${blob.size} bytes)`);
|
|
173
176
|
}
|
|
174
177
|
} catch (error) {
|
|
175
|
-
|
|
178
|
+
log.warn(`Failed to cache static resource: ${filename}`, error);
|
|
176
179
|
}
|
|
177
180
|
}));
|
|
178
181
|
}
|