One of the biggest missed opportunities thus far with HTTP/2 ("H/2") is that we are not yet able to sunset WebSockets in favor of H/2. Web Sockets and H/2 both support multiplexing messages bi-directionally and can send both textual and binary data.
Server Sent Events ("SSE"), by contrast, are not bi-directional (they're a "server-push-only" channel) and binary data cannot be sent easily. They are, however, very simple to implement. Adding to the menagerie of options, RTCPeerConnection
can also be used to signal data to applications in a low-latency (but potentially lossy) way.
Because H/2 does not support the handshake (upgrade) that WebSockets use to negotiate a connection (abandoned draft approach to repairing here), web developers are faced with a complex set of choices regarding how to handle low-latency data updates for their apps.
Web apps may benefit from a WebSocket-like API for receiving data updates without a custom protocol (e.g., Web Sockets or SSE) and associated infrastructure. An API for H/2 push receipts seems like a natural solution to this quandry, but H/2 has created a non-origin sharing concept for pushed responses. The upshot is that H/2's behavior needs to be futher constrained to only consider single origins (or URLs) so that an application is not incidentially confused by pushes into a shared H/2 cache by an app for which the server is also authoritative but which the application does not wish to trust.
The Server Sent Events API, perhaps, provides a way forward. What if, instead of only connecting to SSE-protocol supporting endpoints, a call to new EventSource(...)
could also subscribe to pushed updates to individual URLs (or groups of URLs) and allow notifiation to the application whenever an H/2 PUSH_PROMISE
resolves?
A minor extension to SSE might allow use of H/2 push as a substrate for delivering messages, with a slight modification of the event payload to accomodate providing Response
objects as values:
// If an H/2 channel is open, any PUSH_PROMISE for the resource at
// `/endpoint` will trigger a "resourcepush" event
let es = new EventSource("/endpoint", {
withCredentials: true
});
es.addEventListener("resourcepush", (e) => {
console.log(e.data instanceof Response);
});
Opting in to globbing for all push promises below a particular scope might look like:
// Any resource pushed to "/endpoint" or anything that matches that
// as a prefix will deliver to this handler
let es = new EventSource("/endpoint", {
withCredentials: true,
matchScope: true
});
es.addEventListener("resourcepush", (e) => {
console.log(e.data.url);
});
// If multiple handlers are installed, it's longest-prefix-wins, ala
// Service Workers:
let es = new EventSource("/endpoint/channel", {
withCredentials: true,
matchScope: true
});
// If a push is sent to `/endpoint/blargity`, will be handled by previous source
// If a push is sent to `/endpoint/channel`, `/endpoint/channel1`, or
// `/endpoint/channel2`, it will be delivered below:
es.addEventListener("resourcepush", (e) => {
console.log("channel-pushed resource:", e.data.url);
});
"resourcepush"
is a terrible name.- should updates to previously pushed resources be opt-in instead of transparent?
- do we need a version of this API that allows delivery to Service Workers?
- how much of a problem is the H/2 vs. Origin Model split in practice? Do we actually know?
- via Ryan Sleevi, to what extent should we allow subscriptions to any origin?
A notable downside of the proposed design is that it might "miss" same-origin payloads sent earlier in the page's lifecycle over the channel. One could imagine a version of the proposal that adds a flag to enable subscription-time delivery of these resources so long as they're still in the push cache. Putting this feature as another optional flag feels clunky, though.
One way to make it less clunky would be to describe the API in terms of an Observer pattern (e.g. Intersection Observer). The above example might then become:
let options = {
path: "/endpoint",
withCredentials: true,
matchScope: true
};
let callback = (records, observer) => {
records.forEach((r) => {
console.log(r instanceof Response); // === true
};
};
let resoureObserver = new ResourceObserver(callback, options);
This feels somewhat better as one of the key distinguishers of Observer-pattern APIs is their ability to scope interest in a specific subset of all events internal to the system. We also dont need to cancel delivery, so phraising it as an Event might not be great. It is, however, much more new API surface area.
Another considered alternative might instead put registration for listening inside a Service Worker's installation or activation phase; e.g.:
self.addEventListener("install", (e) => {
e.registerResourceListener("/endpoint", {
withCredentials: true,
matchScope: true
});
});
self.addEventListener("resourcePush", (e) => {
console.log("channel-pushed resource:", e.data.url);
});
This is interesting because it would allow us to avoid delivering these events to all windows and tabs which are open, and doesn't upset our design goal for Service Workers to be shut down when not in use. The ergonomics suffer somewhat: what about pages that don't have a SW (e.g., first-load)? Does handling resource pushes then gate on SW install time (which can take a long while)? Also, Response
objects can't be sent postMessage
to clients as they aren't structured-clonable, meaning the SW would need to signal to a page that an update is available and then, perhaps, transfer the data by extracting it from the Response
body to send via postMessage
or transfer using the Cache API.
@martinthomson I agree generally that
Another addition to SSE would further entrench the use of what is a pretty bad mechanism.
[link]. At the same time, the SSE proposal on the table is IMO a significantly stronger, more useful recommendation than anything else I've seen. It's ability to act broadly, cross-resourcefully, at scopes, is incredibly powerful & matches the mindspace I have as an application developer.Like, say, bayeux, the SSE extension features the ability to subscribe to scopes. We see similar patterns in Etcd's wait-for-change API with it's
?recursive=true
option- PUSH is different than this long-poll API form, but this resourceful view of pushed, subscribed content is a powerful one, and one SSE is far better mated to than the one-by-one Fetch proposal on the table.The Fetch proposal on the table is by far the cleanest, most direct API I can see for implementing martin's WebPush Protocol. In this use case, a resource (or group of resources) is explicitly requested (Fetch'ed) with the expectation that the request will be held open indefinitely and PUSHes will arrive in reply to it. In this case, the Fetch proposal works great.
Fetch is infinitely more appropriate a place to house this sort of addition
makes semantic good-sense for cracking this use case & is very elegant.But I find the ramifications deeply deeply unsettling and very limiting, such as for basic cases like trying to detect & render linked-data-island resources pushed in response a page (like index.html). If we want to start extending the DOM such that we can see replies pushed in response to documents and images and everything else, we can escape the pit-trap of writing something specific to a very limited piece of the platform (Fetch), but there's got to be more than specific-Fetch grade awareness here.
A long, long time ago, in a issue tracker far far away, there was Define a Fetch registry, which was merged and resolved. Today, in a different place, we still have a CustomElementRegistry. To look at this problem differently (not tied to Fetch or SSE) the idea of having some registry where content is registered (very close to the original Fetch registry purpose), but also extending EventTarget and dispatching ResourcePushEvents, feels to me like a more readily adaptable plan, using the best event-sourcing capabilities of the platform. With streamId's, it can readily implement WebPush protocol, while also allowing for a more general implementation of the "content was pushed I want to detect it and render it". Leaving this last need unfulfilled invites a tactic I have already desultorily fallen back to, polling the server for a list of resources that have been pushed at me (along with the cost of collecting and storing those list of pushed resources in the serving-farm). This is... radically unideal, and a limitation readily circumventable with painful imo unnecessary state tracking & coordination on the server's part. I don't want to see the web-platform create that kind of burden- it's not sympathetic to a use case people will make happen one way or another.