[w3c/ServiceWorker] Declarative routing (#1373)

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.

# Creating a route

```webidl
partial interface ServiceWorkerGlobalScope {
  attribute ServiceWorkerRouter router;
}

[Exposed=ServiceWorker]
interface ServiceWorkerRouter {
  void add(ServiceWorkerRouterItem... items);
}

[Exposed=ServiceWorker]
interface ServiceWorkerRouterItem {}
```

```js
router.add(...items);
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 have attributes, but they could 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 implementation is roughly:

```js
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

```js
// Go straight to the network after 25 hrs.
router.add(
  new RouterIfDate({ to: 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

```js
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

```js
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*

1. If the service worker has activated, throw. Routes must be created before the service worker has activated. They may be created during the install/activate event however. This might be useful if network data is needed to decide which routes to use.
1. 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.

1. Let *routerCallbackId* be the empty string.
1. RouterLoop: For each *route* of this service worker's **routes**:
    1. For each *item* of *route*'s **items**:
        1. If *item* is a `RouterIfMethod`, then:
            1. If *item*'s **method** does not equal *request*'s **method**, then break.
        1. Otherwise, if *item* is a `RouterIfURL`, then:
            1. If *item*'s **url** does not equal *request*'s **url**, then break.
        1. Etc etc for other conditions.
        1. Otherwise, if *item* is a `RouterSourceNetwork`, then:
            1. Let *networkRequest* be *item*'s **request**.
            1. If *networkRequest* is null, then set *networkRequest* to *request*.
            1. Let *response* be the result of fetching *networkRequest*.
            1. If *response* is not an error, return *response*.
        1. Otherwise, if *item* is a `RouterSourceCache`, then:
            1. Let *networkRequest* be *item*'s **request**.
            1. If *networkRequest* is null, then set *networkRequest* to *request*.
            1. Let *response* be the result of looking for a match in the cache, passing in *item*'s options.
            1. If *response* is not null, return *response*.
        1. Otherwise, if *item* is a `RouterSourceFetchEvent`, then:
            1. Set *routerCallbackId* to *item*'s **id**.
            1. Break RouterLoop.
1. 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 like `RouterSourceNetwork` 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".

-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/w3c/ServiceWorker/issues/1373

Received on Tuesday, 4 December 2018 15:07:45 UTC