Skip to content

Commit 190bef9

Browse files
committed
Added the the calculation of randomized backofftime, Corrected the error, Storage related to realtime_backoff_metadata,
last_known_template_version
1 parent e6a49c1 commit 190bef9

File tree

4 files changed

+236
-136
lines changed

4 files changed

+236
-136
lines changed

packages/remote-config/src/client/realtime_handler.ts

Lines changed: 196 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,39 @@
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';
2221
import { _FirebaseInstallationsInternal } from '@firebase/installations';
22+
import { Storage } from '../storage/storage';
23+
import { calculateBackoffMillis, FirebaseError } from '@firebase/util';
24+
2325
export 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

Comments
 (0)