Giter VIP home page Giter VIP logo

fulcro-websockets's Introduction

logo

fulcro websockets

A websocket remote for use with Fulcro 3 applications.

Standardized custom type support was added in Fulcro 3.3.5. Newer versions of this library requires a minimum version of Fulcro.

  • Fulcro 3.3.4 and below: Use fulcro-websockets version 3.1.x

  • Fulcro 3.3.6 and above: Use fulcro-websockets version 3.2.0+

Add the proper dependencies to your project. You’ll need sente, some Ring bits, and of course Fulcro 3.

The server setup is mostly just standard Ring fare.

(ns my-app.websocket-server
  (:require
    [com.fulcrologic.fulcro.server.api-middleware :refer [not-found-handler]]
    [com.fulcrologic.fulcro.networking.websockets :as fws]
    [com.fulcrologic.fulcro.networking.websocket-protocols :as fwsp]
    [immutant.web :as web]
    [ring.middleware.content-type :refer [wrap-content-type]]
    [ring.middleware.not-modified :refer [wrap-not-modified]]
    [ring.middleware.resource :refer [wrap-resource]]
    [ring.middleware.params :refer [wrap-params]]
    [ring.middleware.keyword-params :refer [wrap-keyword-params]]
    [ring.util.response :refer [response file-response resource-response]]
    [taoensso.sente.server-adapters.immutant :refer [get-sch-adapter]]))

(def server (atom nil))
(def ws (atom nil))

(defn query-parser
  ""
  [env query]
  ;; call out to something like a pathom parser. See Fulcro Developers Guide.
  ;; (note that the ring request, which you may want e.g. for session information, is under :request)
  )

(defn http-server []
  (let [websockets (fws/start! (fws/make-websockets
                                 query-parser
                                 {:http-server-adapter (get-sch-adapter)
                                  :parser-accepts-env? true
                                  ;; See Sente for CSRF instructions. If you are using ring-defaults,
                                  ;; you will likely want {:csrf-token-fn :anti-forgery-token} here.
                                  :sente-options       {:csrf-token-fn nil}}))
        middleware (-> not-found-handler
                     (fws/wrap-api websockets)
                     wrap-keyword-params
                     wrap-params
                     (wrap-resource "public")
                     wrap-content-type
                     wrap-not-modified)
        result     (web/run middleware {:host "0.0.0.0"
                                        :port 3000})]
    (reset! ws websockets)
    (reset! server
      (fn []
        (fws/stop! websockets)
        (web/stop result)))))

(comment

  ;; start the server
  (http-server)

  ;; stop the server
  (@server)

  ;; send a push event
  (fwsp/push @ws some-client-id :x {:greeting "hello"}))

Some additional configuration is required in order to have augment-response work with the fulcro-websockets backend. The standard Fulcro middleware provides a function called augment-response which is used to give Pathom resolvers an easy way to write changes to the outgoing Ring response. For example, augment-response is commonly used to make changes to cookies or sessions. Using the normal Fulcro remote, these changes are written when the relevant middleware (e.g. wrap-session) processes the response. With a fulcro-websockets remote, however, response processing is pre-empted as soon as the websocket middleware is reached, since the response will be given via the websocket connection instead of as an HTTP response. This means that any middleware that is not as deep as the fulcro-websockets middleware will not get to process the response—​e.g., wrap-session will not get to side-effect to write changes to the session store.

Sente’s maintainers offer two possible workarounds: first is to hold on to a reference of the relevant mutable state (e.g. the session store) and use that directly somewhere to make any necessary changes. Second is to use a non-websocket endpoint (e.g. a standard Fulcro AJAX remote) to process only the requests which need to make session/cookies changes.

The client setup is even simpler. Just add the websocket remote as one of your remotes:

(ns my-app.client-main
  (:require
    [com.fulcrologic.fulcro.networking.websockets :as fws]
    [com.fulcrologic.fulcro.application :as app]))

(defonce app (app/fulcro-app {:remotes {:remote (fws/fulcro-websocket-remote {})}}))

Sente, the underlying websockets library, added CLJ support for clients in version 1.15. This meant it was trivial to add support for WS remotes for use in Fulcro CLJ clients. Granted, there is no React in CLJ, but Fulcro’s internals are usable in a headless mode, or you could plug in CLJ rendering of your own design.

Unfortunately, prior versions of this library put the server-side code in websockets.clj and the client remote in websockets.cljs, meaning we could not just make the client file CLJC. Thus, there is a new, preferred, namespace to use for a Fulcro remote that works in both CLJ and CLJS Fulcro clients.

The only change is in the require:

(ns my-app.client-main
  (:require
    [com.fulcrologic.fulcro.networking.websocket-remote :as fws]
    [com.fulcrologic.fulcro.application :as app]))

(defonce app (app/fulcro-app {:remotes {:remote (fws/fulcro-websocket-remote {})}}))

The old namespace still exists and still has the 3.2.1 code in it, but will not work in CLJC.

The client ID management is done by Sente. The websockets component you started above has a few conveniences for you to make monitoring client connections and doing sever pushes a little easier.

To use server push you need to store your started websockets component in an atom or some other globally accessible storage (e.g. mount’s defstate). The websocket component implements a protocol that allows you to listen to client connections, and also to send push messages:

(defprotocol WSListener
  (client-added [this ws-net cid]
    "Listener for dealing with client added events.")
  (client-dropped [this ws-net cid]
    "listener for dealing with client dropped events."))

(defprotocol WSNet
  (add-listener [this ^WSListener listener]
    "Add a `WSListener` listener")
  (remove-listener [this ^WSListener listener]
    "Remove a `WSListener` listener")
  (push [this cid verb edn] "Push from server"))

So, (add-listener websockets my-listener) will add a component that you’ve written that satisfies the WSListener protocol to receive notifications when clients connect/disconnect.

The (push websockets cid :x {:value 1}) call will look up the websocket associated with the client that has cid and send the given top/value message to it. Then env of mutations and resolver (if you enable :parser-accepts-env?) will include a :cid key, so you can find out who is talking to you through normal API interactions.

Fulcro 3.3.6+ added support for full-stack cross-language (clj/cljs) standardization of data model type extensions. See the version notes at the beginning of this document.

Important
You MUST install your custom types before creating any websocket artifacts on the client or server. The websocket mechanisms have to have a protocol "packer" when they are created, which means we have to read the custom type support when they are created.

See the Fulcro book for more information on defining custom type support.

fulcro-websockets's People

Contributors

awkay avatar currentoor avatar holyjak avatar janezj avatar lgessler avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

fulcro-websockets's Issues

Using websockets with UI State Machines

How do you plug in fulcro-websockets with UI State Machines?

I've started at the fulcro template:

git clone --depth 1 -o fulcro-template https://github.com/fulcrologic/fulcro-template.git fulcro-app
cd fulcro-app
npm install

Added fulcro-websockets into deps.edn:

com.fulcrologic/fulcro-websockets   {:mvn/version "3.1.1"}

Created app.server-components.websockets ns:

(ns app.server-components.websockets
  (:require [mount.core :refer [defstate]]
            [com.fulcrologic.fulcro.networking.websockets :as fws]
            [taoensso.sente.server-adapters.http-kit :refer [get-sch-adapter]]
            [app.server-components.pathom :refer [parser]]))


(defstate websockets
  :start
  (fws/start! (fws/make-websockets
                parser
                {:http-server-adapter (get-sch-adapter)
                 :parser-accepts-env? true
                 :sente-options       {:csrf-token-fn nil}}))
  :stop
  (fws/stop! websockets))

(defn wrap-websockets [handler]
  (fws/wrap-api handler websockets))

Edited app.server-components.middleware and added an entry in middleware for wrap-websockets:

(ns app.server-components.middleware
  (:require [mount.core :refer [defstate]]
            [com.fulcrologic.fulcro.server.api-middleware :refer [handle-api-request
                                                                  wrap-transit-params
                                                                  wrap-transit-response]]
            [ring.middleware.defaults :refer [wrap-defaults]]
            [ring.middleware.gzip :refer [wrap-gzip]]
            [ring.util.response :refer [response file-response resource-response]]
            [ring.util.response :as resp]
            [hiccup.page :refer [html5]]
            [taoensso.timbre :as log]
            [app.server-components.config :refer [config]]
            [app.server-components.pathom :refer [parser]]
            [app.server-components.websockets :refer [wrap-websockets]])) ; <--- ADDED AT THE END

...

(defstate middleware
  :start
  (let [defaults-config (:ring.middleware/defaults-config config)
        legal-origins   (get config :legal-origins #{"localhost"})]
    (-> not-found-handler
      (wrap-api "/api")
      (wrap-websockets) ; <--- STUCK THIS IN
      wrap-transit-params
      wrap-transit-response
      (wrap-html-routes)
      ;; If you want to set something like session store, you'd do it against
      ;; the defaults-config here (which comes from an EDN file, so it can't have
      ;; code initialized).
      ;; E.g. (wrap-defaults (assoc-in defaults-config [:session :store] (my-store)))
      (wrap-defaults defaults-config)
      wrap-gzip)))

Edited app.application's definition of SPA:

(defonce SPA (app/fulcro-app
               {;; This ensures your client can talk to a CSRF-protected server.
                ;; See middleware.clj to see how the token is embedded into the HTML
                :remotes {:remote (net/fulcro-http-remote
                                    {:url                "/api"
                                     :request-middleware secured-request-middleware})
                          :ws-remote (fws/fulcro-websocket-remote {})}}))

Signup works fine if I change remote to ws-remote in app.model.session:

(defmutation signup! [_]
  (action [{:keys [state]}]
    (log/info "Marking complete")
    (swap! state fs/mark-complete* signup-ident))
  (ok-action [{:keys [app state]}]
    (dr/change-route app ["signup-success"]))
  (ws-remote [{:keys [state] :as env}]    ; <-- Just changed it to match the keyword
    (let [{:account/keys [email password password-again]} (get-in @state signup-ident)]
      (boolean (and (valid-email? email) (valid-password? password)
                 (= password password-again))))))

But if I do:

(defn login [{::uism/keys [event-data] :as env}]
  (-> env
    (clear)
    (uism/trigger-remote-mutation :actor/login-form 'app.model.session/login
      {:username          (:username event-data)
       :password          (:password event-data)
       ::m/returning      (uism/actor-class env :actor/current-session)
       ::uism/ok-event    :event/complete
       ::uism/error-event :event/failed
       ::uism/mutation-remote :ws-remote})
    (uism/activate :state/checking-session)))

It fails to login.

I don't think it's a CSRF error, nor do I think it's a transit error, I can see the websocket sending/receiving data fine:

↑ +[["~:fulcro.client/API",[["~#cmap",[["~#list",["~$app.model.session/login",["^ ","~:password","testpass","~:username","[email protected]"]]],["~:session/valid?","~:account/name...
↓+[["^ ","~:status",200,"~:body",["^ ","~:com.wsscode.pathom/trace",["^ ","~:start",5010,"~:path",[],"~:duration",58,"~:details",[["^ ","~:event","process-pending","^5",0,"^3",5025,...

Which is also in the Fulcro Inspect Network tab:

{(app.model.session/login
  {:password "testpass", :username "[email protected]"})
 [:session/valid? :account/name]}
{:session/valid? true, :account/name "[email protected]"}

The form however still display's Invalid Credentials.

Looking into the code, I started printing out process-session-result:

(defn process-session-result [env error-message]
  (println :process-session-result (pr-str (uism/alias-value env :session-valid?)) (pr-str (:com.fulcrologic.fulcro.ui-state-machines/event-data env)))
  (let [success? (uism/alias-value env :session-valid?)]
    (when success?
      (dr/change-route SPA ["main"]))
    (cond-> (clear env)
      success? (->
                 (uism/assoc-aliased :modal-open? false)
                 (uism/activate :state/logged-in))
      (not success?) (->
                       (uism/assoc-aliased :error error-message)
                       (uism/activate :state/logged-out)))))

Which gives me:

false {:com.fulcrologic.fulcro.ui-state-machines/mutation-result {:status-code 200, :body {app.model.session/login {:session/valid? true, :account/name "[email protected]"}}}}

So I'm not yet clear how uism/alias-value works in this context, but it appears that the mutation result is correctly returning, unless I've misunderstood something?

websocket disconnects can hang Fulcro

Websockets have a timeout because the way they work on a round trip request is to send the frame out, wait for the timeout, and get a resp back. Often you want to set the timeout high for things that can actually take some time, but if there is a disconnect (i.e. server restart) it is obvious you'll never get that response. Websockets should immediately consider a disconnect as a notice that there is no need to wait on the timeout and the client code can become unblocked immediately.

NOTE: The core async code will probably still receive the timeout, so make sure that extra "fail" does not get reported more than once.

Need to add back lifecycle.

Using websockets with things like workspaces breaks things because too many websockets get created if you have a lot of cards, even if not mounted.

Adding the docs for websockets

I noticed that the logo on the README isn't being rendered completely and then I realized that the reason is that it points to a non-existent docs folder in the repo.

I checked further in the fulcro-legacy org but there I could only find the examples. I guess, this could be added in the list of ToDo we have. Using this issue as a reminder for that, please feel free to close it in case there's already some work done in this direction.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.