From 13c1b1db4d1eb80ff629cd311cb36846657ace6a Mon Sep 17 00:00:00 2001 From: Kevin Elko Date: Wed, 13 Nov 2024 11:45:01 -0500 Subject: [PATCH 01/10] add serialization method to server side remote config --- src/remote-config/remote-config-api.ts | 18 ++++++++++++++++++ src/remote-config/remote-config.ts | 12 ++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts index 4dfa1da023..908d35cbe8 100644 --- a/src/remote-config/remote-config-api.ts +++ b/src/remote-config/remote-config-api.ts @@ -727,6 +727,24 @@ export interface ServerConfig { * @returns The value for the given key. */ getValue(key: string): Value; + + /** + * Returns a JSON-serializable representation of the current config values, including an eTag + * that can be utilized by the Remote Config web client SDK. + * + * @returns JSON-serializable config object. + */ + serializeForClient(): FetchResponse; +} + +/** + * JSON-serializable representation of evaluated config values. This can be consumed by + * Remote Config web client SDKs. + */ +export interface FetchResponse { + status: number; + eTag?: string; + config?: {[key: string]: string}; } /** diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index c529501315..f46eb8be80 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -41,6 +41,7 @@ import { GetServerTemplateOptions, InitServerTemplateOptions, ServerTemplateDataType, + FetchResponse, } from './remote-config-api'; /** @@ -438,6 +439,17 @@ class ServerConfigImpl implements ServerConfig { getValue(key: string): Value { return this.configValues[key] || new ValueImpl('static'); } + serializeForClient(): FetchResponse { + const config: {[key:string]:string} = {}; + for (const [param, value] of Object.entries(this.configValues)) { + config[param] = value.asString(); + } + return { + status: 200, + eTag: `etag-${Math.floor(Math.random() * 100000)}`, + config, + }; + } } /** From 62bf436e7c9848cb8cadfe57e4c298cb8295a6dc Mon Sep 17 00:00:00 2001 From: Kevin Elko Date: Tue, 3 Dec 2024 15:23:05 -0500 Subject: [PATCH 02/10] update with latest changes --- src/remote-config/index.ts | 21 +++++++++++++++++++++ src/remote-config/remote-config-api.ts | 7 +++---- src/remote-config/remote-config.ts | 13 ++----------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts index 7b66c44ee9..9d9ae39376 100644 --- a/src/remote-config/index.ts +++ b/src/remote-config/index.ts @@ -23,6 +23,7 @@ import { App, getApp } from '../app'; import { FirebaseApp } from '../app/firebase-app'; import { RemoteConfig } from './remote-config'; +import { ServerConfig, FetchResponse } from './remote-config-api'; export { AndCondition, @@ -31,6 +32,7 @@ export { DefaultConfig, EvaluationContext, ExplicitParameterValue, + FetchResponse, GetServerTemplateOptions, InAppDefaultValue, InitServerTemplateOptions, @@ -96,3 +98,22 @@ export function getRemoteConfig(app?: App): RemoteConfig { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('remoteConfig', (app) => new RemoteConfig(app)); } + +/** + * Returns a JSON-serializable representation of the current config values, including an eTag + * that can be utilized by the Remote Config web client SDK. + * + * @returns JSON-serializable config object. + */ +export function buildFetchResponse(serverConfig: ServerConfig, etag?: string): FetchResponse { + const config: {[key:string]: string} = {}; + for (const [param, value] of Object.entries(serverConfig.getAll())) { + config[param] = value.asString(); + } + // TODO - compute etag + return { + status: 200, + eTag: `etag-${Math.floor(Math.random() * 100000)}`, + config, + }; +} \ No newline at end of file diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts index 908d35cbe8..87fd85fba5 100644 --- a/src/remote-config/remote-config-api.ts +++ b/src/remote-config/remote-config-api.ts @@ -729,12 +729,11 @@ export interface ServerConfig { getValue(key: string): Value; /** - * Returns a JSON-serializable representation of the current config values, including an eTag - * that can be utilized by the Remote Config web client SDK. + * Returns all config values. * - * @returns JSON-serializable config object. + * @returns A map of all config keys to their values. */ - serializeForClient(): FetchResponse; + getAll(): {[key:string]: Value} } /** diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index f46eb8be80..9355452948 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -41,7 +41,6 @@ import { GetServerTemplateOptions, InitServerTemplateOptions, ServerTemplateDataType, - FetchResponse, } from './remote-config-api'; /** @@ -439,16 +438,8 @@ class ServerConfigImpl implements ServerConfig { getValue(key: string): Value { return this.configValues[key] || new ValueImpl('static'); } - serializeForClient(): FetchResponse { - const config: {[key:string]:string} = {}; - for (const [param, value] of Object.entries(this.configValues)) { - config[param] = value.asString(); - } - return { - status: 200, - eTag: `etag-${Math.floor(Math.random() * 100000)}`, - config, - }; + getAll(): {[key: string]: Value} { + return {...this.configValues}; } } From 812f374adabce5e37c8043abda9a03e418944781 Mon Sep 17 00:00:00 2001 From: kjelko Date: Wed, 4 Dec 2024 15:54:10 -0500 Subject: [PATCH 03/10] restructure as a class, add documentation --- src/remote-config/index.ts | 24 +------- src/remote-config/remote-config-api.ts | 4 +- src/remote-config/remote-config.ts | 50 +++++++++++++++- test/unit/remote-config/remote-config.spec.ts | 60 +++++++++++++++++++ 4 files changed, 113 insertions(+), 25 deletions(-) diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts index 9d9ae39376..cb3383e3d3 100644 --- a/src/remote-config/index.ts +++ b/src/remote-config/index.ts @@ -23,7 +23,6 @@ import { App, getApp } from '../app'; import { FirebaseApp } from '../app/firebase-app'; import { RemoteConfig } from './remote-config'; -import { ServerConfig, FetchResponse } from './remote-config-api'; export { AndCondition, @@ -32,7 +31,7 @@ export { DefaultConfig, EvaluationContext, ExplicitParameterValue, - FetchResponse, + FetchResponseData, GetServerTemplateOptions, InAppDefaultValue, InitServerTemplateOptions, @@ -62,7 +61,7 @@ export { ValueSource, Version, } from './remote-config-api'; -export { RemoteConfig } from './remote-config'; +export { RemoteConfig, RemoteConfigFetchResponse } from './remote-config'; /** * Gets the {@link RemoteConfig} service for the default app or a given app. @@ -97,23 +96,4 @@ export function getRemoteConfig(app?: App): RemoteConfig { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('remoteConfig', (app) => new RemoteConfig(app)); -} - -/** - * Returns a JSON-serializable representation of the current config values, including an eTag - * that can be utilized by the Remote Config web client SDK. - * - * @returns JSON-serializable config object. - */ -export function buildFetchResponse(serverConfig: ServerConfig, etag?: string): FetchResponse { - const config: {[key:string]: string} = {}; - for (const [param, value] of Object.entries(serverConfig.getAll())) { - config[param] = value.asString(); - } - // TODO - compute etag - return { - status: 200, - eTag: `etag-${Math.floor(Math.random() * 100000)}`, - config, - }; } \ No newline at end of file diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts index 87fd85fba5..6d83757455 100644 --- a/src/remote-config/remote-config-api.ts +++ b/src/remote-config/remote-config-api.ts @@ -740,9 +740,9 @@ export interface ServerConfig { * JSON-serializable representation of evaluated config values. This can be consumed by * Remote Config web client SDKs. */ -export interface FetchResponse { +export interface FetchResponseData { status: number; - eTag?: string; + eTag: string; config?: {[key: string]: string}; } diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 9355452948..27baaa7e6e 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -15,6 +15,7 @@ */ import { App } from '../app'; +import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { FirebaseRemoteConfigError, RemoteConfigApiClient } from './remote-config-api-client-internal'; import { ConditionEvaluator } from './condition-evaluator-internal'; @@ -41,6 +42,7 @@ import { GetServerTemplateOptions, InitServerTemplateOptions, ServerTemplateDataType, + FetchResponseData, } from './remote-config-api'; /** @@ -439,7 +441,7 @@ class ServerConfigImpl implements ServerConfig { return this.configValues[key] || new ValueImpl('static'); } getAll(): {[key: string]: Value} { - return {...this.configValues}; + return { ...this.configValues }; } } @@ -616,3 +618,49 @@ class VersionImpl implements Version { return validator.isNonEmptyString(timestamp) && (new Date(timestamp)).getTime() > 0; } } + +/** + * Represents a fetch response that can be used to interact with RC's client SDK. + */ +export class RemoteConfigFetchResponse { + private response: FetchResponseData; + + constructor(app: App, serverConfig: ServerConfig, eTag?: string) { + const config: {[key:string]: string} = {}; + for (const [param, value] of Object.entries(serverConfig.getAll())) { + config[param] = value.asString(); + } + + const currentEtag = this.processEtag(config, app); + + if (currentEtag === eTag) { + this.response = { + status: 304, + eTag, + }; + } else { + this.response = { + status: 200, + eTag: currentEtag, + config, + } + } + } + + toJSON(): FetchResponseData { + return this.response; + } + + private processEtag(config: {[key:string]: string}, app: App): string { + const configJson = JSON.stringify(config); + let hash = 0; + for (let i = 0; i < configJson.length; i++) { + const char = configJson.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash |= 0; + } + const projectId = utils.getExplicitProjectId(app); + const parts = ['etag', projectId, 'firebase-server', 'fetch', hash]; + return parts.filter(a => !!a).join('-'); + } +} \ No newline at end of file diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index 526dc0699e..a69d9c44af 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -26,6 +26,7 @@ import { RemoteConfigCondition, TagColor, ListVersionsResult, + RemoteConfigFetchResponse, } from '../../../src/remote-config/index'; import { FirebaseApp } from '../../../src/app/firebase-app'; import * as mocks from '../../resources/mocks'; @@ -1391,6 +1392,65 @@ describe('RemoteConfig', () => { }); }); + describe('RemoteConfigFetchResponse', () => { + it('should return a 200 response with no etag', () => { + const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + // Defines remote parameter values. + templateData.parameters = { + dog_type: { + defaultValue: { + value: 'beagle' + } + } + }; + const template = remoteConfig.initServerTemplate({ template: templateData }); + const fetchResponse = new RemoteConfigFetchResponse(mockApp, template.evaluate()); + expect(fetchResponse.toJSON()).deep.equals({ + status: 200, + eTag: 'etag-project_id-firebase-server-fetch--2039110429', + config: { 'dog_type': 'beagle' } + }); + }); + + it('should return a 200 response with a stale etag', () => { + const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + // Defines remote parameter values. + templateData.parameters = { + dog_type: { + defaultValue: { + value: 'beagle' + } + } + }; + const template = remoteConfig.initServerTemplate({ template: templateData }); + const fetchResponse = new RemoteConfigFetchResponse(mockApp, template.evaluate(), 'fake-etag'); + expect(fetchResponse.toJSON()).deep.equals({ + status: 200, + eTag: 'etag-project_id-firebase-server-fetch--2039110429', + config: { 'dog_type': 'beagle' } + }); + }); + + it('should return a 304 repsonse with matching etag', () => { + const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + // Defines remote parameter values. + templateData.parameters = { + dog_type: { + defaultValue: { + value: 'beagle' + } + } + }; + const template = remoteConfig.initServerTemplate({ template: templateData }); + const fetchResponse = new RemoteConfigFetchResponse( + mockApp, template.evaluate(), 'etag-project_id-firebase-server-fetch--2039110429'); + expect(fetchResponse.toJSON()).deep.equals({ + status: 304, + eTag: 'etag-project_id-firebase-server-fetch--2039110429' + }); + }); + }); + function runInvalidResponseTests(rcOperation: () => Promise, operationName: any): void { it('should propagate API errors', () => { From eaf78e273f5145f7bd6d9c0d6e936c1c79220324 Mon Sep 17 00:00:00 2001 From: kjelko Date: Wed, 4 Dec 2024 16:09:54 -0500 Subject: [PATCH 04/10] more tests for server config --- src/remote-config/remote-config.ts | 2 +- test/unit/remote-config/remote-config.spec.ts | 37 ++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 27baaa7e6e..8a60b2cf9c 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -663,4 +663,4 @@ export class RemoteConfigFetchResponse { const parts = ['etag', projectId, 'firebase-server', 'fetch', hash]; return parts.filter(a => !!a).join('-'); } -} \ No newline at end of file +} diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index a69d9c44af..3270492d5d 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -1293,13 +1293,46 @@ describe('RemoteConfig', () => { // Note the static source is set in the getValue() method, but the other sources // are set in the evaluate() method, so these tests span a couple layers. describe('ServerConfig', () => { + describe('getAll', () => { + it('should return all values', () => { + const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + templateData.parameters = { + dog_type: { + defaultValue: { + value: 'pug' + } + }, + dog_type_enabled: { + defaultValue: { + value: 'true' + } + }, + dog_age: { + defaultValue: { + value: '22' + } + }, + dog_use_inapp_default: { + defaultValue: { + useInAppDefault: true + } + }, + }; + const template = remoteConfig.initServerTemplate({ template: templateData }); + const config = template.evaluate().getAll(); + expect(Object.keys(config)).deep.equal(['dog_type', 'dog_type_enabled', 'dog_age']); + expect(config['dog_type'].asString()).to.equal('pug'); + expect(config['dog_type_enabled'].asBoolean()).to.equal(true); + expect(config['dog_age'].asNumber()).to.equal(22); + }); + }); + describe('getValue', () => { it('should return static when default and remote are not defined', () => { const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; // Omits remote parameter values. templateData.parameters = { - }; - // Omits in-app default values. + } const template = remoteConfig.initServerTemplate({ template: templateData }); const config = template.evaluate(); const value = config.getValue('dog_type'); From e1dff128a4424169a1249864b0ba22b004d19756 Mon Sep 17 00:00:00 2001 From: kjelko Date: Fri, 3 Jan 2025 11:59:22 -0500 Subject: [PATCH 05/10] run apidocs --- etc/firebase-admin.remote-config.api.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md index 68d760468c..0d39f2b0ab 100644 --- a/etc/firebase-admin.remote-config.api.md +++ b/etc/firebase-admin.remote-config.api.md @@ -52,6 +52,18 @@ export interface ExplicitParameterValue { value: string; } +// @public +export interface FetchResponseData { + // (undocumented) + config?: { + [key: string]: string; + }; + // (undocumented) + eTag: string; + // (undocumented) + status: number; +} + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts // // @public @@ -162,6 +174,13 @@ export interface RemoteConfigCondition { tagColor?: TagColor; } +// @public +export class RemoteConfigFetchResponse { + constructor(app: App, serverConfig: ServerConfig, eTag?: string); + // (undocumented) + toJSON(): FetchResponseData; +} + // @public export interface RemoteConfigParameter { conditionalValues?: { @@ -205,6 +224,9 @@ export interface RemoteConfigUser { // @public export interface ServerConfig { + getAll(): { + [key: string]: Value; + }; getBoolean(key: string): boolean; getNumber(key: string): number; getString(key: string): string; From 805dec81e755778cbd7fdd228fcc689e69b7411b Mon Sep 17 00:00:00 2001 From: kjelko Date: Fri, 3 Jan 2025 12:22:17 -0500 Subject: [PATCH 06/10] Refine api and rerun docs --- etc/firebase-admin.remote-config.api.md | 5 +---- src/remote-config/remote-config-api.ts | 25 +++++++++++++++++++---- src/remote-config/remote-config.ts | 27 ++++++++++++++++--------- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md index 0d39f2b0ab..b821417506 100644 --- a/etc/firebase-admin.remote-config.api.md +++ b/etc/firebase-admin.remote-config.api.md @@ -54,13 +54,10 @@ export interface ExplicitParameterValue { // @public export interface FetchResponseData { - // (undocumented) config?: { [key: string]: string; }; - // (undocumented) eTag: string; - // (undocumented) status: number; } @@ -176,7 +173,7 @@ export interface RemoteConfigCondition { // @public export class RemoteConfigFetchResponse { - constructor(app: App, serverConfig: ServerConfig, eTag?: string); + constructor(app: App, serverConfig: ServerConfig, requestEtag?: string); // (undocumented) toJSON(): FetchResponseData; } diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts index 6d83757455..fafdd42cf5 100644 --- a/src/remote-config/remote-config-api.ts +++ b/src/remote-config/remote-config-api.ts @@ -236,7 +236,7 @@ export enum CustomSignalOperator { /** * Matches a numeric value less than or equal to the target value. */ - NUMERIC_LESS_EQUAL ='NUMERIC_LESS_EQUAL', + NUMERIC_LESS_EQUAL = 'NUMERIC_LESS_EQUAL', /** * Matches a numeric value equal to the target value. @@ -537,7 +537,7 @@ export interface ServerTemplate { /** * Generic map of developer-defined signals used as evaluation input signals. */ -export type UserProvidedSignals = {[key: string]: string|number}; +export type UserProvidedSignals = { [key: string]: string | number }; /** * Predefined template evaluation input signals. @@ -733,7 +733,7 @@ export interface ServerConfig { * * @returns A map of all config keys to their values. */ - getAll(): {[key:string]: Value} + getAll(): { [key: string]: Value } } /** @@ -741,9 +741,26 @@ export interface ServerConfig { * Remote Config web client SDKs. */ export interface FetchResponseData { + /** + * The HTTP status, which is useful for differentiating success responses with data from + * those without. + * + * This use of 200 and 304 response codes is consistent with Remote Config's server + * implementation. + */ status: number; + + /** + * Defines the ETag response header value. + * + * This is consistent with Remote Config's server eTag implementation. + */ eTag: string; - config?: {[key: string]: string}; + + /** + * Defines the map of parameters returned as "entries" in the fetch response body. + */ + config?: { [key: string]: string }; } /** diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 8a60b2cf9c..2addb1f055 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -300,7 +300,7 @@ class RemoteConfigTemplateImpl implements RemoteConfigTemplate { */ class ServerTemplateImpl implements ServerTemplate { private cache: ServerTemplateData; - private stringifiedDefaultConfig: {[key: string]: string} = {}; + private stringifiedDefaultConfig: { [key: string]: string } = {}; constructor( private readonly apiClient: RemoteConfigApiClient, @@ -427,7 +427,7 @@ class ServerTemplateImpl implements ServerTemplate { class ServerConfigImpl implements ServerConfig { constructor( private readonly configValues: { [key: string]: Value }, - ){} + ) { } getBoolean(key: string): boolean { return this.getValue(key).asBoolean(); } @@ -440,7 +440,7 @@ class ServerConfigImpl implements ServerConfig { getValue(key: string): Value { return this.configValues[key] || new ValueImpl('static'); } - getAll(): {[key: string]: Value} { + getAll(): { [key: string]: Value } { return { ...this.configValues }; } } @@ -625,18 +625,23 @@ class VersionImpl implements Version { export class RemoteConfigFetchResponse { private response: FetchResponseData; - constructor(app: App, serverConfig: ServerConfig, eTag?: string) { - const config: {[key:string]: string} = {}; + /** + * @param app - The app for this RemoteConfig service. + * @param serverConfig - The server config for which to generate a fetch response. + * @param requestEtag - A request eTag with which to compare the current response. + */ + constructor(app: App, serverConfig: ServerConfig, requestEtag?: string) { + const config: { [key: string]: string } = {}; for (const [param, value] of Object.entries(serverConfig.getAll())) { config[param] = value.asString(); } const currentEtag = this.processEtag(config, app); - if (currentEtag === eTag) { + if (currentEtag === requestEtag) { this.response = { status: 304, - eTag, + eTag: currentEtag, }; } else { this.response = { @@ -647,11 +652,15 @@ export class RemoteConfigFetchResponse { } } - toJSON(): FetchResponseData { + /** + * @returns JSON representation of the fetch response that can be consumed + * by the RC client SDK. + */ + public toJSON(): FetchResponseData { return this.response; } - private processEtag(config: {[key:string]: string}, app: App): string { + private processEtag(config: { [key: string]: string }, app: App): string { const configJson = JSON.stringify(config); let hash = 0; for (let i = 0; i < configJson.length; i++) { From e67c529bcd472d617200fc28220dc39e5f089641 Mon Sep 17 00:00:00 2001 From: kjelko Date: Fri, 3 Jan 2025 12:25:28 -0500 Subject: [PATCH 07/10] add back the newline --- src/remote-config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts index cb3383e3d3..42a2573227 100644 --- a/src/remote-config/index.ts +++ b/src/remote-config/index.ts @@ -96,4 +96,4 @@ export function getRemoteConfig(app?: App): RemoteConfig { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('remoteConfig', (app) => new RemoteConfig(app)); -} \ No newline at end of file +} From 8d66a9148cc191d3931702ab716767e4f7ca8298 Mon Sep 17 00:00:00 2001 From: kjelko Date: Mon, 13 Jan 2025 18:41:37 -0500 Subject: [PATCH 08/10] Apply suggestions from review --- src/remote-config/remote-config.ts | 8 ++++++-- test/unit/remote-config/remote-config.spec.ts | 18 +++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 2addb1f055..a984d67e1f 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -619,6 +619,9 @@ class VersionImpl implements Version { } } +const HTTP_NOT_MODIFIED = 304; +const HTTP_OK = 200; + /** * Represents a fetch response that can be used to interact with RC's client SDK. */ @@ -640,12 +643,12 @@ export class RemoteConfigFetchResponse { if (currentEtag === requestEtag) { this.response = { - status: 304, + status: HTTP_NOT_MODIFIED, eTag: currentEtag, }; } else { this.response = { - status: 200, + status: HTTP_OK, eTag: currentEtag, config, } @@ -663,6 +666,7 @@ export class RemoteConfigFetchResponse { private processEtag(config: { [key: string]: string }, app: App): string { const configJson = JSON.stringify(config); let hash = 0; + // Mimics Java's `String.hashCode()` which is used in RC's servers. for (let i = 0; i < configJson.length; i++) { const char = configJson.charCodeAt(i); hash = (hash << 5) - hash + char; diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index 3270492d5d..976266e5ab 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -1002,14 +1002,14 @@ describe('RemoteConfig', () => { describe('should throw error if there are any JSON or tempalte parsing errors', () => { const INVALID_PARAMETERS: any[] = [null, '', 'abc', 1, true, []]; const INVALID_CONDITIONS: any[] = [null, '', 'abc', 1, true, {}]; - + let sourceTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); const jsonString = '{invalidJson: null}'; it('should throw if template is an invalid JSON', () => { expect(() => remoteConfig.initServerTemplate({ template: jsonString })) .to.throw(/Failed to parse the JSON string: ([\D\w]*)\./); }); - + INVALID_PARAMETERS.forEach((invalidParameter) => { sourceTemplate.parameters = invalidParameter; const jsonString = JSON.stringify(sourceTemplate); @@ -1018,7 +1018,7 @@ describe('RemoteConfig', () => { .to.throw('Remote Config parameters must be a non-null object'); }); }); - + sourceTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); INVALID_CONDITIONS.forEach((invalidConditions) => { sourceTemplate.conditions = invalidConditions; @@ -1339,7 +1339,7 @@ describe('RemoteConfig', () => { expect(value.asString()).to.equal(''); expect(value.getSource()).to.equal('static'); }); - + it('should return default value when it is defined', () => { const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; // Omits remote parameter values. @@ -1357,7 +1357,7 @@ describe('RemoteConfig', () => { expect(value.asString()).to.equal('shiba'); expect(value.getSource()).to.equal('default'); }); - + it('should return remote value when it is defined', () => { const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; // Defines remote parameter values. @@ -1426,7 +1426,7 @@ describe('RemoteConfig', () => { }); describe('RemoteConfigFetchResponse', () => { - it('should return a 200 response with no etag', () => { + it('should return a 200 response when supplied with no etag', () => { const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; // Defines remote parameter values. templateData.parameters = { @@ -1444,8 +1444,8 @@ describe('RemoteConfig', () => { config: { 'dog_type': 'beagle' } }); }); - - it('should return a 200 response with a stale etag', () => { + + it('should return a 200 response when supplied with a stale etag', () => { const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; // Defines remote parameter values. templateData.parameters = { @@ -1463,7 +1463,7 @@ describe('RemoteConfig', () => { config: { 'dog_type': 'beagle' } }); }); - + it('should return a 304 repsonse with matching etag', () => { const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; // Defines remote parameter values. From 20082ef9b5bee81af48977bbdca51c4f31e52990 Mon Sep 17 00:00:00 2001 From: kjelko Date: Tue, 11 Feb 2025 16:39:10 -0500 Subject: [PATCH 09/10] Make eTag optional in fetch response --- src/remote-config/remote-config-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts index fafdd42cf5..d315572192 100644 --- a/src/remote-config/remote-config-api.ts +++ b/src/remote-config/remote-config-api.ts @@ -751,11 +751,11 @@ export interface FetchResponseData { status: number; /** - * Defines the ETag response header value. + * Defines the ETag response header value. Only defined for 200 and 304 responses. * * This is consistent with Remote Config's server eTag implementation. */ - eTag: string; + eTag?: string; /** * Defines the map of parameters returned as "entries" in the fetch response body. From 3d750bf7304d78e16e62607fe419063c642be8b0 Mon Sep 17 00:00:00 2001 From: kjelko Date: Tue, 11 Feb 2025 16:47:25 -0500 Subject: [PATCH 10/10] rerun doc generator --- etc/firebase-admin.remote-config.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md index b821417506..cf7eb32b93 100644 --- a/etc/firebase-admin.remote-config.api.md +++ b/etc/firebase-admin.remote-config.api.md @@ -57,7 +57,7 @@ export interface FetchResponseData { config?: { [key: string]: string; }; - eTag: string; + eTag?: string; status: number; }