FDD 2013 - Workshop

"Node.js - hype or hope?" Fun ;)

Node.js logo Wojciech Gawroński - 2014 © License : CC BY-ND 3.0 PL

Organization and Theory


function sampleCode(request, response) {
  if (request.path.indexOf("Is it readable?") !== -1) {
    usersService.getAnswer(request.path, function (error, answer) {
      if (answer === "YES") {
        response.setStatusCode(200);
        response.send("Move forward.");
      } else {
        //
        // It is not acceptable!
        //
        response.setStatusCode(406);
        response.send("Increase font size.");
      }
    });
  }
}
            

Tasks

Solutions

Linux-crash-course


$ touch NEW_FILE
$ mv SOURCE DESTINATION
$ cp SOURCE DESTINATION
$ rm -ri FILES_FOR_DELETING

$ cp /path/{1,2,3}.txt DESTINATION
$ mv /path/*.tar.gz DESTINATION

$ cat BIG_FILE | grep -i CASE_INSENSITIVE_TEXT
$ find DIRECTORY -iname *CASE_INSENSITIVE_PART_OF_NAME"

$ nohup ./run-in-background &
$ ./run-script.sh > /dev/null

$ curl -X POST -d "DATA" "http://localhost:8080/my/rest/api"
$ netcat sql-server-hostname 1433
$ wget -O OUTPUT_FILE http://http/address/to/file
            

Prerequisites

Git


$ git --version

$ git config --global user.name "Jasiu Nowak"
$ git config --global user.email "jnowak@company.com"
            

Node.js


$ node --version            # Sometimes: nodejs
$ npm --version
$ nvm
            

Sources and "Workshopper"


$ npm install -g jshint grunt-cli istanbul

$ git clone git@github.com:afronski/mighty-penguins-revenge.git

$ git clone git@github.com:afronski/node-js-greenhorn-workshopper.git
$ cd node-js-greenhorn-workshopper
$ npm install -g
            

Task 0


$ node-js-greenhorn-workshopper
              

4 assignments = 20 minutes

"JavaScript 2.0"


"use strict";

const PI = 3.14;

var util = require("util");

function Animal(options) {}

function Dog(options) {
    Animal.call(this, options);
}

util.inherits(Dog, Animal);

var doge = Object.create(Dog.prototype),
    doggyDoge = new Dog();
            

"JavaScript 2.0"


"use strict";

var http = require("http"),
    links = [ "http://first-link.com", "http://second-link.com" ],
    loaded;

function isTrue(element) { return element === true; }

function loaded(index, response) {
    loaded[index] = (response.statusCode === 200);

    if (loaded.every(isTrue)) {
        console.log("All links downloaded successfully!");
    }
}

loaded = links.map(function (link, index) {
                       http.get(link, loaded.bind(null, index));
                       return false;
                   });
            

Tools

Dependencies

Semantic Versioning

package.json


"dependencies": {
  "underscore": "~1.3.0"
},

"devDependencies": {
  "mocking-framework": "1.x.x"
}
            

Useful Modules

Serving static content

Requirements

  • src, libraries JavaScript files
  • images Images
  • sounds, music Sound and music files
  • stylesheets CSS and Fonts
  • client HTML files

Task 1

Serving static content


$ cd mighty-penguins-revenge
$ git checkout tags/TASK_1
$ npm install
            

1 assignment = 10 minutes

Task 1

Serving static content

Apollo 13 - 'Duct Tape' Solution

Storage

LevelDB Logo

An open source, on-disk, key-value store
written and maintained by Google


{
          "key1": "value1",
          "key2": "value2"
}
              

Asynchronous I/O

Event Loop

Reactor Pattern

Deleting entries from storage

Requirements

  • Remove value from storage by provided key.
  • Use asynchronous API provided by LevelDB module.

Task 2

Deleting entries from storage


$ cd mighty-penguins-revenge
$ git checkout tags/TASK_2
$ npm install
            

1 assignment = 10 minutes

Task 2

Deleting entries from storage


  // 1. First invoke an action.
  asynchronousAction(parameters, function continuation(error, result) {
      // 3. After a while, results will arrive.
  });
  // 2. Return from asynchronous action.
            

Buffer

Pure JavaScript is Unicode friendly but not nice to binary data.


var buffer = new Buffer(256),
    length = buffer.write("\u00bd + \u00bc = \u00be", 0);

console.log(length + " bytes: " + buffer.toString("utf8", 0, length));
Buffer.byteLength("JS string length returns characters count.", "utf8");
            

Caching

Requirements

  • Read all files collected from directory with provided pattern.
  • Cache in memory storage the received buffer with content.

Task 3

Caching and Buffers


$ cd mighty-penguins-revenge
$ git checkout tags/TASK_3
$ npm install
            

1 assignment = 10 minutes

Task 3

Caching and Buffers


fs.readFile("/path/to/file", function (error, buffer) {
    /*...*/
});

fs.readFile("/path/to/file", "utf8", function (error, string) {
    /*...*/
});
            

Streams


$ cat file.txt | grep "CONTENT" | wc -l
$ ./script 2>&1 >/dev/null | tee DIFFERENT_FILE.txt
            

We should have some ways of connecting programs like garden hose--screw in another segment when it becomes necessary to massage data  in another way.

Doug McIlroy, 1964

Streams

API

Reaction for first time with streams ;)

// Different types of streams:
// - Readable and Writable
// - Transform and PassThrough
// - Duplex

stream.on("data", function (chunk) { /*...*/ });
stream.on("end", function () { /*...*/ });
stream.on("error", function () { /*...*/ });

from.pipe(to);
readable.pipe(writable);
            

Streams

Under the hood

  • Buffering
  • Compatibility
  • Buffers and Objects
  • State
  • Backpressure
Backpressure ;)

REST and HTTP headers

Requirement

Push as a HTTP response previously cached file
from memory storage.

Task 4

Streams and HTTP


$ cd mighty-penguins-revenge
$ git checkout tags/TASK_4
$ npm install
            

1 assignment = 10 minutes

Task 4

Streams and HTTP


cache.exposeStream(file).pipe(response);
            

Error Handling


asynchronousAction(parameters, function (error, results) {
    if (error) {
        continuation(error);
        return;
    }

    asynchronousAction(parameters, function (error) {
        if (error) {
            continuation(error);
            return;
        }

        asynchronousAction(function (error, results) {
            if (error) {
                continuation(error);
                return;
            }

            // WTF ???
            

Domains

Domains provide a way to handle multiple different I/O operations as a single group.

Resources? RAII

Task 5

Domains


$ cd mighty-penguins-revenge
$ git checkout tags/TASK_5
$ npm install
            

1 assignment = 10 minutes

Task 5

Domains


var domain = require("domain"),
    handler = domain.create();

handler.on("error", function (error) { /*...*/ });

asynchronousAction(parameters, handler.intercept(function (results) {
    asynchronousAction(parameters, handler.intercept(function () {
        asynchronousAction(handler.intercept(function (results) {
            /*...*/
        }));
    }));
}));
            

Event-Driven Model

EventEmitter in action

Event Emitter API


var util = require("util"),
    EventEmitter = require("events").EventEmitter,
    sender;

function Sender() {
  EventEmitter.call(this);
}

util.inherits(Sender, EventEmitter);

sender = new Sender();

sender.on("eventName", function handler() { /*...*/ });
sender.addListener("eventName", function handler() { /*...*/ });
sender.once("eventName", function handler() { /*...*/ });

sender.removeAllListeners();
sender.removeListener("eventName", handler);

sender.emit("eventName", argument1 /* ... */);
            

Task 6

Handle Game Start


$ cd mighty-penguins-revenge
$ git checkout tags/TASK_6
$ npm install
            

1 assignment = 10 minutes

Task 6

Handle Game Start


socket.get("room-author", function (error, isAuthor) {
  owner.queries.getAccessibleRooms(function (error, rooms) {
    rooms = rooms.filter(bySession.bind(null, session))[0];

    if (rooms) {
      if (isAuthor) {
        owner.commands.lockRoom(session, function (error) {
          socket.broadcast.to(session).emit("game-started");
        });
      }
    } else {
      socket.emit("game-failed");
    }
  });
});
            

WebSockets

A protocol providing full-duplex communications channels over a single TCP connection. The WebSocket protocol was standardized by the IETF as RFC 6455 in 2011, and the WebSocket API in Web IDL is being standardized by the W3C.

socket.io


socket.join("room");
socket.leave("room");

socket.set("property", /*...*/);
socket.get("property", /*...*/);

socketIO.of("/rooms").on("event-name", function handler() { /*...*/ });

socket.broadcast.to("room").emit("event-name", /*...*/);
socket.broadcast.emit("event-name", /*...*/);
socket.emit("event-name", /*...*/);
            

Task 7

Broadcasting Game State


$ cd mighty-penguins-revenge
$ git checkout tags/TASK_7
$ npm install
            

1 assignment = 10 minutes

Task 7

Broadcasting Game State


function broadcastPlayerState(socket, session, state) {
  socket.broadcast.to(session).emit("enemy-update", state);
};

function broadcastPlayerBullet(socket, session, nick) {
  socket.broadcast.to(session).emit("new-bullet", nick);
};
            

Patterns

Patterns - "Bulkheading"

Separate compute intensive parts from client-facing parts.

Patterns - Risk Delegation

Separate potentially dangerous parts from client-facing parts.

Patterns - Promises


promise
    .then(function onSuccess() { /*...*/ })
    .error(function onFailure() { /*...*/ });

var fs = require("q-io/fs"),

    readJsonPromise = Q.async(function *(path) {
        return JSON.parse(yield fs.read(path));
    });
            
Q

Patterns - Ask Pattern

It models just one question and one answer.


userService.getUsers(/*...*/)
    .then(function onSuccess() { /*...*/ })
    .error(function onFailure() { /*...*/ });
            

Patterns - Workflows


function wait(ms, value callback) {
    setTimeout(function () {
        callback(null, value);
    }, ms);
}

async.parallel([
      wait.bind(null, 200, "one"),
      wait.bind(null, 100, "two")
  ],

  function (error, results) {
      // The results array will equal [ "one", "two" ] even though
      // the second function had a shorter timeout.
  });
            
async

Task 8

Updating score and ending the game


$ cd mighty-penguins-revenge
$ git checkout tags/TASK_8
$ npm install
            

1 assignment = 10 minutes

Task 8

Updating score and ending the game


owner.commands.updateScore(session, nick, score, function (error, score) {
  if (score >= owner.settings.MaximumScore) {
    owner.commands.closeContest(session, function (error) {
      owner.commands.deleteRoom(session, function (error) {
        socket.emit("game-finished");
        socket.broadcast.to(session).emit("game-finished");
      });
    });
  }
});
            

Supervision

A supervisor is responsible for starting, stopping and monitoring its child processes.

The basic idea of a supervisor is that it should keep its child processes alive by restarting them when necessary.

Task 9

Supervisor


$ npm install -g forever
$ cd mighty-penguins-revenge
$ git checkout tags/TASK_9
$ npm install
$ forever --help
            

1 assignment = 5 minutes

Task 9

Supervisor


#!/bin/sh

forever --minUptime 1000 --spinSleepTime 1000
        -e server/logs/error.log -o server/logs/normal.log
        index.js --no-colors
            

Fun!

Task F1

Destructible terrain!

Client-Side

Task F2

More weapons!

Architecture

Task F3

Streaming game events!

Architecture

Task F4

Generated terrain!

Algorithms

Task F*


$ cd mighty-penguins-revenge
$ git checkout master
$ npm install
            

1 assignment = 20 minutes

Play!

Thank you

... for 110 commits !
... for 10 tags !
... for huge fun ;) !

Remember about survey!

Feedback? Send me an email.

Stars, pull requests and issues are welcome!

Thank you!

References :