@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.
Files changed (34) hide show
  1. package/README.md +60 -0
  2. package/dist/assets/cache-proxy-Cx4Z8XMC.js +2 -0
  3. package/dist/assets/cache-proxy-Cx4Z8XMC.js.map +1 -0
  4. package/dist/assets/cms-api-kzy_Sw-u.js +2 -0
  5. package/dist/assets/cms-api-kzy_Sw-u.js.map +1 -0
  6. package/dist/assets/html2canvas.esm-CBrSDip1.js +23 -0
  7. package/dist/assets/html2canvas.esm-CBrSDip1.js.map +1 -0
  8. package/dist/assets/index-BEhNaWZ4.js +2 -0
  9. package/dist/assets/index-BEhNaWZ4.js.map +1 -0
  10. package/dist/assets/index-BPNsrSEv.js +2 -0
  11. package/dist/assets/index-BPNsrSEv.js.map +1 -0
  12. package/dist/assets/index-BY2j60YZ.js +2 -0
  13. package/dist/assets/index-BY2j60YZ.js.map +1 -0
  14. package/dist/assets/index-CTmjUTVM.js +8 -0
  15. package/dist/assets/index-CTmjUTVM.js.map +1 -0
  16. package/dist/assets/index-_q2HbdAU.js +2 -0
  17. package/dist/assets/index-_q2HbdAU.js.map +1 -0
  18. package/dist/assets/index-_uzldOpz.js +16 -0
  19. package/dist/assets/index-_uzldOpz.js.map +1 -0
  20. package/dist/assets/main-C4ABDfkq.js +27 -0
  21. package/dist/assets/main-C4ABDfkq.js.map +1 -0
  22. package/dist/assets/modulepreload-polyfill-B5Qt9EMX.js +2 -0
  23. package/dist/assets/modulepreload-polyfill-B5Qt9EMX.js.map +1 -0
  24. package/dist/assets/pdf-BnPRJEQ6.js +13 -0
  25. package/dist/assets/pdf-BnPRJEQ6.js.map +1 -0
  26. package/dist/assets/setup-rqZh5qYs.js +2 -0
  27. package/dist/assets/setup-rqZh5qYs.js.map +1 -0
  28. package/dist/index.html +130 -0
  29. package/dist/setup.html +371 -0
  30. package/dist/sw-pwa.js +2 -0
  31. package/dist/sw-pwa.js.map +1 -0
  32. package/dist/sw-utils.js +210 -0
  33. package/dist/sw.test.js +271 -0
  34. package/package.json +39 -0
@@ -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
+ }