diff --git a/el-compiler/runtime/el_seed.c b/el-compiler/runtime/el_seed.c index bc7f54a..618ed47 100644 --- a/el-compiler/runtime/el_seed.c +++ b/el-compiler/runtime/el_seed.c @@ -554,6 +554,94 @@ el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body) return http_response(status, headers_json, body); } +/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */ +/* + * Thread-local file descriptor stashed by http_worker_v2 before calling the + * El handler. El SSE builtins read this to get the raw socket fd. + * + * Lifecycle: + * http_worker_v2 sets _tl_http_conn_fd = fd (via el_seed_set_http_conn_fd) + * El handler calls __http_conn_fd() → receives that fd + * El handler calls __http_sse_open(fd) → sends SSE headers, keeps fd open + * El handler calls __http_sse_send(fd, data) → writes "data: ...\n\n" + * El handler calls __http_sse_close(fd) → closes the fd + * El handler returns "__sse__" sentinel → http_worker_v2 does NOT close fd + * + * The -1 value means no current connection (guard against misuse outside + * a handler context). + */ +static __thread int _tl_http_conn_fd = -1; + +/* Called by el_runtime.c's http_worker_v2 — not part of the El ABI. */ +void el_seed_set_http_conn_fd(int fd) { + _tl_http_conn_fd = fd; +} + +/* __http_conn_fd() — returns the raw fd for the current HTTP connection. + * Valid only inside an http_serve_v2 handler before it returns. */ +el_val_t __http_conn_fd(void) { + return EL_INT(_tl_http_conn_fd); +} + +/* __http_sse_open(fd) — sends SSE response headers on fd, keeping it open. + * Returns 1 on success, 0 on write failure. */ +el_val_t __http_sse_open(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + static const char sse_headers[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Connection: keep-alive\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n"; + size_t n = sizeof(sse_headers) - 1; /* exclude NUL */ + size_t sent = 0; + while (sent < n) { + ssize_t w = write(fd, sse_headers + sent, n - sent); + if (w <= 0) return 0; + sent += (size_t)w; + } + return 1; +} + +/* __http_sse_send(fd, data) — writes one SSE event frame: "data: \n\n". + * data must not contain newlines. Returns 1 on success, 0 on client disconnect. */ +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + const char* s = EL_CSTR(data); + if (!s) s = ""; + /* Build "data: \n\n" in a single buffer for one write call. */ + size_t prefix_len = 6; /* "data: " */ + size_t slen = strlen(s); + size_t total = prefix_len + slen + 2; /* + "\n\n" */ + char* buf = malloc(total + 1); + if (!buf) return 0; + memcpy(buf, "data: ", 6); + memcpy(buf + 6, s, slen); + buf[6 + slen] = '\n'; + buf[6 + slen + 1] = '\n'; + buf[total] = '\0'; + size_t sent = 0; + int ok = 1; + while (sent < total) { + ssize_t w = write(fd, buf + sent, total - sent); + if (w <= 0) { ok = 0; break; } + sent += (size_t)w; + } + free(buf); + return ok ? 1 : 0; +} + +/* __http_sse_close(fd) — closes the SSE connection fd. */ +el_val_t __http_sse_close(el_val_t conn_id) { + int fd = (int)(int64_t)conn_id; + if (fd < 0) return 0; + close(fd); + return 1; +} + /* ── Threading ───────────────────────────────────────────────────────────── */ /* * Design: diff --git a/el-compiler/runtime/el_seed.h b/el-compiler/runtime/el_seed.h index 20f01a4..8a9bd90 100644 --- a/el-compiler/runtime/el_seed.h +++ b/el-compiler/runtime/el_seed.h @@ -81,6 +81,26 @@ void __http_serve_v2(el_val_t port, el_val_t handler_name); * headers_json: JSON object literal like {"Content-Type":"text/plain"} or "{}" */ el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body); +/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */ + +/* Returns the raw file descriptor for the current HTTP connection. + * Valid only inside an http_serve_v2 handler before it returns. + * Returns -1 if called outside a handler context. */ +el_val_t __http_conn_fd(void); + +/* Sends SSE response headers on conn_id (the fd from __http_conn_fd), + * keeping the connection open for streaming. Returns 1 on success, 0 on + * write failure. Call once at the start of an SSE handler. */ +el_val_t __http_sse_open(el_val_t conn_id); + +/* Writes one SSE event frame: "data: \n\n". data must not contain + * newlines. Returns 1 on success, 0 if the client disconnected. */ +el_val_t __http_sse_send(el_val_t conn_id, el_val_t data); + +/* Closes the SSE connection. The handler must return http_sse_sentinel() + * so the HTTP worker does not double-close the fd. */ +el_val_t __http_sse_close(el_val_t conn_id); + /* ── Threading ───────────────────────────────────────────────────────────── */ /* Create a thread that calls the named El function with a String argument. diff --git a/el-compiler/runtime/legacy/el_runtime.c b/el-compiler/runtime/legacy/el_runtime.c index da1e6d3..08d51fb 100644 --- a/el-compiler/runtime/legacy/el_runtime.c +++ b/el-compiler/runtime/legacy/el_runtime.c @@ -1673,6 +1673,7 @@ static void* http_worker_v2(void* arg) { HttpWorkerArg* a = (HttpWorkerArg*)arg; int fd = a->fd; free(a); + int is_sse = 0; char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL; if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) { http_handler4_fn h = http_lookup_active_v2(); @@ -1680,28 +1681,39 @@ static void* http_worker_v2(void* arg) { int head_only = (method && strcmp(method, "HEAD") == 0); const char* dispatch_method = head_only ? "GET" : method; el_request_start(); /* begin per-request arena */ + /* Expose the raw fd to El SSE builtins (__http_conn_fd etc.). */ + el_seed_set_http_conn_fd(fd); if (h) { el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : ""); el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body)); const char* rs = EL_CSTR(r); - size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); - response = malloc(rlen + 1); - if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } - else if (response) { response[0] = '\0'; } + /* Detect SSE sentinel — handler took ownership of the fd. */ + if (rs && strcmp(rs, "__sse__") == 0) { + is_sse = 1; + } else { + size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0); + response = malloc(rlen + 1); + if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; } + else if (response) { response[0] = '\0'; } + } el_release(hmap); } else { response = el_strdup_persist( "el-runtime: no v2 http handler registered " "(call http_set_handler_v2)"); } + el_seed_set_http_conn_fd(-1); /* clear before arena teardown */ el_request_end(); /* free all intermediate strings */ - _tl_http_head_only = head_only; - http_send_response(fd, response); - _tl_http_head_only = 0; - free(response); + if (!is_sse) { + _tl_http_head_only = head_only; + http_send_response(fd, response); + _tl_http_head_only = 0; + free(response); + } } free(method); free(path); free(body); free(hdr_block); - close(fd); + /* SSE handlers close the fd themselves via __http_sse_close. */ + if (!is_sse) close(fd); pthread_mutex_lock(&_http_conn_mu); _http_conn_active--; pthread_cond_signal(&_http_conn_cv); diff --git a/el-compiler/runtime/legacy/el_runtime.h b/el-compiler/runtime/legacy/el_runtime.h index 72bbf4b..8b80ee7 100644 --- a/el-compiler/runtime/legacy/el_runtime.h +++ b/el-compiler/runtime/legacy/el_runtime.h @@ -176,6 +176,11 @@ void http_set_handler_v2(el_val_t name); * auto-content-type contract for legacy handlers that return plain bodies. */ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body); +/* SSE connection fd — set by http_worker_v2 before calling the El handler, + * cleared afterwards. Defined in el_seed.c; called from el_runtime.c. + * The getter is exposed as __http_conn_fd() to El programs. */ +void el_seed_set_http_conn_fd(int fd); + /* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default * 60000ms). Read lazily on first use, so setting the env var any time before * the first http_* call is sufficient. */ diff --git a/runtime/http.el b/runtime/http.el index 3a5e4a4..587d3dc 100644 --- a/runtime/http.el +++ b/runtime/http.el @@ -169,3 +169,53 @@ fn http_serve_v2(port: Int, handler: String) { fn http_response(status: Int, headers_json: String, body: String) -> String { return __http_response(status, headers_json, body) } + +// ── SSE — Server-Sent Events streaming ─────────────────────────────────────── +// +// Usage pattern for an SSE handler: +// +// fn my_handler(method: String, path: String, headers: Map, body: String) -> String { +// let fd: Int = http_conn_fd() +// http_sse_open(fd) +// http_sse_send(fd, "hello") +// http_sse_send(fd, "world") +// http_sse_close(fd) +// return http_sse_sentinel() +// } +// +// The sentinel return value tells http_serve_v2 NOT to close the connection +// automatically — the handler already closed it via http_sse_close. + +// http_conn_fd returns the raw file descriptor for the current HTTP connection. +// Only valid inside an http_serve_v2 handler, before the handler returns. +// Use with http_sse_open / http_sse_send / http_sse_close for streaming. +fn http_conn_fd() -> Int { + return __http_conn_fd() +} + +// http_sse_open sends SSE response headers on the current connection, +// keeping it open for streaming. Call once at the start of an SSE handler. +// Returns true on success. +fn http_sse_open(fd: Int) -> Bool { + return __http_sse_open(fd) +} + +// http_sse_send writes one SSE event to the connection. +// data should not contain newlines (they are added automatically). +// Returns true if the write succeeded (client still connected). +fn http_sse_send(fd: Int, data: String) -> Bool { + return __http_sse_send(fd, data) +} + +// http_sse_close closes the SSE connection. +fn http_sse_close(fd: Int) { + __http_sse_close(fd) + return +} + +// http_sse_sentinel is the return value an SSE handler must return +// to tell the HTTP server NOT to close the connection automatically. +// The handler takes ownership of the fd and closes it via http_sse_close. +fn http_sse_sentinel() -> String { + return "__sse__" +}