@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/cache",
3
- "version": "0.5.4",
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.4"
15
+ "@xiboplayer/utils": "0.5.6"
16
16
  },
17
17
  "devDependencies": {
18
18
  "vitest": "^2.0.0",
@@ -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:8765 = proxy mode.
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' || loc.port !== '8765') return url;
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
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
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 < MAX_RETRIES) {
167
- const delay = RETRY_DELAY_MS * attempt;
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}/${MAX_RETRIES} failed: ${msg}. Retrying in ${delay / 1000}s...`);
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
  });
@@ -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;