UNPKG

microwizard

Version:

A fast and stable microservice framework, mostly compatible with senecas user API

367 lines (315 loc) 9.64 kB
/* Copyright (c) 2013-2015 Richard Rodger, MIT License */ /* Copyright (c) 2024 WizardTales GmbH, MIT License */ import Net from 'net'; import Stream from 'stream'; import Ndjson from 'ndjson'; import Reconnect from 'reconnect-core'; const internals = {}; const RECOOINT = 1000 * 60 * 2; const RECONAWAIT = RECOOINT + 1000 * 60; export const listen = function (opts, tp) { return function (args, callback, reduceActive) { const listenOptions = { ...opts, ...args }; const connections = []; let listenAttempts = 0; const listener = Net.createServer( { noDelay: true }, function (connection) { connection.setTimeout(RECONAWAIT); connection.on('timeout', () => { if (process.env.TRACE?.includes('MW:D')) { console.log('Ending connection'); } connection.end(); }); if (process.env.DEBUG) { console.log( 'listen', 'connection', listenOptions, 'remote', connection.remoteAddress, connection.remotePort ); } const parser = Ndjson.parse(); const stringifier = Ndjson.stringify(); parser.on('error', function (error) { console.error(error); connection.end(); }); parser.on('data', async (data) => { // health ping if (data.h === 1) { stringifier.write({ h: 2 }); return; } if (data instanceof Error) { const out = {}; out.input = data.input; out.error = 'invalid_json'; stringifier.write(out); return; } const out = await tp.handleRequest(data, opts); reduceActive(); if (!out?.sync) { return; } stringifier.write(out); }); connection.pipe(parser); stringifier.pipe(connection); connection.on('error', function (err) { console.error( 'listen', 'pipe-error', listenOptions, err && err.stack ); }); connections.push(connection); } ); listener.once('listening', function () { listenOptions.port = listener.address().port; if (process.env.DEBUG) { console.log('listen', 'open', listenOptions); } return callback(null, listenOptions); }); listener.on('error', function (err) { console.log('listen', 'net-error', listenOptions, err && err.stack); if ( err.code === 'EADDRINUSE' && listenAttempts < listenOptions.max_listen_attempts ) { listenAttempts++; console.log( 'listen', 'attempt', listenAttempts, err.code, listenOptions ); setTimeout( listen, 100 + Math.floor(Math.random() * listenOptions.attempt_delay) ); } }); listener.on('close', function () { if (process.env.DEBUG) { console.log('listen', 'close', listenOptions); } }); function listen () { if (listenOptions.path) { listener.listen(listenOptions.path); } else { listener.listen(listenOptions.port, listenOptions.host); } } listen(); tp.onClose(async function () { // node 0.10 workaround, otherwise it throws if (listener._handle) { listener.close(); } internals.closeConnections(connections); }); }; }; export const client = function (options, tp) { return function (args, callback) { let conStream; let connection; let established = false; let stringifier; const type = args.type; if (args.host) { // under Windows host, 0.0.0.0 host will always fail args.host = args.host === '0.0.0.0' ? '127.0.0.1' : args.host; } const clientOptions = { ...options, ...args }; clientOptions.host = !args.host && clientOptions.host === '0.0.0.0' ? '127.0.0.1' : clientOptions.host; const connect = function () { let interval; if (process.env.DEBUG) { console.log('client', type, 'send-init', '', '', clientOptions); } const reconnect = internals.reconnect( { failAfter: clientOptions.failAfter || 3 }, function (stream) { conStream = stream; const keepalive = { called: false }; const msger = internals.clientMessager(clientOptions, tp, keepalive); const parser = Ndjson.parse(); stringifier = Ndjson.stringify(); stream.pipe(parser).pipe(msger).pipe(stringifier).pipe(stream); interval = setInterval(() => { if (established === false) { clearInterval(interval); interval = null; return; } stringifier.write({ h: 1 }); setTimeout(() => { if (keepalive.called) { keepalive.called = false; } else { // if the connection is dead for whatever reason we close // the connection and only restablish upon request reconnect.disconnect(); internals.closeConnections([conStream]); // mark this disconnected connection = null; clearInterval(interval); interval = null; } }, 1000 * 5); }, RECOOINT); if (!established) reconnect.emit('s_connected', stringifier); established = true; } ); reconnect.on('connect', function (connection) { if (process.env.DEBUG) { console.log('client', type, 'connect', '', '', clientOptions); } // connection.clientOptions = clientOptions // unique per connection // connections.push(connection) // established = true }); reconnect.on('reconnect', function () { if (process.env.DEBUG) { console.log('client', type, 'reconnect', '', '', clientOptions); } }); reconnect.on('disconnect', function (err) { if (process.env.DEBUG) { console.log( 'client', type, 'disconnect', '', '', clientOptions, (err && err.stack) || err ); } clearInterval(interval); interval = null; established = false; }); reconnect.on('error', function (err) { console.log('client', type, 'error', '', '', clientOptions, err.stack); clearInterval(interval); interval = null; }); reconnect.on('fail', function (err) { console.log( 'client', type, 'fail', '', '', clientOptions, err, err?.stack ); clearInterval(interval); interval = null; reconnect.disconnect(); internals.closeConnections([conStream]); // mark this disconnected connection = null; }); reconnect.connect({ port: clientOptions.port, host: clientOptions.host }); tp.onClose(async function () { clearInterval(interval); interval = null; reconnect.disconnect(); internals.closeConnections([conStream]); }); return reconnect; }; function getClient (cb) { if (!connection) connection = connect(); if (established) { cb(stringifier); } else { connection.once('s_connected', cb); } } const send = function (spec, topic, sendDone) { sendDone(null, function (args, done, meta) { // const self = this; let timedout = false; const connectTimeout = setTimeout(() => { timedout = true; done('timeout'); }, meta.$c_to || clientOptions.connectTimeout); getClient(function (stringifier) { if (!timedout) { clearTimeout(connectTimeout); } else { return; } const timeout = setTimeout(() => { done('timeout'); }, clientOptions.timeout); function finish () { clearTimeout(timeout); done.apply(done, arguments); } const outmsg = tp.prepareRequest(args, finish, meta); if (!outmsg.replied) stringifier.write(outmsg); if (!outmsg.sync) { finish(null, {}); } }); }); }; tp.makeClient(send, clientOptions, callback); }; }; internals.clientMessager = function (options, tp, keepalive) { const messager = new Stream.Duplex({ objectMode: true }); messager._read = function () {}; messager._write = function (data, enc, callback) { // we always reset the value on any traffic keepalive.called = true; // keepalive traffic, this is fine ignore if (data?.h === 2) { return callback(); } tp.handleResponse(data, options); return callback(); }; return messager; }; internals.closeConnections = function (connections) { for (let i = 0, il = connections.length; i < il; ++i) { internals.destroyConnection(connections[i]); } }; internals.destroyConnection = function (connection) { try { connection.destroy(); } catch (e) { console.log(e); } }; internals.reconnect = Reconnect(function () { const args = [].slice.call(arguments); return Net.connect.apply(null, args); });