github.com/bitfinexcom/bitfinex-api-go@v0.0.0-20210608095005-9e0b26f200fb/v2/websocket/client.go (about)

     1  package websocket
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  	"unicode"
    11  
    12  	"github.com/gorilla/websocket"
    13  	"github.com/op/go-logging"
    14  
    15  	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/common"
    16  	"github.com/bitfinexcom/bitfinex-api-go/pkg/utils"
    17  
    18  	"crypto/hmac"
    19  	"crypto/sha512"
    20  	"encoding/hex"
    21  )
    22  
    23  var productionBaseURL = "wss://api-pub.bitfinex.com/ws/2"
    24  
    25  // ws-specific errors
    26  var (
    27  	ErrWSNotConnected     = fmt.Errorf("websocket connection not established")
    28  	ErrWSAlreadyConnected = fmt.Errorf("websocket connection already established")
    29  )
    30  
    31  // Available channels
    32  const (
    33  	ChanBook    = "book"
    34  	ChanTrades  = "trades"
    35  	ChanTicker  = "ticker"
    36  	ChanCandles = "candles"
    37  	ChanStatus  = "status"
    38  )
    39  
    40  // Events
    41  const (
    42  	EventSubscribe   = "subscribe"
    43  	EventUnsubscribe = "unsubscribe"
    44  	EventPing        = "ping"
    45  )
    46  
    47  // Authentication states
    48  const (
    49  	NoAuthentication         AuthState = 0
    50  	PendingAuthentication    AuthState = 1
    51  	SuccessfulAuthentication AuthState = 2
    52  	RejectedAuthentication   AuthState = 3
    53  )
    54  
    55  // private type--cannot instantiate.
    56  type authState byte
    57  
    58  // AuthState provides a typed authentication state.
    59  type AuthState authState // prevent user construction of authStates
    60  
    61  // DMSCancelOnDisconnect cancels session orders on disconnect.
    62  const DMSCancelOnDisconnect int = 4
    63  
    64  // Asynchronous interface decouples the underlying transport from API logic.
    65  type Asynchronous interface {
    66  	Connect() error
    67  	Send(ctx context.Context, msg interface{}) error
    68  	Listen() <-chan []byte
    69  	Close()
    70  	Done() <-chan error
    71  }
    72  
    73  type SocketId int
    74  type Socket struct {
    75  	Id SocketId
    76  	Asynchronous
    77  	IsConnected        bool
    78  	ResetSubscriptions []*subscription
    79  	IsAuthenticated    bool
    80  }
    81  
    82  // AsynchronousFactory provides an interface to re-create asynchronous transports during reconnect events.
    83  type AsynchronousFactory interface {
    84  	Create() Asynchronous
    85  }
    86  
    87  // WebsocketAsynchronousFactory creates a websocket-based asynchronous transport.
    88  type WebsocketAsynchronousFactory struct {
    89  	parameters *Parameters
    90  }
    91  
    92  // NewWebsocketAsynchronousFactory creates a new websocket factory with a given URL.
    93  func NewWebsocketAsynchronousFactory(parameters *Parameters) AsynchronousFactory {
    94  	return &WebsocketAsynchronousFactory{
    95  		parameters: parameters,
    96  	}
    97  }
    98  
    99  // Create returns a new websocket transport.
   100  func (w *WebsocketAsynchronousFactory) Create() Asynchronous {
   101  	return newWs(w.parameters.URL, w.parameters.LogTransport, w.parameters.Logger)
   102  }
   103  
   104  // Client provides a unified interface for users to interact with the Bitfinex V2 Websocket API.
   105  // nolint:megacheck,structcheck
   106  type Client struct {
   107  	asyncFactory AsynchronousFactory // for re-creating transport during reconnects
   108  
   109  	timeout            int64 // read timeout
   110  	apiKey             string
   111  	apiSecret          string
   112  	cancelOnDisconnect bool
   113  	Authentication     AuthState
   114  	sockets            map[SocketId]*Socket
   115  	nonce              utils.NonceGenerator
   116  	terminal           bool
   117  	init               bool
   118  	log                *logging.Logger
   119  
   120  	// connection & operational behavior
   121  	parameters *Parameters
   122  
   123  	// subscription manager
   124  	subscriptions *subscriptions
   125  	factories     map[string]messageFactory
   126  	orderbooks    map[string]*Orderbook
   127  
   128  	// close signal sent to user on shutdown
   129  	shutdown chan bool
   130  
   131  	// downstream listener channel to deliver API objects
   132  	listener chan interface{}
   133  
   134  	// race management
   135  	mtx       *sync.RWMutex
   136  	waitGroup sync.WaitGroup
   137  }
   138  
   139  // Credentials assigns authentication credentials to a connection request.
   140  func (c *Client) Credentials(key string, secret string) *Client {
   141  	c.apiKey = key
   142  	c.apiSecret = secret
   143  	return c
   144  }
   145  
   146  // CancelOnDisconnect ensures all orders will be canceled if this API session is disconnected.
   147  func (c *Client) CancelOnDisconnect(cxl bool) *Client {
   148  	c.cancelOnDisconnect = cxl
   149  	return c
   150  }
   151  
   152  func (c *Client) sign(msg string) (string, error) {
   153  	sig := hmac.New(sha512.New384, []byte(c.apiSecret))
   154  	_, err := sig.Write([]byte(msg))
   155  	if err != nil {
   156  		return "", err
   157  	}
   158  	return hex.EncodeToString(sig.Sum(nil)), nil
   159  }
   160  
   161  func (c *Client) registerFactory(channel string, factory messageFactory) {
   162  	c.factories[channel] = factory
   163  }
   164  
   165  // New creates a default client.
   166  func New() *Client {
   167  	return NewWithParams(NewDefaultParameters())
   168  }
   169  
   170  // NewWithAsyncFactory creates a new default client with a given asynchronous transport factory interface.
   171  func NewWithAsyncFactory(async AsynchronousFactory) *Client {
   172  	return NewWithParamsAsyncFactory(NewDefaultParameters(), async)
   173  }
   174  
   175  // NewWithParams creates a new default client with a given set of parameters.
   176  func NewWithParams(params *Parameters) *Client {
   177  	return NewWithParamsAsyncFactory(params, NewWebsocketAsynchronousFactory(params))
   178  }
   179  
   180  // NewWithAsyncFactoryNonce creates a new default client with a given asynchronous transport factory and nonce generator.
   181  func NewWithAsyncFactoryNonce(async AsynchronousFactory, nonce utils.NonceGenerator) *Client {
   182  	return NewWithParamsAsyncFactoryNonce(NewDefaultParameters(), async, nonce)
   183  }
   184  
   185  // NewWithParamsNonce creates a new default client with a given set of parameters and nonce generator.
   186  func NewWithParamsNonce(params *Parameters, nonce utils.NonceGenerator) *Client {
   187  	return NewWithParamsAsyncFactoryNonce(params, NewWebsocketAsynchronousFactory(params), nonce)
   188  }
   189  
   190  // NewWithParamsAsyncFactory creates a new default client with a given set of parameters and asynchronous transport factory interface.
   191  func NewWithParamsAsyncFactory(params *Parameters, async AsynchronousFactory) *Client {
   192  	return NewWithParamsAsyncFactoryNonce(params, async, utils.NewEpochNonceGenerator())
   193  }
   194  
   195  // NewWithParamsAsyncFactoryNonce creates a new client with a given set of parameters, asynchronous transport factory, and nonce generator interfaces.
   196  func NewWithParamsAsyncFactoryNonce(params *Parameters, async AsynchronousFactory, nonce utils.NonceGenerator) *Client {
   197  	c := &Client{
   198  		asyncFactory:   async,
   199  		Authentication: NoAuthentication,
   200  		factories:      make(map[string]messageFactory),
   201  		subscriptions:  newSubscriptions(params.HeartbeatTimeout, params.Logger),
   202  		orderbooks:     make(map[string]*Orderbook),
   203  		nonce:          nonce,
   204  		parameters:     params,
   205  		listener:       make(chan interface{}),
   206  		terminal:       false,
   207  		shutdown:       nil,
   208  		sockets:        make(map[SocketId]*Socket),
   209  		mtx:            &sync.RWMutex{},
   210  		log:            params.Logger,
   211  	}
   212  	c.registerPublicFactories()
   213  	return c
   214  }
   215  
   216  // Connect to the Bitfinex API, this should only be called once.
   217  func (c *Client) Connect() error {
   218  	c.dumpParams()
   219  	c.terminal = false
   220  	go c.listenDisconnect()
   221  	return c.connectSocket(SocketId(len(c.sockets)))
   222  }
   223  
   224  // Returns true if the underlying asynchronous transport is connected to an endpoint.
   225  func (c *Client) IsConnected() bool {
   226  	c.mtx.RLock()
   227  	defer c.mtx.RUnlock()
   228  	for _, socket := range c.sockets {
   229  		if socket.IsConnected {
   230  			return true
   231  		}
   232  	}
   233  	return false
   234  }
   235  
   236  // Listen for all incoming api websocket messages
   237  // When a websocket connection is terminated, the publisher channel will close.
   238  func (c *Client) Listen() <-chan interface{} {
   239  	return c.listener
   240  }
   241  
   242  // Close the websocket client which will cause for all
   243  // active sockets to be exited and the Done() function
   244  // to be called
   245  func (c *Client) Close() {
   246  	c.terminal = true
   247  	var wg sync.WaitGroup
   248  	socketCount := len(c.sockets)
   249  	if socketCount > 0 {
   250  		for _, socket := range c.sockets {
   251  			if socket.IsConnected {
   252  				wg.Add(1)
   253  				socket.IsConnected = false
   254  				go func(s *Socket) {
   255  					c.closeAsyncAndWait(s, c.parameters.ShutdownTimeout)
   256  					wg.Done()
   257  				}(socket)
   258  			}
   259  		}
   260  		wg.Wait()
   261  	}
   262  	c.subscriptions.Close()
   263  	close(c.listener)
   264  }
   265  
   266  // Unsubscribe from the existing subscription with the given id
   267  func (c *Client) Unsubscribe(ctx context.Context, id string) error {
   268  	sub, err := c.subscriptions.lookupBySubscriptionID(id)
   269  	if err != nil {
   270  		return err
   271  	}
   272  	// sub is removed from manager on ack from API
   273  	return c.sendUnsubscribeMessage(ctx, sub)
   274  }
   275  
   276  func (c *Client) listenDisconnect() {
   277  	for {
   278  		select {
   279  		case <-c.shutdown:
   280  			return
   281  		case hbErr := <-c.subscriptions.ListenDisconnect(): // subscription heartbeat timeout
   282  			c.log.Warningf("heartbeat disconnect: %s", hbErr.Error.Error())
   283  			c.mtx.Lock()
   284  			if socket, ok := c.sockets[hbErr.Subscription.SocketId]; ok {
   285  				if socket.IsConnected {
   286  					c.log.Infof("restarting socket (id=%d) connection", socket.Id)
   287  					socket.IsConnected = false
   288  					// reconnect to the socket
   289  					go func() {
   290  						c.closeAsyncAndWait(socket, c.parameters.ShutdownTimeout)
   291  						err := c.reconnect(socket, hbErr.Error)
   292  						if err != nil {
   293  							c.log.Warningf("socket disconnect: %s", err.Error())
   294  							return
   295  						}
   296  					}()
   297  				}
   298  			}
   299  			c.mtx.Unlock()
   300  		}
   301  	}
   302  }
   303  
   304  func extractSymbolResolutionFromKey(subscription string) (symbol string, resolution common.CandleResolution, err error) {
   305  	var res, sym string
   306  	str := strings.Split(subscription, ":")
   307  	if len(str) < 3 {
   308  		return "", resolution, fmt.Errorf("could not convert symbol resolution for %s: len %d", subscription, len(str))
   309  	}
   310  	res = str[1]
   311  	sym = str[2]
   312  	resolution, err = common.CandleResolutionFromString(res)
   313  	if err != nil {
   314  		return "", resolution, err
   315  	}
   316  	return sym, resolution, nil
   317  }
   318  
   319  func (c *Client) registerPublicFactories() {
   320  	c.registerFactory(ChanTicker, newTickerFactory(c.subscriptions))
   321  	c.registerFactory(ChanTrades, newTradeFactory(c.subscriptions))
   322  	c.registerFactory(ChanBook, newBookFactory(c.subscriptions, c.orderbooks, c.parameters.ManageOrderbook))
   323  	c.registerFactory(ChanCandles, newCandlesFactory(c.subscriptions))
   324  	c.registerFactory(ChanStatus, newStatsFactory(c.subscriptions))
   325  }
   326  
   327  func (c *Client) reconnect(socket *Socket, err error) error {
   328  	c.mtx.RLock()
   329  	if c.terminal {
   330  		// dont attempt to reconnect if terminal
   331  		c.mtx.RUnlock()
   332  		return err
   333  	}
   334  	if !c.parameters.AutoReconnect {
   335  		err := fmt.Errorf("AutoReconnect setting is disabled, do not reconnect: %s", err.Error())
   336  		c.mtx.RUnlock()
   337  		return err
   338  	}
   339  	c.mtx.RUnlock()
   340  	reconnectTry := 0
   341  	for ; reconnectTry < c.parameters.ReconnectAttempts; reconnectTry++ {
   342  		c.log.Debugf("socket (id=%d) waiting %s until reconnect...", socket.Id, c.parameters.ReconnectInterval)
   343  		time.Sleep(c.parameters.ReconnectInterval)
   344  		c.log.Infof("socket (id=%d) reconnect attempt %d/%d", socket.Id, reconnectTry+1, c.parameters.ReconnectAttempts)
   345  		if err := c.reconnectSocket(socket); err == nil {
   346  			c.log.Debugf("reconnect OK")
   347  			return nil
   348  		}
   349  		c.log.Warningf("socket (id=%d) reconnect failed: %s", socket.Id, err.Error())
   350  	}
   351  	if err != nil {
   352  		c.log.Errorf("socket (id=%d) could not reconnect: %s", socket.Id, err.Error())
   353  	}
   354  	return err
   355  }
   356  
   357  func (c *Client) dumpParams() {
   358  	c.log.Debug("----Bitfinex Client Parameters----")
   359  	c.log.Debugf("AutoReconnect=%t", c.parameters.AutoReconnect)
   360  	c.log.Debugf("CapacityPerConnection=%t", c.parameters.CapacityPerConnection)
   361  	c.log.Debugf("ReconnectInterval=%s", c.parameters.ReconnectInterval)
   362  	c.log.Debugf("ReconnectAttempts=%d", c.parameters.ReconnectAttempts)
   363  	c.log.Debugf("ShutdownTimeout=%s", c.parameters.ShutdownTimeout)
   364  	c.log.Debugf("ResubscribeOnReconnect=%t", c.parameters.ResubscribeOnReconnect)
   365  	c.log.Debugf("HeartbeatTimeout=%s", c.parameters.HeartbeatTimeout)
   366  	c.log.Debugf("URL=%s", c.parameters.URL)
   367  	c.log.Debugf("ManageOrderbook=%t", c.parameters.ManageOrderbook)
   368  }
   369  
   370  func (c *Client) connectSocket(socketId SocketId) error {
   371  	async := c.asyncFactory.Create()
   372  	// create new socket instance
   373  	socket := &Socket{
   374  		Id:                 socketId,
   375  		Asynchronous:       async,
   376  		IsConnected:        false,
   377  		ResetSubscriptions: nil,
   378  		IsAuthenticated:    false,
   379  	}
   380  	oldSocket, _ := c.socketById(socketId)
   381  	if oldSocket != nil {
   382  		// socket exists so use its state
   383  		socket.IsAuthenticated = oldSocket.IsAuthenticated
   384  		socket.ResetSubscriptions = oldSocket.ResetSubscriptions
   385  	}
   386  	c.mtx.Lock()
   387  	// add socket to managed map
   388  	c.sockets[socket.Id] = socket
   389  	c.mtx.Unlock()
   390  	// connect socket
   391  	err := socket.Asynchronous.Connect()
   392  	if err != nil {
   393  		// unable to establish connection
   394  		return err
   395  	}
   396  	socket.IsConnected = true
   397  	go c.listenUpstream(socket)
   398  	return nil
   399  }
   400  
   401  func (c *Client) reconnectSocket(socket *Socket) error {
   402  	// remove subscriptions from manager but keep a copy so we can resubscribe once
   403  	// a new connection is established
   404  	if socket.ResetSubscriptions == nil && c.parameters.ResubscribeOnReconnect {
   405  		socket.ResetSubscriptions = c.subscriptions.ResetSocketSubscriptions(socket.Id)
   406  	}
   407  	// establish a new connection
   408  	err := c.connectSocket(socket.Id)
   409  	if err != nil {
   410  		return err
   411  	}
   412  	return nil
   413  }
   414  
   415  // start this goroutine before connecting, but this should die during a connection failure
   416  func (c *Client) listenUpstream(socket *Socket) {
   417  	for {
   418  		select {
   419  		case err := <-socket.Asynchronous.Done():
   420  			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
   421  				err := c.reconnect(socket, err)
   422  				if err != nil {
   423  					c.log.Errorf("Unable to reconnect socket (id=%d) after err: %s", socket.Id, err.Error())
   424  					return
   425  				}
   426  			}
   427  			return
   428  		case msg := <-socket.Asynchronous.Listen():
   429  			if msg != nil {
   430  				err := c.handleMessage(socket.Id, msg)
   431  				if err != nil {
   432  					c.log.Warningf("upstream listen error: %s", err.Error())
   433  				}
   434  			}
   435  		}
   436  	}
   437  }
   438  
   439  func (c *Client) closeAsyncAndWait(socket *Socket, t time.Duration) {
   440  	timeout := make(chan bool)
   441  	wg := sync.WaitGroup{}
   442  	wg.Add(1)
   443  	go func() {
   444  		select {
   445  		case <-socket.Asynchronous.Done():
   446  			wg.Done()
   447  		case <-timeout:
   448  			c.log.Errorf("socket (id=%d) took too long to close.", socket.Id)
   449  			wg.Done()
   450  		}
   451  	}()
   452  	go func() {
   453  		time.Sleep(t)
   454  		close(timeout)
   455  	}()
   456  	socket.Asynchronous.Close()
   457  	wg.Wait()
   458  }
   459  
   460  func (c *Client) handleMessage(socketId SocketId, msg []byte) error {
   461  	t := bytes.TrimLeftFunc(msg, unicode.IsSpace)
   462  	var err error
   463  	// either a channel data array or an event object, raw json encoding
   464  	if bytes.HasPrefix(t, []byte("[")) {
   465  		err = c.handleChannel(socketId, msg)
   466  	} else if bytes.HasPrefix(t, []byte("{")) {
   467  		err = c.handleEvent(socketId, msg)
   468  	} else {
   469  		return fmt.Errorf("unexpected message in socket (id=%d): %s", socketId, msg)
   470  	}
   471  	return err
   472  }
   473  
   474  func (c *Client) sendUnsubscribeMessage(ctx context.Context, sub *subscription) error {
   475  	socket, err := c.socketById(sub.SocketId)
   476  	if err != nil {
   477  		return err
   478  	}
   479  	return socket.Asynchronous.Send(ctx, unsubscribeMsg{Event: "unsubscribe", ChanID: sub.ChanID})
   480  }
   481  
   482  func (c *Client) checkResubscription(socketId SocketId) {
   483  	socket, err := c.socketById(socketId)
   484  	if err != nil {
   485  		panic(err)
   486  	}
   487  	if c.parameters.ManageOrderbook {
   488  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   489  		defer cancel()
   490  		_, err_flag := c.EnableFlag(ctx, common.Checksum)
   491  		if err_flag != nil {
   492  			c.log.Errorf("could not enable checksum flag %s ", err_flag)
   493  		}
   494  	}
   495  	if c.parameters.ResubscribeOnReconnect && socket.ResetSubscriptions != nil {
   496  		for _, sub := range socket.ResetSubscriptions {
   497  			if sub.Request.Event == "auth" {
   498  				continue
   499  			}
   500  			ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   501  			defer cancel()
   502  			sub.Request.SubID = c.nonce.GetNonce() // new nonce
   503  			c.log.Infof("socket (id=%d) resubscribing to %s with nonce %s", socket.Id, sub.Request.String(), sub.Request.SubID)
   504  			_, err := c.subscribeBySocket(ctx, socket, sub.Request)
   505  			if err != nil {
   506  				c.log.Errorf("could not resubscribe: %s", err.Error())
   507  			}
   508  		}
   509  		socket.ResetSubscriptions = nil
   510  	}
   511  }
   512  
   513  // called when an info event is received
   514  func (c *Client) handleOpen(socketId SocketId) error {
   515  	authSocket, _ := c.GetAuthenticatedSocket()
   516  	// if we have auth credentials and there is currently no authenticated
   517  	// sockets (we are only allowed one)
   518  	if c.hasCredentials() && authSocket == nil {
   519  		err_auth := c.authenticate(context.Background(), socketId)
   520  		if err_auth != nil {
   521  			return err_auth
   522  		}
   523  		return nil
   524  	}
   525  	// if the opening socket (triggered by reconnect) is authenticated then re-authenticate
   526  	if authSocket != nil && authSocket.Id == socketId {
   527  		err_auth := c.authenticate(context.Background(), socketId)
   528  		if err_auth != nil {
   529  			return err_auth
   530  		}
   531  		return nil
   532  	}
   533  	// resubscribe public channels (we will handle authenticated in authAck)
   534  	c.checkResubscription(socketId)
   535  	return nil
   536  }
   537  
   538  // called when an auth event is received
   539  func (c *Client) handleAuthAck(socketId SocketId, auth *AuthEvent) {
   540  	if c.Authentication == SuccessfulAuthentication {
   541  		// set socket to authenticated
   542  		socket, err := c.socketById(socketId)
   543  		if err != nil {
   544  			panic(err)
   545  		}
   546  		socket.IsAuthenticated = true
   547  		err = c.subscriptions.activate(auth.SubID, auth.ChanID)
   548  		if err != nil {
   549  			c.log.Errorf("could not activate auth subscription: %s", err.Error())
   550  		}
   551  		c.checkResubscription(socketId)
   552  	} else {
   553  		c.log.Error("authentication failed")
   554  	}
   555  }
   556  
   557  func (c *Client) hasCredentials() bool {
   558  	return c.apiKey != "" && c.apiSecret != ""
   559  }
   560  
   561  // Authenticate creates the payload for the authentication request and sends it
   562  // to the API. The filters will be applied to the authenticated channel, i.e.
   563  // only subscribe to the filtered messages.
   564  func (c *Client) authenticate(ctx context.Context, socketId SocketId, filter ...string) error {
   565  	nonce := c.nonce.GetNonce()
   566  	payload := "AUTH" + nonce
   567  	sig, err := c.sign(payload)
   568  	if err != nil {
   569  		return err
   570  	}
   571  	s := &SubscriptionRequest{
   572  		Event:       "auth",
   573  		APIKey:      c.apiKey,
   574  		AuthSig:     sig,
   575  		AuthPayload: payload,
   576  		AuthNonce:   nonce,
   577  		Filter:      filter,
   578  		SubID:       nonce,
   579  	}
   580  	if c.cancelOnDisconnect {
   581  		s.DMS = DMSCancelOnDisconnect
   582  	}
   583  	c.subscriptions.add(socketId, s)
   584  	socket, err := c.socketById(socketId)
   585  	if err != nil {
   586  		return err
   587  	}
   588  	if err = socket.Asynchronous.Send(ctx, s); err != nil {
   589  		return err
   590  	}
   591  	c.Authentication = PendingAuthentication
   592  	return nil
   593  }
   594  
   595  // get a random socket
   596  func (c *Client) getSocket() (*Socket, error) {
   597  	if len(c.sockets) <= 0 {
   598  		return nil, fmt.Errorf("no socket found")
   599  	}
   600  	return c.sockets[0], nil
   601  }
   602  
   603  func (c *Client) getMostAvailableSocket() (*Socket, error) {
   604  	var retSocket *Socket
   605  	bestCapacity := 0
   606  	for _, socket := range c.sockets {
   607  		capac := c.getAvailableSocketCapacity(socket.Id)
   608  		if retSocket == nil {
   609  			retSocket = socket
   610  			bestCapacity = capac
   611  			continue
   612  		}
   613  		if capac > bestCapacity {
   614  			retSocket = socket
   615  			bestCapacity = capac
   616  		}
   617  	}
   618  	if retSocket == nil {
   619  		return nil, fmt.Errorf("no socket found")
   620  	}
   621  	return retSocket, nil
   622  }
   623  
   624  // lookup the socket with the given Id, throw error if not found
   625  func (c *Client) socketById(socketId SocketId) (*Socket, error) {
   626  	c.mtx.RLock()
   627  	defer c.mtx.RUnlock()
   628  	if socket, ok := c.sockets[socketId]; ok {
   629  		return socket, nil
   630  	}
   631  	return nil, fmt.Errorf("could not find socket with ID %d", socketId)
   632  }
   633  
   634  // calculates how many free channels are available across all of the sockets
   635  func (c *Client) getTotalAvailableSocketCapacity() int {
   636  	freeCapacity := 0
   637  	c.mtx.RLock()
   638  	ids := make([]SocketId, len(c.sockets))
   639  	for i, socket := range c.sockets {
   640  		ids[i] = socket.Id
   641  	}
   642  	c.mtx.RUnlock()
   643  	for _, id := range ids {
   644  		freeCapacity += c.getAvailableSocketCapacity(id)
   645  	}
   646  	return freeCapacity
   647  }
   648  
   649  // calculates how many free channels are available on the given socket
   650  func (c *Client) getAvailableSocketCapacity(socketId SocketId) int {
   651  	c.mtx.RLock()
   652  	defer c.mtx.RUnlock()
   653  	subs, err := c.subscriptions.lookupBySocketId(socketId)
   654  	if err == nil {
   655  		return c.parameters.CapacityPerConnection - subs.Len()
   656  	}
   657  	return c.parameters.CapacityPerConnection
   658  }
   659  
   660  // Get the authenticated socket. Due to rate limitations
   661  // there can only be one authenticated socket active at a time
   662  func (c *Client) GetAuthenticatedSocket() (*Socket, error) {
   663  	c.mtx.RLock()
   664  	defer c.mtx.RUnlock()
   665  	for _, socket := range c.sockets {
   666  		if socket.IsAuthenticated {
   667  			return socket, nil
   668  		}
   669  	}
   670  	return nil, fmt.Errorf("no authenticated socket found")
   671  }