@xiboplayer/cache 0.5.19 → 0.6.0

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.
@@ -130,13 +130,17 @@ The proxy server (`packages/proxy/src/proxy.js`) exposes these endpoints backed
130
130
 
131
131
  | Method | Route | Purpose |
132
132
  |--------|-------|---------|
133
- | `GET` | `/store/:type/:id` | Serve file (Range support) |
134
- | `HEAD` | `/store/:type/:id` | Existence + size check |
135
- | `PUT` | `/store/:type/:id` | Store file |
133
+ | `HEAD` | `/store/:type/*` | Existence + size check (returns 404 for incomplete chunked files) |
134
+ | `GET` | `/store/:type/*` | Serve file (Range support) |
135
+ | `PUT` | `/store/:type/*` | Store file |
136
136
  | `POST` | `/store/delete` | Delete files |
137
137
  | `POST` | `/store/mark-complete` | Mark chunked download complete |
138
+ | `POST` | `/store/unmark-complete` | Unmark chunked file (keeps chunks, allows partial re-download) |
139
+ | `GET` | `/store/missing-chunks/:type/*` | Return missing chunk indices for a chunked file |
138
140
  | `GET` | `/store/list` | List all cached files |
139
141
 
142
+ **Note:** HEAD must be registered before GET in Express 5 because `app.get()` also matches HEAD requests.
143
+
140
144
  ### ContentStore (Filesystem)
141
145
 
142
146
  **Location**: `packages/proxy/src/content-store.js`
@@ -208,6 +212,41 @@ Widget HTML is processed on the main thread by `cacheWidgetHtml()`:
208
212
 
209
213
  Static resources are stored before widget HTML to prevent race conditions when the iframe loads.
210
214
 
215
+ ## Chunked Download Resume
216
+
217
+ Large media files are downloaded in chunks (configurable size, default ~100MB per chunk). If a download is interrupted (crash, network failure, process restart), the player resumes by only downloading missing chunks:
218
+
219
+ ### How it works
220
+
221
+ 1. **On startup/collection**: `enqueueFile()` calls `GET /store/missing-chunks/{storeKey}` before enqueueing
222
+ 2. If chunks exist on disk, it populates `file.skipChunks` with the indices of cached chunks
223
+ 3. The download pipeline skips those chunks, downloading only the missing ones
224
+ 4. After all chunks arrive, the file is marked complete
225
+
226
+ ### On video playback error
227
+
228
+ 1. The renderer emits a `videoError` event when a `<video>` element fails
229
+ 2. The PWA calls `GET /store/missing-chunks/{storeKey}` to check for missing chunks
230
+ 3. If chunks are missing, it calls `POST /store/unmark-complete` (keeps all existing chunks on disk)
231
+ 4. Triggers `collectNow()` — the normal enqueue path populates `skipChunks` and re-downloads only the missing chunks
232
+
233
+ ### Incomplete file detection
234
+
235
+ - `HEAD /store/:type/*` returns **404** for chunked files with missing chunks (so the download pipeline picks them up)
236
+ - `cacheThrough()` falls through to the CMS for incomplete chunked files instead of serving broken data from the store
237
+
238
+ ### Example
239
+
240
+ A 2GB video (21 chunks) where chunks 0 and 9 are cached:
241
+ ```
242
+ GET /store/missing-chunks/api/v2/player/media/15
243
+ → { "missing": [1,2,3,4,5,6,7,8,10,11,12,13,14,15,16,17,18,19,20], "numChunks": 21 }
244
+
245
+ # enqueueFile sets skipChunks = {0, 9}
246
+ # Download pipeline fetches only 19 chunks instead of 21
247
+ # Saves ~200MB of bandwidth
248
+ ```
249
+
211
250
  ## Error Handling
212
251
 
213
252
  | Scenario | StoreClient | DownloadClient |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/cache",
3
- "version": "0.5.19",
3
+ "version": "0.6.0",
4
4
  "description": "Offline caching and download management with parallel chunk downloads",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "spark-md5": "^3.0.2",
19
- "@xiboplayer/utils": "0.5.19"
19
+ "@xiboplayer/utils": "0.6.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "vitest": "^2.0.0",
@@ -1,13 +1,11 @@
1
1
  /**
2
- * StoreClient & DownloadClient Tests
2
+ * StoreClient Tests
3
3
  *
4
4
  * StoreClient: pure REST client for ContentStore — no SW dependency
5
- * DownloadClient: SW postMessage client for download orchestration
6
5
  */
7
6
 
8
7
  import { describe, it, expect, beforeEach, vi } from 'vitest';
9
8
  import { StoreClient } from './store-client.js';
10
- import { DownloadClient } from './download-client.js';
11
9
  import { createTestBlob } from './test-utils.js';
12
10
 
13
11
  /**
@@ -15,7 +13,6 @@ import { createTestBlob } from './test-utils.js';
15
13
  */
16
14
  function resetMocks() {
17
15
  global.fetch = vi.fn();
18
- delete global.MessageChannel;
19
16
  }
20
17
 
21
18
  // ===========================================================================
@@ -189,205 +186,3 @@ describe('StoreClient', () => {
189
186
  });
190
187
  });
191
188
 
192
- // ===========================================================================
193
- // DownloadClient Tests
194
- // ===========================================================================
195
-
196
- /**
197
- * Helper: set up navigator.serviceWorker mock.
198
- */
199
- function setupServiceWorker(opts = {}) {
200
- const {
201
- supported = true,
202
- controller = null,
203
- active = undefined,
204
- installing = null,
205
- waiting = null,
206
- swReadyResolves = true,
207
- } = opts;
208
-
209
- if (!supported) {
210
- Object.defineProperty(navigator, 'serviceWorker', {
211
- value: undefined,
212
- configurable: true,
213
- writable: true,
214
- });
215
- delete navigator.serviceWorker;
216
- return;
217
- }
218
-
219
- const activeSW = active !== undefined
220
- ? active
221
- : controller
222
- ? { state: 'activated', postMessage: controller.postMessage }
223
- : null;
224
-
225
- const registration = {
226
- active: activeSW,
227
- installing,
228
- waiting,
229
- };
230
-
231
- const messageListeners = [];
232
-
233
- const swContainer = {
234
- controller,
235
- ready: swReadyResolves
236
- ? Promise.resolve(registration)
237
- : new Promise(() => {}),
238
- getRegistration: vi.fn().mockResolvedValue(registration),
239
- addEventListener: vi.fn((event, handler) => {
240
- if (event === 'message') {
241
- messageListeners.push(handler);
242
- }
243
- }),
244
- removeEventListener: vi.fn(),
245
- };
246
-
247
- swContainer._messageListeners = messageListeners;
248
- swContainer._registration = registration;
249
-
250
- Object.defineProperty(navigator, 'serviceWorker', {
251
- value: swContainer,
252
- configurable: true,
253
- writable: true,
254
- });
255
-
256
- return swContainer;
257
- }
258
-
259
- function dispatchSWMessage(swContainer, data) {
260
- for (const listener of swContainer._messageListeners || []) {
261
- listener({ data });
262
- }
263
- }
264
-
265
- function setupMessageChannel() {
266
- const channels = [];
267
-
268
- global.MessageChannel = class {
269
- constructor() {
270
- const self = { port1: { onmessage: null }, port2: {} };
271
- channels.push(self);
272
- this.port1 = self.port1;
273
- this.port2 = self.port2;
274
- }
275
- };
276
-
277
- return {
278
- get lastChannel() {
279
- return channels[channels.length - 1];
280
- },
281
- respondOnLastChannel(data) {
282
- const ch = channels[channels.length - 1];
283
- if (ch && ch.port1.onmessage) {
284
- ch.port1.onmessage({ data });
285
- }
286
- },
287
- channels,
288
- };
289
- }
290
-
291
- async function createInitialisedDownloadClient() {
292
- const controller = { postMessage: vi.fn() };
293
- const sw = setupServiceWorker({ controller });
294
-
295
- const client = new DownloadClient();
296
- const initPromise = client.init();
297
-
298
- await Promise.resolve();
299
- dispatchSWMessage(sw, { type: 'SW_READY' });
300
-
301
- await initPromise;
302
- return { client, sw, controller };
303
- }
304
-
305
- describe('DownloadClient', () => {
306
- beforeEach(() => {
307
- resetMocks();
308
- Object.defineProperty(navigator, 'serviceWorker', {
309
- value: undefined,
310
- configurable: true,
311
- writable: true,
312
- });
313
- });
314
-
315
- describe('init()', () => {
316
- it('should initialize with SW controller', async () => {
317
- setupMessageChannel();
318
- const { client } = await createInitialisedDownloadClient();
319
-
320
- expect(client.controller).toBeTruthy();
321
- });
322
-
323
- it('should throw if SW not supported', async () => {
324
- setupServiceWorker({ supported: false });
325
-
326
- const client = new DownloadClient();
327
-
328
- await expect(client.init()).rejects.toThrow('Service Worker not supported');
329
- });
330
- });
331
-
332
- describe('download()', () => {
333
- it('should post DOWNLOAD_FILES message to SW', async () => {
334
- setupMessageChannel();
335
- const { client } = await createInitialisedDownloadClient();
336
-
337
- client.controller.postMessage = vi.fn();
338
-
339
- const files = [
340
- { id: '1', type: 'media', path: 'http://test.com/file1.mp4' },
341
- { id: '2', type: 'media', path: 'http://test.com/file2.mp4' },
342
- ];
343
-
344
- const mc = setupMessageChannel();
345
- const downloadPromise = client.download(files);
346
-
347
- // Simulate SW acknowledging the download
348
- mc.respondOnLastChannel({
349
- success: true,
350
- enqueuedCount: 2,
351
- activeCount: 2,
352
- queuedCount: 0,
353
- });
354
-
355
- await expect(downloadPromise).resolves.toBeUndefined();
356
- });
357
-
358
- it('should reject when SW returns error', async () => {
359
- setupMessageChannel();
360
- const { client } = await createInitialisedDownloadClient();
361
-
362
- client.controller.postMessage = vi.fn();
363
-
364
- const mc = setupMessageChannel();
365
- const downloadPromise = client.download([]);
366
-
367
- mc.respondOnLastChannel({ success: false, error: 'Download failed' });
368
-
369
- await expect(downloadPromise).rejects.toThrow('Download failed');
370
- });
371
-
372
- it('should throw if SW controller not available', async () => {
373
- setupMessageChannel();
374
- const { client } = await createInitialisedDownloadClient();
375
-
376
- client.controller = null;
377
-
378
- await expect(client.download([])).rejects.toThrow('Service Worker not available');
379
- });
380
- });
381
-
382
- describe('getProgress()', () => {
383
- it('should return empty object when controller is null', async () => {
384
- setupMessageChannel();
385
- const { client } = await createInitialisedDownloadClient();
386
- client.controller = null;
387
-
388
- const progress = await client.getProgress();
389
-
390
- expect(progress).toEqual({});
391
- });
392
- });
393
- });
@@ -26,37 +26,17 @@
26
26
  */
27
27
 
28
28
  import { createLogger } from '@xiboplayer/utils';
29
+ import { getFileTypeConfig } from './file-types.js';
29
30
 
30
31
  const log = createLogger('Download');
31
32
  const DEFAULT_CONCURRENCY = 6; // Max concurrent HTTP connections (matches Chromium per-host limit)
32
33
  const DEFAULT_CHUNK_SIZE = 50 * 1024 * 1024; // 50MB chunks
33
34
  const DEFAULT_MAX_CHUNKS_PER_FILE = 3; // Max parallel chunk downloads per file
34
35
  const CHUNK_THRESHOLD = 100 * 1024 * 1024; // Files > 100MB get chunked
35
- const MAX_RETRIES = 3;
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
43
- const GETDATA_MAX_REENQUEUES = 5; // Max times a getData can be re-enqueued before permanent failure
44
36
  const URGENT_CONCURRENCY = 2; // Slots when urgent chunk is active (bandwidth focus)
45
37
  const FETCH_TIMEOUT_MS = 600_000; // 10 minutes — 100MB chunk at ~2 Mbps
46
38
  const HEAD_TIMEOUT_MS = 15_000; // 15 seconds for HEAD requests
47
39
 
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
-
60
40
  /**
61
41
  * Infer Content-Type from file path extension.
62
42
  * Used when we skip HEAD (size already known from RequiredFiles).
@@ -116,20 +96,6 @@ export function isUrlExpired(url, graceSeconds = 30) {
116
96
  return (Date.now() / 1000) >= (expiry - graceSeconds);
117
97
  }
118
98
 
119
- /**
120
- * Rewrite an absolute CMS URL through the local proxy when running behind
121
- * the proxy server (Chromium kiosk or Electron).
122
- * Detection: SW/window on localhost (any port) = proxy mode.
123
- */
124
- export function toProxyUrl(url) {
125
- if (!url.startsWith('http')) return url;
126
- const loc = typeof self !== 'undefined' ? self.location : undefined;
127
- if (!loc || loc.hostname !== 'localhost') return url;
128
- const parsed = new URL(url);
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)}`;
132
- }
133
99
 
134
100
  /**
135
101
  * DownloadTask - Single HTTP fetch unit
@@ -147,8 +113,7 @@ export class DownloadTask {
147
113
  this.blob = null;
148
114
  this._parentFile = null;
149
115
  this._priority = PRIORITY.normal;
150
- // Widget data (getData) uses longer retry backoff — CMS "cache not ready" is transient
151
- this.isGetData = fileInfo.isGetData || false;
116
+ this._typeConfig = getFileTypeConfig(fileInfo.type);
152
117
  }
153
118
 
154
119
  getUrl() {
@@ -156,24 +121,7 @@ export class DownloadTask {
156
121
  if (isUrlExpired(url)) {
157
122
  throw new Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);
158
123
  }
159
- let proxyUrl = toProxyUrl(url);
160
-
161
- // Append store key params so the proxy can save to ContentStore
162
- if (proxyUrl.startsWith('/file-proxy')) {
163
- const storeKey = `${this.fileInfo.type || 'media'}/${this.fileInfo.id}`;
164
- proxyUrl += `&storeKey=${encodeURIComponent(storeKey)}`;
165
- if (this.chunkIndex != null) {
166
- proxyUrl += `&chunkIndex=${this.chunkIndex}`;
167
- if (this._parentFile) {
168
- proxyUrl += `&numChunks=${this._parentFile.totalChunks}`;
169
- proxyUrl += `&chunkSize=${this._parentFile.options.chunkSize || 104857600}`;
170
- }
171
- }
172
- if (this.fileInfo.md5) {
173
- proxyUrl += `&md5=${encodeURIComponent(this.fileInfo.md5)}`;
174
- }
175
- }
176
- return proxyUrl;
124
+ return url;
177
125
  }
178
126
 
179
127
  async start() {
@@ -182,8 +130,22 @@ export class DownloadTask {
182
130
  if (this.rangeStart != null) {
183
131
  headers['Range'] = `bytes=${this.rangeStart}-${this.rangeEnd}`;
184
132
  }
133
+ // Pass chunk metadata and MD5 via custom headers for cache-through proxy
134
+ if (this.chunkIndex != null) {
135
+ headers['X-Store-Chunk-Index'] = String(this.chunkIndex);
136
+ if (this._parentFile) {
137
+ headers['X-Store-Num-Chunks'] = String(this._parentFile.totalChunks);
138
+ headers['X-Store-Chunk-Size'] = String(this._parentFile.options.chunkSize || 104857600);
139
+ }
140
+ }
141
+ if (this.fileInfo.md5) {
142
+ headers['X-Store-MD5'] = this.fileInfo.md5;
143
+ }
144
+ if (this.fileInfo.updateInterval) {
145
+ headers['X-Cache-TTL'] = String(this.fileInfo.updateInterval);
146
+ }
185
147
 
186
- const maxRetries = this.isGetData ? GETDATA_MAX_RETRIES : MAX_RETRIES;
148
+ const maxRetries = this._typeConfig.maxRetries;
187
149
 
188
150
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
189
151
  const ac = new AbortController();
@@ -205,9 +167,8 @@ export class DownloadTask {
205
167
  } catch (error) {
206
168
  const msg = ac.signal.aborted ? `Timeout after ${FETCH_TIMEOUT_MS / 1000}s` : error.message;
207
169
  if (attempt < maxRetries) {
208
- const delay = this.isGetData
209
- ? GETDATA_RETRY_DELAYS[attempt - 1]
210
- : RETRY_DELAY_MS * attempt;
170
+ const delay = this._typeConfig.retryDelays?.[attempt - 1]
171
+ ?? this._typeConfig.retryDelayMs * attempt;
211
172
  const chunkLabel = this.chunkIndex != null ? ` chunk ${this.chunkIndex}` : '';
212
173
  log.warn(`[DownloadTask] ${this.fileInfo.type}/${this.fileInfo.id}${chunkLabel} attempt ${attempt}/${maxRetries} failed: ${msg}. Retrying in ${delay / 1000}s...`);
213
174
  await new Promise(resolve => setTimeout(resolve, delay));
@@ -264,17 +225,7 @@ export class FileDownload {
264
225
  if (isUrlExpired(url)) {
265
226
  throw new Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);
266
227
  }
267
- let proxyUrl = toProxyUrl(url);
268
-
269
- // Append store key for ContentStore (same as DownloadTask)
270
- if (proxyUrl.startsWith('/file-proxy')) {
271
- const storeKey = `${this.fileInfo.type || 'media'}/${this.fileInfo.id}`;
272
- proxyUrl += `&storeKey=${encodeURIComponent(storeKey)}`;
273
- if (this.fileInfo.md5) {
274
- proxyUrl += `&md5=${encodeURIComponent(this.fileInfo.md5)}`;
275
- }
276
- }
277
- return proxyUrl;
228
+ return url;
278
229
  }
279
230
 
280
231
  wait() {
@@ -296,7 +247,12 @@ export class FileDownload {
296
247
  this.totalBytes = (size && size > 0) ? parseInt(size) : 0;
297
248
  this._contentType = inferContentType(this.fileInfo);
298
249
 
299
- if (this.totalBytes === 0) {
250
+ // Skip HEAD for types that declare skipHead (e.g. datasets — dynamic API endpoints).
251
+ // These generate responses server-side; HEAD triggers the full handler for nothing
252
+ // and may fail if the CMS cache isn't warm yet. They're always small, never chunked.
253
+ const skipHead = getFileTypeConfig(this.fileInfo.type).skipHead;
254
+
255
+ if (this.totalBytes === 0 && !skipHead) {
300
256
  // No size declared — HEAD fallback (rare: only for files without CMS size)
301
257
  const url = this.getUrl();
302
258
  const ac = new AbortController();
@@ -916,25 +872,26 @@ export class DownloadQueue {
916
872
  task._parentFile._runningCount--;
917
873
  this._activeTasks = this._activeTasks.filter(t => t !== task);
918
874
 
919
- // getData (widget data): defer re-enqueue instead of permanent failure.
875
+ // Re-enqueueable types (e.g. datasets): defer re-enqueue instead of permanent failure.
920
876
  // CMS "cache not ready" resolves when the XTR task runs (30-120s).
921
- if (task.isGetData) {
877
+ const { maxReenqueues, reenqueueDelayMs } = task._typeConfig;
878
+ if (maxReenqueues > 0) {
922
879
  task._reenqueueCount = (task._reenqueueCount || 0) + 1;
923
- if (task._reenqueueCount > GETDATA_MAX_REENQUEUES) {
924
- log.error(`[DownloadQueue] getData ${key} exceeded ${GETDATA_MAX_REENQUEUES} re-enqueues, failing permanently`);
880
+ if (task._reenqueueCount > maxReenqueues) {
881
+ log.error(`[DownloadQueue] ${key} exceeded ${maxReenqueues} re-enqueues, failing permanently`);
925
882
  this.processQueue();
926
883
  task._parentFile.onTaskFailed(task, err);
927
884
  return;
928
885
  }
929
- log.warn(`[DownloadQueue] getData ${key} failed all retries (attempt ${task._reenqueueCount}/${GETDATA_MAX_REENQUEUES}), scheduling re-enqueue in ${GETDATA_REENQUEUE_DELAY_MS / 1000}s`);
886
+ log.warn(`[DownloadQueue] ${key} failed all retries (attempt ${task._reenqueueCount}/${maxReenqueues}), scheduling re-enqueue in ${reenqueueDelayMs / 1000}s`);
930
887
  const timerId = setTimeout(() => {
931
888
  this._reenqueueTimers.delete(timerId);
932
889
  task.state = 'pending';
933
890
  task._parentFile.state = 'downloading';
934
891
  this.queue.push(task);
935
- log.info(`[DownloadQueue] getData ${key} re-enqueued for retry`);
892
+ log.info(`[DownloadQueue] ${key} re-enqueued for retry`);
936
893
  this.processQueue();
937
- }, GETDATA_REENQUEUE_DELAY_MS);
894
+ }, reenqueueDelayMs);
938
895
  this._reenqueueTimers.add(timerId);
939
896
  this.processQueue();
940
897
  return;
@@ -978,6 +935,8 @@ export class DownloadQueue {
978
935
  getProgress() {
979
936
  const progress = {};
980
937
  for (const [key, file] of this.active.entries()) {
938
+ // Skip completed/failed — they stay in active until removeCompleted() runs
939
+ if (file.state === 'complete' || file.state === 'failed') continue;
981
940
  progress[key] = {
982
941
  downloaded: file.downloadedBytes,
983
942
  total: file.totalBytes,
@@ -0,0 +1,26 @@
1
+ /**
2
+ * FILE_TYPES — centralized download behavior per file type.
3
+ *
4
+ * Each type declares retry strategy, HEAD skip, and cache TTL.
5
+ * Used by DownloadTask/FileDownload instead of ad-hoc isGetData checks.
6
+ */
7
+
8
+ export const FILE_TYPES = {
9
+ media: { maxRetries: 3, retryDelayMs: 500, retryDelays: null,
10
+ maxReenqueues: 0, reenqueueDelayMs: 0,
11
+ skipHead: false, cacheTtl: Infinity },
12
+ layout: { maxRetries: 3, retryDelayMs: 500, retryDelays: null,
13
+ maxReenqueues: 0, reenqueueDelayMs: 0,
14
+ skipHead: false, cacheTtl: Infinity },
15
+ dataset: { maxRetries: 4, retryDelayMs: 0,
16
+ retryDelays: [15_000, 30_000, 60_000, 120_000],
17
+ maxReenqueues: 5, reenqueueDelayMs: 60_000,
18
+ skipHead: true, cacheTtl: 300 },
19
+ static: { maxRetries: 3, retryDelayMs: 500, retryDelays: null,
20
+ maxReenqueues: 0, reenqueueDelayMs: 0,
21
+ skipHead: false, cacheTtl: Infinity },
22
+ };
23
+
24
+ export function getFileTypeConfig(type) {
25
+ return FILE_TYPES[type] || FILE_TYPES.media;
26
+ }
package/src/index.d.ts CHANGED
@@ -8,25 +8,26 @@ export class StoreClient {
8
8
  list(): Promise<Array<{ id: string; type: string; size: number }>>;
9
9
  }
10
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
11
  export class DownloadManager {
23
- constructor(options?: { concurrency?: number; chunkSize?: number; maxChunksPerFile?: number });
12
+ constructor(options?: { concurrency?: number; chunkSize?: number; chunksPerFile?: number });
24
13
  enqueue(fileInfo: any): any;
14
+ getTask(key: string): any;
15
+ getProgress(): Record<string, any>;
25
16
  prioritizeLayoutFiles(mediaIds: string[]): void;
17
+ clear(): void;
18
+ queue: any;
26
19
  }
27
20
 
28
- export class FileDownload {}
29
- export class LayoutTaskBuilder {}
21
+ export class FileDownload {
22
+ state: string;
23
+ wait(): Promise<Blob>;
24
+ }
25
+ export class LayoutTaskBuilder {
26
+ constructor(queue: any);
27
+ addFile(fileInfo: any): FileDownload;
28
+ build(): Promise<any[]>;
29
+ }
30
+ export const BARRIER: symbol;
30
31
  export class CacheManager {}
31
32
  export class CacheAnalyzer {
32
33
  constructor(store: StoreClient);
@@ -35,6 +36,4 @@ export class CacheAnalyzer {
35
36
  export const cacheManager: CacheManager;
36
37
 
37
38
  export function isUrlExpired(url: string): boolean;
38
- export function toProxyUrl(url: string): string;
39
- export function setCmsOrigin(origin: string): void;
40
39
  export function cacheWidgetHtml(...args: any[]): any;
package/src/index.js CHANGED
@@ -3,7 +3,7 @@ import pkg from '../package.json' with { type: 'json' };
3
3
  export const VERSION = pkg.version;
4
4
  export { CacheManager, cacheManager } from './cache.js';
5
5
  export { StoreClient } from './store-client.js';
6
- export { DownloadClient } from './download-client.js';
7
- export { DownloadManager, FileDownload, LayoutTaskBuilder, isUrlExpired, toProxyUrl, setCmsOrigin } from './download-manager.js';
6
+ export { DownloadManager, FileDownload, LayoutTaskBuilder, BARRIER, isUrlExpired } from './download-manager.js';
8
7
  export { CacheAnalyzer } from './cache-analyzer.js';
9
8
  export { cacheWidgetHtml } from './widget-html.js';
9
+ export { FILE_TYPES, getFileTypeConfig } from './file-types.js';