Skip to content

Commit

Permalink
Merge pull request #476 from powerhouse-inc/455-force-page-refresh-wh…
Browse files Browse the repository at this point in the history
…en-a-new-version-is-deployed

feat: added browser refresh when a new version of connect is deployed
  • Loading branch information
gpuente authored Aug 12, 2024
2 parents 4924257 + c2255c9 commit 65c31a1
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 8 deletions.
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ VITE_DEFAULT_DRIVES_URL=https://apps.powerhouse.io/staging/makerdao/switchboard/
VITE_BASE_HREF=/
ASSET_URL=/

VITE_APP_REQUIRES_HARD_REFRESH=true

######## RENOWN CONFIG ########
# Renown instance to use for login
# VITE_RENOWN_URL=http://localhost:3000
Expand Down
55 changes: 55 additions & 0 deletions public/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const VERSION_CACHE = 'version-cache';
const VERSION_KEY = 'app-version';

self.addEventListener('install', event => {
self.skipWaiting();
});

self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim());
});


self.addEventListener('message', async event => {
if (event.data && event.data.type === 'SET_APP_VERSION') {
const cache = await caches.open(VERSION_CACHE);
await cache.put(VERSION_KEY, new Response(event.data.version));
}
});

async function checkForUpdates() {
try {
const response = await fetch('/version.json', { cache: 'no-store' });
const newVersion = await response.json();
const cache = await caches.open(VERSION_CACHE);
const cachedResponse = await cache.match(VERSION_KEY);

let currentVersion = '';

if (cachedResponse) {
currentVersion = await cachedResponse.text();
}

if (currentVersion === '') {
// Initial cache
await cache.put(VERSION_KEY, new Response(newVersion.version));
} else if (currentVersion !== newVersion.version) {
// New version detected
console.log('Current version:', currentVersion);
console.log('New version:', newVersion.version);

const clients = await self.clients.matchAll();
clients.forEach(client => {
client.postMessage({ type: 'NEW_VERSION_AVAILABLE', requiresHardRefresh: newVersion.requiresHardRefresh });
});

// Update the stored version
await cache.put(VERSION_KEY, new Response(newVersion.version));
}
} catch (error) {
console.error('Error checking version:', error);
}
}

// Check for updates every minute
setInterval(checkForUpdates, 60 * 1000); // 60 seconds
86 changes: 85 additions & 1 deletion src/components/modal/modals/DebugSettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
Icon,
Modal,
} from '@powerhousedao/design-system';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useDocumentDriveServer } from 'src/hooks/useDocumentDriveServer';
import serviceWorkerManager from 'src/utils/registerServiceWorker';
import { v4 as uuid } from 'uuid';

export interface DebugSettingsModalProps {
Expand All @@ -26,6 +27,11 @@ export const DebugSettingsModal: React.FC<DebugSettingsModalProps> = props => {

console.log('autoRegisterPullResponder', autoRegisterPullResponder);

const [appVersion, setAppVersion] = useState('');
const [serviceWorkerDebugMode, setServiceWorkerDebugMode] = useState({
label: serviceWorkerManager.debug ? 'Enabled' : 'Disabled',
value: serviceWorkerManager.debug,
});
const [selectedDrive, setSelectedDrive] = useState<string>();
const [selectedDriveTrigger, setSelectedDriveTrigger] =
useState<ComboboxOption | null>(null);
Expand All @@ -41,6 +47,10 @@ export const DebugSettingsModal: React.FC<DebugSettingsModalProps> = props => {
registerNewPullResponderTrigger,
} = useDocumentDriveServer();

useEffect(() => {
serviceWorkerManager.setDebug(serviceWorkerDebugMode.value);
}, [serviceWorkerDebugMode]);

console.log('documentDrives', documentDrives);
console.log('selectedDrive', selectedDrive);

Expand Down Expand Up @@ -114,6 +124,13 @@ export const DebugSettingsModal: React.FC<DebugSettingsModalProps> = props => {
</div>

<div className="flex text-sm font-bold">
<Icon name="Ring" size={22} />
<span className="ml-2">
App Version: {process.env.APP_VERSION}
</span>
</div>

<div className="flex text-sm font-bold mt-4">
<Icon name="Hdd" size={22} />
<span className="ml-2">Drive Tools:</span>
</div>
Expand Down Expand Up @@ -240,6 +257,73 @@ export const DebugSettingsModal: React.FC<DebugSettingsModalProps> = props => {
</Button>
</div>
</div>

<div className="flex text-sm font-bold mt-4">
<Icon name="Gear" size={22} />
<span className="ml-2">Service Worker Tools:</span>
</div>

<div className="mt-2 flex items-end justify-between pl-4">
<div className="w-[400px]">
<label
htmlFor="serviceWorkerDebugMode"
className="text-xs"
>
Service Worker Debug Mode:
</label>
<Combobox
id="serviceWorkerDebugMode"
onChange={value => {
setServiceWorkerDebugMode(
value as typeof serviceWorkerDebugMode,
);
}}
value={serviceWorkerDebugMode}
options={[
{ label: 'Enabled', value: true },
{ label: 'Disabled', value: false },
]}
/>
</div>
</div>

<div className="mt-2 flex items-end justify-between pl-4">
<div className="w-[400px]">
<label htmlFor="appVersion" className="text-xs">
Set invalid app version:
</label>
<FormInput
containerClassName="p-1 bg-white border border-gray-200 rounded-md text-sm"
inputClassName="text-xs font-normal"
id="appVersion"
icon={
<div className="flex h-full items-center text-xs">
Version:
</div>
}
value={appVersion}
onChange={element =>
setAppVersion(element.target.value)
}
/>
</div>
<div className="mb-1 flex items-center justify-center">
<Button
color={appVersion === '' ? 'light' : 'red'}
size="small"
disabled={appVersion === ''}
onClick={() => {
serviceWorkerManager.sendMessage({
type: 'SET_APP_VERSION',
version: appVersion,
});
setAppVersion('');
}}
>
Add Invalid App Version
</Button>
</div>
</div>
</div>
</Modal>
);
Expand Down
3 changes: 3 additions & 0 deletions src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ import App from './components/app';
import './i18n';
import './index.css';
import { DocumentEditorDebugTools } from './utils/document-editor-debug-tools';
import serviceWorkerManager from './utils/registerServiceWorker';

if (import.meta.env.MODE === 'development') {
window.documentEditorDebugTools = new DocumentEditorDebugTools();
} else {
serviceWorkerManager.registerServiceWorker(false);
}

createRoot(document.getElementById('app')!).render(App);
110 changes: 110 additions & 0 deletions src/utils/registerServiceWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
type SET_APP_VERSION_MESSAGE = {
type: 'SET_APP_VERSION';
version: string;
};

export type ServiceWorkerPostMessage = SET_APP_VERSION_MESSAGE;

type NEW_VERSION_AVAILABLE_MESSAGE = {
type: 'NEW_VERSION_AVAILABLE';
requiresHardRefresh: boolean;
};

export type ServiceWorkerMessage = NEW_VERSION_AVAILABLE_MESSAGE;

class ServiceWorkerManager {
ready = false;
debug = false;
registration: ServiceWorkerRegistration | null = null;

constructor(debug = false) {
this.debug = debug;
}

setDebug(debug: boolean) {
this.debug = debug;
}

registerServiceWorker(debug = false) {
this.debug = debug;

if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then(registration => {
// Listen for messages from the service worker
if (this.debug) {
console.log(
'ServiceWorker registered: ',
registration,
);
}

navigator.serviceWorker.addEventListener(
'message',
event => {
if (this.debug) {
console.log(
'ServiceWorker message: ',
event,
);
}

if (
event.data &&
event.data.type ===
'NEW_VERSION_AVAILABLE' &&
event.data.requiresHardRefresh === true
) {
if (this.debug) {
console.log('New version available');
}
window.location.reload(); // Reload the page to load the new version
}
},
);

if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'SET_APP_VERSION',
version: process.env.APP_VERSION,
});
}

this.ready = true;
this.registration = registration;
})
.catch(error => {
console.error(
'ServiceWorker registration failed: ',
error,
);
});
});
}
}

sendMessage(message: ServiceWorkerPostMessage) {
if (this.ready && this.registration) {
const serviceWorker =
this.registration.active ||
this.registration.waiting ||
this.registration.installing;
if (serviceWorker) {
if (this.debug) {
console.log('Sending message to service worker: ', message);
}
serviceWorker.postMessage(message);
} else {
console.error('No active service worker found.');
}
} else {
console.error('Service Worker is not ready yet.');
}
}
}

const serviceWorkerManager = new ServiceWorkerManager();
export default serviceWorkerManager;
49 changes: 42 additions & 7 deletions vite.renderer.config.mts
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
import react from '@vitejs/plugin-react';
import fs from 'fs';
import jotaiDebugLabel from 'jotai/babel/plugin-debug-label';
import jotaiReactRefresh from 'jotai/babel/plugin-react-refresh';
import path from 'path';
import { HtmlTagDescriptor, defineConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
import { HtmlTagDescriptor, defineConfig, loadEnv } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
import svgr from 'vite-plugin-svgr';
import pkg from './package.json';

import clientConfig from './client.config';

const appVersion = pkg.version;

const generateVersionPlugin = (hardRefresh = false) => {
return {
name: 'generate-version',
closeBundle() {
const versionManifest = {
version: appVersion,
requiresHardRefresh: hardRefresh,
};

fs.writeFileSync(
path.join('dist', 'version.json'),
JSON.stringify(versionManifest, null, 2),
);
},
};
};

export default defineConfig(({ mode }) => {
const isProd = mode === "production";
const isProd = mode === 'production';
const env = loadEnv(mode, process.cwd());

const requiresHardRefresh = env.VITE_APP_REQUIRES_HARD_REFRESH === 'true';

return {
define: {
'process.env': {
APP_VERSION: appVersion,
REQUIRES_HARD_REFRESH: isProd ? requiresHardRefresh : false,
},
},
plugins: [
react({
include: 'src/**/*.tsx',
Expand All @@ -23,13 +54,14 @@ export default defineConfig(({ mode }) => {
minify: true,
inject: {
tags: [
...clientConfig.meta.map((meta) => ({
...(clientConfig.meta.map(meta => ({
...meta,
injectTo: 'head',
})) as HtmlTagDescriptor[],
]
})) as HtmlTagDescriptor[]),
],
},
}),
generateVersionPlugin(isProd ? requiresHardRefresh : false),
],
build: {
minify: isProd,
Expand All @@ -39,7 +71,10 @@ export default defineConfig(({ mode }) => {
alias: {
'@/assets': path.resolve(__dirname, './assets'),
src: path.resolve(__dirname, './src'),
'connect-config': path.resolve(__dirname, './connect.config.ts'),
'connect-config': path.resolve(
__dirname,
'./connect.config.ts',
),
path: 'rollup-plugin-node-polyfills/polyfills/path',
events: 'rollup-plugin-node-polyfills/polyfills/events',
},
Expand Down

0 comments on commit 65c31a1

Please sign in to comment.