(ns ird.p2p.impl.ws
"One WebSocket over the `ws` npm package. `open` is the default
:socket-fn of ird.p2p.transport.ws: it connects, sends the Origin header
when configured, and delivers a socket map
{:in channel of {:type :text :data <string>} / {:type :binary :data <b64>};
closes when the socket closes
:send! (fn [text]) → nil, or an error value when the socket is gone
:close! (fn [code reason]) → nil}
or an error: :ird.p2p/upgrade-rejected with the HTTP :status when the
server refused the upgrade (403 origin, 404 unknown host, 429 too many
connections), :ird.p2p/transport-error otherwise.
Internal interop namespace."
(:require [cljs.core.async :as async]
[ird.p2p.async :as a]
[ird.p2p.errors :as err]
[ird.p2p.impl.bytes :as bytes]
[ird.p2p.impl.node :as node]))
(def ^:private ws-module (delay (js/require "ws")))
(defn open
"Connect to URL. OPTS: :origin (Origin header), :timeout-ms (handshake
deadline, default 15000). Channel of a socket map or an error value."
[{:keys [url origin timeout-ms]}]
(let [out (async/promise-chan)
{:keys [put! close!] in :out} (a/unbounded-chan)
WebSocket (.-WebSocket @ws-module)
headers (js-obj)
_ (when origin (aset headers "Origin" origin))
sock (try (WebSocket. url #js {:headers headers
:handshakeTimeout (or timeout-ms 15000)
:perMessageDeflate false})
(catch :default e e))
settled (atom false)
settle! (fn [v] (when (compare-and-set! settled false true) (async/put! out v)))]
(if (node/error? sock)
(do (async/put! out (err/transport-error (str "WebSocket failed: " (node/error-message sock)) {:url url}))
out)
(do
(.on sock "unexpected-response"
(fn [_req res]
(let [status (.-statusCode res)]
(.resume res)
(settle! (err/upgrade-rejected status))
(.terminate sock))))
(.on sock "error"
(fn [e]
(settle! (err/transport-error (str "WebSocket error: " (node/error-message e)) {:url url}))))
(.on sock "close" (fn [_code _reason] (close!)))
(.on sock "message"
(fn [data binary?]
(if binary?
(put! {:type :binary :data (bytes/buffer->b64 data)})
(put! {:type :text :data (.toString data "utf8")}))))
(.on sock "open"
(fn []
(settle! {:in in
:send! (fn [text]
(try (.send sock text) nil
(catch :default e
(err/transport-error (str "WebSocket send failed: " (node/error-message e)) {}))))
:close! (fn [code reason]
(try (.close sock (or code 1000) (or reason "")) (catch :default _ nil))
nil)})))
out))))