15
15
* limitations under the License.
16
16
*/
17
17
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' ;
22
21
import { _FirebaseInstallationsInternal } from '@firebase/installations' ;
22
+ import { Storage } from '../storage/storage' ;
23
+ import { calculateBackoffMillis , FirebaseError } from '@firebase/util' ;
24
+
23
25
export class RealtimeHandler {
24
- constructor (
26
+ constructor (
25
27
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
+ ) { }
28
35
29
36
private streamController ?: AbortController ;
30
37
private observers : Set < ConfigUpdateObserver > = new Set < ConfigUpdateObserver > ( ) ;
31
38
private isConnectionActive : boolean = false ;
32
- private retriesRemaining : number = MAX_HTTP_RETRIES ;
39
+ private retriesRemaining : number = ORIGINAL_RETRIES ;
33
40
private isRealtimeDisabled : boolean = false ;
34
41
private isInBackground : boolean = false ;
35
- private backoffCount : number = 0 ;
36
42
private scheduledConnectionTimeoutId ?: ReturnType < typeof setTimeout > ;
37
- // private backoffManager: BackoffManager = new BackoffManager();
38
-
39
-
43
+ private templateVersion : number = 0 ;
40
44
/**
41
45
* Adds an observer to the realtime updates.
42
46
* @param observer The observer to add.
43
47
*/
44
48
addObserver ( observer : ConfigUpdateObserver ) : void {
45
49
this . observers . add ( observer ) ;
46
- // this.beginRealtime();
50
+ this . beginRealtime ( ) ;
47
51
}
48
52
49
53
/**
@@ -58,142 +62,202 @@ export class RealtimeHandler {
58
62
59
63
private beginRealtime ( ) : void {
60
64
if ( this . observers . size > 0 ) {
61
- this . retriesRemaining = MAX_HTTP_RETRIES ;
62
- this . backoffCount = 0 ;
63
- // this.makeRealtimeHttpConnection(0);
65
+ this . makeRealtimeHttpConnection ( 0 ) ;
64
66
}
65
67
}
66
68
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
+ */
71
73
private canEstablishStreamConnection ( ) : boolean {
72
74
const hasActiveListeners = this . observers . size > 0 ;
73
75
const isNotDisabled = ! this . isRealtimeDisabled ;
74
76
const isForeground = ! this . isInBackground ;
75
- return hasActiveListeners && isNotDisabled && isForeground ;
77
+ const isNoConnectionActive = ! this . isConnectionActive ;
78
+ return hasActiveListeners && isNotDisabled && isForeground && isNoConnectionActive ;
76
79
}
77
80
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
- // }
96
81
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
+ }
97
106
98
107
private checkAndSetHttpConnectionFlagIfNotRunning ( ) : boolean {
99
108
if ( this . canEstablishStreamConnection ( ) ) {
100
109
this . streamController = new AbortController ( ) ;
101
- this . isConnectionActive = true ;
110
+ this . setIsHttpConnectionRunning ( true ) ;
102
111
return true ;
103
112
}
104
113
return false ;
105
114
}
106
115
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
+
199
263
}
0 commit comments