mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 18:46:17 +00:00
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
160 lines
5.2 KiB
TypeScript
160 lines
5.2 KiB
TypeScript
// SPDX-License-Identifier: MIT
|
|
// Copyright (c) 2021-2022 The Pybricks Authors
|
|
|
|
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,
|
|
appShowInstallPrompt,
|
|
didInstall,
|
|
} from './actions';
|
|
|
|
/**
|
|
* Handles appReload actions.
|
|
*
|
|
* Must be called (forked) with serviceWorkerRegistration context set.
|
|
*/
|
|
function* handleAppReload(registration: ServiceWorkerRegistration): Generator {
|
|
yield* call(() => registration.unregister());
|
|
|
|
location.reload();
|
|
}
|
|
|
|
/**
|
|
* Handles appCheckForUpdate actions.
|
|
*
|
|
* Must be called (forked) with serviceWorkerRegistration context set.
|
|
*/
|
|
function* handleAppCheckForUpdate(registration: ServiceWorkerRegistration): Generator {
|
|
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());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Marshals CRA serviceWorkerRegistration to saga.
|
|
*/
|
|
function* monitorServiceWorkerRegistration(): Generator {
|
|
const chan = eventChannel<{
|
|
isUpdate: boolean;
|
|
registration: ServiceWorkerRegistration;
|
|
}>((emit) => {
|
|
serviceWorkerRegistration.register({
|
|
onSuccess: (r) => emit({ isUpdate: false, registration: r }),
|
|
onUpdate: (r) => emit({ isUpdate: true, registration: r }),
|
|
});
|
|
|
|
// istanbul ignore next: never unregistered
|
|
return () => serviceWorkerRegistration.unregister();
|
|
});
|
|
|
|
// HACK: is assumed that this will only be called at most two times, once
|
|
// with isUpdate === false and after that, once with isUpdate === true.
|
|
while (true) {
|
|
const { isUpdate, registration } = yield* take(chan);
|
|
|
|
if (isUpdate) {
|
|
yield* put(serviceWorkerDidUpdate());
|
|
} else {
|
|
yield* takeEvery(appReload, handleAppReload, registration);
|
|
yield* takeEvery(appCheckForUpdate, handleAppCheckForUpdate, registration);
|
|
|
|
yield* put(serviceWorkerDidSucceed());
|
|
}
|
|
}
|
|
}
|
|
|
|
function* monitorAppInstalled(): Generator {
|
|
const chan = eventChannel<Event>((emit) => {
|
|
const listener = (e: Event) => {
|
|
emit(e);
|
|
};
|
|
// chromium-only event
|
|
window.addEventListener('appinstalled', listener);
|
|
// istanbul ignore next: currently we don't ever stop monitoring
|
|
return () => window.removeEventListener('appinstalled', listener);
|
|
});
|
|
|
|
while (true) {
|
|
yield* take(chan);
|
|
yield* put(didInstall());
|
|
}
|
|
}
|
|
|
|
function* handleBeforeInstallPromptEvent(event: BeforeInstallPromptEvent): Generator {
|
|
// wait for user to request to install the app - may never happen
|
|
yield* take(appShowInstallPrompt);
|
|
|
|
yield* call(() => event.prompt());
|
|
const choice = yield* call(() => event.userChoice);
|
|
yield* put(appDidResolveInstallPrompt(choice));
|
|
}
|
|
|
|
function* monitorBeforeInstallPrompt(): Generator {
|
|
const chan = eventChannel<BeforeInstallPromptEvent>((emit) => {
|
|
const listener = (e: BeforeInstallPromptEvent) => {
|
|
emit(e);
|
|
};
|
|
// @ts-expect-error: chromium-only event
|
|
window.addEventListener('beforeinstallprompt', listener);
|
|
// istanbul ignore next: currently we don't ever stop monitoring
|
|
// @ts-expect-error: chromium-only event
|
|
return () => window.removeEventListener('beforeinstallprompt', listener);
|
|
});
|
|
|
|
// in theory, the before install prompt event should only happen once, so
|
|
// we don't bother canceling the forked task when another event is received
|
|
while (true) {
|
|
const event = yield* take(chan);
|
|
yield* fork(handleBeforeInstallPromptEvent, event);
|
|
yield* put(appDidReceiveBeforeInstallPrompt());
|
|
}
|
|
}
|
|
|
|
// istabul ignore next: for dev environment only
|
|
function* fakeCheckForUpdate(): Generator {
|
|
console.warn('checking for updates is not supported in development mode');
|
|
// let the "busy" indication spin for a bit
|
|
yield* delay(3000);
|
|
// then indicate we are already up-to-date
|
|
yield* put(appDidCheckForUpdate(false));
|
|
}
|
|
|
|
export default function* app(): Generator {
|
|
yield* fork(monitorServiceWorkerRegistration);
|
|
yield* fork(monitorAppInstalled);
|
|
yield* fork(monitorBeforeInstallPrompt);
|
|
|
|
// istabul ignore if: for dev environment only
|
|
if (process.env.NODE_ENV === 'development') {
|
|
yield* takeEvery(appCheckForUpdate, fakeCheckForUpdate);
|
|
}
|
|
}
|