Skip to content

Commit 5ab4efd

Browse files
committed
fixes
1 parent e6a49c1 commit 5ab4efd

File tree

4 files changed

+233
-136
lines changed

4 files changed

+233
-136
lines changed

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

Lines changed: 193 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,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
}

packages/remote-config/src/errors.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const enum ErrorCode {
3434
FETCH_STATUS = 'fetch-status',
3535
INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable',
3636
CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS = 'custom-signal-max-allowed-signals',
37-
REALTIME_UPDATE_STREAM_ERROR = 'stream-error'
37+
CONFIG_UPDATE_STREAM_ERROR = 'stream-error'
3838
}
3939

4040
const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = {
@@ -74,7 +74,7 @@ const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = {
7474
'Indexed DB is not supported by current browser',
7575
[ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS]:
7676
'Setting more than {$maxSignals} custom signals is not supported.',
77-
[ErrorCode.REALTIME_UPDATE_STREAM_ERROR]:
77+
[ErrorCode.CONFIG_UPDATE_STREAM_ERROR]:
7878
'The stream was not able to connect to the backend.'
7979
};
8080

@@ -95,6 +95,7 @@ interface ErrorParams {
9595
[ErrorCode.FETCH_PARSE]: { originalErrorMessage: string };
9696
[ErrorCode.FETCH_STATUS]: { httpStatus: number };
9797
[ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS]: { maxSignals: number };
98+
[ErrorCode.CONFIG_UPDATE_STREAM_ERROR]: { httpStatus?: number; originalErrorMessage?: string };
9899
}
99100

100101
export const ERROR_FACTORY = new ErrorFactory<ErrorCode, ErrorParams>(

0 commit comments

Comments
 (0)