Examples

TCP across Node, Bun & Deno

Understand EOF-based byte exchange, half-close, bounded reads and both sides of connection cleanup.

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 tcp-node
node target/tcp-node.cjs

Other runtime builds

clojure -M:build tcp-bun
bun target/tcp-bun.cjs
clojure -M:build tcp-deno
deno run -A target/tcp-deno.cjs

All three entry namespaces delegate to native-demo.tcp/run! with a runtime keyword. Successful completion prints Configured client/server TCP loopback passed.

Follow the exchange

  1. Configuration selects :transport :tcp, the runtime, :codec :bytes and a 32-byte collection limit.
  2. server/listen! binds to loopback on port 0 and returns a listener map containing its actual :port.
  3. The server's connection callback receives a configured peer and starts asynchronous reading through EOF.
  4. The client connects to that port, sends [4 5 6] and awaits the send.
  5. The client half-closes its writes. The server's receive! now sees EOF, sends the collected bytes back and half-closes its writes.
  6. The client's receive! reaches EOF and returns the reply as a Uint8Array.
  7. finally closes the outbound client, accepted server peer and listener.

Why half-close matters

TCP is a byte stream without application message boundaries. A peer waiting for EOF cannot return its collected result while the sender keeps its write side open. Half-close ends writes while allowing the reply to be read; closing the whole socket too early can prevent reading that reply. Send completion means local write completion, not confirmed receipt by the peer.

Exercises and limitations

Change the payload and assertion together. Send more than 32 bytes to exercise the configured collection bound, adding explicit error handling so failure is visible on both peers. A repeated request protocol needs framing, such as length-prefixing or delimiters; this EOF exchange does not provide it. Runtime capability profiles do not complete TLS qualification, so this loopback example makes no cross-runtime TLS claim.

TCP options · connect! API · half-close! API.

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/tcp.cljs

View this exact file on GitHub.

(ns native-demo.tcp
  (:refer-clojure :exclude [run!])
  (:require [cljs.core :refer [await]]
            [fast-twitch.client.core :as client]
            [fast-twitch.server.core :as server])
  (:refer-global :only [Uint8Array console]))

(defn ^:async run!
  [runtime]
  (let [accepted (atom nil)
        configuration
          {:transport :tcp :runtime runtime :codec :bytes :streaming {:max-bytes 32}}
        listening (await (server/listen! (fn [connection]
                                           (reset! accepted connection)
                                           ((^:async fn
                                             []
                                             (try (await (client/send! connection
                                                                       (await
                                                                         (client/receive!
                                                                           connection))))
                                                  (await (client/half-close! connection))
                                                  (catch :default _ nil)))))
                                         (assoc configuration
                                           :hostname "127.0.0.1"
                                           :port 0)))
        connection (await (client/connect! {:hostname "127.0.0.1" :port (:port listening)}
                                           configuration))]
    (try (await (client/send! connection (Uint8Array. #js [4 5 6])))
         (await (client/half-close! connection))
         (assert (= [4 5 6] (vec (await (client/receive! connection)))))
         (.log console "Configured client/server TCP loopback passed")
         (finally (client/close! connection)
                  (when @accepted (server/close! @accepted))
                  (await (server/close! listening))))))

Source: examples/07-native-transports/src/native_demo/tcp_node.cljs

View this exact file on GitHub.

(ns native-demo.tcp-node
  (:require [native-demo.tcp :as demo]))

(defn -main
  []
  (demo/run! :node))

(set! *main-cli-fn* -main)

Source: examples/07-native-transports/src/native_demo/tcp_bun.cljs

View this exact file on GitHub.

(ns native-demo.tcp-bun
  (:require [native-demo.tcp :as demo]))

(defn -main
  []
  (demo/run! :bun))

(set! *main-cli-fn* -main)

Source: examples/07-native-transports/src/native_demo/tcp_deno.cljs

View this exact file on GitHub.

(ns native-demo.tcp-deno
  (:require [native-demo.tcp :as demo]))

(defn -main
  []
  (demo/run! :deno))

(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")))
Source revision a4eb73f96836d034c7a24fe09dc7d51e4c581f64

Built with Fast-Twitch · Integrant · Datastar

Search documentation

Search pages and API symbols

Start hereWelcome to Fast-TwitchRing HTTP applications and configured native clients and transports in ClojureScript.Start hereGetting startedCompile and run your first Fast-Twitch server.Core conceptsRequest & responseUnderstand the maps at the center of a Fast-Twitch application.Core conceptsRoutingMatch URL patterns and compose handlers.Core conceptsMiddlewareWrap handlers with focused request and response behavior.GuidesHTML & IntegrantCompose a UI system with Integrant and render Hiccup.