runtime: native SSE streaming — http_sse_open/send/close
Add Server-Sent Events support to the El runtime. El v2 handlers can now
hold HTTP connections open and push events in real time.
New builtins in el_seed.c:
__http_conn_fd() — retrieve raw fd from thread-local set by worker
__http_sse_open(fd) — send SSE headers (text/event-stream), keep-alive
__http_sse_send(fd, data) — write "data: <data>\n\n" frame
__http_sse_close(fd) — close the connection fd
http_worker_v2 in legacy/el_runtime.c now:
- stashes the fd via el_seed_set_http_conn_fd() before calling the handler
- detects the "__sse__" sentinel return value to skip http_send_response
and skip close(fd) — SSE handler took ownership of the fd
- clears the thread-local after the handler returns
El wrappers added to runtime/http.el:
http_conn_fd() http_sse_open(fd) http_sse_send(fd, data)
http_sse_close(fd) http_sse_sentinel()
This commit is contained in:
@@ -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: <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: <s>\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:
|
||||
|
||||
@@ -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: <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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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<String, String>, 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__"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user