I have a chat application with SignalR in Net5 and #microsoft/signalr in Angular12. The problem I have is when the connection is lost, i need to reconnect and rejoin a group, the reconnection does work but the reconnection to a group does not.
The function I use to join a group in signalr.service.ts is:
addTogroup(room: any,user:any)
The method I use to recover the connection in signalr.service.ts is:
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: retryContext => {
if (retryContext.elapsedMilliseconds < 60000) {
// If we've been reconnecting for less than 60 seconds so far,
// wait between 0 and 10 seconds before the next reconnect attempt.
return Math.random() * 10000;
} else {
// If we've been reconnecting for more than 60 seconds so far, stop reconnecting.
return null;
}
}
})
Is it possible to access in any way the function that joins you to a group, when the withAutomaticReconnect is being executed? To join the group you need the user and the room that are in app.component.ts
Thank you very much for the help, I have already read the documentation many times but I cannot find the solution.
My code is:
app.component.ts
import { Component, OnInit, ViewChild } from '#angular/core';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
import { SignalrService } from '../services/signalr.service';
import { HttpClient } from '#angular/common/http';
import { i18nMetaToJSDoc } from '#angular/compiler/src/render3/view/i18n/meta';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
#ViewChild('myinputfile') myinputfile: any;
title = 'chat-ui';
listMessages: any[] = [];
messages: string[] = [];
message: string = "";
user: string = "";
ticket: string = "";
currentFiles: any = [];
myimg: any = "";
constructor(public signalrService: SignalrService, private http: HttpClient) {
}
ngOnInit(): void {
this.signalrService.startConnection((message: any) => {
this.listMessages.push({ mensaje: message.mensaje, user: message.user, tipo: "texto" });
}, (imageMessage: any) => {
this.listMessages.push(imageMessage);
}, (incomingConnection: any) => {
console.log(incomingConnection);
}, (error:any) => {
console.log(error);
});
}
addRoom() {
this.signalrService.addTogroup(this.ticket, this.user);
}
async sendMessage() {
if (this.myinputfile.nativeElement.files.length > 0) {
let formData = new FormData();
formData.append("RoomId", this.ticket);
formData.append("IdUsuario", this.user);
formData.append("IdCliente", "1");
formData.append("Interno", "1");
formData.append("File", this.myinputfile.nativeElement.files[0]);
await this.signalrService.sendImagesMessage(formData, (response: any) => {
this.myimg = response;
}, (error: any) => {
console.log(error);
});
this.myinputfile.nativeElement.value = "";
} else {
this.signalrService.sendMessageGroup(this.ticket, this.user, this.message);
}
}
onaddremoveFiles() {
if (this.myinputfile.nativeElement.files.length == 0) {
this.myinputfile.nativeElement.click();
} else {
this.myinputfile.nativeElement.value = "";
}
}
onfilesSelected(files: any) { return files.length > 0; }
openImages(src: any) {
let data = src;
let w = window.open('about:blank');
let image = new Image();
image.src = data;
if (w !== null) {
w.document.write(image.outerHTML);
}
}
}
signalr.service.ts
import { Injectable } from '#angular/core';
import * as signalR from "#microsoft/signalr";
import { HttpClient } from '#angular/common/http';
#Injectable({
providedIn: 'root'
})
export class SignalrService {
public hubConnection!: signalR.HubConnection;
constructor(private http:HttpClient) {
this.buildConnection();
//this.startConnection();
}
public buildConnection = () => {
this.hubConnection = new signalR.HubConnectionBuilder()
.configureLogging(signalR.LogLevel.Information)
.withUrl("https://localhost:44352/chatHub",{
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
})
//.withAutomaticReconnect([0,1000,5000,6000,7000,8000,10000,15000])
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: retryContext => {
if (retryContext.elapsedMilliseconds < 60000) {
// If we've been reconnecting for less than 60 seconds so far,
// wait between 0 and 10 seconds before the next reconnect attempt.
return Math.random() * 10000;
} else {
// If we've been reconnecting for more than 60 seconds so far, stop reconnecting.
return null;
}
}
})
.build();
};
public startConnection = (
onMessageCallback:Function,
onMessageImageCallback:Function,
onIncomingConnectionCallback:Function,
onErrorCallback:Function) => {
this.hubConnection
.start()
.then(() => {
console.log("Connection Started...");
this.ListeningConnections();
this.ListeningIncomeMessages(onMessageCallback);
this.ListeningIncomeImagesMessages(onMessageImageCallback);
this.ListeningIncomingConnection(onIncomingConnectionCallback);
this.ListeningError(onErrorCallback);
})
.catch(err => {
/*console.log("Error while starting connection: " + err);
setTimeout(() => {
this.startConnection();
}, 3000);*/
});
};
private ListeningConnections(){
this.hubConnection.on("ReceiveConnID", function (connid) {
console.log("ConnID: " + connid);
});
}
public addTogroup(room: any,user:any){
this.hubConnection.invoke("AddToGroup", room, user);
}
public sendMessageGroup(room: any,user:any,message:any){
this.hubConnection.invoke("SendMessageGroup",room, user,message,0)
.catch(function (err) {
return console.error(err.toString());
});
}
public ListeningIncomeMessages(onMessageCallback:Function){
this.hubConnection.on("ReceiveMessageGroup", (user, message) => {
onMessageCallback({mensaje: message, user: user});
});
}
public ListeningIncomingConnection(onIncomingConnectionCallback:Function){
this.hubConnection.on("IncomingConnection",(message) => {
onIncomingConnectionCallback({mensaje: message});
});
}
public ListeningUserConnected(onMessageCallback:Function){
this.hubConnection.on("ReceiveMessageUserConnected", (user, message) => {
onMessageCallback({mensaje: message, user: user});
});
}
public ListeningError(onErrorCallback:Function){
this.hubConnection.on("onError", (message) => {
onErrorCallback({mensaje: message});
});
}
public ListeningIncomeImagesMessages(onImageMessageCallback:Function){
this.hubConnection.on("ReceiveImageMessageGroup", (user, image) => {
//console.log(image);
// console.log(image.idUsuario);
// console.log(image.idCliente);
onImageMessageCallback({ mensaje: image, user: user, tipo: "imagen" });
});
}
public async sendImagesMessage(formData:FormData,onOkCallback:Function,onErroCallback:Function){
await this.http.post("https://localhost:44352/UploadImages",formData).toPromise().then((response)=>{
onOkCallback(response);
}).catch((error)=>{
onErroCallback(error);
console.log(error);
});
}
}
ChatHub.cs
public class ChatHub : Hub
{
public readonly static List<UserViewModel> _Connections = new List<UserViewModel>();
public readonly static List<RoomViewModel> _Rooms = new List<RoomViewModel>();
private readonly static Dictionary<string, string> _ConnectionsMap = new Dictionary<string, string>();
private readonly DbContext _context;
private readonly IMapper _mapper;
private readonly IMensajeService _mensajeService;
private readonly IUnitOfWork _unitOfWork;
public ChatHub(TicketingContext context, IUnitOfWork unitOfWork, IMensajeService mensajeService, IMapper mapper)
{
_context = context;
_mensajeService = mensajeService;
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public IEnumerable<RoomViewModel> GetRooms()
{
if (_Rooms.Count == 0)
{
foreach (var room in _context.Tickets)
{
var roomViewModel = _mapper.Map<Tickets, RoomViewModel>(room);
_Rooms.Add(roomViewModel);
}
}
return _Rooms.ToList();
}
public IEnumerable<UserViewModel> GetUsers(string roomName)
{
return _Connections.Where(u => u.CurrentRoom == roomName).ToList();
}
//envia mensajes a un grupo determinado
public async Task SendMessageGroup(string room, string user, string message, int interno)
{
try
{
var msg = new MensajeGrabarDto()
{
IdTicket = Int32.Parse(room),
IdUsuario = Int32.Parse(user),
Texto = Regex.Replace(message, #"(?i)<(?!img|a|/a|/img).*?>", string.Empty),
Interno = interno,
Adjunto = new byte[] { },
AdjuntoNombre = "",
AdjuntoTipo = ""
//llenar los campos
};
DbResponse respon = await _unitOfWork.MensajeRepository.InsertarMensajeRepository(msg);
if (respon.retcode == 0)
{
await _unitOfWork.SaveChangesAsync();
var mensaje = await _mensajeService.ObtenerMensajesPorTicketService(Int32.Parse(room));
var mensaje2 = mensaje.Where(x => x.IdTicket == Int32.Parse(room)).OrderByDescending(x => x.IdMensaje).FirstOrDefault();
await Clients.Group(room).SendAsync("ReceiveMessageGroup", user, mensaje2);
}
else {
await Clients.Caller.SendAsync("onError", respon.mensaje);
}
}
catch (Exception)
{
await Clients.Caller.SendAsync("onError", "Message not send! Message should be 1-1500 characters.");
}
}
//une a un usuario a determinado grupo
public async Task AddToGroup(string room, string usuario)
{
try
{
await Groups.AddToGroupAsync(Context.ConnectionId, room);
//mensaje para decir que alguien se conecto
await Clients.Group(room).SendAsync("IncomingConnection", $"Alguien se conecto { usuario}");
}
catch (Exception ex)
{
await Clients.Caller.SendAsync("onError", "You failed to join the chat room!" + ex.Message);
}
}
//desloguea a un usuario de un grupo
public async Task Leave(string roomName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
}
public override Task OnConnectedAsync()
{
try
{
Clients.Client(Context.ConnectionId).SendAsync("ReceiveConnID", Context.User.Identity.Name);
}
catch (Exception ex)
{
Clients.Caller.SendAsync("onError", "OnConnected:" + ex.Message);
}
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
try
{
}
catch (Exception ex)
{
Clients.Caller.SendAsync("onError", "OnDisconnected: " + ex.Message);
}
return base.OnDisconnectedAsync(exception);
}
}
I'm following this tutorial to understand how webSocket works in Spring.
My controller class:
#Controller
public class TestController {
#MessageMapping("/chat")
#SendTo("/topic/messages")
public String greeting(String message) {
System.out.println("HelloWorld"); // <--- The code is executed
return "HelloWorld";
}
}
My WebSocket class:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").setAllowedOriginPatterns("*").withSockJS();
}
}
My js client:
export function connect() {
var socket = new SockJS("http://192.168.1.63:5001/chat");
stompClient = Stomp.over(socket);
//stompClient.debug = null;
stompClient.connect({}, (frame) => {
console.log(frame);
stompClient.subscribe("/topic/messages", messageOutput => {
console.log(messageOutput); // <-- Nothing here!
});
});
}
export function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
console.log("Disconnected");
}
export function sendMessage() {
stompClient.send(
"/app/chat",
{},
"this is only a test!"
);
}
what happens in Chrome Inspector: Chrome Inspector
So, it seems as the server side doesn't replay at client though the "greeting" method is invoked.
Here's I have attached my code. I have implemented global handler and now I need to extract 'dashboard' from 500 Error on zone.js. How can I get it in the global Handler. Is there any way that I can get my desired output?
import { ErrorHandler, Injectable, Injector, NgZone } from '#angular/core';
import { LocationStrategy, PathLocationStrategy } from '#angular/common';
import { LogService } from './logging-service.service';
import * as StackTrace from 'stacktrace-js';
#Injectable()
export class GlobalErrorHandler implements ErrorHandler {
private errors = new Array<any>();
constructor(public injector: Injector, public zone: NgZone) {
}
public handleError(error: any) {
console.log('Im a global handler', JSON.parse(error));
const logService = this.injector.get(LogService);
const location = this.injector.get(LocationStrategy);
const message = error.message ? error.message : error.toString();
const url = location instanceof PathLocationStrategy
? location.path() : '';
const callbackFun = function (stackframes) {
const stringifiedStack = stackframes.map(function (sf) {
return sf.toString();
}).join('\n');
console.log(stringifiedStack);
};
const errback = function (err) {
console.log(err);
console.log(err.stack);
};
window.onerror = function (msg, file, line, col, error) {
// this.zone.fromError.subscribe(this.onZoneError);
StackTrace.fromError(message).then(callbackFun).catch(errback);
StackTrace.get().then(callbackFun).catch(message);
};
const handleErrorData = {
// Some Json to send to server
};
logService.logError(handleErrorData);
throw error;
}
public onZoneError(error) {
console.log(error);
console.error('Error', error instanceof Error ? error.message : error.toString());
}
}
Implement an HttpInterceptor (requires Angular 4.3.x)
From there listen to errors and extract the request url and split it into its parts.
I have some angular code that asks for user name input:
playCtrl.js
var myApp = angular.module('myApp');
/*
* CONTROLLERS METHOD
*/
myApp.controller('PlayController', ['$scope', '$http', function($scope, $http) {
var REQUEST_SERVICE_URI = '/Project2/play.do';
//var REQUEST_SERVICE_URI = 'http://13.59.197.145:8085/Project2/#/play';
var playerObject = {} // create playerObject
$scope.message = "Let's play!";
$scope.message2 = "Please enter your name";
$scope.user = {
userid: '',
username: '',
roleId: '',
statusId: ''
};
$scope.register = function() {
playerObject = $scope.user; // adding user to a playerObject
console.log('playerObject.name: ' + playerObject.name);
//console.log('playerObject: ' + $scope.user.name);
console.log("REGISTER BUTTON WAS CLICKED");
$http.post(REQUEST_SERVICE_URI, playerObject).
then(function(playerObject) {
alert("SUCCESS");
//$scope.user = data;
});
}
}])
Then I have my SpringController.java that's trying to get this user object:
#RestController
public class SpringController {
// has to accept
#RequestMapping(headers="Accept=application/json", value="/play.do", method = RequestMethod.POST)
public String registerUser(Users user, BindingResult bindingResult, ModelMap modelMap, HttpSession session){
Register r = new Register();
System.out.println("TRYING TO CREATE A USER: " + user);
if(r.createUser(user.getUsername())){
session.setAttribute("username", user.getUsername());
session.setAttribute("role", user.getRole());
session.setAttribute("status", user.getStatus());
System.out.println("Created user: " + user.getUsername());
return "lobby";
}else{
return "login";
}
}
My console DOES print out everything in the angularjs perfectly fine, and it also prints "TRYING TO CREATE A USER: " + user from the java side, however it shoes that all everything is null: TRYING TO CREATE A USER: Users [userid=0, username=null, roleId=null, statusId=null]
(my user create implementation only needs a username, all others can be null)
Here's my web.xml:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/beans.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>SpringDispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/beans.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringDispatcher</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
Here's the error I get from the console:
javax.validation.ConstraintViolationException: Validation failed for classes
[com.revature.bean.Users] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='Username cannot be empty(1-20 characters)', propertyPath=username, rootBeanClass=class com.revature.bean.Users, messageTemplate='Username cannot be empty(1-20 characters)'}
Users.java :
package com.revature.bean;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.validation.constraints.Size;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.stereotype.Component;
#Component
#Entity
#Table(name = "Users")
#Cache(usage = CacheConcurrencyStrategy.READ_ONLY, region = "myAwesomeCache")
public class Users {
#Id
#Column(name = "U_Id")
#SequenceGenerator(name = "UID_SEQ", sequenceName = "UID_SEQ")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "UID_SEQ")
private int userid;
#NotEmpty(message="Username cannot be empty(1-20 characters)")
#Size(min=1,max=20)
#Column(name = "Username")
private String username;
public Users() {
super();
}
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "Role_ID")
private Roles role;
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "Status_ID")
private Status status;
public Users(int userid, String username, Roles roleId, Status statusId) {
super();
this.userid = userid;
this.username = username;
this.role = roleId;
this.status = statusId;
}
public Users(String username, Roles roleId, Status statusId) {
super();
this.username = username;
this.role = roleId;
this.status = statusId;
}
public int getUserid() {
return userid;
}
public void setUserid(int userid) {
this.userid = userid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Status getStatusId() {
return status;
}
public void setStatusId(Status status) {
this.status = status;
}
public Roles getRole() {
return role;
}
public void setRole(Roles role) {
this.role = role;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
#Override
public String toString() {
return "Users [userid=" + userid + ", username=" + username + ", roleId=" + role + ", statusId=" + status
+ "]";
}
}
How would I go about sending the object properly?
I just figured it out: I had to add #RequestBody within the () of my SpringController method, and change the input from Object to String type:
public String registerUser(#RequestBody String username, BindingResult bindingResult, ModelMap modelMap, HttpSession session)
I have a simple MVC Get method as below to get a Customer Id from the Session
[HttpGet]
public string GetCustomerId()
{
return Session["CUSTOMERID"].ToString();
}
If I hit this URL directly in the browser
http://localhost/myApp/Home/GetCustomerId I can set a breakpoint int the method and it gets hit and I get the value returned.
However, I need to call the method from my Client code which is Angular 2 written in typescript. My Typescript method is as below - I cannot get it to hit the MVC Breakpoint even though I am logging the exact same URL as above to the console.
public getCustomerIdFromSession() {
console.log('get customer from session called');
let srv = this.environmentService.getCurrentEnvironment();
let httpOrHttps = '';
if (srv === AppSettings.ENV_LOCALHOST) {
httpOrHttps = AppSettings.URL_HTTP;
}
else {
httpOrHttps = AppSettings.URL_HTTPS;
}
let baseUrl = httpOrHttps + srv + AppSettings.URL_GET_CUST_FROM_SESSION;
console.log(baseUrl); //this logs - http://localhost/myApp/Home/GetCustomerId
return this.http.get(baseUrl)
.catch(this.handleError);
}
public handleError(error: Response) {
console.log("error");
return Observable.throw(error.json() || 'Server Error');
}
**UPDATE To include entire Typescript service
import { Injectable, Output, EventEmitter } from '#angular/core';
import { Http, Response, RequestOptions, Headers } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { EnvironmentService } from '../common/environment.service';
import { AppSettings } from '../common/app-settings';
#Injectable()
export class SessionService {
#Output() public gSession: EventEmitter<any> = new EventEmitter();
private sessionTime: number = 1500000; // 25 minute
constructor(private http: Http, private environmentService: EnvironmentService) {
}
public setValue(isLoading: boolean): void {
this.gSession.emit(isLoading);
}
public getValue(): any {
return this.gSession;
}
public startSession(): void {
this.getCustomerIdFromSession();
let timeoutId = setTimeout(() => {
this.setValue(true);
}, this.sessionTime);
}
public getCustomerIdFromSession() {
console.log('get customer from session called');
let srv = this.environmentService.getCurrentEnvironment();
let httpOrHttps = '';
if (srv === AppSettings.ENV_LOCALHOST) {
httpOrHttps = AppSettings.URL_HTTP;
}
else {
httpOrHttps = AppSettings.URL_HTTPS;
}
let baseUrl = httpOrHttps + srv + AppSettings.URL_GET_CUST_FROM_SESSION;
console.log(baseUrl); //this logs - http://localhost/myApp/Home/GetCustomerId
return this.http.get(baseUrl)
.catch(this.handleError);
}
public handleError(error: Response) {
console.log("error");
return Observable.throw(error.json() || 'Server Error');
}
public extractData(res: Response) {
console.log("In extract method");
let body = res.json();
console.log(body);
if (body) {
return body.data || body;
} else {
return {};
}
}
}
You are not mapping the response.
return this.http
.get(baseUrl)
.map(this.extractData)
.catch(this.handleError);
private extractData(res: Response) {
let body = res.json();
if (body) {
return body.data || body;
} else {
return {};
}
}
You have to map the response and process it with json() if you know that will be a JSON or with text(). Usually will be JSON.
I took the liberty of adding a response handler such as extractData. You could just json() the response directly if you wanted.