Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions frontend/src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -15,18 +15,16 @@ const urlStyle: Partial<CSSStyleDeclaration> = {
textDecoration: 'none',
}

export const Header = Shade<HeaderProps, { sessionState: SessionState; themeProvider: ThemeProviderService }>({
export const Header = Shade<HeaderProps, { sessionState: SessionState }>({
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 (
<AppBar id="header">
Expand Down
9 changes: 4 additions & 5 deletions frontend/src/components/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,19 @@ initializeShadeRoot({
rootElement,
jsxElement: <Layout />,
})

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 },
})
},
)
8 changes: 4 additions & 4 deletions frontend/src/pages/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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,
)
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/offline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const Offline = Shade({
height: '100%',
alignItems: 'center',
justifyContent: 'center',
padding: '0 100px',
padding: '100px',
}}>
<div
id="offline"
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/worker/caches/bundle-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CacheBase } from './cache-base'

export class BundleCache extends CacheBase {
private staticAssetList = ['/', '/favicon.ico', '/app.js']

public readonly cacheKey = 'bundle-cache'
public readonly canBeCached = (request: Request) => {
const url = request.url.replace(location.origin, '')
return request.method === 'GET' && this.staticAssetList.includes(url)
}
}
18 changes: 18 additions & 0 deletions frontend/src/worker/caches/cache-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export abstract class CacheBase {
public async getFromCache(request: Request): Promise<Response | undefined> {
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
}
44 changes: 44 additions & 0 deletions frontend/src/worker/fetch-cache.ts
Original file line number Diff line number Diff line change
@@ -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)
}
31 changes: 31 additions & 0 deletions frontend/src/worker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// <reference lib="webworker" />

// 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))
})
2 changes: 1 addition & 1 deletion frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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'. */,
Expand Down
9 changes: 6 additions & 3 deletions frontend/webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
4 changes: 0 additions & 4 deletions frontend/webpack.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
4 changes: 0 additions & 4 deletions frontend/webpack.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down