├── .circleci └── config.yml ├── .gitignore ├── README.md ├── project.clj ├── src └── wscljs │ ├── client.cljs │ ├── format.cljs │ └── spec.cljs └── test └── wscljs ├── client_test.cljs ├── runner.clj └── runner.cljs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | # -browsers-legacy gets us PhantomJS (see https://circleci.com/docs/2.0/circleci-images/#language-image-variants) 6 | - image: circleci/clojure:lein-2.8.1-browsers-legacy 7 | 8 | working_directory: ~/repo 9 | 10 | environment: 11 | LEIN_ROOT: "true" 12 | # Customize the JVM maximum heap limit 13 | JVM_OPTS: -Xmx3200m 14 | 15 | steps: 16 | - checkout 17 | 18 | # Download and cache dependencies 19 | - restore_cache: 20 | keys: 21 | - v1-dependencies-{{ checksum "project.clj" }} 22 | # fallback to using the latest cache if no exact match is found 23 | - v1-dependencies- 24 | 25 | - run: lein deps 26 | 27 | - save_cache: 28 | paths: 29 | - ~/.m2 30 | key: v1-dependencies-{{ checksum "project.clj" }} 31 | 32 | # run tests! 33 | - run: lein test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /resources/public/js/compiled/** 2 | figwheel_server.log 3 | pom.xml 4 | *jar 5 | /lib/ 6 | /classes/ 7 | /out/ 8 | /target/ 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-repl-history 12 | .lein-plugins/ 13 | .repl 14 | .nrepl-port 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wscljs 2 | 3 | [![CircleCI](https://circleci.com/gh/nilenso/wscljs.svg?style=svg)](https://circleci.com/gh/nilenso/wscljs) [![Clojars 4 | Project](https://img.shields.io/clojars/v/nilenso/wscljs.svg)](https://clojars.org/nilenso/wscljs) [![Cljdoc badge](https://cljdoc.org/badge/nilenso/wscljs)](https://cljdoc.org/d/nilenso/wscljs/0.1.3/doc/readme) 5 | 6 | A thin and lightweight(no external dependencies) websocket client for 7 | ClojureScript. 8 | 9 | ## Why did we write this? 10 | 11 | There are already existing Clojure/Clojurescript websocket libraries like [Sente](https://github.com/ptaoussanis/sente) 12 | and [Chord](https://github.com/jarohen/chord). However these libraries support creating both websocket server and 13 | client. This requires additional dependencies which we didn't want. Wscljs is a 14 | thin wrapper over the Javascript websockets and brings in no extra 15 | dependency. 16 | 17 | ## Usage 18 | 19 | ```clojure 20 | (require '[wscljs.client :as ws] 21 | ``` 22 | 23 | To create a new websocket connection: 24 | 25 | ```clojure 26 | (def socket (ws/create "ws://...." handlers)) 27 | ``` 28 | 29 | where `handlers` is a map containing handler functions mapped to the following keys: 30 | 31 | Required: 32 | 33 | - `:on-message` => called when recieving message on the socket 34 | 35 | Optional: 36 | 37 | - `:on-open` => called when opening a socket connection 38 | - `:on-close` => called when closing a socket connection 39 | - `:on-error` => called when an error is received 40 | 41 | For example, to print the data received by the socket, do: 42 | ```clojure 43 | (def handlers {:on-message (fn [e] (prn (.-data e))) 44 | :on-open #(prn "Opening a new connection") 45 | :on-close #(prn "Closing a connection")}) 46 | (def socket (ws/create "ws://...." handlers)) 47 | ``` 48 | To send json data over the socket, do: 49 | 50 | ```clojure 51 | (require '[wscljs.format :as fmt]) 52 | 53 | (ws/send socket {:command "ping"} fmt/json) 54 | ``` 55 | 56 | **The supported formats are:** 57 | 58 | - `json` 59 | - `edn` 60 | - `identity` 61 | 62 | After you're done, close the socket: 63 | 64 | ```clojure 65 | (ws/close socket) 66 | ``` 67 | 68 | ## Setup 69 | 70 | To get an interactive development environment run: 71 | 72 | ```shell 73 | lein figwheel 74 | ``` 75 | 76 | and open your browser at [localhost:3449](http://localhost:3449/). 77 | This will auto compile and send all changes to the browser without the 78 | need to reload. After the compilation process is complete, you will 79 | get a Browser Connected REPL. An easy way to try it is: 80 | 81 | ```clojure 82 | (js/alert "Am I connected?") 83 | ``` 84 | 85 | and you should see an alert in the browser window. 86 | 87 | To clean all compiled files: 88 | 89 | ```shell 90 | lein clean 91 | ``` 92 | 93 | To create a production build run: 94 | 95 | ```shell 96 | lein do clean, cljsbuild once min 97 | ``` 98 | 99 | And open your browser in `resources/public/index.html`. You will not 100 | get live reloading, nor a REPL. 101 | 102 | ## Testing 103 | 104 | Inorder to run tests, you need to have [PhantomJS](http://phantomjs.org/) 105 | installed. After installing it, run the tests: 106 | 107 | ```shell 108 | lein test 109 | ``` 110 | 111 | *Note: I've only tested this with Phantom 2.1.1. As per [this comment](https://github.com/nilenso/wscljs/issues/4#issuecomment-435992513), using < 2.x may not work.* 112 | 113 | 114 | ## Authors 115 | 116 | - Abhik Khanra (@trycatcher) 117 | - Kiran Gangadharan (@kirang89) 118 | - Udit Kumar (@yudistrange) 119 | 120 | ## License 121 | 122 | Copyright © 2017 Nilenso Software LLP 123 | 124 | Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version. 125 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject nilenso/wscljs "0.2.0" 2 | :description "A thin and lightweight websocket client for ClojureScript." 3 | :url "https://github.com/nilenso/wscljs" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :min-lein-version "2.7.1" 8 | 9 | :dependencies [[org.clojure/clojure "1.8.0"] 10 | [org.clojure/clojurescript "1.9.908"]] 11 | 12 | :plugins [[lein-figwheel "0.5.13"] 13 | [lein-cljsbuild "1.1.7" :exclusions [[org.clojure/clojure]]] 14 | [lein-doo "0.1.7"]] 15 | 16 | :source-paths ["src"] 17 | 18 | :deploy-repositories [["clojars" {:url "https://clojars.org/repo" 19 | :sign-releases false}]] 20 | 21 | :aliases {"test" ["run" "-m" "wscljs.runner"]} 22 | 23 | :cljsbuild {:builds 24 | [{:id "dev" 25 | :source-paths ["src"] 26 | :figwheel {:on-jsload "wscljs.core/on-js-reload" 27 | :open-urls ["http://localhost:3449/index.html"]} 28 | 29 | :compiler {:main wscljs.client 30 | :asset-path "js/compiled/out" 31 | :output-to "resources/public/js/compiled/wscljs.js" 32 | :output-dir "resources/public/js/compiled/out" 33 | :source-map-timestamp true}} 34 | 35 | {:id "test" 36 | :source-paths ["test"] 37 | :compiler {:main wscljs.runner 38 | :output-to "resources/public/js/compiled/test.js" 39 | :output-dir "resources/public/js/compiled/test/out" 40 | :target :phantom 41 | :optimizations :none 42 | :process-shim false}} 43 | 44 | {:id "min" 45 | :source-paths ["src"] 46 | :compiler {:output-to "resources/public/js/compiled/wscljs.js" 47 | :main wscljs.client 48 | :optimizations :advanced 49 | :pretty-print false}}]} 50 | 51 | :figwheel {:css-dirs ["resources/public/css"]} 52 | 53 | :profiles {:dev {:dependencies [[binaryage/devtools "0.9.4"] 54 | [figwheel-sidecar "0.5.13"] 55 | [com.cemerick/piggieback "0.2.2"] 56 | [org.clojure/core.async "0.3.443"] 57 | [lein-doo "0.1.7"] 58 | [http-kit "2.2.0"]] 59 | :source-paths ["src" "dev"] 60 | :prep-tasks ["compile" ["cljsbuild" "once"]] 61 | :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]} 62 | :clean-targets ^{:protect false} ["resources/public/js/compiled" 63 | :target-path]}}) 64 | -------------------------------------------------------------------------------- /src/wscljs/client.cljs: -------------------------------------------------------------------------------- 1 | (ns wscljs.client 2 | (:require [wscljs.format :as fmt] 3 | [wscljs.spec :as ws-spec] 4 | [cljs.spec.alpha :as s])) 5 | 6 | 7 | (defn status [socket] 8 | "Retrieves the connection status of the socket." 9 | (condp = (.-readyState socket) 10 | 0 :connecting 11 | 1 :open 12 | 2 :stopping 13 | 3 :stopped)) 14 | 15 | (defn create 16 | "Starts a websocket connection and returns it. 17 | 18 | Takes the following arguments: 19 | 20 | url => the websocket url 21 | handler-map => a hashmap containing handler functions mapping to: 22 | 23 | - :on-open => called when opening a socket connection 24 | - :on-message => called when recieving message on the socket 25 | - :on-close => called when closing a socket connection 26 | 27 | Usage: 28 | 29 | (require '[wscljs.client :as ws] 30 | '[wscljs.format :as fmt]) 31 | 32 | 33 | (def socket (ws/create \"ws://....\" handler-map)) 34 | 35 | (ws/send socket data fmt/json) 36 | " 37 | [url {:keys [on-open on-message on-close on-error] :as handler-map}] 38 | {:pre [(s/valid? ::ws-spec/websocket-handler-map handler-map)]} 39 | (if-let [sock (js/WebSocket. url)] 40 | (do 41 | (set! (.-onopen sock) on-open) 42 | (set! (.-onmessage sock) on-message) 43 | (set! (.-onclose sock) on-close) 44 | (set! (.-onerror sock) on-error) 45 | sock) 46 | (throw (js/Error. (str "Web socket connection failed: " url))))) 47 | 48 | (defn send 49 | "Sends data over socket in the specified format." 50 | ([socket data] 51 | (send socket data fmt/identity)) 52 | ([socket data format] 53 | {:pre [(s/valid? ::ws-spec/websocket-open socket)]} 54 | (.send socket (fmt/write format data)))) 55 | 56 | (defn close 57 | "Closes the socket connection." 58 | [socket] 59 | (.close socket)) 60 | -------------------------------------------------------------------------------- /src/wscljs/format.cljs: -------------------------------------------------------------------------------- 1 | (ns wscljs.format 2 | (:refer-clojure :exclude [identity]) 3 | (:require [cljs.reader :as reader])) 4 | 5 | 6 | (defprotocol Format 7 | "Protocol used to define encoding format for socket messages." 8 | (read [formatter string]) 9 | (write [formatter value])) 10 | 11 | 12 | (def identity 13 | "The identity formatter. Does nothing to the input or output." 14 | (reify Format 15 | (read [_ s] s) 16 | (write [_ v] v))) 17 | 18 | 19 | (def json 20 | "Read and write data encoded in JSON." 21 | (reify Format 22 | (read [_ s] (js->clj (js/JSON.parse s) :keywordize-keys true)) 23 | (write [_ v] (js/JSON.stringify (clj->js v))))) 24 | 25 | (def edn 26 | "Read and write data serialized as EDN." 27 | (reify Format 28 | (read [_ s] (reader/read-string s)) 29 | (write [_ v] (prn-str v)))) 30 | -------------------------------------------------------------------------------- /src/wscljs/spec.cljs: -------------------------------------------------------------------------------- 1 | (ns wscljs.spec 2 | (:require [cljs.spec.alpha :as s])) 3 | 4 | (defn not-nil? [x] (not (nil? x))) 5 | 6 | (s/def ::websocket-open (s/and not-nil? 7 | #(= 1 (.-readyState %)))) 8 | 9 | (s/def ::on-message not-nil?) 10 | 11 | (s/def ::websocket-handler-map 12 | (s/keys :req-un [::on-message] 13 | :opt-un [::on-open 14 | ::on-close 15 | ::on-error])) 16 | -------------------------------------------------------------------------------- /test/wscljs/client_test.cljs: -------------------------------------------------------------------------------- 1 | (ns wscljs.client-test 2 | (:require-macros [cljs.core.async.macros :as m :refer [go]]) 3 | (:require [cljs.test :refer-macros [deftest is testing async use-fixtures]] 4 | [wscljs.client :as ws] 5 | [wscljs.format :as fmt] 6 | [cljs.core.async :refer [chan close!]])) 7 | 8 | (def wsurl "ws://localhost:3200/ws/") 9 | 10 | (deftest fact 11 | (is (= 1 1))) 12 | 13 | (defn timeout [ms] 14 | (let [c (chan)] 15 | (js/setTimeout (fn [] (close! c)) ms) 16 | c)) 17 | 18 | (deftest test-open 19 | (testing "Opening a socket connection" 20 | (async done 21 | (go 22 | (let [socket (ws/create wsurl {:on-message identity})] 23 | (is (= :connecting (ws/status socket))) 24 | (