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' ;
2423import { VisibilityMonitor } from './VisibilityMonitor' ;
2524
25+ const ORIGINAL_RETRIES = 8 ;
26+
2627export class RealtimeHandler {
2728 constructor (
2829 private readonly firebaseInstallations : _FirebaseInstallationsInternal ,
@@ -48,9 +49,9 @@ export class RealtimeHandler {
4849 * Adds an observer to the realtime updates.
4950 * @param observer The observer to add.
5051 */
51- addObserver ( observer : ConfigUpdateObserver ) : void {
52+ async addObserver ( observer : ConfigUpdateObserver ) : Promise < void > {
5253 this . observers . add ( observer ) ;
53- this . beginRealtime ( ) ;
54+ await this . beginRealtime ( ) ;
5455 }
5556
5657 /**
@@ -61,11 +62,14 @@ export class RealtimeHandler {
6162 if ( this . observers . has ( observer ) ) {
6263 this . observers . delete ( observer ) ;
6364 }
65+ if ( this . observers . size === 0 ) {
66+ this . stopRealtime ( ) ;
67+ }
6468 }
6569
66- private beginRealtime ( ) : void {
70+ private async beginRealtime ( ) : Promise < void > {
6771 if ( this . observers . size > 0 ) {
68- this . makeRealtimeHttpConnection ( 0 ) ;
72+ await this . makeRealtimeHttpConnection ( 0 ) ;
6973 }
7074 }
7175
@@ -81,18 +85,17 @@ export class RealtimeHandler {
8185 return hasActiveListeners && isNotDisabled && isForeground && isNoConnectionActive ;
8286 }
8387
84-
85- private makeRealtimeHttpConnection ( delayMillis : number ) : void {
88+ private async makeRealtimeHttpConnection ( delayMillis : number ) : Promise < void > {
8689 if ( this . scheduledConnectionTimeoutId ) {
8790 clearTimeout ( this . scheduledConnectionTimeoutId ) ;
8891 }
8992 if ( ! this . canEstablishStreamConnection ( ) ) {
9093 return ;
9194 }
92- this . scheduledConnectionTimeoutId = setTimeout ( ( ) => {
95+ this . scheduledConnectionTimeoutId = setTimeout ( async ( ) => {
9396 if ( this . retriesRemaining > 0 ) {
9497 this . retriesRemaining -- ;
95- this . beginRealtimeHttpStream ( ) ;
98+ await this . beginRealtimeHttpStream ( ) ;
9699 } else if ( ! this . isInBackground ) {
97100 const error = ERROR_FACTORY . create ( ErrorCode . CONFIG_UPDATE_STREAM_ERROR , { originalErrorMessage : 'Unable to connect to the server. Check your connection and try again.' } ) ;
98101 this . propagateError ( error ) ;
@@ -154,12 +157,13 @@ export class RealtimeHandler {
154157 await this . storage . setLastKnownTemplateVersion ( 0 ) ;
155158 }
156159
157- const backoffEndTime = new Date ( metadata . backoffEndTimeMillis ) . getTime ( ) ;
160+ const backoffEndTime = metadata . backoffEndTimeMillis . getTime ( ) ;
161+
158162 if ( Date . now ( ) < backoffEndTime ) {
159- this . retryHttpConnectionWhenBackoffEnds ( ) ;
163+ await this . retryHttpConnectionWhenBackoffEnds ( ) ;
160164 return ;
161165 }
162- let response ;
166+ let response : Response | undefined ;
163167 try {
164168 const [ installationId , installationTokenResult ] = await Promise . all ( [
165169 this . firebaseInstallations . getId ( ) ,
@@ -168,8 +172,11 @@ export class RealtimeHandler {
168172 const headers = {
169173 'Content-Type' : 'application/json' ,
170174 'Content-Encoding' : 'gzip' ,
175+ 'X-Google-GFE-Can-Retry' : 'yes' ,
176+ 'X-Accept-Response-Streaming' : 'true' ,
171177 'If-None-Match' : '*' ,
172- 'authentication-token' : installationTokenResult
178+ 'authentication-token' : installationTokenResult ,
179+ 'Accept' : 'application/json'
173180 } ;
174181 const url = this . getRealtimeUrl ( ) ;
175182 const requestBody = {
@@ -181,43 +188,43 @@ export class RealtimeHandler {
181188 appInstanceId : installationId
182189 } ;
183190
184- response = await fetch ( url , {
191+ response = await fetch ( url , {
185192 method : "POST" ,
186193 headers,
187194 body : JSON . stringify ( requestBody )
188195 } ) ;
189- if ( response . status === 200 && response . body ) {
196+ if ( response ? .status === 200 && response ? .body ) {
190197 this . resetRetryCount ( ) ;
191- this . resetRealtimeBackoff ( ) ;
198+ await this . resetRealtimeBackoff ( ) ;
192199 //code related to start StartAutofetch
193- //and then give the notification for al the observers
200+ //and then give the notification to all the observers
194201 } else {
195202 throw new FirebaseError ( 'http-status-error' , `HTTP Error: ${ response . status } ` ) ;
196203 }
197- } catch ( error : any ) {
204+ } catch ( error ) {
198205 if ( this . isInBackground ) {
199206 // It's possible the app was backgrounded while the connection was open, which
200207 // threw an exception trying to read the response. No real error here, so treat
201208 // this as a success, even if we haven't read a 200 response code yet.
202209 this . resetRetryCount ( ) ;
210+ } else {
211+ console . error ( 'Exception connecting to real-time RC backend. Retrying the connection...:' , error ) ;
203212 }
204213 } finally {
205214 this . isConnectionActive = false ;
206215 const statusCode = response ?. status ;
207216 const connectionFailed = ! this . isInBackground && ( ! statusCode || this . isStatusCodeRetryable ( statusCode ) ) ;
208217
209218 if ( connectionFailed ) {
210- this . updateBackoffMetadataWithLastFailedStreamConnectionTime ( new Date ( ) ) ;
219+ await this . updateBackoffMetadataWithLastFailedStreamConnectionTime ( new Date ( ) ) ;
211220 }
212221
213222 if ( connectionFailed || statusCode === 200 ) {
214- this . retryHttpConnectionWhenBackoffEnds ( ) ;
223+ await this . retryHttpConnectionWhenBackoffEnds ( ) ;
215224 } else {
216- //still have to implement this part
217225 let errorMessage = `Unable to connect to the server. Try again in a few minutes. HTTP status code: ${ statusCode } ` ;
218- if ( statusCode === 403 ) {
219- //still have to implemet this parseErrorResponseBody method
220- // errorMessage = await this.parseErrorResponseBody(response?.body);
226+ if ( statusCode === 403 && response ) {
227+ errorMessage = await this . parseErrorResponseBody ( response . body ) ;
221228 }
222229 const firebaseError = ERROR_FACTORY . create ( ErrorCode . CONFIG_UPDATE_STREAM_ERROR , {
223230 httpStatus : statusCode ,
@@ -249,7 +256,7 @@ export class RealtimeHandler {
249256 const backoffEndTime = new Date ( metadata . backoffEndTimeMillis ) . getTime ( ) ;
250257 const currentTime = Date . now ( ) ;
251258 const retrySeconds = Math . max ( 0 , backoffEndTime - currentTime ) ;
252- this . makeRealtimeHttpConnection ( retrySeconds ) ;
259+ await this . makeRealtimeHttpConnection ( retrySeconds ) ;
253260 }
254261
255262 private async resetRealtimeBackoff ( ) : Promise < void > {
@@ -259,6 +266,7 @@ export class RealtimeHandler {
259266 } ) ;
260267 }
261268
269+
262270 private getRealtimeUrl ( ) : URL {
263271 const urlBase =
264272 window . FIREBASE_REMOTE_CONFIG_URL_BASE ||
@@ -279,4 +287,35 @@ export class RealtimeHandler {
279287 }
280288 }
281289 }
290+ private async parseErrorResponseBody (
291+ body : ReadableStream < Uint8Array > | null
292+ ) : Promise < string > {
293+ if ( ! body ) {
294+ return 'Response body is empty.' ;
295+ }
296+
297+ try {
298+ const reader = body . getReader ( ) ;
299+ const chunks : Uint8Array [ ] = [ ] ;
300+ while ( true ) {
301+ const { done, value } = await reader . read ( ) ;
302+ if ( done ) {
303+ break ;
304+ }
305+ chunks . push ( value ) ;
306+ }
307+ const blob = new Blob ( chunks ) ;
308+ const text = await blob . text ( ) ;
309+
310+ const jsonResponse = JSON . parse ( text ) ;
311+ return (
312+ jsonResponse . error ?. message ||
313+ jsonResponse . message ||
314+ 'Unknown error from server.'
315+ ) ;
316+ } catch ( e ) {
317+ return 'Could not parse error response body, or body is not JSON.' ;
318+ }
319+ }
282320}
321+
0 commit comments