How to handle cljs-ajax responses? - ajax

I am making a request from a clojurescript frontend with cljs-ajax to an API that responds with JSON but it seems like I need to do something to it before I can use it in cljs.
(defn all-pieces []
(GET "/art/pieces" {:handler ajax-success-handler}))
When I initialize my app-state I assign the key :all-pieces (all-pieces)
When I go to iterate over :all-pieces in a component I get the error Uncaught Error: [object Object] is not ISeqable.
(defn pieces-component []
[:ul (for [piece (:all-pieces #app-state)]
[:li (art-piece piece)])])
Edited re Pratley:
The code below now results in the state of all-pieces being {}, see anything wrong?
;; -------------------------
;; Remote Data
(defn all-pieces [handler]
(GET "/art/pieces" {:handler handler}))
;; -------------------------
;; State Management
(def app-state (atom
{:doc {}
:saved? false
:page-state {}
:all-pieces {}}))
(defn set-pieces-fresh []
(all-pieces (fn [pcs] swap! app-state assoc :all-pieces pcs)))

Don't set :all-peices to the result of (all-pieces).
The function ajax-success-handler should set :all-peices instead.
The result of (all-pieces) is the result of starting the asynchronous call, not the response. The handler is what gets called when the response arrives.
(fn [pcs] swap! app-state assoc :all-pieces pcs)
Does not do any swapping, as swap! needs to be in parens... it is just a function that returns pcs. Consider promoting it to a named function so you can test it separately:
(def app-state
(atom {:all-pieces {}}))
(defn pieces-handler [pcs]
(swap! app-state assoc :all-pieces pcs))
(defn fetch-pieces []
(GET "/art/pieces" {:handler pieces-handler}))
(fetch-pieces)

Related

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.
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.

Invalid event "~#chsk/handshake" when using sente packers.transit/get-transit-packer in clojure

I am getting an error when using sente that results in failure while sending information from client to server or vice versa. The problem seems to be that the handshake fails with an error
cljs$core$ExceptionInfo
message: "Invalid event"
data: {:given "~:chsk/handshake", :errors {:wrong-type {…}}}
The successive ws/ping also fail with the same error but with extra information,
sente.cljc:142 Uncaught #error {:message "Invalid event", :data {:given "~#'", :errors {:wrong-type
{…}[:expected :vector]
[:actual {:type #object[String], :value "~#'"}]
What might be the problem, my code seems okay and follows the default example in sente.
Expected behaviour:
Sente would connect and i would be able to call send-fn and send messages between the server and client, successfully.
Edit: Addition of code as suggested in the comments:
(require '[taoensso.sente.server-adapters.aleph :refer (get-sch-adapter)])
;; Create Websocket connection in server
(let [packer (sente-transit/get-transit-packer)
chsk-server (sente/make-channel-socket-server! (get-sch-adapter) {:packer packer
:csrf-token-fn nil})
{: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)
(def chsk-send! send-fn)
(def connected-uids connected-uids))
;; Start the web server
(defn start-web-server! [& [port]]
(stop-web-server!)
(let [port (or port default-port)
ring-handler (var main-ring-handler)
[port stop-fn]
(let [server (aleph/start-server ring-handler {:port port})
p (promise)]
(future #p)
[(aleph.netty/port server)
(fn [] (.close ^Closeable server) (deliver p nil))])
uri (format "http://localhost:%s/" port)]
(infof "Web server is running at `%s`" uri)
(reset! web-server_ {:port port :stop-fn stop-fn})
(try
(if (and (Desktop/isDesktopSupported)
(.isSupported (Desktop/getDesktop) Desktop$Action/BROWSE))
(.browse (Desktop/getDesktop) (URI. uri))
(.exec (Runtime/getRuntime) (str "xdg-open" uri)))
(Thread/sleep 7500)
(catch HeadlessException _))))
On the client side:
(let [packer (sente-transit/get-transit-packer)
{:keys [chsk ch-recv send-fn state]}
(sente/make-channel-socket-client! "/chsk"
{:type :auto
:packer packer})]
(def chsk chsk)
(def ch-chsk ch-recv)
(def chsk-send! send-fn)
(def chsk-state state))
;; start the router
(defn start-router! []
(stop-router!)
(reset! router_ (sente/start-client-chsk-router! ch-chsk event-msg-handler)))
EDIT, Addition
I have noted that the error only exists when i use the packer, (packers.transit/get-transit-packer) and not :edn
Ran into this issue today coincidentally, so after this issue I was scratching my head a bit, could it be the library which hasn't seen much activity/is stable?
Nope. I simply forgot to include the [com.cognitect/transit-cljs "0.8.256"] cljs dependency!
The example did warn:
https://github.com/ptaoussanis/sente/blob/master/example-project/src/example/client.cljs#L47
Not exactly an answer, but we gotta see your code for that.
The error "Invalid event" is generated here:
(defn validate-event
"Returns nil if given argument is a valid [ev-id ?ev-data] form. Otherwise
returns a map of validation errors like `{:wrong-type {:expected _ :actual _}}`."
[x]
(cond
(not (vector? x)) {:wrong-type (expected :vector x)}
(not (#{1 2} (count x))) {:wrong-length (expected #{1 2} x)}
:else
(let [[ev-id _] x]
(cond
(not (keyword? ev-id)) {:wrong-id-type (expected :keyword ev-id)}
(not (namespace ev-id)) {:unnamespaced-id (expected :namespaced-keyword ev-id)}
:else nil))))
```

Howto enable Fulcro Websockets

I have a basic question about how to get started with Fulcro and Websockets.
i) I started with the Fulcro lein template. ii) Then added the websocket client and server bits. iii) In my server, I also added a com.fulcrologic.fulcro.networking.websocket-protocols.WSListener to detect when a WS client is connecting.
Between the WSListener, and the browser's network console, I can see that the client is never making a WS connection.
How does Fulcro make the initial WS connection?
After that, how can I make server pushes to client?
client.cljs
(ns foo.client
(:require [fulcro.client :as fc]
[foo.ui.root :as root]
[fulcro.client.network :as net]
[fulcro.client.data-fetch :as df]
[com.fulcrologic.fulcro.networking.websockets :as fws]
[com.fulcrologic.fulcro.application :as app]))
;; Neither this nor the below ":websocket (fws/fulcro-websocket-remote {})" works
;; (defonce app (app/fulcro-app {:remotes {:remote (fws/fulcro-websocket-remote {})}}))
(defonce SPA (atom nil))
(defn mount [] (reset! SPA (fc/mount #SPA root/Root "app")))
(defn start [] (mount))
(def secured-request-middleware
;; The CSRF token is embedded via server_components/html.clj
(->
(net/wrap-csrf-token (or js/fulcro_network_csrf_token "TOKEN-NOT-IN-HTML!"))
(net/wrap-fulcro-request)))
(defn ^:export init []
(reset! SPA (fc/make-fulcro-client
{:client-did-mount (fn [foo]
(df/load foo :all-users root/User))
;; This ensures your client can talk to a CSRF-protected server.
;; See middleware.clj to see how the token is embedded into the HTML
:networking {:remote (net/fulcro-http-remote
{:url "/api"
:request-middleware secured-request-middleware})
:websocket (fws/fulcro-websocket-remote {})}}))
(start))
middleware.cljs
(defrecord FooWSListener []
WSListener
(client-added [this ws-net cid]
(println (str "Listener for dealing with client added events." [ws-net cid])))
(client-dropped [this ws-net cid]
(println (str "listener for dealing with client dropped events." [ws-net cid]))))
(def foo-ws-listener (->FooWSListener))
(def websockets' (atom nil))
(defn query-parser [env query] )
(defstate middleware
:start
(let [websockets (fws/start! (fws/make-websockets
query-parser
{:http-server-adapter (get-sch-adapter)
:parser-accepts-env? true
;; See Sente for CSRF instructions
:sente-options {:csrf-token-fn nil}}))
defaults-config (:ring.middleware/defaults-config config)
legal-origins (get config :legal-origins #{"localhost"})]
(fwsp/add-listener websockets foo-ws-listener)
(reset! websockets' websockets)
(-> not-found-handler
(wrap-api "/api")
(fws/wrap-api websockets)
server/wrap-transit-params
server/wrap-transit-response
(wrap-html-routes)
(wrap-defaults defaults-config)
wrap-gzip)))
Since fulcro-websockets 3.1.0 the websocket connection is made on the first data transfer via websocket remote.
If you want to force the connection, you can do that by sending any mutation over the remote:
(:require [com.fulcrologic.fulcro.mutations :refer [defmutation]
[com.fulcrologic.fulcro.components :as comp])
(defmutation connect-socket [_]
(websocket [_] true))
(comment
;; trigger it via repl or a button handler
(comp/transact! foo.client/SPA `[(connect-socket {})]
Once you make a connection, you can make a push from server like this:
(:require [com.fulcrologic.fulcro.networking.websocket-protocols :refer [push]])
(let [client-uid (-> #(:connected-uids websockets')
:any
first)]
(push websockets' client-uid :foo-topic {:foo "bar"}))
To receive that on the client, you're going to need a :push-handler defined on the websocket remote:
(defn push-handler [{:keys [topic msg] :as data}]
(log/info "push-handler received: " data))
;; optionally you can listen for websocket state changes
(defn state-callback [before after]
(log/info "state-callback: " {:before before
:after after}))
(defn ^:export init []
(reset! SPA (fc/make-fulcro-client
{:client-did-mount (fn [foo]
(df/load foo :all-users root/User))
:remotes {:remote (net/fulcro-http-remote
{:url "/api"
:request-middleware secured-request-middleware})
:websocket (fws/fulcro-websocket-remote
{:push-handler push-handler
:state-callback state-callback})}}))
(start))
BTW since you're using template provided mount, you can use it to handle websockets' on the server:
(defstate websockets'
:start
(fws/start! (fws/make-websockets
query-parser
{:http-server-adapter (get-sch-adapter)
:parser-accepts-env? true})))
This way you can avoid this line: (reset! websockets' websockets)

Clojurescript Websockets with Sente 403 Error

I'm attempting to setup a web socket connection from two different ports on my localhost. I'm using Sente and Immutant. I have the following, but it returns a 403 forbidden when attempting to connect
Server.clj
(defn handler
"Comment"
[]
"<h1>Hello World</h1>")
(let [{:keys [ch-recv send-fn connected-uids
ajax-post-fn ajax-get-or-ws-handshake-fn]}
(sente/make-channel-socket! (get-sch-adapter) {})]
(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
)
(defroutes app
"The router."
(GET "/" [] (handler))
(GET "/chsk" req (ring-ajax-get-or-ws-handshake req))
(POST "/chsk" req (ring-ajax-post req))
(route/not-found
"<h1>Page not found</h1>"))
(def my-app
(-> app
;; Add necessary Ring middleware:
ring.middleware.keyword-params/wrap-keyword-params
ring.middleware.params/wrap-params))
(def wrapped
(wrap-cors my-app :access-control-allow-origin [#".*"]
:access-control-allow-methods [:get :put :post :delete]))
(defn -main
"Start the server"
[& args]
(immutant/run wrapped {:host "localhost" :port 8080 :path "/"}))
This throws no errors, and the "/" route properly displays.
Client.cljs
(let [{:keys [chsk ch-recv send-fn state]}
(sente/make-channel-socket! "/chsk" ; Note the same path as before
"sdasds" ; dummy
{:type :auto ; e/o #{:auto :ajax :ws}
:host "localhost:8080/"
}
)]
(def chsk chsk)
(def ch-chsk ch-recv) ; ChannelSocket's receive channel
(def chsk-send! send-fn) ; ChannelSocket's send API fn
(def chsk-state state) ; Watchable, read-only atom
)
This throws 403 errors as it tries to connect. I'm not sure why it's doing so, I've been looking at it for a while and have come up short.
I believe this is the issue with CSRF anti-forgery:
Sente docs:
This is important. Sente has support, but you'll need to use middleware like ring-anti-forgery to generate and check CSRF codes. The ring-ajax-post handler should be covered (i.e. protected).
In Sente official example they show how to setup it properly.

ClojureScript Ajax Output

GET is called and when referred to the handler. The result is not the data of the properties, the output is just "null". How can I output the needed value?
(ns places.core
(:require [ajax.core :refer [GET POST]]))
(defn handler [response]
(.log js/console (:sourceId (:_source (:hits (:hits response))))))
The solution was to parse the raw json output like this:
(defn handler [response]
(.log js/console
(.. (nth (.. (JSON/parse response) -hits -hits) 0) -_source -sourceId)))

Resources