# Server-Sent Events (SSE) Implementation Plan ## Overview This plan outlines the implementation of Server-Sent Events (SSE) support for Astralis, following the existing architectural paradigm where objects outside the `Server/` folder have no knowledge of server internals. ## Architectural Analysis ### Existing Patterns The current architecture uses these key abstractions: ```mermaid classDiagram class HttpResult { +Dictionary headers +StatusCode status +HttpResultFlag flags +send_body_async AsyncOutput output } class AsyncOutput { <> +write_async BinaryData data +write_stream_async InputStream stream } class ServerOutput { -Series chunks +write_async BinaryData data +read_chunk void* buffer size_t max +on_new_chunk signal } class ResponseContext { +HttpResult result +ServerOutput body_output +begin_response +suspend_connection } HttpResult <|-- HttpDataResult HttpResult <|-- HttpStreamResult HttpResult <|-- HttpEmptyResult AsyncOutput <|.. ServerOutput ResponseContext --> ServerOutput ResponseContext --> HttpResult ``` ### Key Design Principle **Separation of Concerns:** - `Core/` - Public abstractions visible to endpoints - `Server/` - Internal implementation details marked `internal` - Endpoints interact only with `Core/` types ## Proposed Design ### Component Overview ```mermaid classDiagram class SseEvent { +string id +string event_type +string data +int64 retry +string format } class SseChannel { <> +send_event_async SseEvent event +send_async string data +CancellationToken cancellation_token } class SseHandler { <> +handle_sse_async SseChannel channel } class HttpSseResult { +int64 retry_interval +SseHandler handler +send_body_async AsyncOutput output } class ServerSseChannel { -AsyncOutput output -CancellationToken token +send_event_async SseEvent event +send_async string data } class CancellationToken { +bool is_cancelled +cancel +on_cancelled signal } HttpResult <|-- HttpSseResult SseChannel <|.. ServerSseChannel ServerSseChannel --> AsyncOutput ServerSseChannel --> CancellationToken HttpSseResult --> SseChannel HttpSseResult --> SseHandler ``` ### File Structure ``` src/ ├── Core/ │ ├── SseEvent.vala # Public: SSE event data structure │ ├── SseChannel.vala # Public: Interface for sending SSE events │ ├── SseHandler.vala # Public: Interface for SSE handlers - similar to Endpoint │ ├── CancellationToken.vala # Public: Token for manual cancellation │ └── HttpSseResult.vala # Public: HttpResult subclass for SSE └── Server/ └── ServerSseChannel.vala # Internal: ServerOutput-backed implementation ``` ## Detailed Component Specifications ### 1. SseEvent - Core/SseEvent.vala A simple data class representing an SSE event following the W3C specification. ```vala public class SseEvent : Object { public string? id { get; set; } public string? event_type { get; set; } public string data { get; set; } public int64? retry { get; set; } public SseEvent(string data); public SseEvent.with_id(string id, string data); public SseEvent.with_type(string event_type, string data); public string format(); // Returns properly formatted SSE string } ``` **Format output example:** ``` id: 123 event: message data: Hello World retry: 3000 ``` ### 2. CancellationToken - Core/CancellationToken.vala A cancellation token for manual connection management. ```vala public class CancellationToken : Object { public bool is_cancelled { get; private set; } public signal void cancelled(); public void cancel(); } ``` ### 3. SseChannel - Core/SseChannel.vala Interface for sending SSE events - endpoints use this to push data. ```vala public interface SseChannel : Object { public abstract async void send_event(SseEvent event) throws Error; public abstract async void send_data(string data) throws Error; public abstract CancellationToken cancellation_token { get; } } ``` ### 4. SseHandler - Core/SseHandler.vala Interface for SSE handlers - follows the same pattern as Endpoint. ```vala public interface SseHandler : Object { public abstract async void handle_sse(SseChannel channel) throws Error; } ``` ### 5. HttpSseResult - Core/HttpSseResult.vala HttpResult subclass that enables SSE streaming. ```vala public class HttpSseResult : HttpResult { public int64 retry_interval { get; set; } public SseHandler handler { get; set; } public HttpSseResult(); public HttpSseResult.with_retry(int64 retry_ms); public HttpSseResult.with_handler(SseHandler handler); public override async void send_body(AsyncOutput output) throws Error; } ``` ### 6. ServerSseChannel - Server/ServerSseChannel.vala Internal implementation that wraps an AsyncOutput. ```vala internal class ServerSseChannel : Object, SseChannel { private AsyncOutput output; private CancellationToken cancellation_token; public ServerSseChannel(AsyncOutput output, CancellationToken token); public async void send_event(SseEvent event) throws Error; public async void send_data(string data) throws Error; } ``` ## Usage Example ```vala // Define an SSE handler class class StockTickerHandler : Object, SseHandler { public async void handle_sse(SseChannel channel) throws Error { while (!channel.cancellation_token.is_cancelled) { var price = yield get_latest_stock_price(); var evt = new SseEvent.with_type("price-update", price.to_string()); yield channel.send_event(evt); yield AsyncUtil.delay_async(1000); // Wait 1 second } } } // Use in endpoint class StockTickerEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext ctx, RouteContext route) throws Error { return new HttpSseResult.with_handler(new StockTickerHandler()); } } ``` ## Alternative: Anonymous Class Pattern For simpler cases, Vala allows inline class implementation: ```vala class SimpleSseEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext ctx, RouteContext route) throws Error { var handler = new Object() with SseHandler { public async void handle_sse(SseChannel channel) throws Error { while (!channel.cancellation_token.is_cancelled) { yield channel.send_data("ping"); Timeout.add(1000, () => { handle_sse.callback(); return false; }); yield; } } }; return new HttpSseResult.with_handler(handler); } } ``` ## Implementation Steps ### Phase 1: Core Abstractions 1. Create `SseEvent` class in `Core/SseEvent.vala` 2. Create `CancellationToken` class in `Core/CancellationToken.vala` 3. Create `SseChannel` interface in `Core/SseChannel.vala` 4. Create `SseHandler` interface in `Core/SseHandler.vala` ### Phase 2: HttpResult Integration 5. Create `HttpSseResult` class in `Core/HttpSseResult.vala` 6. Update `meson.build` to include new files ### Phase 3: Server Implementation 7. Create `ServerSseChannel` in `Server/ServerSseChannel.vala` ### Phase 4: Example and Testing 8. Create example endpoint in `examples/SseExample.vala` 9. Update `examples/meson.build` ## Design Decisions and Rationale ### Why SseChannel Interface? Following the existing pattern where `AsyncOutput` is an interface in Core but `ServerOutput` is an internal implementation, `SseChannel` provides: - Clean separation between public API and server implementation - Testability - mock implementations for unit tests - Flexibility - alternative implementations possible ### Why CancellationToken vs Automatic Detection? User preference for manual management. Benefits: - Explicit control in endpoint code - Clear resource ownership - Predictable cleanup behavior - Endpoint can perform cleanup operations before terminating ### Why SseHandler Interface Instead of Async Delegate? Vala does not support async delegates. Using an interface: - Follows the same pattern as `Endpoint` in the existing codebase - Enables IoC container registration and dependency injection - Provides clear contract for SSE handlers - Allows both named classes and anonymous implementations ## SSE Protocol Compliance The implementation follows the W3C Server-Sent Events specification: - Content-Type: `text/event-stream` - Events formatted with `field: value` syntax - Events separated by blank lines - Support for `id`, `event`, `data`, and `retry` fields - Automatic reconnection via `retry` field ## Integration with Existing Components ### Compression Pipeline SSE streams should set `DO_NOT_COMPRESS` flag since: - Content is text-based and already efficient - Compression adds latency - Real-time nature conflicts with compression buffering ### Response Headers `HttpSseResult` automatically sets: - `Content-Type: text/event-stream` - `Cache-Control: no-cache` - `Connection: keep-alive` ## Minimal Feature Set Based on discussion, keeping features minimal: - No built-in Last-Event-ID support (endpoints can handle if needed) - No built-in heartbeat (endpoints can send comments if needed) - No connection timeout (endpoints manage via CancellationToken)