├── .gitignore ├── LICENSE ├── README.markdown ├── dev └── user.clj ├── example ├── README.markdown ├── public │ └── index.html └── src │ ├── clj │ └── com │ │ └── keminglabs │ │ └── jetty7_websockets_async │ │ └── example │ │ ├── core.clj │ │ ├── macros.clj │ │ └── system.clj │ └── cljs │ └── com │ └── keminglabs │ └── jetty7_websockets_async │ └── example.cljs ├── project.clj ├── src └── clj │ └── com │ └── keminglabs │ └── jetty7_websockets_async │ └── core.clj ├── test └── clj │ └── com │ └── keminglabs │ └── jetty7_websockets_async │ └── t_core.clj └── todo.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | example/public/*.js 3 | pom.xml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Kevin Lynagh 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * The name Kevin Lynagh may not be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL KEVIN LYNAGH BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 24 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 26 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Jetty 7 websockets async 2 | 3 | Did you know that your familiar friend Jetty 7 (of [ring-jetty-adapter](https://github.com/ring-clojure/ring/tree/master/ring-jetty-adapter) fame) can talk websockets? 4 | This library provides a Jetty 7 configurator that exposes websockets as core.async channels. 5 | 6 | [Install](#install) | [Server quick-start](#server-quick-start) | [Client quick-start](#client-quick-start) | [Example app](/example) | [Thanks!](#thanks) | [Similar libraries](#similar-libraries) 7 | 8 | ## Install 9 | 10 | Add to your `project.clj`: 11 | 12 | [com.keminglabs/jetty7-websockets-async "0.1.0"] 13 | 14 | See the [in-depth example](example/) for fancy core.match message dispatch and a core.async client in ClojureScript. 15 | 16 | 17 | ## Server quick start 18 | 19 | ```clojure 20 | (require '[com.keminglabs.jetty7-websockets-async.core :refer [configurator]] 21 | '[clojure.core.async :refer [chan go >! ! (:in ws-req) "Hello new websocket client!") 40 | (recur)))) 41 | ``` 42 | 43 | ## Client quick start 44 | 45 | ```clojure 46 | (require '[com.keminglabs.jetty7-websockets-async.core :refer [connect!]] 47 | '[clojure.core.async :refer [chan go >! ! (:in ws-req) "Hello remote websocket server!") 56 | (recur)))) 57 | ``` 58 | 59 | ## Thanks 60 | 61 | [Zach Allaun](https://github.com/zachallaun) for suggesting that the websocket server and client code could be handled symmetrically. 62 | 63 | 64 | ## Similar libraries 65 | 66 | Take a look at @ptaoussanis's [Sente](https://github.com/ptaoussanis/sente), which provides channels over WebSockets and Ajax on the [http-kit](https://github.com/http-kit/http-kit) server. 67 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :as str] 4 | [clojure.pprint :refer [pprint]] 5 | [clojure.stacktrace :refer [e]] 6 | [clojure.tools.namespace.repl :refer [refresh refresh-all]] 7 | [com.keminglabs.jetty7-websockets-async.example.system :as system])) 8 | 9 | (def system nil) 10 | 11 | (defn init 12 | "Constructs the current development system." 13 | [] 14 | (alter-var-root #'system (constantly (system/system)))) 15 | 16 | (defn start! 17 | "Starts the current development system." 18 | [] 19 | (alter-var-root #'system system/start!)) 20 | 21 | (defn stop! 22 | "Shuts down and destroys the current development system." 23 | [] 24 | (alter-var-root #'system 25 | (fn [s] (when s (system/stop! s))))) 26 | 27 | (defn go 28 | "Initializes the current development system and starts it running." 29 | [] 30 | (init) 31 | (start!)) 32 | 33 | (defn reset [] 34 | (stop!) 35 | (refresh :after 'user/go) 36 | nil) 37 | 38 | (comment 39 | (clojure.tools.namespace.repl/refresh) 40 | (reset) 41 | 42 | 43 | 44 | (require '[com.keminglabs.jetty7-websockets-async.core :refer [configurator]] 45 | '[clojure.core.async :refer [chan go >! ! (:send ws-req) "Hello new websocket client!") 64 | (recur)))) 65 | 66 | 67 | 68 | 69 | 70 | 71 | ) -------------------------------------------------------------------------------- /example/README.markdown: -------------------------------------------------------------------------------- 1 | # Clojure/ClojureScript websocket example application 2 | 3 | This example application shows how to communicate via websockets between Clojure/ClojureScript using core.match for fancy dispatch. 4 | 5 | TODO. 6 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jetty 7 Websockets Async Example 5 | 6 | 7 | Checkout the JavaScript console, yo. 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/src/clj/com/keminglabs/jetty7_websockets_async/example/core.clj: -------------------------------------------------------------------------------- 1 | (ns com.keminglabs.jetty7-websockets-async.example.core 2 | (:require [com.keminglabs.jetty7-websockets-async.core :as ws] 3 | [clojure.core.async :refer [go !]] 4 | [clojure.core.match :refer [match]] 5 | [compojure.core :refer [routes]] 6 | [compojure.route :as route])) 7 | 8 | (def app 9 | (routes 10 | (route/files "/" {:root "example/public"}))) 11 | 12 | (defn register-ws-app! 13 | [conn-chan] 14 | (go 15 | (while true 16 | (match [(! in "Yo") 20 | (loop [] 21 | (when-let [msg (! chan close! put! take! sliding-buffer 6 | dropping-buffer timeout]] 7 | goog.net.WebSocket)) 8 | 9 | (def host 10 | (aget js/window "location" "host")) 11 | 12 | (let [socket (goog.net.WebSocket.)] 13 | 14 | (.open socket (str "ws://" host "/")) 15 | (.addEventListener socket goog.net.WebSocket.EventType.MESSAGE 16 | (fn [e] 17 | (p e) 18 | (.send socket "sup?") 19 | (.send socket "yeah.")))) 20 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.keminglabs/jetty7-websockets-async "0.1.0" 2 | :description "Clojure core.async interface to Jetty7's websockets" 3 | :url "https://github.com/lynaghk/jetty7-websockets-async" 4 | :license {:name "BSD" :url "http://www.opensource.org/licenses/BSD-3-Clause"} 5 | 6 | :dependencies [[org.clojure/clojure "1.5.1"] 7 | [org.clojure/core.async "0.1.222.0-83d0c2-alpha"] 8 | [org.eclipse.jetty/jetty-server "7.6.8.v20121106"] 9 | [org.eclipse.jetty/jetty-websocket "7.6.8.v20121106"]] 10 | 11 | :repositories {"sonatype-oss-public" "https://oss.sonatype.org/content/groups/public/"} 12 | :min-lein-version "2.0.0" 13 | 14 | :profiles {:dev {:source-paths ["dev" "example/src/clj"] 15 | :dependencies [[ring/ring-jetty-adapter "1.2.0"] 16 | [ring/ring-servlet "1.2.0"] 17 | [compojure "1.1.5" :exclusions [ring/ring-core]] 18 | [org.clojure/core.match "0.2.0-rc5"] 19 | [midje "1.5.1"]]}} 20 | 21 | :source-paths ["src/clj" "src/cljs"] 22 | :test-paths ["test/clj"] 23 | 24 | :plugins [[lein-cljsbuild "0.3.2"]] 25 | :cljsbuild {:builds 26 | [{:source-paths ["example/src/cljs"] 27 | :compiler {:output-to "example/public/example.js" 28 | :pretty-print true 29 | :optimizations :whitespace}}]}) 30 | -------------------------------------------------------------------------------- /src/clj/com/keminglabs/jetty7_websockets_async/core.clj: -------------------------------------------------------------------------------- 1 | (ns com.keminglabs.jetty7-websockets-async.core 2 | (:require [clojure.core.async :refer [go chan close! !! >! dropping-buffer]] 3 | [ring.util.servlet :as servlet]) 4 | (:import (org.eclipse.jetty.websocket WebSocket WebSocket$OnTextMessage WebSocketHandler WebSocketClientFactory) 5 | org.eclipse.jetty.server.handler.ContextHandler 6 | org.eclipse.jetty.server.handler.ContextHandlerCollection 7 | java.net.URI)) 8 | 9 | (defn default-chan [] 10 | (chan (dropping-buffer 137))) 11 | 12 | (defn ->WebSocket$OnTextMessage 13 | [connection-chan {:keys [in out] :as connection-msg}] 14 | (proxy [WebSocket$OnTextMessage] [] 15 | (onOpen [conn] 16 | (>!! connection-chan (assoc connection-msg :conn conn)) 17 | (go (loop [] 18 | (let [^String msg (!! out msg)) 28 | (onClose [close-code msg] 29 | (close! in) 30 | (close! out)))) 31 | 32 | 33 | ;;;;;;;;;;;;;;;;;; 34 | ;;WebSocket client 35 | 36 | (def ws-client-factory 37 | (WebSocketClientFactory.)) 38 | 39 | ;;TODO: how to prevent docstring duplication? 40 | (defn connect! 41 | "Tries to connect to websocket at `url`; if sucessful, places a request map on `connection-chan`. 42 | Request maps contain following keys: 43 | 44 | :uri - the string URI on which the connection was made 45 | :conn - the underlying Jetty7 websocket connection (see: http://download.eclipse.org/jetty/stable-7/apidocs/org/eclipse/jetty/websocket/WebSocket.Connection.html) 46 | :in - a core.async port where you can put string messages 47 | :out - a core.async port whence string messages 48 | 49 | Accepts the following options: 50 | 51 | :in - a zero-arg function called to create the :in port for each new websocket connection (default: a non-blocking dropping channel) 52 | :out - a zero-arg function called to create the :out port for each new websocket connection (default: a non-blocking dropping channel) 53 | " 54 | ([connection-chan url] 55 | (connect! connection-chan url {})) 56 | ([connection-chan url {:keys [in out] 57 | :or {in default-chan, out default-chan}}] 58 | 59 | ;;Start WebSocket client factory on first `connect!` call. 60 | (when-not (or (.isStarted ws-client-factory) 61 | (.isStarting ws-client-factory)) 62 | (.start ws-client-factory)) 63 | 64 | 65 | (.open (.newWebSocketClient ws-client-factory) 66 | (URI. url) 67 | (->WebSocket$OnTextMessage connection-chan 68 | {:uri url :in (in) :out (out)})))) 69 | 70 | 71 | ;;;;;;;;;;;;;;;;;; 72 | ;;WebSocket server 73 | 74 | (defn handler 75 | [connection-chan in-thunk out-thunk] 76 | (proxy [WebSocketHandler] [] 77 | (doWebSocketConnect [request response] 78 | (let [in (in-thunk) out (out-thunk)] 79 | (->WebSocket$OnTextMessage connection-chan 80 | {:request (servlet/build-request-map request) 81 | :in in 82 | :out out}))))) 83 | 84 | (defn configurator 85 | "Returns a Jetty configurator that configures server to listen for websocket connections and put request maps on `connection-chan`. 86 | 87 | Request maps contain following keys: 88 | 89 | :request - basic ring request map 90 | :conn - the underlying Jetty7 websocket connection (see: http://download.eclipse.org/jetty/stable-7/apidocs/org/eclipse/jetty/websocket/WebSocket.Connection.html) 91 | :in - a core.async port where you can put string messages 92 | :out - a core.async port whence string messages 93 | 94 | Accepts the following options: 95 | 96 | :path - the string path at which the server should listen for websocket connections (default: \"/\") 97 | :in - a zero-arg function called to create the :in port for each new websocket connection (default: a non-blocking dropping channel) 98 | :out - a zero-arg function called to create the :out port for each new websocket connection (default: a non-blocking dropping channel) 99 | " 100 | ([connection-chan] 101 | (configurator connection-chan {})) 102 | ([connection-chan {:keys [in out path] 103 | :or {in default-chan, out default-chan, path "/"}}] 104 | (fn [server] 105 | (let [ws-handler (handler connection-chan in out) 106 | existing-handler (.getHandler server) 107 | contexts (doto (ContextHandlerCollection.) 108 | (.setHandlers (into-array [(doto (ContextHandler. path) 109 | (.setAllowNullPathInfo true) 110 | (.setHandler ws-handler)) 111 | 112 | (doto (ContextHandler. "/") 113 | (.setHandler existing-handler))])))] 114 | (.setHandler server contexts))))) 115 | -------------------------------------------------------------------------------- /test/clj/com/keminglabs/jetty7_websockets_async/t_core.clj: -------------------------------------------------------------------------------- 1 | (ns com.keminglabs.jetty7-websockets-async.t-core 2 | (:require [com.keminglabs.jetty7-websockets-async.core :refer :all] 3 | [ring.adapter.jetty :refer [run-jetty]] 4 | [clojure.core.async :refer [go close! >!! chan timeout alt!!]] 5 | clojure.core.async.impl.protocols 6 | [midje.sweet :refer :all]) 7 | (:import (org.eclipse.jetty.websocket WebSocketClientFactory WebSocket WebSocket$OnTextMessage) 8 | java.net.URI)) 9 | 10 | (defn :empty 43 | (.open client (URI. (str "ws://localhost:" test-port ctx-path)) 44 | (proxy [WebSocket] [] 45 | (onOpen [_]) 46 | (onClose [_ _]))) 47 | (let [{:keys [conn in out request]} ( map? 49 | request => (contains {:uri ctx-path}) 50 | conn => #(instance? org.eclipse.jetty.websocket.WebSocket$Connection %) 51 | in => #(satisfies? clojure.core.async.impl.protocols/WritePort %) 52 | out => #(satisfies? clojure.core.async.impl.protocols/ReadPort %))) 53 | 54 | (fact "Send to client" 55 | (let [received-messages (chan)] 56 | 57 | (.open client (URI. (str "ws://localhost:" test-port ctx-path)) 58 | (proxy [WebSocket$OnTextMessage] [] 59 | (onOpen [conn]) 60 | (onClose [close-code msg]) 61 | (onMessage [msg] 62 | (>!! received-messages msg)))) 63 | 64 | (let [test-message "test-message" 65 | {:keys [in]} (!! in test-message) 67 | ( test-message))) 68 | 69 | 70 | (fact "Receive from client" 71 | (let [test-message "test-message"] 72 | (.open client (URI. (str "ws://localhost:" test-port ctx-path)) 73 | (proxy [WebSocket$OnTextMessage] [] 74 | (onOpen [conn] 75 | (.sendMessage conn test-message)) 76 | (onClose [close-code msg]) 77 | (onMessage [msg]))) 78 | 79 | (let [{:keys [out]} ( test-message))))) 81 | 82 | 83 | 84 | (fact "Websocket client" 85 | (with-state-changes [(around :facts 86 | (let [server-new-connections (chan) 87 | server (run-jetty nil {:configurator (configurator server-new-connections) 88 | :port test-port :join? false})] 89 | 90 | (go ;;echo first message back to client 91 | (let [{:keys [in out]} (! in ( :empty 104 | (connect! new-connections echo-url) 105 | (let [{:keys [conn in out uri] :as a} ( echo-url 107 | conn => #(instance? org.eclipse.jetty.websocket.WebSocket$Connection %) 108 | in => #(satisfies? clojure.core.async.impl.protocols/WritePort %) 109 | out => #(satisfies? clojure.core.async.impl.protocols/ReadPort %))) 110 | 111 | (fact "Echo" 112 | (connect! new-connections echo-url) 113 | (let [test-message "test-message" 114 | {:keys [in out]} (!! in test-message) 116 | ( test-message)))) 117 | -------------------------------------------------------------------------------- /todo.markdown: -------------------------------------------------------------------------------- 1 | Accept binary messages 2 | --------------------------------------------------------------------------------