Skip to content

Commit c84317b

Browse files
committed
frontend: fix service worker not being available
When force reloading the page the service worker was not available this commit fixes it by always updating the service worker and stop using serviceWorker.controller, which is always null when force reloading.
1 parent 74ce9f3 commit c84317b

File tree

6 files changed

+82
-62
lines changed

6 files changed

+82
-62
lines changed

frontend/index.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
<title>Remote Access</title>
99
<script type="text/javascript">
1010
if ('serviceWorker' in navigator) {
11-
navigator.serviceWorker.register("/sw.js", {type: "module"});
11+
navigator.serviceWorker.register("/sw.js", {type: "module", updateViaCache: "none"}).then((s) => {
12+
// When force reloading the page the service worker is not updated, so we manually check for updates here
13+
s.update();
14+
});
1215
}
1316
function median_app_resumed() {
1417
if (sessionStorage.getItem("currentConnection")) {

frontend/src/index.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export function App() {
151151
refresh_access_token();
152152
startVersionChecking(10);
153153
}, []);
154+
154155
useEffect(() => {
155156
if (loggedIn.value === AppState.LoggedIn) {
156157
refreshInterval = setInterval(async () => {
@@ -160,20 +161,7 @@ export function App() {
160161
} else {
161162
clearInterval(refreshInterval);
162163
}
163-
}, [loggedIn.value])
164-
165-
// Migrate secret from localStorage to service worker
166-
useEffect(() => {
167-
const secretFromLocalStorage = localStorage.getItem("secretKey");
168-
if (secretFromLocalStorage) {
169-
localStorage.removeItem("secretKey");
170-
const msg: Message = {
171-
type: MessageType.StoreSecret,
172-
data: secretFromLocalStorage
173-
};
174-
navigator.serviceWorker.controller?.postMessage(msg);
175-
}
176-
}, []);
164+
}, [loggedIn.value]);
177165

178166
// Prepare persistence explanation modal to render alongside any app state
179167
const persistModal = (

frontend/src/pages/Frame.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ class VirtualNetworkInterface {
153153
}
154154

155155
// This handles the Message coming from the Charger once the setup is done
156-
private handleWorkerMessage(e: MessageEvent) {
156+
private async handleWorkerMessage(e: MessageEvent) {
157157
if (typeof e.data === "string") {
158158
switch (e.data) {
159159
case "ready":
@@ -215,10 +215,8 @@ class VirtualNetworkInterface {
215215
break;
216216

217217
case MessageType.FetchResponse:
218-
if (!navigator.serviceWorker.controller) {
219-
throw new Error("ServiceWorker controller is not available");
220-
}
221-
navigator.serviceWorker.controller.postMessage(msg);
218+
const controller = await navigator.serviceWorker.ready;
219+
controller.active?.postMessage(msg);
222220
break;
223221

224222
case MessageType.Error:

frontend/src/sw.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import { Message, MessageType, FetchMessage, ResponseMessage } from "./types";
2121

2222
declare const self: ServiceWorkerGlobalScope;
23+
declare const __BUILD_TIMESTAMP__: string;
2324

2425
self.addEventListener("activate", (event) => {
2526
event.waitUntil(self.clients.claim());
@@ -182,6 +183,10 @@ self.addEventListener("message", async (e: ExtendableMessageEvent) => {
182183
return;
183184
}
184185

186+
if (typeof e.data === "string" && e.data === "version") {
187+
e.source?.postMessage(__BUILD_TIMESTAMP__);
188+
}
189+
185190
const msg = e.data as Message;
186191

187192
switch (msg.type) {
@@ -201,7 +206,20 @@ self.addEventListener("message", async (e: ExtendableMessageEvent) => {
201206
break;
202207

203208
case MessageType.StoreSecret:
204-
await storeSecretKeyInCache(msg.data as string);
209+
try {
210+
await storeSecretKeyInCache(msg.data as string);
211+
const responseMsg: Message = {
212+
type: MessageType.StoreSecret,
213+
data: "stored"
214+
};
215+
e.source?.postMessage(responseMsg);
216+
} catch (error) {
217+
const responseMsg: Message = {
218+
type: MessageType.StoreSecret,
219+
data: `error: ${(error as Error).message}`
220+
};
221+
e.source?.postMessage(responseMsg);
222+
}
205223
break;
206224

207225
case MessageType.RequestSecret:

frontend/src/utils.tsx

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -131,64 +131,77 @@ export async function refresh_access_token() {
131131
export let secret: Uint8Array | null = null;
132132
export let pub_key: Uint8Array | null = null;
133133

134-
// Service Worker communication functions
134+
let storeSecretKeyPromise: Promise<void> | null = null;
135135
export async function storeSecretKeyInServiceWorker(secretKey: string): Promise<void> {
136-
if (!navigator.serviceWorker.controller) {
137-
return;
136+
if (storeSecretKeyPromise) {
137+
return storeSecretKeyPromise;
138138
}
139139

140-
const msg: Message = {
141-
type: MessageType.StoreSecret,
142-
data: secretKey
143-
};
144-
navigator.serviceWorker.controller.postMessage(msg);
145-
}
140+
storeSecretKeyPromise = new Promise(async (resolve, reject) => {
141+
// We dont use navigator.serviceWorker.controller here since it can be, for whatever reason,
142+
// null for an entire browser session
143+
const controller = await navigator.serviceWorker.ready;
144+
145+
const timeout = setTimeout(() => {
146+
console.error("Service Worker: Failed to store secretKey within timeout. Retrying...");
147+
storeSecretKeyPromise = null;
148+
reject("Timeout waiting for storing secretKey in Service Worker");
149+
}, 5000);
150+
151+
const handleMessage = (event: MessageEvent) => {
152+
const msg = event.data as Message;
153+
if (msg.type === MessageType.StoreSecret) {
154+
if (msg.data !== "stored") {
155+
clearTimeout(timeout);
156+
navigator.serviceWorker.removeEventListener('message', handleMessage);
157+
storeSecretKeyPromise = null;
158+
reject("Failed to store secretKey in Service Worker: " + msg.data);
159+
return;
160+
}
161+
clearTimeout(timeout);
162+
navigator.serviceWorker.removeEventListener('message', handleMessage);
163+
storeSecretKeyPromise = null;
164+
resolve();
165+
}
166+
};
146167

147-
let gettingSecretInProgress = false;
148-
let secretKeyPromise: Promise<string | null> | null = null;
149-
let retries = 0;
168+
navigator.serviceWorker.addEventListener('message', handleMessage);
169+
170+
const msg: Message = {
171+
type: MessageType.StoreSecret,
172+
data: secretKey
173+
};
174+
controller.active?.postMessage(msg);
175+
});
150176

151-
export async function getSecretKeyFromServiceWorker(): Promise<string | null> {
152-
if (gettingSecretInProgress) {
177+
return storeSecretKeyPromise;
178+
}
179+
180+
let secretKeyPromise: Promise<string> | null = null;
181+
export async function getSecretKeyFromServiceWorker(): Promise<string> {
182+
if (secretKeyPromise) {
153183
return secretKeyPromise;
154184
}
155185

156-
secretKeyPromise = new Promise(async (resolve) => {
186+
secretKeyPromise = new Promise(async (resolve, reject) => {
157187
// We dont use navigator.serviceWorker.controller here since it can be, for whatever reason,
158188
// null for an entire browser session
159-
const controller = await navigator.serviceWorker.getRegistration(location.origin);
160-
if (!controller?.active && retries < 3) {
161-
console.error("ServiceWorker controller not active, retrying...");
162-
retries++;
163-
setTimeout(async () => {
164-
resolve(await getSecretKeyFromServiceWorker());
165-
}, 500);
166-
return;
167-
} else if (!controller?.active) {
168-
console.error("service worker controller not active");
169-
return resolve(null);
170-
} else if (retries >= 3) {
171-
console.error("Max retries reached for getting secretKey from service worker");
172-
return resolve(null);
173-
}
174-
gettingSecretInProgress = true;
189+
const controller = await navigator.serviceWorker.ready;
175190

176191
const timeout = setTimeout(async () => {
177192
console.error("Service Worker: Failed to get secretKey within timeout. Retrying...");
178193
if (!appSleeps || !Median.isNativeApp()) {
179-
gettingSecretInProgress = false;
180-
retries++;
181-
resolve(await getSecretKeyFromServiceWorker());
194+
reject("Timeout waiting for secretKey from Service Worker");
182195
}
196+
secretKeyPromise = null;
183197
}, 5000);
184-
retries = 0;
185198

186199
const handleMessage = (event: MessageEvent) => {
187200
const msg = event.data as Message;
188201
if (msg.type === MessageType.StoreSecret) {
189202
clearTimeout(timeout);
190203
navigator.serviceWorker.removeEventListener('message', handleMessage);
191-
gettingSecretInProgress = false;
204+
secretKeyPromise = null;
192205
resolve(msg.data as string);
193206
}
194207
};
@@ -206,15 +219,12 @@ export async function getSecretKeyFromServiceWorker(): Promise<string | null> {
206219
}
207220

208221
export async function clearSecretKeyFromServiceWorker(): Promise<void> {
209-
if (!navigator.serviceWorker.controller) {
210-
return;
211-
}
212-
222+
const controller = await navigator.serviceWorker.ready;
213223
const msg: Message = {
214224
type: MessageType.ClearSecret,
215225
data: null
216226
};
217-
navigator.serviceWorker.controller.postMessage(msg);
227+
controller.active?.postMessage(msg);
218228
}
219229

220230
export async function get_decrypted_secret() {

frontend/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ const swBuildPlugin: Plugin = {
1818
entryPoints: [join(process.cwd(), "src", "sw.ts")],
1919
outfile: join(process.cwd(), "dist", "sw.js"),
2020
format: "esm",
21+
define: {
22+
__BUILD_TIMESTAMP__: `"${new Date().toISOString()}"`,
23+
}
2124
})
2225
}
2326
}

0 commit comments

Comments
 (0)