WebSocket echo
Upgrade an ordinary route, connect with a native client and echo one message.
Build and run
Requires Java 21+, Clojure CLI and the runtime named below. The example pins the compiler that supports native ^:async and await. Run these commands in a separate terminal from other examples.
git clone https://github.com/OUTCASTGEEK-TECH/fast-twitch.git
cd fast-twitch
git checkout a4eb73f96836d034c7a24fe09dc7d51e4c581f64
cd examples/07-native-transports
clojure -M:build websocket
bun target/websocket.cjs
# Alternatively: deno run -A target/websocket.cjs
Expected result
Use Bun or Deno. Successful completion prints Native WebSocket through existing routes and serve passed. On Node the example prints its runtime-support message and does not create an echo server: Node's adapter does not implement a native server upgrade.
Follow the message
/socketreturns:ring.websocket/listenerwith an:on-messagefunction.server/listen!creates the HTTP listener;:on-listenresolves its actual port.client/connect!uses:transport :websocketand resolves when the socket opens.client/send!sends"hello". The server callback receives[connection value]and sends that value back.- The client callback resolves the reply Promise. The assertion compares it with
"hello". finallycloses the client connection and awaits HTTP listener shutdown.
The connection-attempt abort signal does not provide a session deadline after open. Give long-lived connections their own lifecycle. Messages are event-driven; receive! is a TCP collector, not a WebSocket receive operation.
Exercises
Return a different string from the server's message callback. Add :on-close with [connection code reason] to observe shutdown. Use client/close! with a valid close code and short reason. Keep the codec native: JSON values need explicit application serialization for WebSocket messages.
WebSocket options and callbacks · WebSocket routes.
Complete source and build files
The files below are copied without abbreviation from Fast-Twitch revision a4eb73f96836d034c7a24fe09dc7d51e4c581f64. The walkthrough and API reference use the same revision.
Source: examples/07-native-transports/src/native_demo/websocket.cljs
View this exact file on GitHub.
(ns native-demo.websocket
(:require [cljs.core :refer [await]]
[fast-twitch.routing :as routing]
[fast-twitch.client.core :as client]
[fast-twitch.server.core :as server])
(:refer-global :only [Promise console globalThis]))
(defn ^:async -main
[]
(if-not (or (aget globalThis "Deno") (aget globalThis "Bun"))
(.log console "Native server upgrade is available in the Deno and Bun profiles")
(let [listened (atom nil)
ready (Promise. (fn [resolve _]
(reset! listened resolve)))
handler (routing/routes [{:pattern "/socket"
:handler (fn [_]
{:ring.websocket/listener
{:on-message (fn [connection value]
(client/send!
connection
value))}})}]
(fn [_]
(routing/not-found "missing")))
serving (server/listen! handler {:port 0 :on-listen @listened})
connection (atom nil)]
(try (let [received (atom nil)
reply (Promise. (fn [resolve _]
(reset! received resolve)))
url (str "ws://127.0.0.1:" (.-port (await ready)) "/socket")]
(reset! connection (await (client/connect!
{:url url}
{:transport :websocket
:listener {:on-message (fn [_ value]
(@received value))}})))
(client/send! @connection "hello")
(assert (= "hello" (await reply)))
(.log console "Native WebSocket through existing routes and serve passed"))
(finally (when @connection (client/close! @connection))
(await (server/close! serving {:force true})))))))
(set! *main-cli-fn* -main)
Source: examples/07-native-transports/deps.edn
View this exact file on GitHub.
{:aliases {:build {:main-opts ["-m" "native-demo.build"]}}
:deps {fast-twitch/fast-twitch {:local/root "../.."}
org.clojure/clojurescript {:git/sha "56a94ac13bfac22108e4d868dfd4763edaceb864"
:git/url "https://github.com/clojure/clojurescript"}}
:paths ["src" "dev"]}
Source: examples/07-native-transports/dev/native_demo/build.clj
View this exact file on GitHub.
(ns native-demo.build
(:require [cljs.build.api :as cljs]))
(def modes
#{"http" "websocket" "sse" "streams" "ports" "tcp-node" "tcp-deno" "tcp-bun"})
(defn -main
[mode]
(doseq [mode (if (= mode "all") (sort modes) [mode])]
(when-not (modes mode) (throw (ex-info "Choose an example" {:modes modes})))
(cljs/build "src"
{:main (symbol (str "native-demo." mode))
:target :nodejs
:output-to (str "target/" mode ".cjs")
:output-dir (str "target/" mode)
:optimizations :simple})
(println mode "COMPILED")))