@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 +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/download-manager.js +16 -2
- package/src/widget-html.js +3 -3
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.
|
|
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.
|
|
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/download-manager.js
CHANGED
|
@@ -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() {
|
package/src/widget-html.js
CHANGED
|
@@ -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
|
|
39
|
-
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/">`;
|
|
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);
|