Skip to main content
Version: v5

Server-Sent Events

Added in: v4.2.0

Harper supports Server-Sent Events (SSE), a simple and efficient mechanism for browser-based applications to receive real-time updates from the server over a standard HTTP connection. SSE is a one-directional transport — the server pushes events to the client, and the client has no way to send messages back on the same connection.

Connecting

SSE connections are made by targeting a resource URL. By default, connecting to a resource path subscribes to changes for that resource and streams events as they occur.

let eventSource = new EventSource('https://server/my-resource/341', {
withCredentials: true,
});

eventSource.onmessage = (event) => {
let data = JSON.parse(event.data);
};

The URL path maps to the resource in the same way as REST and WebSocket connections. Connecting to /my-resource/341 subscribes to updates for the record with id 341 in the my-resource table (or custom resource).

connect() Handler

SSE connections use the same connect() method as WebSockets on resource classes, with one key difference: since SSE is one-directional, connect() is called without an incomingMessages argument.

export class MyResource extends Resource {
async *connect() {
// yield messages to send to the client
while (true) {
await someCondition();
yield { event: 'update', data: { value: 42 } };
}
}
}

The default connect() behavior subscribes to the resource and streams changes automatically.

Reading the request in connect()

To read the incoming request (query parameters, headers, the path), define connect() as a static method. Its first argument is the RequestTarget, which exposes the query string via target.get(). Because target is an ordinary argument, it is captured in the generator's scope — read it directly inside the async *connect() body:

export class Search extends Resource {
static async *connect(target) {
const query = target.get('q'); // GET /Search?q=harper
const apiKey = target.get('api_key');
for (const hit of await runSearch(query)) yield { data: hit };
}
}

This works even though the generator body doesn't begin executing until the first event is requested — the target argument is already bound when connect() is called.

Note: the RequestTarget argument is only passed to a static connect(). An instance method's first argument is not the target, so define connect() as static.

Doing synchronous work before streaming

If you must read the request and act on it synchronously — before the stream starts — split connect() into a reader that returns a separate generator. This is also required when reading the request context via getContext(), which must be captured in the synchronous call frame: it is no longer available once the generator body is deferred to the first event.

export class Search extends Resource {
static connect(target) {
const query = target.get('q');
if (!query) throw new Error('q is required'); // reject before streaming starts
return this.stream(query);
}
static async *stream(query) {
for (const hit of await runSearch(query)) yield { data: hit };
}
}

When to Use SSE vs WebSockets

SSEWebSockets
DirectionServer → Client onlyBidirectional
TransportStandard HTTPHTTP upgrade
Browser supportNative EventSource APINative WebSocket API
Use caseLive feeds, dashboards, notificationsInteractive real-time apps, MQTT

SSE is simpler to implement and has built-in reconnection in browsers. For scenarios requiring bidirectional communication, use WebSockets.

See Also