Skip to content
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

SSR related improvements for RC client SDK. #8699

Merged
merged 19 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from 11 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
6 changes: 6 additions & 0 deletions .changeset/flat-plums-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@firebase/remote-config': minor
'firebase': minor
---

Adds support for initial state hydration (from SSR contexts)
21 changes: 20 additions & 1 deletion common/api-review/remote-config.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,22 @@ export function fetchAndActivate(remoteConfig: RemoteConfig): Promise<boolean>;
// @public
export function fetchConfig(remoteConfig: RemoteConfig): Promise<void>;

// @public
export interface FetchResponse {
config?: FirebaseRemoteConfigObject;
eTag?: string;
status: number;
}

// @public
export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle';

// @public
export interface FirebaseRemoteConfigObject {
// (undocumented)
[key: string]: string;
}

// @public
export function getAll(remoteConfig: RemoteConfig): Record<string, Value>;

Expand All @@ -37,7 +50,7 @@ export function getBoolean(remoteConfig: RemoteConfig, key: string): boolean;
export function getNumber(remoteConfig: RemoteConfig, key: string): number;

// @public (undocumented)
export function getRemoteConfig(app?: FirebaseApp): RemoteConfig;
export function getRemoteConfig(app?: FirebaseApp, options?: RemoteConfigOptions): RemoteConfig;

// @public
export function getString(remoteConfig: RemoteConfig, key: string): string;
Expand All @@ -62,6 +75,12 @@ export interface RemoteConfig {
settings: RemoteConfigSettings;
}

// @public
export interface RemoteConfigOptions {
initialFetchResponse?: FetchResponse;
templateId?: string;
}

// @public
export interface RemoteConfigSettings {
fetchTimeoutMillis: number;
Expand Down
6 changes: 6 additions & 0 deletions docs-devsite/_toc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,14 @@ toc:
section:
- title: CustomSignals
path: /docs/reference/js/remote-config.customsignals.md
- title: FetchResponse
path: /docs/reference/js/remote-config.fetchresponse.md
- title: FirebaseRemoteConfigObject
path: /docs/reference/js/remote-config.firebaseremoteconfigobject.md
- title: RemoteConfig
path: /docs/reference/js/remote-config.remoteconfig.md
- title: RemoteConfigOptions
path: /docs/reference/js/remote-config.remoteconfigoptions.md
- title: RemoteConfigSettings
path: /docs/reference/js/remote-config.remoteconfigsettings.md
- title: Value
Expand Down
67 changes: 67 additions & 0 deletions docs-devsite/remote-config.fetchresponse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# FetchResponse interface
Defines a successful response (200 or 304).

<p>Modeled after the native interface, but simplified for Remote Config's use case.

<b>Signature:</b>

```typescript
export interface FetchResponse
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [config](./remote-config.fetchresponse.md#fetchresponseconfig) | [FirebaseRemoteConfigObject](./remote-config.firebaseremoteconfigobject.md#firebaseremoteconfigobject_interface) | Defines the map of parameters returned as "entries" in the fetch response body.<p>Only defined for 200 responses. |
| [eTag](./remote-config.fetchresponse.md#fetchresponseetag) | string | Defines the ETag response header value.<p>Only defined for 200 and 304 responses. |
| [status](./remote-config.fetchresponse.md#fetchresponsestatus) | number | The HTTP status, which is useful for differentiating success responses with data from those without.<p> is modeled after the native interface, so HTTP status is first-class.<p>Disambiguation: the fetch response returns a legacy "state" value that is redundant with the HTTP status code. The former is normalized into the latter. |

## FetchResponse.config

Defines the map of parameters returned as "entries" in the fetch response body.

<p>Only defined for 200 responses.

<b>Signature:</b>

```typescript
config?: FirebaseRemoteConfigObject;
```

## FetchResponse.eTag

Defines the ETag response header value.

<p>Only defined for 200 and 304 responses.

<b>Signature:</b>

```typescript
eTag?: string;
```

## FetchResponse.status

The HTTP status, which is useful for differentiating success responses with data from those without.

<p> is modeled after the native interface, so HTTP status is first-class.

<p>Disambiguation: the fetch response returns a legacy "state" value that is redundant with the HTTP status code. The former is normalized into the latter.

<b>Signature:</b>

```typescript
status: number;
```
19 changes: 19 additions & 0 deletions docs-devsite/remote-config.firebaseremoteconfigobject.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# FirebaseRemoteConfigObject interface
Defines a self-descriptive reference for config key-value pairs.

<b>Signature:</b>

```typescript
export interface FirebaseRemoteConfigObject
```
10 changes: 7 additions & 3 deletions docs-devsite/remote-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm
| Function | Description |
| --- | --- |
| <b>function(app, ...)</b> |
| [getRemoteConfig(app)](./remote-config.md#getremoteconfig_cf608e1) | |
| [getRemoteConfig(app, options)](./remote-config.md#getremoteconfig_61d368f) | |
| <b>function(remoteConfig, ...)</b> |
| [activate(remoteConfig)](./remote-config.md#activate_722a192) | Makes the last fetched config available to the getters. |
| [ensureInitialized(remoteConfig)](./remote-config.md#ensureinitialized_722a192) | Ensures the last activated config are available to the getters. |
Expand All @@ -38,7 +38,10 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm
| Interface | Description |
| --- | --- |
| [CustomSignals](./remote-config.customsignals.md#customsignals_interface) | Defines the type for representing custom signals and their values.<p>The values in CustomSignals must be one of the following types:<ul> <li><code>string</code> <li><code>number</code> <li><code>null</code> </ul> |
| [FetchResponse](./remote-config.fetchresponse.md#fetchresponse_interface) | Defines a successful response (200 or 304).<p>Modeled after the native interface, but simplified for Remote Config's use case. |
| [FirebaseRemoteConfigObject](./remote-config.firebaseremoteconfigobject.md#firebaseremoteconfigobject_interface) | Defines a self-descriptive reference for config key-value pairs. |
| [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) | The Firebase Remote Config service interface. |
| [RemoteConfigOptions](./remote-config.remoteconfigoptions.md#remoteconfigoptions_interface) | Options for Remote Config initialization. |
| [RemoteConfigSettings](./remote-config.remoteconfigsettings.md#remoteconfigsettings_interface) | Defines configuration options for the Remote Config SDK. |
| [Value](./remote-config.value.md#value_interface) | Wraps a value with metadata and type-safe getters. |

Expand All @@ -52,19 +55,20 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm

## function(app, ...)

### getRemoteConfig(app) {:#getremoteconfig_cf608e1}
### getRemoteConfig(app, options) {:#getremoteconfig_61d368f}

<b>Signature:</b>

```typescript
export declare function getRemoteConfig(app?: FirebaseApp): RemoteConfig;
export declare function getRemoteConfig(app?: FirebaseApp, options?: RemoteConfigOptions): RemoteConfig;
```

#### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| app | [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) | The [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) instance. |
| options | [RemoteConfigOptions](./remote-config.remoteconfigoptions.md#remoteconfigoptions_interface) | |

<b>Returns:</b>

Expand Down
46 changes: 46 additions & 0 deletions docs-devsite/remote-config.remoteconfigoptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# RemoteConfigOptions interface
Options for Remote Config initialization.

<b>Signature:</b>

```typescript
export interface RemoteConfigOptions
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [initialFetchResponse](./remote-config.remoteconfigoptions.md#remoteconfigoptionsinitialfetchresponse) | [FetchResponse](./remote-config.fetchresponse.md#fetchresponse_interface) | Hydrates the state with an initial fetch response. |
| [templateId](./remote-config.remoteconfigoptions.md#remoteconfigoptionstemplateid) | string | The ID of the template to use. If not provided, defaults to "firebase". |

## RemoteConfigOptions.initialFetchResponse

Hydrates the state with an initial fetch response.

<b>Signature:</b>

```typescript
initialFetchResponse?: FetchResponse;
```

## RemoteConfigOptions.templateId

The ID of the template to use. If not provided, defaults to "firebase".

<b>Signature:</b>

```typescript
templateId?: string;
```
41 changes: 36 additions & 5 deletions packages/remote-config/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,24 @@
*/

import { _getProvider, FirebaseApp, getApp } from '@firebase/app';
import { deepEqual, getModularInstance } from '@firebase/util';
import {
CustomSignals,
LogLevel as RemoteConfigLogLevel,
RemoteConfig,
Value
Value,
RemoteConfigOptions
} from './public_types';
import { RemoteConfigAbortSignal } from './client/remote_config_fetch_client';
import {
RC_COMPONENT_NAME,
RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH,
RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH
} from './constants';
import { ErrorCode, hasErrorCode } from './errors';
import { ERROR_FACTORY, ErrorCode, hasErrorCode } from './errors';
import { RemoteConfig as RemoteConfigImpl } from './remote_config';
import { Value as ValueImpl } from './value';
import { LogLevel as FirebaseLogLevel } from '@firebase/logger';
import { getModularInstance } from '@firebase/util';

/**
*
Expand All @@ -41,10 +42,40 @@ import { getModularInstance } from '@firebase/util';
*
* @public
*/
export function getRemoteConfig(app: FirebaseApp = getApp()): RemoteConfig {
export function getRemoteConfig(
app: FirebaseApp = getApp(),
options: RemoteConfigOptions = {}
): RemoteConfig {
app = getModularInstance(app);
const rcProvider = _getProvider(app, RC_COMPONENT_NAME);
return rcProvider.getImmediate();
if (rcProvider.isInitialized()) {
const initialOptions = rcProvider.getOptions() as RemoteConfigOptions;
if (deepEqual(initialOptions, options)) {
return rcProvider.getImmediate();
}
throw ERROR_FACTORY.create(ErrorCode.ALREADY_INITIALIZED);
}
rcProvider.initialize({ options });
const rc = rcProvider.getImmediate() as RemoteConfigImpl;

if (options.initialFetchResponse) {
// We use these initial writes as the initialization promise since they will hydrate the same
// fields that storageCache.loadFromStorage would set.
rc._initializePromise = Promise.all([
rc._storage.setLastSuccessfulFetchResponse(options.initialFetchResponse),
rc._storage.setActiveConfigEtag(options.initialFetchResponse?.eTag || ''),
rc._storageCache.setLastSuccessfulFetchTimestampMillis(Date.now()),
rc._storageCache.setLastFetchStatus('success'),
rc._storageCache.setActiveConfig(
options.initialFetchResponse?.config || {}
)
]).then();
// The storageCache methods above set their in-memory fields sycnhronously, so it's
// safe to declare our initialization complete at this point.
rc._isInitializationComplete = true;
}

return rc;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/remote-config/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { ErrorFactory, FirebaseError } from '@firebase/util';

export const enum ErrorCode {
ALREADY_INITIALIZED = 'already-initialized',
REGISTRATION_WINDOW = 'registration-window',
REGISTRATION_PROJECT_ID = 'registration-project-id',
REGISTRATION_API_KEY = 'registration-api-key',
Expand All @@ -36,6 +37,7 @@ export const enum ErrorCode {
}

const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = {
[ErrorCode.ALREADY_INITIALIZED]: 'Remote Config already initialized',
[ErrorCode.REGISTRATION_WINDOW]:
'Undefined window object. This SDK only supports usage in a browser environment.',
[ErrorCode.REGISTRATION_PROJECT_ID]:
Expand Down
23 changes: 23 additions & 0 deletions packages/remote-config/src/public_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@
*/

import { FirebaseApp } from '@firebase/app';
import { FetchResponse } from './client/remote_config_fetch_client';

export {
FetchResponse,
FirebaseRemoteConfigObject
} from './client/remote_config_fetch_client';

/**
* Options for Remote Config initialization.
*
* @public
*/
export interface RemoteConfigOptions {
/**
* The ID of the template to use. If not provided, defaults to "firebase".
*/
templateId?: string;

/**
* Hydrates the state with an initial fetch response.
*/
initialFetchResponse?: FetchResponse;
}

/**
* The Firebase Remote Config service interface.
Expand Down
Loading
Loading