diff --git a/lib/session/call-session.js b/lib/session/call-session.js index 321a1322..ac81dc8f 100644 --- a/lib/session/call-session.js +++ b/lib/session/call-session.js @@ -19,6 +19,7 @@ const { normalizeJambones } = require('@jambonz/verb-specifications'); const listTaskNames = require('../utils/summarize-tasks'); const HttpRequestor = require('../utils/http-requestor'); const WsRequestor = require('../utils/ws-requestor'); +const ActionHookDelayProcessor = require('../utils/action-hook-delay'); const { JAMBONES_INJECT_CONTENT, JAMBONES_EAGERLY_PRE_CACHE_AUDIO, @@ -519,13 +520,6 @@ class CallSession extends Emitter { this._actionHookDelayRetries = e; } - get actionHookDelayActions() { - return this._actionHookDelayActions; - } - - set actionHookDelayActions(e) { - this._actionHookDelayActions = e; - } // Getter/setter for current tts vendor get currentTtsVendor() { return this._currentTtsVendor; @@ -535,6 +529,71 @@ class CallSession extends Emitter { this._currentTtsVendor = vendor; } + get actionHookDelayProcessor() { + return this._actionHookDelayProcessor; + } + + set actionHookDelayProperties(opts) { + if (this._actionHookDelayProcessor) { + this._actionHookDelayProcessor.stop(); + if (!this._actionHookDelayProcessor.init(opts)) { + this._actionHookDelayProcessor.removeAllListeners(); + this._actionHookDelayProcessor = null; + } + } + else { + try { + this._actionHookDelayProcessor = new ActionHookDelayProcessor(this.logger, opts, this, this.ep); + this._actionHookDelayProcessor.on('giveup', () => { + this.logger.info('CallSession: ActionHookDelayProcessor: giveup event - hanging up call'); + this._jambonzHangup(); + if (this.wakeupResolver) { + this.logger.debug('CallSession: Giveup timer expired - waking up'); + this.wakeupResolver({reason: 'noResponseGiveUp'}); + this.wakeupResolver = null; + } + }); + } catch (err) { + this.logger.error({err}, 'CallSession: Error creating ActionHookDelayProcessor'); + } + } + } + + async clearOrRestoreActionHookDelayProcessor() { + if (this._actionHookDelayProcessor) { + await this._actionHookDelayProcessor.stop(); + if (!this.popActionHookDelayProperties()) { + //this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - ahd settings'); + //await this.clearActionHookDelayProcessor(); + } + else { + this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - restore ahd after gather override'); + } + } + } + + async clearActionHookDelayProcessor() { + if (this._actionHookDelayProcessor) { + await this._actionHookDelayProcessor.stop(); + this._actionHookDelayProcessor.removeAllListeners(); + this._actionHookDelayProcessor = null; + } + } + + stashActionHookDelayProperties() { + this._storedActionHookDelayProperties = this._actionHookDelayProcessor.properties; + } + + popActionHookDelayProperties() { + if (this._storedActionHookDelayProperties) { + this._actionHookDelayProcessor.init(this._storedActionHookDelayProperties); + this._storedActionHookDelayProperties = null; + return true; + } + return false; + } + + hasGlobalSttPunctuation() { return this._globalSttPunctuation !== undefined; } @@ -961,7 +1020,6 @@ class CallSession extends Emitter { task.on('VerbHookSpanWaitForEnd', ({span}) => { this.verbHookSpan = span; }); - task.on('ActionHookDelayActionOptions', this._onActionHookDelayActions.bind(this)); try { const resources = await this._evaluatePreconditions(task); let skip = false; @@ -1007,6 +1065,10 @@ class CallSession extends Emitter { ) { try { await this._awaitCommandsOrHangup(); + + await this.clearOrRestoreActionHookDelayProcessor(); + + //TODO: remove filler noise code and simply create as action hook delay if (this._isPlayingFillerNoise) { this._isPlayingFillerNoise = false; this.ep.api('uuid_break', this.ep.uuid) @@ -1237,9 +1299,8 @@ class CallSession extends Emitter { this.currentTask.kill(this); } this._endVerbHookSpan(); - // clear all delay action hook timeout if there is - this._clearActionHookNoResponseGiveUpTimer(); - this._clearActionHookNoResponseTimer(); + + await this.clearOrRestoreActionHookDelayProcessor(); } /** @@ -1753,7 +1814,7 @@ Duration=${duration} ` this._jambonzHangup(); } - _onCommand({msgid, command, call_sid, queueCommand, data}) { + async _onCommand({msgid, command, call_sid, queueCommand, data}) { this.logger.info({msgid, command, queueCommand, data}, 'CallSession:_onCommand - received command'); let resolution; switch (command) { @@ -1778,9 +1839,9 @@ Duration=${duration} ` } resolution = {reason: 'received command, new tasks', queue: queueCommand, command}; resolution.command = listTaskNames(t); + // clear all delay action hook timeout if there is - this._clearActionHookNoResponseGiveUpTimer(); - this._clearActionHookNoResponseTimer(); + await this.clearOrRestoreActionHookDelayProcessor(); } else this._lccCallHook(data); break; @@ -2040,8 +2101,7 @@ Duration=${duration} ` this.rootSpan && this.rootSpan.end(); // close all background tasks this.backgroundTaskManager.stopAll(); - this._clearActionHookNoResponseGiveUpTimer(); - this._clearActionHookNoResponseTimer(); + this.clearOrRestoreActionHookDelayProcessor().catch((err) => {}); } /** @@ -2450,6 +2510,16 @@ Duration=${duration} ` this.logger.info('_awaitCommandsOrHangup - waiting...'); this.wakeupResolver = resolve; + if (this._actionHookDelayProcessor) { + this._actionHookDelayProcessor.start(); + } + + /** + * TODO: filler noise can be handled as an ActionHookDelayProcessor - + * it's just one specific scenario for action hook delay - + * remove the code below and simply implement filler noise as an action hook delay + */ + /* start filler noise if configured while we wait for new commands */ if (this.fillerNoise?.url && this.ep?.connected && !this.ep2) { this.logger.debug('CallSession:_awaitCommandsOrHangup - playing filler noise'); @@ -2494,81 +2564,6 @@ Duration=${duration} ` this.verbHookSpan = null; } } - // actionHook delay actions - _onActionHookDelayActions(options) { - this._actionHookDelayRetryCount = 0; - this._startActionHookNoResponseTimer(options); - this._startActionHookNoResponseGiveUpTimer(options); - } - - _startActionHookNoResponseTimer(options) { - this._clearActionHookNoResponseTimer(); - this._actionHookDelayResolved = false; - if (options.noResponseTimeoutMs) { - this.logger.debug(`CallSession:_startActionHookNoResponseTimer ${options.noResponseTimeoutMs}`); - this._actionHookNoResponseTimer = setTimeout(() => { - if (this._actionHookDelayRetryCount >= options.retries) { - this._jambonzHangup(); - } - const verb = options.actions[this._actionHookDelayRetryCount % options.actions.length]; - // Inject verb to main stack - const t = normalizeJambones(this.logger, [verb]) - .map((tdata) => makeTask(this.logger, tdata)); - if (t.length) { - t[0].on('playDone', (err) => { - if (err) this.logger.error({err}, `Call-Session:exec Error delay action, play ${verb}`); - if (!this._actionHookDelayResolved) { - this._startActionHookNoResponseTimer(options); - } - }); - } - this.tasks.push(...t); - if (this.wakeupResolver) { - this.wakeupResolver({reason: 'actionHook no response, applied delay actions', verb}); - this.wakeupResolver = null; - } - - this.logger.debug(`CallSession:_startActionHookNoResponseTimer, executing verb ${JSON.stringify(verb)}`); - - this._actionHookDelayRetryCount++; - }, options.noResponseTimeoutMs); - } - } - - _clearActionHookNoResponseTimer() { - if (this._actionHookNoResponseTimer) { - // Action Hook delay is solved. - this._actionHookDelayResolved = true; - clearTimeout(this._actionHookNoResponseTimer); - // if delay action is enabled - // and bot has responded with list of new verbs - // Only kill current running play task. - //https://github.com/jambonz/jambonz-feature-server/issues/710 - if (this.currentTask?.name === TaskName.Play) { - this.currentTask.kill(this); - } - } - this._actionHookNoResponseTimer = null; - } - - _startActionHookNoResponseGiveUpTimer(options) { - this._clearActionHookNoResponseGiveUpTimer(); - if (options.noResponseGiveUpTimeoutMs) { - this.logger.debug(`CallSession:_startActionHookNoResponseGiveUpTimer ${options.noResponseGiveUpTimeoutMs}`); - this._actionHookNoResponseGiveUpTimer = setTimeout(() => { - this.logger.debug('CallSession:_startActionHookNoResponseGiveUpTimer Timeout'); - this._jambonzHangup(); - this._actionHookNoResponseGiveUpTimer = null; - }, options.noResponseGiveUpTimeoutMs); - } - } - - _clearActionHookNoResponseGiveUpTimer() { - if (this._actionHookNoResponseGiveUpTimer) { - clearTimeout(this._actionHookNoResponseGiveUpTimer); - } - this._actionHookNoResponseGiveUpTimer = null; - } } module.exports = CallSession; diff --git a/lib/session/inbound-call-session.js b/lib/session/inbound-call-session.js index 20e6fcd6..fc27c1d7 100644 --- a/lib/session/inbound-call-session.js +++ b/lib/session/inbound-call-session.js @@ -71,7 +71,7 @@ class InboundCallSession extends CallSession { } _jambonzHangup() { - this._hangup(); + this.dlg?.destroy(); } _hangup(terminatedBy = 'jambonz') { diff --git a/lib/tasks/config.js b/lib/tasks/config.js index 526c1c85..c85da18f 100644 --- a/lib/tasks/config.js +++ b/lib/tasks/config.js @@ -262,11 +262,7 @@ class TaskConfig extends Task { } } if (Object.keys(this.actionHookDelayAction).length !== 0) { - cs.actionHookDelayEnabled = this.actionHookDelayAction.enabled || false; - cs.actionHookNoResponseTimeout = this.actionHookDelayAction.noResponseTimeout || 0; - cs.actionHookNoResponseGiveUpTimeout = this.actionHookDelayAction.noResponseGiveUpTimeout || 0; - cs.actionHookDelayRetries = this.actionHookDelayAction.retries || 1; - cs.actionHookDelayActions = this.actionHookDelayAction.actions || []; + cs.actionHookDelayProperties = this.actionHookDelayAction; } if (this.data.sipRequestWithinDialogHook) { cs.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook; diff --git a/lib/tasks/gather.js b/lib/tasks/gather.js index 0ff05041..4a810d79 100644 --- a/lib/tasks/gather.js +++ b/lib/tasks/gather.js @@ -117,6 +117,7 @@ class TaskGather extends SttTask { } if (this.sayTask) s += ',with nested say task'; if (this.playTask) s += ',with nested play task'; + if (this.actionHookDelayAction) s += ',with actionHookDelayAction'; s += '}'; return s; } @@ -163,28 +164,15 @@ class TaskGather extends SttTask { this.interim = true; this.logger.debug('Gather:exec - early hints match enabled'); } - // actionHook delay - this._hookDelayEn = cs.actionHookDelayEnabled || !!this.actionHookDelayAction; - this._hookDelayActions = this.actionHookDelayAction?.actions || cs.actionHookDelayActions || []; - - // Only enable NoResponseTimeout if there is _hookDelayActions - this._hookNoResponseTimeout = (this._hookDelayActions?.length ? - (this.actionHookDelayAction?.noResponseTimeout || cs.actionHookNoResponseTimeout || 0) - : 0) * 1000; - - this._hookNoResponseGiveUpTimeout = (this.actionHookDelayAction?.noResponseGiveUpTimeout || - cs.actionHookNoResponseGiveUpTimeout || 0) * 1000; - - this._hookDelayRetries = this.actionHookDelayAction?.retries || cs.actionHookDelayRetries || 1; - this._hookDelayRetryCount = 0; - this.hookDelayActionOpts = { - enabled: this._hookDelayEn, - actions: this._hookDelayActions, - noResponseTimeoutMs: this._hookNoResponseTimeout, - noResponseGiveUpTimeoutMs: this._hookNoResponseGiveUpTimeout, - retries: this._hookDelayRetries - }; + // if we have actionHook delay, and the session does as well, stash the session config + if (this.actionHookDelayAction) { + if (cs.actionHookDelayProcessor) { + this.logger.debug('Gather:exec - stashing session-level ahd proprerties'); + cs.stashActionHookDelayProperties(); + } + cs.actionHookDelayProperties = this.actionHookDelayAction; + } this._startVad(); @@ -296,7 +284,6 @@ class TaskGather extends SttTask { kill(cs) { super.kill(cs); this._killAudio(cs); - this._killActionHookDelayAction(); this._clearFillerNoiseTimer(); this.ep.removeAllListeners('dtmf'); clearTimeout(this.interDigitTimer); @@ -592,98 +579,6 @@ class TaskGather extends SttTask { this.cs.hangup(); } - _actionHookDelaySayAction(verb) { - delete verb.verb; - this.logger.debug(`_actionHookDelaySayAction ${verb}`); - this._actionHookDelaySayTask = makeTask(this.logger, {say: verb}, this); - const {span, ctx} = this.startChildSpan(`actionHookDelayAction:${this._actionHookDelaySayTask.summary}`); - this._actionHookDelaySayTask.span = span; - this._actionHookDelaySayTask.ctx = ctx; - this._actionHookDelaySayTask.exec(this.cs, {ep: this.ep}); - this._actionHookDelaySayTask.on('playDone', (err) => { - this._actionHookDelaySayTask = null; - span.end(); - if (err) this.logger.error({err}, 'Gather:actionHookDelay Error playing tts'); - }); - } - - _killActionHookDelayAction() { - if (this._actionHookDelaySayTask && !this._actionHookDelaySayTask.killed) { - this._actionHookDelaySayTask.removeAllListeners('playDone'); - this._actionHookDelaySayTask.kill(this.cs); - this._actionHookDelaySayTask.span.end(); - this._actionHookDelaySayTask = null; - } - - if (this._actionHookDelayPlayTask && !this._actionHookDelayPlayTask.killed) { - this._actionHookDelayPlayTask.removeAllListeners('playDone'); - this._actionHookDelayPlayTask.kill(this.cs); - this._actionHookDelayPlayTask.span.end(); - this._actionHookDelayPlayTask = null; - } - } - - _actionHookDelayPlayAction(verb) { - delete verb.verb; - this.logger.debug(`_actionHookDelayPlayAction ${verb}`); - this._actionHookDelayPlayTask = makeTask(this.logger, {play: verb}, this); - const {span, ctx} = this.startChildSpan(`actionHookDelayAction:${this._actionHookDelayPlayTask.summary}`); - this._actionHookDelayPlayTask.span = span; - this._actionHookDelayPlayTask.ctx = ctx; - this._actionHookDelayPlayTask.exec(this.cs, {ep: this.ep}); - this._actionHookDelayPlayTask.on('playDone', (err) => { - this._actionHookDelayPlayTask = null; - span.end(); - if (err) this.logger.error({err}, 'Gather:actionHookDelay Error playing tts'); - }); - - } - - _startActionHookNoResponseTimer() { - assert(this._hookNoResponseTimeout > 0); - this._clearActionHookNoResponseTimer(); - this.logger.debug('startActionHookNoResponseTimer'); - this._actionHookNoResponseTimer = setTimeout(() => { - if (this._hookDelayRetryCount >= this._hookDelayRetries) { - this._hangupCall(); - return; - } - const verb = this._hookDelayActions[this._hookDelayRetryCount % this._hookDelayActions.length]; - if (verb.verb === 'say') { - this._actionHookDelaySayAction(verb); - } else if (verb.verb === 'play') { - this._actionHookDelayPlayAction(verb); - } - this._hookDelayRetryCount++; - this._startActionHookNoResponseTimer(); - - }, this._hookNoResponseTimeout); - - } - - _clearActionHookNoResponseTimer() { - if (this._actionHookNoResponseTimer) { - clearTimeout(this._actionHookNoResponseTimer); - } - this._actionHookNoResponseTimer = null; - } - - _startActionHookNoResponseGiveUpTimer() { - assert(this._hookNoResponseGiveUpTimeout > 0); - this._clearActionHookNoResponseGiveUpTimer(); - this.logger.debug('startActionHookNoResponseGiveUpTimer'); - this._actionHookNoResponseGiveUpTimer = setTimeout(() => { - this._hangupCall(); - }, this._hookNoResponseGiveUpTimeout); - } - - _clearActionHookNoResponseGiveUpTimer() { - if (this._actionHookNoResponseGiveUpTimer) { - clearTimeout(this._actionHookNoResponseGiveUpTimer); - } - this._actionHookNoResponseGiveUpTimer = null; - } - _startFastRecognitionTimer(evt) { assert(this.fastRecognitionTimeout > 0); this._clearFastRecognitionTimer(); @@ -1101,7 +996,10 @@ class TaskGather extends SttTask { this.logger.error({err}, 'Error stopping transcription'); }); } - if (this.resolved) return; + if (this.resolved) { + this.logger.debug('TaskGather:_resolve - already resolved'); + return; + } this.resolved = true; // If bargin is false and ws application return ack to verb:hook @@ -1125,15 +1023,13 @@ class TaskGather extends SttTask { return; } - // Enabled action Hook delay timer to applied actions - if (this._hookNoResponseTimeout > 0) { - this._startActionHookNoResponseTimer(); - } - - if (this._hookNoResponseGiveUpTimeout > 0) { - this._startActionHookNoResponseGiveUpTimer(); + // action hook delay + if (this.cs.actionHookDelayProcessor) { + this.logger.debug('TaskGather:_resolve - actionHookDelayProcessor exists - starting it'); + this.cs.actionHookDelayProcessor.start(); } + // TODO: remove and implement as actionHookDelay if (this.hasFillerNoise && (reason.startsWith('dtmf') || reason.startsWith('speech'))) { if (this.fillerNoiseStartDelaySecs > 0) { this._startFillerNoiseTimer(); @@ -1144,40 +1040,57 @@ class TaskGather extends SttTask { } } + let returnedVerbs = false; try { if (reason.startsWith('dtmf')) { if (this.parentTask) this.parentTask.emit('dtmf', evt); else { this.emit('dtmf', evt); - await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'}); + returnedVerbs = await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'}); } } else if (reason.startsWith('speech')) { if (this.parentTask) this.parentTask.emit('transcription', evt); else { this.emit('transcription', evt); - await this.performAction({speech: evt, reason: 'speechDetected'}); + this.logger.debug('TaskGather:_resolve - invoking performAction'); + returnedVerbs = await this.performAction({speech: evt, reason: 'speechDetected'}); + this.logger.debug({returnedVerbs}, 'TaskGather:_resolve - back from performAction'); } } else if (reason.startsWith('timeout')) { if (this.parentTask) this.parentTask.emit('timeout', evt); else { this.emit('timeout', evt); - await this.performAction({reason: 'timeout'}); + returnedVerbs = await this.performAction({reason: 'timeout'}); } } else if (reason.startsWith('stt-error')) { if (this.parentTask) this.parentTask.emit('stt-error', evt); else { this.emit('stt-error', evt); - await this.performAction({reason: 'error', details: evt.error}); + returnedVerbs = await this.performAction({reason: 'error', details: evt.error}); } } } catch (err) { /*already logged error*/ } - // Gather got response from hook, cancel all delay timers if there is any - this._clearActionHookNoResponseTimer(); - this._clearActionHookNoResponseGiveUpTimer(); + // Gather got response from hook, cancel actionHookDelay processing + this.logger.debug('TaskGather:_resolve - checking ahd'); + if (this.cs.actionHookDelayProcessor) { + if (returnedVerbs) { + this.logger.debug('TaskGather:_resolve - got response from action hook, cancelling actionHookDelay'); + await this.cs.actionHookDelayProcessor.stop(); + if (this.actionHookDelayAction && !this.cs.popActionHookDelayProperties()) { + // no session level ahd was running when this task started, so clear it + this.cs.clearActionHookDelayProcessor(); + this.logger.debug('TaskGather:_resolve - clear ahd'); + } + } + else { + this.logger.debug('TaskGather:_resolve - no response from action hook, continue actionHookDelay'); + } + } + this._clearFillerNoiseTimer(); this.notifyTaskDone(); diff --git a/lib/tasks/say.js b/lib/tasks/say.js index 5f6817b9..8af575e3 100644 --- a/lib/tasks/say.js +++ b/lib/tasks/say.js @@ -108,7 +108,7 @@ class TaskSay extends TtsTask { // If vendor is changed from the previous one, then reset the cache_speech_handles flag cs.currentTtsVendor = vendor; - if (!preCache) this.logger.info({vendor, language, voice, model}, 'TaskSay:exec'); + if (!preCache && !this._disableTracing) this.logger.info({vendor, language, voice, model}, 'TaskSay:exec'); try { if (!credentials) { writeAlerts({ @@ -127,7 +127,7 @@ class TaskSay extends TtsTask { if (text.startsWith('silence_stream://')) return text; /* otel: trace time for tts */ - if (!preCache) { + if (!preCache && !this._disableTracing) { const {span} = this.startChildSpan('tts-generation', { 'tts.vendor': vendor, 'tts.language': language, @@ -162,7 +162,7 @@ class TaskSay extends TtsTask { lastUpdated = true; updateSpeechCredentialLastUsed(credentials.speech_credential_sid).catch(() => {/* logged error */}); } - if (!servedFromCache && rtt && !preCache) { + if (!servedFromCache && rtt && !preCache && !this._disableTracing) { this.notifyStatus({ event: 'synthesized-audio', vendor, diff --git a/lib/tasks/task.js b/lib/tasks/task.js index 556da88c..5b587e1d 100644 --- a/lib/tasks/task.js +++ b/lib/tasks/task.js @@ -45,6 +45,10 @@ class Task extends Emitter { return this.name; } + set disableTracing(val) { + this._disableTracing = val; + } + toJSON() { return this.data; } @@ -177,15 +181,16 @@ class Task extends Emitter { // If actionHook delay action is configured, and ws application have not responded yet any verb for actionHook // We have to transfer the task to call-session to await on next ws command verbs, and also run action Hook // delay actions - if (this.hookDelayActionOpts) { - this.emit('ActionHookDelayActionOptions', this.hookDelayActionOpts); - } + //if (this.hookDelayActionOpts) { + // this.emit('ActionHookDelayActionOptions', this.hookDelayActionOpts); + //} } if (expectResponse && json && Array.isArray(json)) { const makeTask = require('./make_task'); const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata)); if (tasks && tasks.length > 0) { this.callSession.replaceApplication(tasks); + return true; } } } catch (err) { @@ -193,6 +198,7 @@ class Task extends Emitter { span.end(); throw err; } + return false; } } diff --git a/lib/utils/action-hook-delay.js b/lib/utils/action-hook-delay.js new file mode 100644 index 00000000..b5176b30 --- /dev/null +++ b/lib/utils/action-hook-delay.js @@ -0,0 +1,158 @@ +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;