eventStream function

Response eventStream(
  1. Stream<ServerSentEvent> events, {
  2. Duration? keepAlive = const Duration(seconds: 15),
  3. int status = 200,
})

Streams events to the client as text/event-stream.

Return it from a handler like any other value:

Future<Object?> ticks(Request request) async => eventStream(
      Stream.periodic(
        const Duration(seconds: 1),
        (count) => ServerSentEvent.json({'tick': count}),
      ),
    );

The connection stays open until the stream closes or the client goes away. Compared with a WebSocket this is one-way and rides on plain HTTP, which is why it survives proxies that mangle upgrades — and why it is usually the cheaper choice for a live feed nobody talks back on.

The headers matter as much as the body. Cache-Control: no-cache stops a proxy serving one client's stream to another, and X-Accel-Buffering: no tells nginx not to buffer — without it a stream can sit invisible until the buffer fills, which looks exactly like a server that has hung.

keepAlive sends a comment on an idle stream so an intermediary does not decide the connection is dead. Set it to null to send nothing.

Implementation

Response eventStream(
  Stream<ServerSentEvent> events, {
  Duration? keepAlive = const Duration(seconds: 15),
  int status = 200,
}) {
  final source = keepAlive == null ? events : _withKeepAlive(events, keepAlive);
  final body = _reported(source);

  return Response(
    status,
    body: body.map((event) => utf8.encode(event.encode())),
    headers: const {
      'content-type': 'text/event-stream; charset=utf-8',
      'cache-control': 'no-cache',
      'x-accel-buffering': 'no',
    },
    // Without this the adapter buffers the body and flushes it when the stream
    // ends, which for a stream that never ends means never. Every event would
    // arrive at once, and a progress bar would show nothing until the work it
    // reports on had finished — the exact failure `x-accel-buffering` above
    // prevents at the proxy, happening in this process instead.
    context: const {'shelf.io.buffer_output': false},
  );
}