|
1 | 1 | let VERSION = null; |
2 | 2 |
|
3 | | -/* |
4 | | - * This Service Worker is an optional optimisation to load the app faster. |
5 | | - * Whenever using raw es module without any build, we had a large number |
6 | | - * of assets getting through the network. When we looked through the |
7 | | - * developer console -> network, and look at the timing, 98% of the time |
8 | | - * was spent "waiting for the server response". |
9 | | - * HTTP2/3 should solve that issue but we don't control the proxy side of |
10 | | - * things of how people install Filestash, hence the idea to bulk download |
11 | | - * as much as we can through SSE, store it onto a cache and get our |
12 | | - * service worker to inject the response. |
13 | | - * This approach alone make the app a lot faster to load but relies on |
14 | | - * the server being able to bundle our assets via SSE. |
15 | | - * |
16 | | - * TODO: |
17 | | - * - wait until browser support DecompressionStream("brotli") natively |
18 | | - * and use that. As of 2025, downloading a brotli decompress library |
19 | | - * make the gain br / gz negative for our app |
20 | | - * - wait until Firefox support SSE within service worker. As of 2025, |
21 | | - * someone was implementing it in Firefox but it's not everywhere yet |
22 | | - * Once that's done, we want to be 100% sure everything is working great |
23 | | - */ |
24 | | - |
25 | 3 | self.addEventListener("install", (event) => { |
26 | 4 | if (!self.EventSource) throw new Error("turboload not supported on this platform"); |
27 | 5 |
|
@@ -58,104 +36,27 @@ self.addEventListener("message", (event) => { |
58 | 36 | ); |
59 | 37 | }); |
60 | 38 |
|
61 | | -async function handlePreloadMessage(chunks, clear, version, resolve, reject) { |
| 39 | +async function handlePreloadMessage(imports, clear, version, resolve, reject) { |
62 | 40 | VERSION = version; |
63 | | - const cleanup = []; |
64 | 41 | try { |
65 | | - let execHTTP = true; |
66 | 42 | await caches.keys().then(async(names) => { |
67 | | - for (let i=0; i<names.length; i++) { |
68 | | - if (names[i] === VERSION && !clear) { |
69 | | - execHTTP = false; |
70 | | - return; |
71 | | - } |
| 43 | + for (let i = 0; i<names.length; i++) { |
72 | 44 | await caches.delete(names[i]); |
73 | 45 | } |
74 | 46 | }); |
75 | | - if (execHTTP) { |
76 | | - const cache = await caches.open(VERSION); |
77 | | - chunks = await Promise.all(chunks.map(async(urls) => { |
78 | | - const missing = []; |
79 | | - await Promise.all(urls.map(async(url) => { |
80 | | - if (!await cache.match(location.origin + url)) missing.push(url); |
81 | | - })); |
82 | | - return missing; |
83 | | - })); |
84 | | - if (chunks.filter((urls) => urls.length > 0).length > 0) { |
85 | | - await Promise.all(chunks.map((urls) => { |
86 | | - return preload({ urls, cache, cleanup }); |
87 | | - })); |
88 | | - } |
| 47 | + const cache = await caches.open(VERSION); |
| 48 | + for (const path in imports) { |
| 49 | + let mime = "application/octet-stream"; |
| 50 | + if (path.endsWith(".css")) mime = "text/css"; |
| 51 | + else if (path.endsWith(".js")) mime = "application/javascript"; |
| 52 | + await cache.put(location.origin + path, new Response( |
| 53 | + new Blob([imports[path]]), |
| 54 | + { headers: { "Content-Type": mime } }, |
| 55 | + )); |
89 | 56 | } |
90 | 57 | resolve(); |
91 | 58 | } catch (err) { |
92 | 59 | console.log("ERR", err); |
93 | 60 | reject(err); |
94 | | - } finally { |
95 | | - cleanup.forEach((fn) => fn()); |
96 | 61 | } |
97 | 62 | }; |
98 | | - |
99 | | -async function preload({ urls, cache, cleanup }) { |
100 | | - const evtsrc = new self.EventSource("/assets/bundle?" + urls.map((url) => `url=${url}`).join("&")); |
101 | | - cleanup.push(() => evtsrc.close()); |
102 | | - |
103 | | - let i = 0; |
104 | | - const messageHandler = async(resolve, event, decoder) => { |
105 | | - const url = event.lastEventId; |
106 | | - let mime = "application/octet-stream"; |
107 | | - if (url.endsWith(".css")) mime = "text/css"; |
108 | | - else if (url.endsWith(".js")) mime = "application/javascript"; |
109 | | - |
110 | | - i += 1; |
111 | | - await cache.put( |
112 | | - location.origin + url, |
113 | | - new Response( |
114 | | - decoder(new Blob([base128Decode(event.data)]).stream()), |
115 | | - { headers: { "Content-Type": mime } }, |
116 | | - ), |
117 | | - ); |
118 | | - if (i === urls.length) { |
119 | | - resolve(); |
120 | | - } |
121 | | - }; |
122 | | - const errorHandler = (reject, err) => { |
123 | | - reject(err); |
124 | | - }; |
125 | | - |
126 | | - await new Promise((resolve, reject) => { |
127 | | - evtsrc.addEventListener("static::raw", (event) => messageHandler( |
128 | | - resolve, |
129 | | - event, |
130 | | - (stream) => stream, |
131 | | - )); |
132 | | - evtsrc.addEventListener("static::gzip", (event) => messageHandler( |
133 | | - resolve, |
134 | | - event, |
135 | | - (stream) => stream.pipeThrough(new DecompressionStream("gzip")), |
136 | | - )); |
137 | | - evtsrc.onerror = (err) => { |
138 | | - if (i === urls.length) return; |
139 | | - errorHandler(reject, err); |
140 | | - }; |
141 | | - }); |
142 | | -} |
143 | | - |
144 | | -function base128Decode(s) { // encoder is in server/ctrl/static.go -> encodeB128 |
145 | | - const out = new Uint8Array(Math.floor((s.length * 7) / 8) + 1); |
146 | | - let acc = 0; |
147 | | - let bits = 0; |
148 | | - let oi = 0; |
149 | | - for (let i = 0; i < s.length; i++) { |
150 | | - const ch = s.charCodeAt(i); |
151 | | - const digit = ch & 0x7F; // undo 0x80 masking for NUL/LF/CR |
152 | | - acc = (acc << 7) | digit; |
153 | | - bits += 7; |
154 | | - while (bits >= 8) { |
155 | | - bits -= 8; |
156 | | - out[oi++] = (acc >> bits) & 0xFF; |
157 | | - acc &= (1 << bits) - 1; |
158 | | - } |
159 | | - } |
160 | | - return out.subarray(0, oi); |
161 | | -} |
0 commit comments