How manage the online/offline status of the user in SignalR - javascript

I have created the chat application with SignalR version 2.2.1 and I would like to create the information about user status - that means online/offline state.
I have tried use the method:
public override Task OnConnected()
{
var token = Context.QueryString["token"];
if (string.IsNullOrEmpty(token)) return Task.FromResult(0);
using (var dbContext = new AppDbContext())
{
dbContext.UserConnections.Add(new UserConnection { ConnectionKey = Context.ConnectionId, UserId = token.GetUserId() });
dbContext.SaveChanges();
}
Clients.Others.UserConnected(token.GetUserId());
return Task.FromResult(0);
}
public override Task OnDisconnected(bool stopCalled)
{
using (var dbContext = new AppDbContext())
{
var connection = dbContext.UserConnections.First(x => x.ConnectionKey == Context.ConnectionId);
var userId = connection.UserId;
if (connection == null) return Task.FromResult(0);
dbContext.UserConnections.Remove(connection);
dbContext.SaveChanges();
}
Clients.Others.UserDisconnected(userId);
return Task.FromResult(0);
}
I don't know how correctly work with disconnect method on the client - because when user refresh the browser automaticly is called the OnDisconnected event with parameter stopCalled=true - I can now send some information about disconnection this user and set the icon for offline but immediately is user connect again and the icon is set to the online state and the user experience is strange.
How can I manage the disconnect state on javascript client without blinking the online/offline status especialy thought refresh page?

Related

Angular 8 - handling SSE reconnect on error

I'm working on an Angular 8 (with Electron 6 and Ionic 4) project and right now we are having evaluation phase where we are deciding whether to replace polling with SSE (Server-sent events) or Web Sockets. My part of the job is to research SSE.
I created small express application which generates random numbers and it all works fine. The only thing that bugs me is correct way to reconnect on server error.
My implementation looks like this:
private createSseSource(): Observable<MessageEvent> {
return Observable.create(observer => {
this.eventSource = new EventSource(SSE_URL);
this.eventSource.onmessage = (event) => {
this.zone.run(() => observer.next(event));
};
this.eventSource.onopen = (event) => {
console.log('connection open');
};
this.eventSource.onerror = (error) => {
console.log('looks like the best thing to do is to do nothing');
// this.zone.run(() => observer.error(error));
// this.closeSseConnection();
// this.reconnectOnError();
};
});
}
I tried to implement reconnectOnError() function following this answer, but I just wasn't able to make it work. Then I ditched the reconnectOnError() function and it seems like it's a better thing to do. Do not try to close and reconnect nor propagate error to observable. Just sit and wait and when the server is running again it will reconnect automatically.
Question is, is this really the best thing to do? Important thing to mention is, that the FE application communicates with it's own server which can't be accessed by another instance of the app (built-in device).
I see that my question is getting some attention so I decided to post my solution. To answer my question: "Is this really the best thing to do, to omit reconnect function?" I don't know :). But this solution works for me and it was proven in production, that it offers way how to actually control SSE reconnect to some extent.
Here's what I did:
Rewritten createSseSource function so the return type is void
Instead of returning observable, data from SSE are fed to subjects/NgRx actions
Added public openSseChannel and private reconnectOnError functions for better control
Added private function processSseEvent to handle custom message types
Since I'm using NgRx on this project every SSE message dispatches corresponding action, but this can be replaced by ReplaySubject and exposed as observable.
// Public function, initializes connection, returns true if successful
openSseChannel(): boolean {
this.createSseEventSource();
return !!this.eventSource;
}
// Creates SSE event source, handles SSE events
protected createSseEventSource(): void {
// Close event source if current instance of SSE service has some
if (this.eventSource) {
this.closeSseConnection();
this.eventSource = null;
}
// Open new channel, create new EventSource
this.eventSource = new EventSource(this.sseChannelUrl);
// Process default event
this.eventSource.onmessage = (event: MessageEvent) => {
this.zone.run(() => this.processSseEvent(event));
};
// Add custom events
Object.keys(SSE_EVENTS).forEach(key => {
this.eventSource.addEventListener(SSE_EVENTS[key], event => {
this.zone.run(() => this.processSseEvent(event));
});
});
// Process connection opened
this.eventSource.onopen = () => {
this.reconnectFrequencySec = 1;
};
// Process error
this.eventSource.onerror = (error: any) => {
this.reconnectOnError();
};
}
// Processes custom event types
private processSseEvent(sseEvent: MessageEvent): void {
const parsed = sseEvent.data ? JSON.parse(sseEvent.data) : {};
switch (sseEvent.type) {
case SSE_EVENTS.STATUS: {
this.store.dispatch(StatusActions.setStatus({ status: parsed }));
// or
// this.someReplaySubject.next(parsed);
break;
}
// Add others if neccessary
default: {
console.error('Unknown event:', sseEvent.type);
break;
}
}
}
// Handles reconnect attempts when the connection fails for some reason.
// const SSE_RECONNECT_UPPER_LIMIT = 64;
private reconnectOnError(): void {
const self = this;
this.closeSseConnection();
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = setTimeout(() => {
self.openSseChannel();
self.reconnectFrequencySec *= 2;
if (self.reconnectFrequencySec >= SSE_RECONNECT_UPPER_LIMIT) {
self.reconnectFrequencySec = SSE_RECONNECT_UPPER_LIMIT;
}
}, this.reconnectFrequencySec * 1000);
}
Since the SSE events are fed to subject/actions it doesn't matter if the connection is lost since at least last event is preserved within subject or store. Attempts to reconnect can then happen silently and when new data are send, there are processed seamlessly.

Hub Method only running once

(I'm new to Signalr)
I'm developing an web application which uses Signalr-core to update the page in realtime.
The problem i walk into is that when i run multiple clients the method i'm running wil run as many times at once as there are clients.
so i want to know if there is any way to make the first client call the hub method and then keep it running and broadcasting to all connected clients.
this is what i'm doing with my client:
connection.on('update', (id, Color) => {
var x = document.getElementById(id);
if (Color === "green" && x.classList.contains("red"))
{
//console.log("green");
x.classList.remove("red");
x.classList.add("green");
}
else if (Color === "red" && x.classList.contains("green"))
{
//console.log("Red");
x.classList.remove("green");
x.classList.add("red");
}});
connection.start()
.then(() => connection.invoke('updateclient', updating));
and this is what i'm doing with my hub:
public void UpdateClient(bool updating)//this must run only once
{
while (updating == true)
{
Thread.Sleep(2000);
foreach (var item in _context.Devices)
{
IPHostEntry hostEntry = Dns.GetHostEntry(item.DeviceName);
IPAddress[] ips = hostEntry.AddressList;
foreach (IPAddress Ip in ips)
{
Ping MyPing = new Ping();
PingReply reply = MyPing.Send(Ip, 500);
if (reply.Status == IPStatus.Success)
{
Color = "green";
Clients.All.InvokeAsync("update", item.DeviceID, Color);
break;
}
else
{
Color = "red";
Clients.All.InvokeAsync("update", item.DeviceID, Color);
}
}
}
}
}
please let me know if i'm unclear about something.
and thank you in advance.
As I mention in the comment, you can invoke UpdateClient method on first appearance of any client. The option I came up with is as follows;
It is quite common to use a static list for clients connected to hub
public static class UserHandler
{
public static List<string> UserList = new List<string>();
}
In your hub, override OnConnected and OnDisconnected methods to add/remove clients to/from the list and define the very first client connected to hub for your purpose.
public class MyHub : Hub
{
private static IHubContext context = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
public override Task OnConnected()
{
//Here check the list if this is going to be our first client and if so call your method, next time because our list is filled your method won't be invoked.
if(UserHandler.UserList.Count==0)
UpdateClient(true);
UserHandler.UserList.Add(Context.ConnectionId)
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
UserHandler.UserList.RemoveAll(u => u.ConnectionId == Context.ConnectionId);
return base.OnDisconnected(stopCalled);
}
}
I didn't take your business logic or your needings into consideration, this is just a general idea. For example, you might want to add some logic for when all the clients goes off, you should stop the loop inside UpdateClient with using updating flag.

Unsubscribing from subscriptions in socket.io

Let's say I have angular page with a table with 10 rows, that each row has an id, and redirects me to a page with unique id.
I want to subscribe to this id in socket.io, and when I will leave this page / change to another, I want to replace the previous subscription to the new one.
How can I do that? Do I have to save all my current subscriptions in a database?
(I work with sails.js framework on node, and angular on client).
When you disconnect a socket (i.e. change page to another), socket.io unsuscribe your socket automatically.
To subscribe to a room, you could simply send a message from angular when you connect with the id and then in your sails controller you subscribe the user to the id
EDIT
Like this page says, you need to implement your system to track sockets.
First, create a service in /api/services/ and put that :
module.exports = {
switchRoom: function (socket, to_id) {
var tmp = _.find(sails.mysubscribers, function (sub) {
return sub.socket === socket;
});
if (tmp) { // If user came from another page unsuscribe him
sails.sockets.leave(socket, 'Unique prefix' + tmp.roomId);
} else {
tmp = {
socket: socket
};
sails.mysubscribers.push(tmp);
}
if (!to_id) { // If the user leave remove him from the array
var index = sails.mysubscribers.indexOf(tmp);
if (index > -1) sails.mysubscribers.splice(index, 1);
} else {
tmp.roomId = to_id;
sails.sockets.join(socket, 'Unique prefix' + to_id);
}
}
};
Then, in /config/sockets.js edit the afterDisconnect function :
module.exports.sockets = {
afterDisconnect: function (session, socket, cb) {
YourService.switchRoom(socket);
return cb();
}
};
Now, when you need to switchRoom, you just have to call YourService.switchRoom(req.socket, roomId) where roomId is the id send by angular...
That's it !

Oauth 2 popup with Angular 2

I'm upgrading/rewriting an existing angular app to use angular2. My problem is that I want to open a OAuth flow in a new pop up window and once the OAuth flow is completed use window.postMessage to communicate back to the angular 2 app that the OAuth flow was successful.
Currently what I have is in the angular 2 service is
export class ApiService {
constructor(private _loggedInService: LoggedInService) {
window.addEventListener('message', this.onPostMessage, false);
}
startOAuthFlow() {
var options = 'left=100,top=10,width=400,height=500';
window.open('http://site/connect-auth', , options);
}
onPostMessage(event) {
if(event.data.status === "200") {
// Use an EventEmitter to notify the other components that user logged in
this._loggedInService.Stream.emit(null);
}
}
}
This template that is loaded at the end of the OAuth flow
<html>
<head>
<title>OAuth callback</title>
<script>
var POST_ORIGIN_URI = 'localhost:8000';
var message = {"status": "200", "jwt":"2"};
window.opener.postMessage(message, POST_ORIGIN_URI);
window.close();
</script>
</head>
</html>
Using window.addEventListener like this seems to completely break the angular 2 app, dereferencing this.
So my question is can I use window.addEventListener or should I not use postMessage to communicate back to the angular2 app?
** Complete angular2 noob so any help is appreciated
I have a complete Angular2 OAuth2 skeleton application on Github that you can refer to.
It makes use of an Auth service for OAuth2 Implicit grants that in turn uses a Window service to create the popup window. It then monitors that window for the access token on the URL.
You can access the demo OAuth2 Angular code (with Webpack) here.
Here is the login routine from the Auth service, which will give you an idea of what's going on without having to look at the entire project. I've added a few extra comments in there for you.
public doLogin() {
var loopCount = this.loopCount;
this.windowHandle = this.windows.createWindow(this.oAuthTokenUrl, 'OAuth2 Login');
this.intervalId = setInterval(() => {
if (loopCount-- < 0) { // if we get below 0, it's a timeout and we close the window
clearInterval(this.intervalId);
this.emitAuthStatus(false);
this.windowHandle.close();
} else { // otherwise we check the URL of the window
var href:string;
try {
href = this.windowHandle.location.href;
} catch (e) {
//console.log('Error:', e);
}
if (href != null) { // if the URL is not null
var re = /access_token=(.*)/;
var found = href.match(re);
if (found) { // and if the URL has an access token then process the URL for access token and expiration time
console.log("Callback URL:", href);
clearInterval(this.intervalId);
var parsed = this.parse(href.substr(this.oAuthCallbackUrl.length + 1));
var expiresSeconds = Number(parsed.expires_in) || 1800;
this.token = parsed.access_token;
if (this.token) {
this.authenticated = true;
}
this.startExpiresTimer(expiresSeconds);
this.expires = new Date();
this.expires = this.expires.setSeconds(this.expires.getSeconds() + expiresSeconds);
this.windowHandle.close();
this.emitAuthStatus(true);
this.fetchUserInfo();
}
}
}
}, this.intervalLength);
}
Feel free to ask if you have any questions or problems getting the app up and running.
So with a bit of investigation found out the problem. I was de-referencing this. This github wiki helped me understand it a bit more.
To solve it for my case needed to do a couple of things. Firstly I created a service that encapsulated the adding of an eventListener
import {BrowserDomAdapter} from 'angular2/platform/browser';
export class PostMessageService {
dom = new BrowserDomAdapter();
addPostMessageListener(fn: EventListener): void {
this.dom.getGlobalEventTarget('window').addEventListener('message', fn,false)
}
}
Then using this addPostMessageListener I can attach a function in my other service to fire
constructor(public _postMessageService: PostMessageService,
public _router: Router) {
// Set up a Post Message Listener
this._postMessageService.addPostMessageListener((event) =>
this.onPostMessage(event)); // This is the important as it means I keep the reference to this
}
Then it works how I expected keeping the reference to this
I think this is the Angular2 way:
(Dart code but TS should be quite similar)
#Injectable()
class SomeService {
DomAdapter dom;
SomeService(this.dom) {
dom.getGlobalEventTarget('window').addEventListener("message", fn, false);
}
}
I fiddled around with this for ages but in the end, the most robust way for me was to redirect the user to the oath page
window.location.href = '/auth/logintwitter';
do the oath dance in the backend (I used express) and then redirect back to a receiving front end page...
res.redirect(`/#/account/twitterReturn?userName=${userName}&token=${token}`);
There are some idiosyncracies to my solution because e.g. I wanted to use only JsonWebToken on the client regardless of login type, but if you are interested, whole solution is here.
https://github.com/JavascriptMick/learntree.org

CometD taking more time in pushing messages

I am trying to implement CometD in our application. But it is taking more time compared to the existing implementation in our project. The existing system is taking time in milliseconds where as CometD is taking 2 seconds to push the message.
I am not sure where I am going wrong. Any guidance will help me lot.
My code:
Java script at client side
(function($)
{
var cometd = $.cometd;
$(document).ready(function()
{
function _connectionEstablished()
{
$('#body').append('<div>CometD Connection Established</div>');
}
function _connectionBroken()
{
$('#body').append('<div>CometD Connection Broken</div>');
}
function _connectionClosed()
{
$('#body').append('<div>CometD Connection Closed</div>');
}
// Function that manages the connection status with the Bayeux server
var _connected = false;
function _metaConnect(message)
{
if (cometd.isDisconnected())
{
_connected = false;
_connectionClosed();
return;
}
var wasConnected = _connected;
_connected = message.successful === true;
if (!wasConnected && _connected)
{
_connectionEstablished();
}
else if (wasConnected && !_connected)
{
_connectionBroken();
}
}
// Function invoked when first contacting the server and
// when the server has lost the state of this client
function _metaHandshake(handshake)
{
if (handshake.successful === true)
{
cometd.batch(function()
{
cometd.subscribe('/java/test', function(message)
{
$('#body').append('<div>Server Says: ' + message.data.eventID + ':'+ message.data.updatedDate + '</div>');
});
});
}
}
// Disconnect when the page unloads
$(window).unload(function()
{
cometd.disconnect(true);
});
var cometURL = "http://localhost:8080/cometd2/cometd";
cometd.configure({
url: cometURL,
logLevel: 'debug'
});
cometd.addListener('/meta/handshake', _metaHandshake);
cometd.addListener('/meta/connect', _metaConnect);
cometd.handshake();
});
})(jQuery);
Comet service class
#Listener("/service/java/*")
public void processMsgFromJava(ServerSession remote, ServerMessage.Mutable message)
{
Map<String, Object> input = message.getDataAsMap();
String eventId = (String)input.get("eventID");
//setting msg id
String channelName = "/java/test";
// Initialize the channel, making it persistent and lazy
bayeux.createIfAbsent(channelName, new ConfigurableServerChannel.Initializer()
{
public void configureChannel(ConfigurableServerChannel channel)
{
channel.setPersistent(true);
channel.setLazy(true);
}
});
// Publish to all subscribers
ServerChannel channel = bayeux.getChannel(channelName);
channel.publish(serverSession, input, null);
}
Is there any thing I need to change in server side code.
You have made your channel lazy, so a delay in message broadcasting is expected (that is what lazy channels are all about).
Please have a look at the documentation for lazy channels.
If you want immediate broadcasting don't set the channel as lazy.

Categories

Resources