Session Management: Alert Inactive Users
Matthieu Duchemin, Engineer, Potix Corporation
January, 2022
ZK 9.6.0.1
Introduction
Application servers maintain a session to keep a client's state. When the server receives the first request from a client, the server creates a session and gives the session a unique identifier. The client should send a request with the session identifier. The server can then determine which session the request belongs to.
In a Java EE environment, an application server creates a javax.servlet.http.HttpSession object to tracking client's session. ZK's org.zkoss.zk.ui.Session is a wrapper of HttpSession, you can use it to store users' data when you handle events.
This session will expire if the user has been inactive for longer than the configured session timeout. As a developer, it is important to keep a balance between user convenience (the longer a session can be inactive without timing out) and server performance (how many sessions are stored in memory at any given time).
With a longer timeout, the server will consume more resources, since it will keep track of more session objects and their associated data. A shorter timeout will free up more resources but could be inconvenient for the end-user. Nobody wants to restart a form after getting a coffee, or after sorting through papers to find a requested identification code.
In order to make the session expiration more forgiving for the end-user, we can implement a session expiration warning into our ZK application, to let them know that they have been inactive for a while and that they are liable to lose their current progress unless they perform an action to refresh their session.
Note: A ZK application uses Web Sessions provided by the Java / Jakarta servlet container in which it is hosted. For the rest of this Smalltalk, we will use “Java” as shorthand for “Java or Jakarta”, as they are simply two implementations of the same specifications.
Abstract concept
The requirements for this timeout workflow is as follow:
No “home call” ping to check if the session still exists.
A ping to the server will extend the session as if the user had made an action, which we want to avoid. The session expiration policy exists for good reasons, and we don’t need to extend the session timeout indefinitely. If this was our goal, we would use the session expiration timer configuration in zk.xml.
A user may have multiple pages open at once in the same session. We want to avoid generating traffic if possible.
User action in a browser tab should refresh the warning message timer for all pages in the same session.
A ZK session encompass all tabs sharing the same session identifier (usually all tabs opened onto the same domain, in the same browser instance). If a user has 3 tabs open but is currently only active in one of them, we should still refresh the timer for the other tabs, as their timeout is also extended when the user performs an action in the active tab.
On a fixed amount of time before session timeout, we want to display a message to the user to let them know that their session is about to expire. The message should contain a button to “click to refresh” their current session if they want to reset their timeout without sending server update while working on the current page.
We assume that the user may not be able to make an update resulting in a request to the server (which would refresh their session), if they are in the middle of a long process. Therefore, providing an opportunity to send a request without causing an update to their current work state will let them refresh without side effects.
Implementation
Now that we have defined the outlines of this workflow, let’s get to the actual implementation.
Client-side state sharing
Modern browsers provide the local storage API, which can be used to store data across pages according to the current document origin. It is similar to session storage, with less expiration and access constraints.
We can use this client-side storage shared by pages in the same session to store the session expiration timestamp. Since client-side processing is cheap, especially for simple operations, we can check the store value regularly, and warn the user once necessary.
Server-side configuration and initialization
Since we are working inside of a ZK application, we will work under the assumption that the session timeout is configured by ZK.
Note: If your Servlet container has its own session expiration policy and if this timeout is shorter than the ZK session timeout, the entire web session will be expired by the container before ZK timeout. If you want to use this session warning, you will need to make sure that both your application and server settings match your intended purpose.
In order to initialize this timeout message automatically in every ZK page loaded in this application, we will use an Initiator, implementing both the Initiator and InitiatorExt interfaces.
This will allow us to declare a doAfterCompose method to be executed on every ZK page generated before they are sent to the client. The doAfterCompose timing is important for us since the page has already generated its content at this stage. This will allow us to modify the page (by adding or removing components from the component tree) and to execute other client commands, such as queuing up a JavaScript execution.
public class TimeoutWarningInitiator implements Initiator, InitiatorExt {
@Override
public void doAfterCompose(Page page, Component[] comps) throws Exception {
...
Sample available in github.
Warning design
The warning itself is a ZK window, which can be toggled visible or non-visible with a simple JavaScript command. We add it to the page during initialization (at server-side), but all operations showing or hiding the window will be performed at client-side by the JavaScript handler.
Since this is a ZK window, its content can be designed directly as a zul file, allowing for any customization, or even complex workflows if appropriate.
In this case, we will only warn the user and provide an option to refresh the timeout on their session.
<window xmlns:n="native" mode="overlapped" closable="false" border="normal" id="sessionsExpirationWarning" visible="false"
position="center,center"
title="Session expiration warning">
<vlayout>
<n:i class="z-icon-warning" style="color:#ff6700; width: 100%; font-size: 60px; height:60px; margin:5px; text-align: center;"></n:i>
<label sclass="warningContent"
value='${c:cat3("Your session is going to expire in ", TimeoutWarningInitiator.WARNING_BEFORE_TIMEOUT_IN_SECONDS, " seconds.")}' />
<label sclass="warningContent"
value="If you want to refresh your session's timeout, click this button:" />
<button label="Click here to keep session alive" onClick="" />
</vlayout>
</window>
Sample available in github.
Refreshing the timeout
The timeout of the ZK session is reset every time a client-side request is fired to a desktop (a ZK page) located in the same session. In order to trigger a reset, we only need to declare a server-side listener, in this case, onClick, on the target button. The listener itself doesn’t need to do anything, so we can make it an empty onClick=””. Since the listener is declared, the click event will be sent to the server, regardless of being empty.
Note: the event still needs to be declared in order to have an effect. If there is no onClick event on the button, clicking it will not trigger a request, and will not refresh the session.
<button label="Click here to keep session alive" onClick="" />
Sample available in github.
Client-side handling
The client-side workflow has 4 main elements:
Initialization
The init function is called by the server-side initiator with the Client.evalJavaScript() method. This function will create the warning check interval and check every second if the current time is greater than the declared timeout warning time.
intervalId = window.setInterval(check, 1000);
Sample available in github.
Interval check
The interval check will pull the timeout warning time from the local storage. This time may have been set by the current page, or by any other tab displaying the same website through the same browser session.
Since a user action in any of the pages located in the same browse session will also extend the session, this shared storage allows indirect communication between these tabs.
If a tab stores a new value, it will replace the previously entered value into local storage. Under normal conditions, we will assume that a value will always have a later timeout warning than a previous value.
var timeoutWarningTime = window.localStorage.getItem('timeout-warning-time');
if(timeoutWarningTime < Date.now()) {
...
Sample available in github.
Reset timeout
If we passed true as the auto-extend parameter, we want the timeout to be reset every time the user sends a request to the server. We do this by hooking into the ZK engine’s existing _resetTimeout client-side function: zAu._resetTimeout
This function is called by the ZK engine when a request is sent, and the current session timeout is extended, so it’s the perfect place to also reset our local timer.
var reset = function () {
var timeoutWarningTime = Date.now() + warningDelay;
window.localStorage.setItem('timeout-warning-time', timeoutWarningTime);
console.log("reset timeout warning time to:", timeoutWarningTime);
hide();
};
Sample available in github.
Show and hide warning
Since we created a ZK component as a zul file to act as the warning message, we can perform default ZK operations on this component at client side.
We use a simple selector to retrieve the Widget (the client-side object matching the component).
We have assigned a component ID to the window in our zul file, so we can select by the ZK ID directly, with the syntax zk.$(“$id”)
zk.$("$sessionsExpirationWarning")
Sample available in github.
Once we have retrieved the widget, we can simply call setVisible(true) or setVisible(false) to either display or hide this widget.
var show = function() {
zk.$("$sessionsExpirationWarning").setVisible(true);
};
var hide = function() {
zk.$("$sessionsExpirationWarning").setVisible(false);
};
Sample available in github.
Conclusion
With a little bit of custom JavaScript code and a few out-of-the-box ZK components, we have created an automatic warning message which will improve the end-user experience and prevent frustration if our users are performing long entries or have to leave their session unattended while using a ZK application.
You can find the runnable sample project on github. The project contains run instructions in the included Readme file.