From 7f5adb25e3e3bec05102ef08034c50082cd29f13 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Tue, 11 Jul 2023 12:19:51 +0200 Subject: [PATCH] [new] Misc improvements to example project Incl. notably some additional debugging tools for [#431]: - `connected-uids_` and `conns_` now both printed in loop - New buttons: - Print connected uids - Test repeated logins --- example-project/src/example/client.cljs | 128 ++++++++++++----- example-project/src/example/server.clj | 182 +++++++++++++++--------- 2 files changed, 205 insertions(+), 105 deletions(-) diff --git a/example-project/src/example/client.cljs b/example-project/src/example/client.cljs index 7c39447..0968c21 100644 --- a/example-project/src/example/client.cljs +++ b/example-project/src/example/client.cljs @@ -17,39 +17,54 @@ ;;;; Logging config +(defonce min-log-level_ (atom nil)) (defn- set-min-log-level! [level] (sente/set-min-log-level! level) ; Min log level for internal Sente namespaces (timbre/set-ns-min-level! level) ; Min log level for this namespace - ) + (reset! min-log-level_ level)) -(set-min-log-level! :info) +(when-let [el (.getElementById js/document "sente-min-log-level")] + (let [level (if-let [attr (.getAttribute el "data-level")] + (keyword attr) + :warn)] + (set-min-log-level! level))) ;;;; Util for logging output to on-screen console -(def output-el (.getElementById js/document "output")) -(defn ->output! [fmt & args] - (let [msg (apply encore/format fmt args)] - ;; (timbre/tracef "->output: %s" msg) - (aset output-el "value" (str (.-value output-el) "\n• " msg)) +(let [output-el (.getElementById js/document "output")] + (defn- ->output!! [x] + (aset output-el "value" (str (.-value output-el) x)) (aset output-el "scrollTop" (.-scrollHeight output-el)))) +(defn ->output! + ([ ] (->output!! "\n")) + ([fmt & args] + (let [msg (apply encore/format fmt args)] + (->output!! (str "\n• " msg))))) + (->output! "ClojureScript has successfully loaded") +(->output! "Sente version: %s" sente/sente-version) +(->output! "Min log level: %s" @min-log-level_) +(->output!) ;;;; Define our Sente channel socket (chsk) client (def ?csrf-token (when-let [el (.getElementById js/document "sente-csrf-token")] - (.getAttribute el "data-csrf-token"))) + (.getAttribute el "data-token"))) (if ?csrf-token (->output! "CSRF token detected in HTML, great!") (->output! "**IMPORTANT** CSRF token NOT detected in HTML, default Sente config will reject requests!")) -(let [;; For this example, select a random protocol: - rand-chsk-type (if (>= (rand) 0.5) :ajax :auto) - _ (->output! "Randomly selected chsk type: %s" rand-chsk-type) +(def chsk-type + "We'll select a random protocol for this example" + (if (>= (rand) 0.5) :ajax :auto)) + +(->output! "Randomly selected chsk type: %s" chsk-type) +(->output!) - ;; Serializtion format, must use same val for client + server: +(let [;; Serializtion format, must use same val for client + server: packer :edn ; Default packer, a good choice in most cases ;; (sente-transit/get-transit-packer) ; Needs Transit dep @@ -57,7 +72,7 @@ (sente/make-channel-socket-client! "/chsk" ; Must match server Ring routing URL ?csrf-token - {:type rand-chsk-type + {:type chsk-type :packer packer})] (def chsk chsk) @@ -117,71 +132,86 @@ ;;;; UI events -(when-let [target-el (.getElementById js/document "btn1")] +(when-let [target-el (.getElementById js/document "btn-send-with-reply")] (.addEventListener target-el "click" (fn [ev] - (->output! "Will send event to server WITH callback") (chsk-send! [:example/button2 {:had-a-callback? "indeed"}] 5000 - (fn [cb-reply] (->output! "Callback reply: %s" cb-reply)))))) + (fn [cb-reply] + (->output! "Callback reply: %s" cb-reply)))))) -(when-let [target-el (.getElementById js/document "btn2")] +(when-let [target-el (.getElementById js/document "btn-send-wo-reply")] (.addEventListener target-el "click" (fn [ev] - (->output! "Will send event to server WITHOUT callback") (chsk-send! [:example/button1 {:had-a-callback? "nope"}])))) -(when-let [target-el (.getElementById js/document "btn3")] +(when-let [target-el (.getElementById js/document "btn-test-broadcast")] (.addEventListener target-el "click" (fn [ev] - (->output! "Will ask server to test rapid async push") - (chsk-send! [:example/test-rapid-push])))) + (->output!) + (chsk-send! [:example/test-broadcast])))) -(when-let [target-el (.getElementById js/document "btn4")] +(when-let [target-el (.getElementById js/document "btn-toggle-broadcast-loop")] (.addEventListener target-el "click" (fn [ev] - (chsk-send! [:example/toggle-broadcast] 5000 + (chsk-send! [:example/toggle-broadcast-loop] 5000 (fn [cb-reply] (when (cb-success? cb-reply) - (let [loop-enabled? cb-reply] - (if loop-enabled? - (->output! "Server async broadcast loop now ENABLED") - (->output! "Server async broadcast loop now DISABLED"))))))))) + (let [enabled? cb-reply] + (if enabled? + (->output! "Server broadcast loop now ENABLED") + (->output! "Server broadcast loop now DISABLED"))))))))) -(when-let [target-el (.getElementById js/document "btn5")] +(when-let [target-el (.getElementById js/document "btn-disconnect")] (.addEventListener target-el "click" (fn [ev] - (->output! "Disconnecting...\n\n") + (->output!) (sente/chsk-disconnect! chsk)))) -(when-let [target-el (.getElementById js/document "btn6")] +(when-let [target-el (.getElementById js/document "btn-reconnect")] (.addEventListener target-el "click" (fn [ev] - (->output! "Reconnecting...\n\n") + (->output!) (sente/chsk-reconnect! chsk)))) -(when-let [target-el (.getElementById js/document "btn7")] +(when-let [target-el (.getElementById js/document "btn-break-with-close")] (.addEventListener target-el "click" (fn [ev] - (->output! "Simulating basic broken connection (WITH close)...\n\n") + (->output!) (sente/chsk-break-connection! chsk {:close-ws? true})))) -(when-let [target-el (.getElementById js/document "btn8")] +(when-let [target-el (.getElementById js/document "btn-break-wo-close")] (.addEventListener target-el "click" (fn [ev] - (->output! "Simulating basic broken connection (WITHOUT close)...\n\n") + (->output!) (sente/chsk-break-connection! chsk {:close-ws? false})))) -(when-let [target-el (.getElementById js/document "btn9")] +(when-let [target-el (.getElementById js/document "btn-toggle-logging")] (.addEventListener target-el "click" (fn [ev] - (->output! "Will ask server to toggle minimum log level") (chsk-send! [:example/toggle-min-log-level] 5000 (fn [cb-reply] (if (cb-success? cb-reply) (let [level cb-reply] (set-min-log-level! level) (->output! "New minimum log level (client+server): %s" level)) - (->output! "Failed to toggle minimum log level: %s" cb-reply))))))) + (->output! "Request failed: %s" cb-reply))))))) + +(when-let [target-el (.getElementById js/document "btn-toggle-bad-conn-rate")] + (.addEventListener target-el "click" + (fn [ev] + (chsk-send! [:example/toggle-bad-conn-rate] 5000 + (fn [cb-reply] + (if (cb-success? cb-reply) + (->output! "New rate: %s" cb-reply) + (->output! "Request failed: %s" cb-reply))))))) + +(when-let [target-el (.getElementById js/document "btn-connected-uids")] + (.addEventListener target-el "click" + (fn [ev] + (chsk-send! [:example/connected-uids] 5000 + (fn [cb-reply] + (when (cb-success? cb-reply) + (->output! "Connected uids: %s" cb-reply))))))) (when-let [target-el (.getElementById js/document "btn-login")] (.addEventListener target-el "click" @@ -190,7 +220,8 @@ (if (str/blank? user-id) (js/alert "Please enter a user-id first") (do - (->output! "Logging in with user-id %s...\n\n" user-id) + (->output!) + (->output! "Logging in with user-id %s..." user-id) ;;; Use any login procedure you'd like. Here we'll trigger an Ajax ;;; POST request that resets our server-side session. Then we ask @@ -212,6 +243,25 @@ (->output! "Login successful") (sente/chsk-reconnect! chsk)))))))))))) +(when-let [target-el (.getElementById js/document "btn-repeated-logins")] + (.addEventListener target-el "click" + (fn [ev] + (->output!) + (->output! "Will rapidly change user-id from \"1\" to \"10\"...") + (let [c (async/chan)] + (go-loop [uids (range 11)] + (when-let [[next-uid] uids] + (sente/ajax-lite "/login" + {:method :post + :headers {:X-CSRF-Token (:csrf-token @chsk-state)} + :params {:user-id (str next-uid)}} + (fn [ajax-resp] + (when (:success? ajax-resp) (sente/chsk-reconnect! chsk)) + (put! c :continue))) + (user async push"] " " - [:button#btn4 {:type "button"} "Toggle server>user async broadcast push loop"]] - [:p - [:button#btn5 {:type "button"} "Disconnect"] " " - [:button#btn6 {:type "button"} "Reconnect"] " " - [:button#btn7 {:type "button"} "Simulate break (with on-close)"] " " - [:button#btn8 {:type "button"} "Simulate break (w/o on-close)"]] - [:p [:button#btn9 {:type "button"} "Toggle min log level"]] + [:section + [:h4 "Standard controls"] + [:p + [:button#btn-send-with-reply {:type "button"} "chsk-send! (with reply)"] " " + [:button#btn-send-wo-reply {:type "button"} "chsk-send! (without reply)"] " "] + [:p + [:button#btn-test-broadcast {:type "button"} "Test broadcast (server>user async push)"] " " + [:button#btn-toggle-broadcast-loop {:type "button"} "Toggle broadcast loop"]] + [:p + [:button#btn-disconnect {:type "button"} "Disconnect"] " " + [:button#btn-reconnect {:type "button"} "Reconnect"]] + + [:p "Log in with a " [:strong "user-id"] " so that the server can directly address this user-id's connected clients:"] + [:p + [:input#input-login {:type :text :placeholder "User-id"}] " " + [:button#btn-login {:type "button"} "← Log in with user-id"]] + + [:p "You may want to try open this page with multiple browser windows (with and/or without the same browser session)."] + [:p [:strong "Tip:"] " use your browser's Private Browsing / Incognito mode to log in with different user-ids."]] + + [:hr] + + [:section + [:h4 "Debug and testing"] + [:p + [:button#btn-toggle-logging {:type "button"} "Toggle minimum log level"] " " + [:button#btn-toggle-bad-conn-rate {:type "button"} "Toggle simulated bad conn rate"]] + [:p + [:button#btn-break-with-close {:type "button"} "Simulate broken conn (with on-close)"] " " + [:button#btn-break-wo-close {:type "button"} "Simulate broken conn (w/o on-close)"]] + [:p + [:button#btn-repeated-logins {:type "button"} "Test repeated logins"] " " + [:button#btn-connected-uids {:type "button"} "Print connected uids"]]] - [:p "Log in with a " [:strong "user-id"] " below so that the server can directly address this user-id's connected clients:"] - [:p - [:input#input-login {:type :text :placeholder "User-id"}] " " - [:button#btn-login {:type "button"} "← Log in with user-id"]] [:script {:src "main.js"}] ; Include our cljs target )) @@ -149,38 +168,40 @@ ;;;; Some server>user async push examples -(defn test-fast-server>user-pushes - "Quickly pushes 100 events to all connected users. Note that this'll be - fast+reliable even over Ajax!" +(defn broadcast! + "Pushes given event to all connected users." + [event] + (let [all-uids (:any @connected-uids_)] + (doseq [uid all-uids] + (timbre/debugf "Broadcasting server>user to %s uids" (count all-uids)) + (chsk-send! uid event)))) + +(defn test-broadcast! + "Quickly broadcasts 100 events to all connected users. + Note that this'll be fast+reliable even over Ajax!" [] - (doseq [uid (:any @connected-uids)] + (doseq [uid (:any @connected-uids_)] (doseq [i (range 100)] - (chsk-send! uid [:fast-push/is-fast (str "hello " i "!!")])))) + (chsk-send! uid [:example/broadcast (str {:i i, :uid uid})])))) + +(comment (test-broadcast!)) -(comment (test-fast-server>user-pushes)) +(defonce broadcast-loop?_ (atom true)) +(defonce ^:private auto-loop_ + (delay + (go-loop [i 0] + (user async pushes, setup a loop to broadcast an - event to all connected users every 10 seconds" - [] - (let [broadcast! - (fn [i] - (let [uids (:any @connected-uids)] - (timbre/debugf "Broadcasting server>user: %s uids" (count uids)) - (doseq [uid uids] - (chsk-send! uid - [:some/broadcast - {:what-is-this "An async broadcast pushed from server" - :how-often "Every 10 seconds" - :to-whom uid - :i i}]))))] + (when @broadcast-loop?_ + (broadcast! + [:example/broadcast-loop + {:my-message "A broadcast, pushed asynchronously from server" + :i i}])) - (go-loop [i 0] - (user-pushes)) +(defmethod -event-msg-handler :example/test-broadcast + [ev-msg] (test-broadcast!)) -(defmethod -event-msg-handler :example/toggle-broadcast +(defmethod -event-msg-handler :example/toggle-broadcast-loop [{:as ev-msg :keys [?reply-fn]}] - (let [loop-enabled? (swap! broadcast-enabled?_ not)] + (let [loop-enabled? (swap! broadcast-loop?_ not)] (?reply-fn loop-enabled?))) (defmethod -event-msg-handler :example/toggle-min-log-level [{:as ev-msg :keys [?reply-fn]}] - (let [new-level + (let [new-val (case @min-log-level_ :trace :debug :debug :info @@ -240,8 +261,27 @@ :error :trace :trace)] - (set-min-log-level! new-level) - (?reply-fn new-level))) + (set-min-log-level! new-val) + (?reply-fn new-val))) + +(defmethod -event-msg-handler :example/toggle-bad-conn-rate + [{:as ev-msg :keys [?reply-fn]}] + (let [new-val + (case sente/*simulated-bad-conn-rate* + nil 0.25 + 0.25 0.5 + 0.5 0.75 + 0.75 1.0 + 1.0 nil)] + + (alter-var-root #'sente/*simulated-bad-conn-rate* (constantly new-val)) + (?reply-fn new-val))) + +(defmethod -event-msg-handler :example/connected-uids + [{:as ev-msg :keys [?reply-fn]}] + (let [uids @connected-uids_] + (timbre/infof "Connected uids: %s" uids) + (?reply-fn uids))) ;; TODO Add your (defmethod -event-msg-handler [ev-msg] )s here... @@ -267,36 +307,46 @@ [port stop-fn] ;;; TODO Choose (uncomment) a supported web server ------------------ (let [stop-fn (http-kit/run-server ring-handler {:port port})] - [(:local-port (meta stop-fn)) (fn [] (stop-fn :timeout 100))]) + [(:local-port (meta stop-fn)) (fn stop-fn [] (stop-fn :timeout 100))]) ;; ;; (let [server (immutant/run ring-handler :port port)] - ;; [(:port server) (fn [] (immutant/stop server))]) + ;; [(:port server) (fn stop-fn [] (immutant/stop server))]) ;; ;; (let [port (nginx-clojure/run-server ring-handler {:port port})] - ;; [port (fn [] (nginx-clojure/stop-server))]) + ;; [port (fn stop-fn [] (nginx-clojure/stop-server))]) ;; ;; (let [server (aleph/start-server ring-handler {:port port}) ;; p (promise)] ;; (future @p) ; Workaround for Ref. https://goo.gl/kLvced ;; ;; (aleph.netty/wait-for-close server) ;; [(aleph.netty/port server) - ;; (fn [] (.close ^java.io.Closeable server) (deliver p nil))]) + ;; (fn stop-fn [] (.close ^java.io.Closeable server) (deliver p nil))]) ;; ------------------------------------------------------------------ uri (format "http://localhost:%s/" port)] - (timbre/infof "Web server is running at `%s`" uri) + (timbre/infof "HTTP server is running at `%s`" uri) (try (.browse (java.awt.Desktop/getDesktop) (java.net.URI. uri)) (catch java.awt.HeadlessException _)) (reset! web-server_ stop-fn))) -(defn stop! [] (stop-router!) (stop-web-server!)) -(defn start! [] (start-router!) (start-web-server!) (start-example-broadcaster!)) +(defn stop! [] (stop-router!) (stop-web-server!)) +(defn start! [] + (timbre/reportf "Sente version: %s" sente/sente-version) + (timbre/reportf "Min log level: %s" @min-log-level_) + (start-router!) + (let [stop-fn (start-web-server!)] + @auto-loop_ + stop-fn)) (defn -main "For `lein run`, etc." [] (start!)) (comment (start!) ; Eval this at REPL to start server via REPL - (test-fast-server>user-pushes)) + (test-broadcast!) + + (broadcast! [:example/foo]) + @connected-uids_ + @conns_)