15
15
* limitations under the License.
16
16
*/
17
17
18
- import { ConfigUpdateObserver , FetchResponse } from '../public_types' ;
19
- const ORIGINAL_RETRIES = 8 ;
18
+ import { ConfigUpdateObserver } from '../public_types' ;
20
19
import { ERROR_FACTORY , ErrorCode } from '../errors' ;
21
20
import { _FirebaseInstallationsInternal } from '@firebase/installations' ;
22
21
import { Storage } from '../storage/storage' ;
23
22
import { calculateBackoffMillis , FirebaseError } from '@firebase/util' ;
24
23
24
+ const ORIGINAL_RETRIES = 8 ;
25
+
25
26
export class RealtimeHandler {
26
27
constructor (
27
28
private readonly firebaseInstallations : _FirebaseInstallationsInternal ,
@@ -45,9 +46,9 @@ export class RealtimeHandler {
45
46
* Adds an observer to the realtime updates.
46
47
* @param observer The observer to add.
47
48
*/
48
- addObserver ( observer : ConfigUpdateObserver ) : void {
49
+ async addObserver ( observer : ConfigUpdateObserver ) : Promise < void > {
49
50
this . observers . add ( observer ) ;
50
- this . beginRealtime ( ) ;
51
+ await this . beginRealtime ( ) ;
51
52
}
52
53
53
54
/**
@@ -58,11 +59,14 @@ export class RealtimeHandler {
58
59
if ( this . observers . has ( observer ) ) {
59
60
this . observers . delete ( observer ) ;
60
61
}
62
+ if ( this . observers . size === 0 ) {
63
+ this . stopRealtime ( ) ;
64
+ }
61
65
}
62
66
63
- private beginRealtime ( ) : void {
67
+ private async beginRealtime ( ) : Promise < void > {
64
68
if ( this . observers . size > 0 ) {
65
- this . makeRealtimeHttpConnection ( 0 ) ;
69
+ await this . makeRealtimeHttpConnection ( 0 ) ;
66
70
}
67
71
}
68
72
@@ -78,18 +82,17 @@ export class RealtimeHandler {
78
82
return hasActiveListeners && isNotDisabled && isForeground && isNoConnectionActive ;
79
83
}
80
84
81
-
82
- private makeRealtimeHttpConnection ( delayMillis : number ) : void {
85
+ private async makeRealtimeHttpConnection ( delayMillis : number ) : Promise < void > {
83
86
if ( this . scheduledConnectionTimeoutId ) {
84
87
clearTimeout ( this . scheduledConnectionTimeoutId ) ;
85
88
}
86
89
if ( ! this . canEstablishStreamConnection ( ) ) {
87
90
return ;
88
91
}
89
- this . scheduledConnectionTimeoutId = setTimeout ( ( ) => {
92
+ this . scheduledConnectionTimeoutId = setTimeout ( async ( ) => {
90
93
if ( this . retriesRemaining > 0 ) {
91
94
this . retriesRemaining -- ;
92
- this . beginRealtimeHttpStream ( ) ;
95
+ await this . beginRealtimeHttpStream ( ) ;
93
96
} else if ( ! this . isInBackground ) {
94
97
const error = ERROR_FACTORY . create ( ErrorCode . CONFIG_UPDATE_STREAM_ERROR , { originalErrorMessage : 'Unable to connect to the server. Check your connection and try again.' } ) ;
95
98
this . propagateError ( error ) ;
@@ -110,6 +113,15 @@ export class RealtimeHandler {
110
113
return false ;
111
114
}
112
115
116
+ private stopRealtime ( ) : void {
117
+ if ( this . scheduledConnectionTimeoutId ) {
118
+ clearTimeout ( this . scheduledConnectionTimeoutId ) ;
119
+ this . scheduledConnectionTimeoutId = undefined ;
120
+ }
121
+ this . streamController ?. abort ( ) ;
122
+ this . isConnectionActive = false ;
123
+ }
124
+
113
125
private resetRetryCount ( ) : void {
114
126
this . retriesRemaining = ORIGINAL_RETRIES ;
115
127
}
@@ -142,12 +154,13 @@ export class RealtimeHandler {
142
154
await this . storage . setLastKnownTemplateVersion ( 0 ) ;
143
155
}
144
156
145
- const backoffEndTime = new Date ( metadata . backoffEndTimeMillis ) . getTime ( ) ;
157
+ const backoffEndTime = metadata . backoffEndTimeMillis . getTime ( ) ;
158
+
146
159
if ( Date . now ( ) < backoffEndTime ) {
147
- this . retryHttpConnectionWhenBackoffEnds ( ) ;
160
+ await this . retryHttpConnectionWhenBackoffEnds ( ) ;
148
161
return ;
149
162
}
150
- let response ;
163
+ let response : Response | undefined ;
151
164
try {
152
165
const [ installationId , installationTokenResult ] = await Promise . all ( [
153
166
this . firebaseInstallations . getId ( ) ,
@@ -156,8 +169,11 @@ export class RealtimeHandler {
156
169
const headers = {
157
170
'Content-Type' : 'application/json' ,
158
171
'Content-Encoding' : 'gzip' ,
172
+ 'X-Google-GFE-Can-Retry' : 'yes' ,
173
+ 'X-Accept-Response-Streaming' : 'true' ,
159
174
'If-None-Match' : '*' ,
160
- 'authentication-token' : installationTokenResult
175
+ 'authentication-token' : installationTokenResult ,
176
+ 'Accept' : 'application/json'
161
177
} ;
162
178
163
179
const url = this . getRealtimeUrl ( ) ;
@@ -170,43 +186,43 @@ export class RealtimeHandler {
170
186
appInstanceId : installationId
171
187
} ;
172
188
173
- response = await fetch ( url , {
189
+ response = await fetch ( url , {
174
190
method : "POST" ,
175
191
headers,
176
192
body : JSON . stringify ( requestBody )
177
193
} ) ;
178
- if ( response . status === 200 && response . body ) {
194
+ if ( response ? .status === 200 && response ? .body ) {
179
195
this . resetRetryCount ( ) ;
180
- this . resetRealtimeBackoff ( ) ;
196
+ await this . resetRealtimeBackoff ( ) ;
181
197
//code related to start StartAutofetch
182
- //and then give the notification for al the observers
198
+ //and then give the notification to all the observers
183
199
} else {
184
200
throw new FirebaseError ( 'http-status-error' , `HTTP Error: ${ response . status } ` ) ;
185
201
}
186
- } catch ( error : any ) {
202
+ } catch ( error ) {
187
203
if ( this . isInBackground ) {
188
204
// It's possible the app was backgrounded while the connection was open, which
189
205
// threw an exception trying to read the response. No real error here, so treat
190
206
// this as a success, even if we haven't read a 200 response code yet.
191
207
this . resetRetryCount ( ) ;
208
+ } else {
209
+ console . error ( 'Exception connecting to real-time RC backend. Retrying the connection...:' , error ) ;
192
210
}
193
211
} finally {
194
212
this . isConnectionActive = false ;
195
213
const statusCode = response ?. status ;
196
214
const connectionFailed = ! this . isInBackground && ( ! statusCode || this . isStatusCodeRetryable ( statusCode ) ) ;
197
215
198
216
if ( connectionFailed ) {
199
- this . updateBackoffMetadataWithLastFailedStreamConnectionTime ( new Date ( ) ) ;
217
+ await this . updateBackoffMetadataWithLastFailedStreamConnectionTime ( new Date ( ) ) ;
200
218
}
201
219
202
220
if ( connectionFailed || statusCode === 200 ) {
203
- this . retryHttpConnectionWhenBackoffEnds ( ) ;
221
+ await this . retryHttpConnectionWhenBackoffEnds ( ) ;
204
222
} else {
205
- //still have to implement this part
206
223
let errorMessage = `Unable to connect to the server. Try again in a few minutes. HTTP status code: ${ statusCode } ` ;
207
- if ( statusCode === 403 ) {
208
- //still have to implemet this parseErrorResponseBody method
209
- // errorMessage = await this.parseErrorResponseBody(response?.body);
224
+ if ( statusCode === 403 && response ) {
225
+ errorMessage = await this . parseErrorResponseBody ( response . body ) ;
210
226
}
211
227
const firebaseError = ERROR_FACTORY . create ( ErrorCode . CONFIG_UPDATE_STREAM_ERROR , {
212
228
httpStatus : statusCode ,
@@ -238,7 +254,7 @@ export class RealtimeHandler {
238
254
const backoffEndTime = new Date ( metadata . backoffEndTimeMillis ) . getTime ( ) ;
239
255
const currentTime = Date . now ( ) ;
240
256
const retrySeconds = Math . max ( 0 , backoffEndTime - currentTime ) ;
241
- this . makeRealtimeHttpConnection ( retrySeconds ) ;
257
+ await this . makeRealtimeHttpConnection ( retrySeconds ) ;
242
258
}
243
259
244
260
private async resetRealtimeBackoff ( ) : Promise < void > {
@@ -248,6 +264,7 @@ export class RealtimeHandler {
248
264
} ) ;
249
265
}
250
266
267
+
251
268
private getRealtimeUrl ( ) : URL {
252
269
const urlBase =
253
270
window . FIREBASE_REMOTE_CONFIG_URL_BASE ||
@@ -257,4 +274,35 @@ export class RealtimeHandler {
257
274
return new URL ( urlString ) ;
258
275
}
259
276
277
+ private async parseErrorResponseBody (
278
+ body : ReadableStream < Uint8Array > | null
279
+ ) : Promise < string > {
280
+ if ( ! body ) {
281
+ return 'Response body is empty.' ;
282
+ }
283
+
284
+ try {
285
+ const reader = body . getReader ( ) ;
286
+ const chunks : Uint8Array [ ] = [ ] ;
287
+ while ( true ) {
288
+ const { done, value } = await reader . read ( ) ;
289
+ if ( done ) {
290
+ break ;
291
+ }
292
+ chunks . push ( value ) ;
293
+ }
294
+ const blob = new Blob ( chunks ) ;
295
+ const text = await blob . text ( ) ;
296
+
297
+ const jsonResponse = JSON . parse ( text ) ;
298
+ return (
299
+ jsonResponse . error ?. message ||
300
+ jsonResponse . message ||
301
+ 'Unknown error from server.'
302
+ ) ;
303
+ } catch ( e ) {
304
+ return 'Could not parse error response body, or body is not JSON.' ;
305
+ }
306
+ }
260
307
}
308
+
0 commit comments