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

I'm not keen on the use of constructors in this proposal, but I didn't think there was another way. I'd like to explore it further, and at least show my working.

```js
router.add(conditions, sources);
```

# Conditions

This could be a simple object:

```js
router.add({
  method: 'GET',
  url: { endsWith: '.mp4', ignoreSearch: true },
  date: { to: Date.now() + 1000 * 60 * 60 * 24 * 5 },
}, sources);
```

This seems pretty nice and easy to spec. You could also add some sensible defaults like `method: 'GET'`, where the developer would need to set this to an empty string if they wanted to cover all methods.

I'm worried about feature detection. The usual trick is to give the API an object with a getter, and see which properties are read. It isn't a great user experience, and especially horrible in this case as you'd have to add a dummy route just to feature detect. I think we'd want something like:

```js
router.supportsCondition('date', { to: Date.now() }); // boolean
```

I'm not quite sure how to handle 

In terms of extensibility, here's 'or' and 'not'.

```js
router.add({
  url: { endsWith: '.mp4', ignoreSearch: true },
  or: {
    url: { endsWith: '.jpg', ignoreSearch: true },
    or: {
      url: { endsWith: '.gif', ignoreSearch: true },
    },
  },
}, sources);

// vs:

router.add(
  new RouterIfAny(
    new RouterIfURL({ endsWith: '.mp4', ignoreSearch: true }),
    new RouterIfURL({ endsWith: '.jpg', ignoreSearch: true }),
    new RouterIfURL({ endsWith: '.gif', ignoreSearch: true }),
  ),
  sources,
);
```

…and:

```js
router.add({
  not: { method: 'POST' },
}, sources);

// vs:

router.add(
  new RouterNot(new RouterIfMethod('POST')),
  sources,
);
```

I was worried it might be difficult to figure out the order of operations, but it seems to read ok, provided we can have something like `supportsCondition`.

If we support 'or' and 'not', we'd probably want 'and' too:

```js
router.add({
  method: 'POST',
  and: {
    url: {
      endsWith: '.mp4', ignoreSearch: true,
      or: {
        url: { endsWith: '.gif', ignoreSearch: true },
      },
    },
  },
}, sources);
```

# Sources

Unlike conditions, the order matters here. It could be:

```js
router.add(conditions, [
  // Where each source is a enum string:
  'network',
  // Or an array of [source, options],
  ['cache', { request: '/shell.html' }],
]);
```

It's a bit of a weird convention, but I guess using classes is weird too. But, is it possible to spec this? The sticking point seems to be `[enumValue, options]` where the type of `options` depends on the value of `enumValue`. This is pretty easy to do in TypeScript, but I don't think it's possible in WebIDL (@domenic, am I correct here?).

This has the same feature detection problem, so we'd also want:

```js
router.supportsSource('cache', { request: '/shell.html' }); // boolean
```

If the WebIDL thing becomes a sticking point, `sources` could be a sequence of enums or `RouterSource` instances:

```js
router.add(conditions, [
  'network',
  new RouterSourceCache('/shell.html'),
]);
```

If every enum had an equivalent constructor, we wouldn't need `router.supportsSource`.

# Are instances useful?

If we stuck with classes (in either a full or partial way as mentioned above), source instances could have a method to perform their action, and condition instances could have a way to test them.

```js
const condition = new RouterIfURL({ startsWith: '/article/' });
condition.test(request); // boolean
const source = new RouterSourceCache({ ignoreSearch: true });
const response = await source.doYourThing(request);
```

This might be useful when writing tests, but I'm not sure if it's useful beyond that.

# Examples

Taking the initial examples from [my blog post](https://jakearchibald.com/2019/service-worker-declarative-router/):

```js
router.get(
  new RouterIfURLStarts('/avatars/'),
  [new RouterSourceCache(), new RouterSourceNetwork()],
);

// becomes:

router.add(
  { url: { startsWith: '/avatars/' } },
  ['cache', 'network'],
);
```

```js
router.get(
  new RouterIfURLEnds('.mp4', { ignoreSearch: true }),
  new RouterSourceNetwork(),
);

// becomes:

router.add(
  { url: { endsWith: '.mp4', ignoreSearch: true } },
  'network',
);
```

```js
router.get(
  new RouterIfURL('/', { ignoreSearch: true }),
  new RouterSourceCache('/shell.html'),
);

// becomes:

router.add(
  { url: { matches: '/', ignoreSearch: true } },
  new RouterSourceCache('/shell.html'),
);

// or:

router.add(
  { url: { matches: '/', ignoreSearch: true } },
  [['cache', { request: '/shell.html' }]],
);
```

That final example looks a bit weird, so I'm leaning towards keeping classes for sources, but allowing a string in cases where options aren't needed.

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

Received on Tuesday, 8 January 2019 10:21:34 UTC