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

Properly define the pushsubscriptionchange event #234

Merged
merged 7 commits into from Feb 17, 2017
236 changes: 158 additions & 78 deletions index.html
Expand Up @@ -358,31 +358,81 @@ <h2>
with a <a>service worker registration</a> and a <a>service worker registration</a> has at
most one <a>push subscription</a>.
</p>
<p>
A <a>push subscription</a> has internal slots for a P-256 <a>ECDH</a> key pair and an
authentication secret in accordance with [[!WEBPUSH-ENCRYPTION]]. These slots MUST be
populated when creating the <a>push subscription</a>, and MUST remain constant for its
lifetime.
</p>
<p>
When a <a>push subscription</a> is <dfn data-lt=
"deactivate">deactivated</dfn>, both the <a>user agent</a> and the <a>push
service</a> MUST delete any stored copies of its details. Subsequent <a>push messages</a>
for this <a>push subscription</a> MUST NOT be delivered.
</p>
<p>
A <a>push subscription</a> is <a>deactivated</a> when its associated <a>service worker
registration</a> is unregistered, though a <a>push subscription</a> MAY be
<a>deactivated</a> earlier. A <a>push subscription</a> is removed when the <a>clear
registration</a> algorithm is run for the <a>service worker registration</a>.
</p>
<p>
A <a>push subscription</a> has an associated <dfn data-lt=
"push endpoints">push endpoint</dfn>. It MUST be the absolute URL exposed
by the <a>push service</a> where the <a>application server</a> can send <a>push
messages</a> to. A <a>push endpoint</a> MUST uniquely identify the <a>push
subscription</a>.
</p>
<p>
A <a>push subscription</a> has internal slots for a P-256 <a>ECDH</a> key pair and an
authentication secret in accordance with [[!WEBPUSH-ENCRYPTION]]. These slots MUST be
populated when creating the <a>push subscription</a>.
</p>
<p>
If the <a>user agent</a> has to change the keys for any reason,
it MUST <a>fire the pushsubscriptionchange event</a> with the
<a>service worker registration</a> associated with the <a>push subscription</a> as
<var>registration</var>, a <a>PushSubscription</a> instance representing the
<a>push subscription</a> having the old keys as <var>oldSubscription</var> and a
<a>PushSubscription</a> instance representing the <a>push subscription</a> having the new
keys as <var>newSubscription</var>.
</p>
<section>
<h2>
Subscription refreshes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
title case for titles (throughout)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

</h2>
<p>
A <a>user agent</a> or <a>push service</a> MAY choose to <dfn>refresh</dfn> a
<a>push subscription</a> at any time, for example because it has reached a certain age.
</p>
<p>
When this happens, the <a>user agent</a> MUST create a new <a>push subscription</a>
with the <a>push service</a> on behalf of the application, using the
<a>PushSubscriptionOptions</a> that were provided for creating the current
<a>push subscription</a>. The new <a>push subscription</a> MUST have a key pair that's
different from the original subscription.
</p>
<p>
When successful, <a>user agent</a> then MUST <a>fire the pushsubscriptionchange event</a>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about when the refresh is not successful? I think that we could recommend/suggest that the user agent either retain the old subscription, though it may treat the subscription as broken at its discretion.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we require the user agent to retry if the refresh is unsuccessful? (Or fire pushsubscriptionchange with newSubscription set to null, as a last resort?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the following paragraph:

If the user agent is not able to refresh the push subscription, it should periodically retry the refresh. When the push subscription can no longer be used, for example because it has expired, the user agent must fire the pushsubscriptionchange event with the service worker registration associated with the push subscription as registration, a PushSubscription instance representing the deactivating push subscription as oldSubscription and null as the newSubscription.

I like Kit's suggestion of setting newSubscription to null since it gives the developer something.

with the <a>service worker registration</a> associated with the <a>push subscription</a>
as <var>registration</var>, a <a>PushSubscription</a> instance representing the initial
<a>push subscription</a> as <var>oldSubscription</var> and a <a>PushSubscription</a>
instance representing the new <a>push subscription</a> as <var>newSubscription</var>.
</p>
<p>
<a>User agents</a> MAY continue to accept messages for the old <a>push subscription</a>
for a brief amount of time, but MUST stop doing so once the first message for the
refreshed <a>push subscription</a> has been received, as this indicates that the
<a>application server</a> has propagated the subscription change.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That creates an interesting externality: if the application is required to update two data sources with updated subscription details, it has to ensure that one source isn't used before the other is emplaced. I think that's a reasonable global trade-off though.

I would restructure this to more directly link the MAY to the reason:

To allow for time to propagate changes to <a>application servers</a>, a <a>user agent</a> MAY continue to accept messages for an old <a>push subscription</a> for a brief time after a refresh. Once messages have been received for a refreshed <a>subscription</a>, any old <a>subscription</a> MUST be <a>deactivated</a>.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Adopted.

</p>

<p class="issue">
The steps for creating a subscription should be separated from the
<a lt="PushManager.subscribe">subscribe</a> method into an algorithm of its own so that
it can be referenced here. Similarly, the <a>PushSubscriptionOptions</a> provided to the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would raise the issue on github.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#235. Removed here.

<a lt="PushManager.subscribe">subscribe</a> method should be associated with the
<a>push subscription</a> so that the refresh can access them.
</p>
</section>
<section>
<h2>
Subscription deactivation
</h2>
<p>
When a <a>push subscription</a> is <dfn data-lt=
"deactivate">deactivated</dfn>, both the <a>user agent</a> and the <a>push
service</a> MUST delete any stored copies of its details. Subsequent
<a>push messages</a> for this <a>push subscription</a> MUST NOT be delivered.
</p>
<p>
A <a>push subscription</a> is <a>deactivated</a> when its associated <a>service worker
registration</a> is unregistered, though a <a>push subscription</a> MAY be
<a>deactivated</a> earlier. A <a>push subscription</a> is removed when the <a>clear
registration</a> algorithm is run for the <a>service worker registration</a>.
</p>
</section>
</section>
<section>
<h2>
Expand Down Expand Up @@ -423,8 +473,13 @@ <h2>
acquiring permission or determining the permission status.
</p>
<p>
When a permission is revoked, all <a>push subscriptions</a> created with that permission
MUST be <a>deactivated</a>.
When a permission is revoked, the <a>user agent</a> MAY
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about if permission is granted again, after being revoked? @beverloo, I think you mentioned Chrome won't fire a pushsubscriptionchange event in that case, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think we found a case where we need to do this too. Let me file a more elaborate issue on it in a bit.

<a>fire the pushsubscriptionchange event</a> for subscriptions created with that permission,
with the <a>servce worker registration</a> associated with the <a>push subscription</a> as
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: servce

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

<var>registration</var>, a <a>PushSubscription</a> instance representing the
<a>push subscription</a> as <var>oldSubscription</var>, and <code>null</code> as
<var>newSubscription</var>. The <a>user agent</a> MUST, in parallel, <a>deactivate</a>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the user agent must deactivate the affected subscriptions in parallel

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

the affected subscriptions.
</p>
<p>
When a <a>service worker registration</a> is unregistered, any associated <a>push
Expand Down Expand Up @@ -999,26 +1054,29 @@ <h2>
<h2>
Events
</h2>
<p>
The Service Worker specification defines a <code>ServiceWorkerGlobalScope</code> interface
[[!SERVICE-WORKERS]], which this specification extends.
</p>
<pre class="idl">
partial interface ServiceWorkerGlobalScope {
attribute EventHandler onpush;
attribute EventHandler onpushsubscriptionchange;
};
</pre>
<p>
The <dfn>onpush</dfn> attribute is
an <a>event handler</a> whose corresponding <a>event handler event type</a> is
<code>push</code>.
</p>
<p>
The <dfn>onpushsubscriptionchange</dfn>
attribute is an <a>event handler</a> whose corresponding <a>event handler event type</a> is
<a>pushsubscriptionchange</a>.
</p>
<section data-dfn-for="ServiceWorkerGlobalScope">
<h2>Extensions to the <a>ServiceWorkerGlobalScope</a> interface</h2>
<p>
The Service Worker specification defines a <code>ServiceWorkerGlobalScope</code> interface
[[!SERVICE-WORKERS]], which this specification extends.
</p>
<pre class="idl">
partial interface ServiceWorkerGlobalScope {
attribute EventHandler onpush;
attribute EventHandler onpushsubscriptionchange;
};
</pre>
<p>
The <dfn>onpush</dfn> attribute is
an <a>event handler</a> whose corresponding <a>event handler event type</a> is
<code>push</code>.
</p>
<p>
The <dfn>onpushsubscriptionchange</dfn>
attribute is an <a>event handler</a> whose corresponding <a>event handler event type</a> is
<code>pushsubscriptionchange</code>.
</p>
</section>
<section>
<h2>
<dfn>PushEvent</dfn> interface
Expand Down Expand Up @@ -1147,59 +1205,81 @@ <h2>
</section>
<section>
<h2>
The "<dfn>pushsubscriptionchange</dfn>" event
The <dfn>pushsubscriptionchange</dfn> event
</h2>
<p>
The <a>pushsubscriptionchange</a> event indicates that a <a>push subscription</a>
has been invalidated, or will soon be invalidated. For example, the <a>push service</a>
MAY set an expiration time. A <a>Service Worker</a> SHOULD attempt to resubscribe while
handling this event, in order to continue receiving <a>push messages</a>.
The <a>pushsubscriptionchange</a> event indicates a change in a <a>push subscription</a>
that was triggered outside of the application's control, for example because it has been
refreshed, revoked or lost.
</p>
<p>
When new <a>push subscription</a> information becomes available, the <a>user agent</a>
MUST run the following steps:
To <dfn>fire the pushsubscriptionchange event</dfn> given a
<a>service worker registration</a> of <var>registration</var>, <var>newSubscription</var>
and <var>oldSubscription</var>, the <a>user agent</a> must run the following steps:
</p>
<ol>
<li>Let <var>registration</var> be the <a>service worker registration</a> corresponding
to the push message.
<li>Create a <a>trusted event</a>, <var>event</var>, that uses the
<a>PushSubscriptionChangeEvent</a> interface, with the event type
<code>pushsubscriptionchange</code>, which does not bubble, is not cancelable, and has no
default action.
</li>
<li>If <var>registration</var> is not found, abort these steps.
<li>Set the <code>newSubscription</code> attribute of <var>event</var> to
<var>newSubscription</var>.
</li>
<li>Invoke the <a>Handle Functional Event</a> algorithm with a <a>service worker
registration</a> of <var>registration</var> and <var>callbackSteps</var> set to the
following steps:
<li>Set the <code>oldSubscription</code> attribute of <var>event</var> to
<var>oldSubscription</var>.
</li>
<li>Invoke the <a>Handle Functional Event</a> algorithm with <var>event</var>,
<var>registration</var> and <var>callbackSteps</var> set to the following steps:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list construction implies that all three inputs are set to the following steps. I don't know how to improve that with some more restructuring though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly rephrased.

<ol>
<li>Set <var>global</var> to the global object that was provided as an argument.
</li>
<li>Create a <a>trusted event</a>, <var>e</var>, that uses the
<a>ExtendableEvent</a> interface, with the event type
"<a>pushsubscriptionchange</a>", which does not bubble, is not cancelable, and
has no default action.
<li>Set <var>global</var> to the global object associated with the
<var>registration</var>.
</li>
<li>Dispatch <var>e</var> to <var>global</var>.
</li>
<li>If the previous <a>push subscription</a> is still active, perform the following
steps in parallel:
<ol>
<li>Set <var>oldSubscription</var> to the previous <a>push subscription</a>.
</li>
<li>Wait for all of the promises in the <a>extend lifetime promises</a> of <var>
e</var> to either resolve or reject.
</li>
<li>Unsubscribe <var>oldSubscription</var>.
</li>
</ol>
<li>Dispatch <var>event</var> to <var>global</var>.
</li>
</ol>
</li>
</ol>
<p>
This algorithm ensures that the <a>Service Worker</a> is able to react to any
non-destructive change in a <a>push subscription</a>, such as an automatic refresh,
without causing any active <a>push subscription</a> to be terminated prematurely. A
<a>Service Worker</a> can request a new <a>push subscription</a> during this process and
ensure that no <a>push messages</a> are lost.
<p class="note">
Consider using a more reliable synchronization mechanism such as [[WEB-BACKGROUND-SYNC]]
when sending the details of the new <a>push subscription</a> to your
<a>application server</a>. The user might be subject to unreliable network conditions that
could cause a fetch to fail.
</p>

<section data-dfn-for="PushSubscriptionChangeEvent">
<h2>
<dfn>PushSubscriptionChangeEvent</dfn> interface
</h2>
<pre class="idl">
dictionary PushSubscriptionChangeInit : ExtendableEventInit {
PushSubscription newSubscription = null;
PushSubscription oldSubscription = null;
};

[Constructor(DOMString type, optional PushSubscriptionChangeInit eventInitDict), Exposed=ServiceWorker]
interface PushSubscriptionChangeEvent : ExtendableEvent {
readonly attribute PushSubscription? newSubscription;
readonly attribute PushSubscription? oldSubscription;
};
</pre>
<p>
The <dfn>newSubscription</dfn> attribute contains the details of the
<a>push subscription</a> that is valid per invocation of the <a>pushsubscriptionchange</a>
event. The value will be <code>null</code> when no new <a>push subscription</a> could be
established, for example because the <a>webapp</a> has lost <a>express permission</a>.
</p>
<p>
The <dfn>oldSubscription</dfn> attribute contains the details of the
<a>push subscription</a> that SHOULD NOT be used anymore. The value will be
<code>null</code> when the <a>user agent</a> is not able to provide the full set of
details, for example because of partial database corruption.
</p>
<p class="issue">
Should we include an attribute that indicates whether the <code>oldSubscription</code>
has been decisively invalidated? How would developers use this information?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the answer here is no. The intent of having the old subscription respond to push messages is to avoid races. We should not encourage use of subscriptions once they are considered old.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Allowing the old subscription to briefly receive messages is helpful, but I don't think we want folks depending on it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Thanks - removed.

</p>
</section>
</section>
</section>
<section data-dfn-for="PushPermissionState">
Expand Down