1515 * limitations under the License.
1616 */
1717
18- import { ConfigUpdateObserver , /*FetchResponse*/ } from '../public_types' ;
19- const MAX_HTTP_RETRIES = 8 ;
20- // import { ERROR_FACTORY, ErrorCode } from '../errors';
21- // import { FetchRequest } from './remote_config_fetch_client';
18+ import { ConfigUpdateObserver , FetchResponse } from '../public_types' ;
19+ const ORIGINAL_RETRIES = 8 ;
20+ import { ERROR_FACTORY , ErrorCode } from '../errors' ;
2221import { _FirebaseInstallationsInternal } from '@firebase/installations' ;
22+ import { Storage } from '../storage/storage' ;
23+ import { calculateBackoffMillis , FirebaseError } from '@firebase/util' ;
24+
2325export class RealtimeHandler {
24- constructor (
26+ constructor (
2527 private readonly firebaseInstallations : _FirebaseInstallationsInternal ,
26- )
27- { }
28+ private readonly storage : Storage ,
29+ private readonly sdkVersion : string ,
30+ private readonly namespace : string ,
31+ private readonly projectId : string ,
32+ private readonly apiKey : string ,
33+ private readonly appId : string
34+ ) { }
2835
2936 private streamController ?: AbortController ;
3037 private observers : Set < ConfigUpdateObserver > = new Set < ConfigUpdateObserver > ( ) ;
3138 private isConnectionActive : boolean = false ;
32- private retriesRemaining : number = MAX_HTTP_RETRIES ;
39+ private retriesRemaining : number = ORIGINAL_RETRIES ;
3340 private isRealtimeDisabled : boolean = false ;
3441 private isInBackground : boolean = false ;
35- private backoffCount : number = 0 ;
3642 private scheduledConnectionTimeoutId ?: ReturnType < typeof setTimeout > ;
37- // private backoffManager: BackoffManager = new BackoffManager();
38-
39-
43+ private templateVersion : number = 0 ;
4044 /**
4145 * Adds an observer to the realtime updates.
4246 * @param observer The observer to add.
4347 */
4448 addObserver ( observer : ConfigUpdateObserver ) : void {
4549 this . observers . add ( observer ) ;
46- // this.beginRealtime();
50+ this . beginRealtime ( ) ;
4751 }
4852
4953 /**
@@ -58,142 +62,202 @@ export class RealtimeHandler {
5862
5963 private beginRealtime ( ) : void {
6064 if ( this . observers . size > 0 ) {
61- this . retriesRemaining = MAX_HTTP_RETRIES ;
62- this . backoffCount = 0 ;
63- // this.makeRealtimeHttpConnection(0);
65+ this . makeRealtimeHttpConnection ( 0 ) ;
6466 }
6567 }
6668
67- // private canMakeHttpConnection(): void {
68-
69- // }
70-
69+ /**
70+ * Checks whether connection can be made or not based on some conditions
71+ * @returns booelean
72+ */
7173 private canEstablishStreamConnection ( ) : boolean {
7274 const hasActiveListeners = this . observers . size > 0 ;
7375 const isNotDisabled = ! this . isRealtimeDisabled ;
7476 const isForeground = ! this . isInBackground ;
75- return hasActiveListeners && isNotDisabled && isForeground ;
77+ const isNoConnectionActive = ! this . isConnectionActive ;
78+ return hasActiveListeners && isNotDisabled && isForeground && isNoConnectionActive ;
7679 }
7780
78- // private async makeRealtimeHttpConnection(delayMillis: number): void {
79- // if (this.scheduledConnectionTimeoutId) {
80- // clearTimeout(this.scheduledConnectionTimeoutId);
81- // }
82-
83- // this.scheduledConnectionTimeoutId = setTimeout(() => {
84- // // Check 1: Can we connect at all? Mirrors Java's first check.
85- // if (!this.canEstablishStreamConnection()) {
86- // return;
87- // }
88- // if (this.retriesRemaining > 0) {
89- // this.retriesRemaining--;
90- // await this.beginRealtimeHttpStream();
91- // } else if (!this.isInBackground) {
92- // throw ERROR_FACTORY.create(ErrorCode.REALTIME_UPDATE_STREAM_ERROR);
93- // }
94- // }, delayMillis);
95- // }
9681
82+ private makeRealtimeHttpConnection ( delayMillis : number ) : void {
83+ this . updateBackoffMetadataWithLastFailedStreamConnectionTime ( new Date ( ) ) ;
84+
85+ if ( this . scheduledConnectionTimeoutId ) {
86+ clearTimeout ( this . scheduledConnectionTimeoutId ) ;
87+ }
88+ if ( ! this . canEstablishStreamConnection ( ) ) {
89+ return ;
90+ }
91+ this . scheduledConnectionTimeoutId = setTimeout ( ( ) => {
92+ if ( this . retriesRemaining > 0 ) {
93+ this . retriesRemaining -- ;
94+ this . beginRealtimeHttpStream ( ) ;
95+ } else if ( ! this . isInBackground ) {
96+ const error = ERROR_FACTORY . create ( ErrorCode . CONFIG_UPDATE_STREAM_ERROR , {
97+ originalErrorMessage : 'Unable to connect to the server. Check your connection and try again.' } ) ;
98+ this . propagateError ( error ) ;
99+ }
100+ } , delayMillis ) ;
101+ }
102+
103+ private setIsHttpConnectionRunning ( connectionRunning : boolean ) : void {
104+ this . isConnectionActive = connectionRunning ;
105+ }
97106
98107 private checkAndSetHttpConnectionFlagIfNotRunning ( ) : boolean {
99108 if ( this . canEstablishStreamConnection ( ) ) {
100109 this . streamController = new AbortController ( ) ;
101- this . isConnectionActive = true ;
110+ this . setIsHttpConnectionRunning ( true ) ;
102111 return true ;
103112 }
104113 return false ;
105114 }
106115
107- // private retryHttpConnectionWhenBackoffEnds(): void {
108- // const currentTime = Date.now();
109- // const timeToWait = Math.max(0, this.backoffManager.backoffEndTimeMillis - currentTime);
110- // this.makeRealtimeHttpConnection(timeToWait);
111- // }
112-
113-
114- // private async createFetchRequest(): Promise<FetchRequest> {
115- // const [installationId, installationTokenResult] = await Promise.all([
116- // this.firebaseInstallations.getId(),
117- // this.firebaseInstallations.getToken(false)
118- // ]);
119-
120- // const url = this._getRealtimeUrl();
121-
122- // const requestBody = {
123- // project: extractProjectNumberFromAppId(this.firebaseApp.options.appId!),
124- // namespace: 'firebase',
125- // lastKnownVersionNumber: this.templateVersion.toString(),
126- // appId: this.firebaseApp.options.appId,
127- // sdkVersion: '20.0.4',
128- // appInstanceId: installationId
129- // };
130-
131- // const request: FetchRequest = {
132- // url: url.toString(),
133- // method: 'POST',
134- // signal: this.streamController!.signal,
135- // body: JSON.stringify(requestBody),
136- // headers: {
137- // 'Content-Type': 'application/json',
138- // 'Accept': 'application/json',
139- // 'X-Goog-Api-Key': this.firebaseApp.options.apiKey!,
140- // 'X-Goog-Firebase-Installations-Auth': installationTokenResult.token,
141- // 'X-Accept-Response-Streaming': 'true',
142- // 'X-Google-GFE-Can-Retry': 'yes'
143- // }
144- // };
145- // return request;
146- // }
147-
148- // //method which is responsible for making an realtime HTTP connection
149- // private async beginRealtimeHttpStream(): void {
150- // if (!this.checkAndSetHttpConnectionFlagIfNotRunning()) {
151- // return;
152- // }
153-
154- // const currentTime = Date.now();
155- // if (currentTime < this.backoffManager.backoffEndTimeMillis) {
156- // this.retryHttpConnectionWhenBackoffEnds();
157- // return;
158- // }
159-
160- // let response: FetchResponse | undefined;
161-
162- // try {
163- // const request = await this.createFetchRequest();
164-
165- // response = await this.fetchClient.fetch(request);
166-
167- // if (response.status === 200 && response.body) {
168- // this.retriesRemaining = MAX_HTTP_RETRIES;
169- // this.backoffCount = 0;
170- // this.backoffManager.reset();
171- // this.saveRealtimeBackoffMetadata();
172-
173- // const parser = new StreamParser(response.body, this.observers);
174- // await parser.listen();
175- // } else {
176- // throw new FirebaseError('http-status-error', `HTTP Error: ${response.status}`);
177- // }
178- // } catch (error) {
179- // if (error.name === 'AbortError') {
180- // return;
181- // }
182- // } finally {
183- // this.isConnectionActive = false;
184-
185- // const statusCode = response?.status;
186- // const connectionFailed = !this.isInBackground && (!statusCode || this.isStatusCodeRetryable(statusCode));
187-
188- // if (connectionFailed) {
189- // this.handleStreamError();
190- // } else if (statusCode && statusCode !== 200) {
191- // const firebaseError = new FirebaseError('config-update-stream-error',
192- // `Unable to connect to the server. HTTP status code: ${statusCode}`);
193- // this.propagateError(firebaseError);
194- // } else {
195- // this.makeRealtimeHttpConnection(0);
196- // }
197- // }
198- // }
116+ private resetRetryCount ( ) : void {
117+ this . retriesRemaining = ORIGINAL_RETRIES ;
118+ }
119+
120+ private async beginRealtimeHttpStream ( ) : Promise < void > {
121+ if ( ! this . checkAndSetHttpConnectionFlagIfNotRunning ( ) ) {
122+ return ;
123+ }
124+
125+ const [ metadataFromStorage , storedVersion ] = await Promise . all ( [
126+ this . storage . getRealtimeBackoffMetadata ( ) ,
127+ this . storage . getLastKnownTemplateVersion ( )
128+ ] ) ;
129+
130+ let metadata ;
131+ if ( metadataFromStorage ) {
132+ metadata = metadataFromStorage ;
133+ } else {
134+ metadata = {
135+ backoffEndTimeMillis : new Date ( 0 ) ,
136+ numFailedStreams : 0
137+ } ;
138+ await this . storage . setRealtimeBackoffMetadata ( metadata ) ;
139+ }
140+
141+ if ( storedVersion !== undefined ) {
142+ this . templateVersion = storedVersion ;
143+ } else {
144+ this . templateVersion = 0 ;
145+ await this . storage . setLastKnownTemplateVersion ( 0 ) ;
146+ }
147+
148+ const backoffEndTime = new Date ( metadata . backoffEndTimeMillis ) . getTime ( ) ;
149+ if ( Date . now ( ) < backoffEndTime ) {
150+ this . retryHttpConnectionWhenBackoffEnds ( ) ;
151+ return ;
152+ }
153+ let response ;
154+ try {
155+ const [ installationId , installationTokenResult ] = await Promise . all ( [
156+ this . firebaseInstallations . getId ( ) ,
157+ this . firebaseInstallations . getToken ( false )
158+ ] ) ;
159+ const headers = {
160+ 'Content-Type' : 'application/json' ,
161+ 'Content-Encoding' : 'gzip' ,
162+ 'If-None-Match' : '*' ,
163+ 'authentication-token' : installationTokenResult
164+ } ;
165+
166+ const url = this . getRealtimeUrl ( ) ;
167+ const requestBody = {
168+ project : this . projectId ,
169+ namespace : this . namespace ,
170+ lastKnownVersionNumber : this . templateVersion . toString ( ) ,
171+ appId : this . appId ,
172+ sdkVersion : this . sdkVersion ,
173+ appInstanceId : installationId
174+ } ;
175+
176+ response = await fetch ( url , {
177+ method : "POST" ,
178+ headers,
179+ body : JSON . stringify ( requestBody )
180+ } ) ;
181+ if ( response . status === 200 && response . body ) {
182+ this . resetRetryCount ( ) ;
183+ this . resetRealtimeBackoff ( ) ;
184+ //code related to start StartAutofetch
185+ //and then give the notification for al the observers
186+ } else {
187+ throw new FirebaseError ( 'http-status-error' , `HTTP Error: ${ response . status } ` ) ;
188+ }
189+ } catch ( error : any ) {
190+ if ( this . isInBackground ) {
191+ // It's possible the app was backgrounded while the connection was open, which
192+ // threw an exception trying to read the response. No real error here, so treat
193+ // this as a success, even if we haven't read a 200 response code yet.
194+ this . resetRetryCount ( ) ;
195+ }
196+ } finally {
197+ this . isConnectionActive = false ;
198+ const statusCode = response ?. status ;
199+ const connectionFailed = ! this . isInBackground && ( ! statusCode || this . isStatusCodeRetryable ( statusCode ) ) ;
200+
201+ if ( connectionFailed ) {
202+ this . updateBackoffMetadataWithLastFailedStreamConnectionTime ( new Date ( ) ) ;
203+ }
204+
205+ if ( connectionFailed || statusCode === 200 ) {
206+ this . retryHttpConnectionWhenBackoffEnds ( ) ;
207+ } else {
208+ //still have to implement this part
209+ let errorMessage = `Unable to connect to the server. Try again in a few minutes. HTTP status code: ${ statusCode } ` ;
210+ if ( statusCode === 403 ) {
211+ //still have to implemet this parseErrorResponseBody method
212+ // errorMessage = await this.parseErrorResponseBody(response?.body);
213+ }
214+ const firebaseError = ERROR_FACTORY . create ( ErrorCode . CONFIG_UPDATE_STREAM_ERROR , {
215+ httpStatus : statusCode ,
216+ originalErrorMessage : errorMessage
217+ } ) ;
218+ this . propagateError ( firebaseError ) ;
219+ }
220+ }
221+ }
222+
223+ private propagateError = ( e : FirebaseError ) => this . observers . forEach ( o => o . error ?.( e ) ) ;
224+
225+ private async updateBackoffMetadataWithLastFailedStreamConnectionTime ( lastFailedStreamTime : Date ) : Promise < void > {
226+ const numFailedStreams = ( ( await this . storage . getRealtimeBackoffMetadata ( ) ) ?. numFailedStreams || 0 ) + 1 ;
227+ const backoffMillis = calculateBackoffMillis ( numFailedStreams ) ;
228+ await this . storage . setRealtimeBackoffMetadata ( {
229+ backoffEndTimeMillis : new Date ( lastFailedStreamTime . getTime ( ) + backoffMillis ) ,
230+ numFailedStreams
231+ } ) ;
232+ }
233+
234+ private isStatusCodeRetryable = ( sc ?: number ) => ! sc || [ 408 , 429 , 500 , 502 , 503 , 504 ] . includes ( sc ) ;
235+
236+ private async retryHttpConnectionWhenBackoffEnds ( ) : Promise < void > {
237+ const metadata = ( await this . storage . getRealtimeBackoffMetadata ( ) ) || {
238+ backoffEndTimeMillis : new Date ( 0 ) ,
239+ numFailedStreams : 0
240+ } ;
241+ const backoffEndTime = new Date ( metadata . backoffEndTimeMillis ) . getTime ( ) ;
242+ const currentTime = Date . now ( ) ;
243+ const retrySeconds = Math . max ( 0 , backoffEndTime - currentTime ) ;
244+ this . makeRealtimeHttpConnection ( retrySeconds ) ;
245+ }
246+
247+ private async resetRealtimeBackoff ( ) : Promise < void > {
248+ await this . storage . setRealtimeBackoffMetadata ( {
249+ backoffEndTimeMillis : new Date ( 0 ) ,
250+ numFailedStreams : 0
251+ } ) ;
252+ }
253+
254+ private getRealtimeUrl ( ) : URL {
255+ const urlBase =
256+ window . FIREBASE_REMOTE_CONFIG_URL_BASE ||
257+ 'https://firebaseremoteconfigrealtime.googleapis.com' ;
258+
259+ const urlString = `${ urlBase } /v1/projects/${ this . projectId } /namespaces/${ this . namespace } :streamFetchInvalidationsfetch?key=${ this . apiKey } ` ;
260+ return new URL ( urlString ) ;
261+ }
262+
199263}
0 commit comments