github.com/anycable/anycable-go@v1.5.1/common/common.go (about)

     1  // Package common contains struts and interfaces shared between multiple components
     2  package common
     3  
     4  import (
     5  	"encoding/json"
     6  	"log/slog"
     7  
     8  	"github.com/anycable/anycable-go/logger"
     9  	"github.com/anycable/anycable-go/utils"
    10  )
    11  
    12  // Command result status
    13  const (
    14  	SUCCESS = iota
    15  	FAILURE
    16  	ERROR
    17  )
    18  
    19  func StatusName(status int) string {
    20  	switch status {
    21  	case SUCCESS:
    22  		return "success"
    23  	case FAILURE:
    24  		return "failure"
    25  	case ERROR:
    26  		return "error"
    27  	default:
    28  		return "unknown"
    29  	}
    30  }
    31  
    32  const (
    33  	ActionCableV1JSON    = "actioncable-v1-json"
    34  	ActionCableV1ExtJSON = "actioncable-v1-ext-json"
    35  )
    36  
    37  func ActionCableProtocols() []string {
    38  	return []string{ActionCableV1JSON, ActionCableV1ExtJSON}
    39  }
    40  
    41  func ActionCableExtendedProtocols() []string {
    42  	return []string{ActionCableV1ExtJSON}
    43  }
    44  
    45  func IsExtendedActionCableProtocol(protocol string) bool {
    46  	for _, p := range ActionCableExtendedProtocols() {
    47  		if p == protocol {
    48  			return true
    49  		}
    50  	}
    51  
    52  	return false
    53  }
    54  
    55  // Outgoing message types (according to Action Cable protocol)
    56  const (
    57  	WelcomeType    = "welcome"
    58  	PingType       = "ping"
    59  	DisconnectType = "disconnect"
    60  	ConfirmedType  = "confirm_subscription"
    61  	RejectedType   = "reject_subscription"
    62  	// Not supported by Action Cable currently
    63  	UnsubscribedType = "unsubscribed"
    64  
    65  	HistoryConfirmedType = "confirm_history"
    66  	HistoryRejectedType  = "reject_history"
    67  
    68  	WhisperType = "whisper"
    69  )
    70  
    71  // Disconnect reasons
    72  const (
    73  	SERVER_RESTART_REASON    = "server_restart"
    74  	REMOTE_DISCONNECT_REASON = "remote"
    75  	IDLE_TIMEOUT_REASON      = "idle_timeout"
    76  	NO_PONG_REASON           = "no_pong"
    77  	UNAUTHORIZED_REASON      = "unauthorized"
    78  )
    79  
    80  // Reserver state fields
    81  const (
    82  	WHISPER_STREAM_STATE = "$w"
    83  )
    84  
    85  // SessionEnv represents the underlying HTTP connection data:
    86  // URL and request headers.
    87  // It also carries channel and connection state information used by the RPC app.
    88  type SessionEnv struct {
    89  	URL             string
    90  	Headers         *map[string]string
    91  	Identifiers     string
    92  	ConnectionState *map[string]string
    93  	ChannelStates   *map[string]map[string]string
    94  }
    95  
    96  // NewSessionEnv builds a new SessionEnv
    97  func NewSessionEnv(url string, headers *map[string]string) *SessionEnv {
    98  	state := make(map[string]string)
    99  	channels := make(map[string]map[string]string)
   100  	return &SessionEnv{
   101  		URL:             url,
   102  		Headers:         headers,
   103  		ConnectionState: &state,
   104  		ChannelStates:   &channels,
   105  	}
   106  }
   107  
   108  // MergeConnectionState updates the current ConnectionState from the given map.
   109  // If the value is an empty string then remove the key,
   110  // otherswise add or rewrite.
   111  func (st *SessionEnv) MergeConnectionState(other *map[string]string) {
   112  	for k, v := range *other {
   113  		if v == "" {
   114  			delete(*st.ConnectionState, k)
   115  		} else {
   116  			(*st.ConnectionState)[k] = v
   117  		}
   118  	}
   119  }
   120  
   121  // MergeChannelState updates the current ChannelStates for the given identifier.
   122  // If the value is an empty string then remove the key,
   123  // otherswise add or rewrite.
   124  func (st *SessionEnv) MergeChannelState(id string, other *map[string]string) {
   125  	if _, ok := (*st.ChannelStates)[id]; !ok {
   126  		(*st.ChannelStates)[id] = make(map[string]string)
   127  	}
   128  
   129  	for k, v := range *other {
   130  		if v == "" {
   131  			delete((*st.ChannelStates)[id], k)
   132  		} else {
   133  			(*st.ChannelStates)[id][k] = v
   134  		}
   135  	}
   136  }
   137  
   138  func (st *SessionEnv) RemoveChannelState(id string) {
   139  	delete((*st.ChannelStates), id)
   140  }
   141  
   142  // Returns a value for the specified key of the specified channel
   143  func (st *SessionEnv) GetChannelStateField(id string, field string) string {
   144  	cst, ok := (*st.ChannelStates)[id]
   145  
   146  	if !ok {
   147  		return ""
   148  	}
   149  
   150  	return cst[field]
   151  }
   152  
   153  // Returns a value for the specified connection state field
   154  func (st *SessionEnv) GetConnectionStateField(field string) string {
   155  	if st.ConnectionState == nil {
   156  		return ""
   157  	}
   158  
   159  	return (*st.ConnectionState)[field]
   160  }
   161  
   162  // SetHeader adds a header to the headers list
   163  func (st *SessionEnv) SetHeader(key string, val string) {
   164  	if st.Headers == nil {
   165  		headers := map[string]string{key: val}
   166  		st.Headers = &headers
   167  		return
   168  	}
   169  
   170  	(*st.Headers)[key] = val
   171  }
   172  
   173  // CallResult contains shared RPC result fields
   174  type CallResult struct {
   175  	Transmissions []string
   176  	Broadcasts    []*StreamMessage
   177  	CState        map[string]string
   178  	IState        map[string]string
   179  }
   180  
   181  // ConnectResult is a result of initializing a connection (calling a Connect method)
   182  type ConnectResult struct {
   183  	Identifier         string
   184  	Transmissions      []string
   185  	Broadcasts         []*StreamMessage
   186  	CState             map[string]string
   187  	IState             map[string]string
   188  	DisconnectInterest int
   189  	Status             int
   190  }
   191  
   192  func (c *ConnectResult) LogValue() slog.Value {
   193  	if c == nil {
   194  		return slog.StringValue("nil")
   195  	}
   196  
   197  	return slog.GroupValue(
   198  		slog.String("status", StatusName(c.Status)),
   199  		slog.Any("transmissions", logger.CompactValues(c.Transmissions)),
   200  		slog.Any("broadcasts", c.Broadcasts),
   201  		slog.String("identifier", c.Identifier),
   202  		slog.Int("disconnect_interest", c.DisconnectInterest),
   203  		slog.Any("cstate", c.CState),
   204  		slog.Any("istate", c.IState),
   205  	)
   206  }
   207  
   208  // ToCallResult returns the corresponding CallResult
   209  func (c *ConnectResult) ToCallResult() *CallResult {
   210  	res := CallResult{Transmissions: c.Transmissions, Broadcasts: c.Broadcasts}
   211  	if c.CState != nil {
   212  		res.CState = c.CState
   213  	}
   214  	if c.IState != nil {
   215  		res.IState = c.IState
   216  	}
   217  	return &res
   218  }
   219  
   220  // CommandResult is a result of performing controller action,
   221  // which contains informations about streams to subscribe,
   222  // messages to sent and broadcast.
   223  // It's a communication "protocol" between a node and a controller.
   224  type CommandResult struct {
   225  	StopAllStreams     bool
   226  	Disconnect         bool
   227  	Streams            []string
   228  	StoppedStreams     []string
   229  	Transmissions      []string
   230  	Broadcasts         []*StreamMessage
   231  	CState             map[string]string
   232  	IState             map[string]string
   233  	DisconnectInterest int
   234  	Status             int
   235  }
   236  
   237  func (c *CommandResult) LogValue() slog.Value {
   238  	if c == nil {
   239  		return slog.StringValue("nil")
   240  	}
   241  
   242  	return slog.GroupValue(
   243  		slog.String("status", StatusName(c.Status)),
   244  		slog.Any("streams", logger.CompactValues(c.Streams)),
   245  		slog.Any("transmissions", logger.CompactValues(c.Transmissions)),
   246  		slog.Any("stopped_streams", logger.CompactValues(c.StoppedStreams)),
   247  		slog.Bool("stop_all_streams", c.StopAllStreams),
   248  		slog.Any("broadcasts", c.Broadcasts),
   249  		slog.Bool("disconnect", c.Disconnect),
   250  		slog.Int("disconnect_interest", c.DisconnectInterest),
   251  		slog.Any("cstate", c.CState),
   252  		slog.Any("istate", c.IState),
   253  	)
   254  }
   255  
   256  // ToCallResult returns the corresponding CallResult
   257  func (c *CommandResult) ToCallResult() *CallResult {
   258  	res := CallResult{Transmissions: c.Transmissions, Broadcasts: c.Broadcasts}
   259  	if c.CState != nil {
   260  		res.CState = c.CState
   261  	}
   262  	if c.IState != nil {
   263  		res.IState = c.IState
   264  	}
   265  	return &res
   266  }
   267  
   268  type HistoryPosition struct {
   269  	Epoch  string `json:"epoch"`
   270  	Offset uint64 `json:"offset"`
   271  }
   272  
   273  func (hp *HistoryPosition) LogValue() slog.Value {
   274  	if hp == nil {
   275  		return slog.StringValue("nil")
   276  	}
   277  
   278  	return slog.GroupValue(slog.String("epoch", hp.Epoch), slog.Uint64("offset", hp.Offset))
   279  }
   280  
   281  // HistoryRequest represents a client's streams state (offsets) or a timestamp since
   282  // which we should return the messages for the current streams
   283  type HistoryRequest struct {
   284  	// Since is UTC timestamp in ms
   285  	Since int64 `json:"since,omitempty"`
   286  	// Streams contains the information of last offsets/epoch received for a particular stream
   287  	Streams map[string]HistoryPosition `json:"streams,omitempty"`
   288  }
   289  
   290  func (hr *HistoryRequest) LogValue() slog.Value {
   291  	if hr == nil {
   292  		return slog.StringValue("nil")
   293  	}
   294  
   295  	return slog.GroupValue(slog.Int64("since", hr.Since), slog.Any("streams", hr.Streams))
   296  }
   297  
   298  // Message represents incoming client message
   299  type Message struct {
   300  	Command    string         `json:"command"`
   301  	Identifier string         `json:"identifier"`
   302  	Data       interface{}    `json:"data,omitempty"`
   303  	History    HistoryRequest `json:"history,omitempty"`
   304  }
   305  
   306  func (m *Message) LogValue() slog.Value {
   307  	if m == nil {
   308  		return slog.StringValue("nil")
   309  	}
   310  
   311  	return slog.GroupValue(
   312  		slog.String("command", m.Command),
   313  		slog.String("identifier", m.Identifier),
   314  		slog.Any("data", logger.CompactAny(m.Data)),
   315  		slog.Any("history", m.History),
   316  	)
   317  }
   318  
   319  // StreamMessageMetadata describes additional information about a stream message
   320  // which can be used to modify delivery behavior
   321  type StreamMessageMetadata struct {
   322  	ExcludeSocket string `json:"exclude_socket,omitempty"`
   323  	// BroadcastType defines the message type to be used for messages sent to clients
   324  	BroadcastType string `json:"broadcast_type,omitempty"`
   325  	// Transient defines whether this message should be stored in the history
   326  	Transient bool `json:"transient,omitempty"`
   327  }
   328  
   329  func (smm *StreamMessageMetadata) LogValue() slog.Value {
   330  	if smm == nil {
   331  		return slog.StringValue("nil")
   332  	}
   333  
   334  	return slog.GroupValue(slog.String("exclude_socket", smm.ExcludeSocket))
   335  }
   336  
   337  // StreamMessage represents a pub/sub message to be sent to stream
   338  type StreamMessage struct {
   339  	Stream string                 `json:"stream"`
   340  	Data   string                 `json:"data"`
   341  	Meta   *StreamMessageMetadata `json:"meta,omitempty"`
   342  
   343  	// Offset is the position of this message in the stream
   344  	Offset uint64
   345  	// Epoch is the uniq ID of the current storage state
   346  	Epoch string
   347  }
   348  
   349  func (sm *StreamMessage) LogValue() slog.Value {
   350  	attrs := []slog.Attr{
   351  		slog.String("stream", sm.Stream),
   352  		slog.Any("data", logger.CompactValue(sm.Data)),
   353  	}
   354  
   355  	if sm.Epoch != "" {
   356  		attrs = append(attrs, slog.Uint64("offset", sm.Offset), slog.String("epoch", sm.Epoch))
   357  	}
   358  
   359  	if sm.Meta != nil {
   360  		attrs = append(attrs, slog.Any("meta", sm.Meta))
   361  	}
   362  
   363  	return slog.GroupValue(attrs...)
   364  }
   365  
   366  func (sm *StreamMessage) ToReplyFor(identifier string) *Reply {
   367  	data := sm.Data
   368  
   369  	var msg interface{}
   370  
   371  	// We ignore JSON deserialization failures and consider the message to be a string
   372  	json.Unmarshal([]byte(data), &msg) // nolint:errcheck
   373  
   374  	if msg == nil {
   375  		msg = sm.Data
   376  	}
   377  
   378  	stream := ""
   379  
   380  	// Only include stream if offset/epovh is present
   381  	if sm.Epoch != "" {
   382  		stream = sm.Stream
   383  	}
   384  
   385  	return &Reply{
   386  		Identifier: identifier,
   387  		Message:    msg,
   388  		StreamID:   stream,
   389  		Offset:     sm.Offset,
   390  		Epoch:      sm.Epoch,
   391  	}
   392  }
   393  
   394  // RemoteCommandMessage represents a pub/sub message with a remote command (e.g., disconnect)
   395  type RemoteCommandMessage struct {
   396  	Command string          `json:"command,omitempty"`
   397  	Payload json.RawMessage `json:"payload,omitempty"`
   398  }
   399  
   400  func (m *RemoteCommandMessage) LogValue() slog.Value {
   401  	if m == nil {
   402  		return slog.StringValue("nil")
   403  	}
   404  
   405  	return slog.GroupValue(slog.String("command", m.Command), slog.Any("payload", m.Payload))
   406  }
   407  
   408  func (m *RemoteCommandMessage) ToRemoteDisconnectMessage() (*RemoteDisconnectMessage, error) {
   409  	dmsg := RemoteDisconnectMessage{}
   410  
   411  	if err := json.Unmarshal(m.Payload, &dmsg); err != nil {
   412  		return nil, err
   413  	}
   414  
   415  	return &dmsg, nil
   416  }
   417  
   418  // RemoteDisconnectMessage contains information required to disconnect a session
   419  type RemoteDisconnectMessage struct {
   420  	Identifier string `json:"identifier"`
   421  	Reconnect  bool   `json:"reconnect"`
   422  }
   423  
   424  func (m *RemoteDisconnectMessage) LogValue() slog.Value {
   425  	if m == nil {
   426  		return slog.StringValue("nil")
   427  	}
   428  
   429  	return slog.GroupValue(slog.String("ids", m.Identifier), slog.Bool("reconnect", m.Reconnect))
   430  }
   431  
   432  // PingMessage represents a server ping
   433  type PingMessage struct {
   434  	Type    string      `json:"type"`
   435  	Message interface{} `json:"message,omitempty"`
   436  }
   437  
   438  func (p *PingMessage) LogValue() slog.Value {
   439  	return slog.GroupValue(slog.String("type", p.Type), slog.Any("message", p.Message))
   440  }
   441  
   442  func (p *PingMessage) GetType() string {
   443  	return PingType
   444  }
   445  
   446  // DisconnectMessage represents a server disconnect message
   447  type DisconnectMessage struct {
   448  	Type      string `json:"type"`
   449  	Reason    string `json:"reason"`
   450  	Reconnect bool   `json:"reconnect"`
   451  }
   452  
   453  func (d *DisconnectMessage) LogValue() slog.Value {
   454  	return slog.GroupValue(slog.String("type", d.Type), slog.String("reason", d.Reason), slog.Bool("reconnect", d.Reconnect))
   455  }
   456  
   457  func (d *DisconnectMessage) GetType() string {
   458  	return DisconnectType
   459  }
   460  
   461  func NewDisconnectMessage(reason string, reconnect bool) *DisconnectMessage {
   462  	return &DisconnectMessage{Type: "disconnect", Reason: reason, Reconnect: reconnect}
   463  }
   464  
   465  // Reply represents an outgoing client message
   466  type Reply struct {
   467  	Type        string      `json:"type,omitempty"`
   468  	Identifier  string      `json:"identifier,omitempty"`
   469  	Message     interface{} `json:"message,omitempty"`
   470  	Reason      string      `json:"reason,omitempty"`
   471  	Reconnect   bool        `json:"reconnect,omitempty"`
   472  	StreamID    string      `json:"stream_id,omitempty"`
   473  	Epoch       string      `json:"epoch,omitempty"`
   474  	Offset      uint64      `json:"offset,omitempty"`
   475  	Sid         string      `json:"sid,omitempty"`
   476  	Restored    bool        `json:"restored,omitempty"`
   477  	RestoredIDs []string    `json:"restored_ids,omitempty"`
   478  }
   479  
   480  func (r *Reply) LogValue() slog.Value {
   481  	if r == nil {
   482  		return slog.StringValue("nil")
   483  	}
   484  
   485  	attrs := []slog.Attr{}
   486  
   487  	if r.Type != "" {
   488  		attrs = append(attrs, slog.String("type", r.Type))
   489  	}
   490  
   491  	if r.Identifier != "" {
   492  		attrs = append(attrs, slog.String("identifier", r.Identifier))
   493  	}
   494  
   495  	if r.Message != nil {
   496  		attrs = append(attrs, slog.Any("message", logger.CompactAny(r.Message)))
   497  	}
   498  
   499  	if r.Reason != "" {
   500  		attrs = append(attrs, slog.String("reason", r.Reason), slog.Bool("reconnect", r.Reconnect))
   501  	}
   502  
   503  	if r.StreamID != "" {
   504  		attrs = append(attrs, slog.String("stream_id", r.StreamID), slog.String("epoch", r.Epoch), slog.Uint64("offset", r.Offset))
   505  	}
   506  
   507  	if r.Sid != "" {
   508  		attrs = append(attrs, slog.String("sid", r.Sid), slog.Bool("restored", r.Restored), slog.Any("restored_ids", r.RestoredIDs))
   509  	}
   510  
   511  	return slog.GroupValue(attrs...)
   512  }
   513  
   514  func (r *Reply) GetType() string {
   515  	return r.Type
   516  }
   517  
   518  // PubSubMessageFromJSON takes raw JSON byte array and return the corresponding struct
   519  func PubSubMessageFromJSON(raw []byte) (interface{}, error) {
   520  	smsg := StreamMessage{}
   521  
   522  	if err := json.Unmarshal(raw, &smsg); err == nil {
   523  		if smsg.Stream != "" {
   524  			return smsg, nil
   525  		}
   526  	}
   527  
   528  	batch := []*StreamMessage{}
   529  
   530  	if err := json.Unmarshal(raw, &batch); err == nil {
   531  		if len(batch) > 0 && batch[0].Stream != "" {
   532  			return batch, nil
   533  		}
   534  	}
   535  
   536  	rmsg := RemoteCommandMessage{}
   537  
   538  	if err := json.Unmarshal(raw, &rmsg); err != nil {
   539  		return nil, err
   540  	}
   541  
   542  	return rmsg, nil
   543  }
   544  
   545  // WelcomeMessage for a session ID
   546  func WelcomeMessage(sid string) string {
   547  	return string(utils.ToJSON(Reply{Sid: sid, Type: WelcomeType}))
   548  }
   549  
   550  // ConfirmationMessage returns a subscription confirmation message for a specified identifier
   551  func ConfirmationMessage(identifier string) string {
   552  	return string(utils.ToJSON(Reply{Identifier: identifier, Type: ConfirmedType}))
   553  }
   554  
   555  // RejectionMessage returns a subscription rejection message for a specified identifier
   556  func RejectionMessage(identifier string) string {
   557  	return string(utils.ToJSON(Reply{Identifier: identifier, Type: RejectedType}))
   558  }
   559  
   560  // DisconnectionMessage returns a disconnect message with the specified reason and reconnect flag
   561  func DisconnectionMessage(reason string, reconnect bool) string {
   562  	return string(utils.ToJSON(DisconnectMessage{Type: DisconnectType, Reason: reason, Reconnect: reconnect}))
   563  }