diff --git a/lib/AmiClient.js b/lib/AmiClient.js index 17aab0d..8a61ff1 100644 --- a/lib/AmiClient.js +++ b/lib/AmiClient.js @@ -11,6 +11,24 @@ const amiConnector = require('asterisk-ami-connector'); const debugLog = require('debug')('AmiClient'); const debugError = require('debug')('AmiClient:error'); +class AmiHeader { + constructor(name, value) { + this.name = name; + this.value = value; + } +} + +class MultivalAction { + constructor(action) { + this.headers = [ new AmiHeader('Action', action) ]; + } + + toString() { + var lines = this.headers.map(header => header.name + ': ' + header.value); + return lines.join('\r\n'); + } +} + /** * AmiClient class */ @@ -27,6 +45,8 @@ class AmiClient extends EventEmitter{ _connector: null, _kaTimer: null, _kaActionId: null, + _lastKa: 0, + _lastKaResponse: 0, _options: Object.assign({ reconnect: false, maxAttemptsCount: 30, @@ -97,7 +117,7 @@ class AmiClient extends EventEmitter{ .on('response', response => { if(this._options.keepAlive && response.ActionID === this._kaActionId){ debugLog('keep-alive heart bit'); - this._keepAliveBit(); + this._lastKaResponse = this._now(); return; } @@ -120,7 +140,7 @@ class AmiClient extends EventEmitter{ .on('data', chunk => this.emit('data', chunk)) .on('error', error => this.emit('internalError', error)) .on('close', () => { - clearTimeout(this._kaTimer); + clearInterval(this._kaTimer); this.emit('disconnect'); this._prEmitter.emit('disconnect'); setTimeout(() => { @@ -138,6 +158,8 @@ class AmiClient extends EventEmitter{ }); if(this._options.keepAlive){ + this._lastKa = 0; + this._lastKaResponse = 0; this._keepAliveBit(); } return this._connection; @@ -149,7 +171,7 @@ class AmiClient extends EventEmitter{ */ disconnect(){ this._userDisconnect = true; - clearTimeout(this._kaTimer); + clearInterval(this._kaTimer); this.emit('disconnect'); if(this._connection){ this._connection.close(); @@ -175,6 +197,10 @@ class AmiClient extends EventEmitter{ message.ActionID = this._genActionId(this._specPrefix); } + if(message.constructor == MultivalAction) { + message = message.toString(); + } + if(promisable){ return this._promisable(message); } @@ -239,18 +265,31 @@ class AmiClient extends EventEmitter{ return this._prepareOptions(); } + + _now(){ + return Math.floor(Date.now()/1000); + } /** * Keep-alive heart bit handler * @private */ _keepAliveBit(){ - this._kaTimer = setTimeout(() => { + this._kaTimer = setInterval(() => { if(this._options.keepAlive && this._connection && this.isConnected){ + if(this._lastKa > 0 && this._lastKaResponse < this._lastKa) { + /* Asterisk manager did not reply - let's disconnect */ + debugLog("disconnect on KA timer"); + this.disconnect(); + /* reset the _userDisconnect flag so that we reconnect automatically */ + this._userDisconnect = false; + return; + } this._kaActionId = this._genActionId(this._specPrefix); this._connection.write({ Action: 'Ping', ActionID: this._kaActionId }); + this._lastKa = this._now(); } }, this._options.keepAliveDelay); this._kaTimer.unref(); @@ -295,6 +334,14 @@ class AmiClient extends EventEmitter{ return true; } + /** + * + * @private + */ + _random() { + return Math.floor(Math.random() * Math.pow(10, 10)); + } + /** * * @param prefix @@ -303,7 +350,7 @@ class AmiClient extends EventEmitter{ */ _genActionId(prefix){ prefix = prefix || ''; - return `${prefix}${Date.now()}`; + return `${prefix}${Date.now()}-${this._random()}`; } /** @@ -383,6 +430,21 @@ class AmiClient extends EventEmitter{ get connection(){ return this._connection; } + + /** + * + * @returns {MultivalAction} + */ + multivalAction(action){ + var result = new MultivalAction(action); + return new Proxy(result, { + set: function(target, property, value) { + target.headers.push(new AmiHeader(property, value)); + return true; + } + }); + } + } -module.exports = AmiClient; \ No newline at end of file +module.exports = AmiClient; diff --git a/test/amiClientTest.js b/test/amiClientTest.js index 71be1da..2523202 100644 --- a/test/amiClientTest.js +++ b/test/amiClientTest.js @@ -577,7 +577,8 @@ describe('Ami Client internal functionality', function(){ client = new AmiClient({dontDeleteSpecActionId: true}); client.connect(USERNAME, SECRET, {port: socketOptions.port}).then(() => { client.once('response', response => { - assert.ok(/^--spec_\d{13}$/.test(response.ActionID)); + console.log("actionid = ", response.ActionID); + assert.ok(/^--spec_\d{13}-\d{1,10}$/.test(response.ActionID)); done(); }) .action({Action: 'Ping'}); @@ -741,5 +742,29 @@ describe('Ami Client internal functionality', function(){ }); + describe('multivalAction', function(){ + + beforeEach(done => { + client = new AmiClient({}); + server = new AmiTestServer(serverOptions); + server.listen(socketOptions).then(done); + }); + + it('multivalAction handles multiple headers with the same name', done => { + let action = client.multivalAction('MessageSend'); + action.Variable = 'VAR1=val1'; + action.Variable = 'VAR2=val2'; + client.connect(USERNAME, SECRET, {port: socketOptions.port}).then(() => { + client._connection.write = function(data) { + assert(data.match(/Action: MessageSend/)); + assert(data.match(/Variable: VAR1=val1/)); + assert(data.match(/Variable: VAR2=val2/)); + assert(data.match(/ActionID:/)); + done(); + }; + client.action(action); + }); + }); + }); });