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

Sw lib caching strategies #108

Merged
merged 10 commits into from Jan 10, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/sw-lib/src/index.js
Expand Up @@ -26,6 +26,33 @@ if (!assert.isSWEnv()) {
throw ErrorFactory.createError('not-in-sw');
}

/**
* The sw-lib module is a high-level library that makes it easier to
* configure routes with caching strategies as well as manage precaching
* of assets during the install step of a service worker.
*
* @example
* importScripts('/<Path to Module>/build/sw-lib.min.js');
*
* goog.swlib.cacheRevisionedAssets([
* '/styles/main.1234.css',
Copy link
Member

Choose a reason for hiding this comment

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

I think what you're trying to show here is that cacheRevisionedAssets accepts both filename paths with revisions in there or an object with the raw path and a separate revision number. Is that the case?

It might be worth splitting these into two examples (one showing caching a few files with one approach (e.g `/styles/main.1234.css', '/js/main.1234.js' etc) and then the object variation. Might make it more easy to read through.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

* {
* url: '/index.html',
* revision: '1234'
* }
* ]);
*
* goog.swlib.warmRuntimeCache([
* '/scripts/main.js',
* new Request('/images/logo.png')
* ]);
*
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as above.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

* goog.swlib.router.registerRoute('/', goog.swlib.cacheFirst);
* goog.swlib.router.registerRoute('/example/', goog.swlib.networkFirst);
*
* @module sw-lib
*/

const swLibInstance = new SWLib();
swLibInstance.Route = Route;
export default swLibInstance;
67 changes: 58 additions & 9 deletions packages/sw-lib/src/lib/router-wrapper.js
Expand Up @@ -21,7 +21,56 @@ import ErrorFactory from './error-factory.js';

/**
* A simple class that pulls together a few different pieces from the
* Router Module to surface them in a slightly easier API surface.
* Router Module to surface them in a friendly API.
*
* @example <caption>How to define a simple route with caching
* strategy.</caption>
*
* self.goog.swlib.router.registerRoute('/about', ....);
*
* @example <caption>How to define a simple route with custom caching
* strategy.</caption>
*
* self.goog.swlib.router.registerRoute('/about', (args) => {
* // The requested URL
* console.log(args.url);
*
* // The FetchEvent to handle
* console.log(args.event);
*
* // The parameters from the matching route.
* console.log(args.params);
*
* // Return a promise that resolves with a Response.
* return fetch(args.url);
* }));
*
* @example <caption>How to define a route using a Route instance.</caption>
*
* const routeInstance = new goog.swlib.Route({
* match: (url) => {
* // Return true or false
* return true;
* },
* handler: {
* handle: (args) => {
* // The requested URL
* console.log(args.url);
*
* // The FetchEvent to handle
* console.log(args.event);
*
* // The parameters from the matching route.
* console.log(args.params);
*
* // Return a promise that resolves with a Response.
* return fetch(args.url);
* },
* },
* });
* self.goog.swlib.router.registerRoute(routeInstance);
*
* @memberof module:sw-lib
*/
class RouterWrapper {
/**
Expand All @@ -32,16 +81,16 @@ class RouterWrapper {
}

/**
* @param {String|Regex|Route} capture the The capture for a route can be one
* @param {String|Regex|Route} capture The capture for a route can be one
* of three types.
* 1. It can be an Express style route, like: '/example/:anything/route/'
* The only gotcha with this is that it will only capture URL's on your
* origin.
* 2. A regex that will be tested against request URL's.
* 3. A Route object
* 1. It can be an Express style route, like: '/example/:anything/route/'
* The only gotcha with this is that it will only capture URL's on your
* origin.
* 1. A regex that will be tested against request URL's.
* 1. A [Route]{@link module:sw-routing.Route} instance.
* @param {function|Handler} handler The handler is ignored if you pass in
* a Route, otherwise it's required. The handler will be called when the route
* is caught by the capture criteria.
* a Route object, otherwise it's required. The handler will be called when
* the route is caught by the capture criteria.
*/
registerRoute(capture, handler) {
if (typeof handler === 'function') {
Expand Down
134 changes: 113 additions & 21 deletions packages/sw-lib/src/lib/sw-lib.js
Expand Up @@ -17,36 +17,51 @@

import RouterWrapper from './router-wrapper.js';
import ErrorFactory from './error-factory.js';
import {PrecacheManager}
from '../../../sw-precaching/src/index.js';
import {PrecacheManager} from '../../../sw-precaching/src/index.js';
import {
CacheFirst, CacheOnly, NetworkFirst, NetworkOnly, StaleWhileRevalidate,
} from '../../../sw-runtime-caching/src/index.js';

/**
* This is a high level library to help with using service worker
* precaching and run time caching.
*
* @memberof module:sw-lib
*/
class SWLib {
/**
* Initialises the classes router wrapper.
* Initialises an instance of SWLib. An instance of this class is
* accessible when the module is imported into a service worker as
* `self.goog.swlib`.
*/
constructor() {
this._router = new RouterWrapper();
this._precacheManager = new PrecacheManager();
this._strategies = {
Copy link
Contributor

Choose a reason for hiding this comment

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

When we talked about this last week, I misunderstood and thought that your plan was to export the class definitions—basically, aliasing them under the sw-lib module namespace. I didn't realize that your suggestion was to create instances of each class and include those in each SWLib instance.

These strategies are most useful when you have some control over their behavior via the RequestWrapper class, but there's no way to configure these instances. The default behavior will work, but in the back of my head I worry that people won't realize that they can do things like change the cache name or configure cache expiration if they're only familiar with using these unconfigured instances.

Do you all share those concerns?

Copy link
Author

Choose a reason for hiding this comment

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

Yes and no.

This API matches that of sw-toolbox, that's all I was aiming for with this as it does cover a simple use case.

There are three 1 options:

  1. This approach and expect developers to use the smaller modules is desired (not ideal).
  2. This approach + expose the classes as well, i.e. allow new goog.swlib.strategies.<Name of Strategy>(). Simple use case plus allows configuration.
  3. Just include the class strategies. Only con is that is requires some boilerplate for basic use.

Copy link
Contributor

Choose a reason for hiding this comment

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

The sw-toolbox-y way of doing this was to pass in the configuration as the third parameter.

I guess we could take inspiration from that and allow developers to pass in a RequestWrapper as an optional third parameter.

For that approach to work, the second parameter should be the an uninstantiated reference to a sw-runtime-caching class, and then SWLib would construct the class at runtime, passing along the RequestWrapper to the constructor if it's provided.

Copy link
Author

Choose a reason for hiding this comment

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

In toolbox I would always go:

toolbox.router.all('/', toolbox.fastest);

The options generally would be global rather than per route.

Would it be worth me removing the strategies from this PR, landing the doc changes and then discussing the strategies in an issue?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, yanking them out of this PR and discussing the approach off-thread would make sense.

There are lots of common use cases for per-route configs that we'd need to support, like one route that used a dedicated image cache with a specific cache expiration policy, and a different route that used a dedicated api-response cache with a different policy.

cacheFirst: new CacheFirst(),
cacheOnly: new CacheOnly(),
networkFirst: new NetworkFirst(),
networkOnly: new NetworkOnly(),
fastest: new StaleWhileRevalidate(),
};
}

/**
* Can be used to define debug logging, default cache name etc.
* @param {Object} options The options to set.
*/
setOptions(options) {

}

/**
* If there are assets that are revisioned, they can be cached intelligently
* Revisioned assets can be cached intelligently
* during the install (i.e. old files are cleared from the cache, new files
* are added tot he cache and unchanged files are left as is).
* @param {Array<String>} revisionedFiles A set of urls to cache when the
* service worker is installed.
* are added to the cache and unchanged files are left as is).
*
* @example
* self.goog.swlib.cacheRevisionedAssets([
* '/styles/main.1234.css',
* {
* url: '/index.html',
* revision: '1234'
* }
* ]);
*
* @param {Array<String|Object>} revisionedFiles A set of urls to cache
* when the service worker is installed.
*/
cacheRevisionedAssets(revisionedFiles) {
// Add a more helpful error message than assertion error.
Expand All @@ -60,10 +75,18 @@ class SWLib {
}

/**
* If there are assets that should be cached on the install step but
* aren't revisioned, you can cache them here.
* @param {Array<String>} unrevisionedFiles A set of urls to cache when the
* service worker is installed.
* Any assets you wish to cache which can't be revisioned should be
Copy link
Contributor

Choose a reason for hiding this comment

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

How about:

Any assets you wish to cache ahead of time which can't be revisioned should be...

Since in many of the cases for unversioned resources, just populating the caches via a runtime caching strategy is desirable.

Copy link
Author

Choose a reason for hiding this comment

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

done.

* cached with this method. All assets are cached on install regardless of
* whether an older version of the request is in the cache.
*
* @example
* self.goog.swlib.warmRuntimeCache([
* '/scripts/main.js',
* new Request('/images/logo.png')
* ]);
*
* @param {Array<String|Request>} unrevisionedFiles A set of urls to cache
* when the service worker is installed.
*/
warmRuntimeCache(unrevisionedFiles) {
// Add a more helpful error message than assertion error.
Expand All @@ -77,12 +100,81 @@ class SWLib {
}

/**
* A getter for the Router Wrapper.
* @return {RouterWrapper} Returns the Router Wrapper
* You can access the [Router]{@link module:sw-lib.RouterWrapper}
* with `self.goog.swlib.router`.
* @return {RouterWrapper} Returns the Router.
*/
get router() {
return this._router;
}

/**
* This handler will check is there is a cache response and respond with it if
* there is, otherwise it will make a network request and respond with that,
* caching the response for the next time it's requested.
*
* @example
* self.goog.swlib.router.registerRoute('/', self.google.swlib.cacheFirst);
*
* @return {Handler} A CacheFirst response handler.
*/
get cacheFirst() {
return this._strategies.cacheFirst;
}

/**
* This handler will check is there is a cache response and respond with it if
* there is, otherwise it will throw an error.
*
* @example
* self.goog.swlib.router.registerRoute('/', self.google.swlib.cacheOnly);
*
* @return {Handler} A CacheOnly response handler.
*/
get cacheOnly() {
return this._strategies.cacheOnly;
}

/**
* This handler will attempt to get a response from the network and respond
* with it if available, updating the cache as well. If the network request
* fails, it will respond with any cached response available.
*
* @example
* self.goog.swlib.router.registerRoute('/', self.google.swlib.networkFirst);
*
* @return {Handler} A NetworkFirst response handler.
*/
get networkFirst() {
return this._strategies.networkFirst;
}

/**
* This handle will only return with network requests.
*
* @example
* self.goog.swlib.router.registerRoute('/', self.google.swlib.networkOnly);
*
* @return {Handler} A NetworkOnly response handler.
*/
get networkOnly() {
return this._strategies.networkOnly;
}

/**
* This handler will check the cache and make a network request for all
* requests. If the caches has a value it will be returned and when the
* network request has finished, the cache will be updated. If there is no
* cached response, the request will be forfilled by the network request.
*
* @example
* self.goog.swlib.router.registerRoute('/', self.google.swlib.fastest);
*
* @return {Handler} A StaleWhileRevalidate response handler.
*/
get fastest() {
return this._strategies.fastest;
}
}

export default SWLib;
5 changes: 5 additions & 0 deletions packages/sw-lib/test/browser-unit/library-namespace.js
Expand Up @@ -51,6 +51,11 @@ describe('Test Behaviors of Loading the Script', function() {
return window.goog.mochaUtils.registerServiceWorkerMochaTests(serviceWorkerPath);
});

it('should perform caching-strategies.js sw tests', function() {
const serviceWorkerPath = 'sw-unit/caching-strategies.js';
return window.goog.mochaUtils.registerServiceWorkerMochaTests(serviceWorkerPath);
});

it('should perform cache-revisioned-assets.js sw tests', function() {
const serviceWorkerPath = 'sw-unit/cache-revisioned-assets.js';
return window.goog.mochaUtils.registerServiceWorkerMochaTests(serviceWorkerPath);
Expand Down
44 changes: 44 additions & 0 deletions packages/sw-lib/test/browser-unit/sw-unit/caching-strategies.js
@@ -0,0 +1,44 @@
importScripts('/node_modules/mocha/mocha.js');
importScripts('/node_modules/chai/chai.js');
importScripts('/node_modules/sw-testing-helpers/build/browser/mocha-utils.js');

importScripts('/packages/sw-lib/build/sw-lib.min.js');

/* global goog */

const expect = self.chai.expect;
self.chai.should();
mocha.setup({
ui: 'bdd',
reporter: null,
});

describe('Test swlib.cacheOnly', function() {
it('should be accessible goog.swlib.cacheOnly', function() {
Copy link
Member

Choose a reason for hiding this comment

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

We talked about why these functions aren't just using arrow syntax offline. I think your reason about Mocha breaking decorated it bindings etc if arrows are used was fine. I did see https://github.com/skozin/arrow-mocha in case we wanted to explore how to work around this in future.

Copy link
Author

Choose a reason for hiding this comment

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

I'm probably going to stick with the function name for now. Ideally Mocha would change to have a more explicit approach of declaring the timeout and retries of a unit test.

expect(goog.swlib.cacheOnly).to.exist;
});
});

describe('Test swlib.cacheFirst', function() {
it('should be accessible goog.swlib.cacheFirst', function() {
expect(goog.swlib.cacheFirst).to.exist;
});
});

describe('Test swlib.networkOnly', function() {
it('should be accessible goog.swlib.networkOnly', function() {
expect(goog.swlib.networkOnly).to.exist;
});
});

describe('Test swlib.networkFirst', function() {
it('should be accessible goog.swlib.networkFirst', function() {
expect(goog.swlib.networkFirst).to.exist;
});
});

describe('Test swlib.fastest', function() {
it('should be accessible goog.swlib.fastest', function() {
expect(goog.swlib.fastest).to.exist;
});
});