node package manager

architect-agent

This module is the actual serialization format and protocol for the remoteagent system.

It is transport agnostic so that it can work on any duplex socket. A modified version of msgpack is used as the serialization format when using the binary socket transport (undefined and Buffer types are added). Other transports such as socket.io in the browser can be used.

Encoded on top of the serialzation format is functions and cycles. This almost any basic value can be encoded and sent across the socket.

Since functions can be serialized, rpc using callbacks is natural. Simply pass your callback as an argument and the other side will get a proxy function when it's deserialized. When they call that proxy function, a message will be sent back and your callback will get called with the deserialized arguments (which can include yet another callback). Since the callbacks are executed on their native side, closure variables and all other state is preserved. Callbacks functions may only be called once. The internal reference is deleted after the first call. For functions that are called multiple times, use the named functions map passed into the Agent.

Message Encoding

On top of serializing primitive values and basic data structures, remoteagent- protocol can encode proxy functions and cycles in an object. This is encoded using objects with the $ magic key. Any existing keys that start with $ will be escaped by prefixing an extra $.

Function Encoding

Functions are encoded with the $ key. The value of this object is the unique function index in the local function repository. Function keys integers. An example encoded function can look like {$: 3} where callbacks[3] in the server is the real function.

Cycle Encoding

Sometimes objects have cycles in them. It would be nice if these could be encoded, serialized, and send to the other side intact without blowing up the rpc system. Cycles are encoded with the $ key. The value is the path to the actual value as an array of strings. In this way it works like a file- system symlink. Currently the path is absolute starting at the root of the message. For example. Given the following cyclic object:

var entry = {
  name: "Bob",
  boss: { name: "Steve" }
};
entry.self = entry;
entry.manager = entry.boss;

The following encoded object is generated by the internal freeze function in protocol.serializer().

{
  name: 'Bob',
  boss: { name: 'Steve' },
  self: { $: [] },
  manager: { $: [ 'boss' ] }
}

See that the path [] point to the object itself, and ['boss'] points to the boss property in the root.

Agents

The main public interface is the Agent class. There is typically one of these per network node (process) in a remoteagent mesh. The agent holds the named functions that this node serves to the other nodes.

var Agent = require('architect-agent').Agent;
 
var agent = new Agent({
    add: function (a, b, callback) {
        callback(+ b);
    }
});

In this example, we created a new agent, gave it the ID "main" and declared that it provides an add function.

Transports

Transports handle internally the serialization of static objects. This can be JSON or msgpack or something else.

Transports must have a .send() property for sending messages to the other side and emit "message" events (or call their onMessage property) whenever a message arrives from the other side. The message is an object not a json string or msgpack buffer. The transport is not responsible for encoding functions and cycles, that is done at a higher level.

When using the built-in socket-transport, messages are framed in the stream using a 4 byte length header (UInt32BE) before every message. This way the receiving end knows how much buffer to allocate and can efficiently scan and deframe the incoming message stream. This also means that the msgpack parser can assume it has the entire message in memory once the message emit from the deframer.

Here is an example networkserver that accepts remoteagent connections over a tcp port. We are assuming the agent variable declared in the sample above.

var net = require('net');
var socketTransport = require('architect-socket-transport');
 
net.createServer(function (socket) {
    agent.attach(socketTransport(socket), function (client) {
        // Do something with client if it has functions to serve 
    });
}).listen(1337);

Then to connect to this, from the client connect to this server.

var net = require('net');
var socketTransport = require('architect-socket-transport');
 
var Agent = require('architect-agent').Agent;
 
var agent = new Agent();
 
var client = net.connect(1337, function () {
    agent.attach(socketTransport(client), function (server) {
        // Use server's exported functions 
        server.add(1, 2, function (result) {
            // result should be 3! 
        });
    });
});