├── .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 |
--------------------------------------------------------------------------------