Class: Lilac::CLI::LiveReload
- Inherits:
-
Object
- Object
- Lilac::CLI::LiveReload
- Defined in:
- lib/lilac/cli/live_reload.rb
Overview
SSE pub/sub for the dev server's live-reload endpoint.
Each connected browser holds an open SSE response served by #call.
When notify_all fires (because the file watcher detected a build),
every subscriber receives a "reload" event and refreshes.
Subscribers are tracked as Queue instances; subscription cleanup
happens in an ensure so a closed/aborted client doesn't leak.
Constant Summary collapse
- ENDPOINT_PATH =
"/__lilac/livereload"- KEEPALIVE_INTERVAL =
:keepaliveis a SSE-comment frame: clients ignore it, but writing it lets us detect a dropped connection (the write raises Errno::EPIPE) when no real reload event has fired in a while. Short interval keeps dead subscribers from clogging the wsv connection-throttle pool (default cap 8) on rapid page reloads — without this we'd see 503s after ~8 reloads within 30 s. 5- SCRIPT =
Client snippet injected by the builder into every dev-server page. Subscribes to the dev server's SSE channel and handles two event types:
- default `message` event → successful rebuild, reload the page (any in-page error overlay is implicitly discarded by the reload, no explicit cleanup needed) - `error` event → build failed; render an in-page overlay with the error type + message so the dev sees the failure without switching to the terminalThe overlay is intentionally inline-styled and self-contained (
__lilac_err_overlay), no dependency on user styles. <<~HTML <script> // lilac dev: live reload + error overlay via SSE (function () { const ES_URL = "/__lilac/livereload"; const OVERLAY_ID = "__lilac_err_overlay"; function renderOverlay(payload) { document.getElementById(OVERLAY_ID)?.remove(); const root = document.createElement("div"); root.id = OVERLAY_ID; root.setAttribute("style", [ "position:fixed", "inset:0", "z-index:2147483647", "background:rgba(0,0,0,0.78)", "color:#fff", "font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace", "padding:32px", "overflow:auto", ].join(";")); const panel = document.createElement("div"); panel.setAttribute("style", [ "max-width:880px", "margin:0 auto", "background:#1f1f23", "border:1px solid #ff5b6c", "border-radius:8px", "padding:20px 24px", "box-shadow:0 12px 40px rgba(0,0,0,0.45)", ].join(";")); const head = document.createElement("div"); head.setAttribute("style", "display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px"); const title = document.createElement("strong"); title.textContent = "lilac dev: build failed"; title.setAttribute("style", "color:#ff5b6c;font-size:15px"); const close = document.createElement("button"); close.type = "button"; close.textContent = "×"; close.setAttribute("aria-label", "Dismiss"); close.setAttribute("style", "background:transparent;border:0;color:#fff;font-size:22px;cursor:pointer;line-height:1"); close.addEventListener("click", () => root.remove()); head.appendChild(title); head.appendChild(close); panel.appendChild(head); if (payload && payload.type) { const t = document.createElement("div"); t.textContent = payload.type; t.setAttribute("style", "color:#9aa0a6;font-size:12px;margin-bottom:8px"); panel.appendChild(t); } const msg = document.createElement("pre"); msg.textContent = (payload && payload.message) || "(no message)"; msg.setAttribute("style", "white-space:pre-wrap;word-break:break-word;margin:0;color:#f5f5f5"); panel.appendChild(msg); const hint = document.createElement("div"); hint.textContent = "Save the file to retry — this overlay will close automatically on a successful rebuild."; hint.setAttribute("style", "color:#9aa0a6;font-size:12px;margin-top:14px"); panel.appendChild(hint); root.appendChild(panel); document.body.appendChild(root); } const es = new EventSource(ES_URL); es.addEventListener("message", () => location.reload()); es.addEventListener("error", (ev) => { // EventSource fires "error" on transport failure too — those // have no `data` field. Distinguish from server-sent error // events by presence of `ev.data`. if (!ev.data) return; try { renderOverlay(JSON.parse(ev.data)); } catch (_e) { renderOverlay({ type: "(parse failure)", message: ev.data }); } }); })(); </script> HTML
- ERROR_MARKER =
Sentinel object used to tag error tuples in the queue without colliding with any plausible reload-message string.
Object.new.freeze
Instance Method Summary collapse
- #call(_request) ⇒ Object
-
#initialize ⇒ LiveReload
constructor
A new instance of LiveReload.
- #notify_all(message = "reload") ⇒ Object
-
#notify_error(payload) ⇒ Object
Push a
event: errorSSE frame to all subscribers with the given payload encoded as JSON. - #subscriber_count ⇒ Object
Constructor Details
#initialize ⇒ LiveReload
Returns a new instance of LiveReload.
118 119 120 121 |
# File 'lib/lilac/cli/live_reload.rb', line 118 def initialize @subscribers = [] @mutex = Mutex.new end |
Instance Method Details
#call(_request) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/lilac/cli/live_reload.rb', line 123 def call(_request) queue = subscribe Wsv::Response.sse do |io| io.write(":connected\n\n") io.flush serve_loop(queue, io) rescue Errno::EPIPE, IOError # Client disconnected mid-stream; producer just exits. ensure unsubscribe(queue) end end |
#notify_all(message = "reload") ⇒ Object
136 137 138 |
# File 'lib/lilac/cli/live_reload.rb', line 136 def notify_all( = "reload") @mutex.synchronize { @subscribers.each { |q| q << } } end |
#notify_error(payload) ⇒ Object
Push a event: error SSE frame to all subscribers with the
given payload encoded as JSON. The client overlay reads this
via addEventListener("error", ...) and renders an overlay.
A subsequent successful build calls notify_all("reload")
which reloads the page and the overlay disappears on its own.
145 146 147 148 149 150 |
# File 'lib/lilac/cli/live_reload.rb', line 145 def notify_error(payload) require "json" json = JSON.generate(payload) marker = [ERROR_MARKER, json] @mutex.synchronize { @subscribers.each { |q| q << marker } } end |
#subscriber_count ⇒ Object
152 153 154 |
# File 'lib/lilac/cli/live_reload.rb', line 152 def subscriber_count @mutex.synchronize { @subscribers.length } end |