0xK.co/s

Logo Logo Logo

Server-Sent Events

tasos@kadena.io

September 29, 2023

whois “Tasos Bitsios”

  • Developer @ Kadena Developer Experience team

Takadenoshi

@Takadenoshi

SSE?

Server-Sent Events (SSE)

Polling vs SSE

Like polling but better

Best suited for UTF-8 updates

Especially for multiple update channels

Tradeoff: more connections vs querying your data store

Like WebSockets but -

Use cases

Use cases: Notifications

Use cases: Real-time ticker data

Use cases: Async job progress

Use cases: Kadena

// TODO

Can I use?

Yes (96.11%)

caniuse.com/eventsource

What is this new thing?

Old enough to vote

🥳 SSE is 19 years old

🧙 By Ian Hickson, while at Opera

🔧 13 years of mainstream support

Timeline

  • 2004 · Server-sent DOM Events, Ian Hickson, Opera Software, WHATWG Web Applications 1.0
  • 2006 · [Production] Opera browser implementation
  • 2009 · W3C Working Draft, Ian Hickson, Google Inc
  • 2010 · [Production] Safari v5, Chrome v6
  • 2011 · [Production] Firefox v6
  • 2015 · W3C Recommendation

W3C Publication History · HTML Living Standard § 9.2

Largely overlooked: StackOverflow

SSE Tags

Polling Tags

Websocket Tags

Largely overlooked: why?

Contemporary to Web sockets, HTML5, <video>, Web workers, Web storage

Narrow use case

Show me the <>

Playground

Minimum Viable SSE response

The simplest server-sent event stream specifies just data events.

> GET /stream/hello HTTP/1.1

< HTTP/1.1 200 OK
< Content-Type: text/event-stream

< data: Hello\n\n

< data: ReactLive are you there?\n\n

Events separated by two newline characters \n\n

Data is encoded in UTF-8 (mandatory)

Playground: “simple” scenario

Simple EventSource consumer

Client-side: SSE consumer API is EventSource

let i=0;

const source = new EventSource("http://localhost:3001/stream/simple");

// "message" event emitted for each "data" event received
source.addEventListener("message", event => console.log(++i, event.data), false);

// or .onmessage = (...) if that is your jam
< HTTP/1.1 200 OK
< Content-Type: text/event-stream

< data: Hello\n\n

< data: ReactLive are you there?\n\n



1 Hello

2 ReactLive are you there?

Named Events

You can “namespace” your events using the event field with any custom name:

< event: goal
< data: "ARS-LIV 1-1 45"\n\n

< event: spectator-chat
< data: "Did you see that ludicrous display just now"\n\n

The goal and spectator-chat events are handled separately on the frontend

  • Allows multiplexing / routing events
    • no need for pattern matching on a shared data payload

Interactive part - scan me!

Scan the QR to interact with this presentation directly

Demo app:

  • Emote with 💖 👍 🎉 👏 😂 😲 🤔 👎
  • See SSE data
  • Link to SSE playground & presentation 📚

Bottom right corner:

  • Fountain of emoji reactions
  • Connection status 🔌
  • Number of streaming clients
  • QR again (you can scan later)

Named Events: Live Demo

In the live reactions demo, we stream two types of things:

< event: clients
< data: 10

< data: [1,2,3,4]

Reconnection (1)

EventSource will reconnect if the connection is interrupted.

Default reconnection timeout ~ 3-5 s.

Reconnection (2) - Custom timeouts

The reconnection timeout can be customized.

Emit a retry: field in any of the events:

< retry: 2500
< data: Hello!\n\n

Playground: “Retry-flaky” scenario

Reconnection (3) - Last-Event-ID

Events can include an id field with any UTF-8 string as value

Connection interrupted? Sets reconnection header Last-Event-ID: x

< id: data-0
< retry: 5000
< data: Data Zero event\n

💔 Disconnects ➡️ 5 seconds later

> GET /stream/notifications
> Last-Event-ID: data-0

Playground: “notifications” scenario

Comments

Any lines starting with : (colon)

< :TODO emit some events in the near future

These are ignored on the client-side

Full SSE response

Entire SSE gramar: 4+1 fields

  • Setting reconnection time retry: 2000
  • Event identifiers id: 0
  • Unnamed events data: Hello\n\n
  • Comment: starts with colon :I am a comment
  • Named events event: status
> GET /stream/hello HTTP/1.1

< HTTP/1.1 200 OK
< Content-Type: text/event-stream

< retry: 2000
< id: 0
< data: {"message":"Hello"}\n\n

< :I am a comment

< id: 1
< event: status
< data: {"warn":"Service degraded"}\n\n

EventSource: named events

You can subscribe to custom events with .addEventListener:

const source = new EventSource('/stream/hello');

// [name]: triggers for custom named event, here: "status"
source.addEventListener(
  "status",
  ({ data }) => console.log("custom event: status", JSON.parse(data)),
  false,
);

EventSource: connection events

Subscribe to open and error for connection management:

// on connection established
source.addEventListener(
  "open",
  (event) => { console.log("Connection opened"); },
  false
);

// on error or disconnection
source.addEventListener(
  "error",
  (event) => { console.log("Connection error"); },
  false
);

The EventSource Interface

In React

  useEffect(() => {
    const eventSource = new EventSource(` http://localhost:3001/stream/${endpoint}` );

    // connection mgmt
    eventSource.addEventListener('open', () => console.log("Got open"));
    eventSource.addEventListener('error', () => console.log("Got error"));

    // data callbacks
    eventSource.addEventListener('message', () => console.log("Got message"));
    eventSource.addEventListener('custom', () => console.log("Got custom"));

    // destroy when unloaded
    return () => eventSource.close();
  }, []);

To be clear

I hold no $SSE stock

SSE NFTs

will not be made available

You should definitely use it

idk u

You should definitely know about it

Warts and all

Considerations

I ate some dog food for you science

Error event is a bit useless

Implementation Considerations: HTTP/1.1

HTTP/1.1 connections quota

Max number of connections: 6

Per-hostname quota

Browser-wide enforcement (shared by all tabs)

Too much SSE without planning -> choke

Implementation Considerations: HTTP/1.1

Possible Solutions

Implementation Considerations: Reconnecting

Different Browser Implementations

When a network error is encountered:


Test it out in the playground repo:

  • Start react-playground but not the server
  • Open react-playground in Chrome and Firefox
  • Try to connect to any endpoint
  • Observe behavior of each browser

Implementation Considerations: Computer says no

A server can signal “do not reconnect”:

Playground: “Not SSE” scenario

Implementation Considerations: Reconnecting

Important payload?

Consider handling reconnections explicitly

Implementation Considerations: Proxies (1)

Proxies can kill

Some proxies dislike idle connections & will kill them quickly. Fix(es):


1/ Comment (not client-aware)

You can emit a comment

< :)

Good enough to keep connection alive.

EventSource won’t emit any event.

Implementation Considerations: Proxies (2)

Proxies can kill

Some proxies dislike idle connections & will kill them quickly. Fix(es):


2/ Heartbeat event (client-aware)

Emit a custom event every ~15 seconds:

< event: heartbeat
< data: ""

(data field must be present)

Use it to detect stale connections: “expect heartbeats every N seconds, otherwise reconnect”

Implementation Considerations: 🔥 🦊 💁 🤖

Firefox Service Workers 💔 EventSource

Firefox has yet to implement support for EventSource in its Service Worker context.

Future people can track the present validity of this statement here.

✅ But you can use it in a SharedWorker

whois “Kadena”

  • DX Team
    • Enable and empower our ecosystem developers

Kadena-io & Kadena-community

@Kadena_io · 🌐 https://kadena.io

Chainweb use case

Blockchain stuff usually comes with lots of polling. E.g. determining finality

Kadena’s Chainweb is 20 “braided” chains -> 20x polling threads (worst case)

Chainweb-stream:

  • SSE Server
  • Streams transactions of a certain type (specific account or application/contract)

Chainweb-stream-client:

  • Client side lib (node, browser)
  • Detects stale connections (heartbeat events)
  • Detects initial connection timeouts
  • Custom reconnection (exponential backoffs)

Thank you