sdsds
Version:
SDSDS is a data storage system that automatically discovers peers in the same subnet (using UDP broadcast) and synchronizes (over TCP) a shared data object between them. Changes propagate to all nodes and each node acts as a redundant to the other nodes.
284 lines (236 loc) • 8.1 kB
JavaScript
var JsonSocket = require('json-socket');
var dgram = require("dgram");
var net = require("net");
var util = require("util");
var os = require("os");
module.exports = SDSDS = function(host, port, data, udpSwarmPort, debugLevel) {
var instance = this;
this.host = host || this.findFirstNetworkInterfaceAddress();
this.port = port || 9000;
this.data = data || {};
this.udpSwarmPort = udpSwarmPort || 18468;
this.debugLevel = debugLevel || "ERR";
var events = {};
this.on = function(eventName, callback) {
events[eventName] = events[eventName] || [];
events[eventName].push(callback);
};
this.fire = function(eventName, _) {
var args = Array.prototype.slice.call(arguments, 1);
instance.debug("DBG", "firing event: " + eventName);
if(!events[eventName])
return;
for(var i = 0, length = events[eventName].length; i < length; i++)
events[eventName][i].apply(instance, args);
}
this.hostInformation = {
name: this.host + ":" + this.port,
host: this.host,
port: this.port
};
this.hosts = {};
this.connectedOutNodes = {};
this.connectedInNodes = {};
this.udp4 = dgram.createSocket("udp4");
this.udp4.on('error', function(err) {
instance.error(err);
});
this.udp4.on('close', function() {
instance.close();
});
this.udp4.on('message', function(incoming_msg, rinfo) {
try {
var msg = JSON.parse(incoming_msg);
if(msg.host == instance.hostInformation.host && msg.port == instance.hostInformation.port)
return; // This is a message to ourselves!
instance.debug("ALL", "udp message: " + incoming_msg);
instance.connectToSwarmNode(msg);
} catch(e) { }
});
this.udp4.on('listening', function() {
var address = instance.udp4.address();
instance.debug("DBG", "udp server listening " + address.address + ":" + address.port);
setInterval(instance.sendHostBeacon.bind(instance), 5000);
});
this.udp4.bind(this.udpSwarmPort, function() {
instance.udp4.setBroadcast(true);
instance.tcp4 = net.createServer(function(socket) {
socket = new JsonSocket(socket);
socket.sendMessage({type: "handshake_start"});
socket.on('error', function(err) { console.log("tcp4server - error", err); });
socket.on('message', function(data) {
instance.processInHandshake(socket, data);
});
}).listen(instance.hostInformation.port, undefined, undefined, function() {
instance.debug("DBG", "tcp server listening " + instance.hostInformation.port);
instance.fire('listening');
});
});
};
SDSDS.prototype.findFirstNetworkInterfaceAddress = function() {
var ifaces = os.networkInterfaces();
for(var iface in ifaces)
for(var d in ifaces[iface])
if(ifaces[iface][d].family == "IPv4")
return ifaces[iface][d].address;
return "127.0.0.1";
};
SDSDS.prototype.debug = function(level, message) {
var instance = this;
var levels = ["ERR", "WRN", "DBG", "ALL"];
if(levels.indexOf(level) > levels.indexOf(this.debugLevel)) return;
console.log(message);
};
SDSDS.prototype.error = function(err) {
var instance = this;
this.debug("ERR", err);
this.close();
this.fire("error", err);
};
SDSDS.prototype.close = function() {
var instance = this;
if(this.udp4) this.udp4.close();
if(this.tcp4) this.tcp4.close();
this.fire("close");
};
SDSDS.prototype.sendHostBeacon = function() {
var instance = this;
if(!this.udp4 || !this.tcp4) return;
var message = new Buffer(JSON.stringify(this.hostInformation));
this.udp4.send(message, 0, message.length, this.udpSwarmPort, "255.255.255.255", function(err, bytes) {
if(err) instance.error(err);
instance.debug("ALL", "udp broadcast beacon sent");
});
};
SDSDS.prototype.connectToSwarmNode = function(nodeInformation) {
var instance = this;
if(this.connectedOutNodes[nodeInformation.name]) return;
this.debug("DBG", "connecting to new node: " + nodeInformation.name);
var socket = new net.Socket();
socket.connect(nodeInformation.port, nodeInformation.host, function() {
socket = new JsonSocket(socket);
socket.name = nodeInformation.name;
nodeInformation.socket = socket;
instance.connectedOutNodes[socket.name] = {
name: nodeInformation.name,
host: nodeInformation.host,
port: nodeInformation.port,
socket: socket
};
});
socket.on('message', function(data) {
console.log("message", data);
instance.processOut(instance.connectedOutNodes[socket.name], socket, data);
});
socket.on('error', function(err) { console.log("tcp4client - error", err); });
socket.on('close', function() {
instance.fire('disconnected', instance.connectedOutNodes[socket.name]);
delete instance.connectedOutNodes[socket.name];
});
};
SDSDS.prototype.processOut = function(node, socket, data) {
var instance = this;
this.debug("ALL", "> recieved handshake data from " + socket.name + ", " + data);
if(!data.type) return;
switch(data.type) {
case "handshake_start":
socket.sendMessage({
type: "host_details",
host: instance.hostInformation
});
return;
case "sync":
this.processIncomingSync(node, data);
socket.sendMessage({type: "handshake_end"});
instance.fire('connected', node);
return
}
};
SDSDS.prototype.processInHandshake = function(socket, data) {
var instance = this;
this.debug("ALL", "< recieved handshake data from " + (socket.name ? socket.name : "unnamed socket") + ", " + data);
if(!data.type) return;
switch(data.type) {
case "host_details":
socket.name = data.host.name;
instance.connectedInNodes[socket.name] = {
name: socket.name,
host: data.host.host,
port: data.host.port,
socket: socket
};
socket.sendMessage({
type: "sync",
data: instance.data
});
socket.on('error', function(err) {
console.log("tcp4client - error", err);
delete instance.connectedInNodes[socket.name];
instance.fire('left', socket.name);
});
instance.fire('joined', socket.name);
return;
case "handshake_end":
socket._socket.removeAllListeners('message');
socket.on('message', function(data) {
instance.processIn(socket, data);
});
instance.debug("DBG", "connected to swarm node: " + socket.name);
return;
}
};
SDSDS.prototype.processIn = function(socket, data) {
var instance = this;
var node = this.connectedInNodes[socket.name];
if(!node) return;
this.debug("ALL", "< recieved data from " + socket.name + ", " + data);
if(!data.type) return;
switch(data.type) {
case "sync":
return this.processIncomingSync(node, data);
case "write":
return this.processIncomingWrite(node, data);
}
};
SDSDS.prototype.processIncomingSync = function(node, data) {
this.debug("ALL", "syncing data with node " + node.name);
for(var key in data.data) {
data.data[key].timestamp = Date.now();
if(this.data[key] === undefined) {
this.data[key] = data.data[key];
} else if(this.data[key].timestamp < data.data[key].timestamp) {
this.data[key] = data.data[key];
}
}
this.fire('data_sync', node, this.data);
};
SDSDS.prototype.processIncomingWrite = function(node, data) {
this.debug("ALL", "writing data from node " + node.name);
if(this.data[data.key] === undefined) {
this.data[data.key] = data.value;
} else if(this.data[data.key].timestamp < data.value.timestamp) {
this.data[data.key] = data.value;
}
this.fire('data_write', node, data.key, data.value);
};
SDSDS.prototype.get = function(key) {
var instance = this;
return this.data[key];
};
SDSDS.prototype.set = function(key, value) {
var instance = this;
this.data[key] = value;
this.data[key].timestamp = Date.now();
for(var node in this.connectedOutNodes) {
this.connectedOutNodes[node].socket.sendMessage({
type: "write",
key: key,
value: this.data[key]
});
}
return true;
};
SDSDS.prototype.getAll = function(data) {
var instance = this;
return this.data;
};