EventSource
Server-sent events: a one-way stream of text pushed from the server over ordinary HTTP, with reconnection and resume-from-last-id built into the browser rather than into your code.
@inject Bit.Butil.EventSource eventSourceMDN reference
/sse/ticks on the demo server, which emits one event a
second - every third one named heartbeat rather than unnamed, so both listener
kinds are exercised. Each event carries an id, which is what lets a reconnect resume the
sequence instead of restarting it.
Returns true when the runtime exposes EventSource. Supported by every current engine. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.
@inject Bit.Butil.EventSource eventSource
var supported = await eventSource.IsSupported();Named events never reach the default message listener, so anything the server names has to be declared in eventNames to be seen at all. The onError callback's argument distinguishes 'the browser is about to retry' (false) from 'this stream is finished' (true) - the browser reconnects on its own by default, so a transient failure is not something you have to handle.
private EventSourceHandle? _stream;
_stream = await eventSource.Open(
"/sse/ticks",
onMessage: e => InvokeAsync(() =>
{
// e.EventName is "message" for unnamed events, the server's name otherwise
// e.Data is always text - deserialize it yourself
// e.LastEventId is what the browser replays on reconnect
_events.Insert(0, e);
StateHasChanged();
}),
eventNames: ["heartbeat"],
onOpen: () => InvokeAsync(StateHasChanged),
onError: fatal => InvokeAsync(StateHasChanged));
// closing is what stops the browser reconnecting:
await _stream!.DisposeAsync();Drop the server (or your network) while the stream is open and the browser reconnects by itself, sending the last id it saw as a Last-Event-ID header. The demo endpoint reads that header and carries on counting rather than starting from one - which is the whole point of ids. GetState reports Connecting during the gap, which is a normal mid-stream state rather than a failure.
var state = await _stream!.GetState();
// EventSourceState.Connecting - dropped, browser is retrying
// EventSourceState.Open - delivering
// EventSourceState.Closed - finished for good, no retryEventSource that is never closed keeps reconnecting on the browser's own
schedule for as long as the page lives. Dispose the handle - as a safety net, the
EventSource service closes any leaked stream when its scope is torn down.
API reference
ValueTask<bool> IsSupported()ValueTask<EventSourceHandle?> Open(string url, Action<ServerSentEvent> onMessage, string[]? eventNames = null, Action? onOpen = null, Action<bool>? onError = null, bool withCredentials = false)ValueTask DisposeAsync()ValueTask<EventSourceState> GetState()ValueTask DisposeAsync()record (string EventName, string Data, string LastEventId)