Skip to content

Commit

Permalink
Initial cut of LRU expiration for fetchAndCache().
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffposnick committed Sep 22, 2015
1 parent 1ba5d7f commit d1c3336
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 22 deletions.
2 changes: 2 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<html>
<head>
<title>Service Worker Toolbox Demo</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<p>
Expand All @@ -9,5 +10,6 @@
</p>

<script src="../companion.js" data-service-worker="service-worker.js"></script>
<link rel="stylesheet" href="styles-end.css">
</body>
</html>
1 change: 1 addition & 0 deletions demo/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ importScripts('../sw-toolbox.js');
'use strict';

global.toolbox.options.debug = true;
global.toolbox.options.maxCacheEntries = 2;
global.toolbox.options.networkTimeoutSeconds = 10;
global.toolbox.router.default = global.toolbox.networkFirst;
})(self);
3 changes: 3 additions & 0 deletions demo/styles-end.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
font-size: 2em;
}
3 changes: 3 additions & 0 deletions demo/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
font-family: sans-serif;
}
80 changes: 76 additions & 4 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'use strict';

var globalOptions = require('./options');
var simpleDB = require('./simple-db');

function debug(message, options) {
options = options || {};
Expand All @@ -35,19 +36,90 @@ function openCache(options) {
function fetchAndCache(request, options) {
options = options || {};
var successResponses = options.successResponses || globalOptions.successResponses;
return fetch(request.clone()).then(function(response) {
var maxCacheEntries = options.maxCacheEntries || globalOptions.maxCacheEntries;
var cacheName = options.cacheName || globalOptions.cacheName;

// Only cache GET requests with successful responses
return fetch(request.clone()).then(function(response) {
// Only cache GET requests with successful responses.
// Since this is not part of the promise chain, it will be done asynchronously and will not
// block the response from being returned to the page.
if (request.method === 'GET' && successResponses.test(response.status)) {
openCache(options).then(function(cache) {
cache.put(request, response);
openCache({cacheName: cacheName}).then(function(cache) {
cache.put(request, response).then(function() {
if (maxCacheEntries) {
idbPromise().then(function(idb) {
queueCacheExpiration(request, idb, cache, cacheName, maxCacheEntries);
});
}
});
});
}

return response.clone();
});
}

var staticIdb;
function idbPromise() {
if (staticIdb) {
return Promise.resolve(staticIdb);
}

return simpleDB.open('sw-toolbox-lru-order').then(function(idb) {
staticIdb = idb;
return staticIdb;
});
}

var cacheExpirationPromiseChain;
function queueCacheExpiration(request, idb, cache, cacheName, maxCacheEntries) {
var cacheExpiration = cacheExpirationPromiseFactory.bind(null, request, idb, cache, cacheName, maxCacheEntries);

if (cacheExpirationPromiseChain) {
cacheExpirationPromiseChain = cacheExpirationPromiseChain.then(cacheExpiration);
} else {
cacheExpirationPromiseChain = cacheExpiration();
}
}

function cacheExpirationPromiseFactory(request, idb, cache, cacheName, maxCacheEntries) {
debug('Updating LRU order for ' + request.url + '. Max entries is ' + maxCacheEntries);
return idb.get(cacheName).then(function(lruOrder) {
if (!Array.isArray(lruOrder)) {
lruOrder = [];
}

var url = request.url;
var oldIndex = lruOrder.indexOf(url);
if (oldIndex > 0) {
// If url already is in the array, and it's not in the first position, remove it.
lruOrder.splice(oldIndex, 1);
}
if (oldIndex !== 0) {
// If url isn't already in the first position, add it there.
lruOrder.unshift(url);
}

if (lruOrder.length > maxCacheEntries) {
// If we're over the cap, remove the last entry from the array (representing the least-used
// item) from the cache and then resolve with lruOrder.
var urlToRemove = lruOrder.pop();
debug('Expiring the least-recently used resource, ' + urlToRemove);
return cache.delete(urlToRemove).then(function() {
debug(urlToRemove + ' was successfully deleted.');
return lruOrder;
});
}

// If we're under the cap, then just resolve with the new lruOrder.
return lruOrder;
}).then(function(lruOrder) {
return idb.set(cacheName, lruOrder).then(function() {
debug('Successfully updated IDB with the new LRU order.');
});
});
}

function renameCache(source, destination, options) {
debug('Renaming cache: [' + source + '] to [' + destination + ']', options);
return caches.delete(destination).then(function() {
Expand Down
8 changes: 4 additions & 4 deletions lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ if (self.registration) {
module.exports = {
cacheName: '$$$toolbox-cache$$$' + scope + '$$$',
debug: false,
maxCacheEntries: null,
networkTimeoutSeconds: null,
preCacheItems: [],
// A regular expression to apply to HTTP response codes. Codes that match
// will be considered successes, while others will not, and will not be
// cached.
successResponses: /^0|([123]\d\d)|(40[14567])|410$/
// A regular expression to apply to HTTP response codes. Codes that match will be considered
// successes, while others will not, and will not be cached.
successResponses: /^0|([123]\d\d)|(40[14567])|410$/
};
173 changes: 173 additions & 0 deletions lib/simple-db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* Copyright 2015 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// From https://gist.github.com/inexorabletash/c8069c042b734519680c (Joshua Bell)

'use strict';

var SECRET = Object.create(null);
var DB_PREFIX = '$SimpleDB$';
var STORE = 'store';

function SimpleDBFactory(secret) {
if (secret !== SECRET) throw TypeError('Invalid constructor');
}
SimpleDBFactory.prototype = {
open: function(name) {
return new Promise(function(resolve, reject) {
var request = indexedDB.open(DB_PREFIX + name);
request.onupgradeneeded = function() {
var db = request.result;
db.createObjectStore(STORE);
};
request.onsuccess = function() {
var db = request.result;
resolve(new SimpleDB(SECRET, name, db));
};
request.onerror = function() {
reject(request.error);
};
});
},
delete: function(name) {
return new Promise(function(resolve, reject) {
var request = indexedDB.deleteDatabase(DB_PREFIX + name);
request.onsuccess = function() {
resolve(undefined);
};
request.onerror = function() {
reject(request.error);
};
});
}
};

function SimpleDB(secret, name, db) {
if (secret !== SECRET) throw TypeError('Invalid constructor');
this._name = name;
this._db = db;
}
SimpleDB.cmp = indexedDB.cmp;
SimpleDB.prototype = {
get name() {
return this._name;
},
get: function(key) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var req = store.get(key);
// NOTE: Could also use req.onsuccess/onerror
tx.oncomplete = function() { resolve(req.result); };
tx.onabort = function() { reject(tx.error); };
});
},
set: function(key, value) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var req = store.put(value, key);
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
},
delete: function(key) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var req = store.delete(key);
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
},
clear: function() {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var request = store.clear();
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
},
forEach: function(callback, options) {
var that = this;
return new Promise(function(resolve, reject) {
options = options || {};
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var request = store.openCursor(
options.range,
options.direction === 'reverse' ? 'prev' : 'next');
request.onsuccess = function() {
var cursor = request.result;
if (!cursor) return;
try {
var terminate = callback(cursor.key, cursor.value);
if (!terminate) cursor.continue();
} catch (ex) {
tx.abort(); // ???
}
};
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
},
getMany: function(keys) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var results = [];
keys.forEach(function(key) {
store.get(key).onsuccess(function(result) {
results.push(result);
});
});
tx.oncomplete = function() { resolve(results); };
tx.onabort = function() { reject(tx.error); };
});
},
setMany: function(entries) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
entries.forEach(function(entry) {
store.put(entry.value, entry.key);
});
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
},
deleteMany: function(keys) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
keys.forEach(function(key) {
store.delete(key);
});
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
}
};

module.exports = new SimpleDBFactory(SECRET);
Loading

0 comments on commit d1c3336

Please sign in to comment.