app: catch error when checking for updates

ServiceWorkerRegistration.update() can raise exceptions, so we need to
catch and handle them, otherwise it will crash redux sagas and the app
will stop responding.

Fixes: https://github.com/pybricks/pybricks-code/issues/1299
This commit is contained in:
David Lechner
2022-11-11 13:00:40 -06:00
parent 6c906c9cec
commit b2e098f2dd
10 changed files with 89 additions and 3 deletions
+5
View File
@@ -4,6 +4,11 @@
## [Unreleased]
### Fixed
- Fixed app freezing when checking for updates and update server is unreachable ([pybricks-code#1299]).
[pybricks-code#1299]: https://github.com/pybricks/pybricks-code/issues/1299
## [2.0.0-beta.10] - 2022-11-11
### Added
+2
View File
@@ -3,6 +3,7 @@
import { ToastProps } from '@blueprintjs/core';
import alerts from './alerts/alerts';
import app from './app/alerts';
import ble from './ble/alerts';
import explorer from './explorer/alerts';
import firmware from './firmware/alerts';
@@ -12,6 +13,7 @@ import type { CreateToast } from './toasterTypes';
/** This collects alerts from all of the subsystems of the app */
const alertDomains = {
alerts,
app,
ble,
explorer,
firmware,
+5
View File
@@ -21,6 +21,11 @@ export const appDidCheckForUpdate = createAction((updateFound: boolean) => ({
updateFound,
}));
/** Action that indicates that checking for an update failed. */
export const appDidFailToCheckForUpdate = createAction(() => ({
type: 'app.action.didFailToCheckForUpdate',
}));
/* Action that indicates the browser wants to prompt the use to install the app. */
export const appDidReceiveBeforeInstallPrompt = createAction(() => ({
type: 'app.action.didBeforeInstallPrompt',
+21
View File
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { Intent } from '@blueprintjs/core';
import React from 'react';
import type { CreateToast } from '../../toasterTypes';
import { useI18n } from './i18n';
const UpdateServerFailure: React.VoidFunctionComponent = () => {
const i18n = useI18n();
return <p>{i18n.translate('updateServerFailure.message')}</p>;
};
export const updateServerFailure: CreateToast = (onAction) => {
return {
message: <UpdateServerFailure />,
icon: 'error',
intent: Intent.DANGER,
onDismiss: () => onAction('dismiss'),
};
};
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { useI18n as useShopifyI18n } from '@shopify/react-i18n';
import type { TypedI18n } from '../../i18n';
import type translations from './translations/en.json';
export function useI18n(): TypedI18n<typeof translations> {
// istanbul ignore next: babel-loader rewrites this line
const [i18n] = useShopifyI18n();
return i18n;
}
+9
View File
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { updateServerFailure } from './UpdateServerFailure';
// gathers all of the alert creation functions for passing up to the top level
export default {
updateServerFailure,
};
+5
View File
@@ -0,0 +1,5 @@
{
"updateServerFailure": {
"message": "Failed to connect to update server. The Internet connection or the server may be down. Try again later."
}
}
+5
View File
@@ -9,6 +9,7 @@ import {
import {
appCheckForUpdate,
appDidCheckForUpdate,
appDidFailToCheckForUpdate,
appDidReceiveBeforeInstallPrompt,
appDidResolveInstallPrompt,
appShowInstallPrompt,
@@ -59,6 +60,10 @@ test('checkingForUpdate', () => {
reducers({ checkingForUpdate: true } as State, appDidCheckForUpdate(false))
.checkingForUpdate,
).toBe(false);
expect(
reducers({ checkingForUpdate: true } as State, appDidFailToCheckForUpdate())
.checkingForUpdate,
).toBe(false);
expect(
reducers({ checkingForUpdate: true } as State, serviceWorkerDidUpdate())
.checkingForUpdate,
+5
View File
@@ -11,6 +11,7 @@ import {
import {
appCheckForUpdate,
appDidCheckForUpdate,
appDidFailToCheckForUpdate,
appDidReceiveBeforeInstallPrompt,
appDidResolveInstallPrompt,
appShowInstallPrompt,
@@ -39,6 +40,10 @@ const checkingForUpdate: Reducer<boolean> = (state = false, action) => {
return state;
}
if (appDidFailToCheckForUpdate.matches(action)) {
return false;
}
if (serviceWorkerDidUpdate.matches(action)) {
return false;
}
+20 -3
View File
@@ -3,15 +3,18 @@
import { eventChannel } from 'redux-saga';
import { call, delay, fork, put, take, takeEvery } from 'typed-redux-saga/macro';
import { alertsShowAlert } from '../alerts/actions';
import {
serviceWorkerDidSucceed,
serviceWorkerDidUpdate,
} from '../service-worker/actions';
import * as serviceWorkerRegistration from '../serviceWorkerRegistration';
import { ensureError } from '../utils';
import { BeforeInstallPromptEvent } from '../utils/dom';
import {
appCheckForUpdate,
appDidCheckForUpdate,
appDidFailToCheckForUpdate,
appDidReceiveBeforeInstallPrompt,
appDidResolveInstallPrompt,
appReload,
@@ -36,9 +39,23 @@ function* handleAppReload(registration: ServiceWorkerRegistration): Generator {
* Must be called (forked) with serviceWorkerRegistration context set.
*/
function* handleAppCheckForUpdate(registration: ServiceWorkerRegistration): Generator {
yield* call(() => registration.update());
const updateFound = registration.installing !== null;
yield* put(appDidCheckForUpdate(updateFound));
try {
yield* call(() => registration.update());
const updateFound = registration.installing !== null;
yield* put(appDidCheckForUpdate(updateFound));
} catch (err) {
if (err instanceof TypeError) {
yield* put(alertsShowAlert('app', 'updateServerFailure'));
} else {
yield* put(
alertsShowAlert('alerts', 'unexpectedError', {
error: ensureError(err),
}),
);
}
yield* put(appDidFailToCheckForUpdate());
}
}
/**