From 93c658ba398596a908dc2c44bf8e65df86830c73 Mon Sep 17 00:00:00 2001 From: Gallay Lajos Date: Tue, 24 May 2022 14:49:28 +0200 Subject: [PATCH] pwa init --- frontend/src/components/header.tsx | 14 +++---- frontend/src/components/layout.tsx | 9 ++--- frontend/src/index.tsx | 16 ++++++++ frontend/src/pages/login.tsx | 8 ++-- frontend/src/pages/offline.tsx | 2 +- frontend/src/worker/caches/bundle-cache.ts | 11 ++++++ frontend/src/worker/caches/cache-base.ts | 18 +++++++++ frontend/src/worker/fetch-cache.ts | 44 ++++++++++++++++++++++ frontend/src/worker/index.ts | 31 +++++++++++++++ frontend/tsconfig.json | 2 +- frontend/webpack.common.js | 9 +++-- frontend/webpack.dev.js | 4 -- frontend/webpack.prod.js | 4 -- 13 files changed, 142 insertions(+), 30 deletions(-) create mode 100644 frontend/src/worker/caches/bundle-cache.ts create mode 100644 frontend/src/worker/caches/cache-base.ts create mode 100644 frontend/src/worker/fetch-cache.ts create mode 100644 frontend/src/worker/index.ts diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx index 390d381..720956c 100644 --- a/frontend/src/components/header.tsx +++ b/frontend/src/components/header.tsx @@ -1,5 +1,5 @@ import { createComponent, RouteLink, Shade } from '@furystack/shades' -import { AppBar, Button, ThemeProviderService } from '@furystack/shades-common-components' +import { AppBar, Button } from '@furystack/shades-common-components' import { environmentOptions } from '..' import { SessionService, SessionState } from '../services/session' import { GithubLogo } from './github-logo' @@ -15,18 +15,16 @@ const urlStyle: Partial = { textDecoration: 'none', } -export const Header = Shade({ +export const Header = Shade({ shadowDomName: 'shade-app-header', getInitialState: ({ injector }) => ({ sessionState: injector.getInstance(SessionService).state.getValue(), - themeProvider: injector.getInstance(ThemeProviderService), }), - constructed: ({ injector, updateState }) => { - const observable = injector.getInstance(SessionService).state.subscribe((newState) => { + resources: ({ injector, updateState }) => [ + injector.getInstance(SessionService).state.subscribe((newState) => { updateState({ sessionState: newState }) - }) - return () => observable.dispose() - }, + }), + ], render: ({ props, injector, getState }) => { return ( diff --git a/frontend/src/components/layout.tsx b/frontend/src/components/layout.tsx index b439cfc..1fff24e 100644 --- a/frontend/src/components/layout.tsx +++ b/frontend/src/components/layout.tsx @@ -5,12 +5,11 @@ import { Header } from './header' export const Layout = Shade({ shadowDomName: 'shade-app-layout', - constructed: ({ injector, element }) => { - const t = injector.getInstance(ThemeProviderService).theme.subscribe((newTheme) => { + resources: ({ injector, element }) => [ + injector.getInstance(ThemeProviderService).theme.subscribe((newTheme) => { ;(element.firstChild as any).style.background = newTheme.background.default - }) - return () => t.dispose() - }, + }), + ], render: ({ injector }) => { return (
, }) + +navigator.serviceWorker.register('/service-worker.js').then( + () => { + getLogger(shadeInjector).withScope('Worker').verbose({ + message: 'Service worker registered', + }) + }, + (err) => { + getLogger(shadeInjector) + .withScope('Worker') + .error({ + message: 'Service worker registration failed', + data: { error: err }, + }) + }, +) diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index f907aa7..f79d67f 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -51,10 +51,10 @@ export const Login = Shade<{}, { username: string; password: string; error: stri disabled={getState().isOperationInProgress} placeholder="The user's login name" value={username} - onchange={(ev) => { + onTextChange={(text) => { updateState( { - username: (ev.target as HTMLInputElement).value, + username: text, }, true, ) @@ -68,10 +68,10 @@ export const Login = Shade<{}, { username: string; password: string; error: stri placeholder="The password for the user" value={password} type="password" - onchange={(ev) => { + onTextChange={(text) => { updateState( { - password: (ev.target as HTMLInputElement).value, + password: text, }, true, ) diff --git a/frontend/src/pages/offline.tsx b/frontend/src/pages/offline.tsx index b8b9998..1dd73db 100644 --- a/frontend/src/pages/offline.tsx +++ b/frontend/src/pages/offline.tsx @@ -12,7 +12,7 @@ export const Offline = Shade({ height: '100%', alignItems: 'center', justifyContent: 'center', - padding: '0 100px', + padding: '100px', }}>
{ + const url = request.url.replace(location.origin, '') + return request.method === 'GET' && this.staticAssetList.includes(url) + } +} diff --git a/frontend/src/worker/caches/cache-base.ts b/frontend/src/worker/caches/cache-base.ts new file mode 100644 index 0000000..d68135b --- /dev/null +++ b/frontend/src/worker/caches/cache-base.ts @@ -0,0 +1,18 @@ +export abstract class CacheBase { + public async getFromCache(request: Request): Promise { + if (!this.canBeCached(request)) { + return Promise.resolve(undefined) + } + return caches.open(this.cacheKey).then((cache) => cache.match(request)) + } + + public async persist(request: Request, response: Response) { + if (!this.canBeCached(request)) { + return + } + return caches.open(this.cacheKey).then((cache) => cache.put(request, response)) + } + + abstract readonly cacheKey: string + abstract readonly canBeCached: (request: Request) => boolean +} diff --git a/frontend/src/worker/fetch-cache.ts b/frontend/src/worker/fetch-cache.ts new file mode 100644 index 0000000..ad0cccc --- /dev/null +++ b/frontend/src/worker/fetch-cache.ts @@ -0,0 +1,44 @@ +import { getLogger } from '@furystack/logging' +import { BundleCache } from './caches/bundle-cache' +import { CacheBase } from './caches/cache-base' +import { workerInjector } from './index' + +const primaryCaches: CacheBase[] = [new BundleCache()] + +const fallbackCaches: CacheBase[] = [new BundleCache()] + +export const fetchCache = async (event: FetchEvent) => { + try { + const availableCaches = primaryCaches.filter((cache) => cache.canBeCached(event.request)) + for (const cache of availableCaches) { + const result = await cache.getFromCache(event.request) + if (result) { + return result + } + } + } catch (error) { + getLogger(workerInjector).withScope('FetchCache').error({ message: 'Primary cache error ', data: { error } }) + } + + try { + const response = await fetch(event.request) + const availableFallbackCaches = fallbackCaches.filter((cache) => cache.canBeCached(event.request)) + await Promise.all(availableFallbackCaches.map((c) => c.persist(event.request, response.clone()))) + return response + } catch (error) { + if (!navigator.onLine) { + throw error + } + const cached = await caches.match(event.request) + if (cached) { + return cached + } + throw error + } +} + +export const clearCache = async () => { + // TODO + // const cache = await caches.open(CACHE_KEY) + // await cache.delete(CACHE_KEY) +} diff --git a/frontend/src/worker/index.ts b/frontend/src/worker/index.ts new file mode 100644 index 0000000..41b809f --- /dev/null +++ b/frontend/src/worker/index.ts @@ -0,0 +1,31 @@ +/// + +// Default type of `self` is `WorkerGlobalScope & typeof globalThis` +// https://github.com/microsoft/TypeScript/issues/14877 +declare const self: ServiceWorkerGlobalScope + +import { Injector } from '@furystack/inject' +import { getLogger, useLogging, VerboseConsoleLogger } from '@furystack/logging' +import { clearCache, fetchCache } from './fetch-cache' + +export const workerInjector = new Injector() +useLogging(workerInjector, VerboseConsoleLogger) + +self.addEventListener('install', () => { + getLogger(workerInjector).withScope('Worker').verbose({ message: 'Installing Worker' }) +}) + +self.addEventListener('update', () => { + getLogger(workerInjector).withScope('Worker').verbose({ message: 'Updating Worker' }) +}) + +self.addEventListener('push', (ev) => { + console.log(ev) + if (ev.data?.text() === 'logOut') { + return ev.waitUntil(clearCache()) + } +}) + +self.addEventListener('fetch', (event) => { + event.respondWith(fetchCache(event)) +}) diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 12b02ce..e8ac44f 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -3,7 +3,7 @@ /* Basic Options */ "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, - // "lib": [], /* Specify library files to be included in the compilation. */ + // "lib": [] /* Specify library files to be included in the compilation. */, // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js index 4765e77..7efc9cd 100644 --- a/frontend/webpack.common.js +++ b/frontend/webpack.common.js @@ -5,10 +5,13 @@ const frontendPackage = require('./package.json') const rootPackage = require('../package.json') module.exports = { - entry: './src/index.tsx', + entry: { app: './src/index.tsx', 'service-worker': './src/worker/index.ts' }, output: { - publicPath: '/', - path: path.resolve(`${__dirname}/bundle`), + filename: '[name].js', + chunkFilename: '[name].[contenthash:8].chunk.js', + // publicPath: '/', + // filename: '[name].js', + // path: path.resolve(`${__dirname}/bundle`), }, resolve: { extensions: ['.ts', '.tsx', '.js', '.json'], diff --git a/frontend/webpack.dev.js b/frontend/webpack.dev.js index bdb1ded..bf245be 100644 --- a/frontend/webpack.dev.js +++ b/frontend/webpack.dev.js @@ -10,10 +10,6 @@ module.exports = merge(common, { devServer: { historyApiFallback: true, }, - output: { - filename: 'static/js/bundle.js', - chunkFilename: 'static/js/[name].chunk.js', - }, plugins: [ new ForkTsCheckerWebpackPlugin(), new HtmlWebpackPlugin({ diff --git a/frontend/webpack.prod.js b/frontend/webpack.prod.js index 8cc5d1d..925db30 100644 --- a/frontend/webpack.prod.js +++ b/frontend/webpack.prod.js @@ -13,10 +13,6 @@ const common = require('./webpack.common.js') module.exports = merge(common, { mode: 'production', devtool: 'source-map', - output: { - filename: '[name].[contenthash:8].js', - chunkFilename: '[name].[contenthash:8].chunk.js', - }, optimization: { minimize: true, usedExports: true,