Mar 20, 2012

ColdFusion 10: Using filterCriteria in WebSockets for subscribing and publishing

Yesterday Ben Nadel asked me a question on Twitter, on using filterCriteria when publishing a message on a web socket channel from server side. The method ‘wspublish’ allows you to perform a server side push to a client who has subscribed to a channel. It takes three parameters – channelName, message and filterCriteria. I always assumed that the values present in subscriberInfo and publisherInfo can be compared in Channel Listener functions (canSendMessage, beforePublish etc) before the message can be received by the client. Although this technique is available, what I found after having a discussion with a fellow developer (Awdhesh), is that I can specify simple conditions when subscribing or publishing.

So what does it mean to subscribe to a channel with conditions? And more importantly why do I need it? Consider a scenario where you are subscribing to a particular company stock and would like to know its stock value in real time. The server on the other hand keeps updating the stock values for all the companies and you might end up receiving unnecessary data. Instead you can specify a filter criteria while subscribing to a channel and only the messages that satisfy the criteria will be delivered. Here’s how you do it:

socket.subscribe("myChannel", {name: 'Sagar', age: 25, selector: "stock eq 'ADBE'"}, myChannelHandler);

The second parameter is a JSON string containing some key-value pairs. One of the keys is the 'selector' where the filter criteria is specified. Here I’m declaring a condition ‘stock eq ADBE’ i.e. I’m interested in receiving stock quotes for ADOBE only.

The publisher can send a message on the channel by specifying the same as the third parameter:

<cfset wspublish("myChannel", 50, {stock=’ADBE’})>

The second parameter is the message (value of the stock) and the third parameter is a struct that specifies the key ‘stock’ with its value set to ‘ADBE’. When this message is published on the channel it will be received by the client since it matches the filter criteria specified by the client at the time of subscribing to the channel. If a message is published on the channel with the third parameter set to {stock=’MSFT’} then the same wouldn’t be sent to the client who has subscribe to ADBE stocks.

Similarly a filter criteria can be specified at the time of publishing as well. In the same context of stocks; say you want only those clients whose age is greater than 21 to receive the stock update. You can do that by specifying the filter criteria while publishing the message on a channel:

<cfset wspublish("myChannel", 50, {stock=’ADBE’, selector="age GT 21"}})>

Again, the filter criteria ‘age GT 21’ is specified as a value to the key ‘selector’. If you observe the subscribe method, it has ‘name’ and ‘age’ as keys specified as third parameter. Before publishing the message to a subscriber the value of the key ‘age’ is checked and if it satisfies the condition (age GT 21) then the message (stock value) will be published and received by the subscriber.

Demo:
I have created a sample demo that you can download it here. Open publisher.cfm, subscriber-ADBE.cfm, subscriber-MSFT.cfm on three browser instances. Click on the subscribe button to subscribe to the channels. In publisher.cfm select the stock from the drop down list and provide a value and click ‘Publish’. You’ll see that the subscribers (ADBE and MSFT) would receive values only for those stock quotes that they have subscribed to.

19 comments:

  1. Sagar, thanks for the write-up. I could have sworn that I did something along these lines; though, it's definitely possible that I was confusing when to "selector" and when not to. Let me go back and try to work something. In the meantime, I worked around the situation by using a persisted UserID, then return( true | false ) in the canSendMessage() event.

    ReplyDelete
  2. yes, the filter criteria is copied to subscriberInfo and publisherInfo which can then be used in listener functions. The canSendMessage function is called for each client and you can achieve the same. I think this is the right way to use filter criteria if you're performing a complex check before allowing the message to be sent to the client. If it is a simple check, then you can use this method.

    ReplyDelete
  3. yes, the filter criteria is copied to subscriberInfo and publisherInfo which can then be used in listener functions. The canSendMessage function is called for each client and you can achieve the same. I think this is the right way to use filter criteria if you're performing a complex check before allowing the message to be sent to the client. If it is a simple check, then you can use this method.

    ReplyDelete
  4. I'm gonna try this again this morning. Hopefully, I can get this to work. I'll let you know.

    ReplyDelete
  5. Sagar, I think there maybe something wrong with my version of the CF10 Beta - maybe it's an earlier version? I download your code and tried to run it. I had to remove the CFForm stuff (since the mapping was doing something strange). However, when I run the code, I didn't see anything happening (on publish). Take a look at this video: http://screencast.com/t/TAk2XRxQA8

    This is the same thing I was experiencing in my experiments. Maybe my CF10 is too early?

    ReplyDelete
  6. I think I uploaded the wrong version of demo files. Can you remove the filtercriteria i.e. the selector statement from publisher.cfm. That should work. 

    ReplyDelete
  7.  Ok, cool. That seems to have worked. Let me see if I can get an equivalent version running in my demo. Thanks!

    ReplyDelete
  8. OK! I'm finally making some progress. I've narrowed down my issue. If I use a *custom* channel listener CFC, I canNOT get the filtering to work; however, if I remove the listener so that the application uses the default ChannelListener.cfc, THEN the filter DOES work. Very strange! I'll see if I can figure out why my custom channel listener is overriding the native behavior. Is that even possible?

    ReplyDelete
  9. I did some more testing - I copied the core ChannelListener.cfc to my local application directory and tried using that. This breaks the filtering. It looks like the only way I can get the filtering to work is to NOT provide a local channel listener component: http://screencast.com/t/tZU7v9MeBa

    Maybe this is the intended behavior? Does the filtering only work if you don't provide your own event handles for canSendMessage(), etc?

    ReplyDelete
  10. In custom channel listener the filter criteria is copied to subscriberInfo and publisherInfo. You can use this perform some check in canSendMessage method and then return a boolean value. And yes, all communication will then go through this method event in a case where you have specified selector in filter criteria.

    If the method canSendMessage returns true always then all clients would receive the message published on the channel.

    ReplyDelete
  11.  I just confirmed, this also breaks your Demo as well. If I copy the ChannelListener.cfc (from the CFIDE folder) into your demo directory (and add the extends="CFIDE.websocket.ChannelListener") attribute, your demo filtering no longer works.

    ReplyDelete
  12. Yes that's the intended behavior because the channel listener function - canSendMessage would be invoked before sending the message to the subscriber. By default this method returns true and hence all the subscribers would get the message and yes it overrides the condition that you have specified in filter criteria (selector).

    ReplyDelete
  13. Sagar, thank you for all your help yesterday! I am super excited to finally understand how this feature works. I put this together for my own collection. http://www.bennadel.com/blog/2352-ColdFusion-10-Native-WebSocket-Filtering-And-Channel-Listeners-Are-Mutually-Exclusive.htm

    ReplyDelete
  14. Thanks for putting that up Ben. I'm glad that I was of help. BTW I would like to meet Joanna, Sarah and Tricia some day :)

    ReplyDelete
  15.  Ha ha, they are nice people :) After I posted this, Ray Camden pointed out that this feature (canSendMessage() vs. filtering) was clearly in the documentation. I guess I missed it! So sad, cause I kept reading and re-reading the docs trying to figure out why it wasn't working :D Oh well.

    ReplyDelete
  16. Sagar, Do you have any experience with front end controllers using websockets? I'm using ColdBox 3.5 on a new project and I cannot get wspublish() to work correctly. It looks like from the stack trace that the exception is being thrown at the onRequest method. The client has no issues in subscribing and it seems that the issue is isolated within the framework. Just thought I'd ask as I spin my wheels :)

    Thanks...Matt B

    ReplyDelete
  17. Matt, I've not tried CF WebSockets with any of the frameworks and hence I can't comment on that. Can you share the code and the stack trace, I'll try to look into it.

    ReplyDelete
  18.  Thank you Sagar, this may actually be tied to how Apache does rewriting of the URL to remove the "/index.cfm." I am tweaking the rule right now and will post back what I find.

    ReplyDelete
  19. btw, this is the bug: https://bugbase.adobe.com/index.cfm?event=bug&id=3650142

    ReplyDelete