From c28ec741c687eabc784acfda25f2db72cc8a6398 Mon Sep 17 00:00:00 2001 From: Dan Jenkins Date: Wed, 13 Nov 2019 12:55:32 +0000 Subject: [PATCH 1/5] Allow SimRinger class users access to events Also make the event names relevant. finalFailure is not the finalFailure for the simringer as a whole, also added data for the events finalSuccess will tell you which uri was successful failure will tell you the err and the uri in an obj --- README.md | 19 ++++++++++++++++--- app.js | 14 ++++++++++++-- lib/call-manager.js | 4 ++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b965f1d..a559790 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A common need is to do a simultaneous ring of multiple SIP endpoints in response This function provides a forking outdial B2BUA that connects the caller to the first endpoint that answers. -##### basic usage +##### Basic usage In basic usage, the exported `simring` function acts almost exactly like [Srf#createB2BUA](https://drachtio.org/api#srf-create-b2bua), except that you pass an array of sip URIs rather than a single sip URI. ```js const {simring} = require('drachtio-fn-b2b-sugar'); @@ -23,7 +23,7 @@ srf.invite(async (req, res) { ``` All of the options that you can pass to [Srf#createB2BUA](https://drachtio.org/api#srf-create-b2bua) can be passed to `simring`. -##### with logging +##### With logging If you want logging from simring, you can treat the exported `simring` reference as a factory function that you invoke with a single argument, that being the logger object that you want to be used. That object must provide 'debug', 'info', and 'error' functions (e.g. [pino](https://www.npmjs.com/package/pino)). Invoking the factory function then returns another function that does the actual simring. @@ -57,13 +57,26 @@ srf.invite(async (req, res) { console.info(`successfully connected to ${uas.remote.uri}`); }) .catch((err) => console.log(err, 'Error connecting call')); - + // assume we are alerted when a new device registers someEmitter.on('someRegisterEvent', () => { if (!simring.finished) simring.addUri('sip:789@example.com'); }); ``` +##### Events + +The Simring class emits two different events - `finalSuccess` and `failure` + +###### finalSuccess + +Emits the uri that was eventually successful. - `uri` + +###### failure + +Emits an object containing the error and the uri that failed - `{err, uri}` + + ## transfer (REFER handler) Handle REFER messasges in your B2B dialogs. diff --git a/app.js b/app.js index 0aed7e9..bb2e5ff 100644 --- a/app.js +++ b/app.js @@ -2,12 +2,14 @@ const Srf = require('drachtio-srf'); const SipRequest = Srf.SipRequest; const SipResponse = Srf.SipResponse; const assert = require('assert'); +const Emitter = require('events'); const noop = () => {}; const noopLogger = {debug: noop, info: noop, error: console.error}; const CallManager = require('./lib/call-manager'); const ReferHandler = require ('./lib/refer-handler'); const forwardInDialogRequests = require('./lib/dialog-request-forwarder'); + function simring(...args) { if (args.length === 1) { const logger = args[0]; @@ -47,7 +49,7 @@ function transfer(opts) { return referHandler.transfer(); } -class Simring { +class Simring extends Emitter{ constructor(req, res, uriList, opts, notifiers) { const callOpts = { req, @@ -56,8 +58,16 @@ class Simring { opts: opts || {}, notifiers: notifiers || {}, logger: noopLogger - } ; + }; this.manager = new CallManager(callOpts); + + this.on('newListener', (event, listener) => { + this.manager.addListener(event, listener); + }); + + this.on('removeListener', (event, listener) => { + this.manager.removeListener(event, listener); + }); } set logger(logger) { diff --git a/lib/call-manager.js b/lib/call-manager.js index e5d7a69..71da60c 100644 --- a/lib/call-manager.js +++ b/lib/call-manager.js @@ -140,11 +140,11 @@ class CallManager extends Emitter { }) .then((dlg) => { uas = dlg; - this.emit('finalSuccess'); + this.emit('finalSuccess', uri); return {uas, uac}; }) .catch((err) => { - this.emit('finalFailure'); + this.emit('failed', {err, uri}); if (timer) clearTimeout(timer); if (!this.callerGone) { this._logger.info(`CallManager#attemptOne: call to ${uri} failed with ${err.status}`); From 226ce45693adc6d5bd79e1c26fff1d44150031ee Mon Sep 17 00:00:00 2001 From: Dan Jenkins Date: Wed, 13 Nov 2019 13:46:22 +0000 Subject: [PATCH 2/5] allow external application to cancel simringer designed so that the simringer no longer handles its own timesouts, and no longer tracks failures as a way of rejecting the final promise. Pass in timeout as null or false, and listen for the relevant events and decide on when you want to terminate the simringer yourself --- README.md | 24 ++++++++++++++++++++++++ app.js | 3 +++ lib/call-manager.js | 45 +++++++++++++++++++++++++++++++++------------ 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a559790..d47862b 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,26 @@ srf.invite(async (req, res) { } }); ``` + +##### Timouts values & Global vs individual leg timeouts + +By default the timeout you can pass to `simring` is assigned to each outbound leg in the fork. The default value is `20` seconds. You may want to change this so that it's a global timeout rather than individual timeouts. You can pass in `globalTimeout` within the `opts` object. You can also change the timeout value by setting `timeout` in that object too; it's set in seconds. + +```js +const {simring} = require('drachtio-fn-b2b-sugar'); +srf.invite(async (req, res) { + try { + const {uas, uac} = await simring(req, res, ['sip:123@example.com', 'sip:456@example.com'], { + globalTimeout: true, + timeout: 30 + }); + console.info(`successfully connected to ${uas.remote.uri}`); + } catch (err) { + console.log(err, 'Error connecting call'); + } +}); +``` + ## Simring class A more advanced usage is to to start a simring against a list of endpoints, and then later (before any have answered) add one or more new endpoints to the simring list. @@ -76,6 +96,10 @@ Emits the uri that was eventually successful. - `uri` Emits an object containing the error and the uri that failed - `{err, uri}` +##### Diasble the timeout and Promise rejection + +You may want to disable simringer's ability to handle a timeout completely as well as decide when your simringer is indeed finished. +This might be the case if you have an active simringer but you want to add a URI later on. Without the ability to handle this decison yourself lets say you add a URI straight away and it fails (500 response), the simringer will see that as a failed simringer and reject the returned promise. But you're still within your timeout value within your app and you want to add another URI to the now failed simringer. The way to handle this is to take control of the timeout yourself by passing in a timeout value of `null` or `false`. In this case, you now need to cancel the Simring class yourself using the exported `Simring#cancel` method. ## transfer (REFER handler) diff --git a/app.js b/app.js index bb2e5ff..71530ff 100644 --- a/app.js +++ b/app.js @@ -90,6 +90,9 @@ class Simring extends Emitter{ return this.manager.addUri(uri, callOpts); } + cancel() { + return this.manager.cancel(); + } } module.exports = {simring, Simring, transfer, forwardInDialogRequests}; diff --git a/lib/call-manager.js b/lib/call-manager.js index 71da60c..f505ee5 100644 --- a/lib/call-manager.js +++ b/lib/call-manager.js @@ -14,6 +14,7 @@ class CallManager extends Emitter { this.callOpts = opts.opts || {}; this.callOpts.localSdp = this.callOpts.localSdpB; this.notifiers = opts.notifiers || {}; + this.globalTimeout = opts.globalTimeout || false; //calls in progress: uri -> SipRequest this.cip = new Map(); @@ -22,7 +23,9 @@ class CallManager extends Emitter { this.callerGone = false; this.callAnswered = false; this.bridged = false; - this.callTimeout = this.callOpts.timeout || DEFAULT_TIMEOUT; + this.callTimeout = this.callOpts.timeout !== undefined ? this.callOpts.timeout : DEFAULT_TIMEOUT; + this.globalTimer = null; + if (!(this.callOpts.headers && this.callOpts.headers.from) && !this.callOpts.callingNumber) { this.callOpts.callingNumber = opts.req.callingNumber; @@ -69,12 +72,18 @@ class CallManager extends Emitter { return; }) .catch((err) => { - if (p === this.intPromise) { + if (p === this.intPromise && this.callTimeout) { this.finished = true; this.finalReject(err); } }); + if (this.globalTimeout && this.callTimeout) { + this.globalTimer = setTimeout(() => { + this.killCalls(); + }, this.callTimeout * 1000); + } + return this.extPromise; } @@ -90,7 +99,7 @@ class CallManager extends Emitter { return; }) .catch((err) => { - if (p === this.intPromise) { + if (p === this.intPromise && this.callTimeout) { this.finished = true; this.finalReject(err); } @@ -118,6 +127,8 @@ class CallManager extends Emitter { } }) .then((dlg) => { + if (this.globalTimer) clearTimeout(this.globalTimer); + if (this.callAnswered) { dlg.destroy(); this._logger.info(`race condition; hanging up call ${dlg.sip.callid} because another leg answered`); @@ -152,19 +163,24 @@ class CallManager extends Emitter { throw err; }); - timer = setTimeout(() => { - if (this.cip.has(uri)) { - this._logger.info(`CallManager#attemptOne: timeout on call to ${uri}; tearing down call`); - const req = this.cip.get(uri); - this.cip.delete(uri); - req.cancel(); - } - }, this.callTimeout * 1000); + if (!this.globalTimeout && this.callTimeout) { + timer = setTimeout(() => { + if (this.cip.has(uri)) { + this._logger.info(`CallManager#attemptOne: timeout on call to ${uri}; tearing down call`); + const req = this.cip.get(uri); + this.cip.delete(uri); + req.cancel(); + } + }, this.callTimeout * 1000); + } return p; } killCalls(spareMe) { + if (this.globalTimer) { + clearTimeout(this.globalTimer); + } for (const arr of this.cip) { const uri = arr[0]; const req = arr[1]; @@ -177,7 +193,12 @@ class CallManager extends Emitter { req.cancel(); } } - this.cip.clear(); + } + + cancel() { + this.finished = true; + this.killCalls(); + this.finalReject(new this.srf.SipError(487)); } copyUACHeadersToUAS(uacRes) { From 7653af2da4f4afe7c0ad79b15a244ec483c0259f Mon Sep 17 00:00:00 2001 From: Dan Jenkins Date: Wed, 13 Nov 2019 13:48:03 +0000 Subject: [PATCH 3/5] whitespace changes to follow the rest of the file --- README.md | 2 +- lib/call-manager.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d47862b..d2f4814 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ srf.invite(async (req, res) { ``` ## Simring class -A more advanced usage is to to start a simring against a list of endpoints, and then later (before any have answered) add one or more new endpoints to the simring list. +A more advanced usage is to to start a simring against a list of endpoints, and then later (before any have answered) add one or more new endpoints to the simring list. This would be useful, for instance, in a scenario where you are ringing all of the registered devices for a user and while doing that a new device registers that you also want to include. diff --git a/lib/call-manager.js b/lib/call-manager.js index f505ee5..41649c6 100644 --- a/lib/call-manager.js +++ b/lib/call-manager.js @@ -202,11 +202,11 @@ class CallManager extends Emitter { } copyUACHeadersToUAS(uacRes) { - this.callOpts.headers = {} ; + this.callOpts.headers = {}; if(this.callOpts.proxyResponseHeaders) { this.callOpts.proxyResponseHeaders.forEach((hdr) => { if (uacRes.has(hdr)) { - this.callOpts[hdr] = uacRes.get(hdr) ; + this.callOpts[hdr] = uacRes.get(hdr); } }); } @@ -219,7 +219,7 @@ class CallManager extends Emitter { Object.assign(this.callOpts.headers, this.callOpts.responseHeaders); } Object.assign(this.callOpts.headers, this.callOpts.responseHeaders); - return this.callOpts.headers ; + return this.callOpts.headers; } } From 2d7fac22f2123c6094099170f0b0ce1caef684d9 Mon Sep 17 00:00:00 2001 From: Dan Jenkins Date: Wed, 13 Nov 2019 13:48:21 +0000 Subject: [PATCH 4/5] dnt clear whole map, delete each uri other code in _attemptOne calls killCalls and then removes a uri from the Map afterwards This proves that you expect the URI to still be in the map. Instead of clearing (because one could still be in there), delete them individually --- lib/call-manager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/call-manager.js b/lib/call-manager.js index 41649c6..17b256c 100644 --- a/lib/call-manager.js +++ b/lib/call-manager.js @@ -191,6 +191,7 @@ class CallManager extends Emitter { else { this._logger.info(`killing call to ${uri}`); req.cancel(); + this.cip.delete(uri); } } } From bf29abd3fce5b675710bc5e6a73f10b586b823e9 Mon Sep 17 00:00:00 2001 From: Dan Jenkins Date: Wed, 27 Nov 2019 14:34:33 +0000 Subject: [PATCH 5/5] add super() for Emitter extends --- app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index 71530ff..8afc0f7 100644 --- a/app.js +++ b/app.js @@ -49,8 +49,9 @@ function transfer(opts) { return referHandler.transfer(); } -class Simring extends Emitter{ +class Simring extends Emitter { constructor(req, res, uriList, opts, notifiers) { + super(); const callOpts = { req, res,