‹ Jack's Brain

Client-side Cassandra Driver in Chrome with Browserify

May 27, 2015

Introduction

For a project entirely irrelevant to this post, I wanted to get a Cassandra DB driver working on clientside JS. A client side database driver? you ask incredulously. Why? Well, the reasons aren’t terribly important, but I know it’s a bad idea, requires some unlikely firewall configs, etc. but that’s not the point – perhaps I can be of assistance to some future soul who tries to do the same thing for some insane usage case that only a mother could love.

To clarify with an analogy, server side database drivers are houses. Client side is the ocean. There are boats on the ocean (which have occupants who still need a house at the end of the day; think your standard async backend calls), and there are even houseboats (which don’t quite work as well as boats or houses at their respective tasks, but work decently; think PouchDB or localstorage). This tutorial/walkthrough covers retrofitting a house to float: it doesn’t make a whole lot of sense, but if you need to do it, here’s how – just don’t expect to get a house or a boat at the end… this is straight out of Frankenstein’s lab.

**This tutorial is Chrome exclusive, and only works when loaded as a Chrome Packaged Application. **The low level socket access this requires isn’t available any other way.

This walkthrough assumes proficient knowledge of node and browserify, npm and browserify installed, etc. (It was also written about ten minutes after getting a working build, so there’s a near certainty that there are limitations I haven’t figured out. Caveat emptor.)

Process

My first instinct was to use Browserify, and see where that got me… so let’s do that.

Basic Code

Start by creating a new node project and installing cassandra-driver. Add some boilerplate and point it at a cluster and keyspace with some data we can check out.

var cassandra = require('cassandra-driver');

var client = new cassandra.Client({ contactPoints: ['127.0.0.1'], keyspace: 'mytest'});
var query = 'SELECT * FROM mytest';

client.execute(query, function(err, result) {
  if(err){console.log("error: " + err);}
  console.log('success: ' + result.rows[0].id);
});

Now we’ve got a super basic query logic. Note the usage of an IP, not a hostname as a contact point – I haven’t gotten around to getting Cassandra’s DNS lookups working (relies on node’s DNS utilities, which obviously aren’t in the browser), but it works fine with IP’s. I’m sure the fix is trivial, but I just haven’t gotten there yet.

Networking

If you bundle this into a Chrome Application now (by running browserify index.js -o bundle.js and setting this as the background script of a Chrome application), you’re going to find that net.isIP is not a function. Bummer. This is because the standard node libraries don’t exist in Chrome, so we need to slip some support in. Thankfully, we have chrome-net! This lets us use native chrome sockets (as exposed to packaged applications) with all of node’s net parameters that we’ve come to know and love.

But now we have to fit this into the Cassandra client… into the source we go. Move into the node_modules/cassandra-driver directory. Here, we’re going to install chrome-net with npm install --save chrome-net. Now, we’re going to force Cassandra to use our new network driver. There are two files you’ll need to change var net = require('net'); into var net = require('chrome-net'); — those are lib\connection.js and lib\control-connection.js.

Timing Fallbacks

Now we’re done, right? Browserify our original code, and… we get the lovely Error in response to sockets.tcp.send: ReferenceError: setImmediate is not defined. setImmediate isn’t defined; d’oh! This is mainly a node and (make sure you’re sitting down) Internet Explorer feature (WHAT? Yeah. I had the same reaction.). Well, let’s shim it into the Cassandra driver. Luckily, we’ve already got async required in the relevant files; we just need to convert the calls to async.setImmediate so we take advantage of the library’s handy fallbacks for setImmediate (in this case, it’s a setTimeout(..., 0);). As of this writing, there are five cases we need to change (hopefully that number drops to zero in the near future; I’ve got a pull request open since it’s a fallthrough if the function is defined, and who doesn’t love better legacy node support?).

There are three relevant files that require changes in node_modules\cassandra-driver\lib\: writers.js (1 replacement), client.js (2 replacements), and connection.js (2 replacements). You can do a simple s/setImmediate/async.setImmediate to find/replace all instances of setImmediate with async.setImmediate.

And you’re done! (Yeah, actually it was that easy). Go ahead and wrap up your JS – in the root of your original project (with the Cassandra query code), run browserify index.js -o bundle.js.

Now let’s bundle this into a Chrome Application. Move your bundle.js to a new directory, and in that directory, create a file called manifest.json and put the following code in it:

{
    "manifest_version": 2,
    "name": "Cassandra+Chrome",
    "version": "0.1.4",
    "app": {
        "background": {
            "scripts": ["bundle.js"]
        }
    },
    "sockets": {
        "udp": {
            "send": "*"
        },
        "tcp": {
            "connect": "*"
        },
        "tcpServer": {
            "listen": "*"
        }
    }
}

This is a simple app manifest that requests full socket permissions (this is overly permissive, obviously, but just for the sake of demonstration), and starts up our bundle.js in the background of the browser.

Now, go to chrome://extensions in your browser, make sure developer mode is enabled, and click Load an unpacked extension. Select the directory that contains your manifest and bundle, and add it. You should see it appear on the extensions page! Go ahead and click background page next to “Inspect views:”; this will inspect the generated page that our background code is running in. In the console, you should see your query results. Congratulations; you’ve just run a Cassandra query from a browser(ish)!

Our chrome-net extension whines about not getting the expected parameters (the socketId is undefined, another thing on my list to chase down), but thankfully it normalizes things such that the connection is established and the query runs, so it’s not a code breaking bug (and at this level of hackiness, which is far more novelty than production (or even dev) ready code, if it doesn’t break the query, it’s getting fixed later).

Future Thoughts

There are obviously lots of bugs to fix – I know about the DNS lookup issue and the problematic socket parameters, and there’ve gotta be more. Practically, this is a bit too hacky for my tastes to put into anything really useful, but it’s a good reminder that JS is JS, and most of node’s native secret sauce can be emulated or handled by other packages when you pull a project out of node. It’s also always nice to have a portable binary protocol client, because coding one from the ground up is out of reach for most developers who don’t have time to really grok the spec.

Lessons learned:

  • Find a balance between native calls and polyfills – async is great with this, and the more I use facets of a language that aren’t universal, the more I’m going to be turning to polyfills and shims to ensure my code is portable rather than chained to a given environment
  • Node isn’t magic – this probably sounds kind of silly, but I’ve had less than a year of experience with node, so any day spent learning more about how it works (and how modular its backend is) is always a good day in my book. Although it’s stating the obvious, packages are tremendously easy to slot in and out (although chrome-net is kind of cheating) and it (and some of the upcoming features of ECMA6) has made the transition from “JS as a DOM tool” to “JS as a programming language” a lot easier, and made JavaScript application frameworks a lot less daunting and a lot more fun.
  • Cassandra devs (Jorge Bay Gondra, original author of the node Cassandra driver, especially) are on TOP of it. Of course, you don’t get to be that big without a great codebase written by great engineers, but the structure is logical and conducive to understanding, and the comments are of a perfect density and clarity. I’m really looking forward to digging my hands deeper into OpsCenter code this summer; from the little I’ve decompiled, it looks just as great.

Closing

So there you have it – you can run Cassandra queries from Chrome. Just because you can, though, doesn’t mean you should. If you use this code in production, you’re insane – this was done just to see how far error-by-error conversion of a complex node package could be pushed, and, to continue the earlier metaphor, was more of an exercise of “I wonder if you can retrofit a house to float” rather than trying to make any semblance of decent code (which this isn’t)… also, if anyone from DataStax is reading this, don’t worry – I won’t do this to your codebase this summer ;).

Demo code is available on GitHub for the manifest and bundle of the Chrome Application. If you want to customize the query, it’s at the very bottom of the bundle. Happy hacking; be forkful and multiply.

Questions? Comments? Abuse? Drop me a line below.

Edit 1: The version in the manifest is a necessary field, but the number can be anything you want. The 0.1.4 is a hangover from another project I lifted the manifest template from.