Background sync & push messaging: declarative vs imperative

[cross-posted to public-webapps and public-sysapps]

A couple of us from Chrome have taken a holistic look at how we could add
standardized APIs for web apps to execute/sync in the background.

This is an important capability, yet can be safely granted to low-privilege
web apps, as long as battery consumption and metered data usage are (very)
carefully limited.

This is not a spec proposal - instead I’m looking for feedback on the
high-level choice between an imperative or declarative set of APIs.


USE CASES

1. Sync when next online

- Need to send emails & document edits written offline as soon as the
device goes back online (even if app is not open), and get back sending
status, or for documents the result of merging the user's diff with any
changes made by other users.

e.g. document editing / email client / instant messaging / play-by-mail
games

2. Background sync

- Need to periodically fetch new content in the background, so user will
have fresh content if they open the app offline.

- Content often depends on device location (hence requires polling).

e.g. news / weather / maps / social stream

3. Large background transfers

- Need to upload/download large files. App won't remain in the foreground
for long enough, so this must happen in the background; and even in the
background, the app is unlikely to be allowed to stay open long enough
(e.g. iOS 7 limits the execution time of Background Fetch to 30
seconds<http://www.objc.io/issue-5/multitasking.html>),
so there needs to be a way of handing over large transfers to the
browser/OS to perform efficiently without the app having to keep running.

- Ideally such transfers would auto-resume after device reboots.

e.g. file syncing / movies / video camera

4. Push notifications

- Need instant push notification of messages received by the server, or as
soon as the device comes online if it was offline (even if app is not open).

e.g. email client / instant messaging / play-by-mail games

5. Push to sync (tickle)

- Sometimes a push needs to trigger a sync, for example to fetch email
attachments.

e.g. file & document syncing / email client / play-by-mail games

6. Delayed local notifications

- Need to show notifications at some time in the future.

- Various possible interactions with timezone changes and daylight savings
time.

e.g. egg timer / alarm clock / calendar

7. Delayed remote notifications

- Consider a calendar app: it must reliably show notifications for
remotely-added events, even if the app hasn’t been opened since the event
was added. Hence there must be some mechanism for a push message from the
server to cause a local notification to be set at some time in the future.

e.g. calendar / cloud synced alarm clock


Solving all of these requires a variety of different APIs. But it's
important that they fit together well, and it turns out that the API design
choices are closely interrelated.

There are two broad approaches to providing such capabilities on the web:


A) IMPERATIVE APPROACH

One approach is to allow JavaScript to be run on demand in the background.
The prime candidate for this is to extend
ServiceWorkers<https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md>to
be a generic mechanism for receiving events in the background (without
a
page having to be open).

1. Sync when next online

To address use case #1, add an API that requests your ServiceWorker to get
woken up once in the background, as soon as possible after the device next
goes online. This could be an extension of the Task Scheduler
API<http://www.w3.org/2012/sysapps/web-alarms/>.
This API should probably only be available when running in the foreground.

navigator.taskScheduler.addOneShot({

    requireOnline: true

}, myData);

2. Background sync

Similarly, to address use case #2, let web apps request their ServiceWorker
to be periodically run in the background, at an interval of the UA’s choice
(using aggressive
heuristics<http://lists.w3.org/Archives/Public/public-sysapps/2013Nov/0039.html>to
choose the most battery/data-efficient moment to run it); though we
could allow the developer to specify a minimum interval (i.e. if content
updates daily, you could specify that there’s no point syncing more often).

navigator.taskScheduler.addRepeating({

   minInterval: "2h"  // UA will pick a >= interval

}, myData);

We’d probably strictly limit the execution time of the ServiceWorker to
conserve battery (by comparison, in native apps iOS 7 limits the execution
time of Background Fetch to 30
seconds<http://www.objc.io/issue-5/multitasking.html>
).

3. Large background transfers

Although we’d be limiting background execution time of the ServiceWorker,
we can compensate by making it possible to initiate long-running
up/downloads, managed by the browser/OS.

This could be done by letting you mark async XMLHttpRequests as
"persistent", somewhat like the Beacon API <http://www.w3.org/TR/beacon/>,
except that the browser would periodically retry requests if the device was
offline, and make the results available to the page or ServiceWorker next
time the web app is launched.

Alternatively we could reuse ServiceWorker’s scriptable Cache
objects<https://github.com/slightlyoff/ServiceWorker/blob/master/caching.md>.
You’d get syntax something like this:

caches.set("movie-cache", new Cache(

   "http://my-app.com/The%20Lion%20King.mp4",

   "http://my-app.com/The%20Jungle%20Book.mp4"

));

This would cause the UA to download these large files in the background.
Once all the downloads in the cache are finished, they would show up in
caches.get("movie-cache").

4. Push notifications & 5. Push to sync (tickle)

To address use cases #4 and #5, implement something like the Push
API<http://www.w3.org/TR/push-api/>,
allowing servers to remotely wake up a ServiceWorker. The ServiceWorker can
then sync, show a notification, etc. (For battery life reasons, silent push
messages probably need to be throttled, in which case we could consider
adding a never-throttled variant for messages that show a user-visible
notification).

PUT /push/send/device-c0fa407591 HTTP/1.1

Host: browser-push-server.com

version=5

6. Delayed local notifications

To address use case #6, extend the Notifications
API<http://notifications.spec.whatwg.org>so notifications can exist
independently of the lifetime of a web page, and
when activated will wake up the ServiceWorker or web page and fire an event
on them.

There also needs to be a way to fire a notification after a precise time
delay; the natural way to do this would be to also allow exact scheduling
of time sensitive tasks using the Task Scheduler
API<http://www.w3.org/2012/sysapps/web-alarms/>,
but for battery reasons we shouldn’t give web apps carte blanche here;
instead to prevent abuse we might want to only allow exact scheduling if
you also show a user-visible notification. So we could either silently
throttle precisely scheduled tasks if it turns out they’re not showing
notifications, or we could not implement exact scheduling in Task Scheduler
at all, and instead extend Notifications to have an optional time delay.

new Notification("Meeting about to begin", {

    utcTime: 1386011460303,

   body: "Room 101",

   launchUrl:  "https://my-app.com/?from=notification"

});

7. Delayed remote notifications

By combining the APIs from the previous two sections, you can have a push
message handler that schedules a delayed notification, satisfying use case
#7.


B) (SEMI-)DECLARATIVE APPROACH

Alternatively, it would be possible to solve almost all these use cases
without having to run JavaScript in the background, with a combination of
declarative APIs. This seems to be a new idea, so it warrants more detailed
explanation.

A key premise of this approach, is that it’s always ok to wake up the
device/app in order to display a user-visible notification, since these
provide value to the user (or if they don’t, the user can deal with it by
revoking the permissions of the web app). But other than that, it’s best to
limit battery/bandwidth/RAM consumption in the background.

*These are just pseudo-APIs to demonstrate possible capabilities and not
actual proposals.*

1. Sync when next online & 3. Large background transfers

For use cases #1 and #3, uploads and downloads need to happen in the
background, and in a declarative world these would need to be fully managed
by the browser. As in the imperative approach to use case #3, we could
reuse ServiceWorker’s scriptable Cache
objects<https://github.com/slightlyoff/ServiceWorker/blob/master/caching.md>for
background downloads, either from a ServiceWorker, or by exposing them
to ordinary web pages:

caches.set("movie-cache", new Cache(

   "http://my-app.com/The%20Lion%20King.mp4",

   "http://my-app.com/The%20Jungle%20Book.mp4"

));

This would cause the UA to download these large files in the background.
Once all the downloads in the cache are finished, they would show up in
caches.get("movie-cache").

Similarly background uploads are sometimes necessary. For use case #1 (e.g.
email), it is important that the background upload happen as soon as
possible once the device goes back online; for use case #3 (e.g. video
sharing), the background uploads need to support uploading large files,
however timeliness is less important, and it might be best for the upload
to only happen over WiFi. It might still be possible for the two to use the
same syntax; for example we could allow
adding<https://github.com/slightlyoff/ServiceWorker/issues/118>a
Request<https://github.com/slightlyoff/ServiceWorker/blob/062ecbc967e11969adef85fd044a3fab0cdf7e1c/service_worker.ts#L210>object
to a Cache instead of just URLs:

caches.set("outbox", new Cache(new Request({

   method: "POST",

   url: "http://my-app.com/send-mail",

   body: my_mime_multipart_message

}), ...));

The UA would perform these requests in the background, and store the
response in the cache object. As for urgency, perhaps there could be some
"urgent_request_by_user" flag that the web developer can set to indicate
that the user explicitly requested this and intends it to be sent over
cellular data.

2. Background sync

The mechanism above is powerful, but sometimes instead of a one-shot
up/download you need to sync data that regularly updates.

Push can be great for that; but sometimes you have to poll, for example
weather/map apps syncing forecasts/tiles based on your current location.
And sometimes (though perhaps rarely) even when you can push it’s more
efficient to poll smartly - for example if my social network feed has new
posts several times a minute, I really don’t want it waking up my phone
radio every few minutes while I’m asleep (especially if I forgot to plug it
into a charger).

We could extend the caches mechanism above with the ability for the UA to
periodically check -- in the background -- for updates to the files in the
cache (using standard HTTP caching logic). The UA could optionally include
the user’s geolocation in a header when doing so.

caches.set("sync-cache", new Cache(url1, url2, ...));

caches.requestBackgroundSyncing("sync-cache", {

   geolocation: true

});

The client would indicate with cookies or somesuch any user preferences
about what should be synced. The server could update the cookies to
indicate how much it has synced, so during the next sync it knows what to
do.

However this is still quite limiting, as it requires the client to know in
advance the URLs of the files the server will want it to download. It would
probably lead to hacks where clients request that a bunch of meaningless
foo1, foo2, foo3 urls get synced, and the actual content of those urls gets
rotated server-side. A more flexible approach would add indirection, and
let the server provide a barebones "manifest" file, that would just be a
newline-separated list of URLs to sync.

caches.set("sync-cache", new CacheManifest("/sync-manifest"));

caches.requestBackgroundSyncing("sync-cache", {

   geolocation: true

});

The UA would periodically check for updates to the manifest file, fetch any
new URLs, update existing ones, and delete cache entries for any URLs
removed from the manifest. Updates would presumably be atomic (sync all
files in the manifest before updating the cache exposed to the page). I’ve
glossed over various details, such as what constitutes an update to the
manifest file, but it should be possible to define something reasonable,
learning from the lessons of AppCache.

As with the imperative approach, the UA would use various
heuristics<http://lists.w3.org/Archives/Public/public-sysapps/2013Nov/0039.html>to
determine when is a good time to sync each app, such as batching,
screen
on, wifi available, charging, or even what times of day each web app is
typically used.

4. Push notifications

For use case #4, there needs to be a standardized way for an app’s server
to instantly push user-visible notifications to the device (with automatic
retry if the device is offline). Interacting with the notification would
launch the app, at the given URL.

POST /push/send/device-c0fa407591 HTTP/1.1

Host: browser-push-server.com

Content-Type: application/json

{

    "title": "Message from Ben",

    "body":  "This should arrive instantly :)",

   "sound": "https://my-app.com/you-got-mail.mp3",

   "launchUrl":   "https://my-app.com/?from=notification"

}

5. Push to sync (tickle)

In the push notification example above, the notification didn’t contain any
data other than the notification text, and the URL to launch when the
notification was clicked. It would be reasonable to add a "data" member to
the JSON, which would somehow be passed to the web page when the
notification is clicked.

However most push messaging servers have strict size limits on the payload
(e.g. 4096 bytes). There are many cases, where an app needs to send more
than this, for example an email client that needs to download the
attachments that accompany an email, so that if the user clicks the
notification whilst offline, they will be able to view the attachments.

To handle this case, we can allow a silent push message that kicks off an
immediate background sync of one or more of the named caches from "2.
Background sync" above:

POST /push/send/device-c0fa407591 HTTP/1.1

Host: browser-push-server.com

Content-Type: application/json

{

    "updateCaches": ["sync-cache", "attachments-cache"]

}

6. Delayed local notifications

For use case #6, there needs to be a way to locally schedule notifications
with a time delay (since the device might be offline the whole time, you
can’t rely on push notifications for this). We could extend the Notifications
API <http://notifications.spec.whatwg.org> as follows:

new Notification("Meeting about to begin", {

    utcTime: 1386011460303,

   body: "Room 101",

   launchUrl:  "https://my-app.com/?from=notification"

});

The notification would be delayed until the given moment. A URL is provided
to launch the app in response to the user clicking on the notification, if
the app isn’t already running; usually, this URL would be available offline
due to AppCache or a
ServiceWorker<https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md>.
An API like Notification.get()<http://notifications.spec.whatwg.org/#dom-notification-get>would
let you read and cancel pending notifications.

7. Delayed remote notifications

A subtle variant of notifications is use case #7. If you add a same day
event to your cloud calendar with a reminder 10 minutes before the event,
then the device goes online for a while but the calendar app does not get
launched by the user, and the device goes back offline for the hours
leading up to the event, the calendar app needs to be able to fire that
notification 10 minutes before the event, despite (in this declarative
model) having been executed neither at the time the push notification
arrived, nor at any time since then.

Since it doesn’t get executed, such an app can’t locally schedule a delayed
notification, so instead the push notification API introduced for use case
#4 could be extended so you can specify a time delay before the
notification fires (as with delayed local notifications).

POST /push/send/device-c0fa407591 HTTP/1.1

Host: browser-push-server.com

Content-Type: application/json

{

    "utcTime": 1386011460303,

   "title":   "Meeting about to begin",

   "body":    "Room 101",

   "launchUrl":     "https://my-app.com/?from=notification"

}

We’d need to also support push messages that cancel earlier delayed push
notifications (e.g. if the event later gets removed from your cloud
calendar). And the same API that lets you inspect local delayed
notifications could also read/cancel pushed delayed notifications.


CONCLUSION

The imperative approach (A) seems a cleaner set of APIs, from an extensible
web <http://extensiblewebmanifesto.org/> point of view.

However whenever a ServiceWorker (or equivalent) runs in the background, it
requires a full JS interpreter, and all the associated browser machinery to
support APIs like Geolocation, which together consumes a significant amount
of RAM. This can be a problem on mobile; for example on Windows Phone,
background
agents are restricted to 11 MB of
RAM<http://msdn.microsoft.com/en-us/library/windowsphone/develop/hh202942(v=vs.105).aspx#BKMK_ConstraintsforallScheduledTaskTypes>on
devices with less than 1GB, otherwise 20 MB, and are terminated if
they
exceed this limit. This is presumably in order to ensure that the
foreground app (and other important processes) remain responsive (and don’t
get evicted). The limits on Android and iOS are vaguer and
device-dependent, but increased background RAM usage could potentially be a
deal-killer for the imperative approach.

(The UA can tightly control battery and data usage in both the imperative
and declarative approaches described above, which is why I’m not focusing
on those here).

We’re currently thinking of prototyping the imperative approach. But it
seems that the 2 main capabilities introduced incrementally in section B
(declarative push message actions, and the caches mechanisms) could provide
a viable plan B if the imperative approach turns out to use too much RAM.

*Please don’t bikeshed the syntaxes yet*; at this stage the main open
questions are:


   1.

   Is this a reasonable vision for the set of imperative capabilities?
   2.

   Would such declarative capabilities be sufficient to address all
   important use cases?
   3.

   How easy would web developers find developing against such declarative
   APIs, compared to the imperative approach? The server would play a slightly
   greater role in driving the sync logic; but that may not be so terrible.
   4.

   How much RAM would such a declarative approach ultimately save? Is it
   worth it?


--John

Received on Thursday, 19 December 2013 17:03:17 UTC