moonslice-talk



moonslice-talk

0 7


moonslice-talk

A talk I'm giving at SenchaCon about experimental low-level APIs for node.

On Github creationix / moonslice-talk

MoonSlice

Continuing Innovation in node.js API Design

By Tim Caswell

Background

Discoveries

  • Conventions
  • Swappable Abstractions
  • Minimal Dependencies

Continuables

Simple Flow Control Primitives

A Simple Convention

var continuable = fs.readFile("myFile.txt");

continuable(function (err, data) { ... });

Composable Helpers

  • parallal(continuable, ...) → continuable
  • serial(continuable, ...) → continuable
function init(conf, description, exclude) {
  return serial(
    fs.mkdir("."),
    parallel(
      fs.mkdir("branches"),
      write("config", conf),
      write("description", description),
      serial(
        fs.mkdir("info"),
        write("info/exclude", exclude)
      )
    )
  );
}
function serial() {
  var items = Arrayslice.call(arguments);
  return function (callback) {
    check();
    function check(err) {
      if (err) return callback(err);
      var next = items.shift();
      if (!next) return callback();
      next(check);
    }
  };
}

Generators

Resumable Function Bodies

function* fib() {
  var a = 1, b = 0;
  while (true) {
    yield a
    var temp = a;
    a = a + b;
    b = temp;
  }
}
run(function* () {
  var repo = yield jsgit.repo("/path/to/repo");
  var head = yield repo.readRef("HEAD");
  var commit = yield repo.load(head);
  var tree = yield repo.load(commit.tree);
});
run(function* () {
  var names = yield fs.readdir("mydir");
  names.forEach(function (name) {
    var path = path.join("mydir/", name);
    // ERROR!!!
    var data = yield fs.readFile(path, "utf8");
    console.log(path, data);
  });
});
run(function* () {
  var names = yield fs.readdir("mydir");
  yield* each(names, function* (name) {
    var path = path.join("mydir/", name);
    var data = yield fs.readFile(path, "utf8");
    console.log(path, data);
  });
});

function* each(array, callback) {
  for (var i = 0, i < array.length; i++) {
    yield* callback(array[i]);
  }
}

Simple Streams

A Simpler Stream Interface

var stream = {
  read: function (callback) { /*...*/ },
  abort: function (callback) { /*...*/ },
};
var stream = fs.readStream("/path/to/file");
var sink = fs.writeStream("/path/to/newFile");

sink(stream);
var objectStream = parse(jsonStream);
tcp.createServer(8080, function (stream) {

  stream.sink(stream);

});

Stream Demo

State Machines

The bread and butter of protocol streams.

var decoder = JSON.createDecoder(emit);

decoder.write('{"name":"Tim');
decoder.write(' Caswell"}[1');
decoder.write(',2,3][4,5,6]');

decoder.end();
{ name: 'Tim Caswell' }
[ 1, 2, 3 ]
[ 4, 5, 6 ]
var encoder = JSON.createEncoder(console.log);

encoder.write({ name: 'Tim Caswell' });
encoder.write([ 1, 2, 3 ]);
encoder.write([ 4, 5, 6 ]);

encoder.end();
'{"name":"Tim Caswell"}'
'[1,2,3]'
'[4,5,6]'
json = JSON.stringify({name: 'Tim Caswell'});
json = JSON.stringify([1, 2, 3]);
json = JSON.stringify([4, 5, 6]);
var parser = HTTP.createParser(console.log);

parser.write('GET / HTTP/1.1\r\nHost: ');
parser.write('creationix.com\r\nConnec');
parser.write('tion: Keep-Alive\r\n\r\n');
parser.write('GET /favicon.ico HTTP/1.');
parser.write('1\r\nHost: creationix.co');
parser.write('m\r\nConnection: Close\r');
parser.write('\n\r\n');

parser.end();
{
  method: 'GET', path: '/',
  headers: [
    [ 'Host', 'creationix.com' ],
    [ 'Connection', 'Keep-Alive' ]
  ]
}
{
  method: 'GET', path: '/favicon.ico',
  headers: [
    [ 'Host', 'creationix.com' ],
    [ 'Connection', 'Close' ]
  ]
}
function transformer(emit) {

  return function (item) {

  };

}
var state = inflate(onByte, $after);

for (var i = 0; i < data.length; i++) {
  state = state(data[i]);
}
function $before(byte) {
  return inflate(onByte, $after);
}

function $after(byte) {
  return $someOtherState;
}
function hexMachine(emit) {
  var left = 4, num = 0;

  return $hex;

  function $hex(byte) {
    num |= parseHex(byte) << (--left * 4);
    if (left) return $hex;
    return emit(num);
  }

}

How do I know which style to use?

Decoupling I/O from Protocol

Reasons to Decouple

  • Tests!
  • Multi-platform web apps.
  • Less dependencies
var tcp = require('simple-tcp');
tcp.createServer(8080, function (socket) {
  // A client connected
  // socket = { read, abort, sink }
});
var tcp = require('simple-tcp');
var parser = require('http-request-parser');
var encoder = require('http-response-encoder');
tcp.createServer(8080, function (socket) {
  var httpSocket = parser(socket);
  httpSocket.sink = function (stream) {
    socket.sink(encoder(stream));
  };
});

An HTTP API

  • request-parser: (stream<binary>) → stream<request>
  • response-encoder: (stream<response>) → stream<binary>
  • request-encoder: (stream<request>) → stream<binary>
  • response-parser: (stream<binary>) → stream<response>

A Websocket API

  • decoder: (stream<binary>) → stream<message>
  • encoder: (stream<message>) → stream<binary>
  • Here "binary" means raw websocket protocol data including framing and masking.
  • "message" means the message body as a Buffer or String.

A JSON API

  • decoder: (stream<json>) → stream<object>
  • encoder: (stream<object>) → stream<json>
  • Converts between raw JSON strings or buffers and JavaScript objects.
function app(request) {
  console.log(request.method, request.path);
  return "Hello World";
}

The API

  • app(request) -> continuable<response>
  • request = { method, path, headers, body }
  • response = { code, headers, body }
  • For ease of use, lots of sugar and shortcuts are added.
test("Root returns HTML", function (assert) {
  var request = {
    method: "GET", path: "/", headers: []
  };
  var result = app(request);
  normalize(result, function (result) {
    assert.equal(result.code, 200);
    assert.end();
  });
});

Takeaways

Simple Conventions

Decouple Protocols From I/O

Use the Right Tool for the Job

Thank You!

Take the Survey