@xiboplayer/cache 0.3.4 → 0.3.6

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.
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Widget HTML caching — preprocesses widget HTML and stores in Cache API
3
+ *
4
+ * Handles:
5
+ * - <base> tag injection for relative path resolution
6
+ * - CMS signed URL → local cache path rewriting
7
+ * - CSS font URL rewriting and font file caching
8
+ * - Interactive Control hostAddress rewriting
9
+ * - CSS object-position fix for CMS template alignment
10
+ *
11
+ * Runs on the main thread (needs window.location for URL construction).
12
+ * Uses Cache API directly — the SW also serves from the same cache.
13
+ */
14
+
15
+ const CACHE_NAME = 'xibo-media-v1';
16
+
17
+ // Dynamic base path for multi-variant deployment (pwa, pwa-xmds, pwa-xlr)
18
+ const BASE = (typeof window !== 'undefined')
19
+ ? window.location.pathname.replace(/\/[^/]*$/, '').replace(/\/$/, '') || '/player/pwa'
20
+ : '/player/pwa';
21
+
22
+ /**
23
+ * Store widget HTML in cache for iframe loading
24
+ * @param {string} layoutId - Layout ID
25
+ * @param {string} regionId - Region ID
26
+ * @param {string} mediaId - Media ID
27
+ * @param {string} html - Widget HTML content
28
+ * @returns {Promise<string>} Cache key URL
29
+ */
30
+ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
31
+ const cacheKey = `${BASE}/cache/widget/${layoutId}/${regionId}/${mediaId}`;
32
+ const cache = await caches.open(CACHE_NAME);
33
+
34
+ // 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/">';
37
+ let modifiedHtml = html;
38
+
39
+ // Insert base tag after <head> opening tag
40
+ if (html.includes('<head>')) {
41
+ modifiedHtml = html.replace('<head>', '<head>' + baseTag);
42
+ } else if (html.includes('<HEAD>')) {
43
+ modifiedHtml = html.replace('<HEAD>', '<HEAD>' + baseTag);
44
+ } else {
45
+ // No head tag, prepend base tag
46
+ modifiedHtml = baseTag + html;
47
+ }
48
+
49
+ // Rewrite absolute CMS signed URLs to local cache paths
50
+ // Matches: https://cms/xmds.php?file=... or https://cms/pwa/file?file=...
51
+ // These absolute URLs bypass the <base> tag entirely, causing slow CMS fetches
52
+ const cmsUrlRegex = /https?:\/\/[^"'\s)]+(?:xmds\.php|pwa\/file)\?[^"'\s)]*file=([^&"'\s)]+)[^"'\s)]*/g;
53
+ const staticResources = [];
54
+ modifiedHtml = modifiedHtml.replace(cmsUrlRegex, (match, filename) => {
55
+ const localPath = `${BASE}/cache/static/${filename}`;
56
+ staticResources.push({ filename, originalUrl: match });
57
+ console.log(`[Cache] Rewrote widget URL: ${filename} → ${localPath}`);
58
+ return localPath;
59
+ });
60
+
61
+ // Inject CSS default for object-position to suppress CMS template warning
62
+ // CMS global-elements.xml uses {{alignId}} {{valignId}} which produces
63
+ // invalid CSS (empty value) when alignment is not configured
64
+ const cssFixTag = '<style>img,video{object-position:center center}</style>';
65
+ if (modifiedHtml.includes('</head>')) {
66
+ modifiedHtml = modifiedHtml.replace('</head>', cssFixTag + '</head>');
67
+ } else if (modifiedHtml.includes('</HEAD>')) {
68
+ modifiedHtml = modifiedHtml.replace('</HEAD>', cssFixTag + '</HEAD>');
69
+ }
70
+
71
+ // Rewrite Interactive Control hostAddress to SW-interceptable path
72
+ // The IC library uses hostAddress + '/info', '/trigger', etc.
73
+ // Original: hostAddress: "https://cms.example.com" → XHR to /info goes to CMS (fails)
74
+ // Rewritten: hostAddress: "/player/pwa/ic" → XHR to /player/pwa/ic/info (intercepted by SW)
75
+ modifiedHtml = modifiedHtml.replace(
76
+ /hostAddress\s*:\s*["']https?:\/\/[^"']+["']/g,
77
+ `hostAddress: "${BASE}/ic"`
78
+ );
79
+
80
+ console.log(`[Cache] Injected base tag and rewrote CMS URLs in widget HTML`);
81
+
82
+ // Construct full URL for cache storage
83
+ const cacheUrl = new URL(cacheKey, window.location.origin);
84
+
85
+ const response = new Response(modifiedHtml, {
86
+ headers: {
87
+ 'Content-Type': 'text/html; charset=utf-8',
88
+ 'Access-Control-Allow-Origin': '*'
89
+ }
90
+ });
91
+
92
+ await cache.put(cacheUrl, response);
93
+ console.log(`[Cache] Stored widget HTML at ${cacheKey} (${modifiedHtml.length} bytes)`);
94
+
95
+ // Fetch and cache static resources (shared Cache API - accessible from main thread and SW)
96
+ if (staticResources.length > 0) {
97
+ const STATIC_CACHE_NAME = 'xibo-static-v1';
98
+ const staticCache = await caches.open(STATIC_CACHE_NAME);
99
+
100
+ await Promise.all(staticResources.map(async ({ filename, originalUrl }) => {
101
+ const staticKey = `${BASE}/cache/static/${filename}`;
102
+ const existing = await staticCache.match(staticKey);
103
+ if (existing) return; // Already cached
104
+
105
+ try {
106
+ const resp = await fetch(originalUrl);
107
+ if (!resp.ok) {
108
+ console.warn(`[Cache] Failed to fetch static resource: ${filename} (HTTP ${resp.status})`);
109
+ return;
110
+ }
111
+
112
+ const ext = filename.split('.').pop().toLowerCase();
113
+ const contentType = {
114
+ 'js': 'application/javascript',
115
+ 'css': 'text/css',
116
+ 'otf': 'font/otf', 'ttf': 'font/ttf',
117
+ 'woff': 'font/woff', 'woff2': 'font/woff2',
118
+ 'eot': 'application/vnd.ms-fontobject',
119
+ 'svg': 'image/svg+xml'
120
+ }[ext] || 'application/octet-stream';
121
+
122
+ // For CSS files, rewrite font URLs and cache referenced font files
123
+ if (ext === 'css') {
124
+ let cssText = await resp.text();
125
+ const fontResources = [];
126
+ const fontUrlRegex = /url\((['"]?)(https?:\/\/[^'")\s]+\?[^'")\s]*file=([^&'")\s]+\.(?:woff2?|ttf|otf|eot|svg))[^'")\s]*)\1\)/gi;
127
+ cssText = cssText.replace(fontUrlRegex, (_match, quote, fullUrl, fontFilename) => {
128
+ fontResources.push({ filename: fontFilename, originalUrl: fullUrl });
129
+ console.log(`[Cache] Rewrote font URL in CSS: ${fontFilename}`);
130
+ return `url(${quote}${BASE}/cache/static/${encodeURIComponent(fontFilename)}${quote})`;
131
+ });
132
+
133
+ await staticCache.put(staticKey, new Response(cssText, {
134
+ headers: { 'Content-Type': 'text/css' }
135
+ }));
136
+ console.log(`[Cache] Cached CSS with ${fontResources.length} rewritten font URLs: ${filename}`);
137
+
138
+ // Fetch and cache referenced font files
139
+ await Promise.all(fontResources.map(async ({ filename: fontFile, originalUrl: fontUrl }) => {
140
+ const fontKey = `${BASE}/cache/static/${encodeURIComponent(fontFile)}`;
141
+ const existingFont = await staticCache.match(fontKey);
142
+ if (existingFont) return; // Already cached (by SW or previous widget)
143
+
144
+ try {
145
+ const fontResp = await fetch(fontUrl);
146
+ if (!fontResp.ok) {
147
+ console.warn(`[Cache] Failed to fetch font: ${fontFile} (HTTP ${fontResp.status})`);
148
+ return;
149
+ }
150
+ const fontBlob = await fontResp.blob();
151
+ const fontExt = fontFile.split('.').pop().toLowerCase();
152
+ const fontContentType = {
153
+ 'otf': 'font/otf', 'ttf': 'font/ttf',
154
+ 'woff': 'font/woff', 'woff2': 'font/woff2',
155
+ 'eot': 'application/vnd.ms-fontobject',
156
+ 'svg': 'image/svg+xml'
157
+ }[fontExt] || 'application/octet-stream';
158
+
159
+ await staticCache.put(fontKey, new Response(fontBlob, {
160
+ headers: { 'Content-Type': fontContentType }
161
+ }));
162
+ console.log(`[Cache] Cached font: ${fontFile} (${fontContentType}, ${fontBlob.size} bytes)`);
163
+ } catch (fontErr) {
164
+ console.warn(`[Cache] Failed to cache font: ${fontFile}`, fontErr);
165
+ }
166
+ }));
167
+ } else {
168
+ const blob = await resp.blob();
169
+ await staticCache.put(staticKey, new Response(blob, {
170
+ headers: { 'Content-Type': contentType }
171
+ }));
172
+ console.log(`[Cache] Cached static resource: ${filename} (${contentType}, ${blob.size} bytes)`);
173
+ }
174
+ } catch (error) {
175
+ console.warn(`[Cache] Failed to cache static resource: ${filename}`, error);
176
+ }
177
+ }));
178
+ }
179
+
180
+ return cacheKey;
181
+ }