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.
The current architecture uses these key abstractions:
classDiagram
class HttpResult {
+Dictionary headers
+StatusCode status
+HttpResultFlag flags
+send_body_async AsyncOutput output
}
class AsyncOutput {
<<interface>>
+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
Separation of Concerns:
Core/ - Public abstractions visible to endpointsServer/ - Internal implementation details marked internalCore/ typesclassDiagram
class SseEvent {
+string id
+string event_type
+string data
+int64 retry
+string format
}
class SseChannel {
<<interface>>
+send_event_async SseEvent event
+send_async string data
+CancellationToken cancellation_token
}
class SseHandler {
<<interface>>
+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
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
A simple data class representing an SSE event following the W3C specification.
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
A cancellation token for manual connection management.
public class CancellationToken : Object {
public bool is_cancelled { get; private set; }
public signal void cancelled();
public void cancel();
}
Interface for sending SSE events - endpoints use this to push data.
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; }
}
Interface for SSE handlers - follows the same pattern as Endpoint.
public interface SseHandler : Object {
public abstract async void handle_sse(SseChannel channel) throws Error;
}
HttpResult subclass that enables SSE streaming.
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;
}
Internal implementation that wraps an AsyncOutput.
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;
}
// 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());
}
}
For simpler cases, Vala allows inline class implementation:
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);
}
}
SseEvent class in Core/SseEvent.valaCancellationToken class in Core/CancellationToken.valaSseChannel interface in Core/SseChannel.valaSseHandler interface in Core/SseHandler.valaHttpSseResult class in Core/HttpSseResult.valameson.build to include new filesServerSseChannel in Server/ServerSseChannel.valaexamples/SseExample.valaexamples/meson.buildFollowing the existing pattern where AsyncOutput is an interface in Core but ServerOutput is an internal implementation, SseChannel provides:
User preference for manual management. Benefits:
Vala does not support async delegates. Using an interface:
Endpoint in the existing codebaseThe implementation follows the W3C Server-Sent Events specification:
text/event-streamfield: value syntaxid, event, data, and retry fieldsretry fieldSSE streams should set DO_NOT_COMPRESS flag since:
HttpSseResult automatically sets:
Content-Type: text/event-streamCache-Control: no-cacheConnection: keep-aliveBased on discussion, keeping features minimal: