EventFlux

A Server-Sent Events client for Dart and Flutter, with WHATWG-compliant parsing, automatic reconnection, and native and web support.

Building with an AI coding assistant? EventFlux bundles a package skill that teaches your agent how to integrate streams, manage connection lifecycles, and migrate from v2.

Installation Β· Examples Β· API reference Β· Migrating from v2

Features 🌟

  • πŸ“œ WHATWG SSE spec-compliant event stream parsing with persistent lastEventId and retry: field support
  • πŸ”„ Auto-reconnect with linear or exponential backoff, random jitter, and configurable maxBackoff cap
  • πŸ”— Interceptor chain β€” hook into request, response, and error lifecycle stages
  • πŸ›‘ Mid-flight abort via a Future<void> trigger
  • ⏱️ Idle timeout detection β€” drops the connection if no data arrives within a configured duration
  • 🌐 Web platform support with CORS, credentials, and caching configuration via WebConfig
  • πŸ” Event filtering by type using response.where()
  • πŸ“Ž Multipart request support
  • πŸ—οΈ Singleton (EventFlux.instance) and multiple independent connections (EventFlux.spawn())
  • πŸ”Œ Pluggable HTTP clients via HttpClientAdapter
  • 🧠 HTTP error classification β€” retries 5xx, 408, and 429 responses; other HTTP errors are not retried

Installation

Requires Dart SDK >=3.4.0 and Flutter >=3.0.0.

Agents

Install with your AI assistant

1. Add EventFlux to your app

flutter pub add 'eventflux:^3.0.2'

2. Install the package skill

From your app's root directory, run:

dart run skills@ get -p eventflux

Choose your coding assistant when prompted. For Codex, use dart run skills@ get --agent codex -p eventflux.

The bundled eventflux-usage skill covers connection ownership, stream subscriptions, reconnects, authentication headers, browser setup, and v2 migration. The skills CLI requires Dart 3.10 or later and installs agent guidance without adding a runtime dependency. See the Dart package skills guide for setup details.

3. Ask your assistant to build the integration

Replace <SSE_URL> with your endpoint, then use a prompt like:

Use the eventflux-usage skill to connect this app to <SSE_URL>.
Follow the app's existing state management. Handle incoming events,
errors, and reconnects, and clean up the connection and subscription
when their owner is disposed. Include WebConfig if the app targets web.

For an existing v2 integration, ask: β€œUse eventflux-usage to migrate this app to EventFlux v3 and check its connection lifecycle.”

Manual

To set up manually, add EventFlux to your pubspec.yaml, then run flutter pub get:

dependencies:
  eventflux: ^3.0.2

Usage

On web, every connect() call requires webConfig: WebConfig() (or a custom configuration). See the Web Platform example below.

Basic Connection β€” Connect to an SSE endpoint in a few lines
import 'package:eventflux/eventflux.dart';

void main() {
  EventFlux.instance.connect(
    EventFluxConnectionType.get,
    'https://example.com/events',
    onSuccessCallback: (EventFluxResponse? response) {
      response?.stream?.listen((EventFluxData data) {
        print('Event: ${data.event}');
        print('Data: ${data.data}');
      });
    },
    onError: (EventFluxException error) {
      print('Error: $error');
    },
    onConnectionClose: () {
      print('Connection closed');
    },
  );
}
Auto-Reconnect β€” Exponential backoff with jitter and token refresh

Replace refreshAccessToken() with your app's token-refresh function. Auto-reconnect handles connection errors, stream closure, and idle timeouts; among HTTP error responses, only 5xx, 408, and 429 are retried.

import 'package:eventflux/eventflux.dart';

void main() {
  EventFlux.instance.connect(
    EventFluxConnectionType.get,
    'https://example.com/events',
    onSuccessCallback: (EventFluxResponse? response) {
      response?.stream?.listen((data) {
        print('Data: ${data.data}');
      });
    },
    onError: (error) {
      print('Error: $error');
    },
    autoReconnect: true,
    reconnectConfig: ReconnectConfig(
      mode: ReconnectMode.exponential,
      interval: Duration(seconds: 2),
      maxAttempts: 10,
      maxBackoff: Duration(seconds: 30),
      connectionTimeout: Duration(seconds: 60),
      onReconnect: (int attempt, Duration delay) {
        print('Reconnect attempt $attempt after $delay');
      },
      reconnectHeader: () async {
        String newToken = await refreshAccessToken();
        return {
          'Authorization': 'Bearer $newToken',
          'Accept': 'text/event-stream',
        };
      },
    ),
  );
}
Interceptors β€” Inject auth headers, log responses, suppress errors
import 'package:eventflux/eventflux.dart';
import 'package:http/http.dart';

class AuthInterceptor extends EventFluxInterceptor {
  @override
  Future<BaseRequest> onRequest(BaseRequest request) async {
    request.headers['Authorization'] = 'Bearer my-token';
    return request;
  }

  @override
  Future<StreamedResponse> onResponse(StreamedResponse response) async {
    print('Response status: ${response.statusCode}');
    return response;
  }

  @override
  Future<EventFluxException?> onError(EventFluxException exception) async {
    print('Intercepted error: $exception');
    // Return null to suppress the error, or return exception to propagate it
    return exception;
  }
}

void main() {
  EventFlux.instance.connect(
    EventFluxConnectionType.get,
    'https://example.com/events',
    interceptors: [AuthInterceptor()],
    onSuccessCallback: (EventFluxResponse? response) {
      response?.stream?.listen((data) {
        print('Data: ${data.data}');
      });
    },
    onError: (error) {
      print('Error: $error');
    },
  );
}
Abort a Connection β€” Cancel an in-flight request at any time
import 'dart:async';
import 'package:eventflux/eventflux.dart';

void main() {
  final completer = Completer<void>();

  EventFlux.instance.connect(
    EventFluxConnectionType.get,
    'https://example.com/events',
    abortTrigger: completer.future,
    onSuccessCallback: (EventFluxResponse? response) {
      response?.stream?.listen((data) {
        print('Data: ${data.data}');
      });
    },
    onError: (error) {
      print('Error: $error');
    },
  );

  // Cancel the connection at any time
  Future.delayed(Duration(seconds: 10), () {
    completer.complete();
  });
}
Web Platform β€” Configure CORS and credentials for browser SSE
import 'package:eventflux/eventflux.dart';
import 'package:flutter/foundation.dart' show kIsWeb;

void main() {
  EventFlux.instance.connect(
    EventFluxConnectionType.get,
    'https://example.com/events',
    webConfig: kIsWeb
        ? WebConfig(
            mode: WebConfigRequestMode.cors,
            credentials: WebConfigRequestCredentials.omit,
          )
        : null,
    onSuccessCallback: (EventFluxResponse? response) {
      response?.stream?.listen((data) {
        print('Data: ${data.data}');
      });
    },
    onError: (error) {
      print('Error: $error');
    },
  );
}
Multiple Connections β€” Run independent SSE streams in parallel
import 'package:eventflux/eventflux.dart';

void main() {
  EventFlux e1 = EventFlux.spawn();
  EventFlux e2 = EventFlux.spawn();

  e1.connect(
    EventFluxConnectionType.get,
    'https://example.com/stream-1',
    tag: 'Stream 1',
    onSuccessCallback: (EventFluxResponse? response) {
      response?.stream?.listen((data) {
        print('Stream 1: ${data.data}');
      });
    },
    onError: (error) {
      print('Stream 1 error: $error');
    },
  );

  e2.connect(
    EventFluxConnectionType.get,
    'https://example.com/stream-2',
    tag: 'Stream 2',
    onSuccessCallback: (EventFluxResponse? response) {
      response?.stream?.listen((data) {
        print('Stream 2: ${data.data}');
      });
    },
    onError: (error) {
      print('Stream 2 error: $error');
    },
  );

  // Disconnect both when done
  // await e1.disconnect();
  // await e2.disconnect();
}

Event Filtering β€” filter events by type using where():

response?.where('message').listen((data) {
  print('Message event: ${data.data}');
});

API reference

Connection options, reconnect configuration, and public types

Connect

Connects to a server-sent event stream.

Parameter Type Description Default
type EventFluxConnectionType HTTP method (get or post) β€”
url String SSE stream URL β€”
onSuccessCallback Function(EventFluxResponse?) Callback on successful connection (required) β€”
header Map<String, String> HTTP headers; Cache-Control is removed on web in favor of WebConfig.cache {'Accept': 'text/event-stream', 'Cache-Control': 'no-store'}
onConnectionClose Function()? Called when the connection closes β€”
autoReconnect bool Auto-reconnect on disconnection false
reconnectConfig ReconnectConfig? Reconnection settings (required if autoReconnect is true) β€”
onError Function(EventFluxException)? Error callback β€”
body Map<String, dynamic>? Request body for POST β€”
files List<MultipartFile>? Files for multipart requests β€”
multipartRequest bool Send as multipart false
tag String? Debug tag (appears in logs) β€”
logReceivedData bool Log received SSE data false
httpClient HttpClientAdapter? Custom HTTP client β€”
webConfig WebConfig? Web platform configuration (required on web) β€”
interceptors List<EventFluxInterceptor>? Request/response/error interceptors β€”
abortTrigger Future<void>? Future that aborts the connection when completed β€”

ReconnectConfig

Parameter Type Description Default
mode ReconnectMode linear or exponential (required) β€”
interval Duration Base retry interval Duration(seconds: 2)
maxAttempts int Max reconnect attempts (-1 for unlimited) 5
maxBackoff Duration Cap for exponential backoff Duration(seconds: 30)
connectionTimeout Duration? Idle timeout β€” drops connection if no data received β€”
onReconnect void Function(int attempt, Duration delay)? Called on each reconnect attempt β€”
reconnectHeader Future<Map<String, String>> Function()? Async header refresh for reconnect β€”

EventFluxInterceptor

Subclass EventFluxInterceptor and override any of the following methods:

Method Signature Description
onRequest Future<BaseRequest> onRequest(BaseRequest request) Modify the request before sending (e.g., inject auth headers). Throw EventFluxException to abort.
onResponse Future<StreamedResponse> onResponse(StreamedResponse response) Inspect the response after receiving. Must not consume the stream body.
onError Future<EventFluxException?> onError(EventFluxException exception) Handle or suppress errors. Return null to suppress the exception.

EventFluxStatus

Value Description
connectionInitiated Connection process has started
connected Successfully connected to the event stream
reconnecting Auto-reconnect is in progress
disconnected Connection has been closed
error An error occurred during connection or disconnection

Disconnect

EventFluxStatus status = await EventFlux.instance.disconnect();

Returns a Future<EventFluxStatus> indicating the disconnection status.

Spawn

EventFlux instance = EventFlux.spawn();

Returns a new independent EventFlux instance for managing parallel SSE connections.

Migrating from v2

Breaking changes and additions in v3

If you're upgrading from v2, here's what changed:

Breaking

  • onReconnect callback signature changed from () to (int attempt, Duration delay)
  • Default request headers now include Cache-Control: no-store on native platforms; browsers use WebConfig.cache instead
  • Minimum Dart SDK raised to >=3.4.0, Flutter >=3.0.0
  • webConfig is now required when running on web

New in v3

  • EventFluxStatus.reconnecting status value
  • originalError and stackTrace fields on EventFluxException
  • Request/response/error interceptors via interceptors parameter
  • Mid-flight abort support via abortTrigger parameter
  • Idle timeout detection via connectionTimeout on ReconnectConfig
  • maxBackoff on ReconnectConfig to cap exponential backoff
  • WHATWG-compliant SSE parser with persistent lastEventId, retry: field support, and U+2028 sanitization

Contributors πŸ’œ

EventFlux wouldn't exist without these people who believed it could be better.

peter-trost pedrohsampaioo krolmic FelippeNO jcarvalho-ptech aabegg jangruenwaldt glukose

Contributing 🀝

Contributions are welcome β€” open an issue or submit a pull request. Every bit helps.

License

Licensed under MIT.