init commit

This commit is contained in:
2026-03-09 23:09:46 -04:00
parent 5cbc493cc5
commit 5da77e3360
73 changed files with 9935 additions and 103 deletions
+142
View File
@@ -0,0 +1,142 @@
;;; ChatRoom — process-based chat room using spawn/send/receive
;;;
;;; This is a single-VM demo showing BEAM concurrency primitives.
;;; Run with: mix clje.run examples/chat_room.clje
;;;
;;; For a multi-terminal chat experience, see:
;;; examples/tcp_chat_server.clje and examples/tcp_chat_client.clje
(ns ChatRoom
(:require [clje.core :refer :all]
[Enum] [IO] [Map] [Process] [String]))
;; ── Room loop ─────────────────────────────────────────────────────
;; Manages members map of {username → pid} and broadcasts messages.
(defn run-loop [state]
(receive
[:join username pid]
(let [members (assoc (:members state) username pid)]
(send pid #el[:welcome username (count members)])
;; Notify existing members
(doseq [[_name member-pid] (:members state)]
(send member-pid #el[:system (str username " joined the room")]))
(recur (assoc state :members members)))
[:message from body] :guard [(is-binary body)]
(do
(doseq [[_name pid] (:members state)]
(send pid #el[:chat from body]))
(recur state))
[:leave username]
(let [new-members (dissoc (:members state) username)]
(doseq [[_name pid] new-members]
(send pid #el[:system (str username " left the room")]))
(recur (assoc state :members new-members)))
[:who reply-pid]
(do
(send reply-pid #el[:members (Map/keys (:members state))])
(recur state))
:shutdown
(do
(doseq [[_name pid] (:members state)]
(send pid :room-closed))
:ok)
:after 5000
(if (== (count (:members state)) 0)
:empty-timeout
(recur state))))
;; ── User listener ─────────────────────────────────────────────────
;; Collects messages received by a user and prints them.
(defn listen [username messages]
(receive
[:welcome _name member-count]
(do
(println (str " [" username " sees: Welcome! " member-count " user(s) here]"))
(ChatRoom/listen username messages))
[:chat from body]
(do
(println (str " [" username " sees: " from "> " body "]"))
(ChatRoom/listen username (cons #el[from body] messages)))
[:system text]
(do
(println (str " [" username " sees: * " text "]"))
(ChatRoom/listen username messages))
:room-closed
(println (str " [" username " sees: room closed]"))
:dump
messages
:after 2000
(do
(println (str " [" username " done listening]"))
messages)))
;; ── Run the demo ────────────────────────────────────────────────────
(println "=== ChatRoom Demo ===\n")
;; Start the room
(let [room (spawn (fn [] (ChatRoom/run-loop {:owner "system" :members {}})))
alice-listener (spawn (fn [] (ChatRoom/listen "alice" (list))))
bob-listener (spawn (fn [] (ChatRoom/listen "bob" (list))))
carol-listener (spawn (fn [] (ChatRoom/listen "carol" (list))))]
;; Alice joins
(println "Alice joins...")
(send room #el[:join "alice" alice-listener])
(Process/sleep 100)
;; Bob joins
(println "\nBob joins...")
(send room #el[:join "bob" bob-listener])
(Process/sleep 100)
;; Alice sends a message
(println "\nAlice sends a message...")
(send room #el[:message "alice" "Hello everyone!"])
(Process/sleep 100)
;; Carol joins
(println "\nCarol joins...")
(send room #el[:join "carol" carol-listener])
(Process/sleep 100)
;; Bob sends a message
(println "\nBob sends a message...")
(send room #el[:message "bob" "Hey Alice! Welcome Carol!"])
(Process/sleep 100)
;; Carol sends a message
(println "\nCarol sends a message...")
(send room #el[:message "carol" "Thanks Bob!"])
(Process/sleep 100)
;; Check who's online
(println "\nWho's online?")
(send room #el[:who *self*])
(receive
[:members names]
(println (str " Online: " (Enum/join names ", "))))
;; Bob leaves
(println "\nBob leaves...")
(send room #el[:leave "bob"])
(Process/sleep 100)
;; Shutdown the room
(println "\nShutting down room...")
(send room :shutdown)
(Process/sleep 200)
(println "\n=== Demo complete ==="))
+130
View File
@@ -0,0 +1,130 @@
;;; TCP Chat Client — connects to the TCP chat server
;;;
;;; Usage:
;;; mix clje.run --no-halt examples/tcp_chat_client.clje -- <username>
;;;
;;; The server must be running first:
;;; mix clje.run --no-halt examples/tcp_chat_server.clje
;;;
;;; Commands:
;;; Type a message and press Enter to send
;;; /who — list online users
;;; /quit — disconnect and exit
(ns TcpChatClient
(:require [clje.core :refer :all]
[IO] [String] [System] [erlang] [gen_tcp]))
;; ── Receiver process ──────────────────────────────────────────────
;; Listens for TCP messages from the server and prints them.
(defn receiver [socket parent]
(receive
[:tcp _sock data]
(let [line (String/trim data)]
(cond
(String/starts-with? line "MSG:")
(let [rest (String/slice line 4 (String/length line))
parts (String/split rest ":" (list #el[:parts 2]))
from (hd parts)
text (hd (tl parts))]
(IO/puts (str from "> " text))
(TcpChatClient/receiver socket parent))
(String/starts-with? line "SYS:")
(do
(IO/puts (str "* " (String/slice line 4 (String/length line))))
(TcpChatClient/receiver socket parent))
(String/starts-with? line "JOIN:")
(do
(IO/puts (str "* " (String/slice line 5 (String/length line)) " joined"))
(TcpChatClient/receiver socket parent))
(String/starts-with? line "QUIT:")
(do
(IO/puts (str "* " (String/slice line 5 (String/length line)) " left"))
(TcpChatClient/receiver socket parent))
:else
(do
(IO/puts line)
(TcpChatClient/receiver socket parent))))
[:tcp_closed _sock]
(do
(IO/puts "* Connection closed by server.")
(System/halt 0))
[:tcp_error _sock _reason]
(do
(IO/puts "* Connection error.")
(System/halt 1))))
;; ── Input loop ────────────────────────────────────────────────────
;; Reads from stdin and sends to the server.
(defn send-line [socket line]
(gen_tcp/send socket line))
(defn input-loop [socket]
(let [line (IO/gets "")]
(cond
(== line :eof)
(do
(TcpChatClient/send-line socket "QUIT\n")
(gen_tcp/close socket)
(System/halt 0))
:else
(let [trimmed (String/trim line)]
(cond
(== trimmed "/quit")
(do
(TcpChatClient/send-line socket "QUIT\n")
(gen_tcp/close socket)
(IO/puts "Goodbye!")
(System/halt 0))
(== trimmed "")
(TcpChatClient/input-loop socket)
:else
(do
(TcpChatClient/send-line socket (str "MSG:" trimmed "\n"))
(TcpChatClient/input-loop socket)))))))
;; ── Connect ───────────────────────────────────────────────────────
(defn start [username]
(case (gen_tcp/connect (erlang/binary-to-list "127.0.0.1") 4040
(list :binary #el[:active true] #el[:packet :line]))
[:ok socket]
(do
;; Send JOIN
(TcpChatClient/send-line socket (str "JOIN:" username "\n"))
;; Spawn receiver and hand it the socket
(let [me *self*
recv-pid (spawn (fn [] (TcpChatClient/receiver socket me)))]
(gen_tcp/controlling-process socket recv-pid)
;; Run input loop in the main process
(IO/puts (str "Connected as " username ". Type a message or /quit to exit."))
(TcpChatClient/input-loop socket)))
[:error reason]
(do
(IO/puts (str "Could not connect: " reason))
(IO/puts "Is the server running? Start it with:")
(IO/puts " mix clje.run --no-halt examples/tcp_chat_server.clje")
(System/halt 1))))
;; ── Entry point ─────────────────────────────────────────────────────
(let [args (System/argv)]
(if (== (count args) 0)
(do
(IO/puts "Usage: mix clje.run --no-halt examples/tcp_chat_client.clje -- <username>")
(System/halt 1))
(TcpChatClient/start (hd args))))
+131
View File
@@ -0,0 +1,131 @@
;;; TCP Chat Server — multi-terminal chat using gen_tcp
;;;
;;; Start the server:
;;; mix clje.run --no-halt examples/tcp_chat_server.clje
;;;
;;; Then connect clients in separate terminals:
;;; mix clje.run --no-halt examples/tcp_chat_client.clje -- alice
;;; mix clje.run --no-halt examples/tcp_chat_client.clje -- bob
;;;
;;; Protocol (line-based):
;;; Client → Server: JOIN:<username> | MSG:<text> | QUIT
;;; Server → Client: SYS:<text> | MSG:<user>:<text> | JOIN:<user> | QUIT:<user>
(ns TcpChatServer
(:require [clje.core :refer :all]
[Map] [String] [gen_tcp] [inet]))
;; ── Room manager ──────────────────────────────────────────────────
;; Holds {username → socket} map, broadcasts messages to all members.
(defn room-loop [members]
(receive
[:join username socket handler-pid]
(do
(TcpChatServer/broadcast members (str "JOIN:" username "\n"))
(TcpChatServer/send-line socket (str "SYS:Welcome " username "! " (count members) " user(s) online.\n"))
(TcpChatServer/room-loop (assoc members username #el[socket handler-pid])))
[:msg username text]
(do
(TcpChatServer/broadcast members (str "MSG:" username ":" text "\n"))
(TcpChatServer/room-loop members))
[:quit username]
(let [new-members (dissoc members username)]
(TcpChatServer/broadcast new-members (str "QUIT:" username "\n"))
(TcpChatServer/room-loop new-members))
[:list reply-pid]
(do
(send reply-pid #el[:members (Map/keys members)])
(TcpChatServer/room-loop members))))
(defn broadcast [members line]
(doseq [[_name [socket _pid]] members]
(gen_tcp/send socket line)))
(defn send-line [socket line]
(gen_tcp/send socket line))
;; ── Per-client handler ────────────────────────────────────────────
;; Receives TCP data via active mode and forwards to room manager.
(defn client-handler [socket room username]
(receive
[:tcp _sock data]
(let [line (String/trim data)]
(cond
(String/starts-with? line "MSG:")
(do
(send room #el[:msg username (String/slice line 4 (String/length line))])
(TcpChatServer/client-handler socket room username))
(== line "QUIT")
(do
(send room #el[:quit username])
(gen_tcp/close socket)
(println (str " [" username " quit]")))
:else
(do
(TcpChatServer/send-line socket (str "SYS:Unknown command. Send MSG:<text> or QUIT\n"))
(TcpChatServer/client-handler socket room username))))
[:tcp_closed _sock]
(do
(send room #el[:quit username])
(println (str " [" username " disconnected]")))
[:tcp_error _sock _reason]
(do
(send room #el[:quit username])
(println (str " [" username " error]")))))
;; ── Accept loop ───────────────────────────────────────────────────
(defn accept-loop [listen-socket room]
(case (gen_tcp/accept listen-socket)
[:ok client-socket]
(do
(TcpChatServer/handle-new-client client-socket room)
(TcpChatServer/accept-loop listen-socket room))
[:error reason]
(println (str "Accept error: " reason))))
(defn handle-new-client [socket room]
;; First message must be JOIN:<username>
;; Socket starts in passive mode so we can recv synchronously
(case (gen_tcp/recv socket 0 5000)
[:ok data]
(let [line (String/trim data)]
(if (String/starts-with? line "JOIN:")
(let [username (String/slice line 5 (String/length line))
;; Switch to active mode for the handler
_ (inet/setopts socket (list #el[:active true]))
handler (spawn (fn []
(TcpChatServer/client-handler socket room username)))]
(gen_tcp/controlling-process socket handler)
(send room #el[:join username socket handler])
(println (str " [" username " connected]")))
(do
(TcpChatServer/send-line socket "SYS:First message must be JOIN:<username>\n")
(gen_tcp/close socket))))
[:error _reason]
(gen_tcp/close socket)))
;; ── Start ─────────────────────────────────────────────────────────
(defn start [port]
(case (gen_tcp/listen port (list :binary #el[:active false] #el[:packet :line] #el[:reuseaddr true]))
[:ok listen-socket]
(do
(println (str "TCP Chat Server listening on port " port))
(println "Connect with: mix clje.run --no-halt examples/tcp_chat_client.clje -- <username>")
(let [room (spawn (fn [] (TcpChatServer/room-loop {})))]
(TcpChatServer/accept-loop listen-socket room)))
[:error reason]
(println (str "Failed to listen on port " port ": " reason))))
;; Start the server on port 4040
(TcpChatServer/start 4040)