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 `