mercure_dart 1.0.2 copy "mercure_dart: ^1.0.2" to clipboard
mercure_dart: ^1.0.2 copied to clipboard

Pure Dart implementation of the Mercure protocol. Zero dependencies. Multi-platform: mobile, desktop, server and web.

mercure_dart #

Pure Dart implementation of the Mercure protocol β€” zero dependencies, fully spec-compliant, multi-platform.

CI License: MIT Dart 3.0+

Mercure is a protocol for pushing data updates to web browsers and other HTTP clients using Server-Sent Events (SSE). This package implements the full Mercure client specification from scratch, with no external dependencies.

Table of Contents #

Features #

  • πŸ“‘ Subscribe to real-time updates via Server-Sent Events
  • πŸ“€ Publish updates to a Mercure hub
  • πŸ” Discovery of hub URLs from HTTP Link headers
  • πŸ“‹ Subscriptions API to query active subscriptions on the hub
  • 🌍 Multi-platform β€” mobile (iOS/Android), desktop, server (dart:io) and web (dart:html)
  • πŸ“¦ Zero runtime dependencies β€” only uses the Dart SDK
  • πŸ”„ Automatic reconnection with exponential backoff and Last-Event-ID
  • πŸ” Three auth methods β€” Bearer token, cookie, query parameter
  • βœ… Spec-compliant SSE parser handling all edge cases
  • πŸ›‘οΈ Hardened SSE pipeline with configurable size limits against memory exhaustion

Installation #

Add to your pubspec.yaml:

dependencies:
  mercure_dart: ^1.0.0

Then run:

dart pub get

Quick Start #

import 'package:mercure_dart/mercure_dart.dart';

void main() async {
  final hubUrl = Uri.parse('https://hub.example.com/.well-known/mercure');

  // Subscribe to updates
  final subscriber = MercureSubscriber(
    hubUrl: hubUrl,
    topics: ['https://example.com/books/{id}'],
    auth: Bearer('your-subscriber-jwt'),
  );

  subscriber.subscribe().listen((event) {
    print('${event.type}: ${event.data}');
  });

  // Publish an update
  final publisher = MercurePublisher(
    hubUrl: hubUrl,
    auth: Bearer('your-publisher-jwt'),
  );

  final id = await publisher.publish(PublishOptions(
    topics: ['https://example.com/books/1'],
    data: '{"title": "Updated Title"}',
  ));
  print('Published: $id'); // urn:uuid:...
}

Usage #

Subscribe #

final subscriber = MercureSubscriber(
  hubUrl: Uri.parse('https://hub.example.com/.well-known/mercure'),
  topics: [
    'https://example.com/books/{id}',   // URI template
    'https://example.com/users/dunglas', // Exact topic
  ],
  auth: Bearer(subscriberToken),
  lastEventId: 'earliest', // Get full history
);

final subscription = subscriber.subscribe().listen(
  (event) {
    print('ID: ${event.id}');
    print('Type: ${event.type}');
    print('Data: ${event.data}');
    print('Retry: ${event.retry}');
  },
  onError: (error) => print('Error: $error'),
);

// Later: stop listening
await subscription.cancel();
subscriber.close();

Publish #

final publisher = MercurePublisher(
  hubUrl: hubUrl,
  auth: Bearer(publisherToken),
);

// Simple update
final id = await publisher.publish(PublishOptions(
  topics: ['https://example.com/books/1'],
  data: '{"title": "The Great Gatsby"}',
));

// Private update (only authorized subscribers receive it)
await publisher.publish(PublishOptions(
  topics: ['https://example.com/users/42'],
  data: '{"email": "user@example.com"}',
  private: true,
));

// With event type and retry hint
await publisher.publish(PublishOptions(
  topics: ['https://example.com/books/1'],
  data: '{"stock": 0}',
  type: 'out-of-stock',
  retry: 5000,
));

publisher.close();

Discovery #

Discover the Mercure hub URL from a resource that advertises it via a Link header:

final result = await discoverMercureHub(
  'https://example.com/books/1',
  auth: Bearer(token),
);

print(result.hubUrls);  // [https://example.com/.well-known/mercure]
print(result.topicUrl);  // https://example.com/books/1

Subscriptions API #

Query active subscriptions on the hub (requires authorization):

final api = MercureSubscriptionsApi(
  hubUrl: hubUrl,
  auth: Bearer(adminToken),
);

// All subscriptions
final response = await api.getSubscriptions();
for (final sub in response.subscriptions) {
  print('${sub.topic} β€” active: ${sub.active}');
}

// Filtered by topic
final filtered = await api.getSubscriptionsForTopic(
  'https://example.com/books/{id}',
);

// Specific subscription
final sub = await api.getSubscription(
  topic: 'https://example.com/books/{id}',
  subscriber: 'urn:uuid:bb3de268-05b0-4c65-b44e-8f9acefc29d6',
);

api.close();

Authentication #

Three authentication strategies are available, matching the Mercure spec:

// Bearer token β€” sent via Authorization header
// Best option for server-side and mobile
const auth = Bearer('your-jwt-token');

// Cookie β€” sent via Cookie header (web: automatic with withCredentials)
const auth = Cookie('cookie-value');
const auth = Cookie('value', name: 'customCookieName'); // default: mercureAuthorization

// Query parameter β€” token appended to URL as ?authorization=<token>
// Fallback for web subscribers (EventSource doesn't support custom headers)
const auth = QueryParam('your-jwt-token');

Web subscribers: The browser's EventSource API does not support custom headers. When using Bearer auth for subscriptions on web, the transport automatically falls back to the authorization query parameter (as specified by the protocol). Cookie auth works natively with withCredentials: true. For publishing on web, Bearer works normally since HttpRequest supports custom headers.

Reconnection #

The dart:io transport handles reconnection automatically:

  • Exponential backoff β€” delay doubles on each failed attempt (base 3s, max 60s) with random jitter to avoid thundering herd
  • Last-Event-ID β€” sent on reconnection so the hub replays missed events
  • retry: hint β€” when the hub sends a retry: field, it updates the base reconnection delay
  • Reset on success β€” the backoff counter resets after a successful connection
  • Auth errors β€” 401/403 responses stop reconnection (no point retrying with bad credentials)
  • Stream size limits β€” configurable maxLineLength (1 MB) and maxEventSize (10 MB) protect against memory exhaustion from malicious streams

To request the full event history on first connection:

final subscriber = MercureSubscriber(
  hubUrl: hubUrl,
  topics: ['https://example.com/books/{id}'],
  lastEventId: 'earliest',
);

On web, the browser's native EventSource handles reconnection internally.

Supported Platforms #

Platform Transport SSE Parsing Reconnection Bearer Auth Cookie Auth
Mobile (iOS/Android) dart:io HttpClient Custom (SseLineDecoder + SseParser) Exponential backoff Authorization header Cookie header
Desktop (macOS/Linux/Windows) dart:io HttpClient Custom Exponential backoff Authorization header Cookie header
Server (Dart CLI) dart:io HttpClient Custom Exponential backoff Authorization header Cookie header
Web (Flutter web) dart:html EventSource Native browser Native EventSource Query param fallback withCredentials: true

Architecture #

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Public API: MercureSubscriber,              β”‚
β”‚  MercurePublisher, discoverMercureHub,       β”‚
β”‚  MercureSubscriptionsApi                     β”‚ ← FaΓ§ades
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  MercureTransport (abstract interface)       β”‚ ← Platform boundary
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  IO transport        β”‚  Web transport        β”‚ ← Platform-specific
β”‚  (HttpClient)        β”‚  (EventSource + XHR)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  SSE parser, models, auth                    β”‚ ← Pure Dart, shared
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

A single conditional import selects the correct transport at compile time. Everything outside the transport/ directory is pure Dart β€” no dart:io or dart:html imports.

Tests #

# Unit tests (pure Dart, no network)
dart test test/unit/

# Integration tests (requires Docker)
dart test test/integration/

# All tests
dart test

# Single test file
dart test test/unit/sse/sse_parser_test.dart

# Single test by name
dart test --name "parses multi-data"

Integration tests start a Mercure hub Docker container automatically. Make sure Docker is running.

Contributing #

See CONTRIBUTING.md for development setup, coding conventions, and the PR process.

Security #

See SECURITY.md for our vulnerability disclosure policy.

Protocol Reference #

License #

MIT β€” see LICENSE.

0
likes
160
points
13
downloads

Documentation

API reference

Publisher

verified publisherowlnext.fr

Weekly Downloads

Pure Dart implementation of the Mercure protocol. Zero dependencies. Multi-platform: mobile, desktop, server and web.

Repository (GitHub)
View/report issues
Contributing

License

MIT (license)

More

Packages that depend on mercure_dart