Claude-skill-registry clojure-sente
Realtime bidirectional communications between Clojure server and ClojureScript client. Use when building apps where BOTH client and server use sente - NOT for connecting to third-party WebSocket APIs. Provides server push, realtime updates, and reliable async messaging with automatic WebSocket/Ajax fallback.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/clojure-sente" ~/.claude/skills/majiayu000-claude-skill-registry-clojure-sente && rm -rf "$T"
skills/data/clojure-sente/SKILL.mdSente
Realtime web communications library for Clojure/Script using WebSockets with automatic Ajax fallback.
Sente provides bidirectional async communications between a Clojure server and ClojureScript browser client, with automatic protocol selection (WebSocket or long-polling), reconnection handling, and event batching. Send arbitrary Clojure values between client and server with efficient serialization.
NOTE: Sente is for communication between your own Clojure server and ClojureScript client - both sides must use sente. It is NOT a generic WebSocket client for connecting to third-party WebSocket APIs. For connecting to external WebSocket servers, use hato, http-kit client, or gniazdo instead.
Setup
deps.edn:
com.taoensso/sente {:mvn/version "1.21.0"}
Leiningen:
[com.taoensso/sente "1.21.0"]
See https://clojars.org/com.taoensso/sente for the latest version.
Quick Start
Server (Clojure):
(ns my-app.server (:require [taoensso.sente :as sente] [taoensso.sente.server-adapters.http-kit :refer [get-sch-adapter]] [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] [compojure.core :refer [defroutes GET POST]])) ;; Create channel socket server (let [{:keys [ch-recv send-fn connected-uids ajax-post-fn ajax-get-or-ws-handshake-fn]} (sente/make-channel-socket-server! (get-sch-adapter) {})] (def ring-ajax-post ajax-post-fn) (def ring-ajax-get-or-ws-handshake ajax-get-or-ws-handshake-fn) (def ch-chsk ch-recv) ; Receive channel (def chsk-send! send-fn) ; Send function (def connected-uids connected-uids)) ; Atom of connected users ;; Add routes for channel socket (defroutes my-routes (GET "/chsk" req (ring-ajax-get-or-ws-handshake req)) (POST "/chsk" req (ring-ajax-post req))) ;; Wrap with necessary middleware (def app (-> my-routes wrap-keyword-params wrap-params wrap-anti-forgery ; Important for security wrap-session))
Client (ClojureScript):
(ns my-app.client (:require [taoensso.sente :as sente :refer [cb-success?]])) ;; Get CSRF token from page (def ?csrf-token (when-let [el (.getElementById js/document "sente-csrf-token")] (.getAttribute el "data-csrf-token"))) ;; Create channel socket client (let [{:keys [chsk ch-recv send-fn state]} (sente/make-channel-socket-client! "/chsk" ; Must match server route ?csrf-token {:type :auto})] ; :auto, :ws, or :ajax (def chsk chsk) (def ch-chsk ch-recv) ; Receive channel (def chsk-send! send-fn) ; Send function (def chsk-state state)) ; Watchable state atom
HTML (include CSRF token):
;; In your Hiccup/HTML template (let [csrf-token (force ring.middleware.anti-forgery/*anti-forgery-token*)] [:div#sente-csrf-token {:data-csrf-token csrf-token}])
Core Concepts
Event - Messages have the form
[event-id event-data]:
[:my-app/some-event {:data "value"}]
Event-msg - Events arrive wrapped in maps with metadata:
;; Server event-msg {:event [:my-app/some-event {:data "value"}] :id :my-app/some-event :?data {:data "value"} :ring-req {...} ; Ring request map :?reply-fn (fn [reply]) ; Present when client requested reply :uid "user-123" ; User ID :client-id "..."} ; Specific client/tab ;; Client event-msg {:event [:my-app/some-event {:data "value"}] :id :my-app/some-event :?data {:data "value"} :send-fn chsk-send!}
User vs Client - Sente distinguishes between users and clients:
- User ID: Persistent identity (can survive across sessions/devices)
- Client ID: Specific browser tab/connection
- One user can have multiple connected clients
- Server push targets user IDs, not individual clients
Sending Events
Client to Server (with optional reply):
;; Fire and forget (chsk-send! [:my-app/request {:user-input "data"}]) ;; With callback for reply (chsk-send! [:my-app/request {:user-input "data"}] 5000 ; Timeout in ms (fn [reply] (if (sente/cb-success? reply) ; Check for :chsk/closed, :chsk/timeout, :chsk/error (println "Success:" reply) (println "Failed"))))
Server to User (push):
;; Send to all clients of a specific user (chsk-send! "user-id" [:my-app/notification {:msg "New update!"}]) ;; Send returns true if at least one client received the message (when-not (chsk-send! user-id event) (println "User not connected"))
Server Reply to Client Request:
;; In your event handler on server (defn handle-request [{:keys [?reply-fn ?data]}] (when ?reply-fn (?reply-fn {:status :ok :result "processed"})))
Event Routing
Use a multimethod to dispatch events by ID:
Client:
(defmulti -event-msg-handler :id) (defmethod -event-msg-handler :default [{:keys [event]}] (println "Unhandled event:" event)) (defmethod -event-msg-handler :chsk/state [{:keys [?data]}] (let [[old-state new-state] ?data] (if (:first-open? new-state) (println "Channel socket successfully established!") (println "Channel socket state change:" new-state)))) (defmethod -event-msg-handler :my-app/notification [{:keys [?data]}] (println "Received notification:" ?data)) ;; Start event router (defonce router (sente/start-client-chsk-router! ch-chsk -event-msg-handler))
Server:
(defmulti -event-msg-handler :id) (defmethod -event-msg-handler :default [{:keys [event]}] (println "Unhandled event:" event)) (defmethod -event-msg-handler :my-app/request [{:keys [?data ?reply-fn uid ring-req]}] (println "Request from user" uid ":" ?data) (when ?reply-fn (?reply-fn {:status :ok}))) ;; Start event router (defonce router (sente/start-server-chsk-router! ch-chsk -event-msg-handler))
User Identity
Set user ID in one of two ways:
- Via Ring session (most common):
;; In your login handler {:status 200 :session (assoc session :uid "user-123")}
- Via custom user-id-fn:
(sente/make-channel-socket-server! (get-sch-adapter) {:user-id-fn (fn [ring-req] (get-in ring-req [:params :user-id]))})
For anonymous/per-session users, use a random UUID:
{:session (assoc session :uid (str (java.util.UUID/randomUUID)))}
Connected Users
Watch the connected-uids atom to track who's online:
;; Server-side (add-watch connected-uids :watcher (fn [_ _ old-state new-state] (when (not= old-state new-state) (println "Connected users changed:") (println " WebSocket:" (:ws new-state)) ; Set of user IDs (println " Ajax:" (:ajax new-state)) ; Set of user IDs (println " All:" (:any new-state))))) ; Set of all user IDs
Channel Socket State
Client-side state changes trigger
:chsk/state events:
;; Client receives [:chsk/state [old-state new-state]] events (defmethod -event-msg-handler :chsk/state [{:keys [?data]}] (let [[old new] ?data] (if (:first-open? new) (println "Connected!") (when (not= (:open? old) (:open? new)) (if (:open? new) (println "Reconnected") (println "Disconnected"))))))
State map keys:
- Is connection currently open?:open?
- First successful connection?:first-open?
- Has ever connected successfully?:ever-opened?
- Current protocol (:type
or:ws
):ajax
- User ID from server:uid
- CSRF token from server:csrf-token
Common Patterns
Broadcast to all connected users:
;; Server (doseq [uid (:any @connected-uids)] (chsk-send! uid [:my-app/broadcast {:msg "System announcement"}]))
Request/response with timeout:
;; Client (chsk-send! [:my-app/fetch-data {:id 123}] 3000 (fn [reply] (if (sente/cb-success? reply) (update-ui! reply) (show-error! "Request timed out"))))
Wait for connection before sending:
;; Client - wait for first connection (defonce connected? (atom false)) (defmethod -event-msg-handler :chsk/state [{:keys [?data]}] (when (:first-open? (second ?data)) (reset! connected? true) (chsk-send! [:my-app/init {}])))
Lifecycle management with component:
;; Both start-client-chsk-router! and start-server-chsk-router! ;; return a (fn stop []) for cleanup (defonce router (sente/start-server-chsk-router! ch-chsk event-msg-handler)) ;; Later, on shutdown: (router) ; Stops the router
Server Adapters
Sente supports multiple web servers via adapters:
;; http-kit [taoensso.sente.server-adapters.http-kit :refer [get-sch-adapter]] ;; Immutant [taoensso.sente.server-adapters.immutant :refer [get-sch-adapter]] ;; Aleph [taoensso.sente.server-adapters.aleph :refer [get-sch-adapter]] ;; nginx-clojure [taoensso.sente.server-adapters.nginx-clojure :refer [get-sch-adapter]] ;; Jetty 9+ [taoensso.sente.server-adapters.jetty9 :refer [get-sch-adapter]]
All use the same
(get-sch-adapter) function.
Serialization (Packers)
Sente uses "packers" for serialization. Default is edn:
;; Using Transit (recommended for performance + binary data) (require '[taoensso.sente.packers.transit :as sente-transit]) (sente/make-channel-socket-server! (get-sch-adapter) {:packer (sente-transit/get-packer :json)}) ; or :msgpack ;; Custom Transit handlers (e.g., for Joda Time) (def packer (sente-transit/->TransitPacker :json {:handlers {org.joda.time.DateTime my-write-handler}} {:handlers {"m" my-read-handler}}))
Client and server must use the same packer.
Gotchas / Caveats
Event ordering - Sente does NOT guarantee event ordering. Events may arrive out of order due to buffering, async serialization, etc. Don't depend on ordering.
Large payloads - Do NOT use Sente for payloads > 1MB:
- WebSocket connections will bottleneck
- Large transfers can cause client disconnects
- Instead: Use Sente for signaling, make large transfers via Ajax
- Client->Server: Client requests large data via Ajax
- Server->Client: Server signals client to fetch data via Ajax
Security requirements:
- ALWAYS use CSRF protection (ring-anti-forgery or ring-defaults)
- ALWAYS protect the POST endpoint (ajax-post-fn)
- Use HTTPS in production (automatic for WebSockets = WSS)
User ID for push - Server push requires a user ID. Client->server requests don't need one, but server->client push does. Set via Ring session
:uid key or custom :user-id-fn.
Session modification - WebSocket events use the INITIAL handshake request's session. To modify sessions (login/logout), use regular HTTP Ajax, not WebSocket events.
Router lifecycle - The router functions return a
stop function. Call it on shutdown to clean up (though the cost of not doing so is minimal - just a parked go thread).
Advanced Topics
For these features, consult the official documentation:
- Custom event batching and buffering
- Debugging connections at protocol level
- Testing strategies
- Performance tuning
- Alternative server adapters
- Component/lifecycle integration libraries
References
- API Docs: https://cljdoc.org/d/com.taoensso/sente/
- GitHub: https://github.com/taoensso/sente
- Wiki: https://github.com/taoensso/sente/wiki
- Example Projects: https://github.com/taoensso/sente/wiki/3-Example-projects
- Getting Started: https://github.com/taoensso/sente/wiki/1-Getting-started
- FAQ: https://github.com/taoensso/sente/wiki/3-FAQ