Re: [w3c/ServiceWorker] Declarative routing (#1373)

Ok, here's a second draft:

# Creating a route

**WebIDL**

```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(
    (RouterCondition or sequence<RouterCondition>) conditions,
    (RouterSource or sequence<RouterSource>) sources,
  );
}

[Exposed=ServiceWorker]
interface RouterSource {}

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

**JavaScript**

```js
addEventListener('install', (event) => {
  event.router.add(conditions, sources);
  event.router.add(otherConditions, otherSources);
});
```

The browser will consider routes in the order declared, and if all conditions match, each source will be tried in turn.

# Conditions

These determine if a particular static route should be used rather than dispatching a fetch event.

**WebIDL**

```webidl
[Exposed=ServiceWorker, Constructor(ByteString method)]
interface RouterIfMethod : RouterCondition {}

[Exposed=ServiceWorker, Constructor(USVString url, optional RouterIfURLOptions options)]
interface RouterIfURL : RouterCondition {}

dictionary RouterIfURLOptions {
  boolean ignoreSearch = false;
}

[Exposed=ServiceWorker, Constructor(USVString url)]
interface RouterIfURLPrefix : RouterCondition {}

[Exposed=ServiceWorker, Constructor(USVString url, optional RouterIfURLOptions options)]
interface RouterIfURLSuffix : RouterCondition {}

[Exposed=ServiceWorker, Constructor(optional RouterIfDateOptions options)]
interface RouterIfDate : RouterCondition {}

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 : RouterCondition {}

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.

## Sources

These determine where the route should try to get a response from.

**WebIDL**

```webidl
[Exposed=ServiceWorker, Constructor(optional RouterSourceNetworkOptions options)]
interface RouterSourceNetwork : RouterSource {}

dictionary RouterSourceNetworkOptions {
  // A specific request can be provided, otherwise the current request is used.
  Request request;
}

[Exposed=ServiceWorker, Constructor(optional RouterSourceCacheOptions options)]
interface RouterSourceCache : RouterSource {}

RouterSourceCacheOptions : MultiCacheQueryOptions {
  // A specific request can be provided, otherwise the current request is used.
  Request request;
}

[Exposed=ServiceWorker, Constructor(optional RouterSourceFetchEventOptions options)]
interface RouterSourceFetchEvent : RouterSource {}

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.

# Shortcuts

GET requests are the most common type of request to provide specific routing for.

**WebIDL**

```webidl
partial interface ServiceWorkerRouter {
  void get(/* same as add */);
}
```

Where the **JavaScript** implementation is roughly:

```js
router.get = function(conditions, sources) {
  if (conditions instanceof RouterCondition) {
    conditions = [conditions];
  }
  router.add([new RouterIfMethod('GET'), ...conditions], sources);
};
```

We may also consider treating strings as URL matchers.

* `router.add('/foo/', sources)` === `router.add(new RouterIfURL('/foo/'), sources)`.
* `router.add('/foo/*', sources)` === `router.add(new RouterIfURLPrefix('/foo/'), sources)`.
* `router.add('*.png', sources)` === `router.add(new RouterIfURLSuffix('.png'), sources)`.

# Examples

## Bypassing the service worker for particular resources

**JavaScript**

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

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

**JavaScript**

```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, 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 **conditions** and **sources**.

## To create a new route containing *conditions* and *sources*

1. If the service worker is not "installing", throw. Routes must be created before the service worker has installed.
1. Create a new route with *conditions* and *sources*, 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. RouterLoop: For each *route* of this service worker's **routes**:
    1. For each *condition* of *route*'s **conditions**:
        1. If *condition* is a `RouterIfMethod`, then:
            1. If *condition*'s **method** does not equal *request*'s **method**, then continue RouterLoop.
        1. Otherwise, if *condition* is a `RouterIfURL`, then:
            1. If *condition*'s **url** does not equal *request*'s **url**, then continue RouterLoop.
        1. Etc etc for other conditions.
    1. For each *source* of *route*'s **sources**:
        1. If *source* is a `RouterSourceNetwork`, then:
            1. Let *networkRequest* be *source*'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 *source* is a `RouterSourceCache`, then:
            1. Let *networkRequest* be *source*'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 *source*'s options.
            1. If *response* is not null, return *response*.
        1. Otherwise, if *source* is a `RouterSourceFetchEvent`, then:
            1. Set *routerCallbackId* to *source*'s **id**.
            1. Call the fetch event as usual, but with *source*'s **id** as one of the event properties.
            1. Return.
    1. Return a network error.
1. Call the fetch event as usual.

# Extensibility

I can imagine things like:

* `RouterOr(...conditionalItems)` – True if any of the conditional items are true.
* `RouterNot(condition)` – Inverts a condition.
* `RouterFilterResponse(options)` – Right now, a response is returned immediately once one is found. However, the route could continue, skipping sources, but processing filters. This could check the response and discard it if it doesn't match. An example would be discarding responses that don't have an ok status.
* `RouterCacheResponse(cacheName)` – If a response has been found, add it to a cache.

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#issuecomment-451436409

Received on Friday, 4 January 2019 12:55:49 UTC