Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Myriad of changes #9

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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.
Expand All @@ -40,8 +40,28 @@ 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:[email protected]', 'sip:[email protected]'], {
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.
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.

Expand All @@ -57,13 +77,30 @@ 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:[email protected]');
});
```

##### 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}`

##### 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)

Handle REFER messasges in your B2B dialogs.
Expand Down
18 changes: 16 additions & 2 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -47,17 +49,26 @@ function transfer(opts) {
return referHandler.transfer();
}

class Simring {
class Simring extends Emitter {
constructor(req, res, uriList, opts, notifiers) {
super();
const callOpts = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't you need to call super() as the first thing in the constructor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're probably 100% right here :) Like I said - completely non tested. I'll go test the changes myself this week and we can go from there

req,
res,
uriList: typeof uriList == 'string' ? [uriList] : (uriList || []),
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) {
Expand All @@ -80,6 +91,9 @@ class Simring {
return this.manager.addUri(uri, callOpts);
}

cancel() {
return this.manager.cancel();
}
}

module.exports = {simring, Simring, transfer, forwardInDialogRequests};
56 changes: 39 additions & 17 deletions lib/call-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}
Expand Down Expand Up @@ -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`);
Expand All @@ -140,31 +151,36 @@ 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}`);
}
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];
Expand All @@ -175,17 +191,23 @@ class CallManager extends Emitter {
else {
this._logger.info(`killing call to ${uri}`);
req.cancel();
this.cip.delete(uri);
}
}
this.cip.clear();
}

cancel() {
this.finished = true;
this.killCalls();
this.finalReject(new this.srf.SipError(487));
}

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);
}
});
}
Expand All @@ -198,7 +220,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;
}
}

Expand Down