Skip to content

Server-sent events and static files

import 'package:gisila/gisila.dart';
Response sseResponse(
Stream<SseEvent> events, {
Duration? keepAlive = const Duration(seconds: 15),
Map<String, String> headers = const {},
})

A text/event-stream response that sends events as they arrive.

When to use: one-way live updates to browsers (order status, notifications, progress) over plain HTTP. Use WebSockets (package:shelf_web_socket) when the client also needs to send messages on the same connection.

Parameter Default Description
events required The event stream. An error on it ends the response.
keepAlive 15 seconds Interval of : comment lines that keep proxies from closing idle connections. null sends none.
headers {} Extra response headers.

The response sets cache-control: no-cache and x-accel-buffering: no (so nginx doesn’t buffer) and turns off Shelf’s output buffering. When the client disconnects, the subscription to events is cancelled: release resources in the stream’s onCancel. Compression and ETags leave event streams alone, and the access log never reads them.

@Get('/orders/{id}/events')
@RequireAuth()
Future<Response> events(String id, RequestContext ctx, OrderService orders) async {
final since = lastEventId(ctx.request);
return sseResponse(
orders.updates(id, since: since)
.map((o) => SseEvent.json(o, event: 'order', id: '${o.version}')),
);
}

Route and app timeouts measure how long the handler takes to return the Response, not how long the stream stays open, so they don’t cut an event stream short. The request’s database session is closed once the handler returns, so open your own connection for queries made while streaming.

One server-sent event.

class SseEvent {
const SseEvent({String? data, String? event, String? id, Duration? retry, String? comment});
factory SseEvent.json(Object? value, {String? event, String? id});
final String? data;
final String? event;
final String? id;
final Duration? retry;
final String? comment;
String encode();
}
Field Description
data Text sent as data: lines; line breaks become separate lines that the browser joins back with \n.
event Event type, for eventSource.addEventListener(type, ...). Must be one line.
id Sent back by a reconnecting browser as Last-Event-ID. Must be one line without NUL.
retry How long the browser waits before reconnecting.
comment A : comment line, ignored by browsers.
SseEvent.json(value) An event whose data is jsonEncode(value).
encode() The wire format. Throws ArgumentError for a multi-line event or id.
String? lastEventId(Request request)

The Last-Event-ID header a reconnecting browser sent, so the stream can resume after the last event the client saw.

class StaticFilesConfig {
const StaticFilesConfig({
String? indexFile = 'index.html',
bool spaFallback = false,
Duration maxAge = Duration.zero,
RegExp? immutable,
bool allowDotfiles = false,
});
}

Settings for GisilaApp.serveStatic and staticFilesHandler.

When to use: to serve a built frontend or public uploads from the same process as the API. Behind nginx or a CDN, let them serve files when you can.

Field Default Description
indexFile 'index.html' Served for a directory path. null serves none.
spaFallback false Answer unknown GET paths that have no file extension and accept text/html with the root indexFile, so a client-side router can handle them. API calls and missing assets still get a 404.
maxAge Duration.zero Cache-Control: public, max-age=... for non-HTML files. HTML always gets no-cache.
immutable null Paths (relative to the directory) matching it get max-age=31536000, immutable. Use it for content-hashed names: RegExp(r'^assets/.*\.[0-9a-f]{8,}\.').
allowDotfiles false Serve names starting with a dot (.env, .git). Leave it off.

Files carry a weak ETag and Last-Modified and answer 304 to If-None-Match / If-Modified-Since. Only GET and HEAD are served.

app.serveStatic('/', 'web/dist',
config: StaticFilesConfig(
spaFallback: true,
maxAge: const Duration(hours: 1),
immutable: RegExp(r'^assets/.*\.[0-9a-f]{8,}\.'),
));
app.serveStatic('/uploads', 'storage/public');
Handler staticFilesHandler(String directory,
[StaticFilesConfig config = const StaticFilesConfig()])

A Shelf handler serving files under directory. Paths it has no file for return Router.routeNotFound, so a Router moves on to its next route. Never serves anything outside directory: .., encoded slashes and NUL are refused, and symlinks that lead out are treated as missing. No directory listings. Throws ArgumentError when directory does not exist.

When to use: to mount static files on your own Router. With GisilaApp, serveStatic is simpler.