/home/embrenneke

A mostly-fresh start

Nodedub Walkthrough

A brief(?) introduction to the source for Is WWDC Sold Out Yet?

As I said in the release announcement, the site was a way for me to get some experience with a new toolset, so don't expect to see a lot of best practices or even correct practices when looking through the code. I had used nodejs on the job briefly before, but it was someone else's existing design, and I believe you don't really know a language/framework until you have built something in it from scratch yourself.

This is also not a How to get started with Heroku or How to get started with Node.js article. There are plenty of those on the web. I used several building it. This is just me trying to hit the highlights of what makes this site unique.

Before I start, I would like to thank @esigler for jumping on the domain registration and setting up a github repo and heroku instance as soon as we started joking about it on twitter. I definately would not have actually written this if he hadn't taken it seriously and created them right away.

dubstate.js

The app has 1 main piece of state. What is the current status of WWDC?

1
2
3
4
5
var DUBSTATES = {
  NO : {name: "NOPE", description: "Tickets are not on sale yet." },
  MAYBE : {name: "ALMOST", description: "Tickets have gone on sale. Go!"},
  YES : {name: "YEP", description: "You are too late, they sold out." }
};

apple.js

This file provides the meat of the application. Is WWDC actually sold out yet? It gets https://developer.apple.com/wwdc/, counts the number of instances of 2012 and 2013, and if 2013 is mentioned more, it assumes WWDC 2013 has been announced and triggers the state change from NO to MAYBE. This function is called every 5 minutes via a simple setTimeout() call.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var checkYear = function () {
  https.get('https://developer.apple.com/wwdc/', function(res) {
    var wwdcContent = "";
    res.setEncoding('utf8');
    res.on('data', function (d) {
      wwdcContent = wwdcContent.concat(d);
    });
    res.on('end', function() {
      var oldyear = wwdcContent.match(/2012/g);
      var newyear = wwdcContent.match(/2013/g);
      ...snip error checking...

      if ((dubstate.currentState == dubstate.DUBSTATES.NO) && (newyear > oldyear)) {
        dubstate.trigger("MAYBE");
      }
    });
  });
};

That trigger action at the end checks the database to see if it has already happened (restart, multiple nodes, etc) and if it hasn't, then it sends and email to everyone who has registered their email address.

Handling email

This part was tricky. Registrations trickle in slowly, but when the site changes, everyone expects to receive an email immediately. So to ensure we get as many emails out as we can and still keep it cheap/free to run, we:

  1. Confirm email addresses. Nothing too hard, just a simple encryption of the email address & a nonce bundled in a url for the user to click to make sure the recipient really wanted to be added. We send this confirmation through gmail since they happen slowly.
  2. Cull uncomfirmed addresses regularly. Lots of people will test with fake addresses for some reason, or bail when they find out it is a two step process. This keeps the record count low, and keeps us in the free rowcount for heroku.
  3. Once confirmed, add users to a mailgun mailing list. This allows us to send a single email when Apple's site is updated and ensures everyone gets it in a reasonable timeframe.

building the confirmation url

1
2
3
4
5
6
var confirmationString = email + ":" + nonce;
var cipher = crypto.createCipher('aes-256-cbc', credentials.aesKey);
var crypted = cipher.update(confirmationString, 'utf8', 'hex');
crypted += cipher.final('hex');
var url = "http://" + req.headers.host + "/confirm?confirmationString=" + crypted;
emailer.sendConfirmationEmail(email, url);

credentials.js

At one point in development I had most third party credentials (gmail, mailgun) hardcoded in the source, which is why the github repo has no history. It has since been changed to read these values from environment variables. Heroku and other cloud hosting services allow you to set these env vars easily based on environment (dev/test/production), and reading them in with javascript is extremely straightforward.

1
2
3
4
5
/* for encoding/decoding confirmation string */
exports.aesKey = process.env.CONFIRM_AES_KEY || "";
if (exports.aesKey == "") {
  console.log("Error: Missing AES key.  Won't be able to process confirmation emails.");
}

route/index

By default, the express framework uses the jade templating engine to reduce the amount of HTML you have to write. It takes a little getting used to, and I'll admit I barely learned enough to get a functional page displayed. This is one of many problems I have with ruby as well, I don't want or need a shorter HTML, I just want some defined way of inserting templated variables like with underscore.js or jquery.

the index javascript where variables are passed

1
2
3
4
5
exports.index = function(req, res) {
  res.render('index', { title: 'Is WWDC Sold Out Yet?',
                        value: dubstate.currentState.name,
                  description: dubstate.currentState.description });
};

the jade index template file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extends layout

block content
  h1= value
  p= description

  if value == 'NOPE'
    p Register your email below to be notified when WWDC is announced.
    form(name="registration", action="/add", method="get")
      input(type="text", name="email", size="35", maxlength="320")
      br
      input(type="submit", value="Submit")

  if value == 'ALMOST'
    a(href="https://developer.apple.com/wwdc") WWDC at Apple

  br
  br
  a(href="/privacy") privacy policy

fin

That's it as far as the unique bits I think. There are some helper functions I wrote for accessing the database and emailing, but they are pretty self explanatory, and the app.js (int main() if you will) is mostly boilerplate stuff to get the server running and set up the routes/pages. If you want further clarification hit me up on email/twitter and I'll be glad to expand this post.