Appearance
@aeppic/service-worker
The service worker project compiles into a javascript file which can be used as a service worker for aeppic installations.
Usage
The default behavior in aeppic is to load a service-worker Layout when rendering ae-root
which triggers the loading of the service worker from a Preference document.
Caching Strategy
The service worker caches all files returned by /api/v4/statics/_list
.
All files listed in there with http://
or https://
are seens as external and cached with no-cors mode.
This actual implementation is here:
ts
async function cacheStaticFiles(cacheName: string, endpointUrl: string): Promise<string | undefined> {
const lastStaticFileListCachedEtag = await get(cacheName, 'lastStaticFileListCached')
try {
const cache = await caches.open(cacheName)
const response = await fetch(endpointUrl)
if (!response.ok) {
fail({ status: response.status }, 'Failed to fetch static files list:', response.statusText)
return null
}
const eTag = response.headers.get('ETag')
// We do not want to cache the static files list if it has not changed.
if (lastStaticFileListCachedEtag === eTag) {
info({ status: response.status }, 'Static files list is up to date')
return lastStaticFileListCachedEtag
}
info('Caching files (etag:%s)', eTag)
const staticFiles: StaticFile[] = await response.json() as StaticFile[]
let errorsDuringCaching = 0
staticFiles.push({ url: DEFAULT_SPA_URL, path: '*', hash: '', size: 0 })
// We limit the number of concurrent requests to avoid
// overloading the server with requests or the browser to
// make too many calls at once.
const semaphore = new Semaphore(MAX_NUMBER_OF_CONCURRENT_REQUESTS)
await Promise.all(staticFiles.map(async file => {
await semaphore.acquire()
try {
const keyForFileHash = `static:${file.url}`
const hash = await get(cacheName, keyForFileHash)
// Skip files that might already be cached because they have the same hashs
if (hash === file.hash) {
const alreadyPresent = await cache.match(file.url)
if (alreadyPresent) {
return
}
}
const isExternalResource = file.url.startsWith('http://') || file.url.startsWith('https://')
const fileResponse = await fetch(file.url, { mode: isExternalResource ? 'no-cors' : 'cors' })
if (fileResponse.ok) {
await cache.put(file.url, fileResponse)
await set(cacheName, keyForFileHash, file.hash)
} else if (fileResponse.status === 0) {
if (isExternalResource) {
await cache.put(file.url, fileResponse)
// We cannot store the hash of the file because the response
// is opaque so it might be invalid and it definitely should
// be recached on the next request
}
} else {
errorsDuringCaching++
warn('Could not cache file at %s [%d: %s]', file.url, fileResponse.status, fileResponse.statusText)
}
} catch (error) {
errorsDuringCaching++
fail(`Could not cache file '${file.url}':`, error)
} finally {
semaphore.release()
}
}))
// The cache is complete, so we can update the ETag and remember
// to not cache the static files list again until it has changed.
if (errorsDuringCaching === 0) {
await set(cacheName, 'lastStaticFileListCached', response.headers.get('ETag') || '')
}
// Read total size of all files by reading from X-Total-Static-Content-Size header
const totalSizeOfAllFiles = Number.parseInt(response.headers.get('X-Total-Static-Content-Size') || '0', 10)
const note = errorsDuringCaching > 0 ? ` But ${errorsDuringCaching} errors occured.` : ''
info(`Cached ${staticFiles.length} (#${totalSizeOfAllFiles} bytes) static files.` + note)
} catch (error) {
warn('Error fetching static files list. Could be offline.', error)
return null
}
return lastStaticFileListCachedEtag
}
All files returned during normal fetches by the clients are also added to the static cache if any of the following conditions are true:
- They are marked as immutable in the Cache-Control reponse header
- They are cacheable with max-age for at least a day
This actual implementation is here (also checks if response is status ok etc):
ts
function canCache(response: Response) {
if (!response.ok) {
return false
}
// We can cache all requests that are marked as immutable in the Cache-Control header.
// The normal web-cache can be cleared by the users to easily and we know these
// files to not change.
//
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
//
// This specifically is done by the /api/packages api for example when a versioned content
// call is performed
const cacheControlHeader = response.headers.get('Cache-Control')
const cacheControl = cacheControlHeader ? cacheControlHeader.split(',') : []
if (cacheControl.includes('immutable')) {
return true
}
// We also cache all requests that are marked as with a max-age of at least 1 day.
const maxAge = cacheControl.find(value => value.startsWith('max-age='))
if (maxAge) {
const maxAgeInSeconds = Number.parseInt(maxAge.slice(8), 10)
const maxAgeInDays = maxAgeInSeconds / 60 / 60 / 24
if (maxAgeInDays >= 1) {
return true
}
}
return false
}