diff --git a/.changeset/proud-swans-tie.md b/.changeset/proud-swans-tie.md new file mode 100644 index 00000000000..20b9c10cc7e --- /dev/null +++ b/.changeset/proud-swans-tie.md @@ -0,0 +1,5 @@ +--- +"@firebase/remote-config": minor +--- + +[Remote Config] add `connectRemoteConfigEmulator` to allow the SDK to connect to the Remote Config emulator (#6486) diff --git a/common/api-review/remote-config.api.md b/common/api-review/remote-config.api.md index 980d8f3d287..d5497b37dcb 100644 --- a/common/api-review/remote-config.api.md +++ b/common/api-review/remote-config.api.md @@ -9,6 +9,9 @@ import { FirebaseApp } from '@firebase/app'; // @public export function activate(remoteConfig: RemoteConfig): Promise; +// @public +export function connectRemoteConfigEmulator(remoteConfig: RemoteConfig, url: string): void; + // @public export function ensureInitialized(remoteConfig: RemoteConfig): Promise; diff --git a/packages/remote-config-compat/src/remoteConfig.ts b/packages/remote-config-compat/src/remoteConfig.ts index 7617a7f2d0b..6196148dbf1 100644 --- a/packages/remote-config-compat/src/remoteConfig.ts +++ b/packages/remote-config-compat/src/remoteConfig.ts @@ -35,7 +35,8 @@ import { getNumber, getString, getValue, - isSupported + isSupported, + connectRemoteConfigEmulator } from '@firebase/remote-config'; export { isSupported }; @@ -73,6 +74,10 @@ export class RemoteConfigCompatImpl return activate(this._delegate); } + useEmulator(url: string): void { + connectRemoteConfigEmulator(this._delegate, url); + } + ensureInitialized(): Promise { return ensureInitialized(this._delegate); } diff --git a/packages/remote-config/src/api.ts b/packages/remote-config/src/api.ts index aeae67d450e..60fdede3edc 100644 --- a/packages/remote-config/src/api.ts +++ b/packages/remote-config/src/api.ts @@ -23,7 +23,7 @@ import { } from './public_types'; import { RemoteConfigAbortSignal } from './client/remote_config_fetch_client'; import { RC_COMPONENT_NAME } from './constants'; -import { ErrorCode, hasErrorCode } from './errors'; +import { ErrorCode, ERROR_FACTORY, hasErrorCode } from './errors'; import { RemoteConfig as RemoteConfigImpl } from './remote_config'; import { Value as ValueImpl } from './value'; import { LogLevel as FirebaseLogLevel } from '@firebase/logger'; @@ -73,6 +73,36 @@ export async function activate(remoteConfig: RemoteConfig): Promise { return true; } +/** + * Configures the Remote Config SDK to talk to a local emulator + * instead of product. + * + * Must be called before performing any fetches against production + * Remote Config. + * + * @param remoteConfig - The {@link RemoteConfig} instance. + * @param url - The url of the local emulator + * + * @public + */ +export function connectRemoteConfigEmulator( + remoteConfig: RemoteConfig, + url: string +): void { + const rc = getModularInstance(remoteConfig) as RemoteConfigImpl; + + // To avoid the footgun of fetching from prod first, + // then the emulator, only allow emulator setup + // if no fetches have been made. + if (rc._storageCache.getLastFetchStatus() !== undefined) { + throw ERROR_FACTORY.create(ErrorCode.ALREADY_FETCHED); + } + + window.FIREBASE_REMOTE_CONFIG_URL_BASE = url; + + rc._logger.debug('Connected to the Remote Config emulator.'); +} + /** * Ensures the last activated config are available to the getters. * @param remoteConfig - The {@link RemoteConfig} instance. diff --git a/packages/remote-config/src/errors.ts b/packages/remote-config/src/errors.ts index eac9a25657b..1b19afbfc1b 100644 --- a/packages/remote-config/src/errors.ts +++ b/packages/remote-config/src/errors.ts @@ -31,7 +31,8 @@ export const enum ErrorCode { FETCH_THROTTLE = 'fetch-throttle', FETCH_PARSE = 'fetch-client-parse', FETCH_STATUS = 'fetch-status', - INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable' + INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable', + ALREADY_FETCHED = 'already-fetched' } const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { @@ -67,7 +68,9 @@ const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { [ErrorCode.FETCH_STATUS]: 'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.', [ErrorCode.INDEXED_DB_UNAVAILABLE]: - 'Indexed DB is not supported by current browser' + 'Indexed DB is not supported by current browser', + [ErrorCode.ALREADY_FETCHED]: + 'Cannot connect to emulator after a fetch has been made.' }; // Note this is effectively a type system binding a code to params. This approach overlaps with the diff --git a/packages/remote-config/test/remote_config.test.ts b/packages/remote-config/test/remote_config.test.ts index d275adcad89..90d8db1bb29 100644 --- a/packages/remote-config/test/remote_config.test.ts +++ b/packages/remote-config/test/remote_config.test.ts @@ -518,4 +518,28 @@ describe('RemoteConfig', () => { ); }); }); + + describe('connectRemoteConfigEmulator', () => { + it('changes the remote config API URL', () => { + const emulatorUrl = 'http://localhost:9200'; + + // init storage as if it had never fetched + storageCache.getLastFetchStatus = sinon.stub().returns(undefined); + + api.connectRemoteConfigEmulator(rc, emulatorUrl); + expect(window.FIREBASE_REMOTE_CONFIG_URL_BASE === emulatorUrl).to.be.true; + }); + + it('can not be called if a fetch has already happened', () => { + const emulatorUrl = 'http://localhost:9200'; + + // init storage as if it had already fetched + storageCache.getLastFetchStatus = sinon.stub().returns('success'); + + const expectedError = ERROR_FACTORY.create(ErrorCode.ALREADY_FETCHED); + expect(() => api.connectRemoteConfigEmulator(rc, emulatorUrl)).to.throw( + expectedError.message + ); + }); + }); });