Serviceworker: Enabling multiple Service Workers for a single scope

Created on 29 Jun 2016  Ā·  38Comments  Ā·  Source: w3c/ServiceWorker

Today, when a Web Performance service is interested in providing a Service Worker to help accelerate their customers’ sites, they have to make sure that their customers don’t already have a Service Worker that serves other use-cases on that same scope, or require their customers to modify their SW to include the acceleration service’s.

That may be tenable at the moment, but is likely to become less and less so, as more sites adopt Service Workers for flows that are more and more complex.

I’d love to see a way for such a service to install their own SW (which will be served from the customer’s domain), that would operate at a ā€œlower layerā€ than the site’s SW, so that requests would hit the site’s SW first, then the service’s one, and responses would flow back up in the reverse order. The service’s SW would be closest to the network, as it may use proprietary protocols which require decoding before the response is sent to the site’s SW.

Libraries like ServiceWorkerWare seem to enable a similar flow, but they require all service workers to be written as middleware. Would be great to have a native way to do this, without requiring code changes to the site's SW.

What should the API enable?

I'm glad you asked ;)

Registration

Upon registration, SW will either opt-in to modular registration or not. If not, we can assume that order doesn’t matter for that SW and that it should override any other SW that didn’t opt-in to modular registration.

For SW that did opt-in to modular registration, they should be able to state if they want to run first (i.e. closest to the app), last (i.e. closest to the network) or (potentially?) somewhere in between.

Let's define "down the stack" as "closer to the network" for the explanations below.

Fetch flow

Fetch events should cascade between SWs where a fetch() call (or failure to call respondWith()) in one SW will trigger a fetch event in the one below it in the stack (or closer to the network). If there is none, the fetch should go to the network.

A respondWith() call in a SW will return the promise of the fetch call issued by the nearest SW above it in the stack (or closer to the app). If there is none, the resource should return to Fetch (and the browser’s resource handlers).

Events

For message, sync and push events that are registered from the page’s context, we need a way for the page to register them for a specific SW.

For sync and push events it seems simple as SyncManager/PushManager are available on ServiceWorkerRegistration. Maybe a similar mechanism can be adopted for messages.

/cc @jakearchibald @crdumoul

enhancement

Most helpful comment

How having 2 SW on / is different from having one SW on / and second on /pages/, when serving /pages/article10.html? Will second SW "block" first SW and browser pretend that first SW doesn't exist?

Currently the most specific (longest) scope wins. So /pages/article10.html is controlled by the /pages/ service worker. The / service worker is not invoked at all.

All 38 comments

Is this mainly for handling fetches? If so, it feels like a same-origin foreign fetch.

I feel like there have been a number of folks at mozilla who wanted a more composable API for service workers. We've pretty much said that importScripts() is our method for composition, but it kind of sucks due to tight coupling in a single place.

On the other hand, though, I don't know if spinning up N worker threads for every network request is a good idea either.

On the other hand, though, I don't know if spinning up N worker threads for every network request is a good idea either.

Yeah, sounds like a big overhead, taking in account this issue: https://github.com/slightlyoff/ServiceWorker/issues/920

... unless everyone such worked will live in the same process and all will have the same active (_SW is working_) time. Which sounds weird too.

Is this mainly for handling fetches? If so, it feels like a same-origin foreign fetch.

I guess you can think of it that way, yeah.

On the other hand, though, I don't know if spinning up N worker threads for every network request is a good idea either.

Can't we run all of them on the same worker thread? At least the use cases I can think of don't need multiple workers running in parallel. One SW yields (by calling fetch() or respondWith()) and another takes over.

Just for the info, if one SW is registered at / scope and second on
/scripts/ scope. Then I fetch the /scripts/main.js file. Will browser spawn
both SW in the same process or in separate processes?

On Jun 30, 2016 11:31 AM, "Yoav Weiss" [email protected] wrote:

Is this mainly for handling fetches? If so, it feels like a same-origin
foreign fetch.

I guess you can think of it that way, yeah.

On the other hand, though, I don't know if spinning up N worker threads
for every network request is a good idea either.

Can't we run all of them on the same worker thread? At least the use cases
I can think of don't need multiple workers running in parallel. One SW
yields (by calling fetch() or respondWith()) and another takes over.

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/slightlyoff/ServiceWorker/issues/921#issuecomment-229596010,
or mute the thread
https://github.com/notifications/unsubscribe/ABIlkf8YYjKd2OaIhLKuF4shfKx_KYh3ks5qQ37ogaJpZM4JBS5x
.

@NekR - I assumed that since requests in different scopes are all in the same context, both will be handled by a single renderer, and therefore the same process. But I may be wrong, as there's a lot I don't know about how SW work.

Can't we run all of them on the same worker thread? At least the use cases I can think of don't need multiple workers running in parallel. One SW yields (by calling fetch() or respondWith()) and another takes over.

Well, this is possible in script today as you noted. You just have to importScripts() the framework you want to use. Since what you are proposing is opt-in as well, I'm not sure how baking this in to the browser is better at this time.

Just for the info, if one SW is registered at / scope and second on /scripts/ scope. Then I fetch the /scripts/main.js file. Will browser spawn both SW in the same process or in separate processes?

This is completely implementation dependent and could change over time. Ideally you should not rely on details like this.

I also think there are lots of unknowns that would have to be solved at the cost of increased complexity:

  1. What does updating a "second service worker" for the same scope look like? Can the primary service worker still be processing events while this happens? Or is it essentially treated as a single new service worker effectively? Running in the same thread implies a single new service worker.
  2. How would the separate service worker instances be exposed? Would registration.active by an array of ServiceWorker objects? How would the page know which one to postMessage() to?

I'm not saying that we should never do this, but I think its still pretty early days in terms of developers figuring out which patterns they want and need. My gut feeling is we should wait to see if a dominant design emerges in the framework ecosystem before baking something into the platform itself.

Well, this is possible in script today as you noted. You just have to importScripts() the framework you want to use. Since what you are proposing is opt-in as well, I'm not sure how baking this in to the browser is better at this time.

The advantage of baking this in is that, at least for the optimization service scenario, only the service SW has to opt in, and the site can remain oblivious to the fact that a SW is running in a lower layer.

The advantage of baking this in is that, at least for the optimization service scenario, only the service SW has to opt in, and the site can remain oblivious to the fact that a SW is running in a lower layer.

I don't understand what you mean here. The site can be oblivious to whether the SW script uses importScripts() as well. Did you mean the reverse? You can have SW scripts are oblivious to the fact they are running collaboratively with other SW scripts?

Yeah.
s/the site can remain oblivious/the site's SW can remain oblivious/

Basically, if I'm running such an optimization service, I want to be able to add my own optimization SW, yet I don't want my customers to have to change their existing code to make it work. In current script based solutions, I can potentially do that by overriding most SW related APIs, while hoping that sites won't register a SW using Link: headers or declarative markup.

Otherwise, I could use something like serviceworkerware and have to talk to customers and convince them to change their SW for my sake.

Neither is ideal.

I understand there's added complexity involved in baking this in. There's also a real use-case here, that cannot be entirely resolved in script.

Basically, if I'm running such an optimization service, I want to be able to add my own optimization SW, yet I don't want my customers to have to change their existing code to make it work. In current script based solutions, I can potentially do that by overriding most SW related APIs, while hoping that sites won't register a SW using Link: headers or declarative markup.

How do you do this without asking your clients to change their site? Do you inject a script at the CDN level?

As far as I can tell, this proposal seems to be based on the belief that it's easier for sites/optimization services to drop in a new <script> tag to register a second service worker, than it is for those same sites/optimization services to drop in a new importScripts() call to work within the existing service worker? Why would that be the case?

How would the separate service worker instances be exposed? Would registration.active by an array of ServiceWorker objects? How would the page know which one to postMessage() to?

How having 2 SW on / is different from having one SW on / and second on /pages/, when serving /pages/article10.html? Will second SW "block" first SW and browser pretend that first SW doesn't exist?

... I am not saying you have to add or not this proposal, just trying to understand.

How having 2 SW on / is different from having one SW on / and second on /pages/, when serving /pages/article10.html? Will second SW "block" first SW and browser pretend that first SW doesn't exist?

Currently the most specific (longest) scope wins. So /pages/article10.html is controlled by the /pages/ service worker. The / service worker is not invoked at all.

@wanderview I see, thanks. Then this proposal indeed introduces _a lot_ of complexity.

As far as I can tell, this proposal seems to be based on the belief that it's easier for sites/optimization services to drop in a new

I guess you just have to ask clients to use your performance-proof framework. I rally doubt that you can just drop some JS in their code and make their main thread faster.

With multi-SW-per-domain, in some cases, you may even make things worse. Imagine browsers not paying much attention to this feature, not making fixes for it Pri-1 because it's too complex and every change requires many engineer-hours. This is just as an example. Another example could be that not much people will be using it, so some browsers won't effectively support it or just don't implement it at all.

Let me expand on the use-case @crdumoul described.

Let's say example.com has a SW that performs some analysis on the resource's content as well as making sure its static assets are available offline and AwesomeCDNā„¢ is their CDN which uses the magical Flofli compression, a proprietary compression format with great gains.

Since Flofli is a proprietary compression format, it has no browser decoding support, and AwesomeCDN relies on SW in order to perform the decoding.

As example.com's CDN, AwesomeCDN can inject headers, and with some larger effort HTML tags and scripts into example.com's site.

How can AwesomeCDN register its SW so that it would play well with example.com's SW and make sure that it still sees non-Flofli traffic? How can it make sure that its SW sees requests as they were modified by example.com's SW? That it doesn't see requests that got respondWith() by example.com's SW?

How can AwesomeCDN register its SW so that it would play well with example.com's SW and make sure that it still sees non-Flofli traffic? How can it make sure that its SW sees requests as they were modified by example.com's SW? That it doesn't see requests that got respondWith() by example.com's SW?

If AwesomeCDN is on a different origin you could use foreign fetch for this. example.com's SW runs as normal. An incoming requests to AwesomeCDN origin trigger a ForeignFetchEvent in their SW. This will show the request coming out of example.com's SW and can return a modified Response back to example.com.

All requests in above scenario go to example.com's own domain.

I see how ForeignFetch can do that for some requests that are on a 3rd party domain, but that's not the case here. (hence @jakearchibald's comment that what I'm looking for is same-origin foreign-fetch).

@yoavweiss Can you accomplish what you want by overriding self.fetch, self.XMLHttpRequest, and self.caches (for cache.add/addAll)? It would seem these would let you intercept network requests and manipulate responses before the main SW script sees them.

I have been working on a SW script which optimises images on the fly - https://dexecure.com/image.html
It would be cool if I could declare a way to the page that I want to intercept all "image" fetch requests and then pass it on to other SWs registered on the page.

@wanderview overriding self.fetch and self.caches can probably satisfy the basic "lower layer interception" use-case, so it might be good enough for what I'm trying to do. (aside: isn't XMLHTTPRequest unavailable from a SW context?)

It does add complexity (as the code would need to be significantly different if the site has a SW or not, and I'd have to make sure that the overriding happens before the site's SW is run), and I'm having a hard time figuring out how/if a similar model can be used to receive push notifications. (I see how I can intercept them by creating wrappers around the entire SW registration process, which seems be a bit much)

Also, IIUC, I'd have to override these values on each fetch call, as I can't rely on the SW state to remain intact between events. Is that correct?

Also, IIUC, I'd have to override these values on each fetch call, as I can't rely on the SW state to remain intact between events. Is that correct?

If you tell your client's to put an import("awesomeCDNWrapper.js") at the top of their service worker script it will get evaluated each time the service worker starts up. I think that should get the wrappers defined.

You would need to manage state in IDB or Cache API, though. That would be the case no matter how we do this, though.

Long term I think the routing API @jakearchibald is talking about over in #920 could probably encompass this use case as well. In addition to falling back from fetch or cache to the service worker, we could add the ability to fallback from one service worker event handler to the next.

I'm still not sure we should implement this system yet, though. It would be a big new high level feature and we're still just getting the underlying primitives out now.

We just encountered this problem again today.
One of our clients is already using service workers provided by a third party (for push notifications)
https://www.scentbird.com/service-worker.js
We also leverage on service workers to optimise images and we can't deploy to their website unless we look at the logic of their existing (minified 😢 )service-worker code and see how to interleave our logic into their existing scripts..

@inian can you describe how importScripts and the patching of fetch (https://github.com/slightlyoff/ServiceWorker/issues/921#issuecomment-229956274) doesn't work for you?

Hey @jakearchibald, I am not sure if I got that fully. You are recommending overriding self.fetch in the main HTML document like this?

const oldFetch = self.fetch;
self.fetch = function () {
    console.log(arguments);
    // handle fetch before any SW script sees it
    return oldFetch.apply(this, arguments);
};

I tried this and this patched fetch is not the one picked up by the fetch used in the SW. Were you recommending something else?

You are recommending overriding self.fetch in the main HTML document like this?

I guess @jakearchibald means overriding fetch in own library which should be included in the SW via importScripts.

Ah got it. Yes, that should work. Seems super hacky though, doesn't it?
And hopefully browsers let the self.fetch property remain writable and configurable

F2F:

  • Maybe this got a whole lot better now the contents of importScripts is checked for updates
  • We'd like to see a prototype of this (overwriting the fetch function and such), so we can see where the pain points are

Has there been some more work done on this up till now? Last comment is from juli 2016.
Thanks for pointing to new resources if available :) @jakearchibald @yoavweiss

I'm not aware of progress on this front

@jakearchibald - was this discussed as part of TPAC's F2F?

@yoavweiss Meanwhile I've been pointed to Workbox for this kind of functionality.
https://developers.google.com/web/tools/workbox/

@yoavweiss we didn't get onto this. There doesn't seem to be a lot of hunger for it given how much it complicates the model vs other stuff.

Was this page helpful?
0 / 5 - 0 ratings