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,199 @@ 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+ if ( this . scheduledConnectionTimeoutId ) {
84+ clearTimeout ( this . scheduledConnectionTimeoutId ) ;
85+ }
86+ if ( ! this . canEstablishStreamConnection ( ) ) {
87+ return ;
88+ }
89+ this . scheduledConnectionTimeoutId = setTimeout ( ( ) => {
90+ if ( this . retriesRemaining > 0 ) {
91+ this . retriesRemaining -- ;
92+ this . beginRealtimeHttpStream ( ) ;
93+ } else if ( ! this . isInBackground ) {
94+ const error = ERROR_FACTORY . create ( ErrorCode . CONFIG_UPDATE_STREAM_ERROR , { originalErrorMessage : 'Unable to connect to the server. Check your connection and try again.' } ) ;
95+ this . propagateError ( error ) ;
96+ }
97+ } , delayMillis ) ;
98+ }
99+
100+ private setIsHttpConnectionRunning ( connectionRunning : boolean ) : void {
101+ this . isConnectionActive = connectionRunning ;
102+ }
97103
98104 private checkAndSetHttpConnectionFlagIfNotRunning ( ) : boolean {
99105 if ( this . canEstablishStreamConnection ( ) ) {
100106 this . streamController = new AbortController ( ) ;
101- this . isConnectionActive = true ;
107+ this . setIsHttpConnectionRunning ( true ) ;
102108 return true ;
103109 }
104110 return false ;
105111 }
106112
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- // }
113+ private resetRetryCount ( ) : void {
114+ this . retriesRemaining = ORIGINAL_RETRIES ;
115+ }
116+
117+ private async beginRealtimeHttpStream ( ) : Promise < void > {
118+ if ( ! this . checkAndSetHttpConnectionFlagIfNotRunning ( ) ) {
119+ return ;
120+ }
121+
122+ const [ metadataFromStorage , storedVersion ] = await Promise . all ( [
123+ this . storage . getRealtimeBackoffMetadata ( ) ,
124+ this . storage . getLastKnownTemplateVersion ( )
125+ ] ) ;
126+
127+ let metadata ;
128+ if ( metadataFromStorage ) {
129+ metadata = metadataFromStorage ;
130+ } else {
131+ metadata = {
132+ backoffEndTimeMillis : new Date ( 0 ) ,
133+ numFailedStreams : 0
134+ } ;
135+ await this . storage . setRealtimeBackoffMetadata ( metadata ) ;
136+ }
137+
138+ if ( storedVersion !== undefined ) {
139+ this . templateVersion = storedVersion ;
140+ } else {
141+ this . templateVersion = 0 ;
142+ await this . storage . setLastKnownTemplateVersion ( 0 ) ;
143+ }
144+
145+ const backoffEndTime = new Date ( metadata . backoffEndTimeMillis ) . getTime ( ) ;
146+ if ( Date . now ( ) < backoffEndTime ) {
147+ this . retryHttpConnectionWhenBackoffEnds ( ) ;
148+ return ;
149+ }
150+ let response ;
151+ try {
152+ const [ installationId , installationTokenResult ] = await Promise . all ( [
153+ this . firebaseInstallations . getId ( ) ,
154+ this . firebaseInstallations . getToken ( false )
155+ ] ) ;
156+ const headers = {
157+ 'Content-Type' : 'application/json' ,
158+ 'Content-Encoding' : 'gzip' ,
159+ 'If-None-Match' : '*' ,
160+ 'authentication-token' : installationTokenResult
161+ } ;
162+
163+ const url = this . getRealtimeUrl ( ) ;
164+ const requestBody = {
165+ project : this . projectId ,
166+ namespace : this . namespace ,
167+ lastKnownVersionNumber : this . templateVersion . toString ( ) ,
168+ appId : this . appId ,
169+ sdkVersion : this . sdkVersion ,
170+ appInstanceId : installationId
171+ } ;
172+
173+ response = await fetch ( url , {
174+ method : "POST" ,
175+ headers,
176+ body : JSON . stringify ( requestBody )
177+ } ) ;
178+ if ( response . status === 200 && response . body ) {
179+ this . resetRetryCount ( ) ;
180+ this . resetRealtimeBackoff ( ) ;
181+ //code related to start StartAutofetch
182+ //and then give the notification for al the observers
183+ } else {
184+ throw new FirebaseError ( 'http-status-error' , `HTTP Error: ${ response . status } ` ) ;
185+ }
186+ } catch ( error : any ) {
187+ if ( this . isInBackground ) {
188+ // It's possible the app was backgrounded while the connection was open, which
189+ // threw an exception trying to read the response. No real error here, so treat
190+ // this as a success, even if we haven't read a 200 response code yet.
191+ this . resetRetryCount ( ) ;
192+ }
193+ } finally {
194+ this . isConnectionActive = false ;
195+ const statusCode = response ?. status ;
196+ const connectionFailed = ! this . isInBackground && ( ! statusCode || this . isStatusCodeRetryable ( statusCode ) ) ;
197+
198+ if ( connectionFailed ) {
199+ this . updateBackoffMetadataWithLastFailedStreamConnectionTime ( new Date ( ) ) ;
200+ }
201+
202+ if ( connectionFailed || statusCode === 200 ) {
203+ this . retryHttpConnectionWhenBackoffEnds ( ) ;
204+ } else {
205+ //still have to implement this part
206+ 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);
210+ }
211+ const firebaseError = ERROR_FACTORY . create ( ErrorCode . CONFIG_UPDATE_STREAM_ERROR , {
212+ httpStatus : statusCode ,
213+ originalErrorMessage : errorMessage
214+ } ) ;
215+ this . propagateError ( firebaseError ) ;
216+ }
217+ }
218+ }
219+
220+ private propagateError = ( e : FirebaseError ) => this . observers . forEach ( o => o . error ?.( e ) ) ;
221+
222+ private async updateBackoffMetadataWithLastFailedStreamConnectionTime ( lastFailedStreamTime : Date ) : Promise < void > {
223+ const numFailedStreams = ( ( await this . storage . getRealtimeBackoffMetadata ( ) ) ?. numFailedStreams || 0 ) + 1 ;
224+ const backoffMillis = calculateBackoffMillis ( numFailedStreams ) ;
225+ await this . storage . setRealtimeBackoffMetadata ( {
226+ backoffEndTimeMillis : new Date ( lastFailedStreamTime . getTime ( ) + backoffMillis ) ,
227+ numFailedStreams
228+ } ) ;
229+ }
230+
231+ private isStatusCodeRetryable = ( sc ?: number ) => ! sc || [ 408 , 429 , 500 , 502 , 503 , 504 ] . includes ( sc ) ;
232+
233+ private async retryHttpConnectionWhenBackoffEnds ( ) : Promise < void > {
234+ const metadata = ( await this . storage . getRealtimeBackoffMetadata ( ) ) || {
235+ backoffEndTimeMillis : new Date ( 0 ) ,
236+ numFailedStreams : 0
237+ } ;
238+ const backoffEndTime = new Date ( metadata . backoffEndTimeMillis ) . getTime ( ) ;
239+ const currentTime = Date . now ( ) ;
240+ const retrySeconds = Math . max ( 0 , backoffEndTime - currentTime ) ;
241+ this . makeRealtimeHttpConnection ( retrySeconds ) ;
242+ }
243+
244+ private async resetRealtimeBackoff ( ) : Promise < void > {
245+ await this . storage . setRealtimeBackoffMetadata ( {
246+ backoffEndTimeMillis : new Date ( 0 ) ,
247+ numFailedStreams : 0
248+ } ) ;
249+ }
250+
251+ private getRealtimeUrl ( ) : URL {
252+ const urlBase =
253+ window . FIREBASE_REMOTE_CONFIG_URL_BASE ||
254+ 'https://firebaseremoteconfigrealtime.googleapis.com' ;
255+
256+ const urlString = `${ urlBase } /v1/projects/${ this . projectId } /namespaces/${ this . namespace } :streamFetchInvalidationsfetch?key=${ this . apiKey } ` ;
257+ return new URL ( urlString ) ;
258+ }
259+
199260}
0 commit comments