Ask Ben: Extending A ColdFusion Session On A Long-Lived Page
Hi Ben, I am trying to learn ColdFusion and have a question. I hope you can help me. I have an application where a user logs into a portal. Session variables are set to time out in 30 mins. But, is it possible to extend the session if the user keeps using the page. For the application, even if the user is moving the page up and down and making some interaction with the page, the session still expires. Thank you so much in advance.!
First of all, welcome to the ColdFusion community. I believe that you will find much joy in the CFML language. I've been coding ColdFusion for years; and, I love it a little more every day!
Most ColdFusion applications generate a good amount of network activity that naturally serves to keep the user's session (and application) alive. However, it's not entirely uncommon for a ColdFusion application to provide an experience that holds the user's attention for an extended period of time without triggering any network calls. Consider reading a long document; or, reviewing a large InVision prototype; or, trying to solve a puzzle. In such cases, the user's session may timeout in the background while the user is still consuming the already-rendered content. The most common solution to this problem is to create a "heartbeat" that acts to extend the ColdFusion session using AJAX requests.
A "heartbeat" is very much what it sounds like: a rhythmic event that lets the server know that the user's session is still alive. Very much like a human heartbeat. Only, instead of cardiac muscles, we're using AJAX requests.
The idea is actually quite straightforward once you've seen it: on the long-lived page, we're going to setup some event-handlers that listen for user interactions. Things like mouse-clicks, key-presses, and page-scrolls. These events serve to let us know that the user is actively engaged with the page (ie, they haven't just walked away from their computer). And, in response to these events, we're going to ping the server.
Of course, we don't want to ping the server after every event. This would create an unnecessary amount of network activity and potentially put the server under problematic load. Instead, we want to strike a nice balance between extending the user's session and not performing too much processing.
There's no one-size-fits-all technique; so, what I'm going to show you is just one possible approach to configuring a heartbeat. But first, let's create a demo ColdFusion application:
component
output = false
hint = "I define the application settings and event handlers."
{
// Configure application management.
this.name = "SessionHeartBeatDemo";
this.applicationTimeout = createTimeSpan( 1, 0, 0, 0 );
// Configure session management.
this.sessionManagement = true;
this.sessionTimeout = createTimeSpan( 0, 0, 30, 0 );
this.setClientCookies = true;
// ---
// PUBLIC METHODS.
// ---
/**
* I get called once at the start of each session to initialize the session.
*/
public void function onSessionStart() {
session.id = createUuid();
}
}
As you can see, our session timeout is set to 30-minutes. As stated before, every network request to this ColdFusion application will naturally extend this 30-minute rolling window. However, if we have a page that might hold a user's attention for longer than 30-minutes, we're going to have to actively work to keep that session alive by calling a heartbeat end-point. The heartbeat end-point doesn't really have to do anything - it just has to wire-into the ColdFusion application:
<cfscript>
// This pages doesn't actually have to do anything - just making the AJAX request to
// this page should extend the user's current session timeout (assuming that the
// session is still active).
// Logging for the demo.
writeDump(
var = "Session heartbeat for: #session.id#",
output = "console"
);
cfcontent(
type = "application/json; charset=utf-8",
variable = charsetDecode( serializeJson({ "sessionID": session.id }), "utf-8" )
);
</cfscript>
The most simple approach to a heartbeat implementation is to bind all of your event-handlers and then to just throttle the network requests to your ColdFusion server. Such an approach works; but, by keeping your events bound at all times, it means that the browser has to do a lot of work that gets thrown away.
Instead, I like to use a timer that defers the event-bindings until some time in the future. Then, if the user triggers one of the relevant events, I ping the heartbeat, teardown the event-bindings, and restart the timer. This way, we can actually listen to a larger number of event-types while keeping the processing overhead extremely small (basically nothing).
The fine-tuning of this approach relates to how long the timer delay should be. For a 30-minute ColdFusion session timeout, I might set the timer for 20-minutes, giving us a 10-minute "capture window" for user interactions. Of course, you could set this delay to be smaller if you want to be more aggressive in how often you extend the session.
Here's my implementation of a heartbeat that extends the ColdFusion session:
<cfoutput>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>
Ask Ben: Extending A ColdFusion Session On A Long-Lived Page
</title>
</head>
<body style="min-height: 300vh ;">
<h1>
Welcome to Our Portal
</h1>
<p>
Please enjoy all of the wondrous things we have to offer here. Put your feet
up, make yourself comfortable. And, maybe drop us a long note that will take
a really long time to type:
</p>
<textarea placeholder="Leave us a message..." cols="50" rows="5"></textarea>
<script type="text/javascript">
// The events that will be used to drive the session heartbeat. These are the
// user-interaction events that indicate that the user is still here.
var heartbeatEvents = [ "scroll", "touchstart", "mousemove", "mousedown", "keydown" ];
// How long should the page wait until it starts monitoring user-interactions.
// This delay acts both as a throttle to the network requests; but, also as a
// way for us to reduce the event-handler activity on the page as a whole.
// --
// NOTE: For the demo, I'm keeping this rather short. In production, you might
// want to set it to something like half your session timeout.
var heartbeatDelayInMilliseconds = 10000;
startHeartbeatTimer();
// ----------------------------------------------------------------------- //
// ----------------------------------------------------------------------- //
// Our heartbeat is going to be triggered by user-interaction events. However,
// there's no need to constantly be listening for events - we just want to
// start listening at some point in the future, before the session times-out,
// but with enough time for the user to interact with the page.
function startHeartbeatTimer() {
setTimeout( setupHeartbeatEvents, heartbeatDelayInMilliseconds );
}
// I bind the heartbeat events to the page such that the next meaningful user-
// interaction triggers a single ping to the server in order to extend the
// life of the user's session.
function setupHeartbeatEvents() {
console.info( "Setting up heartbeat event-bindings." );
// When binding events, we can tell the browser that our binding will
// be "passive". This means that we'll never invoke the preventDefault()
// method on the event object. This allows the browser to enable some
// performance enhancements that will reduce jank. This is especially true
// for "scroll" events.
var options = {
passive: true,
once: true
};
for ( var eventType of heartbeatEvents ) {
window.addEventListener( eventType, handleHeartbeatTriggerEvent, options );
}
}
// I unbind the heartbeat events from the page. We only need them in place
// when we start to listen for interaction events - there's no need to have
// them in place all the time.
function teardownHeartbeatEvents() {
for ( var eventType of heartbeatEvents ) {
window.removeEventListener( eventType, handleHeartbeatTriggerEvent );
}
}
// I handle one of the user-interaction events relating to our heartbeat.
function handleHeartbeatTriggerEvent( event ) {
console.warn( "User-interaction detected [%s], triggering heartbeat.", event.type );
// Now that we're about to ping the heartbeat end-point, let's reset the
// event-bindings and setup the timer that adds event-handlers back to the
// DOM in the future when we need to trigger another heartbeat.
teardownHeartbeatEvents();
startHeartbeatTimer();
// Hit the server. This should extend the user's current session.
fetch( "./heartbeat.cfm" ).then(
() => {
console.info( "Heartbeat pinged successfully." );
}
// How you handle errors is going to depend on what kind of AJAX
// client you are using. For example, a retry option might be built
// into the client. Or, you might have to implement retry yourself.
// Or you could just ignore errors and set the timer-delay to be
// small, thereby allowing errors to be "absorbed" naturally.
);
}
</script>
</body>
</html>
</cfoutput>
For the sake of the demo, I'm setting the timer delay to be super short: 10-seconds. This way, we can actually see the control-flow cycling through the binding and unbinding of event-handlers. Every time the event-handler fires, it immediately unbinds all of the other event-handlers. This way, we only trigger one network request per window, regardless of what the user is doing to interact with the page.
Now, if we load this ColdFusion page and start typing a long message (imagine that it will take longer than the session timeout), here's what we'll see in our console:
As you can see, the heartbeat event-handlers are only wired-up every 10-seconds. As such, the overwhelming majority of my keystrokes have no side-effect (other than data entry). It's only the very first keystroke after the event-binding is applied that gets translated into an AJAX request. This should keep the ColdFusion session alive without negatively impacting the processing of the client-side application.
The one part of this that is going to be very context-specific is error handling in the heartbeat network request. If the network request fails, waiting another 20-minutes to trigger another heartbeat could lead to a session timeout. As such, in the event of failure, you may want to either retry the network request with some sort of back-off; or, reset the timer with a smaller delay. I didn't want to add that logic to the demo because it heavily depends on what tools you already have at your disposal (and how longer timer delay is to begin with).
Like I said before, there's no single solution to this problem. Or, I should say, all the solutions that I've seen for session timeout are heartbeat-based; but, each implementation of the heartbeat is different. Even if you don't like my approach, hopefully I've provided some inspiration for your own approach to handling ColdFusion session timeouts on long-lived client-side pages.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →