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 }