node-serf
Version:
Serf management and orchestration
275 lines (225 loc) • 6.77 kB
JavaScript
const net = require('net')
const debug = require('debug')('serf')
const msgpack = require('msgpack-lite')
const Stream = require('./stream').Stream
const util = require('util')
function camelize (str) {
if (str === null) str = ''
return str.trim().replace(/[-_\s]+(.)?/g, function (match, c) {
return c === null ? '' : c.toUpperCase()
})
}
function expectBody (seq) {
return seq % 3 === 0
}
function isStream (seq) {
return (seq - 2) % 3 === 0
}
let ids = 0
function Serf (arg1) {
if (!(this instanceof Serf)) {
throw new Error('Class constructor cannot be invoked without "new"')
}
net.Socket.call(this, arg1)
const _this = this
this._id = ids++
// Sequence controls the type of respond handling
this._seqBody = 0 // 3, 6, 9...
this._seqNoBody = 1 // 4, 7, 10...
this._seqStream = 2 // 5, 8, 11...
// Map of sequences that are in body phase
_this._bodyPhaseStreamSeqs = {}
function isStreamInBodyPhase (seq) {
return isStream(seq) && _this._bodyPhaseStreamSeqs[seq]
}
this._next = null
const decoder = msgpack.createDecodeStream()
this.pipe(decoder)
decoder.on('data', function (obj) {
debug('[%j] received %j', _this._id, obj)
const Seq = obj.Seq
if (Seq !== undefined) {
// Header
if ((obj.Error !== null && obj.Error !== undefined) && obj.Error !== '') {
const err = new Error(obj.Error)
return _this.emit(Seq, err)
}
if (expectBody(Seq) || isStreamInBodyPhase(Seq)) {
_this._next = Seq
} else {
if (isStream(Seq)) _this._bodyPhaseStreamSeqs[Seq] = true
_this.emit(Seq, null)
}
} else {
// Body
_this.emit(_this._next, null, obj)
}
})
this.once('end', function () {
return debug('[%j] disconnected', _this._id)
})
const commands = [
{ name: 'handshake', hasResponse: false },
{ name: 'auth', hasResponse: false },
{ name: 'event', hasResponse: false },
{ name: 'force-leave', hasResponse: false },
{ name: 'join', hasResponse: true },
{ name: 'members', hasResponse: true },
{ name: 'members-filtered', hasResponse: true },
{ name: 'tags', hasResponse: false },
{ name: 'stop', hasResponse: false },
{ name: 'respond', hasResponse: false },
{ name: 'install-key', hasResponse: true },
{ name: 'use-key', hasResponse: true },
{ name: 'remove-key', hasResponse: true },
{ name: 'list-keys', hasResponse: true },
{ name: 'stats', hasResponse: true },
{ name: 'get-coordinate', hasResponse: true }
]
commands.forEach(function (command) {
const commandName = command.name
const hasResponse = command.hasResponse
_this[commandName] = _this[camelize(commandName)] = function (body, cb) {
return _this.send(commandName, hasResponse, body, cb)
}
})
const streamingCommands = [
{ name: 'stream' },
{ name: 'monitor' },
{ name: 'query' }
]
streamingCommands.forEach(function (command) {
const commandName = command.name
_this[commandName] = _this[camelize(commandName)] = function (body, cb) {
return _this.sendStream(commandName, body, cb)
}
})
}
util.inherits(Serf, net.Socket)
Serf.prototype.leave = function () {
const Seq = this._seqNoBody += 2
const header = {
Command: 'leave',
Seq: Seq
}
this.end(msgpack.encode(header))
}
Serf.prototype.sendStream = function (Command, body, cb) {
const Seq = this._seqStream += 3
const header = {
Command: Command,
Seq: Seq
}
const stream = new Stream(this, Seq)
const ondata = function ondata (err, result) {
if (err) {
stream.emit('error', err)
} else {
stream.emit('data', result)
if (Command === 'query' && result.Type === 'done') {
stream.emit('stop')
}
}
}
// Call the listen callback/emit 'listen' first, then bind 'ondata' instead.
this.once(Seq, function (err) {
stream.emit('listen', err)
this.on(Seq, ondata)
if (typeof cb === 'function') cb(err)
})
const _this = this
stream.once('stop', function () {
if (typeof cb === 'function') {
_this.removeListener(Seq, cb)
}
stream.removeListener('data', ondata)
delete _this._bodyPhaseStreamSeqs[Seq]
})
dowrite(this, header, body)
return stream
}
Serf.prototype.send = function (Command, hasResponse, body, cb) {
if (Command === null) Command = ''
if (typeof body === 'function') {
cb = body
body = null
}
const Seq = hasResponse ? (this._seqBody += 3) : (this._seqNoBody += 3)
const header = {
Command: Command,
Seq: Seq
}
if (typeof cb === 'function') {
this.once(Seq, cb)
}
dowrite(this, header, body)
}
function dowrite (client, header, body) {
debug('[%j] sending header: %j', client._id, header)
client.write(msgpack.encode(header))
if (body !== null) {
debug('[%j] sending body: %j', client._id, body)
client.write(msgpack.encode(body))
}
}
/*
* normalizeConnectArgs, isPipeName & toNumber have been extracted from the
* Node.js source, as it's an undocumented API and this ensures future
* compatability. All credit goes to the Node.js team.
*/
function toNumber (x) { return (x = Number(x)) >= 0 ? x : false }
function isPipeName (s) {
return typeof s === 'string' && toNumber(s) === false
}
function normalizeConnectArgs (args) {
let options = {}
if (args.length === 0) {
return [options]
} else if (args[0] !== null && typeof args[0] === 'object') {
// connect(options, [cb])
options = args[0]
} else if (isPipeName(args[0])) {
// connect(path, [cb]);
options.path = args[0]
} else {
// connect(port, [host], [cb])
options.port = args[0]
if (args.length > 1 && typeof args[1] === 'string') {
options.host = args[1]
}
}
const cb = args[args.length - 1]
return typeof cb === 'function' ? [options, cb] : [options]
}
exports.connect = function connect () {
const argsLen = arguments.length
let args = new Array(argsLen)
for (let i = 0; i < argsLen; i++) {
args[i] = arguments[i]
}
if (typeof args[0] === 'function') {
// Default
args.unshift({
port: 7373
})
}
args = normalizeConnectArgs(args)
debug('create connection with args: %j', args)
const s = new Serf(args[0])
const onHandshake = typeof args[args.length - 1] === 'function'
? args.pop()
: function () {}
// Pass errors from the connection phase to the callback.
s.on('error', onHandshake)
const doHandshake = function () {
debug('[%j] connected', s._id)
s.removeListener('error', onHandshake)
s.handshake({
Version: 1
}, onHandshake)
}
args.push(doHandshake)
Serf.prototype.connect.apply(s, args)
return s
}