Goodbye JavaScript
The JavaScript server code for Turtle Paint is becoming increasingly difficult to manage. People had warned me beforehand, but there’s no teacher like first-hand experience. The problems in a nutshell:
- Immaturity. Often, there are five different Node.js libraries that do approximately the same thing, but all of them are buggy and none do it exactly the way you’d want.
- Documentation. Nonexistent for most Node.js modules; most just provide one or two canonical usage examples. You could argue that I should simply read the source, but that is often difficult if you don’t have the high-level picture of how things fit together.
- Callback spaghetti. For instance, the way I’m importing a word list into the MongoDB database backend takes no less than seven levels of nested callbacks: create database connection, open database, drop collection, create collection, open file, read lines, insert data. When done reading, open other collection, update word count. When written like this it looks straightforward, but in code, each action is a new level of nested callbacks. There are libraries like flow.js that make this a little easier, but they mostly remove indentation levels while not fundamentally changing anything. I like to write event-driven code when it makes sense, but being forced to use it for everything is a pain.
- Difficulty of event-driven programming. It gets really difficult to tell what gets executed when. For instance, I had a bug where I opened a file and wanted to read it line-by-line in an event-driven way. I knew how many lines the file had, but the code consistently kept reading fewer lines than that. The cause? After verifying that the file existed by having opened it successfully, I opened a database connection. But while doing so, my file object already started streaming out read events, even though no event handler had been hooked up to it yet! This caused the first few thousand lines of the file to be ignored. As the event handler needed an open database, there was no way to hook it up earlier. The “fix” was to open the database first, then open the file and synchronously hook up the data event handler.
- Tooling. With a language as tricky as JavaScript, a proper linter is essential. The popular solution, Crockford’s JSLint, is too opinionated and just gets in the way; for instance, it wants me to declare
var i;
at the top of the function if I usei
as a counter in afor
loop, K&R C-style. Some such nonsense can be turned off, but not all. JSHint is better in this respect, but has other problems. Both struggle with unrecognized global variables that Node.js provides. - Syntax. It matters. Too many
function
objects make me go dizzy.
I could have switched to CoffeeScript, which is a Ruby-inspired language that is translated in a fairly straightforward way into vanilla JavaScript. However, it would mess up stack traces from Node.js because those refer to the compiled JavaScript code, not the CoffeeScript source.
Therefore, I decided to rewrite the server in Ruby. The client will obviously have to remain JavaScript, but it is pretty straightforward and mostly does DOM manipulation; all the logic lives on the server side.
I’m planning to use Sinatra for serving static files, and Cramp (built on top of EventMachine) for the game logic. This will still be event-driven code, but only where it makes sense, and hopefully Ruby syntax will make it more pleasant to use. Cramp supports WebSockets out of the box, assuming one uses Thin or Rainbows as the HTTP server.
For older browsers, I might use the Flash fallback of web-sockets-js, or I could use socket.io-ruby. But as I don’t like Socket.IO very much either (feature creep, bad documentation, and new bugs every release), it’ll probably be the former. There won’t be a “polling” fallback for browsers that support neither WebSockets nor Flash, but I think I can live with that.
It’ll be interesting to see how the JavaScript code compares to its Ruby counterpart.