B4J Question [ABMaterial] Session variables in safari

PCastagnetti

Member
Licensed User
Longtime User
Last edited:

PCastagnetti

Member
Licensed User
Longtime User
I confirm that with Chrome on the iPad the problem is the same

Thanks for the link, but the thing i need is that the Filter event in ABMSessionCreator is fired(only necessary if the user has visited the site and then he turned off the tablet).

The Refresh that I placed in the code (# 20) was added to generate that event.


Thank you
 
Last edited:
Upvote 0

alwaysbusy

Expert
Licensed User
Longtime User
I personally don't think it is a good idea to force this refresh as in #20, as it could cause upredictable page_refresh events and things like that. I can not guarantee ABMaterial wil behave well if you do this.

To find the OS, you could use ABMPlatform:

usage: in WebSocket_Connected, AFTER the page.SetWebsocket(ws), put:

Dim info As ABMPlatform = page.GetPlatform

The result is something like this:

Manufacturer:
Layout :
Name : Chrome
Version: 47.0.2526.106
OSArchitecture: 64
OSFamily: Windows
OSVersion: 10
Prerelease:
Product:
Description: Chrome 47.0.2526.106 32-bit on Windows 10 64-bit
User Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36

If you want to check the same in ABMSessionCreated filter, you can do this but then you have to parse the string yourself:

Log(req.GetHeader("User-Agent"))

Result something like:

Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36
 
Upvote 0

PCastagnetti

Member
Licensed User
Longtime User
I tried to use
B4X:
Dim info As ABMPlatform = Page.GetPlatform
to limit the generation of the event only to ios, but I always get this error:
java.util.concurrent.TimeoutException
at anywheresoftware.b4j.object.WebSocket$SimpleFuture.getValue(WebSocket.java:63)
at com.ab.abmaterial.ABMPage.GetPlatform(Unknown Source)
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:497)
at anywheresoftware.b4a.shell.Shell.runMethod(Shell.java:612)
at anywheresoftware.b4a.shell.Shell.raiseEventImpl(Shell.java:226)
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:497)
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:497)
at anywheresoftware.b4a.BA.raiseEvent2(BA.java:93)
at anywheresoftware.b4a.ShellBA.raiseEvent2(ShellBA.java:90)
at anywheresoftware.b4a.BA.raiseEvent(BA.java:84)

Did I do something wrong

Thank you
 
Upvote 0

alwaysbusy

Expert
Licensed User
Longtime User
make sure it is after the page.SetWebsocket(ws) and try adding page.NeedsPlatform = true in BuildPage() after page.BuildGrid(). Could be related to the .bas file analyse problem in 2.00 (because when I tried it the second time after restarting the jar, it worked). Close the browser, restart the jar.
 
Upvote 0

mindful

Active Member
Licensed User
I think your problem is related to : https://www.b4x.com/android/forum/threads/safari-session-variables-in-b4j-v4.61652/#content

As Erel stated:
The workaround is to create the session and send the cookie before the upgrade request.
This is done by adding this filter:
B4X:
srvr.AddFilter("/b4j_ws.js", "SessionCreator", False)

In your case, because you use ABMaterial this is already implemented with ABMSessionCreator class.
B4X:
srvr.AddFilter("/js/b4j_ws.min.js", "ABMSessionCreator", False)

B4X:
'Return True to allow the request to proceed.
Public Sub Filter(req AsServletRequest, resp AsServletResponse) As Boolean
  req.GetSession 'a new session will be created if a session doesn't exist.
  Return True
End Sub

The problem with this as I stated is that the filter event will be fired only if "/js/b4j_ws.min.js" is requested. So in case this file is cached there will be no request for this file and the filter sub will not get fired.
There are 2 solutions to your problem:

1. disable cache of the server, so that file will not be cached:
In ABMApplication at StartServer sub you have a line:
B4X:
    #If RELEASE
        srvr.SetStaticFilesOptions(CreateMap("cacheControl": "max-age=604800,public","gzip":True,"dirAllowed":False, "maxIdleTime":"300000"))
    #Else
        srvr.SetStaticFilesOptions(CreateMap("cacheControl": "max-age=604800,public","gzip":False,"dirAllowed":False, "maxIdleTime":"300000"))
    #End If
you need to make it like this:
B4X:
    #If RELEASE
        srvr.SetStaticFilesOptions(CreateMap("cacheControl": "max-age=0,no-store","gzip":True,"dirAllowed":False)
    #Else
        srvr.SetStaticFilesOptions(CreateMap("cacheControl": "max-age=0,no-store","gzip":False,"dirAllowed":False)
    #End If
P.S. "maxIdleTime":"300000" doesn't do anything, it can't be set like this, so you better remove it
After this be sure to clear your browser cache and try again.

2. add another filter tot the app that will monitor each request ("/*")
in this new filter sub you need to add req.GetSession

*This is not a solution is more like a workaround, I have tested it and it works on my end - at each request the filter sub will get fired and you issue a new session if necessary.
*I think you should wait for the new 2.02, like AB said, maybe the problem is solved ...

Good luck!
 
Upvote 0

mindful

Active Member
Licensed User
I was not aware of this. How does one set it then?
Please see: https://www.b4x.com/android/forum/threads/jetty-setting-maxidletime.64502/#post-408995

Erel answered your question ;)

I do not recommend disabling the cache either as all files have to be pull from the server each time you open the page. The solution 2 will catch all request so if you have a large userbase maybe it will have some effect on the server performance. You could add something like: add a random query string to the script scr of b4j_ws, but I've seen in the feedback app that this has been done already, but the problem will still persist as it will cache that file with that query sting ... So I'm out of ideeas right now :)
 
Upvote 0

mindful

Active Member
Licensed User
Maybe this could work:

Create a Filter class "GetSessionFilter" and in the filter sub add req.GetSession

Now you need to modify the StartServer sub in ABMApplication to add the filter:
srvr.AddFilter("/ws/*", "GetSessionFilter", False)

So it will fire the Filter sub only when a websocket is requested.

This is different from the 2nd solution in above post, because the "/*" will catch any request - css, js, png, html, directory will fire the Filter Sub ... so this solution is better than my previous one I think ;) haven't tested it yet ..
 
Upvote 0

alwaysbusy

Expert
Licensed User
Longtime User
Erel answered your question
Lol. I must have missed this completely! Going to change this in my code.

Yes, nowadays I use AppVersion = DateTime.Now to try to refresh it every time I restart the server. But it won't solve PCastagnettis problem.

Maybe this could work:
So you would add this in addition to the existing srvr.AddFilter("/js/b4j_ws.min.js", "ABMSessionCreator", False)?
 
Upvote 0

mindful

Active Member
Licensed User
So you would add this in addition to the existing srvr.AddFilter("/js/b4j_ws.min.js", "ABMSessionCreator", False)?
I would replace it completly ... beacuse /ws/* is non cacheable and this will fire the Filter each time a websocket is requested ... In theory this will work, but I do not own an Apple device ... I just tested and it will fire for sure :)
 
Upvote 0

alwaysbusy

Expert
Licensed User
Longtime User
@mindful just test it with my new system in 2.02 and this /ws/* solution completely disables the purpose of my new system. One can use it they want (or maybe need).

My system works great if you temporary lost your internet connection. It keeps track of all your variables you used on the page and restores them when the connection is back. It does not 'reload' the page if it reconnects (page_ready will not be raised again so you can safely keep using ConnectPage()). This has been tested by 3 people with different connection problems and it looks like it solves all those problems.

The whole purpose of this system is I do NOT want to pass through ABMSessionCreator if event (*1*) (see further) happened.

Mixed with your solution, it always passes through event (*1*). Now I'm getting confused...

------------------------------------------------------------------------------------------------------------------------------
THE CHANGES THAT NEED TO BE DONE IN YOUR PROJECT for 2.02:
------------------------------------------------------------------------------------------------------------------------------

1. In ABMShared
a. Public AppVersion As String = DateTime.now ' NEW 2.01 this helps to get the latest js/css files when the app is started/restarted
b. The new NavigateToPage()
B4X:
Public Sub NavigateToPage(ws As WebSocket, TargetUrl As String)
  If AppVersion <> "" Then
    TargetUrl = TargetUrl & "?" & AppVersion
  End If
  If ws.Open Then
    ws.session.SetAttribute("ABMNewSession", True)  ' NEW 2.01
    ws.Eval("window.location = arguments[0]", Array As Object(TargetUrl))   
  End If
End Sub
2. In ABMSessionCreator
a. The new Filter()
B4X:
'Return True to allow the request to proceed.
Public Sub Filter(req As ServletRequest, resp As ServletResponse) As Boolean
  DateTime.DateFormat = "dd/MM/yyyy"
  DateTime.TimeFormat = "HH:mm"

  Log("In filter: " & DateTime.Date(DateTime.Now) & " " & DateTime.Time(DateTime.now))
 
  Dim session As HttpSession = req.GetSession 'a new session will be created if a session doesn't exist.
  session.SetAttribute("ABMNewSession", True) ' NEW  2.01
  Return True
End Sub
3. In ABMApplication
a. Make sure your Initialize method does NOT load icons that you do not have/use. This causes 404 error.
b. The new Page_ParseEvent()
B4X:
Sub Page_ParseEvent(Params As Map)
  Dim eventName As String = Params.Get("eventname")
  Dim eventParams() As String = Regex.Split(",",Params.Get("eventparams"))
  If eventName = "beforeunload" Then ' NEW 2.01
    Log("preparing for url refresh")
    ws.session.SetAttribute("ABMNewSession", True)
    Return
  End If
  If SubExists(Me, eventName) Then
    Params.Remove("eventname")
    Params.Remove("eventparams")
    Select Case Params.Size
      Case 0
        CallSub(Me, eventName)
      Case 1
        CallSub2(Me, eventName, Params.Get(eventParams(0)))       
      Case 2
        If Params.get(eventParams(0)) = "abmistable" Then
          Dim PassedTables As List = ABM.ProcessTablesFromTargetName(Params.get(eventParams(1)))
          CallSub2(Me, eventName, PassedTables)
        Else
          CallSub3(Me, eventName, Params.Get(eventParams(0)), Params.Get(eventParams(1)))
        End If
      Case Else
        ' cannot be called diretly, to many param
        CallSub2(Me, eventName, Params)     
    End Select
  End If
End Sub
4. In each of your own pages
a. This is where you have to be focused and see how this fits into your own app! Some explaining:

The page.SaveSessionVariables(Me, session) and page.RestoreSessionVariables(Me, session) are very powerful methods. They essentialy keep a reference to all the variables (except WebSockets, ABMPages, ABMaterial) you declared in Class_Globals and are restored in case of a Websocket interruption (like internet temporary lost).

The flow is:
Event (*1*) happened (your page is loaded from the url, the user pressed F5 (refresh) or pressed the refresh button, or you used the NavigateToPage() function)
1. Filter in ABMSessionCreator is called
2. WebSocket_Connected is called, entering (*1*), saving a reference to all variables + the page
3. Page_Ready() is called, make sure in ConnectPage you initialize all your variables! (e.g. a List, or Map)

Event (*2*) happened (The websocket has been interrupted, e.g. due because the internet was temporary lost and has now reconnected)
1. WebSocket_Connected is called, entering (*2*), loading all the references to the variables + the page. Do not reinitialize your variables!
B4X:
Private Sub WebSocket_Connected (WebSocket1 As WebSocket)
  Log("Connected")
  ws = WebSocket1
  Dim session As HttpSession = ws.UpgradeRequest.GetSession ' NEW 2.01
  If ABMShared.NeedsAuthorization Then
    If session.GetAttribute2("IsAuthorized", "") = "" Then
      ABMShared.NavigateToPage(ws, "../")
      Return
    End If
  End If
  If session.HasAttribute("ABMPage") = False Or session.HasAttribute("ABMNewSession") = True Then ' NEW 2.01 (*1*)
    Log("saving session")
    ' IMPORTANT!
    session.RemoveAttribute("ABMNewSession")
    ' IMPORTANT: Redim to reinit the page object
    Dim page As ABMPage
    ' reload the BuildPage
    BuildPage 
    ' connect our page with the websocket
    page.SetWebSocket(ws)
    ' Prepare the page
    page.Prepare
    ' save at least page
    session.SetAttribute("ABMPage", page)    ' has to be done manually, SaveSessionVariables can't handle a page object.
    ' And all other variables you need for your program
    page.SaveSessionVariables(Me, session)
  Else  ' (*2*)
    ' restore the page
    Log("Loading session")
    ' load at least the page
    page = session.GetAttribute("ABMPage") 
    ' and load all your own variables previously saved
    page.RestoreSessionVariables(Me, session)
    ' connect our page with the websocket
    page.SetWebSocket(ws) 
    ' refresh the page
    page.Refresh
    ' because we use ShowLoaderType=ABM.LOADER_TYPE_MANUAL
    page.FinishedLoading
  End If
End Sub
b. In Websocket_disconnect() do the following:
B4X:
Private Sub WebSocket_Disconnected
   Log("Disconnected")
   page.ws = Null
   ' And all other variables you need for your program
   page.SaveSessionVariables(Me, ws.session)
End Sub
c. The new Page_ParseEvent (same as in ABMApplication)
B4X:
Sub Page_ParseEvent(Params As Map)
  Dim eventName As String = Params.Get("eventname")
  Dim eventParams() As String = Regex.Split(",",Params.Get("eventparams"))
  If eventName = "beforeunload" Then
    Log("preparing for url refresh")
    ws.session.SetAttribute("ABMNewSession", True)
    Return
  End If
  If SubExists(Me, eventName) Then
    Params.Remove("eventname")
    Params.Remove("eventparams")
    Select Case Params.Size
      Case 0
        CallSub(Me, eventName)
      Case 1
        CallSub2(Me, eventName, Params.Get(eventParams(0)))       
      Case 2
        If Params.get(eventParams(0)) = "abmistable" Then
          Dim PassedTables As List = ABM.ProcessTablesFromTargetName(Params.get(eventParams(1)))
          CallSub2(Me, eventName, PassedTables)
        Else
          CallSub3(Me, eventName, Params.Get(eventParams(0)), Params.Get(eventParams(1)))
        End If
      Case Else
        ' cannot be called diretly, to many param
        CallSub2(Me, eventName, Params)     
    End Select
  End If
End Sub
 
Last edited:
Upvote 0

alwaysbusy

Expert
Licensed User
Longtime User
This video basically demonstates what is possible (the bullet is a new property you'll be able to set to show the user if he has a connection to the server or not).


1. When I'm adding a new record + the connection is lost, and when it reconnects everything is still usable the way it was (the modal sheet is still open, the input fields contain the values I entered)

2. When I made a selection to show only 'Alain' + the connection is lost, and when it reconnects the same selection is still active (the refresh you see is because that is how the table works when doing a refresh, something I plan to work on in the future).

Using your system:

in case 1. you would start back with the page showing the table and all the names

in case 2. your selection would've been gone and you would see all the names
 
Upvote 0

mindful

Active Member
Licensed User
So basically the SaveSessionVariables and RestoreSessionVariables saves the websocket class (the page) into session .. so if a user has internet problems the disconnect event will happen it saves the websocket into session and when the internet connection is back online the user will get a new instance of the websocket and you check if the user had such a websocket class before, if so you get that class from session.

But doing this you will keep a reference of that websocket class in session so it will not be disposed. So if the user's internet connection never enstablishes the server will hold a reference of that websocket class untill the session will expire.

The session purpose is for keeping variable data on the server so we will not lose data when changing websockets (pages) so this is how I do it:
I save all my variables in session (like user id - the id of the authenticated user or the result of a method)
If the user has an internet problem long enough so that the WebSocket_Disconnect event will happen then it will lose all private variable from websocket class (page).
The user resolves internet problems and reconnects -> the WebSocket_Connected event will happen and in this sub I will check for whats in session and display the content with the data from the session.

What my solution post #32 does it creates a session therefore a cookie session and send the cookie before the upgrade request. So the browser will have a session cookie. Note - It will not create another cookie if one was already created for that user. What it does it only forces the safari browser to have the session cookie reference so it will not get null values. - At least this is what I think it happens.
Later edit: This is the solution that Erel posted only I change the file which the server listens for because the cacheControl makes the b4j_ws.min cacheable and will not be requested by the browser. So I put in place the virtual files (websockets) that are not cacheable so the server will send the cookie before the UpgradeRequest (101). So basically my solution is the same with erel's but adapted for ABMaterial with cache enabled. If you don't use cache the you can leave it as is (ABMSessionCreator) because the browser will request the b4j_ws file and therefore it will fire the filter sub in ABMSessionCreator.

So with your new reconnecting system I think it will not fix the safari browser problem - just my thought I do not own an Apple device to test :(

Later Edit: Correct me if I understood it wrong ...
 
Last edited:
Upvote 0
Top