@@ -12,16 +12,27 @@ const XMLReadyStateMap = ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DO
1212
1313const defaultOptions : EventSourceOptions = {
1414 body : undefined ,
15- debug : false ,
1615 headers : { } ,
1716 method : 'GET' ,
18- pollingInterval : 5000 ,
1917 timeout : 0 ,
20- timeoutBeforeConnection : 0 ,
2118 withCredentials : false ,
2219 retryAndHandleError : undefined ,
20+ initialRetryDelayMillis : 1000 ,
21+ logger : undefined ,
2322} ;
2423
24+ const maxRetryDelay = 30 * 1000 ; // Maximum retry delay 30 seconds.
25+ const jitterRatio = 0.5 ; // Delay should be 50%-100% of calculated time.
26+
27+ export function backoff ( base : number , retryCount : number ) {
28+ const delay = base * Math . pow ( 2 , retryCount ) ;
29+ return Math . min ( delay , maxRetryDelay ) ;
30+ }
31+
32+ export function jitter ( computedDelayMillis : number ) {
33+ return computedDelayMillis - Math . trunc ( Math . random ( ) * jitterRatio * computedDelayMillis ) ;
34+ }
35+
2536export default class EventSource < E extends string = never > {
2637 ERROR = - 1 ;
2738 CONNECTING = 0 ;
@@ -41,16 +52,16 @@ export default class EventSource<E extends string = never> {
4152
4253 private method : string ;
4354 private timeout : number ;
44- private timeoutBeforeConnection : number ;
4555 private withCredentials : boolean ;
4656 private headers : Record < string , any > ;
4757 private body : any ;
48- private debug : boolean ;
4958 private url : string ;
5059 private xhr : XMLHttpRequest = new XMLHttpRequest ( ) ;
5160 private pollTimer : any ;
52- private pollingInterval : number ;
5361 private retryAndHandleError ?: ( err : any ) => boolean ;
62+ private initialRetryDelayMillis : number = 1000 ;
63+ private retryCount : number = 0 ;
64+ private logger ?: any ;
5465
5566 constructor ( url : string , options ?: EventSourceOptions ) {
5667 const opts = {
@@ -61,25 +72,29 @@ export default class EventSource<E extends string = never> {
6172 this . url = url ;
6273 this . method = opts . method ! ;
6374 this . timeout = opts . timeout ! ;
64- this . timeoutBeforeConnection = opts . timeoutBeforeConnection ! ;
6575 this . withCredentials = opts . withCredentials ! ;
6676 this . headers = opts . headers ! ;
6777 this . body = opts . body ;
68- this . debug = opts . debug ! ;
69- this . pollingInterval = opts . pollingInterval ! ;
7078 this . retryAndHandleError = opts . retryAndHandleError ;
79+ this . initialRetryDelayMillis = opts . initialRetryDelayMillis ! ;
80+ this . logger = opts . logger ;
7181
72- this . pollAgain ( this . timeoutBeforeConnection , true ) ;
82+ this . tryConnect ( true ) ;
7383 }
7484
75- private pollAgain ( time : number , allowZero : boolean ) {
76- if ( time > 0 || allowZero ) {
77- this . logDebug ( `[EventSource] Will open new connection in ${ time } ms.` ) ;
78- this . dispatch ( 'retry' , { type : 'retry' } ) ;
79- this . pollTimer = setTimeout ( ( ) => {
80- this . open ( ) ;
81- } , time ) ;
82- }
85+ private getNextRetryDelay ( ) {
86+ const delay = jitter ( backoff ( this . initialRetryDelayMillis , this . retryCount ) ) ;
87+ this . retryCount += 1 ;
88+ return delay ;
89+ }
90+
91+ private tryConnect ( forceNoDelay : boolean = false ) {
92+ let delay = forceNoDelay ? 0 : this . getNextRetryDelay ( ) ;
93+ this . logger ?. debug ( `[EventSource] Will open new connection in ${ delay } ms.` ) ;
94+ this . dispatch ( 'retry' , { type : 'retry' , delayMillis : delay } ) ;
95+ this . pollTimer = setTimeout ( ( ) => {
96+ this . open ( ) ;
97+ } , delay ) ;
8398 }
8499
85100 open ( ) {
@@ -113,7 +128,7 @@ export default class EventSource<E extends string = never> {
113128 return ;
114129 }
115130
116- this . logDebug (
131+ this . logger ?. debug (
117132 `[EventSource][onreadystatechange] ReadyState: ${
118133 XMLReadyStateMap [ this . xhr . readyState ] || 'Unknown'
119134 } (${ this . xhr . readyState } ), status: ${ this . xhr . status } `,
@@ -128,16 +143,18 @@ export default class EventSource<E extends string = never> {
128143
129144 if ( this . xhr . status >= 200 && this . xhr . status < 400 ) {
130145 if ( this . status === this . CONNECTING ) {
146+ this . retryCount = 0 ;
131147 this . status = this . OPEN ;
132148 this . dispatch ( 'open' , { type : 'open' } ) ;
133- this . logDebug ( '[EventSource][onreadystatechange][OPEN] Connection opened.' ) ;
149+ this . logger ?. debug ( '[EventSource][onreadystatechange][OPEN] Connection opened.' ) ;
134150 }
135151
152+ // retry from server gets set here
136153 this . handleEvent ( this . xhr . responseText || '' ) ;
137154
138155 if ( this . xhr . readyState === XMLHttpRequest . DONE ) {
139- this . logDebug ( '[EventSource][onreadystatechange][DONE] Operation done.' ) ;
140- this . pollAgain ( this . pollingInterval , false ) ;
156+ this . logger ?. debug ( '[EventSource][onreadystatechange][DONE] Operation done.' ) ;
157+ this . tryConnect ( ) ;
141158 }
142159 } else if ( this . xhr . status !== 0 ) {
143160 this . status = this . ERROR ;
@@ -149,20 +166,20 @@ export default class EventSource<E extends string = never> {
149166 } ) ;
150167
151168 if ( this . xhr . readyState === XMLHttpRequest . DONE ) {
152- this . logDebug ( '[EventSource][onreadystatechange][ERROR] Response status error.' ) ;
169+ this . logger ?. debug ( '[EventSource][onreadystatechange][ERROR] Response status error.' ) ;
153170
154171 if ( ! this . retryAndHandleError ) {
155- // default implementation
156- this . pollAgain ( this . pollingInterval , false ) ;
172+ // by default just try and reconnect if there's an error.
173+ this . tryConnect ( ) ;
157174 } else {
158- // custom retry logic
175+ // custom retry logic taking into account status codes.
159176 const shouldRetry = this . retryAndHandleError ( {
160177 status : this . xhr . status ,
161178 message : this . xhr . responseText ,
162179 } ) ;
163180
164181 if ( shouldRetry ) {
165- this . pollAgain ( this . pollingInterval , true ) ;
182+ this . tryConnect ( ) ;
166183 }
167184 }
168185 }
@@ -207,13 +224,6 @@ export default class EventSource<E extends string = never> {
207224 }
208225 }
209226
210- private logDebug ( ...msg : string [ ] ) {
211- if ( this . debug ) {
212- // eslint-disable-next-line no-console
213- console . debug ( ...msg ) ;
214- }
215- }
216-
217227 private handleEvent ( response : string ) {
218228 const parts = response . slice ( this . lastIndexProcessed ) . split ( '\n' ) ;
219229
@@ -234,7 +244,8 @@ export default class EventSource<E extends string = never> {
234244 } else if ( line . indexOf ( 'retry' ) === 0 ) {
235245 retry = parseInt ( line . replace ( / r e t r y : ? \s * / , '' ) , 10 ) ;
236246 if ( ! Number . isNaN ( retry ) ) {
237- this . pollingInterval = retry ;
247+ // GOTCHA: Ignore the server retry recommendation. Use our own custom getNextRetryDelay logic.
248+ // this.pollingInterval = retry;
238249 }
239250 } else if ( line . indexOf ( 'data' ) === 0 ) {
240251 data . push ( line . replace ( / d a t a : ? \s * / , '' ) ) ;
@@ -307,7 +318,7 @@ export default class EventSource<E extends string = never> {
307318 this . onerror ( data ) ;
308319 break ;
309320 case 'retry' :
310- this . onretrying ( { delayMillis : this . pollingInterval } ) ;
321+ this . onretrying ( data ) ;
311322 break ;
312323 default :
313324 break ;
0 commit comments