@@ -45,6 +45,7 @@ type WSConfig struct {
4545 WriteBufferSize int `json:"writeBufferSize,omitempty"`
4646 InitialDelay time.Duration `json:"initialDelay,omitempty"`
4747 MaximumDelay time.Duration `json:"maximumDelay,omitempty"`
48+ DelayFactor float64 `json:"delayFactor,omitempty"`
4849 InitialConnectAttempts int `json:"initialConnectAttempts,omitempty"`
4950 DisableReconnect bool `json:"disableReconnect"`
5051 AuthUsername string `json:"authUsername,omitempty"`
@@ -59,6 +60,14 @@ type WSConfig struct {
5960 ReceiveExt bool
6061}
6162
63+ type WSWrapConfig struct {
64+ HeartbeatInterval time.Duration `json:"heartbeatInterval,omitempty"`
65+ ThrottleRequestsPerSecond int `json:"requestsPerSecond,omitempty"`
66+ ThrottleBurst int `json:"burst,omitempty"`
67+ // This one cannot be set in JSON - must be configured on the code interface
68+ ReceiveExt bool
69+ }
70+
6271// WSPayload allows API consumers of this package to stream data, and inspect the message
6372// type, rather than just being passed the bytes directly.
6473type WSPayload struct {
@@ -98,7 +107,7 @@ type wsClient struct {
98107 initialRetryAttempts int
99108 wsdialer * websocket.Dialer
100109 wsconn * websocket.Conn
101- retry retry.Retry
110+ connRetry retry.Retry
102111 closed bool
103112 useReceiveExt bool
104113 receive chan []byte
@@ -122,6 +131,7 @@ type WSPreConnectHandler func(ctx context.Context, w WSClient) error
122131// WSPostConnectHandler will be called after every connect/reconnect. Can send data over ws, but must not block listening for data on the ws.
123132type WSPostConnectHandler func (ctx context.Context , w WSClient ) error
124133
134+ // Creates a new outbound client that can be connected to a remote server
125135func New (ctx context.Context , config * WSConfig , beforeConnect WSPreConnectHandler , afterConnect WSPostConnectHandler ) (WSClient , error ) {
126136 l := log .L (ctx )
127137 wsURL , err := buildWSUrl (ctx , config )
@@ -138,9 +148,10 @@ func New(ctx context.Context, config *WSConfig, beforeConnect WSPreConnectHandle
138148 TLSClientConfig : config .TLSClientConfig ,
139149 HandshakeTimeout : config .ConnectionTimeout ,
140150 },
141- retry : retry.Retry {
151+ connRetry : retry.Retry {
142152 InitialDelay : config .InitialDelay ,
143153 MaximumDelay : config .MaximumDelay ,
154+ Factor : config .DelayFactor ,
144155 },
145156 initialRetryAttempts : config .InitialConnectAttempts ,
146157 headers : make (http.Header ),
@@ -153,11 +164,7 @@ func New(ctx context.Context, config *WSConfig, beforeConnect WSPreConnectHandle
153164 disableReconnect : config .DisableReconnect ,
154165 rateLimiter : ffresty .GetRateLimiter (config .ThrottleRequestsPerSecond , config .ThrottleBurst ),
155166 }
156- if w .useReceiveExt {
157- w .receiveExt = make (chan * WSPayload )
158- } else {
159- w .receive = make (chan []byte )
160- }
167+ w .setupReceiveChannel ()
161168 for k , v := range config .HTTPHeaders {
162169 if vs , ok := v .(string ); ok {
163170 w .headers .Set (k , vs )
@@ -182,6 +189,40 @@ func New(ctx context.Context, config *WSConfig, beforeConnect WSPreConnectHandle
182189 return w , nil
183190}
184191
192+ // Wrap an existing connection (including an inbound server connection) with heartbeating and throttling.
193+ // No reconnect functions are supported when wrapping an existing connection like this, but the supplied
194+ // callback will be invoked when the connection closes (allowing cleanup/tracking).
195+ func Wrap (ctx context.Context , config WSWrapConfig , wsconn * websocket.Conn , onClose func ()) WSClient {
196+ w := & wsClient {
197+ ctx : ctx ,
198+ url : wsconn .LocalAddr ().String (),
199+ wsconn : wsconn ,
200+ disableReconnect : true ,
201+ heartbeatInterval : config .HeartbeatInterval ,
202+ rateLimiter : ffresty .GetRateLimiter (config .ThrottleRequestsPerSecond , config .ThrottleBurst ),
203+ useReceiveExt : config .ReceiveExt ,
204+ send : make (chan []byte ),
205+ closing : make (chan struct {}),
206+ }
207+ w .setupReceiveChannel ()
208+ w .pongReceivedOrReset (false )
209+ w .wsconn .SetPongHandler (w .pongHandler )
210+ log .L (ctx ).Infof ("WS %s wrapped" , w .url )
211+ go func () {
212+ w .receiveReconnectLoop ()
213+ onClose ()
214+ }()
215+ return w
216+ }
217+
218+ func (w * wsClient ) setupReceiveChannel () {
219+ if w .useReceiveExt {
220+ w .receiveExt = make (chan * WSPayload )
221+ } else {
222+ w .receive = make (chan []byte )
223+ }
224+ }
225+
185226func (w * wsClient ) Connect () error {
186227
187228 if err := w .connect (true ); err != nil {
@@ -291,7 +332,7 @@ func buildWSUrl(ctx context.Context, config *WSConfig) (string, error) {
291332
292333func (w * wsClient ) connect (initial bool ) error {
293334 l := log .L (w .ctx )
294- return w .retry .DoCustomLog (w .ctx , func (attempt int ) (retry bool , err error ) {
335+ return w .connRetry .DoCustomLog (w .ctx , func (attempt int ) (retry bool , err error ) {
295336 if w .closed {
296337 return false , i18n .NewError (w .ctx , i18n .MsgWSClosing )
297338 }
@@ -436,6 +477,7 @@ func (w *wsClient) sendLoop(receiverDone chan struct{}) {
436477 l .Errorf ("WS %s closing: %s" , w .url , err )
437478 disconnecting = true
438479 } else if wsconn != nil {
480+ l .Debugf ("WS %s send heartbeat ping" , w .url )
439481 if err := wsconn .WriteMessage (websocket .PingMessage , []byte {}); err != nil {
440482 l .Errorf ("WS %s heartbeat send failed: %s" , w .url , err )
441483 disconnecting = true
0 commit comments