mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 16:50:39 +00:00
* major refactor and simplification of actionHookDelay feature * wip for #765 * wip * testing * wip * added validity checks for actionHookDelay properties * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix bug where config happens before endpoint is established * wip * hangup and clear ws connection if nogiveuptimer expires * wip * wip * wip
159 lines
5.4 KiB
JavaScript
159 lines
5.4 KiB
JavaScript
const makeTask = require('../tasks/make_task');
|
|
const Emitter = require('events');
|
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
|
const {TaskName} = require('../utils/constants');
|
|
|
|
/**
|
|
* ActionHookDelayProcessor
|
|
* @extends Emitter
|
|
*
|
|
* @param {Object} logger - logger instance
|
|
* @param {Object} opts - options
|
|
* @param {Object} cs - call session
|
|
* @param {Object} ep - endpoint
|
|
*
|
|
* @emits {Event} 'giveup' - when associated giveup timer expires
|
|
*
|
|
* Ref:https://www.jambonz.org/docs/supporting-articles/handling-action-hook-delays/
|
|
*/
|
|
class ActionHookDelayProcessor extends Emitter {
|
|
constructor(logger, opts, cs) {
|
|
super();
|
|
this.logger = logger;
|
|
this.cs = cs;
|
|
|
|
const enabled = this.init(opts);
|
|
if (enabled && (!this.actions || !Array.isArray(this.actions) || this.actions.length === 0)) {
|
|
throw new Error('ActionHookDelayProcessor: no actions specified');
|
|
}
|
|
else if (enabled && this.actions.some((a) => !a.verb || ![TaskName.Say, TaskName.Play].includes(a.verb))) {
|
|
throw new Error(`ActionHookDelayProcessor: invalid actions specified: ${JSON.stringify(this.actions)}`);
|
|
}
|
|
}
|
|
|
|
get properties() {
|
|
return {
|
|
actions: this.actions,
|
|
retries: this.retries,
|
|
noResponseTimeout: this.noResponseTimeout,
|
|
noResponseGiveUpTimeout: this.noResponseGiveUpTimeout
|
|
};
|
|
}
|
|
|
|
get ep() {
|
|
return this.cs.ep;
|
|
}
|
|
|
|
init(opts) {
|
|
this.logger.debug({opts}, 'ActionHookDelayProcessor#init');
|
|
|
|
this.actions = opts.actions;
|
|
this.retries = opts.retries || 0;
|
|
this.noResponseTimeout = opts.noResponseTimeout || 0;
|
|
this.noResponseGiveUpTimeout = opts.noResponseGiveUpTimeout;
|
|
|
|
// return false if these options actually disable the ahdp
|
|
return ('enable' in opts && opts.enable === true) ||
|
|
('enabled' in opts && opts.enabled === true) ||
|
|
(!('enable' in opts) && !('enabled' in opts));
|
|
}
|
|
|
|
start() {
|
|
this.logger.debug('ActionHookDelayProcessor#start');
|
|
if (this._noResponseTimer) {
|
|
this.logger.debug('ActionHookDelayProcessor#start: already started due to prior gather which is continuing');
|
|
return;
|
|
}
|
|
this._retryCount = 0;
|
|
const timeoutMs = this.noResponseTimeout === 0 ? 1 : this.noResponseTimeout * 1000;
|
|
this._noResponseTimer = setTimeout(this._onNoResponseTimer.bind(this), timeoutMs);
|
|
|
|
if (this.noResponseGiveUpTimeout > 0) {
|
|
const timeoutMs = this.noResponseGiveUpTimeout * 1000;
|
|
this._noResponseGiveUpTimer = setTimeout(this._onNoResponseGiveUpTimer.bind(this), timeoutMs);
|
|
}
|
|
}
|
|
|
|
async stop() {
|
|
this.logger.debug('ActionHookDelayProcessor#stop');
|
|
if (this._noResponseTimer) {
|
|
clearTimeout(this._noResponseTimer);
|
|
this._noResponseTimer = null;
|
|
}
|
|
if (this._noResponseGiveUpTimer) {
|
|
clearTimeout(this._noResponseGiveUpTimer);
|
|
this._noResponseGiveUpTimer = null;
|
|
}
|
|
if (this._taskInProgress) {
|
|
this.logger.debug(`ActionHookDelayProcessor#stop: killing task in progress: ${this._taskInProgress.name}`);
|
|
|
|
/** if we are doing a play, kill it immediately
|
|
* if we are doing a say, wait for it to finish
|
|
*/
|
|
if (TaskName.Say === this._taskInProgress.name) {
|
|
this._sayResolver = () => {
|
|
this.logger.debug('ActionHookDelayProcessor#stop: say is done, continue on..');
|
|
this._taskInProgress.kill(this.cs);
|
|
this._taskInProgress = null;
|
|
};
|
|
this.logger.debug('ActionHookDelayProcessor#stop returning promise');
|
|
return new Promise((resolve) => this._sayResolver = resolve);
|
|
}
|
|
else {
|
|
/* play */
|
|
this._taskInProgress.kill(this.cs);
|
|
this._taskInProgress = null;
|
|
}
|
|
}
|
|
this.logger.debug('ActionHookDelayProcessor#stop returning');
|
|
}
|
|
|
|
_onNoResponseTimer() {
|
|
this.logger.debug('ActionHookDelayProcessor#_onNoResponseTimer');
|
|
this._noResponseTimer = null;
|
|
|
|
/* get the next play or say action */
|
|
const verb = this.actions[this._retryCount % this.actions.length];
|
|
|
|
const t = normalizeJambones(this.logger, [verb]);
|
|
this.logger.debug({verb}, 'ActionHookDelayProcessor#_onNoResponseTimer: starting action');
|
|
try {
|
|
this._taskInProgress = makeTask(this.logger, t[0]);
|
|
this._taskInProgress.disableTracing = true;
|
|
this._taskInProgress.exec(this.cs, {ep: this.ep});
|
|
} catch (err) {
|
|
this.logger.info(err, 'ActionHookDelayProcessor#_onNoResponseTimer: error starting action');
|
|
this._taskInProgress = null;
|
|
return;
|
|
}
|
|
|
|
this.ep.once('playback-stop', (evt) => {
|
|
this._taskInProgress = null;
|
|
if (this._sayResolver) {
|
|
/* we were waiting for the play to finish before continuing to next task */
|
|
this.logger.debug({evt}, 'got playback-stop');
|
|
this._sayResolver();
|
|
this._sayResolver = null;
|
|
}
|
|
else {
|
|
/* possibly start the no response timer again */
|
|
if (this.retries > 0 && this._retryCount < this.retries && this.noResponseTimeout > 0) {
|
|
this.logger.debug({evt}, 'ActionHookDelayProcessor#_onNoResponseTimer: playback-stop on play/say action');
|
|
const timeoutMs = this.noResponseTimeout * 1000;
|
|
this._noResponseTimer = setTimeout(this._onNoResponseTimer.bind(this), timeoutMs);
|
|
}
|
|
}
|
|
});
|
|
|
|
this._retryCount++;
|
|
}
|
|
|
|
_onNoResponseGiveUpTimer() {
|
|
this.logger.info('ActionHookDelayProcessor#_onNoResponseGiveUpTimer');
|
|
this.stop().catch((err) => {});
|
|
this.emit('giveup');
|
|
}
|
|
}
|
|
|
|
module.exports = ActionHookDelayProcessor;
|