Skip to content

Commit e330ef2

Browse files
committed
[new] Allow WebSocket constructors to delay connection
Previously: WebSocket constructors returned a connected WebSocket. After this commit: WebSocket constructors may return: - nil (=> no WebSocket support), or - a delay (=> deref to return a connected WebSocket) In other words, we've decoupled construction and connection. The major advantage of this change is that it's now possible for the default `ChAutoSocket` (:auto type chsk) to distinguish between two kinds of errors: 1. The platform doesn't support WebSockets (=> fall back to Ajax) 2. The platform does support WebSockets, but current connection attempts are failing due to transient reasons like the internet being temporarily unavailable (=> keep retrying) Note: the new capability is implemented in such a way that we won't break pre-existing custom constructors. While constructors should ideally now return an unconnected delay, they MAY still directly return an already-connected socket.
1 parent 6021258 commit e330ef2

File tree

1 file changed

+111
-108
lines changed

1 file changed

+111
-108
lines changed

src/taoensso/sente.cljc

+111-108
Original file line numberDiff line numberDiff line change
@@ -1278,26 +1278,29 @@
12781278
#?(:clj
12791279
(defn- make-client-ws-java
12801280
[{:as opts :keys [uri-str headers on-error on-message on-close]}]
1281-
(let [uri (java.net.URI. uri-str)
1282-
1283-
;; headers
1284-
;; (ImmutableMap/of
1285-
;; "Origin" "http://localhost:3200"
1286-
;; "Referer" "http://localhost:3200"
1287-
;; "Sec-WebSocket-Extensions" "permessage-deflate; client_max_window_bits"
1288-
;; )
1289-
1290-
ws-client
1291-
(proxy [WebSocketClient] [^java.net.URI uri ^java.util.Map headers]
1292-
(onOpen [^org.java_websocket.handshake.ServerHandshake handshakedata] nil)
1293-
(onError [ex] (on-error ex))
1294-
(onMessage [^String message] (on-message message))
1295-
(onClose [code reason remote] (on-close code reason remote)))]
1296-
1297-
;; JS client attempts to connect right away at construction time.
1298-
;; Java client doesn't need to, but we'll do anyway for consistency.
1299-
(.connect ws-client)
1300-
(do ws-client))))
1281+
(when-let [ws-client
1282+
(try
1283+
(let [uri (java.net.URI. uri-str)
1284+
#_headers
1285+
#_
1286+
(ImmutableMap/of
1287+
"Origin" "http://localhost:3200"
1288+
"Referer" "http://localhost:3200"
1289+
"Sec-WebSocket-Extensions" "permessage-deflate; client_max_window_bits")]
1290+
1291+
(proxy [WebSocketClient] [^java.net.URI uri ^java.util.Map headers]
1292+
(onOpen [^org.java_websocket.handshake.ServerHandshake handshakedata] nil)
1293+
(onError [ex] (on-error ex))
1294+
(onMessage [^String message] (on-message message))
1295+
(onClose [code reason remote] (on-close code reason remote))))
1296+
1297+
(catch Throwable t
1298+
(timbre/errorf t "Error creating Java WebSocket client")
1299+
nil))]
1300+
1301+
(delay
1302+
(.connect ws-client)
1303+
(do ws-client)))))
13011304

13021305
#?(:cljs
13031306
(defn- make-client-ws-js
@@ -1308,19 +1311,21 @@
13081311
(enc/oget goog/global "MozWebSocket")
13091312
(enc/oget @?node-npm-websocket_ "w3cwebsocket"))]
13101313

1311-
(let [socket (WebSocket. uri-str)]
1312-
(doto socket
1313-
(aset "onerror" on-error)
1314-
(aset "onmessage" on-message) ; Nb receives both push & cb evs!
1315-
;; Fires repeatedly (on each connection attempt) while server is down:
1316-
(aset "onclose" on-close))
1314+
(delay
1315+
(let [socket (WebSocket. uri-str)]
1316+
(doto socket
1317+
(aset "onerror" on-error)
1318+
(aset "onmessage" on-message) ; Nb receives both push & cb evs!
1319+
;; Fires repeatedly (on each connection attempt) while server is down:
1320+
(aset "onclose" on-close))
13171321

1318-
(when-let [bt binary-type] ; "arraybuffer" or "blob" (js default)
1319-
(aset socket "binaryType" bt))
1320-
1321-
socket))))
1322+
(when-let [bt binary-type] ; "arraybuffer" or "blob" (js default)
1323+
(aset socket "binaryType" bt))
1324+
socket)))))
13221325

13231326
(defn- default-client-ws-constructor
1327+
"Returns nil if WebSocket client cannot be created, or a delay
1328+
that can be derefed to get a connected client."
13241329
[{:as opts :keys [on-error on-message on-close uri-str headers]}]
13251330
#?(:cljs (make-client-ws-js opts)
13261331
:clj (make-client-ws-java opts)))
@@ -1551,7 +1556,7 @@
15511556

15521557
(retry-fn))))
15531558

1554-
?new-socket
1559+
?new-socket_
15551560
(try
15561561
(ws-constructor
15571562
(merge ws-opts
@@ -1567,51 +1572,54 @@
15671572
(:csrf-token @state_))}))}))
15681573

15691574
(catch #?(:clj Throwable :cljs :default) t
1570-
(timbre/errorf t "Client WebSocket constructor error")
1575+
(timbre/errorf t "Error creating WebSocket client")
15711576
nil))]
15721577

1573-
(if-let [new-socket ?new-socket]
1574-
(do
1575-
(when-let [[old-s _old-sid]
1576-
(reset-in! socket_
1577-
[new-socket this-socket-id])]
1578-
;; Close old socket if one exists
1579-
(timbre/tracef "Old client WebSocket will be closed")
1580-
#?(:clj (.close ^WebSocketClient old-s 1000 "CLOSE_NORMAL")
1581-
:cljs (.close old-s 1000 "CLOSE_NORMAL")))
1582-
new-socket)
1583-
1584-
;; Couldn't create a socket
1585-
(retry-fn)))))]
1578+
(when-let [new-socket_ ?new-socket_]
1579+
(if-let [new-socket
1580+
(try
1581+
(force new-socket_)
1582+
(catch #?(:clj Throwable :cljs :default) t
1583+
(timbre/errorf t "Error realizing WebSocket client")
1584+
nil))]
1585+
(do
1586+
(when-let [[old-s _old-sid] (reset-in! socket_ [new-socket this-socket-id])]
1587+
;; Close old socket if one exists
1588+
(timbre/tracef "Old client WebSocket will be closed")
1589+
#?(:clj (.close ^WebSocketClient old-s 1000 "CLOSE_NORMAL")
1590+
:cljs (.close old-s 1000 "CLOSE_NORMAL")))
1591+
new-socket)
1592+
(retry-fn))))))]
15861593

15871594
(reset! retry-count_ 0)
1588-
(connect-fn)
15891595

1590-
;; Client-side loop to detect broken conns, Ref. #259
1591-
(when-let [ms ws-kalive-ms]
1592-
(go-loop []
1593-
(let [udt-t0 @udt-last-comms_]
1594-
(<! (async/timeout ms))
1595-
(when (own-conn?)
1596-
(let [udt-t1 @udt-last-comms_]
1597-
(when-let [;; No conn send/recv activity w/in kalive window?
1598-
no-activity? (= udt-t0 udt-t1)]
1599-
1600-
(timbre/debugf "Client will send ws-ping to server: %s"
1601-
{:ms-since-last-activity (- (enc/now-udt) udt-t1)
1602-
:timeout-ms ws-ping-timeout-ms})
1603-
1604-
(-chsk-send! chsk [:chsk/ws-ping]
1605-
{:flush? true
1606-
:timeout-ms ws-ping-timeout-ms
1607-
:cb ; Server will auto reply
1608-
(fn [reply]
1609-
(when (and (own-conn?) (not= reply "pong") #_(= reply :chsk/timeout))
1610-
(timbre/debugf "Client ws-ping to server timed-out, will cycle WebSocket now")
1611-
(-chsk-reconnect! chsk :ws-ping-timeout)))})))
1612-
(recur)))))
1613-
1614-
chsk)))
1596+
(when (connect-fn)
1597+
1598+
;; Client-side loop to detect broken conns, Ref. #259
1599+
(when-let [ms ws-kalive-ms]
1600+
(go-loop []
1601+
(let [udt-t0 @udt-last-comms_]
1602+
(<! (async/timeout ms))
1603+
(when (own-conn?)
1604+
(let [udt-t1 @udt-last-comms_]
1605+
(when-let [;; No conn send/recv activity w/in kalive window?
1606+
no-activity? (= udt-t0 udt-t1)]
1607+
1608+
(timbre/debugf "Client will send ws-ping to server: %s"
1609+
{:ms-since-last-activity (- (enc/now-udt) udt-t1)
1610+
:timeout-ms ws-ping-timeout-ms})
1611+
1612+
(-chsk-send! chsk [:chsk/ws-ping]
1613+
{:flush? true
1614+
:timeout-ms ws-ping-timeout-ms
1615+
:cb ; Server will auto reply
1616+
(fn [reply]
1617+
(when (and (own-conn?) (not= reply "pong") #_(= reply :chsk/timeout))
1618+
(timbre/debugf "Client ws-ping to server timed-out, will cycle WebSocket now")
1619+
(-chsk-reconnect! chsk :ws-ping-timeout)))})))
1620+
(recur)))))
1621+
1622+
chsk))))
16151623

16161624
(defn- new-ChWebSocket [opts csrf-token]
16171625
(map->ChWebSocket
@@ -1808,19 +1816,11 @@
18081816
]
18091817

18101818
IChSocket
1811-
(-chsk-disconnect! [chsk reason]
1812-
(when-let [impl @impl_]
1813-
(-chsk-disconnect! impl reason)))
1814-
1815-
;; Possibly reset impl type:
1816-
(-chsk-reconnect! [chsk reason]
1817-
(when-let [impl @impl_]
1818-
(-chsk-disconnect! impl reason)
1819-
(-chsk-connect! chsk)))
1820-
1821-
(-chsk-break-connection! [chsk opts]
1822-
(when-let [impl @impl_]
1823-
(-chsk-break-connection! impl opts)))
1819+
(-chsk-break-connection! [chsk opts] (when-let [impl @impl_] (-chsk-break-connection! impl opts)))
1820+
(-chsk-disconnect! [chsk reason] (when-let [impl @impl_] (-chsk-disconnect! impl reason)))
1821+
(-chsk-reconnect! [chsk reason]
1822+
(-chsk-disconnect! chsk reason)
1823+
(-chsk-connect! chsk))
18241824

18251825
(-chsk-send! [chsk ev opts]
18261826
(if-let [impl @impl_]
@@ -1830,35 +1830,38 @@
18301830
(chsk-send->closed! ?cb-fn))))
18311831

18321832
(-chsk-connect! [chsk]
1833-
;; Starting with a simple downgrade-only strategy here as a proof of concept
1834-
;; TODO Later consider smarter downgrade or downgrade+upgrade strategies?
1833+
;; Currently using a simplistic downgrade-only strategy.
1834+
;; TODO Consider smarter strategy that can also upgrade?
18351835
(let [ajax-chsk-opts (assoc ajax-chsk-opts :state_ state_)
1836-
ws-chsk-opts (assoc ws-chsk-opts :state_ state_)
1836+
ws-chsk-opts (assoc ws-chsk-opts :state_ state_)
18371837

1838-
ajax-conn!
1838+
ajax-chsk!
18391839
(fn []
1840-
;; Remove :auto->:ajax downgrade watch
1841-
(remove-watch state_ :chsk/auto-ajax-downgrade)
1842-
(-chsk-connect! (new-ChAjaxSocket ajax-chsk-opts (:csrf-token @state_))))
1840+
(let [ajax-chsk (new-ChAjaxSocket ajax-chsk-opts (:csrf-token @state_))]
1841+
(remove-watch state_ :chsk/auto-ajax-downgrade)
1842+
(-chsk-connect! ajax-chsk)))
18431843

1844-
ws-conn!
1844+
ws-chsk!
18451845
(fn []
1846-
;; Configure :auto->:ajax downgrade watch
1847-
(let [downgraded?_ (atom false)]
1846+
(let [ws-chsk (new-ChWebSocket ws-chsk-opts (:csrf-token @state_))
1847+
downgraded?_ (atom false)]
1848+
18481849
(add-watch state_ :chsk/auto-ajax-downgrade
18491850
(fn [_ _ old-state new-state]
1850-
(when-let [impl @impl_]
1851-
(when-let [ever-opened?_ (:ever-opened?_ impl)]
1852-
(when-not @ever-opened?_
1853-
(when (:last-ws-error new-state)
1854-
(when (compare-and-set! downgraded?_ false true)
1855-
(timbre/warnf "Client permanently downgrading chsk mode: :auto -> :ajax")
1856-
(-chsk-disconnect! impl :downgrading-ws-to-ajax)
1857-
(reset! impl_ (ajax-conn!))))))))))
1858-
1859-
(-chsk-connect! (new-ChWebSocket ws-chsk-opts (:csrf-token @state_))))]
1860-
1861-
(reset! impl_ (or (ws-conn!) (ajax-conn!)))
1851+
(enc/when-let [state-changed? (not= old-state new-state)
1852+
impl @impl_
1853+
ever-opened?_ (:ever-opened?_ impl)
1854+
never-opened? (not @ever-opened?_)
1855+
ws-error (:last-ws-error new-state)]
1856+
1857+
(when (compare-and-set! downgraded?_ false true)
1858+
(timbre/warnf "Client permanently downgrading chsk mode: :auto -> :ajax")
1859+
(-chsk-disconnect! impl :downgrading-ws-to-ajax)
1860+
(reset! impl_ (ajax-chsk!))))))
1861+
1862+
(-chsk-connect! ws-chsk)))]
1863+
1864+
(reset! impl_ (or (ws-chsk!) (ajax-chsk!)))
18621865
chsk))))
18631866

18641867
#?(:cljs
@@ -1911,8 +1914,8 @@
19111914
; await reply before regarding the connection as broken
19121915
19131916
:ws-constructor ; Advanced, (fn [{:keys [uri-str headers on-message on-error on-close]}]
1914-
; => connected WebSocket, see `default-client-ws-constructor` code for
1915-
; details."
1917+
; => nil, or delay that can be dereffed to get a connected WebSocket.
1918+
; See `default-client-ws-constructor` code for details."
19161919

19171920
[path ?csrf-token-or-fn &
19181921
[{:as opts

0 commit comments

Comments
 (0)