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)))
Related
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.
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))))
```
I've been struggling with this for over two days now and don't seem to find a solution.
So all I am trying to do is to display an image in the browser, but when I am calling the endpoint this is what happens:
Cannot JSON encode object of class: class java.io.File
Endpoint
(context "/servers" []
:datomic true
(GET "/:id/graph/:panel-type" {db :db user :user}
:summary "Return a server graph image"
:path-params [id :- Long
panel-type :- String]
(let [file-path (str panel-type ".png")
result (-> (response/response (clojure.java.io/file file-path))
(response/content-type "image/png")))]
(ok result)))) ;; ring.util.http-response
I am new to compojure API, but something makes me think there is a problem with middleware implementation - custom formats?
Middleware
(defn wrap-formats [handler]
(let [wrapped (wrap-restful-format
handler
{:formats [:json-kw :transit-json :transit-msgpack]})]
(fn [request]
;; disable wrap-formats for websockets
;; since they're not compatible with this middleware
((if (:websocket? request) handler wrapped) request))))
Stacktrace
com.fasterxml.jackson.core.JsonGenerationException: Cannot JSON encode object of class: class java.io.File: cpu.png
at cheshire.generate$generate.invokeStatic(generate.clj:152)
at cheshire.generate$generate.invoke(generate.clj:116)
at cheshire.generate$generate.invokeStatic(generate.clj:122)
at cheshire.generate$generate.invoke(generate.clj:116)
at cheshire.core$generate_string.invokeStatic(core.clj:74)
at cheshire.core$generate_string.invoke(core.clj:49)
at ring.middleware.format_response$make_json_encoder$fn__20198.invoke(format_response.clj:221)
at ring.middleware.format_response$wrap_format_response$fn__20186.invoke(format_response.clj:204)
at ring.middleware.keyword_params$wrap_keyword_params$fn__4302.invoke(keyword_params.clj:36)
at ring.middleware.nested_params$wrap_nested_params$fn__20350.invoke(nested_params.clj:89)
at ring.middleware.params$wrap_params$fn__4376.invoke(params.clj:67)
at compojure.api.middleware$wrap_options$fn__23551.invoke(middleware.clj:74)
at compojure.api.routes.Route.invoke(routes.clj:74)
at clojure.lang.Var.invoke(Var.java:381)
at compojure.core$routing$fn__4157.invoke(core.clj:185)
at clojure.core$some.invokeStatic(core.clj:2674)
at clojure.core$some.invoke(core.clj:2665)
at compojure.core$routing.invokeStatic(core.clj:185)
at compojure.core$routing.doInvoke(core.clj:182)
at clojure.lang.RestFn.applyTo(RestFn.java:139)
at clojure.core$apply.invokeStatic(core.clj:659)
at clojure.core$apply.invoke(core.clj:652)
at compojure.core$routes$fn__4161.invoke(core.clj:192)
at clojure.lang.Var.invoke(Var.java:381)
at ring.middleware.reload$wrap_reload$fn__46696.invoke(reload.clj:39)
at selmer.middleware$wrap_error_page$fn__46709.invoke(middleware.clj:9)
at prone.middleware$wrap_exceptions$fn__46879.invoke(middleware.clj:126)
at buddy.auth.middleware$wrap_authentication$fn__44919.invoke(middleware.clj:42)
at buddy.auth.middleware$wrap_authorization$fn__44927.invoke(middleware.clj:94)
at ring.middleware.cors$handle_cors.invokeStatic(cors.clj:146)
at ring.middleware.cors$handle_cors.invoke(cors.clj:135)
at ring.middleware.cors$wrap_cors$fn__45217.invoke(cors.clj:160)
at ring.middleware.flash$wrap_flash$fn__45283.invoke(flash.clj:39)
at ring.middleware.session$wrap_session$fn__45465.invoke(session.clj:108)
at ring.middleware.keyword_params$wrap_keyword_params$fn__4302.invoke(keyword_params.clj:36)
at ring.middleware.nested_params$wrap_nested_params$fn__20350.invoke(nested_params.clj:89)
at ring.middleware.multipart_params$wrap_multipart_params$fn__45551.invoke(multipart_params.clj:172)
at ring.middleware.params$wrap_params$fn__4376.invoke(params.clj:67)
at ring.middleware.cookies$wrap_cookies$fn__45416.invoke(cookies.clj:175)
at ring.middleware.absolute_redirects$wrap_absolute_redirects$fn__45638.invoke(absolute_redirects.clj:47)
at ring.middleware.resource$wrap_resource$fn__45567.invoke(resource.clj:37)
at ring.middleware.content_type$wrap_content_type$fn__25387.invoke(content_type.clj:34)
at ring.middleware.default_charset$wrap_default_charset$fn__45610.invoke(default_charset.clj:31)
at ring.middleware.not_modified$wrap_not_modified$fn__25417.invoke(not_modified.clj:53)
at ring.middleware.x_headers$wrap_x_header$fn__45246.invoke(x_headers.clj:22)
at ring.middleware.x_headers$wrap_x_header$fn__45246.invoke(x_headers.clj:22)
at ring.middleware.x_headers$wrap_x_header$fn__45246.invoke(x_headers.clj:22)
at voltage.middleware$wrap_bearer_token$fn__46930.invoke(middleware.clj:72)
at ring.middleware.webjars$wrap_webjars$fn__45738.invoke(webjars.clj:40)
at voltage.middleware$wrap_internal_error$fn__46919.invoke(middleware.clj:37)
at immutant.web.internal.undertow$create_http_handler$reify__57634.handleRequest(undertow.clj:239)
at org.projectodd.wunderboss.web.undertow.async.websocket.UndertowWebsocket$2.handleRequest(UndertowWebsocket.java:107)
at io.undertow.server.session.SessionAttachmentHandler.handleRequest(SessionAttachmentHandler.java:68)
at io.undertow.server.Connectors.executeRootHandler(Connectors.java:202)
at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:802)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Any clue on the above is highly appreciated.
The problem is that you're calling both ring.util.response/response and ring.util.http-response/ok:
(ok (ring.util.response/response "foo"))
=> {:status 200, :headers {}, :body {:status 200, :headers {}, :body "foo"}}
Using either of them separately works:
(ns compojure.api.examples.handler
(:require [clojure.java.io :as io]
[compojure.api.sweet :refer :all]
[ring.middleware.format :refer [wrap-restful-format]]
[ring.util.http-response :refer :all]
[ring.util.response :as response]
[compojure.api.examples.domain :refer :all]
[schema.core :as s]))
(defn wrap-formats [handler]
(let [wrapped (wrap-restful-format
handler
{:formats [:json-kw :transit-json :transit-msgpack]})]
(fn [request]
;; disable wrap-formats for websockets
;; since they're not compatible with this middleware
((if (:websocket? request) handler wrapped) request))))
(def app
(wrap-formats
(api
(GET "/lisplogo" []
(-> (response/response (io/file "lisplogo_256.png"))
(response/content-type "image/png")))
(GET "/lisplogo2" []
(assoc-in (ok (io/file "lisplogo_256.png"))
[:headers "Content-Type"]
"image/png")))))
If you want to display image, you better return HTML page with <img> linked to your dir with static assets (usually resources/).
If you want to download file, here is an example of Compojure-api handler:
(ns example.app
(:require [clojure.java.io :as io]
[ring.util.response :as r]))
(defn file-download [name]
(let [filename (str "resources/" name)
bin (io/file filename)]
(-> (r/response (io/input-stream bin))
(r/header "Content-Type" "application/octet-stream")
(r/header "Content-Disposition" "attachment")
(r/header "Content-Length" (.length bin))
(r/status 200))))
Adapt and use this function in place where your let goes in endpoint, hope it helps.
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)
I want to implement a function with ClojureScript to simplify js/console.log like this:
(defn log [& args]
(apply js/console.log args))
Calling it : (log "foo" "bar")
throws: TypeError: Illegal invocation
but this works : (js/console.log "foo" "bar")
What is the problem ?
js/something is for accessing a js object, but you shouldnt nest dots after that, since it is not clojure compatible syntax and it was going to be removed. In older versions of the compiler (2138) your code works, but it is possible that it has been deprecated in newer versions of the compiler. Which version are you using?
The correct way would be using straightforward js interop like this: [Caveat: see comment below from David Nolen, ClojureScript lead developer]
(defn log [& args] (apply (.-log js/console) args))
And even shorter, since console.log is already variadic (just make an alias):
(def log (.-log js/console))
You can also just use println if you first put this at top of your file: (enable-console-print!).
And pprint has been ported:
:require [cljs.pprint :refer [pprint]]
I found the actual answer
(.apply (.-log js/console) js/console (clj->js args))
Here is a working code for your function (tested with [org.clojure/clojurescript "1.7.228"]):
; working
(defn log [& args]
(.apply js/console.log js/console (to-array args)))
; not working
; (defn log [& args] (apply (.-log js/console) args))
; (defn log [& args] (apply js/console.log args))
; (def log js/console.log)
Here is an article that describes why (apply...) is not playing well with JS functions.
http://clojurescriptmadeeasy.com/blog/how-to-apply-with-the-console-api.html
With console.log makes sense to use a macro instead of a function. If you implement log as a function all the messages will be logged with the line number of where your log function is defined.
A macro solves this problem by generating inline code during compilation, it's important to understand that macros run at compile time.
Define this macro in macros.cljc:
(ns test.macros)
(defmacro log
[& msgs]
`(.log js/console ~#msgs))
`: is like ' or quote but:
Symbols are auto-resolved to include their namespace, preventing ambiguous symbols at the time of evaluation.
Evaluated forms can be inserted using ~ or unquote, as i did for msgs adding # to unpack multiple arguments: ~#msgs. More info about syntax quote.
Then call it from core.cljs:
(ns test.core
(:require-macros [test.macros :refer [log]]))
(log "foo" "bar")