When it comes to pushing realtime messages using WebSocket Software as a Service (SaaS) libraries, I love Pusher App and PubNub. While both services provide the same basic features, I had thought that only PubNub allowed client-to-client events (ie. events published directly from one client to another client without using your server as a proxy). Yesterday, however, I happend to be setting up a new demo app in Pusher and I noticed that Pusher now allows for client-to-client realtime messages (which you have to explicitly enable, per app). I don't know when this was introduced, but it's wicked awesome! So, naturally, I had to give it a try.

Because client-initiated events are more susceptible to security threats, Pusher mandates that all client-to-client events take place over private channels. This ensures that each client is a valid user of your application, and therefore, limits the exposure-threat of your realtime app key.

In the demo below, I'm not really implementing any tight security; but I do supply the necessary authorization end-point (authEndpoint) that is required for all private and presence channels. Hopefully, I can talk more about channel-based security in an upcoming post.

There are other restrictions that are applied to the client-events, such as a 10-event-per-second max; but, let's just get to the code already! In the following demo, I'm simply tracking the mouse-movements across the various clients that are connected to the ColdFusion application.

<em>Note</em>: You should see the Mouse cursor for each user viewing this page.

</p>

<!-- Load jQuery and Pusher from the CDN. -->

<script

type="text/javascript"

src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js">

</script>

<script

type="text/javascript"

src="//js.pusher.com/2.1/pusher.min.js">

</script>

<script type="text/javascript">

// Define the pusher key here so we can limit our use of

// CFOutput within the JavaScript.

var pusherAppKey = "<cfoutput>#request.pusherKey#</cfoutput>";

// -------------------------------------------------- //

// -------------------------------------------------- //

// Generate a unique ID for each user.

var currentUserID = "<cfoutput>#createUUID()#</cfoutput>";

// -------------------------------------------------- //

// -------------------------------------------------- //

// Create a new instance of the Pusher client. The second

// argument - the connection options - is an optional set

// of data that can be passed through with Authentication

// requests. In this case, we need to define the authorization

// end-point since client-events need to be on authorized

// channel subscriptions.

var pusher = new Pusher(

pusherAppKey,

{

authEndpoint: "./channel_auth.cfm",

auth: {

params: {

userID: currentUserID

}

}

}

);

// All the users will listen for and announce mouse-move

// events over this private channel.

// --

// NOTE: client-events need to use an authenciated channel.

var channel = pusher.subscribe( "private-mouse" );

// Bind to the move event (other user's publishing).

// --

// NOTE: ALl client-events must be transmitted on events that

// are prefixed with "client-".

channel.bind(

"client-moved",

function( event ) {

applyMouseMove( event.userID, event.x, event.y );

}

);

// Client-events can only be triggered at a maximum of 10 events

// per second. As such, we'll have to debounce the events that

// are triggered by the local user.

var rateLimitEvent = null;

// I push the current user's mouse-move event out on the

// private channel to all the other users.

// --

// NOTE: Client-events do NOT bounce back to the originating

// user. As such, we can publish events without having to have

// and logic about who sent what.

function pushMouseMove( userID, x, y ) {

if ( rateLimitEvent ) {

rateLimitEvent.x = x;

rateLimitEvent.y = y;

return;

}

rateLimitEvent = {

userID: userID,

x: x,

y: y

};

// Debounce for 100ms (we can only send a max of 10 events

// per second ~ every 100ms).

setTimeout(

function() {

channel.trigger( "client-moved", rateLimitEvent );

rateLimitEvent = null;

},

100

);

}

// -------------------------------------------------- //

// -------------------------------------------------- //

// Keep a collection of the users we know about.

var users = [];

// Start watching the document for mouse movements.

$( document ).mousemove( handleMouseMove );

// I apply a mouse move events that has been received over the

// pusher channel (from another user).

function applyMouseMove( userID, x, y ) {

var user = getUserByID( userID );

// If the user has not yet been defined locally, then

// create it and append it to the BODY.

if ( ! user ) {

user = createNewUser( userID );

users.push( user );

$( "body" ).append( user.mouse );

}

// Update the data and location for the user.

user.mouse.css({

left: ( ( user.x = x ) + "px" ),

top: ( ( user.y = y ) + "px" )

});

}

// I create a user object with the given userID.

function createNewUser( userID ) {

var mouse = $( "<div class='mouse'></div>" )

.append( "<div class='pointer'></div>" )

.append( "<div class='label'></div>")

;

var label = mouse.find( "div.label" );

// Apply a special label to the current user.

if ( userID === currentUserID ) {

label.text( "Me!" );

} else {

label.text( userID );

// Also, let's add a CSS class that animates the

// transition in CSS position. This way, the local

// user will update immediately and the external users

// will update with an animation in alignement with

// the rate-limiting.

mouse.addClass( "external" );

}

var user = {

id: userID,

x: -100,

y: -100,

mouse: mouse

};

user.mouse.css({

left: ( user.x + "px" ),

top: ( user.y + "px" )

});

return( user );

}

// I get the user object using the given userID. If the user

// has not yet been recorded locally, null is returned.

function getUserByID( userID ) {

for ( var i = 0 ; i < users.length ; i++ ) {

if ( users[ i ].id === userID ) {

return( users[ i ] );

}

}

return( null );

}

// I handle the local mouse-move event triggered by the current

// user.

function handleMouseMove( event ) {

// Update the local mouse indicator for the current user.

applyMouseMove( currentUserID, event.pageX, event.pageY );

// Push the mouse move event to all users.

pushMouseMove( currentUserID, event.pageX, event.pageY );

}

</script>

</body>

</html>

There's not too much to explain here; each event carries the unique userID and the x/y position of the user's mouse. When the event is pushed to the various clients, the event is rendered as a dot on the screen. It's pretty cool. Pusher App is really becoming one of my absolute favorite services!

Reader Comments

I'm not sure if it supports this the same way that it can in AS3, but perhaps you could use the Greensock tweening library to tween between event positions. That way even though the position "keyframes" are limited to 10 "fps" the tween engine will smooth out the motion.

Excellent question - animation is definitely one of the weakest parts of my development knowledge-base. I started out with jQuery-based animations... then, I started to learn a little bit about some CSS3 transitions... and that's about all I really know. And, by "know", I mean that I can sort of get it to work. I haven't looked into any animation-frame stuff or even the events that are triggered upon transition-end things.

Well, Greensock AS3 has a plugin called dynamicProps that essentially had a property in a continual tween until it reached its target. I'm thinking that JS may get this "feature" automatically by virtue of it being a runtime interpreted language where everything is always dynamic anyhow. I would really look at the greensock library over jQuery animate, as its performance blows most other JS animation and tweening library out of the water.

Wie geht es dir? I don't really have any great comparison to call upon. I've played around with both and they seem to be solid. I've started to learn more towards Pusher App because it has a mechanism that allows me to authenticate a user, server-side, before they subscribe to a private (and now Client) channel. When I was doing some earlier research, I believe PubNub still relied on channel-name obfuscation to implement that kind of security.

Is Pusher's approach to authorizing channel subscription significantly better? I can't say; but, the fact that they have built that in just gives me an extra little bit of comfort knowing that they are truly thinking about security in a holistic manner.

Also, the Pusher documentation is better and easier to follow. When I was researching the PubNub service, I found it very difficult to find help articles and info on how things should be implemented.

That said, both services do seem really solid and I think you'd be happy going with either.

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.