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

API Generality #445

Closed
martinthomson opened this issue Sep 3, 2014 · 79 comments
Closed

API Generality #445

martinthomson opened this issue Sep 3, 2014 · 79 comments

Comments

@martinthomson
Copy link
Member

Apologies if this was raised previously. I couldn't see anything discussion on the topic, though I'm told that at least some people have been thinking about this already.

One thing I get reading the service worker spec is the sense that the first use case (offline) dominates the form of the API.

The scope attribute is the only real problem in this regard. ServiceWorkerContainer.register takes the RegistrationOptionList, which currently only has a single argument:

  navigator.serviceWorker.register(script, { scope: "/foo" }) 

Limiting the scope (in the more general sense) of a registration seems to be the right idea. However, ad hoc limiting based on a bag of arguments with different purposes doesn't seem like a good way to build the basis of a new generic platform feature. A more modular approach might ensure service workers are suited to use for offline apps, web push, geolocation and other features.

I have a few ideas. @sicking tells me he has some thoughts too. I'll share mine in separate comments.

@sicking
Copy link

sicking commented Sep 4, 2014

Thanks for starting this conversation. I've felt the same discomfort about the registration API from the beginning, but hasn't been able to put a finger on what exactly bothered me until recently.

Currently ServiceWorkers suffers from a couple of problems related to the registration API.

Apps with URLs in multiple directories

First of all, a ServiceWorker currently can only service a single URI pattern. This means that if your application lives under "/calendar/" and under "/meetings/", you are left with mainly bad choices.

You could change your URLs, but that means breaking any existing links.

You could register for "/". But that means that if you have two applications on the same server with the same problem, you now have to use the same serviceworker for two separate applications. It also means that a lot of URLs that are unrelated to the application

You could use create two separate registrations. But that looses some of the nice atomic update behavior that ServiceWorkers provide. It would also mean that we're now running two worker threads rather than one, which costs resources, especially on mobile. Additionally, this would require that manifests support multiple service worker registrations in a single manifest.

You could use two separate registrations, but use the same service worker URL. This could in theory enable the implementation to do a single download for both service workers, so that part would still be atomic. However the install steps might still fail for one but not the other. It also adds additional complexity to avoid having the two service worker instances stomp on each other's caches or download the same resources twice. And it has the same two-threads and manifest-complexity problems as the two-separate-registrations solution.

You could simply choose not to cache one of the two paths. This is the option I'm most worried developers will choose since it means that the app won't be as fast, and might not work offline. But it's also the simplest solution which means that there's a big risk that developers will choose it.

Http-intercept registration differs from other features

The second problem with the registration API is that the "intercept http requests" feature uses a different registration mechanism than things like "register push notifications" or "register geofencing notification". This isn't a huge problem, but is an unfortunate inconsistency.

A slightly bigger problem is that it means that in order to use other service worker features, you have to register for a scope at which to intercept http requests, even if you're not interested in that ability. To handle that we have added the "look for event handlers in the global scope" thing, and use that to effectively unregister for the "intercept http requests" feature.

Proposed solution

A solution to both of these problems would be to separate installing a service worker from registering for http intercepts. This way we could enable websites to make multiple calls to "register for http intercepts" in order to make a service worker cover multiple http scopes.

This could look something like this:

[Exposed=Window]
interface ServiceWorkerRegistration : EventTarget {
  [Unforgeable] readonly attribute ServiceWorker? installing;
  [Unforgeable] readonly attribute ServiceWorker? waiting;
  [Unforgeable] readonly attribute ServiceWorker? active;

  Promise<boolean> unregister();

  Promise<void> addScope(ScalarValueString scope);
  // resolves to false if the scope wasn't registered.
  Promise<boolean> removeScope(ScalarValueString scope);

  // does this need to be async?
  readonly attribute sequence<ScalarValueString> scopes;

  attribute EventHandler onupdatefound;
};

However a question is what will now be the identifier for a SW given that we currently use the http intercept scope as the identifier. The simplest answer seems to be to simply enable service workers to have a name. So registration would look something like:

interface ServiceWorkerContainer {
  ...
  Promise<ServiceWorkerRegistration> register(ScalarValueString scriptURL, optional ScalarValueString name="");

  Promise<ServiceWorkerRegistration> getRegistration(optional ScalarValueString name = "");
  ...
};

This has the following benefits over the current API:

  • Allows a service worker to handle multiple scopes
  • Smoother update path if the URLs that an app is composed of changes over time
  • Makes http-intercepting registration similar to registration for other features
  • No need for the event-registration detection

The main disadvantage that I can see is that registering for http interception now requires slightly more code. This seems ok to me, but if we want to address it, we could as sugar enable passing in a scope during registration:

interface ServiceWorkerContainer {
  ...
  Promise<ServiceWorkerRegistration> register(ScalarValueString scriptURL, optional RegistrationOptionList options);

  Promise<ServiceWorkerRegistration> getRegistration(optional ScalarValueString name = "");
  ...
};

dictionary RegistrationOptionList {
  ScalarValueString name = "";
  (ScalarValueString or sequence<ScalarValueString>) scope;
};

This is almost exactly what the API looks like today, the main difference is that if you don't pass in a scope at registration, that no scope gets registered, rather than that / is used as scope. Technically we could even change that, but that would bring back some of the downsides mentioned above.

@sicking
Copy link

sicking commented Sep 4, 2014

I should note that this still gives the http-intercept feature somewhat of a special status.

Scopes still get a predominant position in the API, and so far scopes are specific to http intercepts. This affects things like navigator.serviceWorker.controller and navigator.serviceWorker.ready().

However I think this is ok. First of all http-intercepts is one of the more important features of ServiceWorkers, which is why I also think it's ok to have syntax for it in the registration API.

But second, the concept of a scope could very well come in handy for other features.

@martinthomson
Copy link
Member Author

I should note that this still gives the http-intercept feature somewhat of a special status.

Despite this being a initial motivation for the feature as a whole, I think that greater generality is still valuable here. First use not conferring any sort of special status.

To that end, here's my alternative approach. It's more disruptive, but it has some encapsulation benefits over both the current and proposed alternatives. It retains the general concept of "scope", but generalizes that concept more. URL-space scope, as used in the fetch/caching cases is largely the same, but it has a different entry point.

The outer ServiceWorkerContainer and ServiceWorkerRegistration no longer require knowledge of the functions of the worker itself. Registration only requires knowledge of the script URL and a name (if multiple instances of the same worker are needed. Note that we probably need to ensure that the name of the worker is exposed to the worker if we're using names.

interface ServiceWorkerContainer {
  Promise<ServiceWorkerRegistration> register(ScalarValueString script, DOMString name = "");
  Promise<ServiceWorkerRegistration> getRegistration(optional DOMString name = "");
  ...
}

This is no different to using names for identification as @sicking proposes.

What changes is the internals and how events are surfaced to workers. During the install event the worker itself registers interest in different scopes.

this.addEventListener("install", e => {
  e.waitUntil(Promise.all(
    this.webPush.register().then(push => {
      push.addEventListener("push", handlePush);
    });
    this.geolocation.register(geolocationOptions).then(geo => {
      geo.addEventListener("enter", handleGeoEnter);
      geo.addEventListener("leave", handleGeoLeave);
    });
    this.fetch.register(scope).then(...);
    this.cache.register(scope).then(...);
  ));
}); 

The trick here being that each different module (or whatever you want to call a logical function that we might use SW for) has particular needs for determining how to pre-filter the events it might generate. Relying on a single string value (or set thereof) isn't sufficient for the new geolocation features (see proposals here and here) and we can't possibly anticipate the needs that a future API might generate.

On the other hand web push has no need for any such affordance, at least as far as current plans indicate, though that could change (and likely will over time). The idea that apps might need the same fetch access for multiple path prefixes is clearly another example of how different use cases evolve to encompass new needs.

interface ServiceWorkerGlobalScope {
  GeolocationWorkerContainer geolocation;
  WebpushWorker push;
  FetchWorker fetch;
  ...
}

interface GeolocationWorkerContainer {
  Promise<GeolocationWorker> register(optional GeolocationWorkerOptions options, optional DOMString name = "");
  Promise<GeolocationWorker> getRegistration(optional DOMString name = "");
}
interface GeolocationWorker : EventTarget {
  readonly attribute DOMString name;
  attribute EventHandler? onleave;
  attribute EventHandler? onleave;
}

Arguably, the register and getRegistration functions here are free to be renamed to anything that suits the less general pattern this fits, but establishing a convention seems to make sense (the use of generics in WebIDL again suggests itself here, which would obviate the need for informal hacks like "convention").

The advantage of an approach like this is that the worker itself determines what it needs. That provides content authors better encapsulation. I think that this also provides the browser with more determinism regarding what events need to hit the service worker. If the set of event registrations has to be complete by the time that install completes, then that ensures that the worker can be suspended more efficiently.

I can imagine surfacing information on ServiceWorkerRegistration that would allow clients of the SW to learn status about what APIs are being used by that SW.

@sicking
Copy link

sicking commented Sep 5, 2014

My vision has been to not have the SW "filter" events. Instead, at the time when you register for some particular feature, you indicate which events you want fired, and which SW to fire the events at.

So myRegistration.geolocation.registerRegion(...) means fire a geofenceenter event at the myRegistration service worker when the user enters the given area.

Likewise myRegistration.push.register() means fire a push event at the myRegistration service worker when a message arrives at the given endpoint.

And myRegistration.addScope("/") means fire a fetch event for any documents whose URL starts with "/", and fire the event at the myRegistration service worker.

So no filtering is happening. And each API can have entirely different patterns of events that are fired. "scope" is really just a "set of URLs in which to invoke the http-intercept feature". Technically we could call addScope something like addHttpInterceptScope, but I'd rather use a shorter name.

I think the main difference between our proposals is that I've stuck the registration APIs on the ServiceWorkerRegistration object, rather than inside the service worker's global scope. The advantage of that is that it makes it more clear that once a http-intercept-scope or a push-registration has been registered, it stays with the service worker even if the worker is upgraded. I.e. these things are tied to a service worker registration, not a particular version of the service worker. When the service worker is updated and we fire the install event again, there is no need to reregister.

@annevk
Copy link
Member

annevk commented Sep 5, 2014

I like it!

@jakearchibald
Copy link
Contributor

@sicking

First of all, a ServiceWorker currently can only service a single URI pattern. This means that if your application lives under "/calendar/" and under "/meetings/", you are left with mainly bad choices.

Do you have any examples of this?

Promise<void> addScope(ScalarValueString scope);

So now I can have multiple registrations claiming the same scopes? What happens then?

@martinthomson

this.addEventListener("install", e => {

Nah, you can't register for these things within the serviceworker, they need to be done from the window so we have somewhere relevant to show permission dialogs. Also, you're adding listeners within the install event, those will be lost once the serviceworker terminates, you'll most likely never get any geo events.

Scope isn't just about fetch interception, it's part of the lifecycle, it's how we control upgrades. If we see sites wanting '/blah/' and '/whatever/' to be controlled by the same worker, but a scope of '/' doesn't work for them, we can look at ways of expanding 'scope' to an array while retaining it's primary key nature.

The proposals here feel like huge changes & complication for very little benefit.

@sicking
Copy link

sicking commented Sep 5, 2014

First of all, a ServiceWorker currently can only service a single URI pattern. This means that if your application lives under "/calendar/" and under "/meetings/", you are left with mainly bad choices.

Do you have any examples of this?

Not off the top of my head, but I'd be shocked if this isn't common among sites that host multiple "apps" on the same domain. I.e. among sites that need scopes at all.

But I'll look for examples.

Scope isn't just about fetch interception, it's part of the lifecycle, it's how we control upgrades.

How so? I.e. how does the scope affect the lifecycle and upgrades? Not doubting you, I might very well be missing pieces.

I agree that it's a big change. Though with the proposed sugar it comes out as very small code changes for the website.

But I think the spec right now is making a very big assumption that websites are generally structured such that each "app" has its own directory. If we're betting wrong on that the result will likely be that websites won't work as well offline, and will be slower online, which is our main goal to avoid.

@jakearchibald
Copy link
Contributor

how does the scope affect the lifecycle and upgrades?

We don't promote a "waiting" worker to "active" until all clients have disconnected. Clients are windows that have opted to use a registration as their controller, which is done based on scope.

Also installEvent.replace() makes all within-scope clients use the registration as their controller.

I guess the clients API itself is also very scope-driven.

@martinthomson
Copy link
Member Author

@sicking

My vision has been to not have the SW "filter" events.

Yes, browser filters, not the SW. I apologize for accidentally implying otherwise.

@jakearchibald

[...] they need to be done from the window so we have somewhere relevant to show permission dialogs.

Is the intent here to ask a series of questions? "Do you want this site to be available offline?" "Do you want this site to be able to track your location when you aren't visiting it?" "Do you want to receive notifications from this site?"

Having had more time to think about it, I think that my own lack of understanding about the lifecycle made my alternative seem plausible, but it really isn't. The fact that scopes are still special bothers me some, but not enough to get excited about.

@jakearchibald
Copy link
Contributor

Is the intent here to ask a series of questions?

We're not changing the permission model of the web here, the idea is to request permission at a moment that makes sense to the user, either after an interaction "enable push messaging", or without interaction if the intent is clear to the user, eg gmaps asking for location permission.

@ehsan
Copy link

ehsan commented Sep 11, 2014

First of all, a ServiceWorker currently can only service a single URI pattern. This means that if your application lives under "/calendar/" and under "/meetings/", you are left with mainly bad choices.

Do you have any examples of this?

https://www.google.com/calendar. :) It accesses www.google.com/csi among other things.

@ehsan
Copy link

ehsan commented Sep 11, 2014

FWIW I like @sicking's proposal here too.

@jakearchibald
Copy link
Contributor

What is http://www.google.com/csi? It doesn't appear to be another place /calendar/ lives (it's an empty page for me).

FWIW I like @sicking's proposal here too.

But liking it isn't enough. The scope is more than just for determining which fetches are captured (see my comment above).

Closing this. Feel free to reopen if there's a desire (and a solution) to separating fetch without breaking the other things that depend on scope.

@mvano
Copy link

mvano commented Sep 11, 2014

Client Side Instrumentation - it's not serving anything, just a place to report to about e.g. how long it took to load the page, how long to render some component, etc.

@jakearchibald
Copy link
Contributor

Ah ok, not a relevant example then.

@KenjiBaheux
Copy link
Collaborator

Assuming that nothing has changed as the result of the discussion => will be adding no impact label.

@ehsan
Copy link

ehsan commented Sep 11, 2014

Ah ok, not a relevant example then.

Here is another one: http://www.bbc.co.uk/persian/. It loads content from /persian, /worldservice, etc. I am really surprised that it's hard for you to believe that there are web properties that live on multiple URL paths.

Can you please reopen the issue? I don't think that we have addressed it at all, and I don't seem to have access to reopen it myself.

@jakearchibald jakearchibald reopened this Sep 11, 2014
@jakearchibald
Copy link
Contributor

@ehsan

Here is another one: http://www.bbc.co.uk/persian/. It loads content from /persian, /worldservice, etc

You're misunderstanding what @sicking was referring to. He was talking about a web property that exists across multiple paths where a origin-scoped serviceworker would be harmful.

http://www.bbc.co.uk/persian/ loads content from /worldservice, yes, but it's static assets. That's not the point. https://jakearchibald.github.io/trained-to-thrill/ loads content from flickr, doesn't mean they should share a serviceworker.

I am really surprised that it's hard for you to believe that there are web properties that live on

Yet everyone here has been unable to come up with an example.

I'm not saying it doesn't happen, but I'd like to see a site that would have this problem before we consider solving it.

Also, this can be solved with less severe changes such as allowing scope to be an array.

Have reopened, but will close again soon if there isn't a good example.

@martinthomson
Copy link
Member Author

My problem with the API remains that selection is based on concepts that aren't universally applicable. Web Push doesn't need to concern itself with scope of control in the same way that fetch might. Nor does geolocation.

The geolocation case at least benefits from access to a parallel means of determining what events a SW is configured to handle, but using that same mechanism as a basis for SW selection would not work. (I do ultimately think a similar event determination process applies to push as well, but that group has decided to simplify so much that that isn't really an option.)

It's that conflation that is the issue here, not the multiple scopes thing, which is a separable issue. I'm happy to defer to others on this. Though I'll point that you are creating a forcing function by limiting scopes this way, which I don't think is wise. If the part of the web you look at happens to form into neat compartments, that's great, but I've learned not to make assumptions along those lines.

@ehsan
Copy link

ehsan commented Sep 12, 2014

@jakearchibald I pinged you on IRC to get a better understanding of what examples you find acceptable. My basic assumption is that a good example would be an application living under the same origin with other independent applications across multiple URL scopes. If you're looking for something else, please be more specific.

Here is another example: https://www.facebook.com/events. The UI to create a new event loads from https://www.facebook.com/ajax/plans/create/dialog.php. Browsing Facebook under the network panel in the Firefox devtools, it seems like /ajax is the URL scope for several helper scripts, such as this one.

@jakearchibald
Copy link
Contributor

I don't think that example is a problem either.

/events/ requests data from /ajax/ - that's fine. You can think of /ajax/
as a sub app. Either the /events/ SW script will handle caching/routing the
way that makes sense for /events/, or if there's enough commonality between
other apps on the origin, it'll importScripts the code, or (and most likely
in Facebook's case) there'll be one SW for the origin, it is a single
native app after all. (yes, ok messaging is separate, but not on the web,
and not spread across URLs in a way that breaks the current model)

Compare to my trains demo again. I request data from Flickr but my app
should not share a SW with flickr. I'm using flickr data, but the SW logic
is unique to my app. Maybe flickr will provide a script to help make flickr
stuff work offline for me, if that works for usecases I can importScripts
it.

What Jonas is talking about is an app that doesn't have a root path. The
app has roots at /a/ and /b/, so they would want the same SW, but /c/ is
something different, and having a SW for the whole origin and a separate
one for /c/ wouldn't work for some reason.

Maybe that's something we should be worrying about, but I'd like to see a
real-world example of that before we work on solutions, so we better
understand the problem. Once we can see it, maybe we can fix it in a way
that isn't removing the bottom block of the jenga tower.
On 12 Sep 2014 21:37, "Ehsan Akhgari" [email protected] wrote:

@jakearchibald https://github.com/jakearchibald I pinged you on IRC to
get a better understanding of what examples you find acceptable. My basic
assumption is that a good example would be an application living under the
same origin with other independent applications across multiple URL scopes.
If you're looking for something else, please be more specific.

Here is another example: https://www.facebook.com/events. The UI to
create a new event loads from
https://www.facebook.com/ajax/plans/create/dialog.php. Browsing Facebook
under the network panel in the Firefox devtools, it seems like /ajax is the
URL scope for several helper scripts, such as this one.


Reply to this email directly or view it on GitHub
#445 (comment)
.

@slightlyoff
Copy link
Contributor

The problem statement as such is tortured. Many plugin features benefit from scoping in the same way that interception does.

Won'tfixing this unless we get a better argument.

@annevk
Copy link
Member

annevk commented Sep 17, 2014

What plugin features benefit from scoping in this manner?

These do not:

  • Background updates
  • Notifications
  • Push

It does seem like a better design to install a service worker for an origin and then associate scope(s) with it. That way we also solve the issue with whether or not we dispatch events inside the service worker. Adding scopes is opting into the fetch event. Registering for plugin features opts into their events.

These seems like a vastly better design and the changes for everyone involved are minimal.

@slightlyoff
Copy link
Contributor

@ehsan : your example doesn't fit. It's not the same app (as covered at some length in my previous comment). Without compelling data, this change appears to be motivated entirely be esthetic concerns, to which, at this late date, I'm not inclined to bend.

If we don't get better data or arguments in the next day or so, I'm going to wontfix this with prejudice.

To your last comment, this isn't about a willingness to change the design. It's about a willingness to change the design without a compelling argument.

@annevk
Copy link
Member

annevk commented Oct 23, 2014

Well, we did list a number of arguments why we wanted to change this and we do think they are compelling enough to change the design.

  1. A service worker should be able to handle multiple scopes.
  2. A service worker should be able to change its scopes over time without losing permissions for push, notifications, etc.
  3. The design for FetchEvent should be consistent with other features.
  4. Instead of detecting event listeners, a service worker should dispatch events based on the features it is being used for. This is how DOM events work.

We listed these in the second comment of this issue. We are unlikely to agree to the current approach.

@slightlyoff
Copy link
Contributor

@annevk: I appreciate that you have become convinced that this is worth making a breaking change for. Others are not convinced (myself included), not least of all because every time we poke at the evidence for the request, there's clearly a way to handle it without making this change (e.g., a global registration that delegates).

I'll take your points in order, though, and appreciate you summarizing.

  1. A SW handling multiple scopes is logically a question of how to compose an application. SW's already provide composition primitives that enable this; notably importScripts() and the ability to register multiple handlers for each event. Given that we have a solution, and noting that we've previously discarded others (russian-doll SW's, e.g.), what makes registration for disjoint scopes essential? No evidence has been presented here and you haven't presented an argument against my alternative: only allowing a single worker per origin.
  2. Why should an application's permission model, which at the core of the web's security model is welded to origins, be expected to cater to the corner-case of multiple registrations per origin when the default advice to all new developer will be "use sub-origins, it's the only way to be secure"?
  3. Fetch is actually a snowflake. Apps that don't do offline aren't real apps. Perhaps you disagree, in which case we are at an impasse.
  4. Detecting event listeners (vs. a declarative syntax, as previously proposed, e.g. in oninstall) is a separable issue that I'm entirely willing to re-open.

Regards

@tabatkins
Copy link
Member

Drive-by comment: One thing that confused the hell out of me reading this thread was Alex's seeming dodging of the issue of "but why do you need a scope to listen to geofence events?".

I had assumed that a scope's only purpose was to specify which fetches you want to intercept; that is, it was only relevant for a fetch-intercepting SW, not any of the others. Alex clarified to me privately just now that it's more meaningful than that - for anything that might need a permission grant, you need to make the request within a page, not in the SW, and the scope gives you a page->SW mapping, so you can tell which SW the permission grant is applying to.

This is only necessary because of the "let's pretend that multiple apps on the same origin aren't a terrible, terrible idea" thing that we're doing for convenience. If it was one-app-per-origin (and if you really want to do multiple, you can handle negotiation and dispatch yourself), then we wouldn't need a scope unless you really were intercepting fetches.

If anyone else was confused about this, hopefully this helps.

@slightlyoff
Copy link
Contributor

Right, what @tabatkins said. This is a debate about what is an app. Apps are what request permissions and where events for things need to be directed back to. Scoping as it currently stands ergonomically connects the surface that requests a service to the handler for the service (the SW script & version). A SW is the service bus for an app.

In general, apps are single origins. If we're going to make any change here, I'd rather we remove scopes entirely in the short run and see how far we get. The advice is already going to be that sites shouldn't host more than one app. Scopes as spec'd so far are only a way to deal with some painful corner cases. Imbuing them with more meaning is ok, but only to the extent that we don't break the notion that a SW registration represents a conceptual app.

Honestly do what to get to a resolution here. And I'm grateful to @tabatkins for pulling the assumed context out of my head.

@domenic
Copy link
Contributor

domenic commented Oct 23, 2014

In general, apps are single origins. If we're going to make any change here, I'd rather we remove scopes entirely in the short run and see how far we get.

+1! Combined with a resolution for

Detecting event listeners (vs. a declarative syntax, as previously proposed, e.g. in oninstall) is a separable issue that I'm entirely willing to re-open.

I would be very happy.

@johnmellor
Copy link

@tabatkins wrote:

the scope gives you a page->SW mapping, so you can tell which SW the permission grant is applying to.

Technically, permissions are still granted to origins, not to SWs. The relevance of scopes to other APIs is that other APIs might want to associate themselves with the active SW for the current page's scope. For example for Push, calling navigator.push.register used to implicitly tie the push registration to whatever the active page's SW was (via scope lookup). In the end we decided to make it explicit instead by moving the PushManager onto ServiceWorkerRegistration instances.

@annevk wrote:

A service worker should be able to change its scopes over time without losing permissions for push, notifications, etc.

That's already true: permissions are granted to origins, so you can unregister a SW, then register a replacement SW, and re-register for push/notifications without any permission prompts.

@ehsan
Copy link

ehsan commented Oct 23, 2014

@slightlyoff In the Mozilla MarketPlace for Firefox OS, we have also limited each app to its own origin, and we have received a ton of negative feedback from developers on that decision. So I think your assumption that all apps are only hosted in one origin is incorrect. I am worried that with the current design, apps that do not adhere to your suggestion will have difficulty for adopting service workers.

@jakearchibald
Copy link
Contributor

As a data point: trained-to-thrill, and isserviceworkerready both live on the same origin (jakearchibald.github.io) and make use of scoping to work independently.

@ehsan
Copy link

ehsan commented Oct 23, 2014

Sure, but my point is, not all apps are written in the future and according to the assumption that the SW spec is making.

@tabatkins
Copy link
Member

Technically, permissions are still granted to origins, not to SWs. The relevance of scopes to other APIs is that other APIs might want to associate themselves with the active SW for the current page's scope.

Right, that's what I meant, sorry for implying otherwise.

@annevk
Copy link
Member

annevk commented Oct 24, 2014

I would be super happy with one service worker per origin and using a potential future suborigins to solve the scenario of having more. Combined with a way to opt into FetchEvent and I think we have a great outcome that would actually simplify the current design and make it more coherent.

@sicking
Copy link

sicking commented Oct 24, 2014

Ok, so there's a couple of things people have asked for, so I'll try to address each one separately.

Examples of websites that need this

This is what has kept me the longest. I hope to get more examples from other mozilla teams, but in the meantime I only have one example which is google maps.

Google maps is currently hosted under https://www.google.com/maps, however preferences for maps are located under https://www.google.com/preferences. (Help and history for maps are also located outside of /maps. But they are on entirely separate origins which means that there's little we can do to handle them in the same SW).

We certainly could ask maps to rewrite their URL structures, but that means breaking any links/bookmarks. Probably not something that they are willing to do. They could set up a redirector, but that redirector would need to have its own SW if you want users to be able to use maps offline.

Also, redirectors of course cost performance, no matter if SW are involved or not.

As for moving content around so that there's "one app per origin", does anyone here really expect the various "apps" hosted under www.google.com to do that? I.e. does anyone here really expect all apps except search to move to other domains?

Scopes are used for updates

It's certainly true that the spec does its best to ensure that a SW never "gets stuck" and doesn't get updated. However as far as I can tell it only succeeds in doing that for http-intercepting SW.

If a website doesn't want to use the http-intercept feature and just wants to use SW to receive push messages, it could very well use "/whatever/foopy/garbage" as scope. There's nothing ensuring that the SW does get updated.

However I agree that there's nothing in my proposal that address when we should check for updates of a scope-less SW. What seems most simple to do is to define that if a SW doesn't have a scope, that we check for updates any time the user visits any URL from that origin.

Scopes are useful for other features too

This keeps being claimed, but I just don't see it.

The way that you register, for example, push messages is that you grab a ServiceWorkerRegistration instance and call registerPush() on it. You have to do this independently of if you're in scope of a service worker or not.

The only thing that's different is that if you're in scope of the ServiceWorkerRegistration that you want to register push with, you can use navigator.serviceWorker.ready rather than navigator.serviceWorker.getRegistration().

I.e. the difference is just 12 characters, and the fact that ready has the ability to wait until registration happens. Both of these problems seem solvable if this is what's holding back attempting to solve the multiple-scope problem.

Interaction with apps

Something else that we haven't talked about so far is the fact that the plan is that service workers is going to play an even more important role once we integrate manifests and service workers.

My hope had been that once the user performs a "bookmark to homescreen" action for a page with a manifest, that the manifest will point to a SW which is installed and given special status. I.e. once the user creates an "app" using a manifest, that information in the manifest enables us to register a SW which is then given a special status within that "app".

For example the SW will receive update checks even if the user doesn't run the app, we will give that SW unlimited amounts of storage, enable it to register push registrations without a prompt, etc.

However with the limitation that SWs can only handle a single URL prefix, will this lead to that the app can only handle one URL prefix? Or at least do so well?

Another way to look at this is that people are constantly up against the same-origin limitation. What's effectively a single website is often spread out over multiple servers. We're making this issue worse by forcing people to not just stay within a server, but within one directory.

Proposed solution Alt 1

This is essentially the same API that was proposed in my first comment in this issue, with some minor tweaks.

[Exposed=Window]
interface ServiceWorkerRegistration : EventTarget {
  readonly attribute ScalarValueString name;

  [Unforgeable] readonly attribute ServiceWorker? installing;
  [Unforgeable] readonly attribute ServiceWorker? waiting;
  [Unforgeable] readonly attribute ServiceWorker? active;

  Promise<boolean> unregister();

  Promise<void> setScope(ScalarValueString scope);
  Promise<sequence<ScalarValueString>> getScope();

  attribute EventHandler onupdatefound;
};

interface ServiceWorkerContainer {
  ...
  Promise<ServiceWorkerRegistration> register(ScalarValueString scriptURL, optional RegistrationOptionList options);

  Promise<ServiceWorkerRegistration> getRegistration(optional ScalarValueString name = "");
  ...
};

dictionary RegistrationOptionList {
  ScalarValueString name = "";
  (ScalarValueString or sequence<ScalarValueString>) scope;
};

This means that for the common use-case of "service worker services whole origin", the two differences in code will be:

  • Registration now looks like navigator.serviceWorker.register(url, { scope: "/" }) rather than the current navigator.serviceWorker.register(url).
  • You can't use navigator.serviceWorker.ready but instead will have to use navigator.serviceWorker.getRegistration().

See also the mainly unrelated issue #534 that should make this difference be even smaller.

So to address the cases where we use scope that @jakearchibald mention in #445 (comment)

  1. which ServiceWorker to dispatch a fetch event to

This continues unchanged.

  1. when an installed ServiceWorker can be promoted to active (when all tabs within a scope are gone)

I would actually argue that this part remains unchanged as well. A SW shouldn't actually become active "when all tabs within a scope are gone", but rather "when the SW is no longer the serviceWorker.controller for any page". For example if a page was opened before any service worker was registered, or if a page was opened through "shift reload" (I forget what key combination that Chrome uses) then the page won't have a controller, even if the page happens to be in scope of a SW. And there's no reason that it in that case should prevent a SW from becoming active.

So effectively a SW that doesn't have a scope is never prevented from becoming active.

  1. when a ServiceWorker can be checked for updates

I agree that my initial proposal didn't address this. For Service Workers with a scope this remains unchanged. I.e. as soon as the user visits any page within the SW's scope we check for updates. For SWs without a scope we check for update any time the user visits a page from the SW's origin.

That leaves one problem that I can't think of a good solution for still. Which is what to do if two SWs claim the same scope. Hopefully this should be really rare given that it's effectively a developer bug.

I'd say that we reject the setScope call (or registration if it happens as part of registration) and put an error in the developer console. Alternatively we can let the registration go through in the hope that the scope of the other SW will be reduced shortly after, still with plenty of warnings in the developer console.

Proposed solution Alt 2

This proposal is trying to solve fewer of the problems mentioned in #445 (comment). It focuses only on solving the multi-directory problem. (You could say that this proposal is.. narrower in scope. Aw yeah)

This proposal still forces all service workers to have a scope, just like the spec does today. However it stops using the scope as identifier for the service worker which means that the scope can be modified without creating a new registration.

The only changes compared to what we have now are

  • There's an optional name parameter when you register a SW.
  • getRegistration takes a name rather than a URL.
  • There's a ServiceWorkerRegistration.name property.
  • There's a ServiceWorkerRegistration.setScope() function.
  • ServiceWorkerRegistration.scope is instead the async ServiceWorkerRegistration.getScope().
[Exposed=Window]
interface ServiceWorkerRegistration : EventTarget {
  readonly attribute ScalarValueString name;

  [Unforgeable] readonly attribute ServiceWorker? installing;
  [Unforgeable] readonly attribute ServiceWorker? waiting;
  [Unforgeable] readonly attribute ServiceWorker? active;

  Promise<boolean> unregister();

  // Throws if you try to set the empty list.
  Promise<void> setScope(sequence<ScalarValueString> scope);
  Promise<sequence<ScalarValueString>> getScope();

  attribute EventHandler onupdatefound;
};

interface ServiceWorkerContainer {
  ...
  Promise<ServiceWorkerRegistration> register(ScalarValueString scriptURL, optional RegistrationOptionList options);

  Promise<ServiceWorkerRegistration> getRegistration(optional ScalarValueString name = "");
  ...
};

dictionary RegistrationOptionList {
  ScalarValueString name = "";
  (ScalarValueString or sequence<ScalarValueString>) scope = "/";
};

This means that we still have to do the trick with looking for event handlers for "fetch" events during SW installation (Sorry Anne).

And we'd still have to handle overlapping scopes. Same proposed solution as for Alt 1.

@phistuck
Copy link

@sicking - regarding the "Examples of websites that need this" section, I am not sure the /preferences example is relevant. Unless I am mistaken, the preferences scope (page, or section, or whatever) seems to be a different application (Google Web Search, or Google Search Preferences). If you look at the preferences it provides, they are actually related to search in general and are not specific to maps.

@annevk
Copy link
Member

annevk commented Oct 24, 2014

The reason I like having one service worker per origin is that it simplifies the model and aligns the actual security guarantees. E.g. cookies have this Path field that is sort of similar and is a complete failure in practice. Going forward we can enable deploying multiple service workers on an origin through an orthogonal feature that preserves the security guarantees: suborigins.

@jakearchibald
Copy link
Contributor

Google maps is currently hosted under https://www.google.com/maps, however preferences for maps are located under https://www.google.com/preferences.

Those aren't really preferences for maps, but preferences for the whole of google.com.

As for moving content around so that there's "one app per origin", does anyone here really expect the various "apps" hosted under www.google.com to do that? I.e. does anyone here really expect all apps except search to move to other domains?

Agreed. We really need scoping.

It's certainly true that the spec does its best to ensure that a SW never "gets stuck" and doesn't get updated. However as far as I can tell it only succeeds in doing that for http-intercepting SW.

I'm pretty happy with #514 (comment). Updating on navigation works well, it's a great indicator of usage. Checking for updates when the SW spins up & hasn't been checked within 24hrs stops the SW going stale if it's only used for push etc etc. If a SW isn't used (no visits within scope, no push message), we're not wasting data/cpu checking for updates.

I agree it should be in the developers power to check for updates per received push message, I'll pick this up in #534.

To be specific: We've fixed the bits that make updates reliant on scopes.

If a website doesn't want to use the http-intercept feature and just wants to use SW to receive push messages, it could very well use "/whatever/foopy/garbage" as scope.

If that makes them think "hmm, maybe I shouldn't hack this, and instead make the result of my push message work (and fast) despite connectivity" then I'm pretty happy that we've created this hoop to jump through.

The only thing that's different is that if you're in scope of the ServiceWorkerRegistration that you want to register push with, you can use navigator.serviceWorker.ready rather than navigator.serviceWorker.getRegistration().

These aren't equivalent, .ready also waits for a worker to reach the active state (which is when you can register for push etc). Will continue this in #534.

Another way to look at this is that people are constantly up against the same-origin limitation. What's effectively a single website is often spread out over multiple servers. We're making this issue worse by forcing people to not just stay within a server, but within one directory.

I don't think I really get this. If I add-to-homescreen jakearchibald.com/space-game and jakearchibald.com/drawing-app, each can have its own SW with different scopes. If I open space-game and navigate within its scope via links, I'd expect that to happen within the same window. If I open space-game and navigate to a url in drawing-app's scope, I'd expect the OS to transition from one "app" to the other as it would with native apps. If I open space-game and navigate to a url that isn't in scope of an added-to-homescreen app, I'd expect it to launch the browser app for it.

Scopes are really useful here.

A SW shouldn't actually become active "when all tabs within a scope are gone", but rather "when the SW is no longer the serviceWorker.controller for any page"

This is the currently specced behaviour, my wording was off.

So effectively a SW that doesn't have a scope is never prevented from becoming active.

That's fair (it should wait until any waitUntils resolve, but I guess you mean that).

Proposed solution Alt 2

Does this work for Mozilla? If so I'd like to give it more thought. Maybe scope could be explicitly set to null to opt out of fetch?

@annevk
Copy link
Member

annevk commented Oct 24, 2014

I am convinced by @slightlyoff that we should not pretend there are boundaries where there are none. And that therefore origin-bound SWs make the most sense.

To get boundaries within an origin, suborigins seem promising. To address complex cross-origin structures such as Wikipedia we've yet to come up with something.

I would vastly prefer origin-bound SWs over Alt 2 + nullable scope.

@tabatkins
Copy link
Member

So, Jonas suggests in emails that, while the permission request is made from a page, it's made using a specific Service Worker object already, so there's no need for the url -> SW mapping that scope provides. Thoughts?

@sicking
Copy link

sicking commented Oct 25, 2014

Google maps is currently hosted under https://www.google.com/maps, however preferences for maps are located under https://www.google.com/preferences.

Those aren't really preferences for maps, but preferences for the whole of google.com.

Sure. But the relevant question is: If you were using offline maps, would you expect to be able to click the "settings" button in the maps UI and then configure your home address? And if maps was updated to gain more preferences, would you expect that preference UI to be updated atomically together with the maps UI which takes advantage of those preferences?

I'm not saying that all of /preferences should be handled by the maps app, but it seems sensible that ones that are specifically for maps are.

See also below regarding UI.

To be specific: We've fixed the bits that make updates reliant on scopes.

Yup. Sounds good. I left some comments in #514, but they are just minor tweaks. I agree that this part is solved. Mainly we need to make the agreed stuff normative in spec.

Another way to look at this is that people are constantly up against the same-origin limitation. What's effectively a single website is often spread out over multiple servers. We're making this issue worse by forcing people to not just stay within a server, but within one directory.

I don't think I really get this. If I add-to-homescreen jakearchibald.com/space-game and jakearchibald.com/drawing-app, each can have its own SW with different scopes. If I open space-game and navigate within its scope via links, I'd expect that to happen within the same window. If I open space-game and navigate to a url in drawing-app's scope, I'd expect the OS to transition from one "app" to the other as it would with native apps. If I open space-game and navigate to a url that isn't in scope of an added-to-homescreen app, I'd expect it to launch the browser app for it.

Scopes are really useful here.

Agreed. I'm definitely not arguing that scopes aren't useful. Quite the opposite.

I agree with all of the flows that you mention above. So a question is what you expect to happen in the maps example? Would you expect that clicking "settings" in google maps would bring you to the browser to set your home address, or would you expect to remain inside of the maps app?

Likewise for @LarsKemmann's example. Would users expect the UI for /manage/ to live in a different app than the UI for /admin/? It most likely depends, but I would say it's a decision that should be @LarsKemmann's, not ours. I.e. it seems like a developer decision, not something that the web platform should have an opinion on.

I agree that in that case you wouldn't want the SW for /manage/ to suck down resources for /admin/ if the user doesn't have permission to access them. But that should be driven by app logic. Presumably the SW can find out if the user has access to the admin UI and only download it if necessary. These types of decisions are exactly why we're using JS and not manifests.

Proposed solution Alt 2

Does this work for Mozilla? If so I'd like to give it more thought. Maybe scope could be explicitly set to null to opt out of fetch?

If we agree that "scope less" workers don't pose a problem for updates, then maybe we can default the scope to "/" but allow it to be explicitly set to null. That way I think we still solve the same problems as Alt 1, including no longer needing the look-for-eventhandlers pattern.

@jakearchibald
Copy link
Contributor

If you were using offline maps, would you expect to be able to click the "settings" button in the maps UI and then configure your home address?

I think it'd launch a separate preferences app, since Google has bundled all preference into one app. As a user flow, it isn't a disaster, but hopefully it'd persuade Google to make their preferences more contextual.

Agreed. I'm definitely not arguing that scopes aren't useful.

@annevk is. Can you two fight it out? Let us know what bits Mozilla is blocking on.

@martinthomson
Copy link
Member Author

I think that the use of scopes was covering a several somewhat distinct things:

  • the routing of fetch events to the right SW (I think that this is super important)
  • the permission grant (@tabatkins seems to have addressed that adequately; though I note that that suggests permissions are scoped more narrowly than origin, which I'm not sure about)
  • update (I think that we've reached some sort of conclusion there, which seems good; I like the idea that update isn't so tightly coupled, particularly if an API hook is provided)
  • identification (this was the original reason for the issue; and if we accept @sicking's proposal, I think that addresses it)

I've always regarded the multiple scope thing as orthogonal to the above. I do think that it's a virtue of @sicking's proposal that it's moved from exactly one scope to any number, but I do want to ensure that this separation is precise.

@annevk
Copy link
Member

annevk commented Oct 28, 2014

Let me try to present the case for a scopeless version of SW. The API would look something like this:

interface ServiceWorkerContainer {
  ...
  Promise<ServiceWorkerRegistration> register(USVString scriptURL, optional RegisterOptions options);
  Promise<ServiceWorkerRegistration> getRegistration();
  ...
};

dictionary RegisterOptions {
  boolean disableFetch = false;
};

No need for naming, no need for scopes.

This does make it harder for google.com to deploy SW for its services short term. I think that is okay. I hope http://www.chromium.org/developers/design-documents/per-page-suborigins can solve that problem going forward, while maintaining the security boundary an origin-bound SW has.

It is hard to quantify now, but I think just as with cookies, we'll find that scoping lacking an actual boundary is problematic and wish we had designed it differently.

And if it turns out that once deployed this is not working, it will be easy to add some kind of scoping without security boundary later through an opt-in API.

If @slightlyoff is no longer interested in something like this I guess I'll cast my vote for Alt #2 + nullable scopes, but I thought it was worth a try.

@jakearchibald
Copy link
Contributor

This does make it harder for google.com

And github pages, and @LarsKemmann's example. I don't think pretending this is a Google-only problem is helpful.

@sicking
Copy link

sicking commented Oct 29, 2014

@jsbell mentioned yesterday that he suspected that the reason Google Maps
had migrated from maps.google.com to www.google.com/maps was that they
wanted to be same-origin with content on www.google.com.

If that's the case then it having google use suborigins to enable separate
SWs for separate apps wouldn't work.

/ Jonas

@annevk
Copy link
Member

annevk commented Oct 31, 2014

Issue #468 is a perfect example why we need actual security boundaries here. Everyone tries to come up with ways to have /A/ avoid taking over the URL space of /B/, without anyone in that thread considering that /B/ can trivially modify the cache of /A/ since they are same-origin.

As for google.com/maps. I suspect it wants the same permissions as google.com and permission grants should probably go per origin. I doubt it needs anything else however as postMessage() and friends can be used for that.

I don't think demos on GitHub should trump actual security issues.

@annevk
Copy link
Member

annevk commented Nov 24, 2014

This is now #566.

@annevk annevk closed this as completed Nov 24, 2014
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