package jackbox import ( "errors" "fmt" ) // Sentinel errors for common failure scenarios var ( // ErrNotAuthenticated indicates the client is not authenticated ErrNotAuthenticated = errors.New("not authenticated") // ErrAuthFailed indicates authentication failed ErrAuthFailed = errors.New("authentication failed") // ErrConnectionLost indicates the WebSocket connection was lost ErrConnectionLost = errors.New("connection lost") // ErrTokenExpired indicates the authentication token has expired ErrTokenExpired = errors.New("token expired") // ErrInvalidResponse indicates an unexpected response from the API ErrInvalidResponse = errors.New("invalid response from API") // ErrSessionNotFound indicates the specified session does not exist ErrSessionNotFound = errors.New("session not found") // ErrNotSubscribed indicates not subscribed to any session ErrNotSubscribed = errors.New("not subscribed to any session") ) // APIError represents an API-related error with context type APIError struct { Op string // Operation that failed (e.g., "vote", "get_session", "authenticate") StatusCode int // HTTP status code Message string // Error message from API Err error // Underlying error } func (e *APIError) Error() string { if e.StatusCode > 0 { return fmt.Sprintf("API error during %s (HTTP %d): %s", e.Op, e.StatusCode, e.Message) } if e.Err != nil { return fmt.Sprintf("API error during %s: %s (%v)", e.Op, e.Message, e.Err) } return fmt.Sprintf("API error during %s: %s", e.Op, e.Message) } func (e *APIError) Unwrap() error { return e.Err } // WebSocketError represents a WebSocket-related error with context type WebSocketError struct { Op string // Operation that failed (e.g., "connect", "subscribe", "send") Err error // Underlying error } func (e *WebSocketError) Error() string { return fmt.Sprintf("WebSocket error during %s: %v", e.Op, e.Err) } func (e *WebSocketError) Unwrap() error { return e.Err } // SessionError represents a session-related error with context type SessionError struct { Op string // Operation that failed (e.g., "subscribe", "get_active") SessionID int // Session ID Err error // Underlying error } func (e *SessionError) Error() string { return fmt.Sprintf("session error during %s for session %d: %v", e.Op, e.SessionID, e.Err) } func (e *SessionError) Unwrap() error { return e.Err } // IsRetryable returns true if the error is transient and the operation should be retried func IsRetryable(err error) bool { if err == nil { return false } // Check for known retryable errors if errors.Is(err, ErrConnectionLost) || errors.Is(err, ErrTokenExpired) { return true } // Check for API errors with retryable status codes var apiErr *APIError if errors.As(err, &apiErr) { // 5xx errors are typically retryable if apiErr.StatusCode >= 500 && apiErr.StatusCode < 600 { return true } // 429 Too Many Requests is retryable if apiErr.StatusCode == 429 { return true } } // WebSocket errors are generally retryable var wsErr *WebSocketError if errors.As(err, &wsErr) { return true } return false } // IsFatal returns true if the error is fatal and reconnection should not be attempted func IsFatal(err error) bool { if err == nil { return false } // Check for known fatal errors if errors.Is(err, ErrAuthFailed) || errors.Is(err, ErrSessionNotFound) { return true } // Check for API errors with fatal status codes var apiErr *APIError if errors.As(err, &apiErr) { // 4xx errors (except 429) are typically fatal if apiErr.StatusCode >= 400 && apiErr.StatusCode < 500 && apiErr.StatusCode != 429 { return true } } return false }