From 31e89dfef85d03a67d6c55ad5abbabd39699427e Mon Sep 17 00:00:00 2001 From: betabites <61530254+betabites@users.noreply.github.com> Date: Wed, 14 May 2025 13:05:10 +1200 Subject: [PATCH 1/3] Switched firebase/database to use the `fetch()` api --- .../database/src/core/ReadonlyRestClient.ts | 212 ++++++++---------- 1 file changed, 91 insertions(+), 121 deletions(-) diff --git a/packages/database/src/core/ReadonlyRestClient.ts b/packages/database/src/core/ReadonlyRestClient.ts index 6b5d6be843a..2f7846594f4 100644 --- a/packages/database/src/core/ReadonlyRestClient.ts +++ b/packages/database/src/core/ReadonlyRestClient.ts @@ -17,10 +17,7 @@ import { assert, - jsonEval, safeGet, - querystring, - Deferred } from '@firebase/util'; import { AppCheckTokenProvider } from './AppCheckTokenProvider'; @@ -56,7 +53,7 @@ export class ReadonlyRestClient extends ServerActions { } else { assert( query._queryParams.isDefault(), - "should have a tag if it's not a default query." + 'should have a tag if it\'s not a default query.' ); return query._path.toString(); } @@ -81,7 +78,7 @@ export class ReadonlyRestClient extends ServerActions { } /** @inheritDoc */ - listen( + async listen( query: QueryContext, currentHashFn: () => string, tag: number | null, @@ -99,35 +96,34 @@ export class ReadonlyRestClient extends ServerActions { query._queryParams ); - this.restRequest_( + let [response, data] = await this.restRequest_( pathString + '.json', queryStringParameters, - (error, result) => { - let data = result; - - if (error === 404) { - data = null; - error = null; - } - - if (error === null) { - this.onDataUpdate_(pathString, data, /*isMerge=*/ false, tag); - } - - if (safeGet(this.listens_, listenId) === thisListen) { - let status; - if (!error) { - status = 'ok'; - } else if (error === 401) { - status = 'permission_denied'; - } else { - status = 'rest_error:' + error; - } - - onComplete(status, null); - } - } ); + + let error = response.status; + + if (error === 404) { + data = null; + error = null; + } + + if (error === null) { + this.onDataUpdate_(pathString, data, /*isMerge=*/ false, tag); + } + + if (safeGet(this.listens_, listenId) === thisListen) { + let status; + if (!error) { + status = 'ok'; + } else if (error === 401) { + status = 'permission_denied'; + } else { + status = 'rest_error:' + error; + } + + onComplete(status, null); + } } /** @inheritDoc */ @@ -136,40 +132,31 @@ export class ReadonlyRestClient extends ServerActions { delete this.listens_[listenId]; } - get(query: QueryContext): Promise { + async get(query: QueryContext): Promise { const queryStringParameters = queryParamsToRestQueryStringParameters( query._queryParams ); const pathString = query._path.toString(); - const deferred = new Deferred(); - - this.restRequest_( + let [response, data] = await this.restRequest_( pathString + '.json', - queryStringParameters, - (error, result) => { - let data = result; - - if (error === 404) { - data = null; - error = null; - } - - if (error === null) { - this.onDataUpdate_( - pathString, - data, - /*isMerge=*/ false, - /*tag=*/ null - ); - deferred.resolve(data as string); - } else { - deferred.reject(new Error(data as string)); - } - } + queryStringParameters ); - return deferred.promise; + + if (response.status === 404) { + data = null; + } else if (!response.ok) { + throw new Error(data as string); + } + + this.onDataUpdate_( + pathString, + data, + /*isMerge=*/ false, + /*tag=*/ null + ); + return data as string; } /** @inheritDoc */ @@ -181,74 +168,57 @@ export class ReadonlyRestClient extends ServerActions { * Performs a REST request to the given path, with the provided query string parameters, * and any auth credentials we have. */ - private restRequest_( + private async restRequest_( pathString: string, - queryStringParameters: { [k: string]: string | number } = {}, - callback: ((a: number | null, b?: unknown) => void) | null - ) { - queryStringParameters['format'] = 'export'; + queryStringParameters: Record = {}, + ): Promise<[Response, T | null]> { - return Promise.all([ + // Fetch tokens + const [authToken, appCheckToken] = await Promise.all([ this.authTokenProvider_.getToken(/*forceRefresh=*/ false), this.appCheckTokenProvider_.getToken(/*forceRefresh=*/ false) - ]).then(([authToken, appCheckToken]) => { - if (authToken && authToken.accessToken) { - queryStringParameters['auth'] = authToken.accessToken; - } - if (appCheckToken && appCheckToken.token) { - queryStringParameters['ac'] = appCheckToken.token; - } + ]); + + // Configure URL parameters + const searchParams = new URLSearchParams(queryStringParameters as Record); + if (authToken && authToken.accessToken) { + searchParams.set('auth', authToken.accessToken); + } + if (appCheckToken && appCheckToken.token) { + searchParams.set("ac", appCheckToken.token); + } + searchParams.set('format', 'export'); + searchParams.set('ns', this.repoInfo_.namespace); + + // Build & send the request + const url = + (this.repoInfo_.secure ? 'https://' : 'http://') + + this.repoInfo_.host + + pathString + + '?' + + searchParams.toString(); + + this.log_('Sending REST request for ' + url); + const response = await fetch(url); + if (!response.ok) { + // Request was not successful, so throw an error + throw new Error(`REST request at ${url} returned error: ${response.status}`); + } + + this.log_( + 'REST Response for ' + url + ' received. status:', + response.status, + ); + let result: T | null = null; + try { + result = await response.json(); + } catch (e) { + warn( + 'Failed to parse server response as json.', + e + ); + } - const url = - (this.repoInfo_.secure ? 'https://' : 'http://') + - this.repoInfo_.host + - pathString + - '?' + - 'ns=' + - this.repoInfo_.namespace + - querystring(queryStringParameters); - - this.log_('Sending REST request for ' + url); - const xhr = new XMLHttpRequest(); - xhr.onreadystatechange = () => { - if (callback && xhr.readyState === 4) { - this.log_( - 'REST Response for ' + url + ' received. status:', - xhr.status, - 'response:', - xhr.responseText - ); - let res = null; - if (xhr.status >= 200 && xhr.status < 300) { - try { - res = jsonEval(xhr.responseText); - } catch (e) { - warn( - 'Failed to parse JSON response for ' + - url + - ': ' + - xhr.responseText - ); - } - callback(null, res); - } else { - // 401 and 404 are expected. - if (xhr.status !== 401 && xhr.status !== 404) { - warn( - 'Got unsuccessful REST response for ' + - url + - ' Status: ' + - xhr.status - ); - } - callback(xhr.status); - } - callback = null; - } - }; - - xhr.open('GET', url, /*asynchronous=*/ true); - xhr.send(); - }); + return [response, result]; } } From 0ff273c7acbaf9f29c81efd7928d3b88d12a3dd4 Mon Sep 17 00:00:00 2001 From: betabites <61530254+betabites@users.noreply.github.com> Date: Thu, 15 May 2025 17:42:17 +1200 Subject: [PATCH 2/3] Added changeset --- .changeset/little-worms-burn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/little-worms-burn.md diff --git a/.changeset/little-worms-burn.md b/.changeset/little-worms-burn.md new file mode 100644 index 00000000000..e93f6eb8b10 --- /dev/null +++ b/.changeset/little-worms-burn.md @@ -0,0 +1,5 @@ +--- +'@firebase/database': patch +--- + +Switched out usage of XMLHttpRequest for fetch() From 39e469e1564065d6a3fba591488d0c8d8a39a697 Mon Sep 17 00:00:00 2001 From: betabites <61530254+betabites@users.noreply.github.com> Date: Thu, 15 May 2025 17:54:53 +1200 Subject: [PATCH 3/3] Corrected formatting --- .../database/src/core/ReadonlyRestClient.ts | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/packages/database/src/core/ReadonlyRestClient.ts b/packages/database/src/core/ReadonlyRestClient.ts index 2f7846594f4..7412707f6ac 100644 --- a/packages/database/src/core/ReadonlyRestClient.ts +++ b/packages/database/src/core/ReadonlyRestClient.ts @@ -15,10 +15,7 @@ * limitations under the License. */ -import { - assert, - safeGet, -} from '@firebase/util'; +import { assert, safeGet } from '@firebase/util'; import { AppCheckTokenProvider } from './AppCheckTokenProvider'; import { AuthTokenProvider } from './AuthTokenProvider'; @@ -53,7 +50,7 @@ export class ReadonlyRestClient extends ServerActions { } else { assert( query._queryParams.isDefault(), - 'should have a tag if it\'s not a default query.' + "should have a tag if it's not a default query." ); return query._path.toString(); } @@ -98,7 +95,7 @@ export class ReadonlyRestClient extends ServerActions { let [response, data] = await this.restRequest_( pathString + '.json', - queryStringParameters, + queryStringParameters ); let error = response.status; @@ -144,19 +141,14 @@ export class ReadonlyRestClient extends ServerActions { queryStringParameters ); - if (response.status === 404) { - data = null; - } else if (!response.ok) { - throw new Error(data as string); - } + if (response.status === 404) { + data = null; + } else if (!response.ok) { + throw new Error(data as string); + } - this.onDataUpdate_( - pathString, - data, - /*isMerge=*/ false, - /*tag=*/ null - ); - return data as string; + this.onDataUpdate_(pathString, data, /*isMerge=*/ false, /*tag=*/ null); + return data as string; } /** @inheritDoc */ @@ -170,9 +162,8 @@ export class ReadonlyRestClient extends ServerActions { */ private async restRequest_( pathString: string, - queryStringParameters: Record = {}, + queryStringParameters: Record = {} ): Promise<[Response, T | null]> { - // Fetch tokens const [authToken, appCheckToken] = await Promise.all([ this.authTokenProvider_.getToken(/*forceRefresh=*/ false), @@ -180,12 +171,14 @@ export class ReadonlyRestClient extends ServerActions { ]); // Configure URL parameters - const searchParams = new URLSearchParams(queryStringParameters as Record); + const searchParams = new URLSearchParams( + queryStringParameters as Record + ); if (authToken && authToken.accessToken) { searchParams.set('auth', authToken.accessToken); } if (appCheckToken && appCheckToken.token) { - searchParams.set("ac", appCheckToken.token); + searchParams.set('ac', appCheckToken.token); } searchParams.set('format', 'export'); searchParams.set('ns', this.repoInfo_.namespace); @@ -202,21 +195,20 @@ export class ReadonlyRestClient extends ServerActions { const response = await fetch(url); if (!response.ok) { // Request was not successful, so throw an error - throw new Error(`REST request at ${url} returned error: ${response.status}`); + throw new Error( + `REST request at ${url} returned error: ${response.status}` + ); } this.log_( 'REST Response for ' + url + ' received. status:', - response.status, + response.status ); let result: T | null = null; try { result = await response.json(); } catch (e) { - warn( - 'Failed to parse server response as json.', - e - ); + warn('Failed to parse server response as json.', e); } return [response, result];