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

Remove periodic bits #91

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 9 additions & 73 deletions explainer.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# Background synchronization explained

This is a specification that brings both one-off and periodic synchronization to the web, in the form of [Service Workers](https://github.com/slightlyoff/ServiceWorker) events.
This is a specification that brings background synchronization to the web, in the form of a [Service Worker](https://github.com/slightlyoff/ServiceWorker) event.

## One-off synchronization
If you write an email, instant message, or simply favourite a tweet, the application needs to communicate that data to the server. If that fails, either due to user connectivity, service availability, or anything in-between, the app can store that action in some kind of 'outbox' for retry later.

If you write an email, instant message, or simply favourite a tweet, the application needs to communicate that data to the server. If that fails, either due to user connectivity, service availability or anything in-between, the app can store that action in some kind of 'outbox' for retry later.

Unfortunately, on the web, that outbox can only be processed while the site is displayed in a browsing context. This is particularly problematic on mobile, where browsing contexts are frequently shut down to free memory.
Unfortunately, on the web, that outbox can only be processed while the site is displayed in a browsing context. If the user navigates away, closes the tab, or closes the browser, the outbox can't be synced until the page is visited again. This is particularly problematic on mobile, where browsing contexts are frequently shut down to free memory.

Native application platforms provide [job scheduling](https://developer.android.com/reference/android/app/job/JobScheduler.html) APIs that enable developers to collaborate with the system to ensure low power usage and background-driven processing. The web platform needs capabilities like this too.

Expand Down Expand Up @@ -52,70 +50,11 @@ The promise passed to `waitUntil` is a signal to the UA that the sync event is o

The UA may coalesce synchronizations to reduce the number of times the device, radio and browser need to wake up. The coalescing can be across origins, and even coalesced across the OS with native synchronizations. Although the event timings are coalesced, you still get an event per pending sync registration.

## Periodic synchronization

Opening a news or social media app to find content you hadn't seen before - without going to the network, is a user experience currently limited to native apps.

[The push API](https://w3c.github.io/push-api/) allows the server to dictate when the service worker should wake up and seek updates, but these are not sensitive to connection and charging state. Also, some sites update too frequently to warrant a push message per update (think Twitter, or a news site).

Periodic syncs are simple to set up, don't require any server configuration, and allow the UA to optimize when they fire to be most-helpful and least-disruptive to the user. E.g. if the UA knows the user has a morning alarm set, it may run synchronizations shortly beforehand, giving the user quick and up-to-date information from their favourite sites.

### The API

**To request a periodic sync:**

```js
navigator.serviceWorker.ready.then(function(registration) {
registration.periodicSync.register({
tag: 'get-latest-news', // default: ''
minPeriod: 12 * 60 * 60 * 1000, // default: 0
powerState: 'avoid-draining', // default: 'auto'
networkState: 'avoid-cellular' // default: 'online'
}).then(function(periodicSyncReg) {
// success
}, function() {
// failure
})
});
```

* `tag`: This operates like a notification's tag. If you register a sync and an existing sync with the same tag is pending, it returns the existing registration and updates it with the options provided. **Note:** one-off and periodic sync tags have separate namespaces.p
* `minPeriod`: The minimum time between successful sync events. A value of 0 (the default) means the UI may fire the event as frequently as it wishes. This value is a suggestion to prevent over-syncing. Syncing may be less frequent depending on heuristics such as visit frequency & device status. If timing is critical, [the push API](https://w3c.github.io/push-api/) may better suit your requirements.
* `powerState`: Either "auto" (default) or "avoid-draining". "avoid-draining" will delay syncs on battery-powered devices while that battery isn't charging. "auto" allows syncs to occur during battery-drain, although the UA may choose to avoid this depending on global device status (such as battery-saving mode) or user preferences.
* `networkState`: One of "online" (default), "avoid-cellular", or "any". "avoid-cellular" will delay syncs if the device is on a [cellular connection](https://w3c.github.io/netinfo/#idl-def-ConnectionType.cellular) - but be aware that some users may never use another connection type. "online" will delay syncs if the device is online, although the UA may choose to avoid particular connection types depending on global device status (such as roaming) or user preferences. "any" is similar to "online", except syncs may happen while the device is offline.

**To respond to a periodic sync:**

Over in the service worker:

```js
self.addEventListener('periodicsync', function(event) {
if (event.registration.tag == 'get-latest-news') {
event.waitUntil(fetchAndCacheLatestNews());
}
else {
// unknown sync, may be old, best to unregister
event.registration.unregister();
}
});
```

Like one-off syncs, the promise passed to `waitUntil` is a signal to the UA that the sync event is ongoing and that it should keep the SW alive if possible. Rejection of the event signals to the UA that the sync failed. Upon rejection the UA should reschedule (likely with a UA-determined backoff). `minPeriod` may be ignored for rescheduling.

Also like one-off syncs, the UA may coalesce synchronizations to reduce the number of times the device, radio and browser need to wake up. In fact, the coalescing is more extreme for periodic syncs, as the result is perceived to be "beneficial" as opposed to "critical".


### What periodic sync is not

Periodic sync is specifically not an exact alarm API. The scheduling granularity is in milliseconds but events may be delayed from firing for several hours depending on usage frequency and device state (battery, connection, location).

The results of a sync running should be "beneficial" not "critical". If your use-case is critical, one-off syncs or [the push API](https://w3c.github.io/push-api/) may serve your requirements.

## Getting pending sync details

As seen in the previous code examples, `sync.register()` and `syncEvent.registration` expose a sync registration object. You can also fetch them using `sync.getRegistration`, `sync.getRegistrations`, and `periodicSync.getRegistration`, `periodicSync.getRegistrations`.
As seen in the previous code examples, `sync.register()` and `syncEvent.registration` expose a sync registration object. You can also fetch them using `sync.getRegistration`, and `sync.getRegistrations`.

For example, to unregister a single one-off sync:
For example, to unregister a single sync:

```js
navigator.serviceWorker.ready.then(function(registration) {
Expand All @@ -125,11 +64,11 @@ navigator.serviceWorker.ready.then(function(registration) {
});
```

To unregister all periodic syncs, except "get-latest-news":
To unregister all syncs, except "get-latest-news":

```js
navigator.serviceWorker.ready.then(function(registration) {
registration.periodicSync.getRegistrations().then(function(syncRegs) {
registration.sync.getRegistrations().then(function(syncRegs) {
syncRegs.filter(function(reg) {
return reg.tag != 'get-latest-news';
}).forEach(function(reg) {
Expand All @@ -141,11 +80,9 @@ navigator.serviceWorker.ready.then(function(registration) {

## Checking for Permission

Permissions for `sync` and `periodicSync` are entirely separate, and `periodicSync` is expected to be more difficult to obtain permission for.

```js
navigator.serviceWorker.ready.then(function(registration) {
registration.periodicSync.permissionState().then(function(state) {
registration.sync.permissionState().then(function(state) {
if (state == 'prompt') showSyncRegisterUI();
});
});
Expand All @@ -155,5 +92,4 @@ navigator.serviceWorker.ready.then(function(registration) {

* Since Service Workers are a requirement for sync, and since Service Workers are limited to HTTPS origins, that restriction applies here too.
* All fetches during sync events must be HTTPS. HTTP fetches will be rejected.
* Sync may not be available to all web applications, not even all apps served over SSL. Browsers may choose to limit the set of applications which can register for synchronization based on quality signals that aren't a part of the visible API. This is especially true of periodic sync.
* Like all ServiceWorker events, 'sync' and 'periodicsync' may be terminated if they're taking an unreasonable amount of time or CPU. This is not a tool for distributed bitcoin mining :)
* Like all ServiceWorker events, 'sync' may be terminated if they're taking an unreasonable amount of time or CPU. This is not a tool for distributed bitcoin mining :)
49 changes: 1 addition & 48 deletions idl.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
```js
partial interface ServiceWorkerRegistration {
readonly attribute SyncManager sync;
readonly attribute PeriodicSyncManager periodicSync;b
};

interface SyncManager {
Expand All @@ -16,7 +15,7 @@ interface SyncManager {
interface SyncRegistration {
readonly attribute DOMString tag;
readonly attribute Promise<boolean> done;

Promise<boolean> unregister();
};

Expand All @@ -30,45 +29,8 @@ enum SyncPermissionState {
"granted"
};

enum SyncNetworkState {
"any",
"avoid-cellular",
"online"
};

enum SyncPowerState {
"auto",
"avoid-draining"
};

interface PeriodicSyncManager {
Promise<PeriodicSyncRegistration> register(optional PeriodicSyncRegistrationOptions options);
Promise<PeriodicSyncRegistration> getRegistration(DOMString tag);
Promise<sequence<PeriodicSyncRegistration>> getRegistrations();
Promise<SyncPermissionState> permissionState();

readonly attribute unsigned long minPossiblePeriod;
};

interface PeriodicSyncRegistration {
readonly attribute DOMString tag;
readonly attribute unsigned long minPeriod;
readonly attribute SyncNetworkState networkState;
readonly attribute SyncPowerState powerState;

Promise<boolean> unregister();
};

dictionary PeriodicSyncRegistrationOptions {
DOMString tag = "";
unsigned long minPeriod = 0;
SyncNetworkType networkState = "online";
SyncPowerState powerState = "auto";
};

partial interface ServiceWorkerGlobalScope {
attribute EventHandler onsync;
attribute EventHandler onperiodicsync;
};

[Constructor(DOMString type, SyncEventInit eventInitDict), Exposed=ServiceWorker]
Expand All @@ -79,13 +41,4 @@ interface SyncEvent : ExtendableEvent {
dictionary SyncEventInit : EventInit {
required SyncRegistration registration;
};

[Constructor(DOMString type, PeriodicSyncEventInit eventInitDict), Exposed=ServiceWorker]
interface PeriodicSyncEvent : ExtendableEvent {
readonly attribute PeriodicSyncRegistration registration;
};

dictionary PeriodicSyncEventInit : EventInit {
required PeriodicSyncRegistration registration;
};
```
13 changes: 0 additions & 13 deletions use-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,6 @@ The above are from actual customers, but not naming names until they agree to be
* If sync fails, reschedule it - `event.waitUntil` can be used to extend lifetime and indicate failure. We’ll want some kind of back-off for reschedules.
* Multiple sync requests for the same name are coalesced into one sync event in the SW - allows multiple independent systems to add to an idb “outbox” and request an an outbox sync.

## Approximately regular sync

### Use-cases

* **News site** - fetching daily news for quick display in the morning
* **Social media** - periodic updates so initial display is content user hasn't seen, even if offline
* **Blog updates** - Updated blog content without having to set up a push server
* **RSS reader** - Check for updates across multiple origins

These are either "I don't want to / can't set up push" or "updates are so frequent push doesn't make sense".

Exact-time syncs are out-of scope, but may be investigated in the context of an alarms API in the future.

# Concerns

* **Location tracking** - an interval sync or failing one-off sync could lead to user tracking via IP
Expand Down