freshen

High-performance, debounced Server-Sent Events (SSE) live-reload broker and automatic client injection helper for Crystal web applications.
Freshen Logo

High-Performance Debounced Server-Sent Events (SSE) Live-Reload Broker & Client Injector for Crystal

CI Status Crystal Version License: GPL-3.0 SHA-256 Repository Ameba Verified Liberapay Support

Donate using Liberapay


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 bounded max_deadline_ms to 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\n comments 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::Handler Middleware: Plug-and-play middleware integration for Kemal, Lucky, Amber, and standard Crystal HTTP::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.

Repository

freshen

Owner
Statistic
  • 0
  • 0
  • 0
  • 1
  • 1
  • about 3 hours ago
  • August 26, 2026
License

GNU General Public License v3.0 only

Links
Synced at

Wed, 26 Aug 2026 04:04:04 GMT

Languages