ajaxwebsocketclojurere-framesente

Non-stop requests over sente web-socket channel for what should be a single request


The goal is to allow someone to update the status of an online exam in real time. (IE. Pressing Activate Charlie's exam, changes Charlies screen to allow him to start taking his exam. The relationship between the proctor is one proctor to many exams.

Currently we're getting the exam activated using sente successfully, but once we click the (activate exam) button, it continues to send requests over the route "/chsk" over and over again. (To sente's credit, it is very fast.) It sends many (10+) of these requests successfully before hitting the following error. We thought the problem would be in the middleware, but after adjusting wrap-formats to handle websocket requests (and requests sent over the "/chsk" route, we're still getting the error. I don't think the problem really lies with the middleware, because the info comes through just fine the first time and activates the exam as expected. I don't know why sente is sending more than a single request at all. sente is working for our purposes, but we need to stop all these extra requests somehow or my machine/internet gets way bogged down.

How can we make sure a request is sent only once from the backend?

(THE PROBLEM) I was able to freeze it as soon as I activated the exam to show that the web-socket did indeed do what we wanted it to do.

Immediately after endless requests are made and the browser crashes soon after.

Error in the REPL

(CLIENT SIDE)

(ns flats.web-sockets
  (:require [goog.string :as gstring]
            [flats.shared :as shared]
            [reagent.core :as r]
            [re-frame.core :as rfc]
            [taoensso.encore :as encore :refer-macros (have)]
            [taoensso.sente  :as sente]
            [taoensso.timbre :as log :refer-macros (tracef)]))

(def ?csrf-token
  (when-let [el (.getElementById js/document "token")]
    (.getAttribute el "value")))

(def -chsk (r/atom nil))
(def -ch-chsk (r/atom nil))
(def -chsk-send! (r/atom nil))
(def -chsk-state (r/atom nil))

(defn sente-setup
  "Takes uid (exam-id, registration-id, user-id?) to use as unique client-id"
  [uid]
  (let [{:keys [chsk ch-recv send-fn state]}
        (sente/make-channel-socket-client! "/chsk" ; Note the same path as before
                                           ?csrf-token
                                           {:type :auto  ; e/o #{:auto :ajax :ws}
                                            :client-id uid})]
    (reset! -chsk       chsk) 
    (reset! -ch-chsk    ch-recv) ; ChannelSocket's receive channel
    (reset! -chsk-send! send-fn) ; ChannelSocket's send API fn
    (reset! -chsk-state state)   ; Watchable, read-only atom
    ))

(defmulti -event-msg-handler
  "Multimethod to handle Sente `event-msg`s"
  :id ; Dispatch on event-id
  )

(defn event-msg-handler
  "Wraps `-event-msg-handler` with logging, error catching, etc."
  [{:as ev-msg :keys [_id _?data _event]}]
  #_(log/info (str "\nin event-msg-handler: "ev-msg))
  (-event-msg-handler ev-msg))

(defmethod -event-msg-handler
  :default ; Default/fallback case (no other matching handler)
  [{:as _ev-msg :keys [event]}]
  (tracef "Unhandled event: %s" event))

(defmethod -event-msg-handler :chsk/state
  [{:as _ev-msg :keys [?data]}]
  #_(log/info "In chsk/state "_ev-msg)
  (let [[_old-state-map new-state-map] (have vector? ?data)]
    (if (:first-open? new-state-map)
      (tracef "Channel socket successfully established!: %s" new-state-map)
      (tracef "Channel socket state change: %s"              new-state-map))))

(defmethod -event-msg-handler :chsk/recv
  [{:as _ev-msg
    :keys [_?data]
    [event-id {:keys [exam-status]}]
    :?data}]
  (log/info (str ":chsk/recv payload: " _ev-msg))
  (when (= event-id :exam/status)
    (rfc/dispatch [:set-current-exam-status exam-status])))

(defmethod -event-msg-handler :chsk/handshake
  [{:as _ev-msg :keys [?data]}]
  (let [[_?uid _?csrf-token _?handshake-data] ?data]
    (tracef "Handshake: %s" ?data)))

;;;; Sente event router (our `event-msg-handler` loop)

(defonce router_ (atom nil))
(defn  stop-router! [] (when-let [stop-f @router_] (stop-f)))
(defn start-router! []
  (stop-router!)
  (reset! router_
          (sente/start-client-chsk-router!
           @-ch-chsk event-msg-handler)))

(SERVER SIDE)

(ns flats.web-sockets
  (:require [taoensso.timbre :as log]
            [taoensso.sente :as sente]
            [clojure.core.async :as async :refer [<!!]]
            [taoensso.sente.server-adapters.immutant :refer (get-sch-adapter)])
  (:import [java.util UUID]))

(def user-id (atom nil))

(let [chsk-server (sente/make-channel-socket-server!
                   (get-sch-adapter)
                   {:packer :edn
                    :user-id-fn (fn [request]
                                  (reset! user-id (:client-id request))
                                  (UUID/fromString (:client-id request))
                                  #_(:client-id request)
                                  )})
      {:keys [ch-recv send-fn connected-uids
              ajax-post-fn ajax-get-or-ws-handshake-fn]} chsk-server]
  (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) ; ChannelSocket's receive channel
  (def chsk-send!                    send-fn) ; ChannelSocket's send API fn
  (def connected-uids                connected-uids) ; Watchable, read-only atom
  )

;;;; Server>user async push

(defn broadcast!
  [registration-id exam-status]
  (chsk-send! registration-id
              [:exam/status {:exam-status exam-status}]
              5000))

(ACTIVATING THE EXAM)

(defn create-exam-action
  "Creates a new `exam_action` with the given `exam-id` and the `action-type`"
  [{{:keys [exam-id action-type]} :path-params}]
  (let [exam-id (UUID/fromString exam-id)
        registration-id (dbx/READ exam-id [:registration-id])]
    (exams/create-exam-action exam-id action-type)
    (ws/broadcast! registration-id action-type) 
    (http-response/ok action-type)))

(ROUTES)

["/chsk" {:get ws/ring-ajax-get-or-ws-handshake
             :post ws/ring-ajax-post}]

*Note: We start the Client side router once an exam is registered, and use the exam's id as the user-id for the front end channel.


Solution

  • Turns out that start-router! and sente-setup functions were being called more than once. So we added a reframe flag so be set once sente had connected, and then chsk-send! sent the requests from the BE only once. A similar idea would be to make the function calls a singleton so if a session had already been started for the exam it shouldn't call the start router again.