@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 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.7",
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.3.7"
15
+ "@xiboplayer/utils": "0.4.1"
16
16
  },
17
17
  "devDependencies": {
18
18
  "vitest": "^2.0.0",
@@ -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
- console.log(`[Cache] ${orphaned.length} media files orphaned after layout ${layoutId} removed:`, orphaned);
67
+ log.info(`${orphaned.length} media files orphaned after layout ${layoutId} removed:`, orphaned);
65
68
  }
66
69
  return orphaned;
67
70
  }
@@ -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
- console.warn(`[DownloadTask] ${this.fileInfo.type}/${this.fileInfo.id}${chunkLabel} attempt ${attempt}/${MAX_RETRIES} failed: ${msg}. Retrying in ${delay / 1000}s...`);
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
- console.log('[FileDownload] Starting:', `${type}/${id}`);
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
- console.log('[FileDownload] File size:', (this.totalBytes / 1024 / 1024).toFixed(1), 'MB');
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
- console.log('[FileDownload] All chunks already cached, nothing to download');
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
- console.log(`[FileDownload] Resuming: ${skippedCount} chunks cached, ${needed.length} to download`);
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
- console.log(`[FileDownload] ${type}/${id}: ${this.tasks.length} chunks` +
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
- console.error('[FileDownload] Prepare failed:', `${this.fileInfo.type}/${this.fileInfo.id}`, error);
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
- console.warn('[FileDownload] onChunkDownloaded callback error:', e);
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
- console.log('[FileDownload] Complete:', `${type}/${id}`, `(${task.blob.size} bytes)`);
368
+ log.info('[FileDownload] Complete:', `${type}/${id}`, `(${task.blob.size} bytes)`);
352
369
  this._resolve(task.blob);
353
370
  } else if (this.onChunkDownloaded) {
354
- console.log('[FileDownload] Complete:', `${type}/${id}`, `(progressive, ${this.totalChunks} chunks)`);
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
- console.log('[FileDownload] Complete:', `${type}/${id}`, `(${assembled.size} bytes, reassembled)`);
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
- console.warn(`[FileDownload] URL expired, dropping${chunkLabel}:`, `${this.fileInfo.type}/${this.fileInfo.id}`);
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
- console.error('[FileDownload] Failed:', `${this.fileInfo.type}/${this.fileInfo.id}`, error);
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
- console.log('[DownloadQueue] Refreshing URL for', key);
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
- console.log('[DownloadQueue] Enqueued:', key);
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
- console.log(`[DownloadQueue] ${tasks.length} tasks added (${this.queue.length} pending, ${this.running} active)`);
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
- console.log(`[DownloadQueue] Ordered queue: ${taskCount} tasks, ${barrierCount} barriers (${this.queue.length} pending, ${this.running} active)`);
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
- console.log('[DownloadQueue] Not found:', key);
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
- console.log('[DownloadQueue] Prioritized:', key, `(${boosted} tasks boosted)`);
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
- console.log('[DownloadQueue] Layout files prioritized:', idSet.size, 'files,', boosted, 'tasks boosted to', priority);
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
- console.log('[DownloadQueue] urgentChunk: file not active:', key, 'chunk', chunkIndex);
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
- console.log(`[DownloadQueue] URGENT: ${key} chunk ${chunkIndex} (already in-flight, limiting slots)`);
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
- console.log('[DownloadQueue] urgentChunk: already urgent:', key, 'chunk', chunkIndex);
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
- console.log('[DownloadQueue] urgentChunk: chunk not in queue:', key, 'chunk', chunkIndex);
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
- console.log(`[DownloadQueue] URGENT: ${key} chunk ${chunkIndex} (moved to front)`);
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
- console.log('[DownloadQueue] All downloads complete');
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
- console.log(`[DownloadQueue] Starting: ${key}${chunkLabel} (${this.running}/${this.concurrency} active)`);
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
- console.log(`[DownloadQueue] Fetched: ${key}${chunkLabel} (${this.running} active, ${this.queue.length} pending)`);
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
  })
@@ -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 /player/cache/media/
36
- const baseTag = '<base href="/player/cache/media/">';
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
- console.log(`[Cache] Rewrote widget URL: ${filename} → ${localPath}`);
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
- console.log(`[Cache] Injected base tag and rewrote CMS URLs in widget HTML`);
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
- console.log(`[Cache] Stored widget HTML at ${cacheKey} (${modifiedHtml.length} bytes)`);
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
- console.warn(`[Cache] Failed to fetch static resource: ${filename} (HTTP ${resp.status})`);
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
- console.log(`[Cache] Rewrote font URL in CSS: ${fontFilename}`);
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
- console.log(`[Cache] Cached CSS with ${fontResources.length} rewritten font URLs: ${filename}`);
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
- console.warn(`[Cache] Failed to fetch font: ${fontFile} (HTTP ${fontResp.status})`);
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
- console.log(`[Cache] Cached font: ${fontFile} (${fontContentType}, ${fontBlob.size} bytes)`);
165
+ log.info(`Cached font: ${fontFile} (${fontContentType}, ${fontBlob.size} bytes)`);
163
166
  } catch (fontErr) {
164
- console.warn(`[Cache] Failed to cache font: ${fontFile}`, fontErr);
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
- console.log(`[Cache] Cached static resource: ${filename} (${contentType}, ${blob.size} bytes)`);
175
+ log.info(`Cached static resource: ${filename} (${contentType}, ${blob.size} bytes)`);
173
176
  }
174
177
  } catch (error) {
175
- console.warn(`[Cache] Failed to cache static resource: ${filename}`, error);
178
+ log.warn(`Failed to cache static resource: ${filename}`, error);
176
179
  }
177
180
  }));
178
181
  }