@xiboplayer/cache 0.5.4 → 0.5.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.
- package/package.json +2 -2
- package/src/download-manager.js +34 -6
- package/src/widget-html.js +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/cache",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
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.5.
|
|
15
|
+
"@xiboplayer/utils": "0.5.6"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"vitest": "^2.0.0",
|
package/src/download-manager.js
CHANGED
|
@@ -34,6 +34,12 @@ const DEFAULT_MAX_CHUNKS_PER_FILE = 3; // Max parallel chunk downloads per file
|
|
|
34
34
|
const CHUNK_THRESHOLD = 100 * 1024 * 1024; // Files > 100MB get chunked
|
|
35
35
|
const MAX_RETRIES = 3;
|
|
36
36
|
const RETRY_DELAY_MS = 500; // Fast: 500ms, 1s, 1.5s → total ~3s
|
|
37
|
+
|
|
38
|
+
// getData (widget data) retry config — CMS "cache not ready" (HTTP 500) resolves
|
|
39
|
+
// when the XTR task runs (30-120s). Use longer backoff to ride it out.
|
|
40
|
+
const GETDATA_MAX_RETRIES = 4;
|
|
41
|
+
const GETDATA_RETRY_DELAYS = [15_000, 30_000, 60_000, 120_000]; // 15s, 30s, 60s, 120s
|
|
42
|
+
const GETDATA_REENQUEUE_DELAY_MS = 60_000; // Re-add to queue after 60s if all retries fail
|
|
37
43
|
const URGENT_CONCURRENCY = 2; // Slots when urgent chunk is active (bandwidth focus)
|
|
38
44
|
const FETCH_TIMEOUT_MS = 600_000; // 10 minutes — 100MB chunk at ~2 Mbps
|
|
39
45
|
const HEAD_TIMEOUT_MS = 15_000; // 15 seconds for HEAD requests
|
|
@@ -100,12 +106,12 @@ export function isUrlExpired(url, graceSeconds = 30) {
|
|
|
100
106
|
/**
|
|
101
107
|
* Rewrite an absolute CMS URL through the local proxy when running behind
|
|
102
108
|
* the proxy server (Chromium kiosk or Electron).
|
|
103
|
-
* Detection: SW/window on localhost
|
|
109
|
+
* Detection: SW/window on localhost (any port) = proxy mode.
|
|
104
110
|
*/
|
|
105
111
|
export function rewriteUrlForProxy(url) {
|
|
106
112
|
if (!url.startsWith('http')) return url;
|
|
107
113
|
const loc = typeof self !== 'undefined' ? self.location : undefined;
|
|
108
|
-
if (!loc || loc.hostname !== 'localhost'
|
|
114
|
+
if (!loc || loc.hostname !== 'localhost') return url;
|
|
109
115
|
const parsed = new URL(url);
|
|
110
116
|
const cmsOrigin = parsed.origin;
|
|
111
117
|
return `/file-proxy?cms=${encodeURIComponent(cmsOrigin)}&url=${encodeURIComponent(parsed.pathname + parsed.search)}`;
|
|
@@ -127,6 +133,8 @@ export class DownloadTask {
|
|
|
127
133
|
this.blob = null;
|
|
128
134
|
this._parentFile = null;
|
|
129
135
|
this._priority = PRIORITY.normal;
|
|
136
|
+
// Widget data (getData) uses longer retry backoff — CMS "cache not ready" is transient
|
|
137
|
+
this.isGetData = fileInfo.isGetData || false;
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
getUrl() {
|
|
@@ -144,7 +152,9 @@ export class DownloadTask {
|
|
|
144
152
|
headers['Range'] = `bytes=${this.rangeStart}-${this.rangeEnd}`;
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
|
|
155
|
+
const maxRetries = this.isGetData ? GETDATA_MAX_RETRIES : MAX_RETRIES;
|
|
156
|
+
|
|
157
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
148
158
|
const ac = new AbortController();
|
|
149
159
|
const timer = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
|
|
150
160
|
try {
|
|
@@ -163,10 +173,12 @@ export class DownloadTask {
|
|
|
163
173
|
|
|
164
174
|
} catch (error) {
|
|
165
175
|
const msg = ac.signal.aborted ? `Timeout after ${FETCH_TIMEOUT_MS / 1000}s` : error.message;
|
|
166
|
-
if (attempt <
|
|
167
|
-
const delay =
|
|
176
|
+
if (attempt < maxRetries) {
|
|
177
|
+
const delay = this.isGetData
|
|
178
|
+
? GETDATA_RETRY_DELAYS[attempt - 1]
|
|
179
|
+
: RETRY_DELAY_MS * attempt;
|
|
168
180
|
const chunkLabel = this.chunkIndex != null ? ` chunk ${this.chunkIndex}` : '';
|
|
169
|
-
log.warn(`[DownloadTask] ${this.fileInfo.type}/${this.fileInfo.id}${chunkLabel} attempt ${attempt}/${
|
|
181
|
+
log.warn(`[DownloadTask] ${this.fileInfo.type}/${this.fileInfo.id}${chunkLabel} attempt ${attempt}/${maxRetries} failed: ${msg}. Retrying in ${delay / 1000}s...`);
|
|
170
182
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
171
183
|
} else {
|
|
172
184
|
this.state = 'failed';
|
|
@@ -859,6 +871,22 @@ export class DownloadQueue {
|
|
|
859
871
|
this.running--;
|
|
860
872
|
task._parentFile._runningCount--;
|
|
861
873
|
this._activeTasks = this._activeTasks.filter(t => t !== task);
|
|
874
|
+
|
|
875
|
+
// getData (widget data): defer re-enqueue instead of permanent failure.
|
|
876
|
+
// CMS "cache not ready" resolves when the XTR task runs (30-120s).
|
|
877
|
+
if (task.isGetData) {
|
|
878
|
+
log.warn(`[DownloadQueue] getData ${key} failed all retries, scheduling re-enqueue in ${GETDATA_REENQUEUE_DELAY_MS / 1000}s`);
|
|
879
|
+
setTimeout(() => {
|
|
880
|
+
task.state = 'pending';
|
|
881
|
+
task._parentFile.state = 'downloading';
|
|
882
|
+
this.queue.push(task);
|
|
883
|
+
log.info(`[DownloadQueue] getData ${key} re-enqueued for retry`);
|
|
884
|
+
this.processQueue();
|
|
885
|
+
}, GETDATA_REENQUEUE_DELAY_MS);
|
|
886
|
+
this.processQueue();
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
862
890
|
this.processQueue();
|
|
863
891
|
task._parentFile.onTaskFailed(task, err);
|
|
864
892
|
});
|
package/src/widget-html.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { createLogger } from '@xiboplayer/utils';
|
|
16
|
+
import { rewriteUrlForProxy } from './download-manager.js';
|
|
16
17
|
|
|
17
18
|
const log = createLogger('Cache');
|
|
18
19
|
const CACHE_NAME = 'xibo-media-v1';
|
|
@@ -110,7 +111,7 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
110
111
|
if (existing) return; // Already cached
|
|
111
112
|
|
|
112
113
|
try {
|
|
113
|
-
const resp = await fetch(originalUrl);
|
|
114
|
+
const resp = await fetch(rewriteUrlForProxy(originalUrl));
|
|
114
115
|
if (!resp.ok) {
|
|
115
116
|
log.warn(`Failed to fetch static resource: ${filename} (HTTP ${resp.status})`);
|
|
116
117
|
return;
|
|
@@ -149,7 +150,7 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
149
150
|
if (existingFont) return; // Already cached (by SW or previous widget)
|
|
150
151
|
|
|
151
152
|
try {
|
|
152
|
-
const fontResp = await fetch(fontUrl);
|
|
153
|
+
const fontResp = await fetch(rewriteUrlForProxy(fontUrl));
|
|
153
154
|
if (!fontResp.ok) {
|
|
154
155
|
log.warn(`Failed to fetch font: ${fontFile} (HTTP ${fontResp.status})`);
|
|
155
156
|
return;
|