Claude Code Plugins

Community-maintained marketplace

Feedback
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name clojure-sente
description 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.

Sente

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:

  1. Via Ring session (most common):
;; In your login handler
{:status 200
 :session (assoc session :uid "user-123")}
  1. 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:

  • :open? - Is connection currently open?
  • :first-open? - First successful connection?
  • :ever-opened? - Has ever connected successfully?
  • :type - Current protocol (:ws or :ajax)
  • :uid - User ID from server
  • :csrf-token - CSRF token from server

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