1515 * limitations under the License.
1616 */
1717
18- import { ConfigUpdateObserver , FetchResponse } from '../public_types' ;
19- const ORIGINAL_RETRIES = 8 ;
18+ import { ConfigUpdateObserver } from '../public_types' ;
2019import { ERROR_FACTORY , ErrorCode } from '../errors' ;
2120import { _FirebaseInstallationsInternal } from '@firebase/installations' ;
2221import { Storage } from '../storage/storage' ;
2322import { calculateBackoffMillis , FirebaseError } from '@firebase/util' ;
2423
24+ const ORIGINAL_RETRIES = 8 ;
25+
2526export class RealtimeHandler {
2627 constructor (
2728 private readonly firebaseInstallations : _FirebaseInstallationsInternal ,
@@ -45,9 +46,9 @@ export class RealtimeHandler {
4546 * Adds an observer to the realtime updates.
4647 * @param observer The observer to add.
4748 */
48- addObserver ( observer : ConfigUpdateObserver ) : void {
49+ async addObserver ( observer : ConfigUpdateObserver ) : Promise < void > {
4950 this . observers . add ( observer ) ;
50- this . beginRealtime ( ) ;
51+ await this . beginRealtime ( ) ;
5152 }
5253
5354 /**
@@ -58,11 +59,14 @@ export class RealtimeHandler {
5859 if ( this . observers . has ( observer ) ) {
5960 this . observers . delete ( observer ) ;
6061 }
62+ if ( this . observers . size === 0 ) {
63+ this . stopRealtime ( ) ;
64+ }
6165 }
6266
63- private beginRealtime ( ) : void {
67+ private async beginRealtime ( ) : Promise < void > {
6468 if ( this . observers . size > 0 ) {
65- this . makeRealtimeHttpConnection ( 0 ) ;
69+ await this . makeRealtimeHttpConnection ( 0 ) ;
6670 }
6771 }
6872
@@ -78,18 +82,17 @@ export class RealtimeHandler {
7882 return hasActiveListeners && isNotDisabled && isForeground && isNoConnectionActive ;
7983 }
8084
81-
82- private makeRealtimeHttpConnection ( delayMillis : number ) : void {
85+ private async makeRealtimeHttpConnection ( delayMillis : number ) : Promise < void > {
8386 if ( this . scheduledConnectionTimeoutId ) {
8487 clearTimeout ( this . scheduledConnectionTimeoutId ) ;
8588 }
8689 if ( ! this . canEstablishStreamConnection ( ) ) {
8790 return ;
8891 }
89- this . scheduledConnectionTimeoutId = setTimeout ( ( ) => {
92+ this . scheduledConnectionTimeoutId = setTimeout ( async ( ) => {
9093 if ( this . retriesRemaining > 0 ) {
9194 this . retriesRemaining -- ;
92- this . beginRealtimeHttpStream ( ) ;
95+ await this . beginRealtimeHttpStream ( ) ;
9396 } else if ( ! this . isInBackground ) {
9497 const error = ERROR_FACTORY . create ( ErrorCode . CONFIG_UPDATE_STREAM_ERROR , { originalErrorMessage : 'Unable to connect to the server. Check your connection and try again.' } ) ;
9598 this . propagateError ( error ) ;
@@ -110,6 +113,15 @@ export class RealtimeHandler {
110113 return false ;
111114 }
112115
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+
113125 private resetRetryCount ( ) : void {
114126 this . retriesRemaining = ORIGINAL_RETRIES ;
115127 }
@@ -142,12 +154,13 @@ export class RealtimeHandler {
142154 await this . storage . setLastKnownTemplateVersion ( 0 ) ;
143155 }
144156
145- const backoffEndTime = new Date ( metadata . backoffEndTimeMillis ) . getTime ( ) ;
157+ const backoffEndTime = metadata . backoffEndTimeMillis . getTime ( ) ;
158+
146159 if ( Date . now ( ) < backoffEndTime ) {
147- this . retryHttpConnectionWhenBackoffEnds ( ) ;
160+ await this . retryHttpConnectionWhenBackoffEnds ( ) ;
148161 return ;
149162 }
150- let response ;
163+ let response : Response | undefined ;
151164 try {
152165 const [ installationId , installationTokenResult ] = await Promise . all ( [
153166 this . firebaseInstallations . getId ( ) ,
@@ -156,8 +169,11 @@ export class RealtimeHandler {
156169 const headers = {
157170 'Content-Type' : 'application/json' ,
158171 'Content-Encoding' : 'gzip' ,
172+ 'X-Google-GFE-Can-Retry' : 'yes' ,
173+ 'X-Accept-Response-Streaming' : 'true' ,
159174 'If-None-Match' : '*' ,
160- 'authentication-token' : installationTokenResult
175+ 'authentication-token' : installationTokenResult ,
176+ 'Accept' : 'application/json'
161177 } ;
162178
163179 const url = this . getRealtimeUrl ( ) ;
@@ -170,43 +186,43 @@ export class RealtimeHandler {
170186 appInstanceId : installationId
171187 } ;
172188
173- response = await fetch ( url , {
189+ response = await fetch ( url , {
174190 method : "POST" ,
175191 headers,
176192 body : JSON . stringify ( requestBody )
177193 } ) ;
178- if ( response . status === 200 && response . body ) {
194+ if ( response ? .status === 200 && response ? .body ) {
179195 this . resetRetryCount ( ) ;
180- this . resetRealtimeBackoff ( ) ;
196+ await this . resetRealtimeBackoff ( ) ;
181197 //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
183199 } else {
184200 throw new FirebaseError ( 'http-status-error' , `HTTP Error: ${ response . status } ` ) ;
185201 }
186- } catch ( error : any ) {
202+ } catch ( error ) {
187203 if ( this . isInBackground ) {
188204 // It's possible the app was backgrounded while the connection was open, which
189205 // threw an exception trying to read the response. No real error here, so treat
190206 // this as a success, even if we haven't read a 200 response code yet.
191207 this . resetRetryCount ( ) ;
208+ } else {
209+ console . error ( 'Exception connecting to real-time RC backend. Retrying the connection...:' , error ) ;
192210 }
193211 } finally {
194212 this . isConnectionActive = false ;
195213 const statusCode = response ?. status ;
196214 const connectionFailed = ! this . isInBackground && ( ! statusCode || this . isStatusCodeRetryable ( statusCode ) ) ;
197215
198216 if ( connectionFailed ) {
199- this . updateBackoffMetadataWithLastFailedStreamConnectionTime ( new Date ( ) ) ;
217+ await this . updateBackoffMetadataWithLastFailedStreamConnectionTime ( new Date ( ) ) ;
200218 }
201219
202220 if ( connectionFailed || statusCode === 200 ) {
203- this . retryHttpConnectionWhenBackoffEnds ( ) ;
221+ await this . retryHttpConnectionWhenBackoffEnds ( ) ;
204222 } else {
205- //still have to implement this part
206223 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 ) ;
210226 }
211227 const firebaseError = ERROR_FACTORY . create ( ErrorCode . CONFIG_UPDATE_STREAM_ERROR , {
212228 httpStatus : statusCode ,
@@ -238,7 +254,7 @@ export class RealtimeHandler {
238254 const backoffEndTime = new Date ( metadata . backoffEndTimeMillis ) . getTime ( ) ;
239255 const currentTime = Date . now ( ) ;
240256 const retrySeconds = Math . max ( 0 , backoffEndTime - currentTime ) ;
241- this . makeRealtimeHttpConnection ( retrySeconds ) ;
257+ await this . makeRealtimeHttpConnection ( retrySeconds ) ;
242258 }
243259
244260 private async resetRealtimeBackoff ( ) : Promise < void > {
@@ -248,6 +264,7 @@ export class RealtimeHandler {
248264 } ) ;
249265 }
250266
267+
251268 private getRealtimeUrl ( ) : URL {
252269 const urlBase =
253270 window . FIREBASE_REMOTE_CONFIG_URL_BASE ||
@@ -257,4 +274,35 @@ export class RealtimeHandler {
257274 return new URL ( urlString ) ;
258275 }
259276
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+ }
260307}
308+
0 commit comments