Skip to content

Commit 001ec63

Browse files
authored
Merge 8a9195a into a11b226
2 parents a11b226 + 8a9195a commit 001ec63

File tree

5 files changed

+172
-11
lines changed

5 files changed

+172
-11
lines changed

backend/routes.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ route.GET('/kv/{contractID}/{key}', {
746746
return notFoundNoCache(h)
747747
}
748748

749-
return h.response(result).etag(createCID(result, multicodes.RAW))
749+
return h.response(result).etag(createCID(result, multicodes.RAW)).type('application/json')
750750
})
751751

752752
// SPA routes
@@ -781,9 +781,13 @@ route.GET('/assets/{subpath*}', {
781781
.etag(basename)
782782
.header('Cache-Control', 'public,max-age=31536000,immutable')
783783
}
784-
// Files like `main.js` or `main.css` should be revalidated before use. Se we use the default headers.
784+
// Files like `main.js` or `main.css` should be revalidated before use.
785+
// We set a short 'stale-while-revalidate' value instead of 'no-cache' to
786+
// signal to the app that it's fine to use old versions when offline or over
787+
// unreliable connections.
785788
// This should also be suitable for serving unversioned fonts and images.
786789
return h.file(subpath)
790+
.header('Cache-Control', 'public,max-age=604800,stale-while-revalidate=86400')
787791
})
788792

789793
route.GET(staticServeConfig.routePath, {}, {
@@ -851,7 +855,13 @@ route.GET('/zkpp/{name}/auth_hash', {
851855
try {
852856
const challenge = await getChallenge(req.params['name'], req.query['b'])
853857

854-
return challenge || notFoundNoCache(h)
858+
if (!challenge) {
859+
return Boom.notFound()
860+
}
861+
862+
return h.response(challenge)
863+
.header('Cache-Control', 'no-store')
864+
.header('Content-Type', 'text/plain')
855865
} catch (e) {
856866
e.ip = req.headers['x-real-ip'] || req.info.remoteAddress
857867
console.error(e, 'Error at GET /zkpp/{name}/auth_hash: ' + e.message)
@@ -875,7 +885,9 @@ route.GET('/zkpp/{name}/contract_hash', {
875885
const salt = await getContractSalt(req.params['name'], req.query['r'], req.query['s'], req.query['sig'], req.query['hc'])
876886

877887
if (salt) {
878-
return salt
888+
return h.response(salt)
889+
.header('Cache-Control', 'no-store')
890+
.header('Content-Type', 'text/plain')
879891
}
880892
} catch (e) {
881893
e.ip = req.headers['x-real-ip'] || req.info.remoteAddress
@@ -901,7 +913,8 @@ route.POST('/zkpp/{name}/updatePasswordHash', {
901913
const result = await updateContractSalt(req.params['name'], req.payload['r'], req.payload['s'], req.payload['sig'], req.payload['hc'], req.payload['Ea'])
902914

903915
if (result) {
904-
return result
916+
return h.response(result)
917+
.header('Content-type', 'text/plain')
905918
}
906919
} catch (e) {
907920
e.ip = req.headers['x-real-ip'] || req.info.remoteAddress

frontend/controller/actions/group.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ export default (sbp('sbp/selectors/register', {
403403
removeEventHandler()
404404
removeLogoutHandler()
405405
// The event handler recursively calls this same selector
406-
// A different path should be taken, since te event handler
406+
// A different path should be taken, since the event handler
407407
// should be called after the key request has been answered
408408
// and processed
409409
sbp('gi.actions/group/join', params).catch((e) => {

frontend/controller/app/identity.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,12 @@ export default (sbp('sbp/selectors/register', {
419419
})
420420
} else {
421421
try {
422-
await sbp('chelonia/contract/sync', identityContractID)
422+
if (navigator.onLine !== false) {
423+
await Promise.race([
424+
sbp('chelonia/contract/sync', identityContractID),
425+
new Promise((resolve) => { setTimeout(resolve, 5000) })
426+
])
427+
}
423428
} catch (e) {
424429
// Since we're throwing or returning, the `await` below will not
425430
// be used. In either case, login won't complete after this point,
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
const CACHE_VERSION = process.env.GI_VERSION
2+
const CURRENT_CACHES = {
3+
assets: `assets-cache_v${CACHE_VERSION}`
4+
}
5+
6+
if (
7+
typeof Cache === 'function' &&
8+
typeof CacheStorage === 'function' &&
9+
typeof caches === 'object' &&
10+
(caches instanceof CacheStorage)
11+
) {
12+
const locationUrl = new URL(self.location)
13+
const routerBase = locationUrl.searchParams.get('routerBase') ?? '/app'
14+
15+
self.addEventListener('install', (event) => {
16+
event.waitUntil(
17+
caches
18+
.open(CURRENT_CACHES.assets)
19+
.then((cache) =>
20+
cache.addAll([
21+
'/assets/pwa-manifest.webmanifest',
22+
'/assets/images/group-income-icon-transparent.png',
23+
'/assets/images/pwa-icons/group-income-icon-maskable_192x192.png',
24+
'/assets/css/main.css',
25+
'/assets/js/main.js',
26+
`${routerBase}/`
27+
]).catch(e => {
28+
console.error('Error adding initial entries to cache', e)
29+
})
30+
)
31+
)
32+
}, false)
33+
34+
// Taken from the MDN example:
35+
// <https://developer.mozilla.org/en-US/docs/Web/API/Cache>
36+
self.addEventListener('activate', (event) => {
37+
const expectedCacheNamesSet = new Set(Object.values(CURRENT_CACHES))
38+
39+
event.waitUntil(
40+
caches.keys().then((cacheNames) =>
41+
Promise.allSettled(
42+
cacheNames.map((cacheName) => {
43+
if (!expectedCacheNamesSet.has(cacheName)) {
44+
// If this cache name isn't present in the set of
45+
// "expected" cache names, then delete it.
46+
console.log('Deleting out of date cache:', cacheName)
47+
return caches.delete(cacheName)
48+
}
49+
50+
return undefined
51+
})
52+
)
53+
)
54+
)
55+
}, false)
56+
57+
self.addEventListener('fetch', function (event) {
58+
console.debug(`[sw] fetch : ${event.request.method} - ${event.request.url}`)
59+
60+
if (!['GET', 'HEAD', 'OPTIONS'].includes(event.request.method)) {
61+
return
62+
}
63+
64+
// Needed mostly for Cypress
65+
let cacheKey = event.request
66+
try {
67+
const url = new URL(event.request.url)
68+
69+
if (url.origin !== self.location.origin) {
70+
return
71+
}
72+
73+
if (
74+
['/eventsAfter/', '/name/', '/latestHEADinfo/', '/file/', '/kv/', '/zkpp/'].some(prefix => url.pathname.startsWith(prefix)) ||
75+
url.pathname === '/time'
76+
) {
77+
return
78+
}
79+
80+
// If the route starts with `${routerBase}/` or `/`, redirect to
81+
// `${routerBase}/`, since the HTML content is presumed to be the same.
82+
// This is _crucial_ for the offline PWA to work, since currently the app
83+
// uses different paths.
84+
if (
85+
url.pathname === '/' ||
86+
(url.pathname.startsWith(`${routerBase}/`) && url.pathname !== `${routerBase}/`)
87+
) {
88+
if (window.Cypress) {
89+
// Apparently, Cypress doesn't like redirects, so we rewrite the
90+
// cache key instead
91+
cacheKey = new Request(`${routerBase}/`)
92+
} else {
93+
event.respondWith(Response.redirect(`${routerBase}/`, 302))
94+
return
95+
}
96+
}
97+
} catch (e) {
98+
return
99+
}
100+
101+
event.respondWith(
102+
caches.match(event.request, { ignoreSearch: true, ignoreVary: true })
103+
.then((cachedResponse) => {
104+
// If a cached response exists, return it. Not only does this
105+
// improve performance, but it also makes the app work 'offline'
106+
// (`navigator.onLine` is unreliable; can be `true` even when
107+
// offline). The downside of this approach is that we may return
108+
// stale assets when the app is updated. Fortunately, so long as the
109+
// version is updated (GI_VERSION), existing cache entries will be
110+
// deleted. This will happen with SW updates, so, ideally, we won't
111+
// serve stale resources for too long.
112+
if (cachedResponse) {
113+
return cachedResponse
114+
}
115+
116+
// We use the original `event.request` for the network fetch
117+
// instead of the possibly re-written `request` (used as a cache
118+
// key) because the re-written request makes tests fail.
119+
return caches.open(CURRENT_CACHES.assets).then((cache) => {
120+
return fetch(event.request).then((response) => {
121+
if (
122+
// Save successful reponses
123+
response.status >= 200 &&
124+
response.status < 400 &&
125+
response.status !== 206 && // Partial response
126+
response.status !== 304 && // Not modified
127+
// Which don't have a 'no-store' directive
128+
!response.headers.get('cache-control')?.split(',').some(x => x.trim() === 'no-store')
129+
) {
130+
event.waitUntil(cache.put(cacheKey, response.clone()).catch(e => {
131+
console.error('Error adding request to cache')
132+
}))
133+
} else if (response.status < 500) {
134+
// For 5xx responses (server errors, we don't delete the cache
135+
// entry. This is so that, in the event of a 5xx error,
136+
// the offline app still works.)
137+
event.waitUntil(cache.delete(cacheKey))
138+
}
139+
140+
return response
141+
})
142+
})
143+
})
144+
)
145+
}, false)
146+
}

frontend/controller/serviceworkers/sw-primary.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
NOTIFICATION_STATUS_LOADED, OFFLINE, ONLINE, RECONNECTING,
3434
RECONNECTION_FAILED, SERIOUS_ERROR, SWITCH_GROUP
3535
} from '../../utils/events.js'
36+
import './cache.js'
3637
import './push.js'
3738
import './sw-namespace.js'
3839

@@ -338,10 +339,6 @@ self.addEventListener('activate', function (event) {
338339
event.waitUntil(self.clients.claim())
339340
})
340341

341-
self.addEventListener('fetch', function (event) {
342-
console.debug(`[sw] fetch : ${event.request.method} - ${event.request.url}`)
343-
})
344-
345342
// TODO: this doesn't persist data across browser restarts, so try to use
346343
// the cache instead, or just localstorage. Investigate whether the service worker
347344
// has the ability to access and clear the localstorage periodically.

0 commit comments

Comments
 (0)