Saturday, June 19, 2010

DRYing up Fab.js Apps

‹prev | My Chain | next›

Tonight, I hope to DRY up the move/bounce/chat handlers in the fab.js part of my (fab) game. The code in question:
  // TODO: These broadcast resources are not DRY
( /move/ )
( function() {
var out = this;
return function listener( obj ) {
if ( !obj ) out();
else if ( obj.body ) {

broadcast(comet_walk_player(obj.body));
update_player_status(JSON.parse(""+obj.body));
}
return listener;
};
} )


( /chat/ )
( function() {
var out = this;
return function listener( obj ) {
if ( !obj ) out();
else if ( obj.body ) {

var msg = JSON.parse(obj.body.toString());
msg.body = msg.say.substr(0,100);
broadcast(comet_player_say(JSON.stringify(msg)));
}
return listener;
};
} )


( /bounce/ )
( function() {
var out = this;
return function listener( obj ) {
if ( !obj ) out();
else if ( obj.body ) {

try {
update_player_status(JSON.parse(""+obj.body));
broadcast(comet_bounce_player(obj.body));
} catch (e) {
puts("[bounce_handler] error type: " + e.type +
", message: " + e.message);
}
finally {
out();
}
}
return listener;
};
} )
Each of these unary (non-middleware) fab apps updates the player status in the local store then broadcasts it to the other players in the room. The exact details of the status attribute being stored / broadcast varies from resource to resource, but the overall structure of the unary apps is nearly identical (as the initial comment notes).

The only difference in structure between the three is the try-catch-finally chain in the bounce handler. I added that last night and would like to add it to the other two, minus the duplication, of course.

First up, I factor the try-catch-finally version out into a library app. Until I can think of a better name, I call it unary_try:
var puts = require( "sys" ).puts;

function unary_try(fn) {
var out = this;
return function listener( obj ) {
if ( !obj ) out();
else if ( obj.body ) {
try {
if ( obj.body ) {
fn.call(obj, obj);
}
} catch (e) {
puts("[unary_try] error type: " + e.type +
", message: " + e.message);
}
finally {
out();
}
}
return listener;
};
}

exports.app = unary_try;
I ought to be able to reduce the bounce handler to this:
  ( /bounce/ )
( unary_try( function () {
update_player_status(JSON.parse(""+this.body));
broadcast(comet_bounce_player(this.body));
}
) )
Sending that anonymous function to the unary_try app establishes a callback that is invoked when the downstream (i.e. web browser) POSTs data. Using the call method and sending the POST contents as the first object binds the this variable to the POST body inside the anonymous function. In this case, it is a cheap way to keep the anonymous functions as small as possible.

When I run the game, however, it exits immediately. This is an indication that I have forgotten a function wrapper somewhere and, indeed, I have done so here. Specifically, the unary_try app is not a fab app—it takes the callback as its only argument, but it has to return a unary app to fit properly in the chain:
function unary_try(fn) {
return function() {
var out = this;
return function listener( obj ) {
if ( !obj ) out();
else if ( obj.body ) {
try {
if ( obj.body ) {
fn.call(obj, obj);
}
} catch (e) {
puts("[unary_try] error type: " + e.type +
", message: " + e.message);
}
finally {
out();
}
}
return listener;
};
};
}
Ah, much better, now the game starts up and collision / bounce messages are broadcast. Before applying this to the other two resources, I switch the fn context to the POSTs body rather than the entire request—it is the only thing that I care about in each of the scenarios, so I might as well be as specific as possible:
//...
try {
if ( obj && obj.body ) {
fn.call(obj.body, obj.body);
}
}
//...
With that, I can DRY up my other two broadcast resource nicely:
  ( /move/ )
( unary_try( function () {
update_player_status(JSON.parse(""+this));
broadcast(comet_walk_player(this));
} ) )

( /chat/ )
( unary_try( function () {
var msg = JSON.parse(this.toString());
msg.body = msg.say.substr(0,100);
broadcast(comet_player_say(JSON.stringify(msg)));
} ) )

( /bounce/ )
( unary_try( function () {
update_player_status(JSON.parse(""+this));
broadcast(comet_bounce_player(this));
} ) )
Much better! I do not have any tests written against this code and I think that I might be able to simplify it a bit. I think I will pick up there tomorrow.

Day #139

No comments:

Post a Comment