[
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Tests\non: [push, pull_request]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Prepare java\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu'\n          java-version: '8'\n\n      - name: Prepare node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'npm'\n\n      - name: Install NPM dependencies\n        run: npm ci\n\n      - name: Install clojure tools\n        uses: DeLaGuardo/setup-clojure@12.5\n        with:\n          lein: 2.9.10\n\n      - name: Cache clojure dependencies\n        uses: actions/cache@v4\n        with:\n          path: ~/.m2/repository\n          key: cljdeps-${{ hashFiles('project.clj') }}\n          restore-keys: cljdeps-\n\n      - name: Run tests\n        run: lein test\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n/classes\n/checkouts\npom.xml\npom.xml.asc\n*.jar\n*.class\n/.lein-*\n/.nrepl-port\n.hgignore\n.hg/\n/node_modules\n/.clj-kondo\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: clojure\ndist: trusty\naddons:\n  chrome: stable\ninstall:\n  - npm install karma karma-cljs-test --save-dev\n  - npm install -g karma-cli\n  - npm install karma-chrome-launcher --save-dev\nscript:\n  - lein test\n"
  },
  {
    "path": "README.md",
    "content": "# Haslett [![Build Status](https://github.com/weavejester/haslett/actions/workflows/test.yml/badge.svg)](https://github.com/weavejester/haslett/actions/workflows/test.yml)\n\nA lightweight WebSocket library for ClojureScript that uses\n[core.async][].\n\n[core.async]: https://github.com/clojure/core.async\n\n## Installation\n\nAdd the following dependency to your deps.edn file:\n\n    haslett/haslett {:mvn/version \"0.2.0\"}\n\nOr to your Leiningen project file:\n\n    [haslett \"0.2.0\"]\n\n## Usage\n\nHaslett provides a simple and idiomatic interface to using WebSockets:\n\n```clojure\n(ns example.core\n  (:require [cljs.core.async :as a :refer [<! >! go]]\n            [haslett.client :as ws]\n            [haslett.format :as fmt]))\n\n(go (let [stream (<! (ws/connect \"ws://echo.websocket.org\"))]\n      (>! (:out stream) \"Hello World\")\n      (js/console.log (<! (:in stream)))\n      (ws/close stream)))\n```\n\nThe `connect` function returns a promise channel that produces a map\nwith four keys: `:socket`, `:close-status`, `:in` and `:out`.\n\n* `:socket` contains the JavaScript `WebSocket` object, in case you need\nto access it directly.\n\n* `:close-status` contains a promise channel that a status map is\ndelivered to when the socket is closed. The status map will provide a\n`:code` and `:reason` keys that will explain why the socket was\nclosed.\n\n* `:in` is a core.async channel to read from.\n\n* `:out` is a core.async channel to write to.\n\nBy default, Haslett sends raw strings, but we can change that by\nsupplying a formatter. Haslett includes formatters for JSON, edn and\nTransit:\n\n```clojure\n(go (let [stream (<! (ws/connect \"ws://echo.websocket.org\" {:format fmt/transit}))]\n      (>! (:out stream) {:foo [1 2 3]})\n      (js/console.log (pr-str (<! (:in stream))))\n      (ws/close stream)))\n```\n\nYou can customize the behaviour further by supplying your own channels\nfor the source and sink. This allows you to tune the channel buffer,\nand add transducers:\n\n```clojure\n(ws/connect \"ws://echo.websocket.org\"\n            {:in  (a/chan 10)\n             :out (a/chan 10)})\n```\n\nWhen the WebSocket is closed, the `:out` and `:in` channels are\nalso closed. In addition, a final status map will be delivered to a\npromise channel held in the `:close-status` key on the stream.\n\n## License\n\nCopyright © 2024 James Reeves\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"devDependencies\": {\n    \"karma\": \"^6.4.3\",\n    \"karma-cljs-test\": \"^0.1.0\",\n    \"karma-firefox-launcher\": \"^2.1.3\"\n  }\n}\n"
  },
  {
    "path": "project.clj",
    "content": "(defproject haslett \"0.2.0\"\n  :description \"A lightweight WebSocket library for ClojureScript\"\n  :url \"https://github.com/weavejester/haslett\"\n  :license {:name \"Eclipse Public License\"\n            :url \"http://www.eclipse.org/legal/epl-v10.html\"}\n  :dependencies [[org.clojure/clojure \"1.11.3\"]\n                 [org.clojure/clojurescript \"1.11.132\"]\n                 [org.clojure/core.async \"1.6.681\"]\n                 [com.cognitect/transit-cljs \"0.8.280\"]]\n  :plugins [[lein-cljsbuild \"1.1.8\"]]\n  :aliases {\"test\" [\"run\" \"-m\" \"haslett.test-runner\"]}\n  :profiles\n  {:dev {:dependencies [[doo \"0.1.11\"]\n                        [http-kit \"2.8.0\"]]\n         :prep-tasks   [\"compile\" [\"cljsbuild\" \"once\"]]\n         :cljsbuild\n         {:builds\n          {:test\n           {:source-paths [\"src\" \"test\"]\n            :compiler {:output-to \"target/main.js\"\n                       :output-dir \"target\"\n                       :main haslett.test-runner\n                       :optimizations :none}}}}}})\n"
  },
  {
    "path": "src/haslett/client.cljs",
    "content": "(ns haslett.client\n  \"A namespace for opening WebSockets in ClojureScript.\"\n  (:require [cljs.core.async :as a :refer [<! go-loop]]\n            [haslett.format :as fmt]))\n\n(defn close\n  \"Close a stream opened by connect.\"\n  [stream]\n  (.close (:socket stream) 1000 \"Closed by creator\")\n  (:close-status stream))\n\n(defn connect\n  \"Create a WebSocket to the specified URL, and returns a 'stream' map of four\n  keys:\n\n    :socket       - contains the WebSocket object\n    :close-status - a promise channel that contains the final close status\n    :in           - a core.async channel to read from\n    :out          - a core.async channel to write to\n\n  Takes the following options:\n\n    :format      - a formatter from haslett.format\n    :in          - a custom channel to use as the reader\n    :out         - a custom channel to use as the writer\n    :protocols   - passed to the WebSocket, a vector of protocol strings\n    :binary-type - passed to the WebSocket, may be :blob or :arraybuffer\n    :close-chan? - true if channels should be closed if WebSocket is closed\n                   (defaults to true)\n\n  The WebSocket may either be closed directly, or by closing the\n  stream's :sink channel.\"\n  ([url]\n   (connect url {}))\n  ([url options]\n   (let [protocols (into-array (:protocols options []))\n         socket    (js/WebSocket. url protocols)\n         in        (:in options (a/chan))\n         out       (:out options (a/chan))\n         format    (:format options fmt/identity)\n         status    (a/promise-chan)\n         return    (a/promise-chan)\n         close?    (:close-chan? options true)\n         stream    {:socket socket, :in in, :out out, :close-status status}]\n     (set! (.-binaryType socket) (name (:binary-type options :arraybuffer)))\n     (set! (.-onopen socket)     (fn [_] (a/put! return stream)))\n     (set! (.-onmessage socket)\n           (fn [e] (a/put! in (fmt/read format (.-data e)))))\n     (set! (.-onclose socket)\n           (fn [e]\n             (a/put! status {:reason (.-reason e), :code (.-code e)})\n             (when close? (a/close! in))\n             (when close? (a/close! out))\n             (a/put! return stream)))\n     (go-loop []\n       (when-let [msg (<! out)]\n         (.send socket (fmt/write format msg))\n         (recur))\n       (close stream))\n     return)))\n\n(defn connected?\n  \"Return true if the stream is currently connected.\"\n  [{:keys [close-status]}]\n  (nil? (a/poll! close-status)))\n"
  },
  {
    "path": "src/haslett/format.cljs",
    "content": "(ns haslett.format\n  \"A namespace containing formatters that read and write information from\n  WebSocket streams. Used with haslett.client/connect.\"\n  (:refer-clojure :exclude [identity])\n  (:require [cljs.reader :as edn]\n            [cognitect.transit :as transit]))\n\n(defprotocol Format\n  \"The format protocol.\"\n  (read  [formatter string])\n  (write [formatter value]))\n\n(def identity\n  \"The identity formatter. Does nothing to the input or output.\"\n  (reify Format\n    (read  [_ s] s)\n    (write [_ v] v)))\n\n(def transit\n  \"Read and write data encoded in transit+json.\"\n  (reify Format\n    (read  [_ s] (transit/read (transit/reader :json) s))\n    (write [_ v] (transit/write (transit/writer :json) v))))\n\n(def edn\n  \"Read and write data encoded in edn.\"\n  (reify Format\n    (read  [_ s] (edn/read-string s))\n    (write [_ v] (pr-str v))))\n\n(def json\n  \"Read and write data encoded in JSON.\"\n  (reify Format\n    (read  [_ s] (js->clj (js/JSON.parse s)))\n    (write [_ v] (js/JSON.stringify (clj->js v)))))\n"
  },
  {
    "path": "test/haslett/client_test.cljs",
    "content": "(ns haslett.client-test\n  (:require [cljs.test :refer-macros [deftest is async]]\n            [cljs.core.async :as a :refer [<! >! go]]\n            [cljs.core.async.impl.protocols :as ap]\n            [haslett.client :as ws]\n            [haslett.format :as fmt]))\n\n(deftest test-defaults\n  (async done\n    (go (let [stream (<! (ws/connect \"ws://localhost:3200\"))]\n          (is (ws/connected? stream))\n          (>! (:out stream) \"Hello World\")\n          (is (= (<! (:in stream)) \"Hello World\"))\n          (ws/close stream)\n          (done)))))\n\n(deftest test-transit\n  (async done\n    (go (let [stream (<! (ws/connect \"ws://localhost:3200\"\n                                     {:format fmt/transit}))]\n          (>! (:out stream) {:hello \"World\"})\n          (is (= (<! (:in stream)) {:hello \"World\"}))\n          (ws/close stream)\n          (done)))))\n\n(deftest test-edn\n  (async done\n    (go (let [stream (<! (ws/connect \"ws://localhost:3200\" {:format fmt/edn}))]\n          (>! (:out stream) {:hello \"World\"})\n          (is (= (<! (:in stream)) {:hello \"World\"}))\n          (ws/close stream)\n          (done)))))\n\n(deftest test-json\n  (async done\n    (go (let [stream (<! (ws/connect \"ws://localhost:3200\" {:format fmt/json}))]\n          (>! (:out stream) {:hello \"World\"})\n          (is (= (<! (:in stream)) {\"hello\" \"World\"}))\n          (ws/close stream)\n          (done)))))\n\n(deftest test-close\n  (async done\n    (go (let [stream (<! (ws/connect \"ws://localhost:3200\"))]\n          (is (= (<! (ws/close stream))\n                 {:code 1000, :reason \"Closed by creator\"}))\n          (is (= (<! (:close-status stream))\n                 {:code 1000, :reason \"Closed by creator\"}))\n          (done)))))\n\n(deftest test-connection-fail\n  (async done\n    (go (let [stream (<! (ws/connect \"ws://localhost:3201\"))]\n          (is (not (ws/connected? stream)))\n          (is (ap/closed? (:out stream)))\n          (is (ap/closed? (:in stream)))\n          (is (= (<! (:close-status stream)) {:code 1006, :reason \"\"}))\n          (done)))))\n\n(deftest test-local-close\n  (async done\n    (go (let [stream (<! (ws/connect \"ws://localhost:3200\"))]\n          (a/close! (:out stream))\n          (is (= (<! (:close-status stream))\n                 {:code 1000, :reason \"Closed by creator\"}))\n          (is (ap/closed? (:in stream)))\n          (done)))))\n\n(deftest test-chans-not-closed\n  (async done\n    (go (let [stream (<! (ws/connect \"ws://localhost:3200\"\n                                     {:close-chan? false}))]\n          (ws/close stream)\n          (is (= (<! (:close-status stream))\n                 {:code 1000, :reason \"Closed by creator\"}))\n          (is (not (ap/closed? (:out stream))))\n          (is (not (ap/closed? (:in stream))))\n          (done)))))\n"
  },
  {
    "path": "test/haslett/test_runner.clj",
    "content": "(ns haslett.test-runner\n  (:require [doo.core :as doo]\n            [org.httpkit.server :as httpkit]))\n\n(def doo-opts\n  {:paths {:karma \"node_modules/karma/bin/karma\"}})\n\n(def compiler-opts\n  {:output-to \"target/main.js\"\n   :output-dir \"target\"\n   :main 'haslett.test-runner\n   :optimizations :none})\n\n(defn echo-handler [request]\n  (httpkit/with-channel request channel\n    (httpkit/on-receive channel (fn [data] (httpkit/send! channel data)))))\n\n(defn run-server []\n  (httpkit/run-server echo-handler {:port 3200}))\n\n(defn run-tests []\n  (doo/run-script :firefox-headless compiler-opts doo-opts))\n\n(defn -main []\n  (println \"Starting server\")\n  (let [stop-server (run-server)]\n    (println \"Running tests\")\n    (run-tests)\n    (println \"Stopping server\")\n    (stop-server)\n    (shutdown-agents)))\n"
  },
  {
    "path": "test/haslett/test_runner.cljs",
    "content": "(ns haslett.test-runner\n  (:require [doo.runner :refer-macros [doo-tests]]\n            [haslett.client-test]))\n\n(doo-tests 'haslett.client-test)\n"
  }
]