@@ -5,6 +5,16 @@ import { getOrganizationId, getSalesforceMessagingUrl, getJwt, getLastEventId }
55 */
66let eventSource ;
77
8+ /**
9+ * Configuration for reconnection behavior
10+ */
11+ const RECONNECT_CONFIG = {
12+ maxAttempts : 10 ,
13+ initialDelay : 1000 , // 1 second
14+ maxDelay : 30000 , // 30 seconds
15+ backoffMultiplier : 1.5
16+ } ;
17+
818/**
919 * Get the request headers for connecting to SSE.
1020 */
@@ -51,6 +61,17 @@ const handleEventSourceListeners = (listenerOperationName, eventListenerMap) =>
5161 } ) ;
5262} ;
5363
64+ /**
65+ * Calculate the delay for the next reconnection attempt using exponential backoff.
66+ *
67+ * @param {Number } attemptNumber - The current attempt number (0-indexed)
68+ * @returns {Number } Delay in milliseconds
69+ */
70+ const calculateReconnectDelay = ( attemptNumber ) => {
71+ const delay = RECONNECT_CONFIG . initialDelay * Math . pow ( RECONNECT_CONFIG . backoffMultiplier , attemptNumber ) ;
72+ return Math . min ( delay , RECONNECT_CONFIG . maxDelay ) ;
73+ } ;
74+
5475/**
5576 * Establish the EventSource object with handlers for onopen and onerror.
5677 *
@@ -70,26 +91,92 @@ export const createEventSource = (fullApiPath, eventListenerMap) => {
7091 }
7192
7293 /**
73- * Create a closure here to isolate `reconnectAttempts` and `reconnectIntervalSeconds` values to a single invocation of `createEventSource`.
74- * All calls to `resolveEventSource` within a call to `createEventSource` share the same `reconnectAttempts` and `reconnectIntervalSeconds` values .
94+ * Create a closure here to isolate `reconnectAttempts` and reconnect logic to a single invocation of `createEventSource`.
95+ * All calls to `resolveEventSource` within a call to `createEventSource` share the same `reconnectAttempts` value .
7596 *
7697 * @param {Promise.resolve } resolve - Event source opened.
7798 * @param {Promise.reject } reject - Attempted to reconnect to event source too many times.
7899 */
79100 const resolveEventSource = ( resolve , reject ) => {
80- try {
81- eventSource = new window . EventSourcePolyfill ( fullApiPath , getEventSourceParams ( ) ) ;
82-
83- eventSource . onopen = ( ) => {
84- handleEventSourceListeners ( "addEventListener" , eventListenerMap ) ;
85- resolve ( ) ;
86- } ;
87- eventSource . onerror = ( error ) => {
88- reject ( ) ;
89- } ;
90- } catch ( error ) {
91- reject ( error ) ;
92- }
101+ let reconnectAttempts = 0 ;
102+ let reconnectTimeoutId = null ;
103+
104+ /**
105+ * Attempts to create a new EventSource connection.
106+ * This function is called initially and on each reconnection attempt.
107+ */
108+ const attemptConnection = ( ) => {
109+ try {
110+ // Close existing eventSource if it exists
111+ if ( eventSource ) {
112+ try {
113+ eventSource . close ( ) ;
114+ } catch ( closeError ) {
115+ // Ignore errors when closing, as the connection may already be closed
116+ console . warn ( "Error closing existing EventSource:" , closeError ) ;
117+ }
118+ }
119+
120+ // Create new EventSource with timestamp to avoid caching
121+ const apiPathWithTimestamp = `${ fullApiPath } ${ fullApiPath . includes ( '?' ) ? '&' : '?' } _ts=${ Date . now ( ) } ` ;
122+ eventSource = new window . EventSourcePolyfill ( apiPathWithTimestamp , getEventSourceParams ( ) ) ;
123+
124+ eventSource . onopen = ( ) => {
125+ // Reset reconnect attempts on successful connection
126+ reconnectAttempts = 0 ;
127+ if ( reconnectTimeoutId ) {
128+ clearTimeout ( reconnectTimeoutId ) ;
129+ reconnectTimeoutId = null ;
130+ }
131+ handleEventSourceListeners ( "addEventListener" , eventListenerMap ) ;
132+ resolve ( ) ;
133+ } ;
134+
135+ eventSource . onerror = ( error ) => {
136+ // Remove event listeners to prevent duplicates, then attenpt to reconnect
137+ handleEventSourceListeners ( "removeEventListener" , eventListenerMap ) ;
138+ reconnectAttempts ++ ;
139+
140+ if ( reconnectAttempts <= RECONNECT_CONFIG . maxAttempts ) {
141+ const delay = calculateReconnectDelay ( reconnectAttempts - 1 ) ;
142+ console . log ( `EventSource connection error. Attempting to reconnect (${ reconnectAttempts } /${ RECONNECT_CONFIG . maxAttempts } ) in ${ delay } ms...` ) ;
143+
144+ reconnectTimeoutId = setTimeout ( ( ) => {
145+ attemptConnection ( ) ;
146+ } , delay ) ;
147+ } else {
148+ console . error ( `EventSource connection failed after ${ RECONNECT_CONFIG . maxAttempts } reconnection attempts.` ) ;
149+ if ( reconnectTimeoutId ) {
150+ clearTimeout ( reconnectTimeoutId ) ;
151+ reconnectTimeoutId = null ;
152+ }
153+ reject ( new Error ( `Failed to establish EventSource connection after ${ RECONNECT_CONFIG . maxAttempts } attempts` ) ) ;
154+ }
155+
156+ } ;
157+ } catch ( error ) {
158+ reconnectAttempts ++ ;
159+
160+ if ( reconnectAttempts <= RECONNECT_CONFIG . maxAttempts ) {
161+ const delay = calculateReconnectDelay ( reconnectAttempts - 1 ) ;
162+ console . log ( `Error creating EventSource. Attempting to reconnect (${ reconnectAttempts } /${ RECONNECT_CONFIG . maxAttempts } ) in ${ delay } ms...` ) ;
163+
164+ reconnectTimeoutId = setTimeout ( ( ) => {
165+ attemptConnection ( ) ;
166+ } , delay ) ;
167+ } else {
168+ console . error ( `Failed to create EventSource after ${ RECONNECT_CONFIG . maxAttempts } attempts.` ) ;
169+ if ( reconnectTimeoutId ) {
170+ clearTimeout ( reconnectTimeoutId ) ;
171+ reconnectTimeoutId = null ;
172+ }
173+ reject ( error ) ;
174+ }
175+ }
176+ } ;
177+
178+ // Start the initial connection attempt
179+ attemptConnection ( ) ;
93180 } ;
94181
95182 return new Promise ( resolveEventSource ) ;
@@ -114,7 +201,7 @@ export const subscribeToEventSource = (eventListenerMap) => {
114201 * Directly connect to event router endpoint on Salesforce Messaging domain instead of going through ia-message.
115202 */
116203 createEventSource (
117- getSalesforceMessagingUrl ( ) . concat ( `/eventrouter/v1/sse?_ts= ${ Date . now ( ) } ` ) ,
204+ getSalesforceMessagingUrl ( ) . concat ( `/eventrouter/v1/sse` ) ,
118205 eventListenerMap
119206 ) . then (
120207 resolve ,
0 commit comments