@xiboplayer/cache 0.3.7 → 0.4.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/README.md +2 -0
- package/docs/CACHE_PROXY_ARCHITECTURE.md +12 -0
- package/package.json +2 -2
- package/src/cache-analyzer.js +8 -0
- package/src/cache-analyzer.test.js +14 -0
- package/src/cache.js +4 -1
- package/src/download-manager.js +47 -30
- package/src/widget-html.js +16 -13
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ Manages media downloads and offline storage for Xibo players:
|
|
|
11
11
|
- **MD5 verification** — integrity checking with CRC32-based skip optimization
|
|
12
12
|
- **Download queue** — flat queue with barriers for layout-ordered downloading
|
|
13
13
|
- **CacheProxy** — browser-side proxy that communicates with a Service Worker backend
|
|
14
|
+
- **Widget data via enriched RequiredFiles** — RSS/dataset widget data is fetched through server-side enriched RequiredFiles paths (CMS adds download URLs), not via client-side pre-fetching
|
|
15
|
+
- **Dynamic BASE path** — widget HTML `<base>` tag uses a dynamic path within the Service Worker scope for correct relative URL resolution
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
16
18
|
|
|
@@ -363,6 +363,18 @@ it('should download and cache files', async () => {
|
|
|
363
363
|
const blob = await cacheProxy.getFile('media', id);
|
|
364
364
|
```
|
|
365
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
|
+
|
|
366
378
|
### For New Platforms
|
|
367
379
|
|
|
368
380
|
Simply use CacheProxy from the start:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/cache",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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.1"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"vitest": "^2.0.0",
|
package/src/cache-analyzer.js
CHANGED
|
@@ -56,6 +56,14 @@ export class CacheAnalyzer {
|
|
|
56
56
|
for (const file of cachedFiles) {
|
|
57
57
|
if (requiredIds.has(String(file.id))) {
|
|
58
58
|
required.push(file);
|
|
59
|
+
} else if (file.type === 'widget') {
|
|
60
|
+
// Widget HTML IDs are "layoutId/regionId/widgetId" — check parent layout
|
|
61
|
+
const parentLayoutId = String(file.id).split('/')[0];
|
|
62
|
+
if (requiredIds.has(parentLayoutId)) {
|
|
63
|
+
required.push(file);
|
|
64
|
+
} else {
|
|
65
|
+
orphaned.push(file);
|
|
66
|
+
}
|
|
59
67
|
} else {
|
|
60
68
|
orphaned.push(file);
|
|
61
69
|
}
|
|
@@ -170,6 +170,20 @@ describe('CacheAnalyzer', () => {
|
|
|
170
170
|
vi.unstubAllGlobals();
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
it('should treat widget HTML as required when parent layout is required', async () => {
|
|
174
|
+
mockCache._addFile({ id: '470', type: 'layout', size: 500, cachedAt: 100 });
|
|
175
|
+
mockCache._addFile({ id: '470/213/182', type: 'widget', size: 0, cachedAt: 0 });
|
|
176
|
+
mockCache._addFile({ id: '470/215/184', type: 'widget', size: 0, cachedAt: 0 });
|
|
177
|
+
mockCache._addFile({ id: '99/10/5', type: 'widget', size: 0, cachedAt: 0 });
|
|
178
|
+
|
|
179
|
+
const report = await analyzer.analyze([{ id: '470', type: 'layout' }]);
|
|
180
|
+
|
|
181
|
+
// Layout 470 + its 2 widgets = 3 required; widget for layout 99 = orphaned
|
|
182
|
+
expect(report.files.required).toBe(3);
|
|
183
|
+
expect(report.files.orphaned).toBe(1);
|
|
184
|
+
expect(report.orphaned[0].id).toBe('99/10/5');
|
|
185
|
+
});
|
|
186
|
+
|
|
173
187
|
it('should handle files with missing size or cachedAt', async () => {
|
|
174
188
|
mockCache._addFile({ id: '1', type: 'media' }); // no size, no cachedAt
|
|
175
189
|
|
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
|
|
@@ -94,6 +97,20 @@ export function isUrlExpired(url, graceSeconds = 30) {
|
|
|
94
97
|
return (Date.now() / 1000) >= (expiry - graceSeconds);
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Rewrite an absolute CMS URL through the local proxy when running behind
|
|
102
|
+
* the proxy server (Chromium kiosk or Electron).
|
|
103
|
+
* Detection: SW/window on localhost:8765 = proxy mode.
|
|
104
|
+
*/
|
|
105
|
+
export function rewriteUrlForProxy(url) {
|
|
106
|
+
if (!url.startsWith('http')) return url;
|
|
107
|
+
const loc = typeof self !== 'undefined' ? self.location : undefined;
|
|
108
|
+
if (!loc || loc.hostname !== 'localhost' || loc.port !== '8765') return url;
|
|
109
|
+
const parsed = new URL(url);
|
|
110
|
+
const cmsOrigin = parsed.origin;
|
|
111
|
+
return `/file-proxy?cms=${encodeURIComponent(cmsOrigin)}&url=${encodeURIComponent(parsed.pathname + parsed.search)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
97
114
|
/**
|
|
98
115
|
* DownloadTask - Single HTTP fetch unit
|
|
99
116
|
*
|
|
@@ -117,7 +134,7 @@ export class DownloadTask {
|
|
|
117
134
|
if (isUrlExpired(url)) {
|
|
118
135
|
throw new Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);
|
|
119
136
|
}
|
|
120
|
-
return url;
|
|
137
|
+
return rewriteUrlForProxy(url);
|
|
121
138
|
}
|
|
122
139
|
|
|
123
140
|
async start() {
|
|
@@ -149,7 +166,7 @@ export class DownloadTask {
|
|
|
149
166
|
if (attempt < MAX_RETRIES) {
|
|
150
167
|
const delay = RETRY_DELAY_MS * attempt;
|
|
151
168
|
const chunkLabel = this.chunkIndex != null ? ` chunk ${this.chunkIndex}` : '';
|
|
152
|
-
|
|
169
|
+
log.warn(`[DownloadTask] ${this.fileInfo.type}/${this.fileInfo.id}${chunkLabel} attempt ${attempt}/${MAX_RETRIES} failed: ${msg}. Retrying in ${delay / 1000}s...`);
|
|
153
170
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
154
171
|
} else {
|
|
155
172
|
this.state = 'failed';
|
|
@@ -204,7 +221,7 @@ export class FileDownload {
|
|
|
204
221
|
if (isUrlExpired(url)) {
|
|
205
222
|
throw new Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);
|
|
206
223
|
}
|
|
207
|
-
return url;
|
|
224
|
+
return rewriteUrlForProxy(url);
|
|
208
225
|
}
|
|
209
226
|
|
|
210
227
|
wait() {
|
|
@@ -220,7 +237,7 @@ export class FileDownload {
|
|
|
220
237
|
try {
|
|
221
238
|
this.state = 'preparing';
|
|
222
239
|
const { id, type, size } = this.fileInfo;
|
|
223
|
-
|
|
240
|
+
log.info('[FileDownload] Starting:', `${type}/${id}`);
|
|
224
241
|
|
|
225
242
|
// Use declared size from RequiredFiles — no HEAD needed for queue building
|
|
226
243
|
this.totalBytes = (size && size > 0) ? parseInt(size) : 0;
|
|
@@ -242,7 +259,7 @@ export class FileDownload {
|
|
|
242
259
|
}
|
|
243
260
|
}
|
|
244
261
|
|
|
245
|
-
|
|
262
|
+
log.info('[FileDownload] File size:', (this.totalBytes / 1024 / 1024).toFixed(1), 'MB');
|
|
246
263
|
|
|
247
264
|
const chunkSize = this.options.chunkSize || DEFAULT_CHUNK_SIZE;
|
|
248
265
|
|
|
@@ -267,14 +284,14 @@ export class FileDownload {
|
|
|
267
284
|
}
|
|
268
285
|
|
|
269
286
|
if (needed.length === 0) {
|
|
270
|
-
|
|
287
|
+
log.info('[FileDownload] All chunks already cached, nothing to download');
|
|
271
288
|
this.state = 'complete';
|
|
272
289
|
this._resolve(new Blob([], { type: this._contentType }));
|
|
273
290
|
return;
|
|
274
291
|
}
|
|
275
292
|
|
|
276
293
|
if (skippedCount > 0) {
|
|
277
|
-
|
|
294
|
+
log.info(`[FileDownload] Resuming: ${skippedCount} chunks cached, ${needed.length} to download`);
|
|
278
295
|
}
|
|
279
296
|
|
|
280
297
|
const isResume = skippedCount > 0;
|
|
@@ -301,7 +318,7 @@ export class FileDownload {
|
|
|
301
318
|
}
|
|
302
319
|
|
|
303
320
|
const highCount = this.tasks.filter(t => t._priority >= PRIORITY.high).length;
|
|
304
|
-
|
|
321
|
+
log.info(`[FileDownload] ${type}/${id}: ${this.tasks.length} chunks` +
|
|
305
322
|
(highCount > 0 ? ` (${highCount} priority)` : '') +
|
|
306
323
|
(isResume ? ' (resume)' : ''));
|
|
307
324
|
|
|
@@ -316,7 +333,7 @@ export class FileDownload {
|
|
|
316
333
|
this.state = 'downloading';
|
|
317
334
|
|
|
318
335
|
} catch (error) {
|
|
319
|
-
|
|
336
|
+
log.error('[FileDownload] Prepare failed:', `${this.fileInfo.type}/${this.fileInfo.id}`, error);
|
|
320
337
|
this.state = 'failed';
|
|
321
338
|
this._reject(error);
|
|
322
339
|
}
|
|
@@ -339,7 +356,7 @@ export class FileDownload {
|
|
|
339
356
|
try {
|
|
340
357
|
await this.onChunkDownloaded(task.chunkIndex, task.blob, this.totalChunks);
|
|
341
358
|
} catch (e) {
|
|
342
|
-
|
|
359
|
+
log.warn('[FileDownload] onChunkDownloaded callback error:', e);
|
|
343
360
|
}
|
|
344
361
|
}
|
|
345
362
|
|
|
@@ -348,10 +365,10 @@ export class FileDownload {
|
|
|
348
365
|
const { type, id } = this.fileInfo;
|
|
349
366
|
|
|
350
367
|
if (task.chunkIndex == null) {
|
|
351
|
-
|
|
368
|
+
log.info('[FileDownload] Complete:', `${type}/${id}`, `(${task.blob.size} bytes)`);
|
|
352
369
|
this._resolve(task.blob);
|
|
353
370
|
} else if (this.onChunkDownloaded) {
|
|
354
|
-
|
|
371
|
+
log.info('[FileDownload] Complete:', `${type}/${id}`, `(progressive, ${this.totalChunks} chunks)`);
|
|
355
372
|
this._resolve(new Blob([], { type: this._contentType }));
|
|
356
373
|
} else {
|
|
357
374
|
const ordered = [];
|
|
@@ -360,7 +377,7 @@ export class FileDownload {
|
|
|
360
377
|
if (blob) ordered.push(blob);
|
|
361
378
|
}
|
|
362
379
|
const assembled = new Blob(ordered, { type: this._contentType });
|
|
363
|
-
|
|
380
|
+
log.info('[FileDownload] Complete:', `${type}/${id}`, `(${assembled.size} bytes, reassembled)`);
|
|
364
381
|
this._resolve(assembled);
|
|
365
382
|
}
|
|
366
383
|
|
|
@@ -376,7 +393,7 @@ export class FileDownload {
|
|
|
376
393
|
// provides fresh URLs and the resume logic (skipChunks) fills the gaps.
|
|
377
394
|
if (error.message?.includes('URL expired')) {
|
|
378
395
|
const chunkLabel = task.chunkIndex != null ? ` chunk ${task.chunkIndex}` : '';
|
|
379
|
-
|
|
396
|
+
log.warn(`[FileDownload] URL expired, dropping${chunkLabel}:`, `${this.fileInfo.type}/${this.fileInfo.id}`);
|
|
380
397
|
this.tasks = this.tasks.filter(t => t !== task);
|
|
381
398
|
// If all remaining tasks completed, resolve as partial
|
|
382
399
|
if (this.tasks.length === 0 || this.completedChunks >= this.tasks.length) {
|
|
@@ -387,7 +404,7 @@ export class FileDownload {
|
|
|
387
404
|
return;
|
|
388
405
|
}
|
|
389
406
|
|
|
390
|
-
|
|
407
|
+
log.error('[FileDownload] Failed:', `${this.fileInfo.type}/${this.fileInfo.id}`, error);
|
|
391
408
|
this.state = 'failed';
|
|
392
409
|
this._reject(error);
|
|
393
410
|
}
|
|
@@ -564,7 +581,7 @@ export class DownloadQueue {
|
|
|
564
581
|
const oldExpiry = getUrlExpiry(existing.fileInfo.path);
|
|
565
582
|
const newExpiry = getUrlExpiry(fileInfo.path);
|
|
566
583
|
if (newExpiry > oldExpiry) {
|
|
567
|
-
|
|
584
|
+
log.info('[DownloadQueue] Refreshing URL for', key);
|
|
568
585
|
existing.fileInfo.path = fileInfo.path;
|
|
569
586
|
}
|
|
570
587
|
}
|
|
@@ -578,7 +595,7 @@ export class DownloadQueue {
|
|
|
578
595
|
});
|
|
579
596
|
|
|
580
597
|
this.active.set(key, file);
|
|
581
|
-
|
|
598
|
+
log.info('[DownloadQueue] Enqueued:', key);
|
|
582
599
|
|
|
583
600
|
// Throttled prepare: HEAD requests are limited to avoid flooding connections
|
|
584
601
|
this._schedulePrepare(file);
|
|
@@ -612,7 +629,7 @@ export class DownloadQueue {
|
|
|
612
629
|
}
|
|
613
630
|
this._sortQueue();
|
|
614
631
|
|
|
615
|
-
|
|
632
|
+
log.info(`[DownloadQueue] ${tasks.length} tasks added (${this.queue.length} pending, ${this.running} active)`);
|
|
616
633
|
this.processQueue();
|
|
617
634
|
}
|
|
618
635
|
|
|
@@ -638,7 +655,7 @@ export class DownloadQueue {
|
|
|
638
655
|
}
|
|
639
656
|
}
|
|
640
657
|
|
|
641
|
-
|
|
658
|
+
log.info(`[DownloadQueue] Ordered queue: ${taskCount} tasks, ${barrierCount} barriers (${this.queue.length} pending, ${this.running} active)`);
|
|
642
659
|
this.processQueue();
|
|
643
660
|
}
|
|
644
661
|
|
|
@@ -652,7 +669,7 @@ export class DownloadQueue {
|
|
|
652
669
|
const file = this.active.get(key);
|
|
653
670
|
|
|
654
671
|
if (!file) {
|
|
655
|
-
|
|
672
|
+
log.info('[DownloadQueue] Not found:', key);
|
|
656
673
|
return false;
|
|
657
674
|
}
|
|
658
675
|
|
|
@@ -665,7 +682,7 @@ export class DownloadQueue {
|
|
|
665
682
|
}
|
|
666
683
|
this._sortQueue();
|
|
667
684
|
|
|
668
|
-
|
|
685
|
+
log.info('[DownloadQueue] Prioritized:', key, `(${boosted} tasks boosted)`);
|
|
669
686
|
return true;
|
|
670
687
|
}
|
|
671
688
|
|
|
@@ -691,7 +708,7 @@ export class DownloadQueue {
|
|
|
691
708
|
}
|
|
692
709
|
this._sortQueue();
|
|
693
710
|
|
|
694
|
-
|
|
711
|
+
log.info('[DownloadQueue] Layout files prioritized:', idSet.size, 'files,', boosted, 'tasks boosted to', priority);
|
|
695
712
|
}
|
|
696
713
|
|
|
697
714
|
/**
|
|
@@ -704,7 +721,7 @@ export class DownloadQueue {
|
|
|
704
721
|
const file = this.active.get(key);
|
|
705
722
|
|
|
706
723
|
if (!file) {
|
|
707
|
-
|
|
724
|
+
log.info('[DownloadQueue] urgentChunk: file not active:', key, 'chunk', chunkIndex);
|
|
708
725
|
return false;
|
|
709
726
|
}
|
|
710
727
|
|
|
@@ -719,13 +736,13 @@ export class DownloadQueue {
|
|
|
719
736
|
);
|
|
720
737
|
if (activeTask && activeTask._priority < PRIORITY.urgent) {
|
|
721
738
|
activeTask._priority = PRIORITY.urgent;
|
|
722
|
-
|
|
739
|
+
log.info(`[DownloadQueue] URGENT: ${key} chunk ${chunkIndex} (already in-flight, limiting slots)`);
|
|
723
740
|
// Don't call processQueue() — can't stop in-flight tasks, but next
|
|
724
741
|
// processQueue() call (when any task completes) will see hasUrgent
|
|
725
742
|
// and limit new starts to URGENT_CONCURRENCY.
|
|
726
743
|
return true;
|
|
727
744
|
}
|
|
728
|
-
|
|
745
|
+
log.info('[DownloadQueue] urgentChunk: already urgent:', key, 'chunk', chunkIndex);
|
|
729
746
|
return false;
|
|
730
747
|
}
|
|
731
748
|
|
|
@@ -735,7 +752,7 @@ export class DownloadQueue {
|
|
|
735
752
|
);
|
|
736
753
|
|
|
737
754
|
if (idx === -1) {
|
|
738
|
-
|
|
755
|
+
log.info('[DownloadQueue] urgentChunk: chunk not in queue:', key, 'chunk', chunkIndex);
|
|
739
756
|
return false;
|
|
740
757
|
}
|
|
741
758
|
|
|
@@ -744,7 +761,7 @@ export class DownloadQueue {
|
|
|
744
761
|
// Move to front of queue (past any barriers)
|
|
745
762
|
this.queue.unshift(task);
|
|
746
763
|
|
|
747
|
-
|
|
764
|
+
log.info(`[DownloadQueue] URGENT: ${key} chunk ${chunkIndex} (moved to front)`);
|
|
748
765
|
this.processQueue();
|
|
749
766
|
return true;
|
|
750
767
|
}
|
|
@@ -809,7 +826,7 @@ export class DownloadQueue {
|
|
|
809
826
|
}
|
|
810
827
|
|
|
811
828
|
if (this.queue.length === 0 && this.running === 0) {
|
|
812
|
-
|
|
829
|
+
log.info('[DownloadQueue] All downloads complete');
|
|
813
830
|
}
|
|
814
831
|
}
|
|
815
832
|
|
|
@@ -827,14 +844,14 @@ export class DownloadQueue {
|
|
|
827
844
|
this._activeTasks.push(task);
|
|
828
845
|
const key = `${task.fileInfo.type}/${task.fileInfo.id}`;
|
|
829
846
|
const chunkLabel = task.chunkIndex != null ? ` chunk ${task.chunkIndex}` : '';
|
|
830
|
-
|
|
847
|
+
log.info(`[DownloadQueue] Starting: ${key}${chunkLabel} (${this.running}/${this.concurrency} active)`);
|
|
831
848
|
|
|
832
849
|
task.start()
|
|
833
850
|
.then(() => {
|
|
834
851
|
this.running--;
|
|
835
852
|
task._parentFile._runningCount--;
|
|
836
853
|
this._activeTasks = this._activeTasks.filter(t => t !== task);
|
|
837
|
-
|
|
854
|
+
log.info(`[DownloadQueue] Fetched: ${key}${chunkLabel} (${this.running} active, ${this.queue.length} pending)`);
|
|
838
855
|
this.processQueue();
|
|
839
856
|
return task._parentFile.onTaskComplete(task);
|
|
840
857
|
})
|
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)
|
|
@@ -32,8 +35,8 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
32
35
|
const cache = await caches.open(CACHE_NAME);
|
|
33
36
|
|
|
34
37
|
// Inject <base> tag to fix relative paths for widget dependencies
|
|
35
|
-
// Widget HTML has relative paths like "bundle.min.js" that should resolve to
|
|
36
|
-
const baseTag =
|
|
38
|
+
// Widget HTML has relative paths like "bundle.min.js" that should resolve to cache/media/
|
|
39
|
+
const baseTag = `<base href="${BASE}/cache/media/">`;
|
|
37
40
|
let modifiedHtml = html;
|
|
38
41
|
|
|
39
42
|
// Insert base tag after <head> opening tag
|
|
@@ -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/data 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
|
}
|