From cf014114815cdfbfd54a6eea5d2e0c08589295f3 Mon Sep 17 00:00:00 2001 From: Marcos Benevides Date: Sat, 6 Jun 2026 06:49:40 -0400 Subject: [PATCH] fix: add heartbeats to close dead connections, also improve live reload script --- src/blogatto/internal/dev/message.gleam | 3 ++ src/blogatto/internal/dev/web_server.gleam | 37 +++++++++++++++------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/blogatto/internal/dev/message.gleam b/src/blogatto/internal/dev/message.gleam index db5307e..356fcc9 100644 --- a/src/blogatto/internal/dev/message.gleam +++ b/src/blogatto/internal/dev/message.gleam @@ -15,4 +15,7 @@ pub type RebuildMessage { /// Message type for server-sent events to the client. pub type SseMessage { Reload + /// Periodic self-message used to probe the SSE socket so dead connections + /// (clients that disconnected) can be detected and reaped. + Heartbeat } diff --git a/src/blogatto/internal/dev/web_server.gleam b/src/blogatto/internal/dev/web_server.gleam index bfcc3ef..b94b7c0 100644 --- a/src/blogatto/internal/dev/web_server.gleam +++ b/src/blogatto/internal/dev/web_server.gleam @@ -66,25 +66,25 @@ fn handle_request( } } +/// Interval between SSE heartbeat probes, used to detect dead client +/// connections. +const sse_heartbeat_interval: Int = 15_000 + fn sse_init( rebuild_subject: Subject(message.RebuildMessage), -) -> fn(Subject(message.SseMessage)) -> - Result(actor.Initialised(Nil, message.SseMessage, Nil), String) { +) -> fn(Subject(message.SseMessage)) -> Subject(message.SseMessage) { fn(subject) { rebuild_actor.register_sse_client(rebuild_subject, subject) - actor.initialised(Nil) - |> Ok + process.send_after(subject, sse_heartbeat_interval, message.Heartbeat) + subject } } fn sse_loop( - state: Result(actor.Initialised(Nil, message.SseMessage, Nil), String), + subject: Subject(message.SseMessage), msg: message.SseMessage, conn: mist.SSEConnection, -) -> actor.Next( - Result(actor.Initialised(Nil, message.SseMessage, Nil), String), - message.SseMessage, -) { +) -> actor.Next(Subject(message.SseMessage), message.SseMessage) { case msg { message.Reload -> { let event = @@ -92,7 +92,22 @@ fn sse_loop( |> mist.event |> mist.event_name("reload") case mist.send_event(conn, event) { - Ok(_) -> actor.continue(state) + Ok(_) -> actor.continue(subject) + Error(_) -> actor.stop() + } + } + message.Heartbeat -> { + // Probe the socket, a failed write means the client disconnected, which + // stops the actor. + let ping = + string_tree.from_string("ping") + |> mist.event + |> mist.event_name("ping") + case mist.send_event(conn, ping) { + Ok(_) -> { + process.send_after(subject, sse_heartbeat_interval, message.Heartbeat) + actor.continue(subject) + } Error(_) -> actor.stop() } } @@ -137,7 +152,7 @@ fn serve_page( } } -pub const live_reload_script = "" +pub const live_reload_script = "" /// Append the live reload script to HTML content. When `live_reload` is /// `True`, the script is injected just before ``. If no closing body