@xiboplayer/cache 0.5.16 → 0.5.18
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 +3 -2
- package/src/download-manager.js +15 -2
- package/src/index.d.ts +40 -0
- package/src/index.js +1 -1
- package/src/widget-html.js +45 -37
- package/src/widget-html.test.js +7 -4
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/cache",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.18",
|
|
4
4
|
"description": "Offline caching and download management with parallel chunk downloads",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": "./src/index.js",
|
|
9
10
|
"./cache": "./src/cache.js",
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
18
|
"spark-md5": "^3.0.2",
|
|
18
|
-
"@xiboplayer/utils": "0.5.
|
|
19
|
+
"@xiboplayer/utils": "0.5.18"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"vitest": "^2.0.0",
|
package/src/download-manager.js
CHANGED
|
@@ -45,6 +45,18 @@ const URGENT_CONCURRENCY = 2; // Slots when urgent chunk is active (bandwidth fo
|
|
|
45
45
|
const FETCH_TIMEOUT_MS = 600_000; // 10 minutes — 100MB chunk at ~2 Mbps
|
|
46
46
|
const HEAD_TIMEOUT_MS = 15_000; // 15 seconds for HEAD requests
|
|
47
47
|
|
|
48
|
+
// CMS origin for proxy filtering — set via setCmsOrigin() at init
|
|
49
|
+
let _cmsOrigin = null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set the CMS origin so toProxyUrl() only proxies CMS URLs.
|
|
53
|
+
* External URLs (CDNs, Google Fonts, geolocation APIs) pass through unchanged.
|
|
54
|
+
* @param {string} origin - e.g. 'https://cms.example.com'
|
|
55
|
+
*/
|
|
56
|
+
export function setCmsOrigin(origin) {
|
|
57
|
+
_cmsOrigin = origin;
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
/**
|
|
49
61
|
* Infer Content-Type from file path extension.
|
|
50
62
|
* Used when we skip HEAD (size already known from RequiredFiles).
|
|
@@ -114,8 +126,9 @@ export function toProxyUrl(url) {
|
|
|
114
126
|
const loc = typeof self !== 'undefined' ? self.location : undefined;
|
|
115
127
|
if (!loc || loc.hostname !== 'localhost') return url;
|
|
116
128
|
const parsed = new URL(url);
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
// Only proxy URLs belonging to the CMS server; external URLs pass through
|
|
130
|
+
if (_cmsOrigin && parsed.origin !== _cmsOrigin) return url;
|
|
131
|
+
return `/file-proxy?cms=${encodeURIComponent(parsed.origin)}&url=${encodeURIComponent(parsed.pathname + parsed.search)}`;
|
|
119
132
|
}
|
|
120
133
|
|
|
121
134
|
/**
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const VERSION: string;
|
|
2
|
+
|
|
3
|
+
export class StoreClient {
|
|
4
|
+
has(type: string, id: string | number): Promise<boolean>;
|
|
5
|
+
get(type: string, id: string | number): Promise<Blob | null>;
|
|
6
|
+
put(type: string, id: string | number, body: Blob | ArrayBuffer | string, contentType?: string): Promise<boolean>;
|
|
7
|
+
remove(files: Array<{ type: string; id: string | number }>): Promise<{ deleted: number; total: number }>;
|
|
8
|
+
list(): Promise<Array<{ id: string; type: string; size: number }>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class DownloadClient {
|
|
12
|
+
controller: ServiceWorker | null;
|
|
13
|
+
fetchReady: boolean;
|
|
14
|
+
init(): Promise<void>;
|
|
15
|
+
download(payload: object | any[]): Promise<void>;
|
|
16
|
+
prioritize(fileType: string, fileId: string): Promise<void>;
|
|
17
|
+
prioritizeLayout(mediaIds: string[]): Promise<void>;
|
|
18
|
+
getProgress(): Promise<Record<string, any>>;
|
|
19
|
+
cleanup(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class DownloadManager {
|
|
23
|
+
constructor(options?: { concurrency?: number; chunkSize?: number; maxChunksPerFile?: number });
|
|
24
|
+
enqueue(fileInfo: any): any;
|
|
25
|
+
prioritizeLayoutFiles(mediaIds: string[]): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class FileDownload {}
|
|
29
|
+
export class LayoutTaskBuilder {}
|
|
30
|
+
export class CacheManager {}
|
|
31
|
+
export class CacheAnalyzer {
|
|
32
|
+
constructor(store: StoreClient);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const cacheManager: CacheManager;
|
|
36
|
+
|
|
37
|
+
export function isUrlExpired(url: string): boolean;
|
|
38
|
+
export function toProxyUrl(url: string): string;
|
|
39
|
+
export function setCmsOrigin(origin: string): void;
|
|
40
|
+
export function cacheWidgetHtml(...args: any[]): any;
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,6 @@ export const VERSION = pkg.version;
|
|
|
4
4
|
export { CacheManager, cacheManager } from './cache.js';
|
|
5
5
|
export { StoreClient } from './store-client.js';
|
|
6
6
|
export { DownloadClient } from './download-client.js';
|
|
7
|
-
export { DownloadManager, FileDownload, LayoutTaskBuilder, isUrlExpired, toProxyUrl } from './download-manager.js';
|
|
7
|
+
export { DownloadManager, FileDownload, LayoutTaskBuilder, isUrlExpired, toProxyUrl, setCmsOrigin } from './download-manager.js';
|
|
8
8
|
export { CacheAnalyzer } from './cache-analyzer.js';
|
|
9
9
|
export { cacheWidgetHtml } from './widget-html.js';
|
package/src/widget-html.js
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* Handles:
|
|
5
5
|
* - <base> tag injection for relative path resolution
|
|
6
6
|
* - CMS signed URL → local store path rewriting
|
|
7
|
-
* - CSS font URL rewriting and font file caching
|
|
8
7
|
* - Interactive Control hostAddress rewriting
|
|
9
8
|
* - CSS object-position fix for CMS template alignment
|
|
10
9
|
*
|
|
10
|
+
* Note: CSS font URL rewriting is primarily handled by the proxy layer (proxy.js).
|
|
11
|
+
* As defense-in-depth, this module also rewrites font URLs inside CSS files
|
|
12
|
+
* before storing them, in case the proxy misses the CSS detection.
|
|
13
|
+
*
|
|
11
14
|
* Runs on the main thread (needs window.location for URL construction).
|
|
12
15
|
* Stores content via PUT /store/... — no Cache API needed.
|
|
13
16
|
*/
|
|
@@ -117,60 +120,65 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
117
120
|
'svg': 'image/svg+xml'
|
|
118
121
|
}[ext] || 'application/octet-stream';
|
|
119
122
|
|
|
120
|
-
//
|
|
123
|
+
// Defense-in-depth: rewrite font URLs inside CSS files before storing.
|
|
124
|
+
// The proxy normally handles this, but if it misses CSS detection
|
|
125
|
+
// (e.g. CMS returns unexpected Content-Type), we catch it here.
|
|
126
|
+
// Also handles truncated CSS from CMS (missing closing ');}) due to
|
|
127
|
+
// Content-Length mismatch after URL expansion).
|
|
121
128
|
if (ext === 'css') {
|
|
122
129
|
let cssText = await resp.text();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
// Match any CMS signed URL with a file= query param in the CSS text.
|
|
131
|
+
// Uses a simple approach: find all https:// URLs with file=<name>, then
|
|
132
|
+
// filter to fonts by extension or fileType=font. This handles both normal
|
|
133
|
+
// url('...') and truncated CSS (where the closing '); is missing).
|
|
134
|
+
const CMS_SIGNED_URL_RE = /https?:\/\/[^\s'")\]]+\?[^\s'")\]]*file=([^&\s'")\]]+)[^\s'")\]]*/g;
|
|
135
|
+
const FONT_EXTS = /\.(?:woff2?|ttf|otf|eot|svg)$/i;
|
|
136
|
+
const fontJobs = [];
|
|
137
|
+
|
|
138
|
+
cssText = cssText.replace(CMS_SIGNED_URL_RE, (fullUrl, fontFilename) => {
|
|
139
|
+
if (!FONT_EXTS.test(fontFilename) && !fullUrl.includes('fileType=font')) return fullUrl;
|
|
140
|
+
fontJobs.push({ filename: fontFilename, originalUrl: fullUrl });
|
|
141
|
+
log.info(`Rewrote CSS font URL: ${fontFilename}`);
|
|
142
|
+
return `${BASE}/cache/static/${encodeURIComponent(fontFilename)}`;
|
|
129
143
|
});
|
|
130
144
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
headers: { 'Content-Type': 'text/css' },
|
|
134
|
-
body: cssText,
|
|
135
|
-
});
|
|
136
|
-
cssResp.body?.cancel();
|
|
137
|
-
log.info(`Stored CSS with ${fontResources.length} rewritten font URLs: ${filename}`);
|
|
138
|
-
|
|
139
|
-
// Fetch and store referenced font files
|
|
140
|
-
await Promise.all(fontResources.map(async ({ filename: fontFile, originalUrl: fontUrl }) => {
|
|
141
|
-
// Check if already stored
|
|
145
|
+
// Fetch and store each referenced font file
|
|
146
|
+
await Promise.all(fontJobs.map(async (font) => {
|
|
142
147
|
try {
|
|
143
|
-
const headResp = await fetch(`/store/static/${
|
|
144
|
-
if (headResp.ok) return;
|
|
148
|
+
const headResp = await fetch(`/store/static/${font.filename}`, { method: 'HEAD' });
|
|
149
|
+
if (headResp.ok) return; // Already stored
|
|
145
150
|
} catch { /* proceed */ }
|
|
146
|
-
|
|
147
151
|
try {
|
|
148
|
-
const fontResp = await fetch(toProxyUrl(
|
|
149
|
-
if (!fontResp.ok) {
|
|
150
|
-
fontResp.body?.cancel();
|
|
151
|
-
log.warn(`Failed to fetch font: ${fontFile} (HTTP ${fontResp.status})`);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
152
|
+
const fontResp = await fetch(toProxyUrl(font.originalUrl));
|
|
153
|
+
if (!fontResp.ok) { fontResp.body?.cancel(); return; }
|
|
154
154
|
const fontBlob = await fontResp.blob();
|
|
155
|
-
const fontExt =
|
|
155
|
+
const fontExt = font.filename.split('.').pop().toLowerCase();
|
|
156
156
|
const fontContentType = {
|
|
157
157
|
'otf': 'font/otf', 'ttf': 'font/ttf',
|
|
158
158
|
'woff': 'font/woff', 'woff2': 'font/woff2',
|
|
159
|
-
'eot': 'application/vnd.ms-fontobject',
|
|
160
|
-
'svg': 'image/svg+xml'
|
|
159
|
+
'eot': 'application/vnd.ms-fontobject', 'svg': 'image/svg+xml',
|
|
161
160
|
}[fontExt] || 'application/octet-stream';
|
|
162
|
-
|
|
163
|
-
const fontPutResp = await fetch(`/store/static/${encodeURIComponent(fontFile)}`, {
|
|
161
|
+
const putFont = await fetch(`/store/static/${font.filename}`, {
|
|
164
162
|
method: 'PUT',
|
|
165
163
|
headers: { 'Content-Type': fontContentType },
|
|
166
164
|
body: fontBlob,
|
|
167
165
|
});
|
|
168
|
-
|
|
169
|
-
log.info(`Stored font: ${
|
|
170
|
-
} catch (
|
|
171
|
-
log.warn(`Failed to store font: ${
|
|
166
|
+
putFont.body?.cancel();
|
|
167
|
+
log.info(`Stored font: ${font.filename} (${fontBlob.size} bytes)`);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
log.warn(`Failed to store font: ${font.filename}`, e);
|
|
172
170
|
}
|
|
173
171
|
}));
|
|
172
|
+
|
|
173
|
+
// Store the rewritten CSS
|
|
174
|
+
const cssBlob = new Blob([cssText], { type: 'text/css' });
|
|
175
|
+
const staticResp = await fetch(`/store/static/${filename}`, {
|
|
176
|
+
method: 'PUT',
|
|
177
|
+
headers: { 'Content-Type': 'text/css' },
|
|
178
|
+
body: cssBlob,
|
|
179
|
+
});
|
|
180
|
+
staticResp.body?.cancel();
|
|
181
|
+
log.info(`Stored CSS with ${fontJobs.length} rewritten font URLs: ${filename} (${cssText.length} bytes)`);
|
|
174
182
|
} else {
|
|
175
183
|
const blob = await resp.blob();
|
|
176
184
|
const staticResp = await fetch(`/store/static/${filename}`, {
|
package/src/widget-html.test.js
CHANGED
|
@@ -196,12 +196,15 @@ describe('cacheWidgetHtml', () => {
|
|
|
196
196
|
expect(widgetKey).toBeTruthy();
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
-
it('
|
|
199
|
+
it('stores fonts.css as a blob (font rewriting handled by proxy)', async () => {
|
|
200
200
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
201
201
|
|
|
202
|
-
// fonts.css
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
// fonts.css should be fetched and stored, but font files inside it
|
|
203
|
+
// are NOT parsed/fetched here — the proxy rewrites CSS font URLs
|
|
204
|
+
const cssFetched = fetchedUrls.some(u => u.includes('fonts.css'));
|
|
205
|
+
expect(cssFetched).toBe(true);
|
|
206
|
+
const cssStored = [...storeContents.keys()].some(k => k.includes('fonts.css'));
|
|
207
|
+
expect(cssStored).toBe(true);
|
|
205
208
|
});
|
|
206
209
|
});
|
|
207
210
|
|