Skip to content

Commit

Permalink
Merge pull request #1883 from GoogleChrome/expiration-idb
Browse files Browse the repository at this point in the history
Update the workbox-expiration IDB data model
  • Loading branch information
philipwalton authored Feb 6, 2019
2 parents 348cea6 + e41c219 commit 4e481a9
Show file tree
Hide file tree
Showing 13 changed files with 456 additions and 420 deletions.
15 changes: 15 additions & 0 deletions infra/testing/comlink/sw-interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ const api = {
});
},

getObjectStoreEntries: (dbName, objStoreName) => {
return new Promise((resolve) => {
const result = indexedDB.open(dbName);
result.onsuccess = (event) => {
const db = event.target.result;
db.transaction(objStoreName)
.objectStore(objStoreName)
.getAll()
.onsuccess = (event) => {
resolve(event.target.result);
};
};
});
},

cacheURLs: async (cacheName) => {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
"semver": "^5.5.1",
"serve-index": "^1.9.1",
"service-worker-mock": "^1.9.3",
"shelving-mock-indexeddb": "github:philipwalton/shelving-mock-indexeddb#c7b3b002472597ee75b027011049737a06460261",
"shelving-mock-indexeddb": "github:philipwalton/shelving-mock-indexeddb#2379a818f8a45873903166d1bdb4ff3dbfbc550d",
"sinon": "^6.3.4",
"tempy": "^0.2.1",
"url-search-params": "^1.1.0",
Expand Down
141 changes: 27 additions & 114 deletions packages/workbox-expiration/CacheExpiration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
https://opensource.org/licenses/MIT.
*/

import CacheTimestampsModel from './models/CacheTimestampsModel.mjs';
import {CacheTimestampsModel} from './models/CacheTimestampsModel.mjs';
import {WorkboxError} from 'workbox-core/_private/WorkboxError.mjs';
import {assert} from 'workbox-core/_private/assert.mjs';
import {logger} from 'workbox-core/_private/logger.mjs';
Expand Down Expand Up @@ -90,34 +90,28 @@ class CacheExpiration {
}
this._isRunning = true;

const now = Date.now();
const minTimestamp = this._maxAgeSeconds ?
Date.now() - (this._maxAgeSeconds * 1000) : undefined;

// First, expire old entries, if maxAgeSeconds is set.
const oldEntries = await this._findOldEntries(now);
const urlsExpired = await this._timestampModel.expireEntries(
minTimestamp, this._maxEntries);

// Once that's done, check for the maximum size.
const extraEntries = await this._findExtraEntries();

// Use a Set to remove any duplicates following the concatenation, then
// convert back into an array.
const allURLs = [...new Set(oldEntries.concat(extraEntries))];

await Promise.all([
this._deleteFromCache(allURLs),
this._deleteFromIDB(allURLs),
]);
// Delete URLs from the cache
const cache = await caches.open(this._cacheName);
for (const url of urlsExpired) {
await cache.delete(url);
}

if (process.env.NODE_ENV !== 'production') {
// TODO: break apart entries deleted due to expiration vs size restraints
if (allURLs.length > 0) {
if (urlsExpired.length > 0) {
logger.groupCollapsed(
`Expired ${allURLs.length} ` +
`${allURLs.length === 1 ? 'entry' : 'entries'} and removed ` +
`${allURLs.length === 1 ? 'it' : 'them'} from the ` +
`Expired ${urlsExpired.length} ` +
`${urlsExpired.length === 1 ? 'entry' : 'entries'} and removed ` +
`${urlsExpired.length === 1 ? 'it' : 'them'} from the ` +
`'${this._cacheName}' cache.`);
logger.log(
`Expired the following ${allURLs.length === 1 ? 'URL' : 'URLs'}:`);
allURLs.forEach((url) => logger.log(` ${url}`));
logger.log(`Expired the following ${urlsExpired.length === 1 ?
'URL' : 'URLs'}:`);
urlsExpired.forEach((url) => logger.log(` ${url}`));
logger.groupEnd();
} else {
logger.debug(`Cache expiration ran and found no entries to remove.`);
Expand All @@ -131,84 +125,6 @@ class CacheExpiration {
}
}

/**
* Expires entries based on the maximum age.
*
* @param {number} expireFromTimestamp A timestamp.
* @return {Promise<Array<string>>} A list of the URLs that were expired.
*
* @private
*/
async _findOldEntries(expireFromTimestamp) {
if (process.env.NODE_ENV !== 'production') {
assert.isType(expireFromTimestamp, 'number', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: '_findOldEntries',
paramName: 'expireFromTimestamp',
});
}

if (!this._maxAgeSeconds) {
return [];
}

const expireOlderThan = expireFromTimestamp - (this._maxAgeSeconds * 1000);
const timestamps = await this._timestampModel.getAllTimestamps();
const expiredURLs = [];
timestamps.forEach((timestampDetails) => {
if (timestampDetails.timestamp < expireOlderThan) {
expiredURLs.push(timestampDetails.url);
}
});

return expiredURLs;
}

/**
* @return {Promise<Array>}
*
* @private
*/
async _findExtraEntries() {
const extraURLs = [];

if (!this._maxEntries) {
return [];
}

const timestamps = await this._timestampModel.getAllTimestamps();
while (timestamps.length > this._maxEntries) {
const lastUsed = timestamps.shift();
extraURLs.push(lastUsed.url);
}

return extraURLs;
}

/**
* @param {Array<string>} urls Array of URLs to delete from cache.
*
* @private
*/
async _deleteFromCache(urls) {
const cache = await caches.open(this._cacheName);
for (const url of urls) {
await cache.delete(url);
}
}

/**
* @param {Array<string>} urls Array of URLs to delete from IDB
*
* @private
*/
async _deleteFromIDB(urls) {
for (const url of urls) {
await this._timestampModel.deleteURL(url);
}
}

/**
* Update the timestamp for the given URL. This ensures the when
* removing entries based on maximum entries, most recently used
Expand All @@ -226,10 +142,7 @@ class CacheExpiration {
});
}

const urlObject = new URL(url, location);
urlObject.hash = '';

await this._timestampModel.setTimestamp(urlObject.href, Date.now());
await this._timestampModel.setTimestamp(url, Date.now());
}

/**
Expand All @@ -244,16 +157,16 @@ class CacheExpiration {
* @return {boolean}
*/
async isURLExpired(url) {
if (!this._maxAgeSeconds) {
throw new WorkboxError(`expired-test-without-max-age`, {
methodName: 'isURLExpired',
paramName: 'maxAgeSeconds',
});
if (process.env.NODE_ENV !== 'production') {
if (!this._maxAgeSeconds) {
throw new WorkboxError(`expired-test-without-max-age`, {
methodName: 'isURLExpired',
paramName: 'maxAgeSeconds',
});
}
}
const urlObject = new URL(url, location);
urlObject.hash = '';

const timestamp = await this._timestampModel.getTimestamp(urlObject.href);
const timestamp = await this._timestampModel.getTimestamp(url);
const expireOlderThan = Date.now() - (this._maxAgeSeconds * 1000);
return (timestamp < expireOlderThan);
}
Expand All @@ -266,7 +179,7 @@ class CacheExpiration {
// Make sure we don't attempt another rerun if we're called in the middle of
// a cache expiration.
this._rerunRequested = false;
await this._timestampModel.delete();
await this._timestampModel.expireEntries(Infinity); // Expires all.
}
}

Expand Down
31 changes: 24 additions & 7 deletions packages/workbox-expiration/Plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
https://opensource.org/licenses/MIT.
*/

import {CacheExpiration} from './CacheExpiration.mjs';
import {WorkboxError} from 'workbox-core/_private/WorkboxError.mjs';
import {assert} from 'workbox-core/_private/assert.mjs';
import {cacheNames} from 'workbox-core/_private/cacheNames.mjs';
import {registerQuotaErrorCallback} from 'workbox-core/index.mjs';
import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.mjs';
import {logger} from 'workbox-core/_private/logger.mjs';
import {WorkboxError} from 'workbox-core/_private/WorkboxError.mjs';
import {registerQuotaErrorCallback}
from 'workbox-core/registerQuotaErrorCallback.mjs';

import {CacheExpiration} from './CacheExpiration.mjs';
import './_version.mjs';

/**
Expand Down Expand Up @@ -104,7 +107,7 @@ class Plugin {

/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox.runtimeCaching` handlers when a `Response` is about to be returned
* `workbox.strategies` handlers when a `Response` is about to be returned
* from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
* the handler. It allows the `Response` to be inspected for freshness and
* prevents it from being used if the `Response`'s `Date` header value is
Expand All @@ -119,7 +122,7 @@ class Plugin {
*
* @private
*/
cachedResponseWillBeUsed({cacheName, cachedResponse}) {
cachedResponseWillBeUsed({event, request, cacheName, cachedResponse}) {
if (!cachedResponse) {
return null;
}
Expand All @@ -131,6 +134,20 @@ class Plugin {
const cacheExpiration = this._getCacheExpiration(cacheName);
cacheExpiration.expireEntries();

// Update the metadata for the request URL to the current timestamp,
// but don't `await` it as we don't want to block the response.
const updateTimestampDone = cacheExpiration.updateTimestamp(request.url);
if (event) {
try {
event.waitUntil(updateTimestampDone);
} catch (error) {
if (process.env.NODE_ENV !== 'production') {
logger.warn(`Unable to ensure service worker stays alive when ` +
`updating cache entry for '${getFriendlyURL(event.request.url)}'.`);
}
}
}

return isFresh ? cachedResponse : null;
}

Expand Down Expand Up @@ -190,7 +207,7 @@ class Plugin {

/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox.runtimeCaching` handlers when an entry is added to a cache.
* `workbox.strategies` handlers when an entry is added to a cache.
*
* @param {Object} options
* @param {string} options.cacheName Name of the cache that was updated.
Expand Down Expand Up @@ -224,7 +241,7 @@ class Plugin {
* This is a helper method that performs two operations:
*
* - Deletes *all* the underlying Cache instances associated with this plugin
* instance, by calling caches.delete() on you behalf.
* instance, by calling caches.delete() on your behalf.
* - Deletes the metadata from IndexedDB used to keep track of expiration
* details for each Cache instance.
*
Expand Down
Loading

0 comments on commit 4e481a9

Please sign in to comment.