freshen
High-Performance Debounced Server-Sent Events (SSE) Live-Reload Broker & Client Injector for Crystal
Overview
freshen is a lightweight, zero-dependency Server-Sent Events (SSE) live-reload broker designed for Crystal web applications, static site generators, and development servers.
It synchronizes filesystem updates with connected browser sessions in real-time, eliminating manual browser refreshes without requiring external Node.js build tools, Webpack plugins, or browser extensions.
Key Features
- β‘ Debounced Event Coalescing: Batches multiple rapid filesystem events within a sliding debounce window (
debounce_ms) with a boundedmax_deadline_msto eliminate reload storms and event starvation. - π Lockless Concurrent Broadcasts: Dispatches SSE events to connected client streams concurrently without holding global mutexes during network I/O, preventing slow sockets from blocking other clients.
- π§Ή Automatic Dead Stream Reaping: Prunes disconnected browser tabs and severed TCP sockets automatically during broadcasts without throwing unhandled exceptions.
- π Automated Keep-Alive Heartbeats: Emits periodic
: ping\n\ncomments to prevent reverse proxies (Nginx, Caddy) and browser timeouts from dropping idle streams. - π¨ CSS Hot-Reloading: Supports instantaneous
<link rel="stylesheet">swapping without destructive full-page reloads. - π‘οΈ XSS-Safe & CSP-Ready: Safely escapes script attributes, JSON-encodes endpoints, and supports CSP nonces (
nonce="..."). - π Standard
HTTP::HandlerMiddleware: Plug-and-play middleware integration for Kemal, Lucky, Amber, and standard CrystalHTTP::Server.
Architecture
ββββββββββββββββββββββββ
β Filesystem Watcher β
ββββββββββββ¬ββββββββββββ
β trigger(path)
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Freshen::Debouncer β
β - Sliding window debounce (default: 50ms) β
β - Anti-starvation max deadline (default: 250ms) β
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ
β on_coalesce([paths])
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Freshen::Broker β
β - Pub/Sub registry with per-stream write mutex β
β - Non-blocking concurrent broadcasts & dead reaping β
β - Background heartbeat (: ping\n\n) β
ββββββββββββ¬ββββββββββββββββββββββββββββββββββ¬ββββββββββββ
β SSE (GET /_freshen/reload) β
βΌ βΌ
βββββββββββββββββββ βββββββββββββββββββ
β Browser Tab #1 β β Browser Tab #2 β
β (EventSource) β β (CSS Hot-Reload)β
βββββββββββββββββββ βββββββββββββββββββ
Installation
Add freshen to your shard.yml:
dependencies:
freshen:
gitlab: renich/freshen
version: ~> 0.2.0
Run shards install.
Quickstart
Option A: Using Freshen::Handler Middleware (Kemal / HTTP::Server)
require "kemal"
require "freshen"
# 1. Initialize broker & debouncer
broker = Freshen::Broker.new
debouncer = Freshen::Debouncer(String).new(50.milliseconds) do |_paths|
broker.broadcast(Freshen::Event.new(name: "reload", data: "reload"))
end
# 2. Add standard HTTP::Handler middleware
add_handler Freshen::Handler.new(broker, endpoint: "/_freshen/reload")
# 3. Inject reload script into HTML responses
get "/" do |env|
html = "<html><body><h1>Hello World</h1></body></html>"
Freshen::Script.inject(html, endpoint: "/_freshen/reload")
end
# 4. Trigger reload on file changes
spawn do
# In your file watcher callback:
debouncer.trigger("src/views/index.ecr")
end
Kemal.run
Option B: Using Freshen::Hub (Facade)
require "kemal"
require "freshen"
hub = Freshen::Hub.new(debounce_ms: 50, max_deadline_ms: 250)
# 1. Mount SSE reload endpoint
get "/_freshen/reload" do |env|
Kemal::EventStream.serve(env) do |stream, _context|
client_id = hub.subscribe(stream.response)
begin
loop { sleep 1.hour }
rescue IO::Error
ensure
hub.unregister_client(client_id)
end
end
end
# 2. Inject client script into HTML responses
get "/" do |env|
html = "<html><body><h1>Welcome</h1></body></html>"
Freshen::Hub.inject_script(html, endpoint: "/_freshen/reload")
end
# 3. Trigger reload on file changes
spawn do
hub.enqueue_change("src/views/index.ecr")
end
Kemal.run
API Reference
Core Components
| Component | Responsibility | Key Methods |
|---|---|---|
Freshen::Debouncer(T) |
Generic sliding-window event coalescer | trigger(item : T), close |
Freshen::Broker |
Pub/Sub SSE connection manager | subscribe(io), unregister(id), broadcast(event), close |
Freshen::Event |
Wire-format SSE event record | Event.new(name, data, id, retry_ms).to_sse |
Freshen::Script |
HTML injection & JavaScript asset synthesis | Script.inject(html, ...), Script.generate(...) |
Freshen::Handler |
Crystal HTTP::Handler middleware |
Handler.new(broker, endpoint) |
Freshen::Hub |
Backward-compatible facade wrapping all components | new(debounce_ms, max_deadline_ms), register_client, enqueue_change, broadcast_reload |
Development & Testing
make spec
make lint
make format
License
GNU General Public License v3.0 or later (GPL-3.0-or-later).
Copyright Β© 2026 RΓ©nich Bon ΔiriΔ and Contributors.
freshen
- 0
- 0
- 0
- 1
- 1
- about 3 hours ago
- August 26, 2026
GNU General Public License v3.0 only
Wed, 26 Aug 2026 04:04:04 GMT