B4J Question [ABMaterial][WebSockets]How can I access all websockets that share the same session?

mindful

Active Member
Licensed User
Hi all,

I am building a web application with B4J Server (non-ui) and ABMaterial - so it's a websockets web app and I am wondering how you handle user logins and logouts in your web app.

So let's take the following scenario:
1. user signed in -> save the username in the session -> redirect the user to the initial page of the app
2. the user duplicates the tab in browser so now he has two tabs logged in the app with the same session, but different websockets
3. the user clicks the sign out button from tab1 -> remove the username from session -> redirect to the login page of app
4. what about the tab2 ? it doesn't get redirected to the login page and still I can click buttons in the page ... i have to refresh the page manually so tab2 gets redirected to the login page.

How can i auto redirect the other tabs that the user may have created when the user click the sign out button in one tab ?!
 

mindful

Active Member
Licensed User
I tried instead to removing attribute from the session when the user clicks the sign out button to Invalidate the session, but still i can't get tab2 to go to the login page ... :(

P.S. by redirect i mean using websocket eval method:
B4X:
ws.Eval("window.location = arguments[0]", ArrayAs Object(TargetUrl))
 
Last edited:
Upvote 0

mindful

Active Member
Licensed User
It is possible to maintain a collection that maps between the user session and a list of WebSockets.

Another solution is to store the user state in the session and check it as the first step of any user event. I think that this is a more common approach.

I am going with the first solution "maintain a collection that maps between the user session and a list of WebSockets" but I bumped into a problem:
1. I create a public map (ActiveUsers) in my code module, I initialize it in AppStart.
2. When a users provides a successful login I put in the ActiveUsers map its userID as key and as value I put a list that contains the websocket from the login page (UserMarkAsConnected).
3. In every page (websocket class) in the WebSocket_Connected event I put UserMarkAsConnected and in the WebSocket_Disconnected i put UserMarkAsDisconnected
B4X:
Public Sub UserMarkAsConnected(UserID As Int, ws As WebSocket)
    If ActiveUsers.ContainsKey(UserID) Then
        'if this user is already in the ActiveUsers Map it means that this websocket is from another browser tab because it has the same session
        Dim wsList As List = ActiveUsers.Get(UserID)
        wsList.Add(ws)
    Else
        Dim wsList As List
        wsList.Initialize2(Array As Object(ws))
        ActiveUsers.Put(UserID, wsList)
    End If
    Log($"User ${UserID} connected"$)
End Sub

Public Sub UserMarkAsDisconnected(UserID As Int, ws As WebSocket)
    If Not(ActiveUsers.ContainsKey(UserID)) Then Return
    Dim wsList As List = ActiveUsers.Get(UserID)
    If wsList.Size > 1 Then
        wsList.RemoveAt(wsList.IndexOf(ws))
        ActiveUsers.Put(UserID, wsList)
    Else 
        ActiveUsers.Remove(UserID)
    End If
    Log($"User ${UserID} disconnected"$)
End Sub

4. An error occures when I open anoter tab in the browser on this line : wsList.Add(ws) from UserMarkAsConnected

B4X:
Error occurred on line: 182 (App)
java.lang.UnsupportedOperationException
    at java.util.AbstractList.add(AbstractList.java:148)
    at java.util.AbstractList.add(AbstractList.java:108)
    at anywheresoftware.b4a.objects.collections.List.Add(List.java:71)
    at b4j.example.app._usermarkasconnected(app.java:1001)
    at b4j.example.useraccounts._websocket_connected(useraccounts.java:370)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at anywheresoftware.b4a.shell.Shell.runMethod(Shell.java:612)
    at anywheresoftware.b4a.shell.Shell.raiseEventImpl(Shell.java:229)
    at anywheresoftware.b4a.shell.Shell.raiseEvent(Shell.java:159)
    at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:93)
    at anywheresoftware.b4a.ShellBA.raiseEvent2(ShellBA.java:90)
    at anywheresoftware.b4a.BA.raiseEvent(BA.java:84)
    at anywheresoftware.b4j.object.WebSocketModule$Adapter$ThreadHandler.run(WebSocketModule.java:192)
    at anywheresoftware.b4a.keywords.SimpleMessageLoop.runMessageLoop(SimpleMessageLoop.java:30)
    at anywheresoftware.b4a.StandardBA.startMessageLoop(StandardBA.java:26)
    at anywheresoftware.b4a.ShellBA.startMessageLoop(ShellBA.java:111)
    at anywheresoftware.b4a.keywords.Common.StartMessageLoop(Common.java:131)
    at anywheresoftware.b4a.shell.Shell.raiseEventImpl(Shell.java:301)
    at anywheresoftware.b4a.shell.Shell.raiseEvent(Shell.java:159)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:93)
    at anywheresoftware.b4a.ShellBA.raiseEvent2(ShellBA.java:90)
    at anywheresoftware.b4a.BA.raiseEvent(BA.java:84)
    at b4j.example.main.main(main.java:29)

Basically it doesn't let me add another websocket in the list that is stored as a value in ActiveUsers map.
 
Upvote 0

mindful

Active Member
Licensed User
My bad :(

corect code in UserMarkAsConnected:
B4X:
Else
  Dim wsList As List
  wsList.Initialize
  wsList.Add(ws)
  ActiveUsers.Put(UserID, wsList)
EndIf

Didn't know that if I use Initialize2 on a List and pass an Array the List will be of a fixed size.

Thank you Erel
 
Upvote 0

mindful

Active Member
Licensed User
Just to be sure all websocket classes that raised the WebSocket_Connected will eventually raise the WebSocket_Disconnected at some point. This is the natural behavior no?

My project requires two rules:
1. the app runs in only one tab of the browser
2. the users can login only from one device/browser

So for the first rule I created a Map that holds each SessionId as key and a counter as value so in all websocket classes (pages) in WebSocket_Connected I verify if the SessionId is already in the map, if so I show a message to the user that the app runs only in one tab of the browser, if not i put the SessionId in the map with the counter set to 1 and let the user continue. Also In WebSocket_Disconnected I get the counter from the map and subtract 1 from the counter. If it reaches to 0 I remove the session id from the map.

For the second rule I created another Map that holds each authenticated UserId as key so in Login method I verify if the users already exists in the map, if so I show a message to the user that he is logged in already in another windows, if not i add the user id to the map and let it continue. Also in all websocket classes (pages) on WebSocked_Connected i add the user id to the map and in WebSocket_Disconnected I remove the user id from the map.

So you see why I need all websocket class that raised Connected eventually at some point (browser exit, device restart, loss of internet connection) to raise the Disconnected event.

If this is the right way to do it do i need to initialize the map as Thread Safe Maps ?

Or maybe is there another way to achieve what i need !?
 
Last edited:
Upvote 0

mindful

Active Member
Licensed User
For the first "problem" you are right ;) I don't need a map.

But for the second one I need a map if I want to keep something else paired with that user (eg. time of login) or at least a list because I need to have only one login from each user. So if the user tries to access from the same browser it will get caught by the first rule - only one tab window, because it has the same session, but if tries to access from another browser or another device it will have a different session so it doesn't have the session attribute set ...

Regarding the heart beat timer ... I will create a private timer in each websocket class which I enable in Connected event and disable it on Disconnected event, but on what interval should I set it so it doesn't affect performance ? From my forum search I see that the disconnected event will take longer to trigger if device is shutdown or cut of from the internet.
I saw that the ws.EvalWithResult and ws.RunFunctionWithResult have a timeout of 5 seconds. So if I set my timer interval to 10 seconds will it affect the performance of the server ?
 
Upvote 0

mindful

Active Member
Licensed User
I just realised that when ws.EvalWithResult throws timeout error (client's browser is not reachable) it also raises the WebSocket_Disconnected event! Nice :D

I guess the same thing is with ws.RunFunctionWithResult
 
Upvote 0
Top