Files
pybricks-code/src/app/sagas.ts
T
David Lechner 1469d2146e app: rework service worker registration
- registration is moved from top-level index to app/sagas.
- mock implementation is provided for tests
- actions are renamed to include "app" prefix
- actions are changed to not use non-serializable arguments
- reducers are changed to not use non-serializable state
2022-03-09 15:48:30 -06:00

129 lines
4.2 KiB
TypeScript

// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2022 The Pybricks Authors
import { eventChannel } from 'redux-saga';
import { call, fork, put, take, takeEvery } from 'typed-redux-saga/macro';
import {
serviceWorkerDidSucceed,
serviceWorkerDidUpdate,
} from '../service-worker/actions';
import * as serviceWorkerRegistration from '../serviceWorkerRegistration';
import { BeforeInstallPromptEvent } from '../utils/dom';
import {
appCheckForUpdate,
appDidCheckForUpdate,
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 {
yield* call(() => registration.update());
const updateFound = registration.installing !== null;
yield* put(appDidCheckForUpdate(updateFound));
}
/**
* 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());
}
}
export default function* app(): Generator {
yield* fork(monitorServiceWorkerRegistration);
yield* fork(monitorAppInstalled);
yield* fork(monitorBeforeInstallPrompt);
}