diff --git a/CHANGELOG.md b/CHANGELOG.md index 50367f1..11ef966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + ## [2.2.3] ### Fixed @@ -11,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - enable message receipts by default; closes #132 - expose deep heartbeat success/failure callback to clients + ## [2.2.2] ### Fixed - reject send callbacks instead of returning null diff --git a/README.md b/README.md index 3f69ff4..9e4ee02 100644 --- a/README.md +++ b/README.md @@ -700,6 +700,17 @@ chatSession.onConnectionLost(event => { Subscribes an event handler that triggers when the session is lost. +##### `chatSession.onDeepHeartbeatFailure()` + +```js +chatSession.onDeepHeartbeatFailure(event => { + const { chatDetails, data } = event; + // ... +}); +``` + +Subscribes an event handler that triggers when deep heartbeat fails. + #### Client side metric In version `1.2.0` the client side metric(CSM) service is added into this library. Client side metric can provide insights into the real performance and usability, it helps us to understand how customers are actually using the website and what UI experiences they prefer. This feature is enabled by default. User can also disable this feature by passing a flag: `disableCSM` when they create a new chat session: diff --git a/src/constants.js b/src/constants.js index ccbd394..21fe600 100644 --- a/src/constants.js +++ b/src/constants.js @@ -48,7 +48,9 @@ export const WEBSOCKET_EVENTS = { ConnectionGained: "WebsocketConnectionGained", Ended: "WebsocketEnded", IncomingMessage: "WebsocketIncomingMessage", - InitWebsocket: "InitWebsocket" + InitWebsocket: "InitWebsocket", + DeepHeartbeatSuccess: "WebsocketDeepHeartbeatSuccess", + DeepHeartbeatFailure: "WebsocketDeepHeartbeatFailure" }; export const CHAT_EVENTS = { @@ -64,7 +66,9 @@ export const CHAT_EVENTS = { MESSAGE_METADATA: "MESSAGEMETADATA", PARTICIPANT_IDLE: "PARTICIPANT_IDLE", PARTICIPANT_RETURNED: "PARTICIPANT_RETURNED", - AUTODISCONNECTION: "AUTODISCONNECTION" + AUTODISCONNECTION: "AUTODISCONNECTION", + DEEP_HEARTBEAT_SUCCESS: "DEEP_HEARTBEAT_SUCCESS", + DEEP_HEARTBEAT_FAILURE: "DEEP_HEARTBEAT_FAILURE" }; export const CONTENT_TYPE = { @@ -161,4 +165,4 @@ export const CREATE_PARTICIPANT_CONACK_FAILURE = "CREATE_PARTICIPANT_CONACK_FAIL export const SEND_EVENT_CONACK_FAILURE = "SEND_EVENT_CONACK_FAILURE"; export const CREATE_PARTICIPANT_CONACK_API_CALL_COUNT = "CREATE_PARTICIPANT_CONACK_CALL_COUNT"; -export const TYPING_VALIDITY_TIME = 10000; \ No newline at end of file +export const TYPING_VALIDITY_TIME = 10000; diff --git a/src/core/chatController.js b/src/core/chatController.js index dd61afb..75649f2 100644 --- a/src/core/chatController.js +++ b/src/core/chatController.js @@ -245,6 +245,8 @@ class ChatController { this.connectionHelper.onConnectionLost(this._handleLostConnection.bind(this)); this.connectionHelper.onConnectionGain(this._handleGainedConnection.bind(this)); this.connectionHelper.onMessage(this._handleIncomingMessage.bind(this)); + this.connectionHelper.onDeepHeartbeatSuccess(this._handleDeepHeartbeatSuccess.bind(this)); + this.connectionHelper.onDeepHeartbeatFailure(this._handleDeepHeartbeatFailure.bind(this)); return this.connectionHelper.start(); } @@ -281,6 +283,20 @@ class ChatController { }); } + _handleDeepHeartbeatSuccess(eventData) { + this._forwardChatEvent(CHAT_EVENTS.DEEP_HEARTBEAT_SUCCESS, { + data: eventData, + chatDetails: this.getChatDetails() + }); + } + + _handleDeepHeartbeatFailure(eventData) { + this._forwardChatEvent(CHAT_EVENTS.DEEP_HEARTBEAT_FAILURE, { + data: eventData, + chatDetails: this.getChatDetails() + }); + } + _handleIncomingMessage(incomingData) { try { let eventType = getEventTypeFromContentType(incomingData?.ContentType); @@ -437,6 +453,10 @@ class ChatController { return NetworkLinkStatus.Broken; case ConnectionHelperStatus.Connected: return NetworkLinkStatus.Established; + case ConnectionHelperStatus.DeepHeartbeatSuccess: + return NetworkLinkStatus.Established; + case ConnectionHelperStatus.DeepHeartbeatFailure: + return NetworkLinkStatus.Broken; } this._sendInternalLogToServer(this.logger.error( "Reached invalid state. Unknown connectionHelperStatus: ", diff --git a/src/core/chatController.spec.js b/src/core/chatController.spec.js index c16482d..30fc614 100644 --- a/src/core/chatController.spec.js +++ b/src/core/chatController.spec.js @@ -85,6 +85,8 @@ describe("ChatController", () => { onMessage: (handler) => { messageHandlers.push(handler); }, + onDeepHeartbeatSuccess: () => {}, + onDeepHeartbeatFailure: () => {}, start: () => startResponse, end: () => endResponse, getStatus: () => ConnectionHelperStatus.Connected, @@ -936,4 +938,32 @@ describe("ChatController", () => { expect(console.error).toBeCalledWith('Cannot call disconnectParticipant when participant is disconnected'); } }); + + test('_handleDeepHeartbeatSuccess is triggered correctly', () => { + const chatController = getChatController(); + chatController._forwardChatEvent = jest.fn(); // Mock _forwardChatEvent to spy on it + + // Directly invoke the method + chatController._handleDeepHeartbeatSuccess({ message: 'Heartbeat succeeded' }); + + // Check if _forwardChatEvent was called correctly + expect(chatController._forwardChatEvent).toHaveBeenCalledWith( + CHAT_EVENTS.DEEP_HEARTBEAT_SUCCESS, + { data: { message: 'Heartbeat succeeded' }, chatDetails: expect.anything() } + ); + }); + + test('_handleDeepHeartbeatFailure is triggered correctly', () => { + const chatController = getChatController(); + chatController._forwardChatEvent = jest.fn(); // Mock _forwardChatEvent to spy on it + + // Directly invoke the method + chatController._handleDeepHeartbeatFailure({ error: 'Heartbeat failed' }); + + // Check if _forwardChatEvent was called correctly + expect(chatController._forwardChatEvent).toHaveBeenCalledWith( + CHAT_EVENTS.DEEP_HEARTBEAT_FAILURE, + { data: { error: 'Heartbeat failed' }, chatDetails: expect.anything() } + ); + }); }); diff --git a/src/core/chatSession.js b/src/core/chatSession.js index 25ba743..2c0cb39 100644 --- a/src/core/chatSession.js +++ b/src/core/chatSession.js @@ -122,6 +122,14 @@ export class ChatSession { this.controller.subscribe(CHAT_EVENTS.CONNECTION_LOST, callback); } + onDeepHeartbeatSuccess(callback){ + this.controller.subscribe(CHAT_EVENTS.DEEP_HEARTBEAT_SUCCESS, callback); + } + + onDeepHeartbeatFailure(callback){ + this.controller.subscribe(CHAT_EVENTS.DEEP_HEARTBEAT_FAILURE, callback); + } + sendMessage(args) { return this.controller.sendMessage(args); } diff --git a/src/core/chatSession.spec.js b/src/core/chatSession.spec.js index 21c4a14..c225eb6 100644 --- a/src/core/chatSession.spec.js +++ b/src/core/chatSession.spec.js @@ -80,6 +80,8 @@ describe("chatSession", () => { const cb9 = jest.fn(); const cb10 = jest.fn(); const cb11 = jest.fn(); + const cb12 = jest.fn(); + const cb13 = jest.fn(); session.onParticipantIdle(cb1); session.onParticipantReturned(cb2); @@ -92,6 +94,8 @@ describe("chatSession", () => { session.onConnectionEstablished(cb9); session.onEnded(cb10); session.onConnectionLost(cb11); + session.onDeepHeartbeatSuccess(cb12); + session.onDeepHeartbeatFailure(cb13); controller._forwardChatEvent(CHAT_EVENTS.PARTICIPANT_IDLE, eventData); controller._forwardChatEvent(CHAT_EVENTS.PARTICIPANT_RETURNED, eventData); @@ -104,6 +108,8 @@ describe("chatSession", () => { controller._forwardChatEvent(CHAT_EVENTS.CONNECTION_ESTABLISHED, eventData); controller._forwardChatEvent(CHAT_EVENTS.CHAT_ENDED, eventData); controller._forwardChatEvent(CHAT_EVENTS.CONNECTION_LOST, eventData); + controller._forwardChatEvent(CHAT_EVENTS.DEEP_HEARTBEAT_SUCCESS, eventData); + controller._forwardChatEvent(CHAT_EVENTS.DEEP_HEARTBEAT_FAILURE, eventData); await new Promise((r) => setTimeout(r, 0)); @@ -118,6 +124,8 @@ describe("chatSession", () => { expect(cb9).toHaveBeenCalled(); expect(cb10).toHaveBeenCalled(); expect(cb11).toHaveBeenCalled(); + expect(cb12).toHaveBeenCalled(); + expect(cb13).toHaveBeenCalled(); }); test('events', () => { @@ -145,4 +153,4 @@ describe("chatSession", () => { session.getChatDetails(args); expect(controller.getChatDetails).toHaveBeenCalled(); }); -}); \ No newline at end of file +}); diff --git a/src/core/connectionHelpers/LpcConnectionHelper.js b/src/core/connectionHelpers/LpcConnectionHelper.js index 37526c4..0f4adff 100644 --- a/src/core/connectionHelpers/LpcConnectionHelper.js +++ b/src/core/connectionHelpers/LpcConnectionHelper.js @@ -48,7 +48,9 @@ class LpcConnectionHelper extends BaseConnectionHelper { this.baseInstance.onEnded(this.handleEnded.bind(this)), this.baseInstance.onConnectionGain(this.handleConnectionGain.bind(this)), this.baseInstance.onConnectionLost(this.handleConnectionLost.bind(this)), - this.baseInstance.onMessage(this.handleMessage.bind(this)) + this.baseInstance.onMessage(this.handleMessage.bind(this)), + this.baseInstance.onDeepHeartbeatSuccess(this.handleDeepHeartbeatSuccess.bind(this)), + this.baseInstance.onDeepHeartbeatFailure(this.handleDeepHeartbeatFailure.bind(this)) ]; } @@ -100,6 +102,22 @@ class LpcConnectionHelper extends BaseConnectionHelper { this.eventBus.trigger(ConnectionHelperEvents.ConnectionLost, {}); } + onDeepHeartbeatSuccess(handler) { + return this.eventBus.subscribe(ConnectionHelperEvents.DeepHeartbeatSuccess, handler); + } + + handleDeepHeartbeatSuccess() { + this.eventBus.trigger(ConnectionHelperEvents.DeepHeartbeatSuccess, {}); + } + + onDeepHeartbeatFailure(handler) { + return this.eventBus.subscribe(ConnectionHelperEvents.DeepHeartbeatFailure, handler); + } + + handleDeepHeartbeatFailure() { + this.eventBus.trigger(ConnectionHelperEvents.DeepHeartbeatFailure, {}); + } + onMessage(handler) { return this.eventBus.subscribe(ConnectionHelperEvents.IncomingMessage, handler); } @@ -133,7 +151,9 @@ class LpcConnectionHelperBase { this.websocketManager.onMessage("aws/chat", this.handleMessage.bind(this)), this.websocketManager.onConnectionGain(this.handleConnectionGain.bind(this)), this.websocketManager.onConnectionLost(this.handleConnectionLost.bind(this)), - this.websocketManager.onInitFailure(this.handleEnded.bind(this)) + this.websocketManager.onInitFailure(this.handleEnded.bind(this)), + this.websocketManager.onDeepHeartbeatSuccess(this.handleDeepHeartbeatSuccess.bind(this)), + this.websocketManager.onDeepHeartbeatFailure(this.handleDeepHeartbeatFailure.bind(this)) ]; this.logger.info("Initializing websocket manager."); if (!websocketManager) { @@ -271,6 +291,28 @@ class LpcConnectionHelperBase { return logEntry; } + + onDeepHeartbeatSuccess(handler) { + return this.eventBus.subscribe(ConnectionHelperEvents.DeepHeartbeatSuccess, handler); + } + + handleDeepHeartbeatSuccess() { + this.status = ConnectionHelperStatus.DeepHeartbeatSuccess; + this.eventBus.trigger(ConnectionHelperEvents.DeepHeartbeatSuccess, {}); + csmService.addCountMetric(WEBSOCKET_EVENTS.DeepHeartbeatSuccess, CSM_CATEGORY.API); + this.logger.info("Websocket deep heartbeat success."); + } + + onDeepHeartbeatFailure(handler) { + return this.eventBus.subscribe(ConnectionHelperEvents.DeepHeartbeatFailure, handler); + } + + handleDeepHeartbeatFailure() { + this.status = ConnectionHelperStatus.DeepHeartbeatFailure; + this.eventBus.trigger(ConnectionHelperEvents.DeepHeartbeatFailure, {}); + csmService.addCountMetric(WEBSOCKET_EVENTS.DeepHeartbeatFailure, CSM_CATEGORY.API); + this.logger.info("Websocket deep heartbeat failure."); + } } export default LpcConnectionHelper; diff --git a/src/core/connectionHelpers/LpcConnectionHelper.spec.js b/src/core/connectionHelpers/LpcConnectionHelper.spec.js index 1658727..815be53 100644 --- a/src/core/connectionHelpers/LpcConnectionHelper.spec.js +++ b/src/core/connectionHelpers/LpcConnectionHelper.spec.js @@ -24,6 +24,8 @@ describe("LpcConnectionHelper", () => { const connectionGainHandlers = []; const endedHandlers = []; const refreshHandlers = []; + const deepHeartbeatSuccessHandlers = []; + const deepHeartbeatFailureHandlers = []; return { subscribeTopics: jest.fn(() => { }), @@ -39,6 +41,14 @@ describe("LpcConnectionHelper", () => { connectionLostHandlers.push(handler); return () => { }; }), + onDeepHeartbeatSuccess: jest.fn((handler) => { + deepHeartbeatSuccessHandlers.push(handler); + return () => { }; + }), + onDeepHeartbeatFailure: jest.fn((handler) => { + deepHeartbeatFailureHandlers.push(handler); + return () => { }; + }), onInitFailure: jest.fn((handler) => { endedHandlers.push(handler); return () => { }; @@ -55,6 +65,12 @@ describe("LpcConnectionHelper", () => { $simulateConnectionGain() { connectionGainHandlers.forEach(f => f()); }, + $simulateDeepHeartbeatSuccess() { + deepHeartbeatSuccessHandlers.forEach(f => f()); + }, + $simulateDeepHeartbeatFailure() { + deepHeartbeatFailureHandlers.forEach(f => f()); + }, $simulateEnded() { endedHandlers.forEach(f => f()); }, @@ -99,6 +115,8 @@ describe("LpcConnectionHelper", () => { expect(websocketManager.onMessage).toHaveBeenCalledWith("aws/chat", expect.any(Function)); expect(websocketManager.onConnectionGain).toHaveBeenCalledTimes(1); expect(websocketManager.onConnectionLost).toHaveBeenCalledTimes(1); + expect(websocketManager.onDeepHeartbeatSuccess).toHaveBeenCalledTimes(1); + expect(websocketManager.onDeepHeartbeatFailure).toHaveBeenCalledTimes(1); }); test("WebsocketManager will only be initialized once", () => { @@ -109,6 +127,8 @@ describe("LpcConnectionHelper", () => { expect(websocketManager.onMessage).toHaveBeenCalledTimes(1); expect(websocketManager.onConnectionGain).toHaveBeenCalledTimes(1); expect(websocketManager.onConnectionLost).toHaveBeenCalledTimes(1); + expect(websocketManager.onDeepHeartbeatSuccess).toHaveBeenCalledTimes(1); + expect(websocketManager.onDeepHeartbeatFailure).toHaveBeenCalledTimes(1); }); test("onConnectionLost handler is called", () => { @@ -133,6 +153,28 @@ describe("LpcConnectionHelper", () => { expect(onConnectionGainHandler2).toHaveBeenCalledTimes(1); }); + test("onDeepHeartbeatSuccess handler is called", () => { + const websocketManager = createWebsocketManager(); + const onDeepHeartbeatSuccessHandler1 = jest.fn(); + const onDeepHeartbeatSuccessHandler2 = jest.fn(); + getLpcConnectionHelper("id1", websocketManager).onDeepHeartbeatSuccess(onDeepHeartbeatSuccessHandler1); + getLpcConnectionHelper("id2", websocketManager).onDeepHeartbeatSuccess(onDeepHeartbeatSuccessHandler2); + websocketManager.$simulateDeepHeartbeatSuccess(); + expect(onDeepHeartbeatSuccessHandler1).toHaveBeenCalledTimes(1); + expect(onDeepHeartbeatSuccessHandler2).toHaveBeenCalledTimes(1); + }); + + test("onDeepHeartbeatFailure handler is called", () => { + const websocketManager = createWebsocketManager(); + const onDeepHeartbeatFailureHandler1 = jest.fn(); + const onDeepHeartbeatFailureHandler2 = jest.fn(); + getLpcConnectionHelper("id1", websocketManager).onDeepHeartbeatFailure(onDeepHeartbeatFailureHandler1); + getLpcConnectionHelper("id2", websocketManager).onDeepHeartbeatFailure(onDeepHeartbeatFailureHandler2); + websocketManager.$simulateDeepHeartbeatFailure(); + expect(onDeepHeartbeatFailureHandler1).toHaveBeenCalledTimes(1); + expect(onDeepHeartbeatFailureHandler2).toHaveBeenCalledTimes(1); + }); + test("onMessage handler is called", () => { const websocketManager = createWebsocketManager(); const onMessageHandler1 = jest.fn(); @@ -201,6 +243,8 @@ describe("LpcConnectionHelper", () => { expect(autoCreatedWebsocketManager.onMessage).toHaveBeenCalledWith("aws/chat", expect.any(Function)); expect(autoCreatedWebsocketManager.onConnectionGain).toHaveBeenCalledTimes(1); expect(autoCreatedWebsocketManager.onConnectionLost).toHaveBeenCalledTimes(1); + expect(autoCreatedWebsocketManager.onDeepHeartbeatSuccess).toHaveBeenCalledTimes(1); + expect(autoCreatedWebsocketManager.onDeepHeartbeatFailure).toHaveBeenCalledTimes(1); expect(autoCreatedWebsocketManager.init).toHaveBeenCalledTimes(1); }); @@ -212,6 +256,8 @@ describe("LpcConnectionHelper", () => { expect(createdWebsocketManager.onMessage).toHaveBeenCalledTimes(1); expect(createdWebsocketManager.onConnectionGain).toHaveBeenCalledTimes(1); expect(createdWebsocketManager.onConnectionLost).toHaveBeenCalledTimes(1); + expect(autoCreatedWebsocketManager.onDeepHeartbeatSuccess).toHaveBeenCalledTimes(1); + expect(autoCreatedWebsocketManager.onDeepHeartbeatFailure).toHaveBeenCalledTimes(1); expect(createdWebsocketManager.init).toHaveBeenCalledTimes(1); }); @@ -257,6 +303,26 @@ describe("LpcConnectionHelper", () => { expect(csmService.addCountMetric).toHaveBeenCalledWith(WEBSOCKET_EVENTS.ConnectionGained, CSM_CATEGORY.API); }); + test("onDeepHeartbeatSuccess handler is called", () => { + const onDeepHeartbeatSuccessHandler = jest.fn(); + getLpcConnectionHelper("id1").onDeepHeartbeatSuccess(onDeepHeartbeatSuccessHandler); + const createdWebsocketManager = autoCreatedWebsocketManager; + createdWebsocketManager.$simulateDeepHeartbeatSuccess(); + expect(onDeepHeartbeatSuccessHandler).toHaveBeenCalledTimes(1); + expect(csmService.addCountMetric).toHaveBeenCalledTimes(1); + expect(csmService.addCountMetric).toHaveBeenCalledWith(WEBSOCKET_EVENTS.DeepHeartbeatSuccess, CSM_CATEGORY.API); + }); + + test("onDeepHeartbeatFailure handler is called", () => { + const onDeepHeartbeatFailureHandler = jest.fn(); + getLpcConnectionHelper("id1").onDeepHeartbeatFailure(onDeepHeartbeatFailureHandler); + const createdWebsocketManager = autoCreatedWebsocketManager; + createdWebsocketManager.$simulateDeepHeartbeatFailure(); + expect(onDeepHeartbeatFailureHandler).toHaveBeenCalledTimes(1); + expect(csmService.addCountMetric).toHaveBeenCalledTimes(1); + expect(csmService.addCountMetric).toHaveBeenCalledWith(WEBSOCKET_EVENTS.DeepHeartbeatFailure, CSM_CATEGORY.API); + }); + test("onMessage handler is called", () => { const onMessageHandler = jest.fn(); getLpcConnectionHelper("id1").onMessage(onMessageHandler); @@ -301,4 +367,4 @@ describe("LpcConnectionHelper", () => { expect(connectionHelper.getStatus()).toBe(ConnectionHelperStatus.Ended); }); }); -}); \ No newline at end of file +}); diff --git a/src/core/connectionHelpers/baseConnectionHelper.js b/src/core/connectionHelpers/baseConnectionHelper.js index e4a194a..53dd657 100644 --- a/src/core/connectionHelpers/baseConnectionHelper.js +++ b/src/core/connectionHelpers/baseConnectionHelper.js @@ -5,14 +5,18 @@ const ConnectionHelperStatus = { Starting: "Starting", Connected: "Connected", ConnectionLost: "ConnectionLost", - Ended: "Ended" + Ended: "Ended", + DeepHeartbeatSuccess: "DeepHeartbeatSuccess", + DeepHeartbeatFailure: "DeepHeartbeatFailure" }; const ConnectionHelperEvents = { ConnectionLost: "ConnectionLost", // event data is: {reason: ...} ConnectionGained: "ConnectionGained", // event data is: {reason: ...} Ended: "Ended", // event data is: {reason: ...} - IncomingMessage: "IncomingMessage" // event data is: {payloadString: ...} + IncomingMessage: "IncomingMessage", // event data is: {payloadString: ...} + DeepHeartbeatSuccess: "DeepHeartbeatSuccess", + DeepHeartbeatFailure: "DeepHeartbeatFailure" }; const ConnectionInfoType = {