@xiboplayer/pwa 0.1.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.
- package/README.md +60 -0
- package/dist/assets/cache-proxy-Cx4Z8XMC.js +2 -0
- package/dist/assets/cache-proxy-Cx4Z8XMC.js.map +1 -0
- package/dist/assets/cms-api-kzy_Sw-u.js +2 -0
- package/dist/assets/cms-api-kzy_Sw-u.js.map +1 -0
- package/dist/assets/html2canvas.esm-CBrSDip1.js +23 -0
- package/dist/assets/html2canvas.esm-CBrSDip1.js.map +1 -0
- package/dist/assets/index-BEhNaWZ4.js +2 -0
- package/dist/assets/index-BEhNaWZ4.js.map +1 -0
- package/dist/assets/index-BPNsrSEv.js +2 -0
- package/dist/assets/index-BPNsrSEv.js.map +1 -0
- package/dist/assets/index-BY2j60YZ.js +2 -0
- package/dist/assets/index-BY2j60YZ.js.map +1 -0
- package/dist/assets/index-CTmjUTVM.js +8 -0
- package/dist/assets/index-CTmjUTVM.js.map +1 -0
- package/dist/assets/index-_q2HbdAU.js +2 -0
- package/dist/assets/index-_q2HbdAU.js.map +1 -0
- package/dist/assets/index-_uzldOpz.js +16 -0
- package/dist/assets/index-_uzldOpz.js.map +1 -0
- package/dist/assets/main-C4ABDfkq.js +27 -0
- package/dist/assets/main-C4ABDfkq.js.map +1 -0
- package/dist/assets/modulepreload-polyfill-B5Qt9EMX.js +2 -0
- package/dist/assets/modulepreload-polyfill-B5Qt9EMX.js.map +1 -0
- package/dist/assets/pdf-BnPRJEQ6.js +13 -0
- package/dist/assets/pdf-BnPRJEQ6.js.map +1 -0
- package/dist/assets/setup-rqZh5qYs.js +2 -0
- package/dist/assets/setup-rqZh5qYs.js.map +1 -0
- package/dist/index.html +130 -0
- package/dist/setup.html +371 -0
- package/dist/sw-pwa.js +2 -0
- package/dist/sw-pwa.js.map +1 -0
- package/dist/sw-utils.js +210 -0
- package/dist/sw.test.js +271 -0
- package/package.json +39 -0
package/dist/sw.test.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Worker Routing Helper Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the routeFileRequest() method that determines how to serve files
|
|
5
|
+
* based on storage format (whole file vs chunks) and request type (HEAD/GET/Range)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
9
|
+
|
|
10
|
+
describe('RequestHandler.routeFileRequest()', () => {
|
|
11
|
+
let requestHandler;
|
|
12
|
+
let mockCacheManager;
|
|
13
|
+
let mockBlobCache;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Mock CacheManager
|
|
17
|
+
mockCacheManager = {
|
|
18
|
+
fileExists: vi.fn(),
|
|
19
|
+
get: vi.fn(),
|
|
20
|
+
getMetadata: vi.fn()
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Mock BlobCache
|
|
24
|
+
mockBlobCache = {
|
|
25
|
+
get: vi.fn()
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Create RequestHandler instance (simplified - no DownloadManager needed for routing)
|
|
29
|
+
requestHandler = {
|
|
30
|
+
cacheManager: mockCacheManager,
|
|
31
|
+
blobCache: mockBlobCache,
|
|
32
|
+
// Copy the actual routeFileRequest implementation
|
|
33
|
+
async routeFileRequest(cacheKey, method, rangeHeader) {
|
|
34
|
+
const fileInfo = await this.cacheManager.fileExists(cacheKey);
|
|
35
|
+
|
|
36
|
+
if (!fileInfo.exists) {
|
|
37
|
+
return { found: false, handler: null, data: null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (fileInfo.chunked) {
|
|
41
|
+
const data = { metadata: fileInfo.metadata, cacheKey };
|
|
42
|
+
|
|
43
|
+
if (method === 'HEAD') {
|
|
44
|
+
return { found: true, handler: 'head-chunked', data };
|
|
45
|
+
}
|
|
46
|
+
if (rangeHeader) {
|
|
47
|
+
return { found: true, handler: 'range-chunked', data: { ...data, rangeHeader } };
|
|
48
|
+
}
|
|
49
|
+
return { found: true, handler: 'full-chunked', data };
|
|
50
|
+
|
|
51
|
+
} else {
|
|
52
|
+
const cached = await this.cacheManager.get(cacheKey);
|
|
53
|
+
const data = { cached, cacheKey };
|
|
54
|
+
|
|
55
|
+
if (method === 'HEAD') {
|
|
56
|
+
return { found: true, handler: 'head-whole', data };
|
|
57
|
+
}
|
|
58
|
+
if (rangeHeader) {
|
|
59
|
+
return { found: true, handler: 'range-whole', data: { ...data, rangeHeader } };
|
|
60
|
+
}
|
|
61
|
+
return { found: true, handler: 'full-whole', data };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('File Not Found', () => {
|
|
68
|
+
it('should return not found when file does not exist', async () => {
|
|
69
|
+
mockCacheManager.fileExists.mockResolvedValue({ exists: false });
|
|
70
|
+
|
|
71
|
+
const result = await requestHandler.routeFileRequest('/cache/media/999', 'GET', null);
|
|
72
|
+
|
|
73
|
+
expect(result).toEqual({
|
|
74
|
+
found: false,
|
|
75
|
+
handler: null,
|
|
76
|
+
data: null
|
|
77
|
+
});
|
|
78
|
+
expect(mockCacheManager.fileExists).toHaveBeenCalledWith('/cache/media/999');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('Whole File Storage', () => {
|
|
83
|
+
const cacheKey = '/player/pwa/cache/media/1';
|
|
84
|
+
const mockCached = { headers: { get: () => 'image/png' } };
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
mockCacheManager.fileExists.mockResolvedValue({
|
|
88
|
+
exists: true,
|
|
89
|
+
chunked: false,
|
|
90
|
+
metadata: null
|
|
91
|
+
});
|
|
92
|
+
mockCacheManager.get.mockResolvedValue(mockCached);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should route HEAD request to head-whole handler', async () => {
|
|
96
|
+
const result = await requestHandler.routeFileRequest(cacheKey, 'HEAD', null);
|
|
97
|
+
|
|
98
|
+
expect(result.found).toBe(true);
|
|
99
|
+
expect(result.handler).toBe('head-whole');
|
|
100
|
+
expect(result.data.cached).toBe(mockCached);
|
|
101
|
+
expect(result.data.cacheKey).toBe(cacheKey);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should route GET with Range to range-whole handler', async () => {
|
|
105
|
+
const result = await requestHandler.routeFileRequest(cacheKey, 'GET', 'bytes=0-1000');
|
|
106
|
+
|
|
107
|
+
expect(result.found).toBe(true);
|
|
108
|
+
expect(result.handler).toBe('range-whole');
|
|
109
|
+
expect(result.data.cached).toBe(mockCached);
|
|
110
|
+
expect(result.data.rangeHeader).toBe('bytes=0-1000');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should route GET without Range to full-whole handler', async () => {
|
|
114
|
+
const result = await requestHandler.routeFileRequest(cacheKey, 'GET', null);
|
|
115
|
+
|
|
116
|
+
expect(result.found).toBe(true);
|
|
117
|
+
expect(result.handler).toBe('full-whole');
|
|
118
|
+
expect(result.data.cached).toBe(mockCached);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Chunked File Storage', () => {
|
|
123
|
+
const cacheKey = '/player/pwa/cache/media/6';
|
|
124
|
+
const mockMetadata = {
|
|
125
|
+
totalSize: 1034784779,
|
|
126
|
+
numChunks: 20,
|
|
127
|
+
chunkSize: 50 * 1024 * 1024,
|
|
128
|
+
contentType: 'video/mp4',
|
|
129
|
+
chunked: true
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
mockCacheManager.fileExists.mockResolvedValue({
|
|
134
|
+
exists: true,
|
|
135
|
+
chunked: true,
|
|
136
|
+
metadata: mockMetadata
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should route HEAD request to head-chunked handler', async () => {
|
|
141
|
+
const result = await requestHandler.routeFileRequest(cacheKey, 'HEAD', null);
|
|
142
|
+
|
|
143
|
+
expect(result.found).toBe(true);
|
|
144
|
+
expect(result.handler).toBe('head-chunked');
|
|
145
|
+
expect(result.data.metadata).toEqual(mockMetadata);
|
|
146
|
+
expect(result.data.cacheKey).toBe(cacheKey);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should route GET with Range to range-chunked handler', async () => {
|
|
150
|
+
const result = await requestHandler.routeFileRequest(cacheKey, 'GET', 'bytes=0-5242880');
|
|
151
|
+
|
|
152
|
+
expect(result.found).toBe(true);
|
|
153
|
+
expect(result.handler).toBe('range-chunked');
|
|
154
|
+
expect(result.data.metadata).toEqual(mockMetadata);
|
|
155
|
+
expect(result.data.rangeHeader).toBe('bytes=0-5242880');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should route GET without Range to full-chunked handler', async () => {
|
|
159
|
+
const result = await requestHandler.routeFileRequest(cacheKey, 'GET', null);
|
|
160
|
+
|
|
161
|
+
expect(result.found).toBe(true);
|
|
162
|
+
expect(result.handler).toBe('full-chunked');
|
|
163
|
+
expect(result.data.metadata).toEqual(mockMetadata);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('Edge Cases', () => {
|
|
168
|
+
it('should handle null Range header as no range', async () => {
|
|
169
|
+
mockCacheManager.fileExists.mockResolvedValue({
|
|
170
|
+
exists: true,
|
|
171
|
+
chunked: false,
|
|
172
|
+
metadata: null
|
|
173
|
+
});
|
|
174
|
+
mockCacheManager.get.mockResolvedValue({ headers: { get: () => null } });
|
|
175
|
+
|
|
176
|
+
const result = await requestHandler.routeFileRequest('/cache/media/1', 'GET', null);
|
|
177
|
+
|
|
178
|
+
expect(result.handler).toBe('full-whole');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle empty Range header as range request', async () => {
|
|
182
|
+
mockCacheManager.fileExists.mockResolvedValue({
|
|
183
|
+
exists: true,
|
|
184
|
+
chunked: true,
|
|
185
|
+
metadata: { totalSize: 1000, numChunks: 1, chunked: true }
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const result = await requestHandler.routeFileRequest('/cache/media/6', 'GET', '');
|
|
189
|
+
|
|
190
|
+
// Empty string is falsy, treated as no range
|
|
191
|
+
expect(result.handler).toBe('full-chunked');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Performance', () => {
|
|
196
|
+
it('should call fileExists only once per routing', async () => {
|
|
197
|
+
mockCacheManager.fileExists.mockResolvedValue({
|
|
198
|
+
exists: true,
|
|
199
|
+
chunked: false,
|
|
200
|
+
metadata: null
|
|
201
|
+
});
|
|
202
|
+
mockCacheManager.get.mockResolvedValue({ headers: { get: () => null } });
|
|
203
|
+
|
|
204
|
+
await requestHandler.routeFileRequest('/cache/media/1', 'GET', null);
|
|
205
|
+
|
|
206
|
+
expect(mockCacheManager.fileExists).toHaveBeenCalledTimes(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should not call get() for chunked files', async () => {
|
|
210
|
+
mockCacheManager.fileExists.mockResolvedValue({
|
|
211
|
+
exists: true,
|
|
212
|
+
chunked: true,
|
|
213
|
+
metadata: { totalSize: 1000, chunked: true }
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await requestHandler.routeFileRequest('/cache/media/6', 'GET', 'bytes=0-100');
|
|
217
|
+
|
|
218
|
+
expect(mockCacheManager.get).not.toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('Routing Logic Integration', () => {
|
|
224
|
+
it('should correctly identify all 6 handler combinations', async () => {
|
|
225
|
+
const testCases = [
|
|
226
|
+
{ storage: 'whole', method: 'HEAD', range: null, expected: 'head-whole' },
|
|
227
|
+
{ storage: 'whole', method: 'GET', range: 'bytes=0-100', expected: 'range-whole' },
|
|
228
|
+
{ storage: 'whole', method: 'GET', range: null, expected: 'full-whole' },
|
|
229
|
+
{ storage: 'chunked', method: 'HEAD', range: null, expected: 'head-chunked' },
|
|
230
|
+
{ storage: 'chunked', method: 'GET', range: 'bytes=0-100', expected: 'range-chunked' },
|
|
231
|
+
{ storage: 'chunked', method: 'GET', range: null, expected: 'full-chunked' }
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
for (const testCase of testCases) {
|
|
235
|
+
const mockCacheManager = {
|
|
236
|
+
fileExists: vi.fn().mockResolvedValue({
|
|
237
|
+
exists: true,
|
|
238
|
+
chunked: testCase.storage === 'chunked',
|
|
239
|
+
metadata: testCase.storage === 'chunked' ? { totalSize: 1000, chunked: true } : null
|
|
240
|
+
}),
|
|
241
|
+
get: vi.fn().mockResolvedValue({ headers: { get: () => null } })
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const handler = {
|
|
245
|
+
cacheManager: mockCacheManager,
|
|
246
|
+
async routeFileRequest(cacheKey, method, rangeHeader) {
|
|
247
|
+
const fileInfo = await this.cacheManager.fileExists(cacheKey);
|
|
248
|
+
if (!fileInfo.exists) return { found: false, handler: null, data: null };
|
|
249
|
+
|
|
250
|
+
if (fileInfo.chunked) {
|
|
251
|
+
const data = { metadata: fileInfo.metadata, cacheKey };
|
|
252
|
+
if (method === 'HEAD') return { found: true, handler: 'head-chunked', data };
|
|
253
|
+
if (rangeHeader) return { found: true, handler: 'range-chunked', data: { ...data, rangeHeader } };
|
|
254
|
+
return { found: true, handler: 'full-chunked', data };
|
|
255
|
+
} else {
|
|
256
|
+
const cached = await this.cacheManager.get(cacheKey);
|
|
257
|
+
const data = { cached, cacheKey };
|
|
258
|
+
if (method === 'HEAD') return { found: true, handler: 'head-whole', data };
|
|
259
|
+
if (rangeHeader) return { found: true, handler: 'range-whole', data: { ...data, rangeHeader } };
|
|
260
|
+
return { found: true, handler: 'full-whole', data };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const result = await handler.routeFileRequest('/cache/media/test', testCase.method, testCase.range);
|
|
266
|
+
|
|
267
|
+
expect(result.handler).toBe(testCase.expected);
|
|
268
|
+
expect(result.found).toBe(true);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xiboplayer/pwa",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight PWA Xibo Player with RendererLite",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": ["dist"],
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "tsc && vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"type-check": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@xiboplayer/utils": "^0.1.0",
|
|
15
|
+
"@xiboplayer/cache": "^0.1.0",
|
|
16
|
+
"@xiboplayer/renderer": "^0.1.0",
|
|
17
|
+
"@xiboplayer/schedule": "^0.1.0",
|
|
18
|
+
"@xiboplayer/xmds": "^0.1.0",
|
|
19
|
+
"@xiboplayer/xmr": "^0.1.0",
|
|
20
|
+
"@xiboplayer/core": "^0.1.0",
|
|
21
|
+
"@xiboplayer/stats": "^0.1.0",
|
|
22
|
+
"@xiboplayer/settings": "^0.1.0",
|
|
23
|
+
"xml2js": "^0.6.2",
|
|
24
|
+
"nanoevents": "^9.0.0",
|
|
25
|
+
"html2canvas": "^1.4.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"typescript": "^5.6.3",
|
|
29
|
+
"vite": "^5.4.11",
|
|
30
|
+
"@types/xml2js": "^0.4.14",
|
|
31
|
+
"@types/node": "^22.10.5"
|
|
32
|
+
},
|
|
33
|
+
"author": "Pau Aliagas <linuxnow@gmail.com>",
|
|
34
|
+
"license": "Apache-2.0",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/xibo-players/xiboplayer-pwa.git"
|
|
38
|
+
}
|
|
39
|
+
}
|