Description
Here are the requirements I'm working towards:
- Be able to bypass the service worker for particular requests.
- Speed up simple offline-first, online-first routes by avoiding service worker startup time.
- Be polyfillable – do not introduce things that cannot already be done in a fetch event.
- Be extensible – consider what future additions to the API might look like.
- Avoid state on the registration if possible – prefer state on the service worker itself.
I'm going to start with static routes, and provide additional ideas in follow-up posts.
The aim is to allow the developer to declaratively express a series of steps the browser should perform in attempt to get a response.
The rest of this post is superseded by the second draft
Creating a route
WebIDL
// Install currently uses a plain ExtendableEvent, so we'd need something specific
partial interface ServiceWorkerInstallEvent {
attribute ServiceWorkerRouter router;
}
[Exposed=ServiceWorker]
interface ServiceWorkerRouter {
void add(ServiceWorkerRouterItem... items);
}
[Exposed=ServiceWorker]
interface ServiceWorkerRouterItem {}
JavaScript
addEventListener('install', (event) => {
event.router.add(...items);
event.router.add(...otherItems);
});
The browser will consider routes in the order declared, and will consider route items in the order they're given.
Route items
Route items fall into two categories:
- Conditions – These determine if additional items should be considered.
- Sources – A place to attempt to get a response from.
Sources
WebIDL
[Exposed=ServiceWorker, Constructor(optional RouterSourceNetworkOptions options)]
interface RouterSourceNetwork : ServiceWorkerRouterItem {}
dictionary RouterSourceNetworkOptions {
// A specific request can be provided, otherwise the current request is used.
Request request;
}
[Exposed=ServiceWorker, Constructor(optional RouterSourceCacheOptions options)]
interface RouterSourceCache : ServiceWorkerRouterItem {}
RouterSourceCacheOptions : MultiCacheQueryOptions {
// A specific request can be provided, otherwise the current request is used.
Request request;
}
[Exposed=ServiceWorker, Constructor(optional RouterSourceFetchEventOptions options)]
interface RouterSourceFetchEvent : ServiceWorkerRouterItem {}
dictionary RouterSourceFetchEventOptions {
DOMString id = '';
}
These interfaces don't currently have attributes, but they could have attributes that reflect the options/defaults passed into the constructor.
Conditions
WebIDL
[Exposed=ServiceWorker, Constructor(ByteString method)]
interface RouterIfMethod : ServiceWorkerRouterItem {}
[Exposed=ServiceWorker, Constructor(USVString url, optional RouterIfURLOptions options)]
interface RouterIfURL : ServiceWorkerRouterItem {}
dictionary RouterIfURLOptions {
boolean ignoreSearch = false;
}
[Exposed=ServiceWorker, Constructor(USVString url)]
interface RouterIfURLPrefix : ServiceWorkerRouterItem {}
[Exposed=ServiceWorker, Constructor(USVString url, optional RouterIfURLOptions options)]
interface RouterIfURLSuffix : ServiceWorkerRouterItem {}
[Exposed=ServiceWorker, Constructor(optional RouterIfDateOptions options)]
interface RouterIfDate : ServiceWorkerRouterItem {}
dictionary RouterIfDateOptions {
// These should accept Date objects too, but I'm not sure how to do that in WebIDL.
unsigned long long from = 0;
// I think Infinity is an invalid value here, but you get the point.
unsigned long long to = Infinity;
}
[Exposed=ServiceWorker, Constructor(optional RouterIfRequestOptions options)]
interface RouterIfRequest : ServiceWorkerRouterItem {}
dictionary RouterIfRequestOptions {
RequestDestination destination;
RequestMode mode;
RequestCredentials credentials;
RequestCache cache;
RequestRedirect redirect;
}
Again, these interfaces don't have attributes, but they could reflect the options/defaults passed into the constructor.
Shortcuts
GET requests are the most common type of request to provide specific routing for.
WebIDL
partial interface ServiceWorkerRouter {
void get(ServiceWorkerRouterItem... items);
}
Where the JavaScript implementation is roughly:
router.get = function(...items) {
router.add(new RouterIfMethod('GET'), ...items);
};
We may also consider treating strings as URL matchers.
router.add('/foo/')
===router.add(new RouterIfURL('/foo/'))
.router.add('/foo/*')
===router.add(new RouterIfURLPrefix('/foo/'))
.router.add('*.png')
===router.add(new RouterIfURLSuffix('.png'))
.
Examples
Bypassing the service worker for particular resources
JavaScript
// Go straight to the network after 25 hrs.
router.add(
new RouterIfDate({ from: Date.now() + 1000 * 60 * 60 * 25 }),
new RouterSourceNetwork(),
);
// Go straight to the network for all same-origin URLs starting '/videos/'.
router.add(
new RouterIfURLPrefix('/videos/'),
new RouterSourceNetwork(),
);
Offline-first
JavaScript
router.get(
// If the URL is same-origin and starts '/avatars/'.
new RouterIfURLPrefix('/avatars/'),
// Try to get a match for the request from the cache.
new RouterSourceCache(),
// Otherwise, try to fetch the request from the network.
new RouterSourceNetwork(),
// Otherwise, try to get a match for the request from the cache for '/avatars/fallback.png'.
new RouterSourceCache({ request: '/avatars/fallback.png' }),
);
Online-first
JavaScript
router.get(
// If the URL is same-origin and starts '/articles/'.
new RouterIfURLPrefix('/articles/'),
// Try to fetch the request from the network.
new RouterSourceNetwork(),
// Otherwise, try to match the request in the cache.
new RouterSourceCache(),
// Otherwise, if the request destination is 'document'.
new RouterIfRequest({ destination: 'document' }),
// Try to match '/articles/offline' in the cache.
new RouterSourceCache({ request: '/articles/offline' }),
);
Processing
This is very rough prose, but hopefully it explains the order of things.
A service worker has routes. The routes do not belong to the registration, so a new empty service worker will have no defined routes, even if the previous service worker defined many.
A route has items.
To create a new route containing items
- If the service worker is not "installing", throw. Routes must be created before the service worker has installed.
- Create a new route with items, and append it to routes.
Handling a fetch
These steps will come before handling navigation preload, meaning no preload will be made if a route handles the request.
request is the request being made.
- Let routerCallbackId be the empty string.
- RouterLoop: For each route of this service worker's routes:
- For each item of route's items:
- If item is a
RouterIfMethod
, then:- If item's method does not equal request's method, then break.
- Otherwise, if item is a
RouterIfURL
, then:- If item's url does not equal request's url, then break.
- Etc etc for other conditions.
- Otherwise, if item is a
RouterSourceNetwork
, then:- Let networkRequest be item's request.
- If networkRequest is null, then set networkRequest to request.
- Let response be the result of fetching networkRequest.
- If response is not an error, return response.
- Otherwise, if item is a
RouterSourceCache
, then:- Let networkRequest be item's request.
- If networkRequest is null, then set networkRequest to request.
- Let response be the result of looking for a match in the cache, passing in item's options.
- If response is not null, return response.
- Otherwise, if item is a
RouterSourceFetchEvent
, then:- Set routerCallbackId to item's id.
- Break RouterLoop.
- If item is a
- For each item of route's items:
- Call the fetch event as usual, but with routerCallbackId as one of the event properties.
Extensibility
I can imagine things like:
RouterOr(...conditionalItems)
– True if any of the conditional items are true.RouterNot(condition)
– Inverts a condition.RouterIfResponse(options)
– Right now, a response is returned immediately once one is found. However, the route could continue, skipping sources, but processing conditions. This condition could check the response and break the route if it doesn't match. Along with a way to discard any selected response, you could discard responses that didn't have an ok status.RouterCacheResponse(cacheName)
– If a response has been found, add it to a cache.RouterCloneRequest()
– It feels likeRouterSourceNetwork
would consume requests, so if you need to do additional processing, this could clone the request.
But these could arrive much later. Some of the things in the main proposal may also be considered "v2".