Skip to content

Commit 8b43ce7

Browse files
authored
Truncate caches when update is available (#241)
* feat: add notification action support * feat: check for updates
1 parent c2f05db commit 8b43ce7

File tree

9 files changed

+223
-5
lines changed

9 files changed

+223
-5
lines changed

web/src/components/modals/Notification/Notification.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@
3232
margin-bottom: .4rem;
3333
}
3434

35+
.Notification__Actions {
36+
margin-top: 1rem;
37+
}

web/src/components/modals/Notification/Notification.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import React from "react";
2-
import {IconButton, ISemanticColors, ProgressIndicator, useTheme} from "@fluentui/react";
2+
import {
3+
Stack,
4+
IconButton,
5+
ISemanticColors,
6+
ProgressIndicator,
7+
DefaultButton,
8+
PrimaryButton,
9+
useTheme,
10+
} from "@fluentui/react";
311
import { FontIcon } from "@fluentui/react/lib/Icon";
412

513
import "./Notification.css"
@@ -17,6 +25,13 @@ interface ProgressState {
1725
current?: number
1826
}
1927

28+
interface NotificationAction {
29+
label: string
30+
key: string
31+
primary?: boolean
32+
onClick?: () => void
33+
}
34+
2035
export interface NotificationProps {
2136
id: number|string
2237
type?: NotificationType
@@ -25,6 +40,7 @@ export interface NotificationProps {
2540
canDismiss?: boolean
2641
progress?: ProgressState
2742
onClose?: () => void
43+
actions?: NotificationAction[]
2844
}
2945

3046
const iconColorPaletteMap: {[k in NotificationType]: keyof ISemanticColors} = {
@@ -51,6 +67,19 @@ const getPercentComplete = (progress: NotificationProps['progress']): (number|un
5167
return percentage / 100;
5268
}
5369

70+
const NotificationActionButton: React.FC<Omit<NotificationAction, 'key'>> = (
71+
{label, primary, onClick}
72+
) => {
73+
const ButtonComponent = primary ? PrimaryButton : DefaultButton;
74+
return (
75+
<ButtonComponent
76+
primary={primary}
77+
onClick={onClick}
78+
text={label}
79+
/>
80+
);
81+
}
82+
5483
const Notification: React.FunctionComponent<NotificationProps> = ({
5584
id,
5685
title,
@@ -59,6 +88,7 @@ const Notification: React.FunctionComponent<NotificationProps> = ({
5988
canDismiss = true,
6089
type = NotificationType.Info,
6190
onClose,
91+
actions
6292
}) => {
6393
const {semanticColors, fonts, ...theme} = useTheme();
6494
return (
@@ -121,6 +151,22 @@ const Notification: React.FunctionComponent<NotificationProps> = ({
121151
)}
122152
</div>
123153
)}
154+
{ actions?.length && (
155+
<Stack
156+
horizontal
157+
className="Notification__Actions"
158+
horizontalAlign="end"
159+
tokens={
160+
{ childrenGap: 10 }
161+
}
162+
>
163+
{
164+
actions.map(({key, ...props}, i) => (
165+
<NotificationActionButton key={key} {...props} />
166+
))
167+
}
168+
</Stack>
169+
)}
124170

125171
</div>
126172
);

web/src/components/pages/Playground.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import React, {useEffect} from 'react';
22
import { useParams } from 'react-router-dom';
33
import { connect } from 'react-redux';
44

5-
import { dispatchPanelLayoutChange, newSnippetLoadDispatcher} from '~/store';
5+
import {
6+
dispatchPanelLayoutChange,
7+
dispatchUpdateCheck,
8+
newSnippetLoadDispatcher,
9+
} from '~/store';
610
import { Header } from '~/components/core/Header';
711
import CodeEditor from '~/components/editor/CodeEditor';
812
import FlexContainer from '~/components/editor/FlexContainer';
@@ -18,6 +22,10 @@ const CodeContainer = connect()(({dispatch}: any) => {
1822
useEffect(() => {
1923
dispatch(newSnippetLoadDispatcher(snippetID));
2024
}, [snippetID, dispatch]);
25+
26+
useEffect(() => {
27+
dispatch(dispatchUpdateCheck)
28+
}, [dispatch]);
2129
return (
2230
<CodeEditor />
2331
);
@@ -33,7 +41,9 @@ const Playground = connect(({panel}: any) => ({panelProps: panel}))(({panelProps
3341
</FlexContainer>
3442
<ResizablePreview
3543
{...panelProps}
36-
onViewChange={changes => dispatch(dispatchPanelLayoutChange(changes))}
44+
onViewChange={changes => {
45+
dispatch(dispatchPanelLayoutChange(changes))
46+
}}
3747
/>
3848
<NotificationHost />
3949
</Layout>

web/src/serviceWorkerRegistration.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,33 @@ export function register(config?: Config) {
6161
}
6262
}
6363

64+
/**
65+
* Manually registers service worker
66+
*
67+
* @param config
68+
*/
69+
export function manualRegister(config?: Config) {
70+
if (process.env.NODE_ENV !== 'production') {
71+
return;
72+
}
73+
if (!('serviceWorker' in navigator)) {
74+
return;
75+
}
76+
77+
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
78+
if (publicUrl.origin !== window.location.origin) {
79+
return;
80+
}
81+
82+
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
83+
if (isLocalhost) {
84+
checkValidServiceWorker(swUrl, config);
85+
return;
86+
}
87+
88+
registerValidSW(swUrl, config);
89+
}
90+
6491
function registerValidSW(swUrl: string, config?: Config) {
6592
navigator.serviceWorker
6693
.register(swUrl)

web/src/services/updates.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import client from '~/services/api';
2+
import environment from "~/environment";
3+
4+
export interface UpdateInfo {
5+
newVersion: string
6+
}
7+
8+
/**
9+
* Checks for application updates and returns new available version.
10+
*
11+
* Returns null if no updates available.
12+
*/
13+
export const checkUpdates = async (): Promise<UpdateInfo|null> => {
14+
if (!window.navigator.onLine) {
15+
console.log('updater: application is offline, skip.');
16+
return null;
17+
}
18+
19+
const { version: newVersion } = await client.getVersion();
20+
const { appVersion } = environment;
21+
if (newVersion === appVersion) {
22+
console.log('updater: app is up to date:', newVersion);
23+
return null;
24+
}
25+
26+
console.log(`updater: new update is available - ${newVersion}`);
27+
return {
28+
newVersion: newVersion
29+
}
30+
31+
// if (!('serviceWorker' in navigator)) {
32+
// console.warn('updater: no SW registrations found, skip');
33+
// return;
34+
// }
35+
//
36+
// const registrations = await navigator.serviceWorker.getRegistrations();
37+
// if (!registrations?.length) {
38+
// console.warn('updater: no SW registrations found, skip');
39+
// return;
40+
// }
41+
42+
43+
// await truncateCachesAndRegister(registrations);
44+
}
45+
46+
// const truncateCachesAndRegister = async (registrations: readonly ServiceWorkerRegistration[]) => {
47+
// console.log(`updater: unregistering ${registrations.length} service workers...`);
48+
// await Promise.all(registrations.map(r => r.unregister()));
49+
//
50+
// console.log('updater: truncating caches', caches.keys());
51+
//
52+
// for (const sw of registrations) {
53+
// const ok = await sw.unregister();
54+
// if (!ok) {
55+
// console.
56+
// }
57+
// }
58+
// console.log('updater: truncating all caches');
59+
// }

web/src/store/actions/ui.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { ActionType} from "./actions";
2-
32
import { UIState } from "../state";
43

54
export interface LoadingStateChanges {

web/src/store/dispatchers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './snippets';
22
export * from './settings';
3+
export * from './updates';
34
export * from './build';
45
export * from './utils';
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {manualRegister} from '~/serviceWorkerRegistration';
2+
import {newAddNotificationAction, NotificationType} from "~/store/notifications";
3+
import {checkUpdates} from "~/services/updates";
4+
5+
import {Dispatcher} from "./utils";
6+
import {DispatchFn} from "../helpers";
7+
8+
const truncateCachesAndRegister = async (registration: ServiceWorkerRegistration) => {
9+
console.log(`updater: unregistering service worker...`);
10+
await registration.unregister();
11+
12+
console.log('updater: truncating all caches');
13+
const keys = await caches.keys();
14+
await Promise.all(keys.map(key => caches.delete(key)));
15+
16+
console.log('updater: re-registering service worker...');
17+
manualRegister();
18+
}
19+
20+
const performSelfUpdate = async (dispatch: DispatchFn, newVersion: string)=> {
21+
if (!('serviceWorker' in navigator)) {
22+
console.warn('updater: no SW registrations found, skip');
23+
return;
24+
}
25+
26+
const registration = await navigator.serviceWorker.getRegistration();
27+
if (!registration) {
28+
console.warn('updater: no SW registrations found, skip');
29+
return;
30+
}
31+
32+
await truncateCachesAndRegister(registration);
33+
dispatch(newAddNotificationAction({
34+
id: 'UPDATE',
35+
type: NotificationType.Warning,
36+
title: `Update available - ${newVersion}`,
37+
description: 'Playground was updated. Please refresh page to apply changes.',
38+
canDismiss: false,
39+
actions: [
40+
{
41+
key: 'refresh',
42+
label: 'Refresh',
43+
primary: true,
44+
onClick: () => {
45+
window.location.replace("");
46+
}
47+
}
48+
]
49+
}));
50+
}
51+
52+
export const dispatchUpdateCheck: Dispatcher = async (dispatch: DispatchFn) => {
53+
try {
54+
const rsp = await checkUpdates();
55+
if (!rsp) {
56+
return;
57+
}
58+
59+
try {
60+
await performSelfUpdate(dispatch, rsp.newVersion);
61+
} catch (err) {
62+
console.error('updater: error during update:', err);
63+
}
64+
} catch (err) {
65+
console.error('updater: failed to check for updates', err);
66+
}
67+
}

web/src/store/notifications/state.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ export interface Notification {
1515
indeterminate?: boolean
1616
total?: number
1717
current?: number
18-
}
18+
},
19+
actions?: {
20+
label: string,
21+
key: string,
22+
primary?: boolean,
23+
onClick?: () => void,
24+
}[]
1925
}
2026

2127
export type NotificationsState = {[k: string]: Notification};

0 commit comments

Comments
 (0)