@xiboplayer/cache 0.3.5 → 0.3.7
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/package.json +2 -2
- package/src/cache-analyzer.js +30 -43
- package/src/cache-analyzer.test.js +8 -29
- package/src/cache-proxy.js +26 -0
- package/src/cache.js +8 -659
- package/src/cache.test.js +15 -616
- package/src/index.js +1 -0
- package/src/widget-html.js +181 -0
|
@@ -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
|
+
}
|