Previously, PicoevStreamWriter used picohttpparser.Response.raw() to write streaming data, which only buffered the data until the entire request handler completed. This caused SSE responses to be delayed until all sleep() calls finished (e.g., 2.89s delay for /stream-text endpoint). Changes: - Modified PicoevStreamWriter to use socket fd directly from picohttpparser.Response - Implemented write_to_fd() using C.send() for immediate data delivery - Updated handle_picoev_streaming_response() to write HTTP headers directly to socket - Added C.send() function declaration for direct socket writes This fix enables true real-time streaming where each write() or write_sse() call immediately sends data to the client, rather than buffering until the callback completes. All 74 test files pass with no regressions.
714 lines
20 KiB
V
714 lines
20 KiB
V
module hono
|
||
|
||
import net
|
||
import net.http
|
||
import picoev
|
||
import picohttpparser
|
||
|
||
// picoev 服务器配置
|
||
pub struct PicoevConfig {
|
||
pub:
|
||
port int = 8080
|
||
host string
|
||
family net.AddrFamily = .ip6
|
||
timeout_secs int = 120 // 高并发场景需要更长超时(原 8 秒太短)
|
||
max_headers int = 100
|
||
max_read int = 8192
|
||
max_write int = 65536
|
||
keepalive_timeout int = 30 // Keep-Alive 超时延长到 30 秒
|
||
max_keepalive_req int = 10000 // 单连接最大请求数提升到 10000
|
||
}
|
||
|
||
// picoev 请求上下文
|
||
struct PicoevRequestContext {
|
||
mut:
|
||
app &Hono = unsafe { nil }
|
||
config PicoevConfig
|
||
}
|
||
|
||
// 使用 picoev 启动服务器
|
||
pub fn (mut app Hono) listen_picoev(port int) {
|
||
app.listen_picoev_with_config(PicoevConfig{
|
||
port: port
|
||
})
|
||
}
|
||
|
||
// 使用 picoev 启动服务器(带配置)
|
||
pub fn (mut app Hono) listen_picoev_with_config(config PicoevConfig) {
|
||
mut ctx := &PicoevRequestContext{
|
||
app: unsafe { &app }
|
||
config: config
|
||
}
|
||
|
||
mut pico := picoev.new(
|
||
port: config.port
|
||
host: config.host
|
||
family: config.family
|
||
timeout_secs: config.timeout_secs
|
||
max_headers: config.max_headers
|
||
max_read: config.max_read
|
||
max_write: config.max_write
|
||
cb: picoev_callback
|
||
user_data: ctx
|
||
) or {
|
||
eprintln('[v-hono] Failed to create picoev server: ${err}')
|
||
return
|
||
}
|
||
|
||
host_str := if config.host == '' { '127.0.0.1' } else { config.host }
|
||
println('[v-hono] Listening on http://${host_str}:${config.port}/ (picoev)')
|
||
|
||
pico.serve()
|
||
}
|
||
|
||
// ============================================================================
|
||
// WebSocket Upgrade Detection and Handling
|
||
// ============================================================================
|
||
|
||
// Check if a picoev request is a WebSocket upgrade request
|
||
fn is_picoev_ws_upgrade(req picohttpparser.Request) bool {
|
||
mut has_upgrade := false
|
||
mut has_connection := false
|
||
mut has_ws_key := false
|
||
|
||
for i in 0 .. req.num_headers {
|
||
h := req.headers[i]
|
||
name_lower := h.name.to_lower()
|
||
|
||
if name_lower == 'upgrade' {
|
||
if h.value.to_lower() == 'websocket' {
|
||
has_upgrade = true
|
||
}
|
||
} else if name_lower == 'connection' {
|
||
if contains_ignore_case(h.value, 'upgrade') {
|
||
has_connection = true
|
||
}
|
||
} else if name_lower == 'sec-websocket-key' {
|
||
if h.value.len > 0 {
|
||
has_ws_key = true
|
||
}
|
||
}
|
||
}
|
||
|
||
return has_upgrade && has_connection && has_ws_key
|
||
}
|
||
|
||
// Get WebSocket key from picoev request headers
|
||
fn get_picoev_ws_key(req picohttpparser.Request) string {
|
||
for i in 0 .. req.num_headers {
|
||
h := req.headers[i]
|
||
if eq_ignore_case(h.name, 'sec-websocket-key') {
|
||
return h.value
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
|
||
// Get WebSocket protocol from picoev request headers
|
||
fn get_picoev_ws_protocol(req picohttpparser.Request) string {
|
||
for i in 0 .. req.num_headers {
|
||
h := req.headers[i]
|
||
if eq_ignore_case(h.name, 'sec-websocket-protocol') {
|
||
return h.value
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
|
||
// Get WebSocket version from picoev request headers
|
||
fn get_picoev_ws_version(req picohttpparser.Request) string {
|
||
for i in 0 .. req.num_headers {
|
||
h := req.headers[i]
|
||
if eq_ignore_case(h.name, 'sec-websocket-version') {
|
||
return h.value
|
||
}
|
||
}
|
||
return '13'
|
||
}
|
||
|
||
// Handle WebSocket upgrade for picoev
|
||
fn handle_picoev_ws_upgrade(mut ctx PicoevRequestContext, req picohttpparser.Request, mut res picohttpparser.Response, route_match ContextRouteMatch, hono_ctx Context) bool {
|
||
// Validate WebSocket version
|
||
ws_version := get_picoev_ws_version(req)
|
||
if ws_version != '13' {
|
||
res.raw('HTTP/1.1 426 Upgrade Required\r\n')
|
||
res.header('Sec-WebSocket-Version', '13')
|
||
res.header('Content-Type', 'text/plain')
|
||
res.body('Unsupported WebSocket version')
|
||
res.end()
|
||
return false
|
||
}
|
||
|
||
// Get WebSocket key
|
||
ws_key := get_picoev_ws_key(req)
|
||
if ws_key.len == 0 {
|
||
res.raw('HTTP/1.1 400 Bad Request\r\n')
|
||
res.header('Content-Type', 'text/plain')
|
||
res.body('Missing Sec-WebSocket-Key header')
|
||
res.end()
|
||
return false
|
||
}
|
||
|
||
// Compute accept key
|
||
accept_key := compute_accept_key(ws_key)
|
||
|
||
// Execute the route handler to get WSEvents
|
||
// The handler should return a 101 response with WebSocket context stored
|
||
mut mutable_ctx := hono_ctx
|
||
response := route_match.handler.handle(mut mutable_ctx)
|
||
|
||
// Check if this is a WebSocket upgrade response
|
||
if response.status_code != 101 {
|
||
// Not a WebSocket upgrade, send the response as-is
|
||
send_picoev_response(mut res, mutable_ctx, response, false, ctx.config)
|
||
return false
|
||
}
|
||
|
||
// Negotiate subprotocol
|
||
mut selected_protocol := ''
|
||
if '_ws_protocol' in mutable_ctx.store {
|
||
selected_protocol = mutable_ctx.store['_ws_protocol']
|
||
}
|
||
|
||
// Send WebSocket handshake response
|
||
res.raw('HTTP/1.1 101 Switching Protocols\r\n')
|
||
res.header('Upgrade', 'websocket')
|
||
res.header('Connection', 'Upgrade')
|
||
res.header('Sec-WebSocket-Accept', accept_key)
|
||
if selected_protocol.len > 0 {
|
||
res.header('Sec-WebSocket-Protocol', selected_protocol)
|
||
}
|
||
res.body('')
|
||
res.end()
|
||
|
||
return true
|
||
}
|
||
|
||
// Send WebSocket frame via picoev response
|
||
fn send_ws_frame_picoev(data []u8) ! {
|
||
// This is a placeholder - actual implementation depends on picoev's raw socket access
|
||
// In practice, we need to write directly to the socket fd
|
||
}
|
||
|
||
// picoev 回调函数
|
||
fn picoev_callback(user_data voidptr, req picohttpparser.Request, mut res picohttpparser.Response) {
|
||
mut ctx := unsafe { &PicoevRequestContext(user_data) }
|
||
|
||
if ctx.app == unsafe { nil } {
|
||
res.http_500()
|
||
res.end()
|
||
return
|
||
}
|
||
|
||
path, query_map := parse_path_and_query(req.path)
|
||
keepalive := check_keepalive_request(req)
|
||
method_str := req.method
|
||
|
||
// Check if this is a WebSocket upgrade request
|
||
is_ws_upgrade := is_picoev_ws_upgrade(req)
|
||
|
||
// 优先使用快速路由器
|
||
if ctx.app.use_fast_router {
|
||
if route_match := ctx.app.fast_router.match_route(method_str, path) {
|
||
mut hono_ctx := create_picoev_context(req, route_match.params, query_map)
|
||
|
||
// Handle WebSocket upgrade if detected
|
||
if is_ws_upgrade {
|
||
if handle_picoev_ws_upgrade(mut ctx, req, mut res, route_match, hono_ctx) {
|
||
// WebSocket upgrade successful, connection is now in WebSocket mode
|
||
return
|
||
}
|
||
// If upgrade failed, response was already sent
|
||
return
|
||
}
|
||
|
||
// 优化:零中间件快速路径
|
||
if !ctx.app.has_middlewares {
|
||
response := route_match.handler.handle(mut hono_ctx)
|
||
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
|
||
return
|
||
}
|
||
|
||
middlewares := get_middlewares_for_path_picoev_optimized(ctx.app, path)
|
||
response := exec_middlewares_picoev(0, middlewares, mut hono_ctx, route_match.handler)
|
||
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
|
||
return
|
||
}
|
||
}
|
||
|
||
// 回退到混合路由器
|
||
if route_match := ctx.app.context_hybrid_router.match_route(method_str, path) {
|
||
mut hono_ctx := create_picoev_context(req, route_match.params, query_map)
|
||
|
||
// Handle WebSocket upgrade if detected
|
||
if is_ws_upgrade {
|
||
if handle_picoev_ws_upgrade(mut ctx, req, mut res, route_match, hono_ctx) {
|
||
// WebSocket upgrade successful
|
||
return
|
||
}
|
||
return
|
||
}
|
||
|
||
// 优化:零中间件快速路径
|
||
if !ctx.app.has_middlewares {
|
||
response := route_match.handler.handle(mut hono_ctx)
|
||
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
|
||
return
|
||
}
|
||
|
||
middlewares := get_middlewares_for_path_picoev_optimized(ctx.app, path)
|
||
response := exec_middlewares_picoev(0, middlewares, mut hono_ctx, route_match.handler)
|
||
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
|
||
return
|
||
}
|
||
|
||
// 404 Not Found
|
||
mut hono_ctx := create_picoev_context(req, map[string]string{}, query_map)
|
||
|
||
if handler := ctx.app.not_found_handler {
|
||
response := handler(mut hono_ctx)
|
||
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
|
||
return
|
||
}
|
||
|
||
// 默认 404 响应
|
||
res.raw('HTTP/1.1 404 Not Found\r\n')
|
||
res.header('Content-Type', 'text/plain')
|
||
res.header('Content-Length', '9')
|
||
if keepalive {
|
||
res.header('Connection', 'keep-alive')
|
||
res.header('Keep-Alive', 'timeout=${ctx.config.keepalive_timeout}, max=${ctx.config.max_keepalive_req}')
|
||
} else {
|
||
res.header('Connection', 'close')
|
||
}
|
||
res.body('Not Found')
|
||
res.end()
|
||
}
|
||
|
||
// 检查客户端是否请求 Keep-Alive - 优化版
|
||
// 避免 to_lower() 创建新字符串,使用大小写不敏感比较
|
||
fn check_keepalive_request(req picohttpparser.Request) bool {
|
||
for i in 0 .. req.num_headers {
|
||
h := req.headers[i]
|
||
// 大小写不敏感比较 "connection"
|
||
if h.name.len == 10 && eq_ignore_case(h.name, 'connection') {
|
||
// 检查是否包含 "keep-alive"(大小写不敏感)
|
||
return contains_ignore_case(h.value, 'keep-alive')
|
||
}
|
||
}
|
||
return true // HTTP/1.1 默认 Keep-Alive
|
||
}
|
||
|
||
// 大小写不敏感字符串比较(避免分配)
|
||
@[inline]
|
||
fn eq_ignore_case(a string, b string) bool {
|
||
if a.len != b.len {
|
||
return false
|
||
}
|
||
for i in 0 .. a.len {
|
||
ca := a[i]
|
||
cb := b[i]
|
||
// 转换为小写比较
|
||
la := if ca >= `A` && ca <= `Z` { ca + 32 } else { ca }
|
||
lb := if cb >= `A` && cb <= `Z` { cb + 32 } else { cb }
|
||
if la != lb {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// 大小写不敏感的 contains 检查(避免分配)
|
||
@[inline]
|
||
fn contains_ignore_case(haystack string, needle string) bool {
|
||
if needle.len > haystack.len {
|
||
return false
|
||
}
|
||
max_start := haystack.len - needle.len
|
||
for i := 0; i <= max_start; i++ {
|
||
mut found := true
|
||
for j in 0 .. needle.len {
|
||
ch := haystack[i + j]
|
||
cn := needle[j]
|
||
lh := if ch >= `A` && ch <= `Z` { ch + 32 } else { ch }
|
||
ln := if cn >= `A` && cn <= `Z` { cn + 32 } else { cn }
|
||
if lh != ln {
|
||
found = false
|
||
break
|
||
}
|
||
}
|
||
if found {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// 获取路径对应的所有中间件(优化版:使用预排序的前缀列表)
|
||
fn get_middlewares_for_path_picoev_optimized(app &Hono, path string) []ContextMiddleware {
|
||
// 优化:只有全局中间件时,直接返回引用(避免克隆)
|
||
if app.route_middlewares.len == 0 {
|
||
return app.context_middlewares
|
||
}
|
||
|
||
mut middlewares := app.context_middlewares.clone()
|
||
|
||
// 优化:使用预排序的前缀列表(启动时已排序,不需要每次请求都排序)
|
||
for prefix in app.sorted_middleware_prefixes {
|
||
if path.starts_with(prefix) || prefix == '/' {
|
||
if mws := app.route_middlewares[prefix] {
|
||
middlewares << mws
|
||
}
|
||
}
|
||
}
|
||
|
||
return middlewares
|
||
}
|
||
|
||
// 获取路径对应的所有中间件(保留旧版本兼容)
|
||
fn get_middlewares_for_path_picoev(app &Hono, path string) []ContextMiddleware {
|
||
return get_middlewares_for_path_picoev_optimized(app, path)
|
||
}
|
||
|
||
// 执行中间件链
|
||
fn exec_middlewares_picoev(idx int, middlewares []ContextMiddleware, mut ctx Context, handler IHandler) http.Response {
|
||
if idx < middlewares.len {
|
||
mw := middlewares[idx]
|
||
return mw(mut ctx, fn [idx, middlewares, handler] (mut c Context) http.Response {
|
||
return exec_middlewares_picoev(idx + 1, middlewares, mut c, handler)
|
||
})
|
||
}
|
||
return handler.handle(mut ctx)
|
||
}
|
||
|
||
// 解析路径和查询参数 - 零分配优化版
|
||
// 使用指针遍历避免创建临时数组
|
||
fn parse_path_and_query(full_path string) (string, map[string]string) {
|
||
mut query_map := map[string]string{}
|
||
len := full_path.len
|
||
|
||
if len == 0 {
|
||
return full_path, query_map
|
||
}
|
||
|
||
// 快速路径:大多数请求没有查询参数
|
||
// 从后向前搜索 '?' 通常更快(查询参数在末尾)
|
||
mut query_start := -1
|
||
for i := len - 1; i >= 0; i-- {
|
||
if full_path[i] == `?` {
|
||
query_start = i
|
||
break
|
||
}
|
||
}
|
||
|
||
// 没有查询参数,直接返回(最常见情况)
|
||
if query_start == -1 {
|
||
return full_path, query_map
|
||
}
|
||
|
||
path := full_path[..query_start]
|
||
|
||
// 解析查询参数(单次遍历,避免 split)
|
||
mut key_start := query_start + 1
|
||
mut key_end := -1
|
||
mut value_start := -1
|
||
|
||
for i := query_start + 1; i <= len; i++ {
|
||
ch := if i < len { full_path[i] } else { `&` } // 末尾视为分隔符
|
||
|
||
if ch == `=` && key_end == -1 {
|
||
key_end = i
|
||
value_start = i + 1
|
||
} else if ch == `&` {
|
||
// 完成一个键值对
|
||
if key_end > key_start && value_start > 0 {
|
||
key := full_path[key_start..key_end]
|
||
value := if value_start < i { full_path[value_start..i] } else { '' }
|
||
query_map[key] = value
|
||
} else if key_end == -1 && i > key_start {
|
||
// 只有 key 没有 value(如 ?foo&bar=1)
|
||
key := full_path[key_start..i]
|
||
query_map[key] = ''
|
||
}
|
||
// 重置状态
|
||
key_start = i + 1
|
||
key_end = -1
|
||
value_start = -1
|
||
}
|
||
}
|
||
|
||
return path, query_map
|
||
}
|
||
|
||
// 创建 picoev 上下文 - 优化版
|
||
// 延迟转换 http.Request,只在真正需要时才转换
|
||
fn create_picoev_context(req picohttpparser.Request, params map[string]string, query map[string]string) Context {
|
||
// 提取纯路径(不含查询参数)
|
||
path := extract_path_only(req.path)
|
||
|
||
return Context{
|
||
req: convert_picoev_request(req)
|
||
params: params
|
||
query: query
|
||
body: req.body
|
||
url: req.path
|
||
path: path
|
||
status_code: 200
|
||
headers: map[string]string{}
|
||
}
|
||
}
|
||
|
||
// 快速提取路径(不含查询参数)
|
||
@[inline]
|
||
fn extract_path_only(full_path string) string {
|
||
for i in 0 .. full_path.len {
|
||
if full_path[i] == `?` {
|
||
return full_path[..i]
|
||
}
|
||
}
|
||
return full_path
|
||
}
|
||
|
||
// 转换 picoev 请求 - 优化版
|
||
// 使用预计算的方法映射避免字符串比较
|
||
fn convert_picoev_request(req picohttpparser.Request) http.Request {
|
||
mut headers := http.new_header()
|
||
|
||
for i in 0 .. req.num_headers {
|
||
h := req.headers[i]
|
||
headers.add_custom(h.name, h.value) or { continue }
|
||
}
|
||
|
||
return http.Request{
|
||
method: parse_http_method_fast(req.method)
|
||
url: req.path
|
||
data: req.body
|
||
header: headers
|
||
}
|
||
}
|
||
|
||
// 快速 HTTP 方法解析(基于首字符和长度)
|
||
@[inline]
|
||
fn parse_http_method_fast(method string) http.Method {
|
||
len := method.len
|
||
if len == 0 {
|
||
return http.Method.get
|
||
}
|
||
|
||
// 基于首字符快速分支
|
||
match method[0] {
|
||
`G` {
|
||
if len == 3 { return http.Method.get }
|
||
}
|
||
`P` {
|
||
if len == 4 && method[1] == `O` { return http.Method.post }
|
||
if len == 3 && method[1] == `U` { return http.Method.put }
|
||
if len == 5 { return http.Method.patch }
|
||
}
|
||
`D` {
|
||
if len == 6 { return http.Method.delete }
|
||
}
|
||
`H` {
|
||
if len == 4 { return http.Method.head }
|
||
}
|
||
`O` {
|
||
if len == 7 { return http.Method.options }
|
||
}
|
||
else {}
|
||
}
|
||
return http.Method.get
|
||
}
|
||
|
||
// 发送 picoev 响应 - 优化版
|
||
fn send_picoev_response(mut res picohttpparser.Response, ctx Context, response http.Response, keepalive bool, config PicoevConfig) {
|
||
// Check if this is a streaming response
|
||
if is_streaming_response(ctx) {
|
||
// Handle streaming response
|
||
handle_picoev_streaming_response(mut res, ctx, config)
|
||
return
|
||
}
|
||
|
||
status_code := if ctx.status_code != 0 { ctx.status_code } else { response.status_code }
|
||
|
||
if status_code == 200 {
|
||
res.http_ok()
|
||
} else {
|
||
res.raw('HTTP/1.1 ${status_code} ${get_status_text(status_code)}\r\n')
|
||
}
|
||
|
||
mut has_content_type := false
|
||
mut has_connection := false
|
||
|
||
for key, value in ctx.headers {
|
||
// 使用快速大小写不敏感比较
|
||
key_len := key.len
|
||
if key_len == 14 && eq_ignore_case(key, 'content-length') {
|
||
continue // 跳过 Content-Length
|
||
}
|
||
res.header(key, value)
|
||
if key_len == 12 && eq_ignore_case(key, 'content-type') {
|
||
has_content_type = true
|
||
} else if key_len == 10 && eq_ignore_case(key, 'connection') {
|
||
has_connection = true
|
||
}
|
||
}
|
||
|
||
if !has_content_type {
|
||
if content_type := response.header.get(.content_type) {
|
||
res.header('Content-Type', content_type)
|
||
has_content_type = true
|
||
}
|
||
}
|
||
|
||
if !has_content_type {
|
||
res.header('Content-Type', 'text/plain; charset=utf-8')
|
||
}
|
||
|
||
if !has_connection {
|
||
if keepalive {
|
||
res.header('Connection', 'keep-alive')
|
||
res.header('Keep-Alive', 'timeout=${config.keepalive_timeout}, max=${config.max_keepalive_req}')
|
||
} else {
|
||
res.header('Connection', 'close')
|
||
}
|
||
}
|
||
|
||
res.body(response.body)
|
||
res.end()
|
||
}
|
||
|
||
// ============================================================================
|
||
// Streaming Response Handling for Picoev
|
||
// ============================================================================
|
||
|
||
// handle_picoev_streaming_response - Handle a streaming response using PicoevStreamWriter
|
||
// This function:
|
||
// 1. Sends the HTTP headers for streaming directly to socket fd
|
||
// 2. Creates a PicoevStreamWriter with the socket fd
|
||
// 3. Executes the streaming callback
|
||
// 4. Handles errors and cleanup
|
||
fn handle_picoev_streaming_response(mut res picohttpparser.Response, ctx Context, config PicoevConfig) {
|
||
// Get stream configuration
|
||
stream_config := get_stream_config(ctx) or {
|
||
// Fallback to error response if stream config not found
|
||
res.raw('HTTP/1.1 500 Internal Server Error\r\n')
|
||
res.header('Content-Type', 'text/plain')
|
||
res.body('Stream configuration not found')
|
||
res.end()
|
||
return
|
||
}
|
||
|
||
// Determine status code
|
||
status_code := if ctx.status_code != 0 { ctx.status_code } else { 200 }
|
||
|
||
// Build HTTP headers string for direct socket write
|
||
mut headers_str := 'HTTP/1.1 ${status_code} ${get_status_text(status_code)}\r\n'
|
||
|
||
// Add 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(key, 'content-length') {
|
||
continue
|
||
}
|
||
headers_str += '${key}: ${value}\r\n'
|
||
if key_len == 17 && eq_ignore_case(key, 'transfer-encoding') {
|
||
has_transfer_encoding = true
|
||
} else if key_len == 10 && eq_ignore_case(key, 'connection') {
|
||
has_connection = true
|
||
}
|
||
}
|
||
|
||
// Ensure Transfer-Encoding: chunked is set
|
||
if !has_transfer_encoding {
|
||
headers_str += 'Transfer-Encoding: chunked\r\n'
|
||
}
|
||
|
||
// Ensure Connection header is set for streaming
|
||
if !has_connection {
|
||
headers_str += 'Connection: keep-alive\r\n'
|
||
headers_str += 'Keep-Alive: timeout=${config.timeout_secs}\r\n'
|
||
}
|
||
|
||
// End headers section
|
||
headers_str += '\r\n'
|
||
|
||
// Write headers directly to socket fd for immediate delivery
|
||
fd := res.fd
|
||
write_to_socket_fd(fd, headers_str.bytes()) or {
|
||
eprintln('[Picoev Stream Error] Failed to write headers: ${err.msg()}')
|
||
cleanup_stream_config(ctx)
|
||
return
|
||
}
|
||
|
||
// Create PicoevStreamWriter for streaming (uses fd directly)
|
||
mut writer := PicoevStreamWriter.new(res)
|
||
|
||
// Create StreamContext with the writer
|
||
mut stream_ctx := StreamContext.new(writer)
|
||
|
||
// Execute the streaming callback
|
||
stream_config.callback(mut stream_ctx) or {
|
||
// Handle error
|
||
if error_handler := stream_config.error_handler {
|
||
error_handler(err, mut stream_ctx)
|
||
} else {
|
||
// Default: log error to console
|
||
eprintln('[Picoev Stream Error] ${err.msg()}')
|
||
}
|
||
}
|
||
|
||
// Auto-close the stream when callback completes
|
||
stream_ctx.close()
|
||
|
||
// Cleanup the stored configuration
|
||
cleanup_stream_config(ctx)
|
||
}
|
||
|
||
// write_to_socket_fd - Helper function to write bytes directly to socket fd
|
||
// Used for sending HTTP headers before streaming begins
|
||
fn write_to_socket_fd(fd int, data []u8) ! {
|
||
if data.len == 0 {
|
||
return
|
||
}
|
||
mut total_written := 0
|
||
for total_written < data.len {
|
||
remaining := data.len - total_written
|
||
ptr := unsafe { &u8(data.data) + total_written }
|
||
written := C.send(fd, ptr, remaining, 0)
|
||
if written < 0 {
|
||
return error('Failed to write to socket')
|
||
}
|
||
if written == 0 {
|
||
return error('Connection closed by peer')
|
||
}
|
||
total_written += written
|
||
}
|
||
}
|
||
|
||
// 获取状态码文本
|
||
fn get_status_text(code int) string {
|
||
return match code {
|
||
101 { 'Switching Protocols' }
|
||
200 { 'OK' }
|
||
201 { 'Created' }
|
||
204 { 'No Content' }
|
||
301 { 'Moved Permanently' }
|
||
302 { 'Found' }
|
||
304 { 'Not Modified' }
|
||
400 { 'Bad Request' }
|
||
401 { 'Unauthorized' }
|
||
403 { 'Forbidden' }
|
||
404 { 'Not Found' }
|
||
405 { 'Method Not Allowed' }
|
||
426 { 'Upgrade Required' }
|
||
500 { 'Internal Server Error' }
|
||
502 { 'Bad Gateway' }
|
||
503 { 'Service Unavailable' }
|
||
else { 'Unknown' }
|
||
}
|
||
}
|