zeromq
Version:
ZeroMQ for node.js
856 lines (711 loc) • 19.4 kB
JavaScript
/**
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter
, zmq = require('../binding')
, util = require('util');
/**
* Expose bindings as the module.
*/
exports = module.exports = zmq;
/**
* Expose zmq version.
*/
exports.version = zmq.zmqVersion();
/**
* Expose zmq_curve_keypair
*/
exports.curveKeypair = zmq.zmqCurveKeypair;
/**
* Map of socket types.
*/
var types = exports.types = {
pub: zmq.ZMQ_PUB
, xpub: zmq.ZMQ_XPUB
, sub: zmq.ZMQ_SUB
, xsub: zmq.ZMQ_XSUB
, req: zmq.ZMQ_REQ
, xreq: zmq.ZMQ_XREQ
, rep: zmq.ZMQ_REP
, xrep: zmq.ZMQ_XREP
, push: zmq.ZMQ_PUSH
, pull: zmq.ZMQ_PULL
, dealer: zmq.ZMQ_DEALER
, router: zmq.ZMQ_ROUTER
, pair: zmq.ZMQ_PAIR
, stream: zmq.ZMQ_STREAM
};
var longOptions = {
ZMQ_HWM: 1
, ZMQ_SWAP: 3
, ZMQ_AFFINITY: 4
, ZMQ_IDENTITY: 5
, ZMQ_SUBSCRIBE: 6
, ZMQ_UNSUBSCRIBE: 7
, ZMQ_RATE: 8
, ZMQ_RECOVERY_IVL: 9
, ZMQ_MCAST_LOOP: 10
, ZMQ_SNDBUF: 11
, ZMQ_RCVBUF: 12
, ZMQ_RCVMORE: 13
, ZMQ_FD: 14
, ZMQ_EVENTS: 15
, ZMQ_TYPE: 16
, ZMQ_LINGER: 17
, ZMQ_RECONNECT_IVL: 18
, ZMQ_BACKLOG: 19
, ZMQ_RECOVERY_IVL_MSEC: 20
, ZMQ_RECONNECT_IVL_MAX: 21
, ZMQ_MAXMSGSIZE: 22
, ZMQ_SNDHWM: 23
, ZMQ_RCVHWM: 24
, ZMQ_MULTICAST_HOPS: 25
, ZMQ_RCVTIMEO: 27
, ZMQ_SNDTIMEO: 28
, ZMQ_IPV4ONLY: 31
, ZMQ_LAST_ENDPOINT: 32
, ZMQ_ROUTER_MANDATORY: 33
, ZMQ_TCP_KEEPALIVE: 34
, ZMQ_TCP_KEEPALIVE_CNT: 35
, ZMQ_TCP_KEEPALIVE_IDLE: 36
, ZMQ_TCP_KEEPALIVE_INTVL: 37
, ZMQ_TCP_ACCEPT_FILTER: 38
, ZMQ_DELAY_ATTACH_ON_CONNECT: 39
, ZMQ_XPUB_VERBOSE: 40
, ZMQ_ROUTER_RAW: 41
, ZMQ_IPV6: 42
, ZMQ_MECHANISM: 43
, ZMQ_PLAIN_SERVER: 44
, ZMQ_PLAIN_USERNAME: 45
, ZMQ_PLAIN_PASSWORD: 46
, ZMQ_CURVE_SERVER: 47
, ZMQ_CURVE_PUBLICKEY: 48
, ZMQ_CURVE_SECRETKEY: 49
, ZMQ_CURVE_SERVERKEY: 50
, ZMQ_ZAP_DOMAIN: 55
, ZMQ_HEARTBEAT_IVL: 75
, ZMQ_HEARTBEAT_TTL: 76
, ZMQ_HEARTBEAT_TIMEOUT: 77
, ZMQ_CONNECT_TIMEOUT: 79
, ZMQ_IO_THREADS: 1
, ZMQ_MAX_SOCKETS: 2
, ZMQ_ROUTER_HANDOVER: 56
};
Object.keys(longOptions).forEach(function(name){
Object.defineProperty(zmq, name, {
enumerable: true,
configurable: false,
writable: false,
value: longOptions[name]
});
});
/**
* Map of socket options.
*/
var opts = exports.options = {
_fd: zmq.ZMQ_FD
, _ioevents: zmq.ZMQ_EVENTS
, _receiveMore: zmq.ZMQ_RCVMORE
, _subscribe: zmq.ZMQ_SUBSCRIBE
, _unsubscribe: zmq.ZMQ_UNSUBSCRIBE
, affinity: zmq.ZMQ_AFFINITY
, backlog: zmq.ZMQ_BACKLOG
, hwm: zmq.ZMQ_HWM
, identity: zmq.ZMQ_IDENTITY
, linger: zmq.ZMQ_LINGER
, mcast_loop: zmq.ZMQ_MCAST_LOOP
, rate: zmq.ZMQ_RATE
, rcvbuf: zmq.ZMQ_RCVBUF
, last_endpoint: zmq.ZMQ_LAST_ENDPOINT
, reconnect_ivl: zmq.ZMQ_RECONNECT_IVL
, recovery_ivl: zmq.ZMQ_RECOVERY_IVL
, sndbuf: zmq.ZMQ_SNDBUF
, swap: zmq.ZMQ_SWAP
, mechanism: zmq.ZMQ_MECHANISM
, plain_server: zmq.ZMQ_PLAIN_SERVER
, plain_username: zmq.ZMQ_PLAIN_USERNAME
, plain_password: zmq.ZMQ_PLAIN_PASSWORD
, curve_server: zmq.ZMQ_CURVE_SERVER
, curve_publickey: zmq.ZMQ_CURVE_PUBLICKEY
, curve_secretkey: zmq.ZMQ_CURVE_SECRETKEY
, curve_serverkey: zmq.ZMQ_CURVE_SERVERKEY
, zap_domain: zmq.ZMQ_ZAP_DOMAIN
, heartbeat_ivl: zmq.ZMQ_HEARTBEAT_IVL
, heartbeat_ttl: zmq.ZMQ_HEARTBEAT_TTL
, heartbeat_timeout: zmq.ZMQ_HEARTBEAT_TIMEOUT
, connect_timeout: zmq.ZMQ_CONNECT_TIMEOUT
};
/**
* Monitor events
*/
var events = exports.events = {
1: "connect" // zmq.ZMQ_EVENT_CONNECTED
, 2: "connect_delay" // zmq.ZMQ_EVENT_CONNECT_DELAYED
, 4: "connect_retry" // zmq.ZMQ_EVENT_CONNECT_RETRIED
, 8: "listen" // zmq.ZMQ_EVENT_LISTENING
, 16: "bind_error" // zmq.ZMQ_EVENT_BIND_FAILED
, 32: "accept" // zmq.ZMQ_EVENT_ACCEPTED
, 64: "accept_error" // zmq.ZMQ_EVENT_ACCEPT_FAILED
, 128: "close" // zmq.ZMQ_EVENT_CLOSED
, 256: "close_error" // zmq.ZMQ_EVENT_CLOSE_FAILED
, 512: "disconnect" // zmq.ZMQ_EVENT_DISCONNECTED
}
// Context management happens here. We lazily initialize a default context,
// and use that everywhere. Also cleans up on exit.
var ctx;
function defaultContext() {
if (ctx) return ctx;
var io_threads = 1;
if (process.env.ZMQ_IO_THREADS) {
io_threads = parseInt(process.env.ZMQ_IO_THREADS, 10);
if (!io_threads || io_threads < 1) {
console.warn('Invalid number in ZMQ_IO_THREADS, using 1 IO thread.');
io_threads = 1;
}
}
ctx = new zmq.Context(io_threads);
process.on('exit', function(){
// ctx.close();
ctx = null;
});
return ctx;
};
/**
* A batch consists of 1 or more message parts with their flags that need to be sent as one unit
*/
function OutBatch() {
this.content = []; // buf, flags, buf, flags, ...
this.cbs = []; // callbacks
this.isClosed = false; // true if the last message does not have SNDMORE in its flags, false otherwise
this.next = null; // next batch (for linked list of batches)
}
OutBatch.prototype.append = function (buf, flags, cb) {
if (!Buffer.isBuffer(buf)) {
buf = Buffer.from(String(buf), 'utf8');
}
this.content.push(buf, flags);
if (cb) {
this.cbs.push(cb);
}
if ((flags & zmq.ZMQ_SNDMORE) === 0) {
this.isClosed = true;
}
};
OutBatch.prototype.invokeError = function (socket, error) {
var returned = false;
for (var i = 0; i < this.cbs.length; i += 1) {
this.cbs[i].call(socket, error);
returned = true;
}
if (!returned) {
throw error;
}
};
OutBatch.prototype.invokeSent = function (socket) {
for (var i = 0; i < this.cbs.length; i += 1) {
this.cbs[i].call(socket);
}
};
function BatchList() {
this.firstBatch = null;
this.lastBatch = null;
this.length = 0;
}
BatchList.prototype.canSend = function () {
return this.firstBatch ? this.firstBatch.isClosed : false;
};
BatchList.prototype.append = function (buf, flags, cb) {
var batch = this.lastBatch;
if (!batch || batch.isClosed) {
batch = new OutBatch();
if (this.lastBatch) {
this.lastBatch.next = batch;
}
this.lastBatch = batch;
if (!this.firstBatch) {
this.firstBatch = batch;
}
this.length += 1;
}
batch.append(buf, flags, cb);
};
BatchList.prototype.fetch = function () {
var batch = this.firstBatch;
if (batch && batch.isClosed) {
this.firstBatch = batch.next;
this.length -= 1;
return batch;
}
return undefined;
};
BatchList.prototype.restore = function (batch) {
this.firstBatch = batch;
this.length += 1;
};
/**
* Create a new socket of the given `type`.
*
* @constructor
* @param {String|Number} type
* @api public
*/
var Socket =
exports.Socket = function (type) {
var self = this;
EventEmitter.call(this);
this.type = type;
this._zmq = new zmq.SocketBinding(defaultContext(), types[type]);
this._paused = false;
this._isFlushingReads = false;
this._isFlushingWrites = false;
this._outgoing = new BatchList();
this._zmq.onReadReady = function () {
setImmediate(function(){
self._flushReads();
});
};
this._zmq.onSendReady = function () {
self._flushWrites();
};
};
/**
* Inherit from `EventEmitter.prototype`.
*/
util.inherits(Socket, EventEmitter);
/**
* Set socket to pause mode
* no data will be emit until resume() is called
* all send() calls will be queued
*
* @api public
*/
Socket.prototype.pause = function() {
this._paused = true;
}
/**
* Set a socket back to normal work mode
*
* @api public
*/
Socket.prototype.resume = function() {
this._paused = false;
this._flushReads();
this._flushWrites();
}
Socket.prototype.ref = function() {
this._zmq.ref();
}
Socket.prototype.unref = function() {
this._zmq.unref();
}
Socket.prototype.read = function() {
var message = [], flags;
if (this._zmq.state !== zmq.STATE_READY) {
return null;
}
flags = this._zmq.getsockopt(zmq.ZMQ_EVENTS);
if (flags & zmq.ZMQ_POLLIN) {
do {
message.push(this._zmq.recv());
} while (this._zmq.getsockopt(zmq.ZMQ_RCVMORE));
return message;
}
return null;
}
/**
* Set `opt` to `val`.
*
* @param {String|Number} opt
* @param {Mixed} val
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.setsockopt = function(opt, val){
this._zmq.setsockopt(opts[opt] || opt, val);
return this;
};
/**
* Get socket `opt`.
*
* @param {String|Number} opt
* @return {Mixed}
* @api public
*/
Socket.prototype.getsockopt = function(opt){
return this._zmq.getsockopt(opts[opt] || opt);
};
/**
* Socket opt accessors allowing `sock.backlog = val`
* instead of `sock.setsockopt('backlog', val)`.
*/
Object.keys(opts).forEach(function(name){
Socket.prototype.__defineGetter__(name, function() {
return this._zmq.getsockopt(opts[name]);
});
Socket.prototype.__defineSetter__(name, function(val) {
if ('string' == typeof val) val = Buffer.from(val, 'utf8');
return this._zmq.setsockopt(opts[name], val);
});
});
/**
* Return true if socket state is closed.
*/
Socket.prototype.__defineGetter__("closed", function() {
return this._zmq.state === zmq.STATE_CLOSED;
});
/**
* Async bind.
*
* Emits the "bind" event.
*
* @param {String} addr
* @param {Function} cb
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.bind = function(addr, cb) {
var self = this;
this._zmq.bind(addr, function(err) {
if (err) {
return cb && cb(err);
}
self._flushReads();
self._flushWrites();
self.emit('bind', addr);
cb && cb();
});
return this;
};
/**
* Sync bind.
*
* @param {String} addr
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.bindSync = function(addr) {
this._zmq.bindSync(addr);
return this;
};
/**
* Async unbind.
*
* Emits the "unbind" event.
*
* @param {String} addr
* @param {Function} cb
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.unbind = function(addr, cb) {
if (zmq.ZMQ_CAN_UNBIND) {
var self = this;
this._zmq.unbind(addr, function(err) {
if (err) {
return cb && cb(err);
}
self.emit('unbind', addr);
self._flushReads();
self._flushWrites();
cb && cb();
});
} else {
cb && cb();
}
return this;
};
/**
* Sync unbind.
*
* @param {String} addr
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.unbindSync = function(addr) {
if (zmq.ZMQ_CAN_UNBIND) {
this._zmq.unbindSync(addr);
}
return this;
}
/**
* Connect to `addr`.
*
* @param {String} addr
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.connect = function(addr) {
this._zmq.connect(addr);
return this;
};
/**
* Disconnect from `addr`.
*
* @param {String} addr
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.disconnect = function(addr) {
if (zmq.ZMQ_CAN_DISCONNECT) {
this._zmq.disconnect(addr);
}
return this;
};
/**
* Enable monitoring of a Socket
*
* @param {Number} timer interval in ms > 0 or Undefined for default
* @param {Number} The maximum number of events to read on each interval, default is 1, use 0 for reading all events
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.monitor = function(interval, numOfEvents) {
if (zmq.ZMQ_CAN_MONITOR) {
var self = this;
self._zmq.onMonitorEvent = function(event_id, event_value, event_endpoint_addr, ex) {
self.emit(events[event_id], event_value, event_endpoint_addr, ex);
}
self._zmq.onMonitorError = function(error) {
self.emit('monitor_error', error);
}
this._zmq.monitor(interval, numOfEvents);
} else {
throw new Error('Monitoring support disabled check zmq version is > 3.2.1 and recompile this addon');
}
return this;
};
/**
* Disable monitoring of a Socket release idle handler
* and close the socket
*
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.unmonitor = function() {
if (zmq.ZMQ_CAN_MONITOR) {
this._zmq.unmonitor();
}
return this;
};
/**
* Subscribe with the given `filter`.
*
* @param {String} filter
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.subscribe = function(filter) {
this._subscribe = filter;
return this;
};
/**
* Unsubscribe with the given `filter`.
*
* @param {String} filter
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.unsubscribe = function(filter) {
this._unsubscribe = filter;
return this;
};
/**
* Send the given `msg`.
*
* @param {String|Buffer|Array} msg
* @param {Number} [flags]
* @param {Function} [cb]
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.send = function(msg, flags, cb) {
flags = flags | 0;
if (Array.isArray(msg)) {
for (var i = 0, len = msg.length; i < len; i++) {
var isLast = i === len - 1;
var msgFlags = isLast ? flags : flags | zmq.ZMQ_SNDMORE;
var callback = isLast ? cb : undefined;
this._outgoing.append(msg[i], msgFlags, callback);
}
} else {
this._outgoing.append(msg, flags, cb);
}
if (this._outgoing.canSend()) {
this._zmq.pending = true;
this._flushWrites();
} else {
this._zmq.pending = false;
}
return this;
};
Socket.prototype._emitMessage = function (message) {
if (message.length === 1) {
// hot path
this.emit('message', message[0]);
} else {
this.emit.apply(this, ['message'].concat(message));
}
}
Socket.prototype._flushRead = function () {
try {
var message = this._zmq.readv(); // can throw
if (!message) {
return false;
}
// Handle received message immediately to prevent memory leak in driver
this._emitMessage(message)
} catch (error) {
this.emit('error', error); // can throw
}
return true;
};
Socket.prototype._flushWrite = function () {
var batch = this._outgoing.fetch();
if (!batch) {
this._zmq.pending = false;
return false;
}
try {
if (this._zmq.sendv(batch.content)) {
this._zmq.pending = this._outgoing.canSend();
batch.invokeSent(this);
return true;
}
this._outgoing.restore(batch);
return false;
} catch (sendError) {
this._zmq.pending = this._outgoing.canSend();
batch.invokeError(this, sendError); // can throw
return false;
}
};
Socket.prototype._flushReads = function() {
if (this._paused || this._isFlushingReads) return;
this._isFlushingReads = true;
while (this._flushRead());
this._isFlushingReads = false;
// if many sends happened, but ended up in the queue (eg. in a req/rep scenario where each send must be followed by a
// response), we can try to send again now
this._flushWrites();
};
Socket.prototype._flushWrites = function() {
if (this._paused || this._isFlushingWrites) return;
this._isFlushingWrites = true;
var sent;
do {
try {
sent = this._flushWrite();
} catch (error) {
this._isFlushingWrites = false;
this.emit('error', error); // can throw
return;
}
} while (sent);
this._isFlushingWrites = false;
};
/**
* Close the socket.
*
* @return {Socket} for chaining
* @api public
*/
Socket.prototype.close = function() {
this._zmq.close();
return this;
};
/**
* Create a `type` socket with the given `options`.
*
* @param {String} type
* @param {Object} options
* @return {Socket}
* @api public
*/
exports.socket =
exports.createSocket = function(type, options) {
var sock = new Socket(type);
for (var key in options) sock[key] = options[key];
return sock;
};
exports.Context.setMaxThreads = function(value) {
if (!zmq.ZMQ_CAN_SET_CTX) {
throw new Error('Setting of context options disabled, check zmq version is >= 3.2.1 and recompile this addon');
}
var defaultCtx = defaultContext();
defaultCtx.setOpt(zmq.ZMQ_IO_THREADS, value);
};
exports.Context.getMaxThreads = function() {
if (!zmq.ZMQ_CAN_SET_CTX) {
throw new Error('Getting of context options disabled, check zmq version is >= 3.2.1 and recompile this addon');
}
var defaultCtx = defaultContext();
return defaultCtx.getOpt(zmq.ZMQ_IO_THREADS);
};
exports.Context.setMaxSockets = function(value) {
if (!zmq.ZMQ_CAN_SET_CTX) {
throw new Error('Setting of context options disabled, check zmq version is >= 3.2.1 and recompile this addon');
}
var defaultCtx = defaultContext();
defaultCtx.setOpt(zmq.ZMQ_MAX_SOCKETS, value);
};
exports.Context.getMaxSockets = function() {
if (!zmq.ZMQ_CAN_SET_CTX) {
throw new Error('Getting of context options disabled, check zmq version is >= 3.2.1 and recompile this addon');
}
var defaultCtx = defaultContext();
return defaultCtx.getOpt(zmq.ZMQ_MAX_SOCKETS);
};
/**
* JS based on API characteristics of the native zmq_proxy()
*/
function proxy (frontend, backend, capture){
switch(frontend.type+'/'+backend.type){
case 'push/pull':
case 'pull/push':
case 'xpub/xsub':
if(capture){
frontend.on('message',function (){
backend.send([].slice.call(arguments));
});
backend.on('message',function (){
frontend.send([].slice.call(arguments));
//forwarding messages over capture socket
capture.send([].slice.call(arguments));
});
} else {
//no capture socket provided, just forwarding msgs to respective sockets
frontend.on('message',function (){
backend.send([].slice.call(arguments));
});
backend.on('message',function (){
frontend.send([].slice.call(arguments));
});
}
break;
case 'router/dealer':
case 'xrep/xreq':
if(capture){
//forwarding router/dealer pack signature: id, delimiter, msg
frontend.on('message',function (id,delimiter,msg){
backend.send([].slice.call(arguments));
});
backend.on('message',function (id,delimiter,msg){
frontend.send([].slice.call(arguments));
//forwarding message to the capture socket
capture.send([].slice.call(arguments, 2));
});
} else {
//forwarding router/dealer signatures without capture
frontend.on('message',function (id,delimiter,msg){
backend.send([].slice.call(arguments));
});
backend.on('message',function (id,delimiter,msg){
frontend.send([].slice.call(arguments));
});
}
break;
default:
throw new Error('wrong socket order to proxy');
}
}
exports.proxy = proxy;