Skip to content

[Remote Config] add connectRemoteConfigEmulator to Remote Config #6486

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/proud-swans-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@firebase/remote-config": minor
---

[Remote Config] add `connectRemoteConfigEmulator` to allow the SDK to connect to the Remote Config emulator (#6486)
3 changes: 3 additions & 0 deletions common/api-review/remote-config.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { FirebaseApp } from '@firebase/app';
// @public
export function activate(remoteConfig: RemoteConfig): Promise<boolean>;

// @public
export function connectRemoteConfigEmulator(remoteConfig: RemoteConfig, url: string): void;

// @public
export function ensureInitialized(remoteConfig: RemoteConfig): Promise<void>;

Expand Down
7 changes: 6 additions & 1 deletion packages/remote-config-compat/src/remoteConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import {
getNumber,
getString,
getValue,
isSupported
isSupported,
connectRemoteConfigEmulator
} from '@firebase/remote-config';

export { isSupported };
Expand Down Expand Up @@ -73,6 +74,10 @@ export class RemoteConfigCompatImpl
return activate(this._delegate);
}

useEmulator(url: string): void {
connectRemoteConfigEmulator(this._delegate, url);
}

ensureInitialized(): Promise<void> {
return ensureInitialized(this._delegate);
}
Expand Down
32 changes: 31 additions & 1 deletion packages/remote-config/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -73,6 +73,36 @@ export async function activate(remoteConfig: RemoteConfig): Promise<boolean> {
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.
Expand Down
7 changes: 5 additions & 2 deletions packages/remote-config/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions packages/remote-config/test/remote_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
});
});
});