@xiboplayer/cache 0.4.0 → 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.4.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.4.0"
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
 
@@ -97,6 +97,20 @@ export function isUrlExpired(url, graceSeconds = 30) {
97
97
  return (Date.now() / 1000) >= (expiry - graceSeconds);
98
98
  }
99
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
+
100
114
  /**
101
115
  * DownloadTask - Single HTTP fetch unit
102
116
  *
@@ -120,7 +134,7 @@ export class DownloadTask {
120
134
  if (isUrlExpired(url)) {
121
135
  throw new Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);
122
136
  }
123
- return url;
137
+ return rewriteUrlForProxy(url);
124
138
  }
125
139
 
126
140
  async start() {
@@ -207,7 +221,7 @@ export class FileDownload {
207
221
  if (isUrlExpired(url)) {
208
222
  throw new Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);
209
223
  }
210
- return url;
224
+ return rewriteUrlForProxy(url);
211
225
  }
212
226
 
213
227
  wait() {
@@ -35,8 +35,8 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
35
35
  const cache = await caches.open(CACHE_NAME);
36
36
 
37
37
  // Inject <base> tag to fix relative paths for widget dependencies
38
- // Widget HTML has relative paths like "bundle.min.js" that should resolve to /player/cache/media/
39
- 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/">`;
40
40
  let modifiedHtml = html;
41
41
 
42
42
  // Insert base tag after <head> opening tag
@@ -80,7 +80,7 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
80
80
  `hostAddress: "${BASE}/ic"`
81
81
  );
82
82
 
83
- log.info('Injected base tag and rewrote CMS URLs in widget HTML');
83
+ log.info('Injected base tag and rewrote CMS/data URLs in widget HTML');
84
84
 
85
85
  // Construct full URL for cache storage
86
86
  const cacheUrl = new URL(cacheKey, window.location.origin);