w3c / ServiceWorker

Service Workers
https://w3c.github.io/ServiceWorker/
Other
3.63k stars 315 forks source link

Declarative routing #1373

Open jakearchibald opened 5 years ago

jakearchibald commented 5 years ago

Here are the requirements I'm working towards:

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:

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.

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

  1. If the service worker is not "installing", throw. Routes must be created before the service worker has installed.
  2. 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.
  2. 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.
      2. Otherwise, if item is a RouterIfURL, then:
        1. If item's url does not equal request's url, then break.
      3. Etc etc for other conditions.
      4. Otherwise, if item is a RouterSourceNetwork, then:
        1. Let networkRequest be item's request.
        2. If networkRequest is null, then set networkRequest to request.
        3. Let response be the result of fetching networkRequest.
        4. If response is not an error, return response.
      5. Otherwise, if item is a RouterSourceCache, then:
        1. Let networkRequest be item's request.
        2. If networkRequest is null, then set networkRequest to request.
        3. Let response be the result of looking for a match in the cache, passing in item's options.
        4. If response is not null, return response.
      6. Otherwise, if item is a RouterSourceFetchEvent, then:
        1. Set routerCallbackId to item's id.
        2. Break RouterLoop.
  3. Call the fetch event as usual, but with routerCallbackId as one of the event properties.

Extensibility

I can imagine things like:

But these could arrive much later. Some of the things in the main proposal may also be considered "v2".

richardkazuomiller commented 5 years ago

@jakearchibald

We could also add things like { url: { pathStartsWith: '/foo/' } } to match against particular components of the URL, but you'd be matching against all origins unless you specify otherwise.

I think that's fine because sometimes we care about checking the domain and we don't at other times. Here's some code that's more-or-less exactly what I'm doing on a real-world service worker.

self.addEventListener('fetch', function(event) {
  const request = event.request;
  event.respondWith(
    caches.match(request)
      .then(function(response) {
        if (response) {
          return response;
        }
        const url = new URL(request);
        if(url.pathname == '/some_path') {
          const redirectUrl = new URL(url.href);
          redirectUrl.pathname = '/';
          return Response.redirect(redirectUrl.href,302);
        }
        if(url.hostname == IMAGE_CDN_HOSTNAME) {
          return fetchFromImageCdn(request);
        }
        return fetch(request);
      })
  );
});

function fetchFromImageCdn (request) {
  // basically fetch once and cache forever
}

Technically, both https://mywebsite.foo/some_path and https://myimagecdn.foo/some_path will both redirect to /, but I know that the page will never try to access https://myimagecdn.foo/some_path so it's fine that both match.

I'd like to use declarative routing to do something like this

// Things that try the cache, then go to the network
const cacheThenNetwork = [RouterSource.cache(), RouterSource.network()];

// These paths match all domains, but I'm only using one so it's fine
router.get(RouterCondition.url.pathname.is('/'), cacheThenNetwork)
router.get(RouterCondition.url.pathname.startsWith('/static/'), cacheThenNetwork)
// Any path on the image CDN
router.get(RouterCondition.url.hostname.is(MY_IMAGE_CDN), cacheThenNetwork)

// fetch event listener can now be simplified
self.addEventListener('fetch', function(event) {
  const request = event.request;
  const url = new URL(request.url);
  if(url.pathname == '/some_path') {
    const redirectUrl = new URL(url.href);
    redirectUrl.pathname = '/';
    event.respondWith(Response.redirect(redirectUrl.href,302));
  }
});
jakearchibald commented 5 years ago

but I know that the page will never try to access https://myimagecdn.foo/some_path so it's fine that both match.

That seems really fragile vs:

if (url.origin === location.origin && url.pathname == '/some_path') {
  // …
}

I wouldn't want developers to be matching a path on all origins unless it's explicit.

richardkazuomiller commented 5 years ago

You're right; that would be better. My point is not so much that it's a good thing to have a path matching multiple domains, but having something like pathStartsWith isn't necessarily creating a new problem.

I know I could get my example to work using RouterIfURLStarts and RouterIfURL (or whatever equivalent API you decide to go with), so I think the conditions in your current draft are sufficient for a v1, but having conditions for each part of a request (href, protocol, hostname, pathname, search, method, and headers) would be nice to have.

To go even deeper, I'd love to be able to do something like

router.add(
  new RouterNot(new RouterConditionHasCookie('I_AM_LOGGED_IN')),
  new RouterSourceCache({ request: '/login.html' })
)
jakearchibald commented 5 years ago

After thinking about it, I think I'm going to go with the object based approach & @domenic's suggestion:

// Without options:
router.add(
  conditions,
  'cache',
);

// With options:
router.add(
  conditions,
  { type: 'cache', request: '/shell.html' },
);

// Multiple sources:
router.add(
  conditions,
  [
    'cache',
    'network',
    { type: 'cache', request: '/shell.html' },
  ],
);

I'm still not keen on having objects with a required property that determines the type of the object, but it seems better than the alternatives.

WORMSS commented 5 years ago

With the object based approach, how to you specify that you want 'cache' or 'network' as race?

RouterSourceFirst(...sources), RouterSourceRace(...sources) etc etc.

jakearchibald commented 5 years ago

@WORMSS

router.add(
  conditions,
  { type: 'race', sources: […sources]},
);

The pattern feels equally extensible.

bkardell commented 5 years ago

fwiw, @domenic and others expressed much better what I briefly tried to say yesterday in some other venue (twitter maybe?) - I like this recent turn though quite a bit.

Siilwyn commented 5 years ago

The option explored in https://github.com/w3c/ServiceWorker/issues/1373#issuecomment-452247362 feels more straightforward and in this case better than using classes, would definitely prefer this version!

jeremy-coleman commented 5 years ago

I'd like to suggest a somewhat different approach that I feel addresses the same issue in a more intuitive manner, which is to provide a (virtual) file system to code against the assets as they will be on the client and in the shape/structure they will actually be in. IE: image

jakearchibald commented 5 years ago

@jeremy-coleman The web isn't compatible with a filesystem. It's compatible with a request/response store, which is what the cache API provides. However, this alone can't communicate when a request should go to the network. I guess I don't understand your proposal fully.

jeremy-coleman commented 5 years ago

I guess from my perspective, the cached files are a file system. Instead of (req res) => fetchHttpRoute => interceptEverything someLogic => if(cache) else(usenet) => next

Req res => fetchCacheRoute => if(!nocache) fetchHttp => next

If you explicitly write the api to query the cached files first you can just drop the intercept all together.

Similar to how you might conventionally write an image element as <|img src=assets/icon.svg/> because you know the location post-build, the 2nd order of that idea would be to write the src as src=clientCache

jakearchibald commented 5 years ago

@jeremy-coleman in your system, how do you express "For any URL path ending .jpg, try to fetch from the network, otherwise fall back to this generic image…"?

jeremy-coleman commented 5 years ago

it'd be the same for both online and offline. Assuming all routes point to the cache first - if it's an online asset, replace with net when available. same for offline, just the timeout for the cache with online data would be 0ms / fetch onSomeUserEvent compared to something like a 24 hour reset for offline stuff. I think a harder thing to make understandable would be differences between something like 'use last successful' vs 'use constant fallback' , for the stuff above on how to handle conditionals, Proxy.revoke() with your conditional checks on propkeys access could probably handle everything needed.

for both online and offline from the examples above, i think something like this:

router.get(
  new RouterIfURLPrefix('/**/*.jpg/'),   // find some URL ending in jpg'.
  new RouterSourceCache(),//use the static asset.
maybe
  new RouterSourceNetwork(),   //Try to fetch the request from the network.   <-- this doesn't need to come first for online reqs , just use the current value in the cache first and always
maybe update static asset with req as new default fallback

somewhat unrelated but really what underpins my line of thinking is that I feel like offline apps should basically be completely downloaded into some form of local storage on 1st req, and the SW routes should support coding against the offline assets more-so than online sources.

jakearchibald commented 5 years ago

@jeremy-coleman based on your example above, I don't understand the difference between your proposal and mine.

asakusuma commented 5 years ago

A bit late to the party but...

+1 for the "side effect operation" issue raised by @jeffposnick and @nhoizey

In addition to refreshing the cache based on a network source, firing analytics beacons is another feature that would be heavily used by LinkedIn. We have a lot of instrumentation to measure how the service worker is working/not working. The highest priority V2 feature for us would be support for a routingcomplete or similar event, to handle both of these cases.

@jakearchibald

I think I'd make this an option to RouterSourceNetwork, as it's the only one that would benefit from a timeout right now.

I think the timeout feature would be useful for RouterSourceFetch, as it might address https://github.com/w3c/ServiceWorker/issues/1292.

Timeout support would be really nice for creating a global fallback "catch all" handler.

router.get(
  new RouterIfURLStarts(‘/profile/*‘),
  [new RouterSourceCache(), new RouterSourceNetwork({ timeout: 10000 })]
);

router.get(‘*’, new RouterSourceCache(‘/oops.html’))
asakusuma commented 5 years ago

re: glob matching, one use case that may not be covered without regex is the "match on any route, but no files" use case.

So if you want to match /profile/123 or /profile/123/details, but not profile/123/photo.jpg.

https://regexr.com/47c2t

jakearchibald commented 5 years ago

@n8schloss something I wanted to double check about this proposal: A service worker's routes live with the service worker. They can't be changed without shipping a new service worker. Does that work for you?

I ask because with navigation preload, you wanted to change things during the life of a service worker (the header value).

ylafon commented 5 years ago

@asakusuma it depends on the exact scope you want to match... and exclude. To take one of easiest syntax to express and combine facts:

  url: { startsWith: '/profile/123',  ignoreSearch: true,
  and: {
    not: {
      url: { endsWith: '.jpg', ignoreSearch: true},
      },
    },
  }

Being able to exclude and use boolean combinators is essential here to really express use-cases that are not the most simple ones. Also, syntax-wise,

and: {  url: { startsWith: '/profile/123',  ignoreSearch: true},
        not: { url: { endsWith: '.jpg', ignoreSearch: true} }
     }

Looks better.

n8schloss commented 5 years ago

@n8schloss something I wanted to double check about this proposal: A service worker's routes live with the service worker. They can't be changed without shipping a new service worker. Does that work for you?

I ask because with navigation preload, you wanted to change things during the life of a service worker (the header value).

Yep! As long as there's the RouterIfDate options then our use case here will be met :)

mgiuca commented 5 years ago

Late to the party but I'd like to give a bit of feedback.

I love the general concept. Seems like this will solve a lot of problems (speed issues with spinning up SWs, and the added "scariness" of what if the fetch handler has a bug that makes my site non-updateable.) This came up as a potential solution to w3c/manifest#774.

I have some superficial criticism (API surface details).

WORMSS commented 5 years ago

Ignore search is because it's what the JavaScript code calls it In window.location

jakearchibald commented 5 years ago
  • I'd like to remove the startsWith and endsWith things in favour of just having a glob syntax. Fewer API calls, less verbose syntax, and more flexible.

This has come up a few times in this issue. I'm not against adding it at some point, but it feels like a big contentious thing to standardise for v1.

  • ignoreSearch is a confusing name (why not ignoreQuery)?

As @WORMSS says, it's for consistency with the rest of the platform. url.search, cache.match(url, { ignoreSearch: true }) etc etc.

  • The name router.get is confusing because a method called "get" implies it's going to return some information out of the router,

Yeah, maybe. Although this is how most node routers seem to do it.

mgiuca commented 5 years ago

@WORMSS Yeah it's called that in the URL object too. It's called query inside the spec language but now that I re-read it, I realised that the word "query" never appears in the API interface itself. So "search" is fine.

fallaciousreasoning commented 5 years ago

Curious whether we could use @wanderview's URLPattern proposal instead of startsWith/endsWith.

yoshisatoyanagisawa commented 1 year ago

FYI, we are about to implement a subset of the API. https://github.com/yoshisatoyanagisawa/service-worker-static-routing-api

webmaxru commented 1 year ago

Thanks for your work! FYI: I tweeted about it to get the developer community ready for experimenting :) https://twitter.com/webmaxru/status/1664214464999182338

sisidovski commented 1 year ago

Should we allow registerRouter() to be called multiple times?

As commented in https://github.com/w3c/ServiceWorker/issues/1373#issuecomment-1569530880, we are about to implement a subset, as ServiceWorker Static Routing API. https://github.com/yoshisatoyanagisawa/service-worker-static-routing-api

We (Google Chrome team) are ready for the Origin Trial, it's available from M116. In the meantime, we'd like to hear opinions around how InstallEvent.registerRouter() should work in this API.

Unlike add() or get() in the original Jake's proposal, registerRouter() sets all the routes at once, and it can only be called once. This is for ease of understanding the latest routes. However, we may rethink this limitation because we saw some interests in using this API from 3rd parties. 3rd parties in this context mean the SW scripts which are served from cross origins, and imported by the main SW script via importScripts() or ES modules.

Like Speed Kit, some companies provide ServiceWorker scripts as SDK, and customer websites use their scripts via importScripts(). If both a 3rd party provider and a customer want to use registerRouter(), in our current implementation the browser only registers the routing info called for the first time, and throws type error for the second registerRouter() call.

I personally feel it makes sense that 3rd parties use registerRouter() to handle something, but do you think we should support multiple registerRouter() calls? If so, a naive approach is just adding a new routing rule into existing rules, but do we need additional mechanisms or API surfaces to manage registered routes more smartly?

cc: @ErikWitt

domenic commented 1 year ago

I think allowing multiple calls is fine. In that case I would rename the method from registerRouter() to registerRoutes().

The naive approach makes perfect sense to me. In other words, registerRoutes(a); registerRoutes(b); should be equivalent to registerRoutes([...a, ...b]);. I think that is what everyone would expect.

jakearchibald commented 1 year ago

Given that, is there a benefit to registerRoutes vs calling registerRoute as many times as needed? One route at a time would make it easier to tell which route caused a throw.

ErikWitt commented 1 year ago

Heythere, sorry for being so late to the discussion. My two cents:

  1. Calling registerRoute from multiple handlers would be awesome to make combining multiple Service Workers easier. We have that case from time to time and it can be a complex task.

  2. Calling registerRoutes outside the install event is off the table right? It's not a deal breaker but would have been convenient. At the moment out Service Worker loads a configuration from indexeddb (the service worker is generic, every customer has a unique config), so it would be convenient to configure the router outside the install event. That said, we are looking into inlining the config in our shipped service worker for every customer which would resolve that issue for us.

  3. Would it be possible to make the origin trial a 3rd party origin trial? I.e. could send the origin trial token in the 3rd party script imported into the same origin service worker? If it is not a third part origin trial, our customers would need to implement the header themselves which is a lengthy process for large e-commerce companies.

btw. I love the use of URLPattern in the api :) Looking forward to trying this in production and reporting back on the performance gains

sisidovski commented 1 year ago

Thank you all for the feedback!

@jakearchibald That's a fair point. WDYT @yoshisatoyanagisawa ?

@ErikWitt

Calling registerRoutes outside the install event is off the table right?

At the moment we want to keep it inside the install event. We don't want to make the API dynamically configurable. If you have a workaround please consider doing so.

Would it be possible to make the origin trial a 3rd party origin trial?

OriginTrial for ServiceWorker features is running on a bit different mechanism, and unfortunately 3rd party origin trial is not supported yet. We understood the use case from 3rd party context. Let us consider how to deal with it, but please expect it takes some time to solve.

sisidovski commented 1 year ago

I came up with this scenario, perhaps it can be a problem for some cases.

a.com/sw.js:

importScripts('b.com/imported-sw.js');

addEventListener('install', (event) => {
  event.registerRouter({
    condition: {
     // example.jpg is returned form the fetch handler.
      urlPattern: {pathname: "example.jpg"}
    },
    source: "fetch-event"
  });
})

b.com/imported-sw.js:

addEventListener('install', (event) => {
  event.registerRouter({
    condition: {
     // All jpg resources are fetched from the network.
      urlPattern: {pathname: "*.jpg"}
    },
    source: "network"
  });
})

registerRouter() in imported-sw.js is executed first, and then the one in sw.js is executed. In the current algorithm, the API simply has the list of registered routing info and try to evaluate from the first item. So any requests to jpg resources, including example.jpg are matched with {pathname: "*.jpg"} which is registered in the imported-sw.js and the routing info registered by the main sw.js is never used.

Do you think the API should have a mechanism to address this case which is introduced by allowing multiple registerRoutes or registerRoute calls? I think this is kind of a WAI behavior, but love to hear anyone's thoughts around it.

domenic commented 1 year ago

I think this behavior is WAI. If you call importScripts() on a script before your own install handler, you are giving that script priority to install routes.

If you would like to have your own code take priority, then you should rearrange sw.js like so:

addEventListener('install', (event) => {
  event.registerRouter(/* ... */);
});

importScripts('b.com/imported-sw.js');
jakearchibald commented 1 year ago

It could be addRoutes(...routes).

I usually don't like that pattern, since it prevents adding new parameters in future, but if seems like any options would be per route.

add is shorter than register, and add makes it clearer that it's additive to the previous call. It might not make it clear that previous routes are cleared with the installation of a new service worker, but I'm not sure register makes that clear either.

yoshisatoyanagisawa commented 1 year ago

Sorry for being late to the discussion. When I wrote the explainer, I did not consider 3rd party SW providers to use the API. However, considering the use case, it makes more sense to allow the routes updated multiple times.

For the naming of the API, I remember that we called it register instead of add to clarify it is a write once operation. Note that Jake's original proposal called it add and allowed to call it multiple times. If we allow it to be called multiple times, I prefer to rename it add or append instead, which sounds more like the rules can grow.

By the way, I have concerns on calling the API multiple times, 1) Is the order of the API call guaranteed? The current registerRouter is an asynchronous API. When the multiple rule has been installed via the API, how should the order of the routes be? I am not so much familiar with JavaScript, but if JavaScript may not wait for the previous API call, then the order of rules may flip? 2) I also have the same concern as @sisidovski. If somebody sets a rule that has an intersection with other rules, it may interfere unexpectedly. Moreover, if somebody sets a rule like:

  {
    condition: {}, // i.e. matches everything.
    source: "fetch-handler"
  }

all rule updates after the rule would be just ignored. I guess it would be fine to be WAI if the order of rules is guaranteed.

Calling registerRoutes outside the install event

It is intended to make the rule updated only inside the install event. We were concerned about the difficulty of understanding rule applications to inflight requests.

Would it be possible to make the origin trial a 3rd party origin trial?

FYI, https://bugs.chromium.org/p/chromium/issues/detail?id=1471005

is there a benefit to registerRoutes vs calling registerRoute as many times as needed?

Current API accepts both sequence of routes and a route. I suppose you suggest only accepting a route. I feel it is fine to allow both ways of writing a rule. i.e. sequence of rules and a rule. For those who want to understand an error, they can write a rule one by one. For those who want to write a rule at once, it is also allowed.

Will you elaborate more on why you want to prohibit accepting a sequence of rules?

yoshisatoyanagisawa commented 11 months ago

Just FYI, the issues on the static routing API have been filed in https://github.com/WICG/service-worker-static-routing-api/issues. Please take a look. Also, I created an issue for "Should we allow registerRouter() to be called multiple times?" as https://github.com/WICG/service-worker-static-routing-api/issues/10 for ease of focusing on this discussion.

yoshisatoyanagisawa commented 6 months ago

FYI, we are discussing on the way to handle an empty "not" condition in https://github.com/WICG/service-worker-static-routing-api/issues/22. We are leaning on raising. Please feel free to share your opinions.

quasi-mod commented 3 days ago

FYI, we are currently actively discussing extending the resource timing API to support static routing API. https://github.com/w3c/resource-timing/issues/389