tl;dr: I don’t think that we need this API. We could (and should)
implement a way for applications to learn how many “silent” pushes
they have remaining.
I realize that it’s taken a long time for me to produce this analysis.
That’s entirely my own fault, I had reservations about the API that I
had trouble forming into something concrete and I really hate
non-specific grumbling. I definitely owe Peter a drink over this one.
Purpose
The entire reason that the Budget API [1] exists is around our
collective attempts to retain some degree of accountability for sites
[2].
In designing the Push API [3] we discovered that we were developing a
way for sites to run at arbitrary times, in the background. That
removes accountability for sites. Web browsers don’t have UX for
background activity. We sort of get away with this for fetch and
service workers because they only run in response to foreground
activity (more or less [4]). However, with Push and Background Sync
[5], the application gets complete control over when this activity
occurs.
The reaction to this was to limit the amount of background activity a
site gets. A site would be able to activate a service worker a
limited number times between site visits. Visit the site and you
would get some more credits to spend on push. We eventually decided
not to penalize sites that show notifications, on the principle that
the notification includes
The solution that this API proposes has two parts. Firstly, let
applications see the budget they are given, let them query it.
Secondly, let them reserve resources. This second part is the
important one, it allows applications to gain some certainty.
The Use Case is Real
The underlying problem is a genuine annoyance. Sites have a real hard
time with the budget we set. It’s especially bad because messages
just stop arriving and no one knows why.
It’s particularly hard for third-party push vendors, who struggle with
the requirement to have users visit their origin. We don’t credit
third-party loads (iframes and the like), but that is often the only
interaction that users have with these senders. Other than to make it
possible for these applications to learn the parameters of their
confinement, the proposal doesn’t really do a lot of alleviate that
problem.
Query Methods are more Liability than Asset
The definition of query methods for examining an origin’s budget is a
little too optimistic for me. I would remove getBudget()
and
getCost()
from the proposal.
The notion that the browser has a fixed hidden variable for each site
that it can then predict might be close to what we have today, but I’m
concerned that it will cause us to commit to a particular model of
interaction that could constrain us in ways we don’t understand.
For instance, this assumes a single budget, which might be the right
simplification, but it could prevent a browser from tracking different
APIs separately. The browser might be able to synthesize a single
value, but that might lose information.
The way that future values are obtained with getBudget()
is
unrealistic, I think. If anything like this were to exist, I would
prefer a query semantic “if I asked for X at time Y, do you think that
you would allow it?” That leads to my suggestion to extend
permissions (below).
Too Generic
Names matter. This API uses the generic “Budget” term without
consideration for the narrower scope of its applicability. It’s very
clear in the proposal that this is for background tasks only, but
the name doesn’t capture that.
The presumption here is that it would be useful for things other than
what it is being used for today, but that’s overly optimistic in my
view. I’ve too much experience with engineering optimistic generality
into systems only to find that the extra effort was wasted. YAGNI
applies.
For background sync, the API doesn’t really apply right now, and it
might never. Background sync only really runs as a consequence of
activity, and while push might be the sort of activity that triggers
the need for a sync (see [6]), it doesn’t seem like background sync
can be run on a timer any more. The lastChance
attribute of SyncEvent
more or less covers the use case that .reserve() does. (I’ve more on
this point below.)
That suggests that background sync could be a non-customer of this
feature, leaving only push, where a more direct approach might work.
If a background sync were to be created that used a timer, that could
build a reservation system in directly. For example, if background
sync were to acquire a second argument to .register() that indicated
how long to wait before attempting a sync, that method might throw if
the site budget was overcommitted.
.request() Alternative: Extend Permissions
One different way of looking at this problem is to consider it part of
the permissions API. Right now, that API assumes a fairly simplistic
model where different capabilities are allowed, denied, or as yet
unknown (usually because they involve asking users for permission). A
possible fourth state might be added: use-limited (name negotiable).
The API could be used to reserve uses of different capabilities. For
instance,
const desc = { name: 'push', userVisibleOnly: false };
const status = await permissions.query(desc);
if (status === 'use-limited') {
const reserved = await permissions.reserve(desc);
if (reserved) {
// do something that might enable push
}
}
However, that’s a superficial change and not really getting into the
real issues. Though integration might address issue #14 from the TAG
review.
Reserve the World
Based on the current API, I can see the following code being written:
let reservations = loadExistingCountFromIndexedDb() || 0;
while (await navigator.budget.reserve('silent-push')) {
++reservations;
}
console.log(`We can send {reservations} more silent push messages.`);
That would be sad, because it somewhat misses the point of the API.
But it’s a very useful reduction in complexity for sites. If
PushManager
exposed a silentPushesRemaining
count (the name is a
strawman) that would save the looping.
The information that is lost by way of this simplification is that
allowances might decay over time, even with the site being active.
That makes silentPushesRemaining
a false promise, because though it
might say 10 today, after 72 hours it might be reduced to 4 even if a
silent push wasn’t sent. I think that it is best to address that with
documentation, noting that this number speaks of independent
activations over the next day (or some fixed time period).
This could be reduced further to a youHaveToShowANotification
boolean
on PushEvent
. That was suggested at one point, but that removes
critical information. The problem with a lastChance
-style boolean is
that it doesn’t really give an application enough information to make
longer-term decisions. It can’t plan out its strategy for pushing
messages when it doesn’t know how many it has to spend.
Just a Simple Push API Attribute
The main weakness with .request()
in my opinion is that it isn’t
necessary. We built the push API without any notion of budget and so
an application can use the API without having to concern themselves
with such things. Without any API, the user agent that limits push
usage finds that sites break in surprising ways.
I spent a lot of time trying to think about a way to make .reserve()
or something like it critical to the functioning of the API, but the
inescapable reality here is that sites are using push today. Had we
built the API with the notion at its core, well, we’d not be stuck
grappling with this issue.
A silentPushesRemaining
attribute would seem to cover most of the use
cases with greater certainty. The problem of applications that were
built before this feature is added remains, but those applications are
currently either stuck without silent pushes, or they have to deal
with the apparent capriciousness of a browser that suddenly stops
delivering messages.
As such, we only have to deal with applications that have been updated
to use this API. Those applications can easily check whether a silent
push was permitted by looking at a silentPushesRemaining
attribute (or
an appropriately asynchronous getter).
(I’m aware that the Budget API allows for the actual cost of an
operation to be lower than the expected cost. That’s easy to account
for by recalculating the silentPushesRemaining
value over time; if
actual costs were lower than expected, you can easily increase the
allowance to reflect that.)
This view is further supported by the observation that .reserve() is
effectively implicit right now: every time you have an active push
subscription, you always have at least one reservation. Having
.reserve()
return false is no different to silentPushesRemaining
becoming 0 or a lastChance
flag.
Conclusion
BudgetService.reserve()
is solving a real problem. However .reserve()
could be replaced with a much simpler feature: an attribute on the
Push API.
Other aspects of the API are probably creating more problems than they
solve and should be removed.
[1] https://wicg.github.io/budget-api/
[2] https://docs.google.com/presentation/d/1soMBhvsd0wuk8PO5UjtrMd116Z95jJiZLxlUFomyBFs
[3] https://w3c.github.io/push-api/
[4] I still don’t understand what happens if we want to show a slow
script warning for a service worker…
[5] https://wicg.github.io/BackgroundSync/spec/
[6] https://github.com/w3c/push-api/issues/240 - this never got raised
on background sync, from what I can see