feat: add SSE streaming helper support

Add Server-Sent Events (SSE) and streaming response support with three
streaming functions:

- stream(): Basic binary streaming with chunked transfer encoding
- stream_text(): Text streaming with proper Content-Type headers
- stream_sse(): SSE event streaming with full event field support

Features:
- SSEEvent struct with data, event, id, retry fields
- StreamContext for write, writeln, write_sse, sleep, pipe operations
- Multi-line data handling in SSE events
- Custom error handler support
- Client disconnect detection via on_abort callback
- Integrated with both Picoev and uSockets backends

New files:
- streaming.v: Core streaming implementation
- examples/sse_demo.v: Comprehensive SSE demo with test page
- tests/test_stream_*.v: 62 property-based tests

Updated:
- picoev_server.v: PicoevStreamWriter integration
- usockets_server.v: UsocketsStreamWriter integration
- README.md: SSE streaming documentation
This commit is contained in:
meiseayoung
2026-01-04 15:13:28 +08:00
parent 6ab6d3b99e
commit 9ae9170bcb
13 changed files with 5993 additions and 4 deletions
+101
View File
@@ -577,6 +577,13 @@ fn exec_middlewares_usockets(idx int, middlewares []ContextMiddleware, mut ctx C
// 发送 uSockets 响应 - 优化版
fn send_usockets_response(s usockets.Socket, ctx Context, response http.Response) {
// Check if this is a streaming response
if is_streaming_response(ctx) {
// Handle streaming response
handle_usockets_streaming_response(s, ctx)
return
}
status_code := if ctx.status_code != 0 { ctx.status_code } else { response.status_code }
mut resp := strings.new_builder(512)
@@ -633,6 +640,100 @@ fn send_usockets_response(s usockets.Socket, ctx Context, response http.Response
s.write_bytes(resp.str())
}
// ============================================================================
// Streaming Response Handling for uSockets
// ============================================================================
// handle_usockets_streaming_response - Handle a streaming response using UsocketsStreamWriter
// This function:
// 1. Sends the HTTP headers for streaming
// 2. Creates a UsocketsStreamWriter
// 3. Executes the streaming callback
// 4. Handles errors and cleanup
//
// Requirements: 7.1, 7.2, 7.3, 7.4
fn handle_usockets_streaming_response(s usockets.Socket, ctx Context) {
// Get stream configuration
stream_config := get_stream_config(ctx) or {
// Fallback to error response if stream config not found
s.write_bytes('HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\nContent-Length: 32\r\n\r\nStream configuration not found')
return
}
// Determine status code
status_code := if ctx.status_code != 0 { ctx.status_code } else { 200 }
// Build HTTP response headers
mut resp := strings.new_builder(512)
// Status line
resp.write_string('HTTP/1.1 ')
resp.write_string(status_code.str())
resp.write_string(' ')
resp.write_string(get_status_text_usockets(status_code))
resp.write_string('\r\n')
// Send headers from context
mut has_transfer_encoding := false
mut has_connection := false
for key, value in ctx.headers {
key_len := key.len
// Skip Content-Length for streaming (we use chunked encoding)
if key_len == 14 && eq_ignore_case_usockets(key, 'content-length') {
continue
}
resp.write_string(key)
resp.write_string(': ')
resp.write_string(value)
resp.write_string('\r\n')
if key_len == 17 && eq_ignore_case_usockets(key, 'transfer-encoding') {
has_transfer_encoding = true
} else if key_len == 10 && eq_ignore_case_usockets(key, 'connection') {
has_connection = true
}
}
// Ensure Transfer-Encoding: chunked is set (Requirement 7.2)
if !has_transfer_encoding {
resp.write_string('Transfer-Encoding: chunked\r\n')
}
// Ensure Connection header is set for streaming
if !has_connection {
resp.write_string('Connection: keep-alive\r\n')
}
// End headers section
resp.write_string('\r\n')
// Send headers to client
s.write_bytes(resp.str())
// Create UsocketsStreamWriter for streaming (Requirement 7.1)
mut writer := UsocketsStreamWriter.new(s)
// Create StreamContext with the writer
mut stream_ctx := StreamContext.new(writer)
// Execute the streaming callback (Requirement 7.4 - compatibility with callback architecture)
stream_config.callback(mut stream_ctx) or {
// Handle error (Requirement 7.3 - handle connection close events)
if error_handler := stream_config.error_handler {
error_handler(err, mut stream_ctx)
} else {
// Default: log error to console
eprintln('[uSockets Stream Error] ${err.msg()}')
}
}
// Auto-close the stream when callback completes
stream_ctx.close()
// Cleanup the stored configuration
cleanup_stream_config(ctx)
}
// 大小写不敏感字符串比较(避免分配)
@[inline]
fn eq_ignore_case_usockets(a string, b string) bool {