@@ -12,16 +12,27 @@ const XMLReadyStateMap = ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DO
12
12
13
13
const defaultOptions : EventSourceOptions = {
14
14
body : undefined ,
15
- debug : false ,
16
15
headers : { } ,
17
16
method : 'GET' ,
18
- pollingInterval : 5000 ,
19
17
timeout : 0 ,
20
- timeoutBeforeConnection : 0 ,
21
18
withCredentials : false ,
22
19
retryAndHandleError : undefined ,
20
+ initialRetryDelayMillis : 1000 ,
21
+ logger : undefined ,
23
22
} ;
24
23
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
+
25
36
export default class EventSource < E extends string = never > {
26
37
ERROR = - 1 ;
27
38
CONNECTING = 0 ;
@@ -41,16 +52,16 @@ export default class EventSource<E extends string = never> {
41
52
42
53
private method : string ;
43
54
private timeout : number ;
44
- private timeoutBeforeConnection : number ;
45
55
private withCredentials : boolean ;
46
56
private headers : Record < string , any > ;
47
57
private body : any ;
48
- private debug : boolean ;
49
58
private url : string ;
50
59
private xhr : XMLHttpRequest = new XMLHttpRequest ( ) ;
51
60
private pollTimer : any ;
52
- private pollingInterval : number ;
53
61
private retryAndHandleError ?: ( err : any ) => boolean ;
62
+ private initialRetryDelayMillis : number = 1000 ;
63
+ private retryCount : number = 0 ;
64
+ private logger ?: any ;
54
65
55
66
constructor ( url : string , options ?: EventSourceOptions ) {
56
67
const opts = {
@@ -61,25 +72,29 @@ export default class EventSource<E extends string = never> {
61
72
this . url = url ;
62
73
this . method = opts . method ! ;
63
74
this . timeout = opts . timeout ! ;
64
- this . timeoutBeforeConnection = opts . timeoutBeforeConnection ! ;
65
75
this . withCredentials = opts . withCredentials ! ;
66
76
this . headers = opts . headers ! ;
67
77
this . body = opts . body ;
68
- this . debug = opts . debug ! ;
69
- this . pollingInterval = opts . pollingInterval ! ;
70
78
this . retryAndHandleError = opts . retryAndHandleError ;
79
+ this . initialRetryDelayMillis = opts . initialRetryDelayMillis ! ;
80
+ this . logger = opts . logger ;
71
81
72
- this . pollAgain ( this . timeoutBeforeConnection , true ) ;
82
+ this . tryConnect ( true ) ;
73
83
}
74
84
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 ) ;
83
98
}
84
99
85
100
open ( ) {
@@ -113,7 +128,7 @@ export default class EventSource<E extends string = never> {
113
128
return ;
114
129
}
115
130
116
- this . logDebug (
131
+ this . logger ?. debug (
117
132
`[EventSource][onreadystatechange] ReadyState: ${
118
133
XMLReadyStateMap [ this . xhr . readyState ] || 'Unknown'
119
134
} (${ this . xhr . readyState } ), status: ${ this . xhr . status } `,
@@ -128,16 +143,18 @@ export default class EventSource<E extends string = never> {
128
143
129
144
if ( this . xhr . status >= 200 && this . xhr . status < 400 ) {
130
145
if ( this . status === this . CONNECTING ) {
146
+ this . retryCount = 0 ;
131
147
this . status = this . OPEN ;
132
148
this . dispatch ( 'open' , { type : 'open' } ) ;
133
- this . logDebug ( '[EventSource][onreadystatechange][OPEN] Connection opened.' ) ;
149
+ this . logger ?. debug ( '[EventSource][onreadystatechange][OPEN] Connection opened.' ) ;
134
150
}
135
151
152
+ // retry from server gets set here
136
153
this . handleEvent ( this . xhr . responseText || '' ) ;
137
154
138
155
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 ( ) ;
141
158
}
142
159
} else if ( this . xhr . status !== 0 ) {
143
160
this . status = this . ERROR ;
@@ -149,20 +166,20 @@ export default class EventSource<E extends string = never> {
149
166
} ) ;
150
167
151
168
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.' ) ;
153
170
154
171
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 ( ) ;
157
174
} else {
158
- // custom retry logic
175
+ // custom retry logic taking into account status codes.
159
176
const shouldRetry = this . retryAndHandleError ( {
160
177
status : this . xhr . status ,
161
178
message : this . xhr . responseText ,
162
179
} ) ;
163
180
164
181
if ( shouldRetry ) {
165
- this . pollAgain ( this . pollingInterval , true ) ;
182
+ this . tryConnect ( ) ;
166
183
}
167
184
}
168
185
}
@@ -207,13 +224,6 @@ export default class EventSource<E extends string = never> {
207
224
}
208
225
}
209
226
210
- private logDebug ( ...msg : string [ ] ) {
211
- if ( this . debug ) {
212
- // eslint-disable-next-line no-console
213
- console . debug ( ...msg ) ;
214
- }
215
- }
216
-
217
227
private handleEvent ( response : string ) {
218
228
const parts = response . slice ( this . lastIndexProcessed ) . split ( '\n' ) ;
219
229
@@ -234,7 +244,8 @@ export default class EventSource<E extends string = never> {
234
244
} else if ( line . indexOf ( 'retry' ) === 0 ) {
235
245
retry = parseInt ( line . replace ( / r e t r y : ? \s * / , '' ) , 10 ) ;
236
246
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;
238
249
}
239
250
} else if ( line . indexOf ( 'data' ) === 0 ) {
240
251
data . push ( line . replace ( / d a t a : ? \s * / , '' ) ) ;
@@ -307,7 +318,7 @@ export default class EventSource<E extends string = never> {
307
318
this . onerror ( data ) ;
308
319
break ;
309
320
case 'retry' :
310
- this . onretrying ( { delayMillis : this . pollingInterval } ) ;
321
+ this . onretrying ( data ) ;
311
322
break ;
312
323
default :
313
324
break ;
0 commit comments