diff --git a/packages/calling/src/CallingClient/calling/call.test.ts b/packages/calling/src/CallingClient/calling/call.test.ts index 30c8b760160..c8dfd8e1dda 100644 --- a/packages/calling/src/CallingClient/calling/call.test.ts +++ b/packages/calling/src/CallingClient/calling/call.test.ts @@ -963,10 +963,11 @@ describe('State Machine handler tests', () => { await flushPromises(3); expect(setInterval).toHaveBeenCalledTimes(1); + expect(setInterval).toHaveBeenCalledWith(expect.any(Function), DEFAULT_SESSION_TIMER); expect(funcSpy).toBeCalledTimes(1); expect(logSpy).toBeCalledWith('Session refresh successful', { file: 'call', - method: 'handleCallEstablished', + method: 'scheduleCallKeepaliveInterval', }); expect(logSpy).toHaveBeenCalledWith( `${METHOD_START_MESSAGE} with: ${call.getCorrelationId()}`, @@ -1023,11 +1024,6 @@ describe('State Machine handler tests', () => { const funcSpy = jest.spyOn(call, 'postStatus').mockRejectedValue(statusPayload); - if (call['sessionTimer'] === undefined) { - /* In cases where this test is run independently/alone, there is no sessionTimer initiated - Thus we will check and initialize the timer when not present by calling handleCallEstablish() */ - call['handleCallEstablished']({} as CallEvent); - } call['handleCallEstablished']({} as CallEvent); jest.advanceTimersByTime(DEFAULT_SESSION_TIMER); @@ -1035,11 +1031,9 @@ describe('State Machine handler tests', () => { /* This is to flush all the promises from the Promise queue so that * Jest.fakeTimers can advance time and also clear the promise Queue */ + await flushPromises(2); - await Promise.resolve(); - await Promise.resolve(); - - expect(clearInterval).toHaveBeenCalledTimes(2); // check this + expect(clearInterval).toHaveBeenCalledTimes(1); expect(funcSpy).toBeCalledTimes(1); }); @@ -1053,7 +1047,7 @@ describe('State Machine handler tests', () => { const okPayload = ({statusCode: 200, body: {}}); - const sendEvtSpy = jest.spyOn(call as any, 'sendCallStateMachineEvt'); + const scheduleKeepaliveSpy = jest.spyOn(call as any, 'scheduleCallKeepaliveInterval'); const postStatusSpy = jest .spyOn(call as any, 'postStatus') .mockRejectedValueOnce(errorPayload) @@ -1064,19 +1058,19 @@ describe('State Machine handler tests', () => { } jest.advanceTimersByTime(DEFAULT_SESSION_TIMER); - await Promise.resolve(); - await Promise.resolve(); + await flushPromises(2); expect(postStatusSpy).toHaveBeenCalledTimes(1); - expect(sendEvtSpy).toHaveBeenCalledWith({type: 'E_CALL_ESTABLISHED'}); + + // Now advance by 1 second for the retry-after interval + jest.advanceTimersByTime(1000); + await flushPromises(2); + + expect(postStatusSpy).toHaveBeenCalledTimes(2); + expect(scheduleKeepaliveSpy).toHaveBeenCalledTimes(2); }); it('keepalive ends after reaching max retry count', async () => { - const resolvePromise = async () => { - await Promise.resolve(); - await Promise.resolve(); - }; - const errorPayload = ({ statusCode: 500, headers: { @@ -1097,30 +1091,32 @@ describe('State Machine handler tests', () => { // Advance timer to trigger the first failure (uses DEFAULT_SESSION_TIMER) jest.advanceTimersByTime(DEFAULT_SESSION_TIMER); - await resolvePromise(); + await flushPromises(2); - // Now advance by 1 second for each of the 3 more retry attempts (retry-after: 1 second each) + // Now advance by 1 second for each of the 4 retry attempts (retry-after: 1 second each) // Need to do this separately to allow state machine to process and create new intervals jest.advanceTimersByTime(1000); - await resolvePromise(); + await flushPromises(2); jest.advanceTimersByTime(1000); - await resolvePromise(); + await flushPromises(2); + + jest.advanceTimersByTime(1000); + await flushPromises(2); jest.advanceTimersByTime(1000); - await resolvePromise(); + await flushPromises(2); // The error handler should detect we're at max retry count and stop expect(warnSpy).toHaveBeenCalledWith( - `Max call keepalive retry attempts reached for call: ${call.getCorrelationId()}`, + `Max keepalive retry attempts reached. Aborting call keepalive for callId: ${call.getCallId()}`, { file: 'call', - method: 'handleCallEstablished', + method: 'keepaliveRetryCallback', } ); - expect(postStatusSpy).toHaveBeenCalledTimes(4); - expect(call['callKeepaliveRetryCount']).toBe(0); - expect(call['sessionTimer']).toBeUndefined(); + expect(postStatusSpy).toHaveBeenCalledTimes(5); + expect(call['callKeepaliveRetryCount']).toBe(4); }); it('state changes during successful incoming call', async () => { diff --git a/packages/calling/src/CallingClient/calling/call.ts b/packages/calling/src/CallingClient/calling/call.ts index 3fdfec0c92f..368e91b7f47 100644 --- a/packages/calling/src/CallingClient/calling/call.ts +++ b/packages/calling/src/CallingClient/calling/call.ts @@ -172,8 +172,6 @@ export class Call extends Eventing implements ICall { private callKeepaliveRetryCount = 0; - private callKeepaliveInterval?: number; - /** * Getter to check if the call is muted or not. * @@ -1499,103 +1497,110 @@ export class Call extends Eventing implements ICall { this.sendCallStateMachineEvt({type: 'E_CALL_CLEARED'}); } - /** - * Handle Call Established - Roap related negotiations. - * - * @param event - Call Events. - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - private handleCallEstablished(event: CallEvent) { - const loggerContext = { - file: CALL_FILE, - method: METHODS.HANDLE_CALL_ESTABLISHED, - }; + private callKeepaliveRetryCallback = (interval: number) => { + if (this.callKeepaliveRetryCount === MAX_CALL_KEEPALIVE_RETRY_COUNT) { + log.warn( + `Max keepalive retry attempts reached. Aborting call keepalive for callId: ${this.callId}`, + { + file: CALL_FILE, + method: 'keepaliveRetryCallback', + } + ); - log.info(`${METHOD_START_MESSAGE} with: ${this.getCorrelationId()}`, loggerContext); + return; + } - this.emit(CALL_EVENT_KEYS.ESTABLISHED, this.correlationId); + this.callKeepaliveRetryCount += 1; - /* Reset Early dialog parameters */ - this.earlyMedia = false; + setTimeout(async () => { + try { + await this.postStatus(); + this.scheduleCallKeepaliveInterval(); + } catch (err: unknown) { + await this.handleCallKeepaliveError(err); + } + }, interval * 1000); + }; - this.connected = true; + private handleCallKeepaliveError = async (err: unknown) => { + const error = err; - /* Session timers need to be reset at all offer/answer exchanges */ + /* We are clearing the timer here as all are error scenarios. Only scenario where + * timer reset won't be required is 503 with retry after. But that case will + * be handled automatically as Mobius will also reset timer when we post status + * in retry-after scenario. + */ + /* istanbul ignore next */ if (this.sessionTimer) { - log.log('Resetting session timer', loggerContext); - clearInterval(this.sessionTimer); } + const abort = await handleCallErrors( + (callError: CallError) => { + this.emit(CALL_EVENT_KEYS.CALL_ERROR, callError); + this.submitCallErrorMetric(callError); + }, + ERROR_LAYER.CALL_CONTROL, + this.callKeepaliveRetryCallback, + this.getCorrelationId(), + error, + 'handleCallEstablished', + CALL_FILE + ); + + if (abort) { + this.sendCallStateMachineEvt({type: 'E_SEND_CALL_DISCONNECT'}); + this.emit(CALL_EVENT_KEYS.DISCONNECT, this.getCorrelationId()); + this.callKeepaliveRetryCount = 0; + } + + await uploadLogs({ + correlationId: this.correlationId, + callId: this.callId, + broadworksCorrelationInfo: this.broadworksCorrelationInfo, + }); + }; + + private scheduleCallKeepaliveInterval = () => { + const loggerContext = { + file: CALL_FILE, + method: 'scheduleCallKeepaliveInterval', + }; + this.sessionTimer = setInterval(async () => { try { // eslint-disable-next-line @typescript-eslint/no-unused-vars const res = await this.postStatus(); - this.callKeepaliveRetryCount = 0; - this.callKeepaliveInterval = undefined; log.info(`Session refresh successful`, loggerContext); } catch (err: unknown) { - const error = err; + await this.handleCallKeepaliveError(err); + } + }, DEFAULT_SESSION_TIMER); + }; - /* We are clearing the timer here as all are error scenarios. Only scenario where - * timer reset won't be required is 503 with retry after. But that case will - * be handled automatically as Mobius will also reset timer when we post status - * in retry-after scenario. - */ - /* istanbul ignore next */ - if (this.sessionTimer) { - clearInterval(this.sessionTimer); - } + /** + * Handle Call Established - Roap related negotiations. + * + * @param event - Call Events. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private handleCallEstablished(event: CallEvent) { + const loggerContext = { + file: CALL_FILE, + method: METHODS.HANDLE_CALL_ESTABLISHED, + }; - const abort = await handleCallErrors( - (callError: CallError) => { - this.emit(CALL_EVENT_KEYS.CALL_ERROR, callError); - this.submitCallErrorMetric(callError); - }, - ERROR_LAYER.CALL_CONTROL, - (interval: number) => { - this.callKeepaliveRetryCount += 1; - this.callKeepaliveInterval = interval * 1000; - - // If we have reached the max retry count, do not attempt to refresh the session - if (this.callKeepaliveRetryCount === MAX_CALL_KEEPALIVE_RETRY_COUNT) { - this.callKeepaliveRetryCount = 0; - clearInterval(this.sessionTimer); - this.sessionTimer = undefined; - this.callKeepaliveInterval = undefined; - - log.warn( - `Max call keepalive retry attempts reached for call: ${this.getCorrelationId()}`, - loggerContext - ); - - return; - } + log.info(`${METHOD_START_MESSAGE} with: ${this.getCorrelationId()}`, loggerContext); - // Scheduling next keepalive attempt - calling handleCallEstablished - this.sendCallStateMachineEvt({type: 'E_CALL_ESTABLISHED'}); - }, - this.getCorrelationId(), - error, - 'handleCallEstablished', - CALL_FILE - ); + this.emit(CALL_EVENT_KEYS.ESTABLISHED, this.correlationId); - if (abort) { - this.sendCallStateMachineEvt({type: 'E_SEND_CALL_DISCONNECT'}); - this.emit(CALL_EVENT_KEYS.DISCONNECT, this.getCorrelationId()); - this.callKeepaliveRetryCount = 0; - this.callKeepaliveInterval = undefined; - } + /* Reset Early dialog parameters */ + this.earlyMedia = false; - await uploadLogs({ - correlationId: this.correlationId, - callId: this.callId, - broadworksCorrelationInfo: this.broadworksCorrelationInfo, - }); - } - }, this.callKeepaliveInterval || DEFAULT_SESSION_TIMER); + this.connected = true; + + this.scheduleCallKeepaliveInterval(); } /**