Rework caching expiration handling

Check expiration of cached items when serving them instead of computing
an expiration date when first caching them. If the user changes the
caching duration after a request was cached, the new setting will be
used now.
This commit is contained in:
Lucas Verney 2019-01-05 15:52:12 +01:00
parent 07ab80542d
commit cecf1fd8a5

155
src/sw.js
View File

@ -45,7 +45,7 @@ Object.keys(TILE_SERVERS).forEach((tileServer) => {
// Get duration (in s) before (cache) expiration from headers of a fetch // Get duration (in s) before (cache) expiration from headers of a fetch
// request. // request.
function getExpiresFromHeaders(headers) { function _getExpiresFromHeaders(headers) {
// Try to use the Cache-Control header (and max-age) // Try to use the Cache-Control header (and max-age)
if (headers.get('cache-control')) { if (headers.get('cache-control')) {
const maxAge = headers.get('cache-control').match(/max-age=(\d+)/); const maxAge = headers.get('cache-control').match(/max-age=(\d+)/);
@ -65,6 +65,60 @@ function getExpiresFromHeaders(headers) {
return null; return null;
} }
// Get tile caching duration from config
function _getTileCachingDurationPromise() {
// Get tile caching duration from settings
let tileCachingDurationPromise = (
Promise.resolve(0) // no caching by default
);
if (
localforage.driver() === localforage.INDEXEDDB
) {
tileCachingDurationPromise = loadDataFromStorage(
'settings', 'tileCachingDuration',
);
}
return tileCachingDurationPromise;
}
// Check expiration of an item in cache
// Return the response is not expired. Deletes it from cache and returns null
// otherwise.
function _checkExpiration(cache, request, response) {
return _getTileCachingDurationPromise().then((cachingDurationFromConfig) => {
const cachedDate = Date.parse(response.headers.get('sw-cached-date'));
const now = new Date();
// Check wether it is expired according to config
if (cachingDurationFromConfig > 0) {
const expirationDateFromConfig = new Date(cachedDate.valueOf());
expirationDateFromConfig.setSeconds(
expirationDateFromConfig.getSeconds() + cachingDurationFromConfig,
);
if (expirationDateFromConfig > now) {
DEBUG && console.log(`SW: Found ${request.url} in cache.`);
return response;
}
DEBUG && console.log(`SW: Deleting expired ${request.url} from cache.`);
cache.delete(request);
}
// If no special caching from config or not expired from config, check
// whether it is expired from HTTP headers.
const cachingDurationFromHeaders = response.headers.get('sw-http-cache-duration');
const expirationDateFromHeaders = new Date(cachedDate.valueOf());
expirationDateFromHeaders.setSeconds(
expirationDateFromHeaders.getSeconds() + cachingDurationFromHeaders,
);
if (expirationDateFromHeaders > now) {
return response;
}
DEBUG && console.log(`SW: Deleting expired ${request.url} from cache.`);
cache.delete(request);
return null;
});
}
global.self.addEventListener('install', (event) => { global.self.addEventListener('install', (event) => {
DEBUG && console.log('SW: installing…'); DEBUG && console.log('SW: installing…');
event.waitUntil( event.waitUntil(
@ -135,59 +189,35 @@ global.self.addEventListener('fetch', (event) => {
event.respondWith(global.caches.open(`${CACHE_NAME}-tiles`).then( event.respondWith(global.caches.open(`${CACHE_NAME}-tiles`).then(
cache => cache.match(request).then( cache => cache.match(request).then(
(response) => { (response) => {
// If there is a match from the cache // Helper function to fetch data from the network
if (response) { const _fetchFromNetwork = (
DEBUG && console.log(`SW: serving ${request.url} from cache.`); // Note: We HAVE to use fetch(request.url) here to ensure we
const expirationDate = Date.parse(response.headers.get('sw-cache-expires')); // have a CORS-compliant request. Otherwise, we could get back
const now = new Date(); // an opaque response which we cannot inspect
// Check it is not already expired and return from the // (https://developer.mozilla.org/en-US/docs/Web/API/Response/type).
// cache () => fetch(request.url).then(
if (expirationDate > now) { liveResponse => _getTileCachingDurationPromise().then(
return response; (cachingDurationFromConfig) => {
}
}
// Otherwise, let's fetch it from the network
DEBUG && console.log(`SW: no match in cache for ${request.url}, using network.`);
return fetch(request.url).then((liveResponse) => {
// Get tile caching duration from settings
let tileCachingDurationPromise = (
Promise.resolve(0) // no caching by default
);
if (
localforage.driver() === localforage.INDEXEDDB
) {
tileCachingDurationPromise = loadDataFromStorage(
'settings', 'tileCachingDuration',
);
}
return tileCachingDurationPromise.then((tileCachingDuration) => {
// This is caching duration from settings
const cachingDuration = (
tileCachingDuration || 0
);
// This is caching duration specified in HTTP headers // This is caching duration specified in HTTP headers
const cachingDurationFromHeaders = getExpiresFromHeaders( const cachingDurationFromHeaders = _getExpiresFromHeaders(
liveResponse.headers, liveResponse.headers,
) || 0; ) || 0;
// If any form of caching is possible, do it // If any form of caching is possible, do it
if (tileCachingDuration > 0 || cachingDurationFromHeaders > 0) { if (
// Compute expires date from caching duration cachingDurationFromConfig > 0
const expires = new Date(); || cachingDurationFromHeaders > 0
expires.setSeconds( ) {
expires.getSeconds()
// Caching duration cannot be less than
// specified by HTTP headers.
+ Math.max(cachingDuration, cachingDurationFromHeaders),
);
// Recreate a Response object from scratch to put // Recreate a Response object from scratch to put
// it in the cache, with the extra header for // it in the cache, with the extra header for
// managing cache expiration. // managing cache expiration.
const cachedResponseFields = { const cachedResponseFields = {
status: liveResponse.status, status: liveResponse.status,
statusText: liveResponse.statusText, statusText: liveResponse.statusText,
headers: { 'SW-Cache-Expires': expires.toUTCString() }, headers: {
'SW-Cached-Date': (new Date()).toUTCString(),
'SW-HTTP-Cache-Duration': cachingDurationFromHeaders,
},
}; };
liveResponse.headers.forEach((v, k) => { liveResponse.headers.forEach((v, k) => {
cachedResponseFields.headers[k] = v; cachedResponseFields.headers[k] = v;
@ -198,18 +228,41 @@ global.self.addEventListener('fetch', (event) => {
const returnedResponse = liveResponse.clone(); const returnedResponse = liveResponse.clone();
return liveResponse.blob().then((body) => { return liveResponse.blob().then((body) => {
DEBUG && console.log( DEBUG && console.log(
`SW: caching tiles ${request.url} until ${expires.toUTCString()}.`, `SW: caching tiles ${request.url}.`,
); );
// Put the duplicated Response in the cache // Put the duplicated Response in the cache
cache.put(request, new Response(body, cachedResponseFields)); cache.put(
request, new Response(body, cachedResponseFields),
);
// Return the live response from the network // Return the live response from the network
return returnedResponse; return returnedResponse;
}); });
} }
// Otherwise, just return the live result from the network // Otherwise, just return the live result from the network
return liveResponse; return liveResponse;
}); },
}); ),
)
);
// If there is a match from the cache
if (response) {
DEBUG && console.log(`SW: Found ${request.url} in cache.`);
return _checkExpiration(cache, request, response).then(
(notExpiredResponse) => {
if (notExpiredResponse) {
DEBUG && console.log(`SW: Serving ${request.url} from cache.`);
return notExpiredResponse;
}
DEBUG && console.log(`SW: Serving ${request.url} from network, expired in cache.`);
return _fetchFromNetwork();
},
);
}
// Otherwise, let's fetch it from the network
DEBUG && console.log(`SW: no match in cache for ${request.url}, using network.`);
return _fetchFromNetwork();
}, },
), ),
)); ));
@ -250,13 +303,7 @@ global.self.addEventListener('message', (event) => {
cache => cache.keys().then( cache => cache.keys().then(
keys => keys.forEach( keys => keys.forEach(
key => cache.match(key).then((cachedResponse) => { key => cache.match(key).then((cachedResponse) => {
const expirationDate = Date.parse(cachedResponse.headers.get('sw-cache-expires')); _checkExpiration(cache, key, cachedResponse);
const now = new Date();
// Check it is not already expired
if (expirationDate < now) {
DEBUG && console.log(`SW: purging (expired) tile ${key.url} from cache.`);
cache.delete(key);
}
}), }),
), ),
), ),