diff seobeo/s_sse.c @ 257:609d3c6aff4e

[seobeo] Add persistent SSE streams Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 16:49:11 -0700
parents
children 04fee26ecce0
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/s_sse.c	Tue Aug 04 16:49:11 2026 -0700
@@ -0,0 +1,450 @@
+#include "seobeo/seobeo.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static const char *SEOBEO_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"
+    "X-Accel-Buffering: no\r\n"
+    "\r\n";
+
+static pthread_mutex_t g_sse_mutex = PTHREAD_MUTEX_INITIALIZER;
+static Seobeo_SSE_Stream **g_sse_streams = NULL;
+
+typedef struct {
+  char *data;
+  uint32 size;
+} Seobeo_SSE_Pending_Frame;
+
+#define SEOBEO_SSE_MAX_PENDING 32
+
+static boolean sse_is_open_unlocked(const Seobeo_SSE_Stream *p_stream)
+{
+  return p_stream &&
+      p_stream->started &&
+      !p_stream->closed &&
+      p_stream->p_handle &&
+      p_stream->p_handle->socket >= 0 &&
+      !atomic_load(&p_stream->p_handle->destroyed);
+}
+
+static void sse_clear_pending_unlocked(Seobeo_SSE_Stream *p_stream)
+{
+  Seobeo_SSE_Pending_Frame *p_frames = p_stream->p_pending_frames;
+  size_t count = Dowa_Array_Length(p_frames);
+  for (size_t i = p_stream->pending_offset; i < count; i++)
+    Dowa_Free(p_frames[i].data);
+  Dowa_Array_Free(p_frames);
+  p_stream->p_pending_frames = NULL;
+  p_stream->pending_offset = 0;
+}
+
+static void sse_release_unlocked(Seobeo_SSE_Stream *p_stream)
+{
+  if (!p_stream)
+    return;
+  if (atomic_fetch_sub(&p_stream->ref_count, 1) != 1)
+    return;
+  sse_clear_pending_unlocked(p_stream);
+  Dowa_Free(p_stream->path);
+  Dowa_Free(p_stream);
+}
+
+static int32 sse_drain_pending_unlocked(Seobeo_SSE_Stream *p_stream)
+{
+  if (!sse_is_open_unlocked(p_stream))
+    return -1;
+  int32 flush_result = Seobeo_Handle_Flush(p_stream->p_handle);
+  if (flush_result != 0)
+    return flush_result;
+
+  Seobeo_SSE_Pending_Frame *p_frames = p_stream->p_pending_frames;
+  size_t count = Dowa_Array_Length(p_frames);
+  while (p_stream->pending_offset < count)
+  {
+    Seobeo_SSE_Pending_Frame *p_frame =
+        &p_frames[p_stream->pending_offset];
+    int32 queue_result = Seobeo_Handle_Queue(
+        p_stream->p_handle,
+        (const uint8 *)p_frame->data,
+        p_frame->size);
+    if (queue_result != 0)
+      return queue_result;
+    Dowa_Free(p_frame->data);
+    p_stream->pending_offset++;
+    flush_result = Seobeo_Handle_Flush(p_stream->p_handle);
+    if (flush_result != 0)
+      return flush_result;
+  }
+
+  Dowa_Array_Free(p_frames);
+  p_stream->p_pending_frames = NULL;
+  p_stream->pending_offset = 0;
+  return 0;
+}
+
+static int32 sse_enqueue_frame_unlocked(
+    Seobeo_SSE_Stream *p_stream,
+    char *frame,
+    uint32 frame_size)
+{
+  Seobeo_SSE_Pending_Frame *p_frames = p_stream->p_pending_frames;
+  size_t pending = Dowa_Array_Length(p_frames) - p_stream->pending_offset;
+  if (pending >= SEOBEO_SSE_MAX_PENDING)
+    return SEOBEO_SSE_BACKPRESSURE;
+  Seobeo_SSE_Pending_Frame pending_frame = {
+    .data = frame,
+    .size = frame_size,
+  };
+  Dowa_Array_Push(p_frames, pending_frame);
+  p_stream->p_pending_frames = p_frames;
+  return sse_drain_pending_unlocked(p_stream);
+}
+
+static boolean sse_field_is_safe(const char *value)
+{
+  return !value || (strchr(value, '\r') == NULL && strchr(value, '\n') == NULL);
+}
+
+static size_t sse_multiline_size(const char *prefix, const char *value)
+{
+  const char *line = value ? value : "";
+  size_t prefix_length = strlen(prefix);
+  size_t total = 0;
+
+  while (TRUE)
+  {
+    const char *end = line;
+    while (*end && *end != '\r' && *end != '\n')
+      end++;
+    size_t line_length = (size_t)(end - line);
+    total += prefix_length + (line_length ? 1 : 0) + line_length + 1;
+    if (*end == '\0')
+      break;
+    if (*end == '\r' && end[1] == '\n')
+      end++;
+    line = end + 1;
+  }
+  return total;
+}
+
+static char *sse_append_multiline(
+    char *output,
+    const char *prefix,
+    const char *value)
+{
+  const char *line = value ? value : "";
+  size_t prefix_length = strlen(prefix);
+
+  while (TRUE)
+  {
+    const char *end = line;
+    while (*end && *end != '\r' && *end != '\n')
+      end++;
+    size_t line_length = (size_t)(end - line);
+    memcpy(output, prefix, prefix_length);
+    output += prefix_length;
+    if (line_length)
+      *output++ = ' ';
+    memcpy(output, line, line_length);
+    output += line_length;
+    *output++ = '\n';
+    if (*end == '\0')
+      break;
+    if (*end == '\r' && end[1] == '\n')
+      end++;
+    line = end + 1;
+  }
+  return output;
+}
+
+static int32 sse_send_frame(
+    Seobeo_SSE_Stream *p_stream,
+    const char *frame,
+    size_t frame_size)
+{
+  if (!sse_is_open_unlocked(p_stream) || !frame)
+    return -1;
+  if (frame_size > p_stream->p_handle->write_buffer_capacity)
+    return SEOBEO_SSE_EVENT_TOO_LARGE;
+
+  int32 flush_result = Seobeo_Handle_Flush(p_stream->p_handle);
+  if (flush_result != 0)
+    return flush_result;
+
+  int32 queue_result = Seobeo_Handle_Queue(
+      p_stream->p_handle,
+      (const uint8 *)frame,
+      (uint32)frame_size);
+  if (queue_result != 0)
+    return queue_result;
+  return Seobeo_Handle_Flush(p_stream->p_handle);
+}
+
+int32 Seobeo_SSE_Start(
+    Seobeo_SSE_Stream *p_stream,
+    Seobeo_Handle *p_handle)
+{
+  if (!p_stream || !p_handle || atomic_load(&p_handle->destroyed))
+    return -1;
+  p_stream->p_handle = p_handle;
+  p_stream->started = TRUE;
+  p_stream->closed = FALSE;
+  return sse_send_frame(
+      p_stream,
+      SEOBEO_SSE_HEADERS,
+      strlen(SEOBEO_SSE_HEADERS));
+}
+
+static int32 sse_send_event_unlocked(
+    Seobeo_SSE_Stream *p_stream,
+    const Seobeo_SSE_Event *p_event)
+{
+  if (!sse_is_open_unlocked(p_stream))
+    return -1;
+  if (!p_event || !sse_field_is_safe(p_event->event) ||
+      !sse_field_is_safe(p_event->id) ||
+      p_event->retry_ms < SEOBEO_SSE_RETRY_NONE)
+  {
+    return SEOBEO_SSE_INVALID_EVENT;
+  }
+
+  size_t frame_size = 1 + sse_multiline_size("data:", p_event->data);
+  if (p_event->id)
+    frame_size += strlen("id: ") + strlen(p_event->id) + 1;
+  if (p_event->event)
+    frame_size += strlen("event: ") + strlen(p_event->event) + 1;
+
+  char retry[32] = {0};
+  if (p_event->retry_ms != SEOBEO_SSE_RETRY_NONE)
+  {
+    int written = snprintf(
+        retry,
+        sizeof(retry),
+        "retry: %d\n",
+        p_event->retry_ms);
+    if (written < 0 || (size_t)written >= sizeof(retry))
+      return SEOBEO_SSE_INVALID_EVENT;
+    frame_size += (size_t)written;
+  }
+
+  if (frame_size > p_stream->p_handle->write_buffer_capacity)
+    return SEOBEO_SSE_EVENT_TOO_LARGE;
+  char *frame = malloc(frame_size);
+  if (!frame)
+    return -1;
+
+  char *output = frame;
+  if (p_event->id)
+    output += sprintf(output, "id: %s\n", p_event->id);
+  if (p_event->event)
+    output += sprintf(output, "event: %s\n", p_event->event);
+  if (retry[0])
+  {
+    size_t retry_length = strlen(retry);
+    memcpy(output, retry, retry_length);
+    output += retry_length;
+  }
+  output = sse_append_multiline(output, "data:", p_event->data);
+  *output++ = '\n';
+
+  int32 result = sse_enqueue_frame_unlocked(
+      p_stream,
+      frame,
+      (uint32)(output - frame));
+  if (result == SEOBEO_SSE_BACKPRESSURE)
+    Dowa_Free(frame);
+  return result;
+}
+
+int32 Seobeo_SSE_Send(
+    Seobeo_SSE_Stream *p_stream,
+    const Seobeo_SSE_Event *p_event)
+{
+  pthread_mutex_lock(&g_sse_mutex);
+  int32 result = sse_send_event_unlocked(p_stream, p_event);
+  pthread_mutex_unlock(&g_sse_mutex);
+  return result;
+}
+
+int32 Seobeo_SSE_Send_Data(
+    Seobeo_SSE_Stream *p_stream,
+    const char *data)
+{
+  Seobeo_SSE_Event event = {
+    .data = data,
+    .retry_ms = SEOBEO_SSE_RETRY_NONE,
+  };
+  return Seobeo_SSE_Send(p_stream, &event);
+}
+
+int32 Seobeo_SSE_Send_Comment(
+    Seobeo_SSE_Stream *p_stream,
+    const char *comment)
+{
+  pthread_mutex_lock(&g_sse_mutex);
+  if (!sse_is_open_unlocked(p_stream))
+  {
+    pthread_mutex_unlock(&g_sse_mutex);
+    return -1;
+  }
+  size_t frame_size = sse_multiline_size(":", comment) + 1;
+  if (frame_size > p_stream->p_handle->write_buffer_capacity)
+  {
+    pthread_mutex_unlock(&g_sse_mutex);
+    return SEOBEO_SSE_EVENT_TOO_LARGE;
+  }
+  char *frame = malloc(frame_size);
+  if (!frame)
+  {
+    pthread_mutex_unlock(&g_sse_mutex);
+    return -1;
+  }
+  char *output = sse_append_multiline(frame, ":", comment);
+  *output++ = '\n';
+  int32 result = sse_enqueue_frame_unlocked(
+      p_stream,
+      frame,
+      (uint32)(output - frame));
+  if (result == SEOBEO_SSE_BACKPRESSURE)
+    Dowa_Free(frame);
+  pthread_mutex_unlock(&g_sse_mutex);
+  return result;
+}
+
+int32 Seobeo_SSE_Flush(Seobeo_SSE_Stream *p_stream)
+{
+  pthread_mutex_lock(&g_sse_mutex);
+  int32 result = sse_is_open_unlocked(p_stream)
+      ? sse_drain_pending_unlocked(p_stream)
+      : -1;
+  pthread_mutex_unlock(&g_sse_mutex);
+  return result;
+}
+
+boolean Seobeo_SSE_Is_Open(const Seobeo_SSE_Stream *p_stream)
+{
+  pthread_mutex_lock(&g_sse_mutex);
+  boolean open = sse_is_open_unlocked(p_stream);
+  pthread_mutex_unlock(&g_sse_mutex);
+  return open;
+}
+
+boolean Seobeo_SSE_Retain(Seobeo_SSE_Stream *p_stream)
+{
+  if (!p_stream)
+    return FALSE;
+  pthread_mutex_lock(&g_sse_mutex);
+  unsigned int references = atomic_load(&p_stream->ref_count);
+  if (references == 0)
+  {
+    pthread_mutex_unlock(&g_sse_mutex);
+    return FALSE;
+  }
+  atomic_fetch_add(&p_stream->ref_count, 1);
+  pthread_mutex_unlock(&g_sse_mutex);
+  return TRUE;
+}
+
+void Seobeo_SSE_Release(Seobeo_SSE_Stream *p_stream)
+{
+  if (!p_stream)
+    return;
+  pthread_mutex_lock(&g_sse_mutex);
+  sse_release_unlocked(p_stream);
+  pthread_mutex_unlock(&g_sse_mutex);
+}
+
+void Seobeo_SSE_Close(Seobeo_SSE_Stream *p_stream)
+{
+  if (!p_stream)
+    return;
+  pthread_mutex_lock(&g_sse_mutex);
+  if (p_stream->started && !p_stream->closed && p_stream->p_handle)
+  {
+    sse_drain_pending_unlocked(p_stream);
+    if (p_stream->managed)
+      shutdown(p_stream->p_handle->socket, SHUT_RDWR);
+  }
+  sse_clear_pending_unlocked(p_stream);
+  p_stream->closed = TRUE;
+  pthread_mutex_unlock(&g_sse_mutex);
+}
+
+Seobeo_SSE_Stream *Seobeo_SSE_Server_Attach(
+    Seobeo_Handle *p_handle,
+    const char *path)
+{
+  if (!p_handle || !path)
+    return NULL;
+  Seobeo_SSE_Stream *p_stream = malloc(sizeof(*p_stream));
+  if (!p_stream)
+    return NULL;
+  memset(p_stream, 0, sizeof(*p_stream));
+  p_stream->path = strdup(path);
+  if (!p_stream->path)
+  {
+    Dowa_Free(p_stream);
+    return NULL;
+  }
+  atomic_init(&p_stream->ref_count, 1);
+  if (Seobeo_SSE_Start(p_stream, p_handle) < 0)
+  {
+    Dowa_Free(p_stream->path);
+    Dowa_Free(p_stream);
+    return NULL;
+  }
+  p_stream->managed = TRUE;
+  p_handle->is_sse = TRUE;
+
+  pthread_mutex_lock(&g_sse_mutex);
+  Dowa_Array_Push(g_sse_streams, p_stream);
+  pthread_mutex_unlock(&g_sse_mutex);
+  return p_stream;
+}
+
+void Seobeo_SSE_Server_Detach_Handle(Seobeo_Handle *p_handle)
+{
+  if (!p_handle)
+    return;
+  p_handle->is_sse = FALSE;
+  pthread_mutex_lock(&g_sse_mutex);
+  size_t count = Dowa_Array_Length(g_sse_streams);
+  for (size_t i = 0; i < count; i++)
+  {
+    Seobeo_SSE_Stream *p_stream = g_sse_streams[i];
+    if (p_stream->p_handle != p_handle)
+      continue;
+    p_stream->closed = TRUE;
+    p_stream->p_handle = NULL;
+    sse_clear_pending_unlocked(p_stream);
+    g_sse_streams[i] = Dowa_Array_Pop(g_sse_streams);
+    sse_release_unlocked(p_stream);
+    break;
+  }
+  pthread_mutex_unlock(&g_sse_mutex);
+}
+
+void Seobeo_SSE_Server_Destroy(void)
+{
+  pthread_mutex_lock(&g_sse_mutex);
+  size_t count = Dowa_Array_Length(g_sse_streams);
+  for (size_t i = 0; i < count; i++)
+  {
+    Seobeo_SSE_Stream *p_stream = g_sse_streams[i];
+    if (!p_stream)
+      continue;
+    p_stream->closed = TRUE;
+    if (p_stream->p_handle)
+      p_stream->p_handle->is_sse = FALSE;
+    p_stream->p_handle = NULL;
+    sse_clear_pending_unlocked(p_stream);
+    sse_release_unlocked(p_stream);
+  }
+  Dowa_Array_Free(g_sse_streams);
+  pthread_mutex_unlock(&g_sse_mutex);
+}