Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with:

ColdFusion 10 - Native WebSocket Filtering And Channel Listeners Are Mutually Exclusive

By Ben Nadel on
Tags: ColdFusion

In the ColdFusion 10 documentation regarding WebSockets, both the publish and subscribe functions mention that special data parameters can be used to filter incoming and outgoing messages. In my previous blog post, I looked at using the WebSocket application pseudo-events as a means to execute filtering manually. Today, I want to look at using the native filtering of WebSocket messages through the use of selectors and custom header.

NOTE: At the time of this writing, ColdFusion 10 was in public beta.


 
 
 

 
  
 
 
 

This demo, while quite small (compared to my other demos) took me a long time to figure out. No matter what I did, I simply could not get the filtering to work. Finally, after much head-scratching and help from Sagar Ganatra, I finally had a breakthrough! I finally closed the understanding gap that was preventing my WebSocket filtering from working:

Native WebSocket filtering functionality (ie. the "selector" property) is mutually exclusive with ColdFusion Channel Listener components.

If you are going to use the "selector" property as a means to filter incoming or outgoing messages, you cannot define a Channel Listener in your WebSocket configuration (for the given channel). Conversely, if you do want to use a channel listener, you cannot use the "selector" property. Using the selector property with a channel listener won't raise an exception - it simply will not work.

With this understanding finally in place, I was able to put together a quick example of ColdFusion 10's native WebSocket filtering functionality. In the following application, I've gotten rid of all session management - all filtering relies on URL query string values.

First, let's take a look at the Application.cfc ColdFusion framework component. In the following code, take special notice that I have defined one WebSocket channel - but, I have not defined a Channel Listener for it.

Appication.cfc - Our ColdFusion Application Framework Component

  • <cfscript>
  • // NOTE: CFScript added for Gist color-coding only. Remove.
  •  
  • component
  • output="false"
  • hint="I define the application settings and event handlers."
  • {
  •  
  •  
  • // Define the application settings.
  • this.name = hash( getCurrentTemplatePath() );
  • this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 );
  •  
  • // Turn off session management. In this case, we'll just rely
  • // completely on the values being passed through the WebSocket
  • // custom headers.
  • this.sessionManagement = false;
  •  
  • // Set up the WebSocket channels.
  • //
  • // NOTE: We are NOT defining a Channel Listener - using the
  • // native WebSocket filtering and selector functionality is
  • // mutually exclusive with the Channel Listener (ie. it's doing
  • // exactly what YOU would have had to do in your own Channel
  • // Lister component).
  • this.wsChannels = [
  • {
  • name: "demo"
  • }
  • ];
  •  
  •  
  • // I initialize the application.
  • function onApplicationStart(){
  •  
  • // Define some users with different IDs. For this demo, we're
  • // gonna look at Pushing messages to specific clients.
  • application.users = [
  • {
  • id: 1,
  • name: "Joanna"
  • },
  • {
  • id: 2,
  • name: "Sarah"
  • },
  • {
  • id: 3,
  • name: "Tricia"
  • }
  • ];
  •  
  • // Return true to the application can load.
  • return( true );
  •  
  • }
  •  
  •  
  • }
  •  
  • // NOTE: CFScript added for Gist color-coding only. Remove.
  • </cfscript>

Without a channel listener in place, I can take advantage of the native filtering functionality using the "selector" data property. Let's look a the server-side code that pushes messages to clients over the WebSocket connection. You'll see that I am using the "userID" value as the filtering key:

send.cfm - Our Server-Side WebSocket Push / Publish Code

  • <!--- Param our User ID variable. --->
  • <cfparam name="url.id" type="numeric" default="0" />
  •  
  • <!--- Check to see if a user ID has been selected. --->
  • <cfif url.id>
  •  
  • <!---
  • Loop over the application users to find one with the same ID
  • so we can send a message to that user.
  • --->
  • <cfloop
  • index="pushUser"
  • array="#application.users#">
  •  
  • <!---
  • Check to see if this user record is the one we're going
  • to be sending a message to.
  • --->
  • <cfif (pushUser.id eq url.id)>
  •  
  • <!---
  • Push a message to a SPECIFIC user (NOTE: This may
  • be multiple clients, depending on the user's browser
  • configuration - this will push a message to any
  • client that has subscribed with the given UserID).
  • --->
  • <cfset wsPublish(
  • "demo",
  • "Hello #pushUser.name#, I hope you are well.",
  • {
  • selector: "userID eq #pushUser.id#"
  • }
  • ) />
  •  
  • <!--- We found the user, no need to keep looping. --->
  • <cfbreak />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Turn off debugging output. --->
  • <cfsetting showdebugoutput="false" />
  •  
  • <!--- Reset the output buffer. --->
  • <cfcontent type="text/html; charset=utf-8" />
  •  
  • <cfoutput>
  •  
  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8">
  • <title>Using ColdFusion 10 WebSocket Native Filtering</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Send A Message To A User
  • </h1>
  •  
  • <ul>
  •  
  • <!---
  • Output a link to send a static message to each of
  • the users.
  • --->
  • <cfloop
  • index="user"
  • array="#application.users#">
  •  
  • <li>
  • <a href="./send.cfm?id=#user.id#">
  • Send to #user.name#
  • </a>
  • </li>
  •  
  • </cfloop>
  •  
  • </ul>
  •  
  •  
  • <!--- Check to see if we have a user we pushed to. --->
  • <cfif !isNull( pushUser )>
  •  
  • <p>
  • Pushed to #pushUser.name#!
  • </p>
  •  
  • </cfif>
  •  
  • </body>
  • </html>
  •  
  • </cfoutput>

As you can see, when I use the wsPublish() function to publish a message on the WebSocket channel, I am including the key-value pair:

selector: "userID eq #pushUser.id#"

This tells ColdFusion's native WebSocket filtering to only push the given message down to users who have previously subscribed to the given channel [demo] with the subscription-key, userID, having the same value as "#pushUser.id#".

On the client-side of the code, the developer must then define a "userID" property when subscribing to the WebSocket channel. Here is the user interface (UI) of the client-side demo - notice that I am grabbing a userID value out of the URL collection and then using it to create a global JavaScript variable:

user.cfm - Our Client-Side User Interface (UI)

  • <!--- Param the user's selected persona. --->
  • <cfparam name="url.id" type="numeric" default="0" />
  • <cfparam name="url.name" type="string" default="" />
  •  
  • <!--- Make sure the user has selected a persona. --->
  • <cfif !url.id>
  •  
  • <!--- Redirect back to login. --->
  • <cflocation
  • url="./index.cfm"
  • addtoken="false"
  • />
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Turn off debugging output. It can't help us in WebSockets. --->
  • <cfsetting showdebugoutput="false" />
  •  
  • <!--- Reset the output buffer. --->
  • <cfcontent type="text/html; charset=utf-8" />
  •  
  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8">
  • <title>Using ColdFusion 10 WebSocket Native Filtering</title>
  •  
  • <script type="text/javascript">
  • <cfoutput>
  •  
  • // We need to pass the Application name through with the
  • // WebSocket connection so ColdFusion knows which memory
  • // space to access.
  • var coldfusionAppName = "#getApplicationMetaData().name#";
  •  
  • // Let's pass the user ID through with each WebSocket
  • // request. This way, we can use implicit WebSocket
  • // filtering on the server-side.
  • var coldfusionUserID = #url.id#;
  •  
  • </cfoutput>
  • </script>
  •  
  • <!--
  • Load the script loader and boot-strapping code. In this
  • demo, the "main" JavaScript file acts as a Controller for
  • the following Demo interface.
  • -->
  • <script
  • type="text/javascript"
  • src="./js/lib/require/require.js"
  • data-main="./js/main">
  • </script>
  • </head>
  • <body>
  • <cfoutput>
  •  
  • <h1>
  • Hello, I'm #url.name#
  • </h1>
  •  
  • <p>
  • Check out my <em>JavaScript console</em> - that's where
  • my messages show up.
  • </p>
  •  
  • <p>
  • <a href="./index.cfm">Choose a differen user</a>.
  • </p>
  •  
  • </cfoutput>
  • </body>
  • </html>

Once I have this global JavaScript variable in place, I can use it to subscribe to the ColdFusion WebSocket. In this case, my JavaScript controller will be using my ColdFusion WebSocket AMD module - ColdFusionWebSocket() - to perform the subscription and listening actions.

main.js - Our JavaScript Demo Controller

  • // Define the paths to be used in the script mappings. Also, define
  • // the named module for certain libraries that are AMD compliant.
  • require.config({
  • baseUrl: "js/",
  • paths: {
  • "domReady": "lib/require/domReady",
  • "jquery": "lib/jquery/jquery-1.7.1",
  • "order": "lib/require/order",
  • "text": "lib/require/text",
  • }
  • });
  •  
  •  
  • // Load the application. In order for the demo controller to
  • // run, we need to wait for jQuery and the CFWebSocket module to
  • // become available.
  • require(
  • [
  • "jquery",
  • "../../../cfwebsocket",
  • "domReady"
  • ],
  • function( $, ColdFusionWebSocket ){
  •  
  •  
  • // Create an instance of our ColdFusion WebSocket module
  • // and subscribe to the "Demo" channel. We are setting the
  • // userID as a custom header that will be passed-through with
  • // each socket request. This way, we can use the native
  • // WebSocket selector and filtering when we either subscribe
  • // to or publish from channels, respectively.
  • var socket = new ColdFusionWebSocket(
  • coldfusionAppName,
  • "demo",
  • {
  • userID: coldfusionUserID
  • }
  • );
  •  
  •  
  • // Listen for published messages on the "Demo" channel.
  • socket.on(
  • "message",
  • "demo",
  • function( event, data ){
  •  
  • console.log( "Published:", data );
  •  
  • }
  • );
  •  
  •  
  • // Listen for publish errors.
  • socket.on(
  • "error",
  • function( event, message ){
  •  
  • console.log( "Error:", message );
  •  
  • }
  • );
  •  
  •  
  • }
  • );

Notice that when we instantiate the ColdFusionWebSocket() module, we are passing along a custom header, userID, with the subscription. This is the property that will be evaluated by the server-side selector property (from above):

selector: "userID eq #pushUser.id#"

This example uses neither ColdFusion session manage nor any WebSocket channel listeners; and still, if you watch the video, you'll see that this filtering configuration pushes messages only to the appropriate client. Without a Channel Listener, you lose the ability to add fine-tuned security and message interception; but, if all you want to do is filter messages by user, the native ColdFusion WebSocket filtering makes it incredibly easy.

The ColdFusionWebSocket() AMD module and this demo is available on my GitHub account.




Reader Comments

@Ben, another great post and a great example of why I read your blog; you let us all learn together.

I still haven't had a break in projects to play with WS but rest assured I'll be back to try this code out!

@Brian,

Thanks my man! Apparently, this behavior is actually documented, as Ray Camden pointed out after I posted this :) I swear, I read the docs over and over again and never saw it!!