@xiboplayer/cache 0.6.3 → 0.6.4
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/README.md +207 -31
- package/package.json +2 -2
- package/src/widget-html.js +8 -7
- package/src/widget-html.test.js +17 -15
package/README.md
CHANGED
|
@@ -1,22 +1,57 @@
|
|
|
1
1
|
# @xiboplayer/cache
|
|
2
2
|
|
|
3
|
-
**Offline caching and download management with
|
|
3
|
+
**Offline caching and download management with parallel chunk downloads, resume support, and storage health monitoring.**
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
Manages media
|
|
7
|
+
Manages the complete lifecycle of media files from CMS to local storage:
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **CacheAnalyzer** — stale media detection and storage-pressure eviction
|
|
16
|
-
- **Widget data via enriched RequiredFiles** — RSS/dataset widget data is fetched through server-side enriched RequiredFiles paths (CMS adds download URLs), not via client-side pre-fetching
|
|
17
|
-
- **Dynamic BASE path** — widget HTML `<base>` tag uses a dynamic path within the Service Worker scope for correct relative URL resolution
|
|
9
|
+
- **REST-based file store** -- StoreClient provides CRUD operations (has, get, put, remove, list) via proxy endpoints
|
|
10
|
+
- **Flat-queue download orchestration** -- DownloadManager with per-file and global concurrency limits
|
|
11
|
+
- **Intelligent chunking** -- files > 100MB split into 50MB chunks with prioritized headers and trailers for fast video start
|
|
12
|
+
- **Download resume** -- partial downloads resume automatically; expired URLs defer to next collection cycle
|
|
13
|
+
- **Stale media detection** -- CacheAnalyzer identifies orphaned files and evicts when storage exceeds threshold
|
|
14
|
+
- **Widget HTML preprocessing** -- base tag injection, dependency rewriting for iframe sandboxing
|
|
18
15
|
|
|
19
|
-
No Cache API is used
|
|
16
|
+
No Cache API is used. All content is stored on the filesystem via the proxy's ContentStore.
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
CMS (RequiredFiles: size, URL, MD5)
|
|
22
|
+
|
|
|
23
|
+
v
|
|
24
|
+
+-------------------------------------+
|
|
25
|
+
| DownloadManager (Facade) |
|
|
26
|
+
| +- enqueue(fileInfo) |
|
|
27
|
+
| +- prioritizeLayoutFiles(mediaIds) |
|
|
28
|
+
| +- urgentChunk(type, id, idx) |
|
|
29
|
+
+-------------------------------------+
|
|
30
|
+
|
|
|
31
|
+
v
|
|
32
|
+
DownloadQueue (Flat)
|
|
33
|
+
+-----------------------------+
|
|
34
|
+
| [Task, Task, BARRIER, |
|
|
35
|
+
| Task, Task, Task] |
|
|
36
|
+
| |
|
|
37
|
+
| Global concurrency: 6 |
|
|
38
|
+
| Per-file chunks: 2-3 |
|
|
39
|
+
+-----------------------------+
|
|
40
|
+
|
|
|
41
|
+
+-> DownloadTask (chunk-0, high priority)
|
|
42
|
+
+-> DownloadTask (chunk-last, high priority)
|
|
43
|
+
+-> BARRIER (gate: waits for above to finish)
|
|
44
|
+
+-> DownloadTask (bulk chunks, normal priority)
|
|
45
|
+
|
|
|
46
|
+
v
|
|
47
|
+
HTTP Range GET
|
|
48
|
+
|
|
|
49
|
+
v
|
|
50
|
+
ContentStore (proxy)
|
|
51
|
+
/store/media/{id}
|
|
52
|
+
/store/layout/{id}
|
|
53
|
+
/store/widget/{L}/{R}/{M}
|
|
54
|
+
```
|
|
20
55
|
|
|
21
56
|
## Installation
|
|
22
57
|
|
|
@@ -26,35 +61,176 @@ npm install @xiboplayer/cache
|
|
|
26
61
|
|
|
27
62
|
## Usage
|
|
28
63
|
|
|
64
|
+
### StoreClient -- direct REST access to ContentStore
|
|
65
|
+
|
|
29
66
|
```javascript
|
|
30
|
-
import { StoreClient
|
|
67
|
+
import { StoreClient } from '@xiboplayer/cache';
|
|
31
68
|
|
|
32
|
-
// Storage operations (pure REST, no SW needed)
|
|
33
69
|
const store = new StoreClient();
|
|
34
|
-
const { exists, size } = await store.has('media', '123');
|
|
35
|
-
const blob = await store.get('media', '123');
|
|
36
|
-
await store.put('widget', '472/221/190', htmlBlob, 'text/html');
|
|
37
70
|
|
|
38
|
-
//
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
71
|
+
// Check existence
|
|
72
|
+
const exists = await store.has('media', '123');
|
|
73
|
+
|
|
74
|
+
// Retrieve file
|
|
75
|
+
const blob = await store.get('media', '456');
|
|
76
|
+
|
|
77
|
+
// Store widget HTML
|
|
78
|
+
const html = new Blob(['<h1>Widget</h1>'], { type: 'text/html' });
|
|
79
|
+
await store.put('widget', 'layout/1/region/2/media/3', html, 'text/html');
|
|
80
|
+
|
|
81
|
+
// List all cached files
|
|
82
|
+
const files = await store.list();
|
|
83
|
+
|
|
84
|
+
// Delete orphaned files
|
|
85
|
+
await store.remove([
|
|
86
|
+
{ type: 'media', id: '999' },
|
|
87
|
+
{ type: 'media', id: '1000' },
|
|
88
|
+
]);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### DownloadManager -- orchestrated downloads
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
import { DownloadManager } from '@xiboplayer/cache';
|
|
95
|
+
|
|
96
|
+
const dm = new DownloadManager({
|
|
97
|
+
concurrency: 6,
|
|
98
|
+
chunkSize: 50 * 1024 * 1024,
|
|
99
|
+
chunksPerFile: 2,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Enqueue a file
|
|
103
|
+
const media = dm.enqueue({
|
|
104
|
+
id: '123',
|
|
105
|
+
type: 'media',
|
|
106
|
+
path: 'https://cdn.example.com/video.mp4',
|
|
107
|
+
size: 250 * 1024 * 1024,
|
|
108
|
+
md5: 'abc123def456',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Wait for completion
|
|
112
|
+
const blob = await media.wait();
|
|
113
|
+
|
|
114
|
+
// Get progress
|
|
115
|
+
const progress = dm.getProgress();
|
|
116
|
+
// { 'media/123': { downloaded: 50M, total: 250M, percent: '20.0', state: 'downloading' } }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Prioritization and emergency chunks
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
// Boost files needed by current layout
|
|
123
|
+
dm.prioritizeLayoutFiles(['123', '456']);
|
|
124
|
+
|
|
125
|
+
// Emergency: chunk needed for video playback is stalled
|
|
126
|
+
// Moves to front of queue, reduces global concurrency to 2
|
|
127
|
+
dm.urgentChunk('media', '789', 3);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### CacheAnalyzer -- storage health
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
import { CacheAnalyzer } from '@xiboplayer/cache';
|
|
134
|
+
|
|
135
|
+
const analyzer = new CacheAnalyzer(store, { threshold: 80 });
|
|
136
|
+
|
|
137
|
+
const report = await analyzer.analyze(requiredFiles);
|
|
138
|
+
console.log(`Storage: ${report.storage.percent}%`);
|
|
139
|
+
console.log(`Orphaned: ${report.files.orphaned} files`);
|
|
140
|
+
console.log(`Evicted: ${report.evicted.length} files`);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Download Pipeline
|
|
144
|
+
|
|
145
|
+
Files flow through stages:
|
|
146
|
+
|
|
147
|
+
1. **Enqueueing** -- deduplication via `type/id` key, URL refresh if new URL has longer expiry
|
|
148
|
+
2. **Preparation** -- HEAD request determines chunking (> 100MB = chunked)
|
|
149
|
+
3. **Task creation** -- chunk-0 and chunk-last get high priority (video headers/trailers); BARRIER separates from bulk chunks
|
|
150
|
+
4. **Processing** -- concurrency-aware: 6 global connections, 2-3 per file
|
|
151
|
+
5. **Execution** -- HTTP Range GET with retries and exponential backoff
|
|
152
|
+
6. **Storage** -- proxy ContentStore saves chunks to filesystem
|
|
153
|
+
7. **Assembly** -- chunks concatenated; progressive callback enables playback before full download
|
|
154
|
+
|
|
155
|
+
### Chunk strategy
|
|
156
|
+
|
|
157
|
+
- **Default chunk size:** 50MB
|
|
158
|
+
- **Threshold:** files > 100MB get chunked
|
|
159
|
+
- **Header+trailer first:** MP4 moov atom in chunk-0 and chunk-last allows instant playback start
|
|
160
|
+
- **Barriers:** critical chunks download before bulk chunks begin
|
|
161
|
+
- **Resume:** cached chunks tracked in `skipChunks` Set; only missing chunks downloaded
|
|
162
|
+
- **Expired URLs:** deferred (not failed) -- next collection provides fresh URL
|
|
163
|
+
|
|
164
|
+
### Retry strategy by type
|
|
165
|
+
|
|
166
|
+
| Type | Max retries | Backoff | Notes |
|
|
167
|
+
|------|------------|---------|-------|
|
|
168
|
+
| media | 3 | 500ms | Large, cacheable files |
|
|
169
|
+
| layout | 3 | 500ms | Layout XML, stable URL |
|
|
170
|
+
| dataset | 4 | 15s, 30s, 60s, 120s | Re-enqueues 5x for "cache not ready" |
|
|
171
|
+
| static | 3 | 500ms | Widget dependencies (CSS, fonts) |
|
|
172
|
+
|
|
173
|
+
## API Reference
|
|
174
|
+
|
|
175
|
+
### StoreClient
|
|
176
|
+
|
|
177
|
+
| Method | Signature | Returns | Description |
|
|
178
|
+
|--------|-----------|---------|-------------|
|
|
179
|
+
| `has()` | `has(type, id)` | `Promise<boolean>` | Check if file exists |
|
|
180
|
+
| `get()` | `get(type, id)` | `Promise<Blob \| null>` | Retrieve file as Blob |
|
|
181
|
+
| `put()` | `put(type, id, body, contentType?)` | `Promise<boolean>` | Store file |
|
|
182
|
+
| `remove()` | `remove(files)` | `Promise<{deleted, total}>` | Delete files |
|
|
183
|
+
| `list()` | `list()` | `Promise<Array>` | List all cached files |
|
|
184
|
+
|
|
185
|
+
### DownloadManager
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
new DownloadManager({ concurrency?, chunkSize?, chunksPerFile? })
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
| Method | Signature | Returns | Description |
|
|
192
|
+
|--------|-----------|---------|-------------|
|
|
193
|
+
| `enqueue()` | `enqueue(fileInfo)` | `FileDownload` | Add file to download queue |
|
|
194
|
+
| `getTask()` | `getTask(key)` | `FileDownload \| null` | Get task by "type/id" key |
|
|
195
|
+
| `getProgress()` | `getProgress()` | `Record<string, Progress>` | All in-progress downloads |
|
|
196
|
+
| `prioritizeLayoutFiles()` | `prioritizeLayoutFiles(mediaIds)` | `void` | Boost priority for layout files |
|
|
197
|
+
| `urgentChunk()` | `urgentChunk(type, id, chunkIndex)` | `boolean` | Move chunk to front, reduce concurrency |
|
|
198
|
+
| `clear()` | `clear()` | `void` | Cancel all, clear queue |
|
|
199
|
+
|
|
200
|
+
### CacheAnalyzer
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
new CacheAnalyzer(storeClient, { threshold: 80 })
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
| Method | Signature | Returns | Description |
|
|
207
|
+
|--------|-----------|---------|-------------|
|
|
208
|
+
| `analyze()` | `analyze(requiredFiles)` | `Promise<Report>` | Compare cache vs required, evict if needed |
|
|
209
|
+
|
|
210
|
+
**Report:**
|
|
211
|
+
```javascript
|
|
212
|
+
{
|
|
213
|
+
storage: { usage, quota, percent },
|
|
214
|
+
files: { required, orphaned, total },
|
|
215
|
+
orphaned: [{ id, type, size, cachedAt }],
|
|
216
|
+
evicted: [{ id, type, size }],
|
|
217
|
+
threshold: 80
|
|
218
|
+
}
|
|
43
219
|
```
|
|
44
220
|
|
|
45
|
-
|
|
221
|
+
### CacheManager
|
|
46
222
|
|
|
47
|
-
|
|
|
48
|
-
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
223
|
+
| Method | Signature | Returns | Description |
|
|
224
|
+
|--------|-----------|---------|-------------|
|
|
225
|
+
| `getCacheKey()` | `getCacheKey(type, id)` | `string` | Get cache key path |
|
|
226
|
+
| `addDependant()` | `addDependant(mediaId, layoutId)` | `void` | Track layout -> media reference |
|
|
227
|
+
| `removeLayoutDependants()` | `removeLayoutDependants(layoutId)` | `string[]` | Remove layout, return orphaned media IDs |
|
|
228
|
+
| `isMediaReferenced()` | `isMediaReferenced(mediaId)` | `boolean` | Check if media is still used |
|
|
53
229
|
|
|
54
230
|
## Dependencies
|
|
55
231
|
|
|
56
|
-
- `@xiboplayer/utils`
|
|
57
|
-
- `spark-md5`
|
|
232
|
+
- `@xiboplayer/utils` -- logger
|
|
233
|
+
- `spark-md5` -- MD5 hashing for file verification
|
|
58
234
|
|
|
59
235
|
---
|
|
60
236
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/cache",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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.6.
|
|
19
|
+
"@xiboplayer/utils": "0.6.4"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"vitest": "^2.0.0",
|
package/src/widget-html.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - CSS object-position fix for CMS template alignment
|
|
8
8
|
*
|
|
9
9
|
* URL rewriting is no longer needed — the CMS serves CSS with relative paths
|
|
10
|
-
* (/
|
|
10
|
+
* (${PLAYER_API}/dependencies/font.otf), and the <base> tag resolves widget
|
|
11
11
|
* media references via mirror routes. Zero translation, zero regex.
|
|
12
12
|
*
|
|
13
13
|
* Runs on the main thread (needs window.location for URL construction).
|
|
@@ -25,7 +25,7 @@ const BASE = (typeof window !== 'undefined')
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Store widget HTML in ContentStore for iframe loading.
|
|
28
|
-
* Stored at mirror path /
|
|
28
|
+
* Stored at mirror path ${PLAYER_API}/widgets/{L}/{R}/{M} — same URL the
|
|
29
29
|
* CMS serves from, so iframes load directly from Express mirror routes.
|
|
30
30
|
*
|
|
31
31
|
* @param {string} layoutId - Layout ID
|
|
@@ -63,12 +63,13 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Rewrite dependency URLs to local mirror paths. CMS sends absolute URLs
|
|
66
|
-
// like https://cms.example.com/
|
|
67
|
-
// which fail due to CORS/auth. Replace with local /
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
// like https://cms.example.com${PLAYER_API}/dependencies/bundle.min.js
|
|
67
|
+
// which fail due to CORS/auth. Replace with local PLAYER_API/dependencies/...
|
|
68
|
+
const depsPattern = new RegExp(
|
|
69
|
+
`https?://[^"'\\s)]+?(${PLAYER_API.replace(/\//g, '\\/')}/dependencies/[^"'\\s?)]+)(\\?[^"'\\s)]*)?`,
|
|
70
|
+
'g'
|
|
71
71
|
);
|
|
72
|
+
modifiedHtml = modifiedHtml.replace(depsPattern, (_, path) => path);
|
|
72
73
|
|
|
73
74
|
// Inject xiboICTargetId — XIC library reads this global before its IIFE runs
|
|
74
75
|
// to set _lib.targetId, which is included in every IC HTTP request as {id: ...}
|
package/src/widget-html.test.js
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
10
10
|
import { cacheWidgetHtml } from './widget-html.js';
|
|
11
|
+
import { PLAYER_API } from '@xiboplayer/utils';
|
|
11
12
|
|
|
12
13
|
const CMS_BASE = 'https://displays.superpantalles.com';
|
|
13
|
-
const SIGNED_PARAMS = 'displayId=152&type=P&itemId=1&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260222T000000Z&X-Amz-Expires=1771803983&X-Amz-SignedHeaders=host&X-Amz-Signature=abc123';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* RSS ticker widget HTML (layout 472, region 223, widget 193).
|
|
@@ -22,8 +22,8 @@ function makeRssTickerHtml() {
|
|
|
22
22
|
<head>
|
|
23
23
|
<meta charset="utf-8">
|
|
24
24
|
<title>RSS Ticker</title>
|
|
25
|
-
<script src="/
|
|
26
|
-
<link rel="stylesheet" href="/
|
|
25
|
+
<script src="${PLAYER_API}/dependencies/bundle.min.js"></script>
|
|
26
|
+
<link rel="stylesheet" href="${PLAYER_API}/dependencies/fonts.css">
|
|
27
27
|
<style>.rss-item { padding: 10px; }</style>
|
|
28
28
|
</head>
|
|
29
29
|
<body>
|
|
@@ -47,8 +47,8 @@ function makePdfWidgetHtml() {
|
|
|
47
47
|
<html>
|
|
48
48
|
<head>
|
|
49
49
|
<meta charset="utf-8">
|
|
50
|
-
<script src="/
|
|
51
|
-
<link rel="stylesheet" href="/
|
|
50
|
+
<script src="${PLAYER_API}/dependencies/bundle.min.js"></script>
|
|
51
|
+
<link rel="stylesheet" href="${PLAYER_API}/dependencies/fonts.css"></link>
|
|
52
52
|
</head>
|
|
53
53
|
<body>
|
|
54
54
|
<object data="11.pdf" type="application/pdf" width="100%" height="100%"></object>
|
|
@@ -86,9 +86,11 @@ describe('cacheWidgetHtml', () => {
|
|
|
86
86
|
global.fetch = createFetchMock();
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
const storePrefix = `/store${PLAYER_API}/widgets/`;
|
|
90
|
+
|
|
89
91
|
function getStoredWidget() {
|
|
90
92
|
for (const [key, value] of storeContents) {
|
|
91
|
-
if (key.startsWith(
|
|
93
|
+
if (key.startsWith(storePrefix)) return value;
|
|
92
94
|
}
|
|
93
95
|
return undefined;
|
|
94
96
|
}
|
|
@@ -101,7 +103,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
101
103
|
await cacheWidgetHtml('472', '223', '193', html);
|
|
102
104
|
|
|
103
105
|
const stored = getStoredWidget();
|
|
104
|
-
expect(stored).toContain(
|
|
106
|
+
expect(stored).toContain(`<base href="${PLAYER_API}/media/file/">`);
|
|
105
107
|
});
|
|
106
108
|
|
|
107
109
|
it('injects <base> tag when no <head> tag exists', async () => {
|
|
@@ -109,7 +111,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
109
111
|
await cacheWidgetHtml('472', '223', '193', html);
|
|
110
112
|
|
|
111
113
|
const stored = getStoredWidget();
|
|
112
|
-
expect(stored).toContain(
|
|
114
|
+
expect(stored).toContain(`<base href="${PLAYER_API}/media/file/">`);
|
|
113
115
|
});
|
|
114
116
|
});
|
|
115
117
|
|
|
@@ -120,8 +122,8 @@ describe('cacheWidgetHtml', () => {
|
|
|
120
122
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
121
123
|
|
|
122
124
|
const stored = getStoredWidget();
|
|
123
|
-
expect(stored).toContain(
|
|
124
|
-
expect(stored).toContain(
|
|
125
|
+
expect(stored).toContain(`${PLAYER_API}/dependencies/bundle.min.js`);
|
|
126
|
+
expect(stored).toContain(`${PLAYER_API}/dependencies/fonts.css`);
|
|
125
127
|
});
|
|
126
128
|
|
|
127
129
|
it('preserves the data URL (193.json) for resolution via base tag', async () => {
|
|
@@ -143,7 +145,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
143
145
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
144
146
|
|
|
145
147
|
// Only the widget HTML PUT should be fetched — no proactive resource fetches
|
|
146
|
-
const putCalls = fetchedUrls.filter(u => u.startsWith(
|
|
148
|
+
const putCalls = fetchedUrls.filter(u => u.startsWith(storePrefix));
|
|
147
149
|
expect(putCalls.length).toBe(1);
|
|
148
150
|
});
|
|
149
151
|
|
|
@@ -151,7 +153,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
151
153
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
152
154
|
|
|
153
155
|
const keys = [...storeContents.keys()];
|
|
154
|
-
expect(keys.some(k => k.includes(
|
|
156
|
+
expect(keys.some(k => k.includes(`${storePrefix}472/223/193`))).toBe(true);
|
|
155
157
|
});
|
|
156
158
|
});
|
|
157
159
|
|
|
@@ -169,7 +171,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
169
171
|
await cacheWidgetHtml('472', '221', '190', makePdfWidgetHtml());
|
|
170
172
|
|
|
171
173
|
const keys = [...storeContents.keys()];
|
|
172
|
-
expect(keys.some(k => k.includes(
|
|
174
|
+
expect(keys.some(k => k.includes(`${storePrefix}472/221/190`))).toBe(true);
|
|
173
175
|
});
|
|
174
176
|
});
|
|
175
177
|
|
|
@@ -257,8 +259,8 @@ describe('cacheWidgetHtml', () => {
|
|
|
257
259
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
258
260
|
|
|
259
261
|
const keys = [...storeContents.keys()];
|
|
260
|
-
expect(keys.some(k => k.includes(
|
|
261
|
-
expect(keys.some(k => k.includes(
|
|
262
|
+
expect(keys.some(k => k.includes(`${storePrefix}472/221/190`))).toBe(true);
|
|
263
|
+
expect(keys.some(k => k.includes(`${storePrefix}472/223/193`))).toBe(true);
|
|
262
264
|
});
|
|
263
265
|
});
|
|
264
266
|
});
|