/fulcro-websockets

A Pluggable Remote for Fulcro 3 that uses Websockets

Primary LanguageClojure

logo

fulcro websockets

A websocket remote for use with Fulcro 3 applications.

1. Version Notes

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+

2. Usage

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

2.1. Server

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]
    [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))

(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! server
      (fn []
        (fws/stop! websockets)
        (web/stop result)))))

(comment

  ;; start the server
  (http-server)

  ;; stop the server
  (@server))

2.1.1. Using augment-response

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.

2.2. Client (Before 3.3)

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 {})}}))

2.3. Client (3.3.0+)

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.

2.4. Connected Clients and Server Push

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.

3. Custom Type Support

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.