jarohen / chord

A library designed to bridge the gap between the triad of CLJ/CLJS, web-sockets and core.async.
439 stars 40 forks source link

Equivalent of http-kit's on-close? #31

Closed AlexHill closed 9 years ago

AlexHill commented 9 years ago

Hi James,

I currently have a small web service, using http-kit's websocket support, which broadcasts messages to a number of clients.

My connection handler looks like this:

(defn update-handler [request]
  (with-channel request channel
                (swap! clients conj channel)
                (on-close channel (fn [status]
                                    (swap! clients disj channel)
                                    (println "channel closed: " status)))))

I want to convert my program to use chord so I don't have to do my own transit handling, but I'm not sure how to handle client disconnections to remove dead channels from clients. As far as I can see on-close isn't exposed. Am I just thinking about this wrong? Is there some better way of doing what I want?

Sorry if this is dumb, I'm very new.

Cheers, Alex

jarohen commented 9 years ago

Hi Alex - not at all!

Chord exposes the closure of the web-socket by closing the core.async channel - we can tell when this has happened because a take from the channel will return nil. (In fact, the only time a take on a core.async channel returns nil is when it's been closed - you cannot put nil onto a channel.)

I usually wrap this up in a go-loop, which repeatedly takes from the channel - if we get a message back, then we process it in some way; otherwise, if we get nil, we know we need to clean up:

(:require [chord.http-kit :refer [with-channel]]
          [clojure.core.async :refer [go-loop <! >!]])

(def clients (atom #{}))

(defn handle-req [req]
  (with-channel req ch
    (swap! clients conj ch)

    (go-loop []
      (if-let [{:keys [message error]} (<! ch)]
        (do
          (if (nil? error)
            (process-msg! message)
            (process-error! error))
          (recur))

        ;; The channel take has returned nil
        ;; -> the channel has closed
        ;; -> the WS has been closed
        ;; -> we need to clean up
        (swap! clients disj ch)))))

HTH :)

James

AlexHill commented 9 years ago

Perfect! Thanks James.

I started with your solution which worked great. Then, since I was dealing with real core.async channels thanks to chord, my use-case allowed me to replace my clients atom with a channel and a mult and further reduce my handler to:

(defn handle-req [message-mult req]
  (with-channel req ch {:format :transit-json}
    (tap message-mult ch)))

(defroutes all-routes
  (GET "/ws" [] (partial handle-req message-mult))
  ...)

chord closes each channel when its websocket disconnects, and the mult automatically untaps closed channels. All I have to do when I want to send a message is chuck a map on message-ch, and chord makes sure that map ends up in my browser. Beautiful:

(defn handle-event [message-ch event]
  (go
    (>! message-ch (make-message event))))

This is downright amazing. I never want to go back.

Many thanks, Alex

jarohen commented 9 years ago

Great, glad to hear it! :D

James