feat: Consider the content of Cache-Control header for application caching
MODIFY
lib/basedriver/helpers.js
lib/basedriver/helpers.js
@@ -70,18 +70,50 @@ async function retrieveHeaders (link) {
return {};
}
-function getCachedApplicationPath (link, currentModified) {
- if (!APPLICATIONS_CACHE.has(link) || !currentModified) {
+function getCachedApplicationPath (link, currentAppProps = {}) {
+ const refresh = () => {
+ logger.debug(`A fresh copy of the application is going to be downloaded from ${link}`);
return null;
- }
+ };
- const {lastModified, fullPath} = APPLICATIONS_CACHE.get(link);
- if (lastModified && currentModified.getTime() <= lastModified.getTime()) {
+ if (APPLICATIONS_CACHE.has(link)) {
+ const {
+ lastModified: currentModified,
+ immutable: currentImmutable,
+ // maxAge is in seconds
+ maxAge: currentMaxAge,
+ } = currentAppProps;
+ const {
+ // Date instance
+ lastModified,
+ // boolean
+ immutable,
+ // Unix time in milliseconds
+ timestamp,
+ fullPath,
+ } = APPLICATIONS_CACHE.get(link);
+ if (lastModified && currentModified) {
+ if (currentModified.getTime() <= lastModified.getTime()) {
+ logger.debug(`The application at ${link} has not been modified since ${lastModified}`);
return fullPath;
}
- logger.debug(`'Last-Modified' timestamp of '${link}' has been updated. ` +
- `A fresh copy of the application is going to be downloaded.`);
- return null;
+ logger.debug(`The application at ${link} has been modified since ${lastModified}`);
+ return refresh();
+ }
+ if (immutable && currentImmutable) {
+ logger.debug(`The application at ${link} is immutable`);
+ return fullPath;
+ }
+ if (currentMaxAge && timestamp) {
+ const msLeft = timestamp + currentMaxAge * 1000 - Date.now();
+ if (msLeft > 0) {
+ logger.debug(`The cached application '${path.basename(fullPath)}' will expire in ${msLeft / 1000}s`);
+ return fullPath;
+ }
+ logger.debug(`The cached application '${path.basename(fullPath)}' has expired`);
+ }
+ }
+ return refresh();
}
function verifyAppExtension (app, supportedAppExtensions) {
@@ -105,7 +137,11 @@ async function configureApp (app, supportedAppExtensions) {
let newApp = app;
let shouldUnzipApp = false;
let archiveHash = null;
- let currentModified = null;
+ const remoteAppProps = {
+ lastModified: null,
+ immutable: false,
+ maxAge: null,
+ };
const {protocol, pathname} = url.parse(newApp);
const isUrl = ['http:', 'https:'].includes(protocol);
@@ -114,11 +150,21 @@ async function configureApp (app, supportedAppExtensions) {
// Use the app from remote URL
logger.info(`Using downloadable app '${newApp}'`);
const headers = await retrieveHeaders(newApp);
+ if (!_.isEmpty(headers)) {
if (headers['last-modified']) {
- logger.debug(`App Last-Modified: ${headers['last-modified']}`);
- currentModified = new Date(headers['last-modified']);
+ remoteAppProps.lastModified = new Date(headers['last-modified']);
}
- const cachedPath = getCachedApplicationPath(app, currentModified);
+ logger.debug(`Last-Modified: ${headers['last-modified']}`);
+ if (headers['cache-control']) {
+ remoteAppProps.immutable = /\bimmutable\b/i.test(headers['cache-control']);
+ const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(headers['cache-control']);
+ if (maxAgeMatch) {
+ remoteAppProps.maxAge = parseInt(maxAgeMatch[1], 10);
+ }
+ }
+ logger.debug(`Cache-Control: ${headers['cache-control']}`);
+ }
+ const cachedPath = getCachedApplicationPath(app, remoteAppProps);
if (cachedPath) {
if (await fs.exists(cachedPath)) {
logger.info(`Reusing previously downloaded application at '${cachedPath}'`);
@@ -225,7 +271,7 @@ async function configureApp (app, supportedAppExtensions) {
verifyAppExtension(newApp, supportedAppExtensions);
- if (app !== newApp && (archiveHash || currentModified)) {
+ if (app !== newApp && (archiveHash || _.values(remoteAppProps).some(Boolean))) {
if (APPLICATIONS_CACHE.has(app)) {
const {fullPath} = APPLICATIONS_CACHE.get(app);
// Clean up the obsolete entry first if needed
@@ -234,8 +280,9 @@ async function configureApp (app, supportedAppExtensions) {
}
}
APPLICATIONS_CACHE.set(app, {
+ ...remoteAppProps,
+ timestamp: Date.now(),
hash: archiveHash,
- lastModified: currentModified,
fullPath: newApp,
});
}

feat: Consider the content of Cache-Control header for application caching (#397)