freshen
Version, currently 0.2.01 version
- 0.2.0latestAug 26, 2026
gitlab.com/renich/freshen
High-performance, debounced Server-Sent Events (SSE) live-reload broker and client injection helper for Crystal web applications.
Installation
# Add this to your shard.yml
dependencies:
freshen:
gitlab: renich/freshen
version: ~> 0.2.0Then run:
shards installshard.yml
- Crystal
>= 1.21.0- License
- GPL-3.0-or-later
- Author
- RΓ©nich Bon ΔiriΔ
Dependencies
Development Dependencies
- ameba*github: crystal-ameba/ameba, branch: masterdev
README
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.
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.2.0- Tagged
- Aug 26, 2026
- Commit
bcedec4d1d35- Crystal
>= 1.21.0- Indexed
- yes
Dependents
Repository
gitlab.com/renich/freshen
Metadata
- Created
- Aug 26, 2026
- Updated
- Aug 26, 2026
- Synced
- Aug 26, 2026
- Versions
- 1