Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enabling multiple Service Workers for a single scope #921

Open
yoavweiss opened this issue Jun 29, 2016 · 40 comments
Open

Enabling multiple Service Workers for a single scope #921

yoavweiss opened this issue Jun 29, 2016 · 40 comments
Milestone

Comments

@yoavweiss
Copy link

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

@jakearchibald jakearchibald added this to the Version 2 milestone Jun 29, 2016
@jakearchibald
Copy link
Contributor

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

@wanderview
Copy link
Member

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.

@NekR
Copy link

NekR commented Jun 29, 2016

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: #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.

@yoavweiss
Copy link
Author

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.

@NekR
Copy link

NekR commented Jun 30, 2016

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" notifications@github.com 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
#921 (comment),
or mute the thread
https://github.com/notifications/unsubscribe/ABIlkf8YYjKd2OaIhLKuF4shfKx_KYh3ks5qQ37ogaJpZM4JBS5x
.

@yoavweiss
Copy link
Author

@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.

@wanderview
Copy link
Member

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.

@wanderview
Copy link
Member

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.

@yoavweiss
Copy link
Author

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.

@wanderview
Copy link
Member

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?

@yoavweiss
Copy link
Author

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.

@wanderview
Copy link
Member

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?

@domenic
Copy link
Contributor

domenic commented Jun 30, 2016

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?

@NekR
Copy link

NekR commented Jun 30, 2016

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.

@wanderview
Copy link
Member

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.

@NekR
Copy link

NekR commented Jun 30, 2016

@wanderview I see, thanks. Then this proposal indeed introduces a lot of complexity.

@crdumoul
Copy link

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?

That's part of the motivation, but a bigger part is the desire to have the code in the two service workers be more independent of each other.

As an example, let's say you have a service worker module that does some alternative compression/content-encoding, and you have another service worker module that does caching or offline availability. Currently, these two things need to be combined in the same fetch handler. This is a use case for having layered fetch handlers.

@yoavweiss
Copy link
Author

yoavweiss commented Jun 30, 2016

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

Yeah. Or add a <link rel=service-worker> or an equivalent header.

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?

Not at all. If an importScripts() call (or any other call) could have solved the same use case, I'd be thrilled.

The problem with the current importScripts() approach is that multiple fetch events that register have no guaranty regarding ordering and the first one of them that calls respondWith wins. There's no real way to chain requests going "down" and responses going "up", without restructuring the SWs to work as plugins in a framework (e.g. middleware in ServiceWorkerWare).

@domenic
Copy link
Contributor

domenic commented Jun 30, 2016

The problem with the current importScripts() approach is that multiple fetch events that register have no guaranty regarding ordering and the first one of them that calls respondWith wins.

This seems false? Like every event, the first-registered listener executes first. If you want to go first, just insert your importScripts at the top, and have your fetch handler call respondWith immediately.

@wanderview
Copy link
Member

This seems false? Like every event, the first-registered listener executes first. If you want to go first, just insert your importScripts at the top, and have your fetch handler call respondWith immediately.

I think what they want is for each handler to be able to incrementally improve the Response.

You still have an ordering problem, though. What if your compression handler runs first, and then their pull-from-cache handler runs second? Some kind of coordination is needed.

@NekR
Copy link

NekR commented Jun 30, 2016

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.

@yoavweiss
Copy link
Author

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?

@wanderview
Copy link
Member

wanderview commented Jun 30, 2016

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.

@yoavweiss
Copy link
Author

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).

@wanderview
Copy link
Member

@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.

@inian
Copy link

inian commented Jul 1, 2016

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.

@yoavweiss
Copy link
Author

@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?

@wanderview
Copy link
Member

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.

@inian
Copy link

inian commented Jul 14, 2016

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

@jakearchibald
Copy link
Contributor

@inian can you describe how importScripts and the patching of fetch (#921 (comment)) doesn't work for you?

@inian
Copy link

inian commented Jul 19, 2016

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?

@RReverser
Copy link
Member

RReverser commented Jul 19, 2016

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.

@inian
Copy link

inian commented Jul 19, 2016

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

@jakearchibald
Copy link
Contributor

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

@samvloeberghs
Copy link

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

@yoavweiss
Copy link
Author

I'm not aware of progress on this front

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

@samvloeberghs
Copy link

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

@jakearchibald
Copy link
Contributor

@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.

@GNUGradyn
Copy link

This would be really nice to see. Would make things alot easier in situations where, for an example, the user has an OIDC library which uses a service worker to inject an auth token and an analytics library that uses a service worker for analytics, and maybe a third service worker for offline functionality

@bradisbell
Copy link

I think the root problem here is that scoping is backwards.

A scope should be defined as the requests that a Service Worker should be allowed to handle, rather than where a Service Worker is allowed to run.

For example, a developer should be able to register a Service Worker from a different origin that handles requests for that origin. Maybe that Service Worker knows how to speed up certain resources, or knows how to fetch them differently. But, the developer may want to limit that third-party service worker from accessing their API on behalf of the user. That's what scope should be for.

If scope was flipped around, having multiple Service Workers would be no problem as there would not be conflicts for the same scope. The use cases presented here are about external libraries and services that use their own Service Workers. Well, if SomeAnalyticsService needs to register, it can be scoped to its origin by default, or the developer can change the scope if necessary. Either way, it won't affect the main scope of the site.

Scope is backwards as implemented today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests