Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the workbox-expiration IDB data model #1883

Merged
merged 7 commits into from
Feb 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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