Skip to content

Implement complete SSE plugin with streaming, lifecycle, and session management#1

Merged
mcollina merged 8 commits intomainfrom
feat/complete-sse-plugin-implementation
Aug 25, 2025
Merged

Implement complete SSE plugin with streaming, lifecycle, and session management#1
mcollina merged 8 commits intomainfrom
feat/complete-sse-plugin-implementation

Conversation

@mcollina
Copy link
Copy Markdown
Member

@mcollina mcollina commented Aug 16, 2025

Summary

This PR implements a comprehensive Server-Sent Events (SSE) plugin for Fastify, directly implementing the API design agreed upon in fastify/fastify#6276. The implementation provides first-class SSE support with clean reply.sse.send() and reply.sse.stream() methods as specified in the community discussion.

Key Features Implemented

Unified API Interface: Clean reply.sse.* methods that integrate seamlessly with Fastify's request/reply cycle
Multiple Data Sources: Support for single messages, async generators, Node.js streams, and plain strings
Session Management: Built-in connection state tracking with isConnected and keepAlive() functionality
Message Replay: Automatic handling of client reconnections with Last-Event-ID header support
Heartbeat System: Configurable keep-alive mechanism to maintain connections
Route-level Configuration: Routes can be marked with { sse: true } for automatic SSE handling
Graceful Fallback: Non-SSE requests to SSE routes receive regular JSON responses

API Examples

Basic Message Sending with reply.sse.send()

// Send simple message
await reply.sse.send('Hello World');

// Send structured data with event type
await reply.sse.send({ user: 'john', message: 'Hello' }, { 
  event: 'chat-message',
  id: '123'
});

// Send with retry configuration
await reply.sse.send(data, { 
  event: 'notification',
  retry: 5000 
});

Streaming with reply.sse.stream()

// Stream from async generator
async function* dataGenerator() {
  for (let i = 0; i < 10; i++) {
    yield { count: i, timestamp: Date.now() };
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
}

await reply.sse.stream(dataGenerator());

// Stream from Node.js readable stream
const fs = require('fs');
const stream = fs.createReadStream('data.json');
await reply.sse.stream(stream);

Route Configuration

fastify.register(ssePlugin);

fastify.get('/events', { sse: true }, async (request, reply) => {
  // SSE context automatically available
  await reply.sse.send({ message: 'Connected!' });
  
  // Stream live data
  await reply.sse.stream(getLiveDataStream());
});

Testing Coverage

Comprehensive test suite covering:

  • Core SSE functionality: Message formatting, headers, connection setup
  • API methods: Both reply.sse.send() and reply.sse.stream() with various data types
  • Connection lifecycle: Connect, disconnect, reconnection with Last-Event-ID
  • Session management: Heartbeat, keep-alive, connection state tracking
  • Error handling: Invalid data, connection errors, stream failures
  • Edge cases: Non-SSE requests, malformed headers, concurrent connections

TypeScript Support

Complete TypeScript definitions provided:

  • Plugin registration: Proper typing for plugin options and configuration
  • Reply decoration: Full IntelliSense support for reply.sse.* methods
  • Route configuration: Type-safe SSE route options with { sse: true }
  • Method signatures: Detailed parameter and return types for all SSE methods
  • Generic support: Parameterized types for data payloads
interface SSEOptions {
  event?: string;
  id?: string;
  retry?: number;
}

interface FastifyReply {
  sse: {
    send<T = any>(data: T, options?: SSEOptions): Promise<void>;
    stream(source: AsyncIterable<any> | NodeJS.ReadableStream): Promise<void>;
    isConnected: boolean;
    keepAlive(): void;
  };
}

Implementation Architecture

The plugin follows the API design from fastify/fastify#6276 by:

  1. Route-level Detection: Automatically detects SSE requests via Accept: text/event-stream header
  2. SSE Context Creation: Sets up connection management with proper headers and state tracking
  3. Reply Decoration: Adds reply.sse.send() and reply.sse.stream() methods to the reply object
  4. Lifecycle Management: Handles connection cleanup, heartbeat timers, and graceful shutdowns
  5. Fallback Handling: Provides regular JSON responses for non-SSE clients

This implementation is production-ready and follows Fastify plugin conventions while delivering the exact API interface agreed upon in the GitHub issue discussion.

…management

- Add comprehensive SSE message formatting with multiline support
- Implement streaming support for async generators and Node.js streams
- Add connection lifecycle management with keepAlive and close handlers
- Support message replay functionality with Last-Event-ID header
- Include configurable heartbeat mechanism for connection health
- Add route-level SSE configuration with fallback to regular handlers
- Provide TypeScript definitions for full type safety
- Include comprehensive test coverage for all features
- Add working examples demonstrating plugin capabilities
- Align reply.sse.send() and reply.sse.stream() method signatures with agreed design
- Update API documentation and examples to reflect final implementation
- Ensure compatibility with Fastify's request/reply lifecycle patterns
- Standardize method interfaces based on community discussion outcomes
- Remove trailing whitespace and add missing newlines for linting compliance
- Update Windows test command to explicitly list test files instead of using glob patterns
- Ensure all files meet Standard.js formatting requirements
The README was incorrectly showing reply.sse() which would cause runtime errors.
Updated all examples to use the correct reply.sse.send() API that matches the
actual implementation. This fixes critical documentation errors that would
prevent users from successfully using the plugin.
@mcollina mcollina marked this pull request as ready for review August 22, 2025 21:20
@mcollina
Copy link
Copy Markdown
Member Author

cc @climba03003 @Uzlopak

Copy link
Copy Markdown
Member

@climba03003 climba03003 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

We should change to neostandard + eslint before release.

- Replace standard dependency with eslint and neostandard
- Update lint scripts to use eslint instead of standard
- Add eslint.config.js with neostandard configuration
- Maintains same code style rules through neostandard preset
Copy link
Copy Markdown
Contributor

@Uzlopak Uzlopak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need a day to review

Comment thread index.js Outdated
@Uzlopak
Copy link
Copy Markdown
Contributor

Uzlopak commented Aug 25, 2025

I will try to use this plugin for smee.io to check if I see issues in real use

Co-authored-by: Aras Abbasi <aras.abbasi@googlemail.com>
Signed-off-by: Matteo Collina <matteo.collina@gmail.com>
@mcollina
Copy link
Copy Markdown
Member Author

@Uzlopak if you lgtm it I'll ship a 0.0.1.

Copy link
Copy Markdown
Contributor

@Uzlopak Uzlopak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Uzlopak
Copy link
Copy Markdown
Contributor

Uzlopak commented Aug 25, 2025

Lets go :)

@mcollina mcollina merged commit efb0768 into main Aug 25, 2025
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants