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

New workbox.expiration.Plugin.deleteCacheAndMetadata() method #1500

Merged
merged 2 commits into from
May 26, 2018
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
4 changes: 1 addition & 3 deletions gulp-tasks/test-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,7 @@ gulp.task('test-integration', async () => {
}
break;
case 'safari':
if (localBrowser.getReleaseName() === 'beta') {
await runIntegrationForBrowser(localBrowser);
}
await runIntegrationForBrowser(localBrowser);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a holdover from when the stable version of Safari lacked SW support. I took the opportunity to drop that restriction as part of this PR.

break;
default:
logHelper.warn(oneLine`
Expand Down
11 changes: 11 additions & 0 deletions packages/workbox-cache-expiration/CacheExpiration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,17 @@ class CacheExpiration {
const expireOlderThan = Date.now() - (this._maxAgeSeconds * 1000);
return (timestamp < expireOlderThan);
}

/**
* Removes the IndexedDB object store used to keep track of cache expiration
* metadata.
*/
async delete() {
// 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();
}
}

export {CacheExpiration};
29 changes: 29 additions & 0 deletions packages/workbox-cache-expiration/Plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,35 @@ class Plugin {
await cacheExpiration.updateTimestamp(request.url);
await cacheExpiration.expireEntries();
}


/**
* 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.
* - Deletes the metadata from IndexedDB used to keep track of expiration
* details for each Cache instance.
*
* When using cache expiration, calling this method is preferable to calling
* `caches.delete()` directly, since this will ensure that the IndexedDB
* metadata is also cleanly removed and open IndexedDB instances are deleted.
*
* Note that if you're *not* using cache expiration for a given cache, calling
* `caches.delete()` and passing in the cache's name should be sufficient.
* There is no Workbox-specific method needed for cleanup in that case.
*/
async deleteCacheAndMetadata() {
// Do this one at at a time instance of all at once via `Promise.all()` to
// reduce the chance of inconsistency if a promise rejects.
for (const [cacheName, cacheExpiration] of this._cacheExpirations) {
await caches.delete(cacheName);
await cacheExpiration.delete();
}

// Reset this._cacheExpirations to its initial state.
this._cacheExpirations = new Map();
}
}

export {Plugin};
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ class CacheTimestampsModel {
async deleteUrl(url) {
await this._db.delete(this._storeName, new URL(url, location).href);
}

/**
* Removes the underlying IndexedDB object store entirely.
*/
async delete() {
await this._db.deleteDatabase();
this._db = null;
}
}

export default CacheTimestampsModel;
17 changes: 17 additions & 0 deletions packages/workbox-core/_private/DBWrapper.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,23 @@ class DBWrapper {
await this._call('delete', storeName, 'readwrite', ...args);
}

/**
* Deletes the underlying database, ensuring that any open connections are
* closed first.
*
* @private
*/
async deleteDatabase() {
this.close();
this._db = null;
await new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(this._name);
request.onerror = (evt) => reject(evt.target.error);
request.onblocked = () => reject(new Error('Deletion was blocked.'));
request.onsuccess = () => resolve();
});
}

/**
* Delegates to the native `getAll()` or polyfills it via the `find()`
* method in older browsers.
Expand Down
67 changes: 67 additions & 0 deletions test/workbox-cache-expiration/integration/expiration-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,71 @@ describe(`expiration.Plugin`, function() {
// If the code path reaches here - the clean up from expiration was
// successful
});

it(`should clean up when deleteCacheAndMetadata() is called`, async function() {
const name = 'expiration-plugin-deletion';
const swUrl = `${testingUrl}sw-deletion.js`;

// Wait for the service worker to register and activate.
await activateAndControlSW(swUrl);

await global.__workbox.webdriver.executeAsyncScript((testingUrl, cb) => {
fetch(`${testingUrl}example-1.txt`)
.then(() => cb())
.catch((err) => cb(err.message));
}, testingUrl);

// Caching is done async from returning a response, so we may need
// to wait until the cache has some content.
await global.__workbox.webdriver.wait(async () => {
return await global.__workbox.webdriver.executeAsyncScript((cb) => {
caches.keys().then((keys) => cb(keys.length > 0));
});
});

let cacheKeys = await global.__workbox.webdriver.executeAsyncScript((cb) => {
caches.keys().then(cb);
});

expect(cacheKeys).to.deep.equal([
name,
]);

let existence = await global.__workbox.webdriver.executeAsyncScript((cb) => {
navigator.serviceWorker.addEventListener('message', (event) => {
cb(event.data);
}, {once: true});

navigator.serviceWorker.controller.postMessage('doesDbExist');
});
expect(existence).to.be.true;

const error = await global.__workbox.webdriver.executeAsyncScript((cb) => {
navigator.serviceWorker.addEventListener('message', (event) => {
cb(event.data);
}, {once: true});

navigator.serviceWorker.controller.postMessage('delete');
});

if (error) {
throw new Error(error);
}

// After cleanup, there shouldn't be any cache keys or IndexedDB dbs.
cacheKeys = await global.__workbox.webdriver.executeAsyncScript((cb) => {
caches.keys().then(cb);
});

expect(cacheKeys).to.deep.equal([]);

existence = await global.__workbox.webdriver.executeAsyncScript((cb) => {
navigator.serviceWorker.addEventListener('message', (event) => {
cb(event.data);
}, {once: true});

navigator.serviceWorker.controller.postMessage('doesDbExist');
});
expect(existence).to.be.false;
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
<head>
</head>
<body>
<p>You need to manually register sw.js</p>
<script src="/node_modules/sinon/pkg/sinon.js"></script>
<p>You need to manually register the service worker.</p>
<script>
window.__test = {};
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
importScripts('/__WORKBOX/buildFile/workbox-core');
importScripts('/__WORKBOX/buildFile/workbox-cache-expiration');
importScripts('/__WORKBOX/buildFile/workbox-routing');
importScripts('/__WORKBOX/buildFile/workbox-strategies');

const expirationPlugin = new workbox.expiration.Plugin({
maxEntries: 1,
});

const cacheName = 'expiration-plugin-deletion';

workbox.routing.registerRoute(
/.*.txt/,
workbox.strategies.cacheFirst({
cacheName,
plugins: [
expirationPlugin,
],
})
);

const doesDbExist = () => {
return new Promise((resolve) => {
const result = indexedDB.open(cacheName);
result.onupgradeneeded = (event) => {
event.target.transaction.abort();
event.target.result.close();
resolve(false);
};
result.onsuccess = (event) => {
event.target.result.close();
resolve(true);
};
});
};

self.addEventListener('message', async (event) => {
let message;

if (event.data === 'delete') {
try {
await expirationPlugin.deleteCacheAndMetadata();
} catch (error) {
message = error.message;
}
} else if (event.data === 'doesDbExist') {
message = await doesDbExist(cacheName);
}

// Send all open clients a message indicating that deletion is done.
const clients = await self.clients.matchAll();
for (const client of clients) {
client.postMessage(message);
}
});

self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', () => self.clients.claim());