Actorify

TJ Holowaychuk
3 min readNov 13, 2013

--

The actorify module allows you to turn any duplex stream into an “actor” for simple & efficient bi-directional messaging.

The API

Actors are instantiated with a duplex stream, so features such as reconnection logic and multiplexing are delegated to third-party.

After an actor is created you may send messages back via the .send() method which takes a variable number of arguments, but requires the initial “message” type string. An actor is also an Emitter, so you can then listen for messages using all of the normal Emitter methods you’re used to.

var net = require('net');
var actorify = require('actorify');

// server

net.createServer(function(sock){
var actor = actorify(sock);

actor.on('ping', function(){
console.log('PING');
actor.send('pong');
});
}).listen(3000);
// client

var
sock = net.connect(3000);
var actor = actorify(sock);
setInterval(function(){
actor.send('ping');
actor.once('pong', function(){
console.log('PONG');
});
}, 300);

Actorify also provides callback mapping for request/response style messaging out of the box, here’s a simple example:

actor.send(‘get user’, ‘tobi’, function(err, user){

});

You can also timeout messages, discarding future responses:

actor.send(‘hello’, function(err, res){

}).timeout(‘5s’);

What About RPC?

Actors provide a simple primitive similar to RPC, but as a small unit — the “actor”. Traditional RPC can be easily implemented on top of actors, however the isolated actor can be very useful for maintaining state between a conversation with another actor.

Actors shine in cases where the requester expects multiple responses. For example suppose you send a request to produce N thumbnails for an image, with traditional RPC you might choose to wait until they’re all completed and then respond with the URLs — with actors you can simply send messages as the thumbnails are produced.

The requesting end of this use-case might look something like this:

 actor.send(‘image thumbnails’, img, [‘150x150', ‘300x300']);

actor.on(‘thumb’, function(size, img){
console.log(‘thumb: %s’, size);
});

Followed by the receiving end producing the thumbnails:

actor.on('image thumbnails', function(img, sizes){
sizes.forEach(function(size){
resize(img, size, function(buffer){
actor.send('thumb', size, buffer);
});
});
});

The Protocol

Actorify sits on top of a super simple protocol called AMP which supports opaque binary arguments. Actorify itself serializes arguments as JSON unless a Buffer is used. Optimizations are used internally to special-case certain primitives such as strings which are not passed through JSON.stringify().

The protocol and serialization process is very simple, so it would be trivial to implement in other languages if cross-language communication is necessary.

The v1.0 of AMP begins with a single octet dedicated to the protocol version and the number of arguments to follow. Each argument is then paired with a 32-bit BE unsigned int to indicate the length of argument data following it.

      0        1 2 3 4    <length> ...
+------------+----------+------------+
| <ver/argc> | <length> | <data> | additional arguments
+------------+----------+------------+

Isolation

Unfortunately with node it’s non-trivial to provide true isolation in a performant way, so that’s one aspect of actors that isn’t really tackled here. For this reason among others you could say it’s not a true actor implementation, and you’d be right! It’s really more like CSP. Perhaps in the future :D

That’s it for now! The project is very new and we’ll still need to find / write some stream-level logic for load balancing between receivers, queueing, and re-connection.

--

--