UNPKG

zum

Version:

Zum is a simple event bus for NodeJS microservices using amp.

244 lines (209 loc) 5.65 kB
'use strict'; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; const Emitter = require('eventemitter3'); const { Stream: Parser } = require('amp'); const Message = require('amp-message'); const async = require('async'); const retry = require('tries'); const yeast = require('yeast'); const net = require('netly'); const dbg = require('debug'); const { encrypt, decrypt } = require('./security'); const { HSK, SUB, UNS, PUB, ACK, HOST } = require('./constants'); const debug = dbg('zum:client'); const reservedEvents = { 'reconnect failed': 1, 'reconnecting': 1, 'disconnect': 1, 'reconnect': 1, 'connect': 1, 'error': 1, 'end': 1 }; const reg = new RegExp(`^${HSK}|${SUB}|${UNS}|${PUB}|${ACK}$`); module.exports = async (options = {}) => { if (typeof options === 'string') { options = { host: options }; } const { secret, algorithm, id = yeast(), host = HOST, delay = 100, minDelay = 1000, maxDelay = 5000, retries = Infinity, secure = true } = options; const fns = {}; const queue = []; const events = []; const emitter = new Emitter(); let sock = null; let queueing = false; let destroyed = false; const pack = args => { const last = args[args.length - 1]; if (typeof last === 'function') { const id = 'i:' + yeast(); const fn = args.pop(); fns[id] = fn; args.unshift(Buffer.from(id)); } const msg = new Message(args); if (secure !== true) return msg; const buf = msg.toBuffer(); const secureBuf = [encrypt(buf, secret, algorithm)]; return new Message(secureBuf); }; const send = (...args) => { let force = false; if (args[args.length - 1] === true) { force = args.pop(); } const msg = pack(args); if (sock && sock.writable && !queueing || force && queueing) { sock.write(msg.toBuffer()); } else { queue.push(msg.args); } }; const handshake = () => send(HSK, events); const isAck = arg => arg[0] === 105 && arg[1] === 58; const re = retry({ retries, min: minDelay, max: maxDelay }); const reserved = event => reg.test(event) || event in reservedEvents; const publish = (ev, data) => send(PUB, `s:${ev}`, data); const listen = (ev, fn) => { if (!reserved(ev)) { events.push(ev); send(SUB, ev); } emitter.on(ev, fn); }; const unlisten = (ev, fn) => { if (!reserved(ev)) send(UNS, ev); emitter.off(ev, fn); }; const subscribe = (ev, fn) => { if (!reserved(ev)) listen(`s:${ev}`, fn); }; const unsubscribe = (ev, fn) => { if (!reserved(ev)) unlisten(`s:${ev}`, fn); }; const reply = (id, args) => { const len = args.length; const msg = new Array(2 + len); msg[0] = ACK; msg[1] = id; for (let i = 0; i < len; ++i) { msg[i + 2] = args[i]; } return msg; }; const ondata = sbuf => { let buf = sbuf; if (secure) { const secureMsg = new Message(sbuf); const secureBuf = secureMsg.args[0]; try { buf = decrypt(secureBuf, secret, algorithm); } catch (err) { return; } } const { args } = new Message(buf); if (PUB === args[0]) { args.shift(); } // Reply message, invoke the given callback if (ACK === args[0]) { args.shift(); const id = args.shift().toString(); const fn = fns[id]; delete fns[id]; if (fn) fn(...args); return; } // Request method, pass a trailing callback if (isAck(args[0])) { const id = args.shift(); args.push((...arg) => send(...reply(id, arg))); } emitter.emit(...args); emitter.emit(...['data', args]); }; const destroy = reconnect => { sock.destroy(); sock.removeAllListeners(); if (reconnect) return; destroyed = true; emitter.removeAllListeners(); }; const reconnect = () => { if (destroyed) { debug('already destroyed'); return; } debug('reconnect'); re.retry((fail, backoff) => { if (fail) { debug('reconnect failed', backoff); emitter.emit('reconnect failed', backoff); return destroy(); } debug('reconnecting', backoff); emitter.emit('reconnecting', backoff); connect(); }); }; const run = () => { queueing = true; async.eachSeries(queue, (args, next) => { setTimeout(() => { args.push(true); send(...args); next(); }, delay); }, () => { queueing = false; queue.length = 0; }); }; const onconnect = () => { handshake(); emitter.emit('connect', { id, host }); run(); re.clear(); }; const onerror = err => { emitter.emit('error', err); destroy(true); reconnect(); }; const connect = () => { const parser = new Parser(); sock = net.connect(host, () => {}); sock.pipe(parser); parser.on('data', ondata); sock.once('error', onerror); sock.once('end', reconnect); sock.once('connect', onconnect); return new Promise((resolve, reject) => { emitter.once('connect', info => { resolve(Object.assign(emitter, _extends({}, info, { send, destroy, reconnect, listen, unlisten, publish, subscribe, unsubscribe }))); }); emitter.once('error', reject); }); }; return connect(); };