Skip to content

Commit 7618f2a

Browse files
authored
Merge 8f92245 into e25317f
2 parents e25317f + 8f92245 commit 7618f2a

15 files changed

+874
-8
lines changed

common/api-review/remote-config.api.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,23 @@
55
```ts
66

77
import { FirebaseApp } from '@firebase/app';
8+
import { FirebaseError } from '@firebase/app';
89

910
// @public
1011
export function activate(remoteConfig: RemoteConfig): Promise<boolean>;
1112

13+
// @public
14+
export interface ConfigUpdate {
15+
getUpdatedKeys(): Set<string>;
16+
}
17+
18+
// @public
19+
export interface ConfigUpdateObserver {
20+
complete: () => void;
21+
error: (error: FirebaseError) => void;
22+
next: (configUpdate: ConfigUpdate) => void;
23+
}
24+
1225
// @public
1326
export interface CustomSignals {
1427
// (undocumented)
@@ -64,6 +77,9 @@ export function isSupported(): Promise<boolean>;
6477
// @public
6578
export type LogLevel = 'debug' | 'error' | 'silent';
6679

80+
// @public
81+
export function onConfigUpdate(remoteConfig: RemoteConfig, observer: ConfigUpdateObserver): Promise<Unsubscribe>;
82+
6783
// @public
6884
export interface RemoteConfig {
6985
app: FirebaseApp;
@@ -93,6 +109,9 @@ export function setCustomSignals(remoteConfig: RemoteConfig, customSignals: Cust
93109
// @public
94110
export function setLogLevel(remoteConfig: RemoteConfig, logLevel: LogLevel): void;
95111

112+
// @public
113+
export type Unsubscribe = () => void;
114+
96115
// @public
97116
export interface Value {
98117
asBoolean(): boolean;

docs-devsite/_toc.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,10 @@ toc:
607607
- title: remote-config
608608
path: /docs/reference/js/remote-config.md
609609
section:
610+
- title: ConfigUpdate
611+
path: /docs/reference/js/remote-config.configupdate.md
612+
- title: ConfigUpdateObserver
613+
path: /docs/reference/js/remote-config.configupdateobserver.md
610614
- title: CustomSignals
611615
path: /docs/reference/js/remote-config.customsignals.md
612616
- title: FetchResponse
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
Project: /docs/reference/js/_project.yaml
2+
Book: /docs/reference/_book.yaml
3+
page_type: reference
4+
5+
{% comment %}
6+
DO NOT EDIT THIS FILE!
7+
This is generated by the JS SDK team, and any local changes will be
8+
overwritten. Changes should be made in the source code at
9+
https://github.com/firebase/firebase-js-sdk
10+
{% endcomment %}
11+
12+
# ConfigUpdate interface
13+
Contains information about which keys have been updated.
14+
15+
<b>Signature:</b>
16+
17+
```typescript
18+
export interface ConfigUpdate
19+
```
20+
21+
## Methods
22+
23+
| Method | Description |
24+
| --- | --- |
25+
| [getUpdatedKeys()](./remote-config.configupdate.md#configupdategetupdatedkeys) | Parameter keys whose values have been updated from the currently activated values. Includes keys that are added, deleted, or whose value, value source, or metadata has changed. |
26+
27+
## ConfigUpdate.getUpdatedKeys()
28+
29+
Parameter keys whose values have been updated from the currently activated values. Includes keys that are added, deleted, or whose value, value source, or metadata has changed.
30+
31+
<b>Signature:</b>
32+
33+
```typescript
34+
getUpdatedKeys(): Set<string>;
35+
```
36+
<b>Returns:</b>
37+
38+
Set&lt;string&gt;
39+
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
Project: /docs/reference/js/_project.yaml
2+
Book: /docs/reference/_book.yaml
3+
page_type: reference
4+
5+
{% comment %}
6+
DO NOT EDIT THIS FILE!
7+
This is generated by the JS SDK team, and any local changes will be
8+
overwritten. Changes should be made in the source code at
9+
https://github.com/firebase/firebase-js-sdk
10+
{% endcomment %}
11+
12+
# ConfigUpdateObserver interface
13+
Observer interface for receiving real-time Remote Config update notifications.
14+
15+
NOTE: Although an `complete` callback can be provided, it will never be called because the ConfigUpdate stream is never-ending.
16+
17+
<b>Signature:</b>
18+
19+
```typescript
20+
export interface ConfigUpdateObserver
21+
```
22+
23+
## Properties
24+
25+
| Property | Type | Description |
26+
| --- | --- | --- |
27+
| [complete](./remote-config.configupdateobserver.md#configupdateobservercomplete) | () =&gt; void | Called when the stream is gracefully terminated. |
28+
| [error](./remote-config.configupdateobserver.md#configupdateobservererror) | (error: [FirebaseError](./util.firebaseerror.md#firebaseerror_class)<!-- -->) =&gt; void | Called if an error occurs during the stream. |
29+
| [next](./remote-config.configupdateobserver.md#configupdateobservernext) | (configUpdate: [ConfigUpdate](./remote-config.configupdate.md#configupdate_interface)<!-- -->) =&gt; void | Called when a new ConfigUpdate is available. |
30+
31+
## ConfigUpdateObserver.complete
32+
33+
Called when the stream is gracefully terminated.
34+
35+
<b>Signature:</b>
36+
37+
```typescript
38+
complete: () => void;
39+
```
40+
41+
## ConfigUpdateObserver.error
42+
43+
Called if an error occurs during the stream.
44+
45+
<b>Signature:</b>
46+
47+
```typescript
48+
error: (error: FirebaseError) => void;
49+
```
50+
51+
## ConfigUpdateObserver.next
52+
53+
Called when a new ConfigUpdate is available.
54+
55+
<b>Signature:</b>
56+
57+
```typescript
58+
next: (configUpdate: ConfigUpdate) => void;
59+
```

docs-devsite/remote-config.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm
2828
| [getNumber(remoteConfig, key)](./remote-config.md#getnumber_476c09f) | Gets the value for the given key as a number.<!-- -->Convenience method for calling <code>remoteConfig.getValue(key).asNumber()</code>. |
2929
| [getString(remoteConfig, key)](./remote-config.md#getstring_476c09f) | Gets the value for the given key as a string. Convenience method for calling <code>remoteConfig.getValue(key).asString()</code>. |
3030
| [getValue(remoteConfig, key)](./remote-config.md#getvalue_476c09f) | Gets the [Value](./remote-config.value.md#value_interface) for the given key. |
31+
| [onConfigUpdate(remoteConfig, observer)](./remote-config.md#onconfigupdate_8b13b26) | Starts listening for real-time config updates from the Remote Config backend and automatically fetches updates from the RC backend when they are available.<p>If a connection to the Remote Config backend is not already open, calling this method will open it. Multiple listeners can be added by calling this method again, but subsequent calls re-use the same connection to the backend. |
3132
| [setCustomSignals(remoteConfig, customSignals)](./remote-config.md#setcustomsignals_aeeb95e) | Sets the custom signals for the app instance. |
3233
| [setLogLevel(remoteConfig, logLevel)](./remote-config.md#setloglevel_039a45b) | Defines the log level to use. |
3334
| <b>function()</b> |
@@ -37,6 +38,8 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm
3738

3839
| Interface | Description |
3940
| --- | --- |
41+
| [ConfigUpdate](./remote-config.configupdate.md#configupdate_interface) | Contains information about which keys have been updated. |
42+
| [ConfigUpdateObserver](./remote-config.configupdateobserver.md#configupdateobserver_interface) | Observer interface for receiving real-time Remote Config update notifications.<!-- -->NOTE: Although an <code>complete</code> callback can be provided, it will never be called because the ConfigUpdate stream is never-ending. |
4043
| [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> |
4144
| [FetchResponse](./remote-config.fetchresponse.md#fetchresponse_interface) | Defines a successful response (200 or 304).<p>Modeled after the native <code>Response</code> interface, but simplified for Remote Config's use case. |
4245
| [FirebaseRemoteConfigObject](./remote-config.firebaseremoteconfigobject.md#firebaseremoteconfigobject_interface) | Defines a self-descriptive reference for config key-value pairs. |
@@ -51,6 +54,7 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm
5154
| --- | --- |
5255
| [FetchStatus](./remote-config.md#fetchstatus) | Summarizes the outcome of the last attempt to fetch config from the Firebase Remote Config server.<ul> <li>"no-fetch-yet" indicates the [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) instance has not yet attempted to fetch config, or that SDK initialization is incomplete.</li> <li>"success" indicates the last attempt succeeded.</li> <li>"failure" indicates the last attempt failed.</li> <li>"throttle" indicates the last attempt was rate-limited.</li> </ul> |
5356
| [LogLevel](./remote-config.md#loglevel) | Defines levels of Remote Config logging. |
57+
| [Unsubscribe](./remote-config.md#unsubscribe) | A function that unsubscribes from a real-time event stream. |
5458
| [ValueSource](./remote-config.md#valuesource) | Indicates the source of a value.<ul> <li>"static" indicates the value was defined by a static constant.</li> <li>"default" indicates the value was defined by default config.</li> <li>"remote" indicates the value was defined by fetched config.</li> </ul> |
5559

5660
## function(app, ...)
@@ -282,6 +286,31 @@ export declare function getValue(remoteConfig: RemoteConfig, key: string): Value
282286

283287
The value for the given key.
284288

289+
### onConfigUpdate(remoteConfig, observer) {:#onconfigupdate_8b13b26}
290+
291+
Starts listening for real-time config updates from the Remote Config backend and automatically fetches updates from the RC backend when they are available.
292+
293+
<p>If a connection to the Remote Config backend is not already open, calling this method will open it. Multiple listeners can be added by calling this method again, but subsequent calls re-use the same connection to the backend.
294+
295+
<b>Signature:</b>
296+
297+
```typescript
298+
export declare function onConfigUpdate(remoteConfig: RemoteConfig, observer: ConfigUpdateObserver): Promise<Unsubscribe>;
299+
```
300+
301+
#### Parameters
302+
303+
| Parameter | Type | Description |
304+
| --- | --- | --- |
305+
| remoteConfig | [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) | The [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) instance. |
306+
| observer | [ConfigUpdateObserver](./remote-config.configupdateobserver.md#configupdateobserver_interface) | The [ConfigUpdateObserver](./remote-config.configupdateobserver.md#configupdateobserver_interface) to be notified of config updates. |
307+
308+
<b>Returns:</b>
309+
310+
Promise&lt;[Unsubscribe](./remote-config.md#unsubscribe)<!-- -->&gt;
311+
312+
An [Unsubscribe](./remote-config.md#unsubscribe) function to remove the listener.
313+
285314
### setCustomSignals(remoteConfig, customSignals) {:#setcustomsignals_aeeb95e}
286315

287316
Sets the custom signals for the app instance.
@@ -365,6 +394,16 @@ Defines levels of Remote Config logging.
365394
export type LogLevel = 'debug' | 'error' | 'silent';
366395
```
367396

397+
## Unsubscribe
398+
399+
A function that unsubscribes from a real-time event stream.
400+
401+
<b>Signature:</b>
402+
403+
```typescript
404+
export type Unsubscribe = () => void;
405+
```
406+
368407
## ValueSource
369408

370409
Indicates the source of a value.

packages/remote-config/src/api.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import {
2222
LogLevel as RemoteConfigLogLevel,
2323
RemoteConfig,
2424
Value,
25-
RemoteConfigOptions
25+
RemoteConfigOptions,
26+
ConfigUpdateObserver,
27+
Unsubscribe
2628
} from './public_types';
2729
import { RemoteConfigAbortSignal } from './client/remote_config_fetch_client';
2830
import {
@@ -351,3 +353,29 @@ export async function setCustomSignals(
351353
);
352354
}
353355
}
356+
357+
// TODO: Add public document for the Remote Config Realtime API guide on the Web Platform.
358+
/**
359+
* Starts listening for real-time config updates from the Remote Config backend and automatically
360+
* fetches updates from the RC backend when they are available.
361+
*
362+
* <p>If a connection to the Remote Config backend is not already open, calling this method will
363+
* open it. Multiple listeners can be added by calling this method again, but subsequent calls
364+
* re-use the same connection to the backend.
365+
*
366+
* @param remoteConfig - The {@link RemoteConfig} instance.
367+
* @param observer - The {@link ConfigUpdateObserver} to be notified of config updates.
368+
* @returns An {@link Unsubscribe} function to remove the listener.
369+
*
370+
* @public
371+
*/
372+
export async function onConfigUpdate(
373+
remoteConfig: RemoteConfig,
374+
observer: ConfigUpdateObserver
375+
): Promise<Unsubscribe> {
376+
const rc = getModularInstance(remoteConfig) as RemoteConfigImpl;
377+
await rc._realtimeHandler.addObserver(observer);
378+
return () => {
379+
rc._realtimeHandler.removeObserver(observer);
380+
};
381+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { assert } from '@firebase/util';
19+
20+
// TODO: Consolidate the Visibility monitoring API code into a shared utility function in firebase/util to be used by both packages/database and packages/remote-config.
21+
/**
22+
* Base class to be used if you want to emit events. Call the constructor with
23+
* the set of allowed event names.
24+
*/
25+
export abstract class EventEmitter {
26+
private listeners_: {
27+
[eventType: string]: Array<{
28+
callback(...args: unknown[]): void;
29+
context: unknown;
30+
}>;
31+
} = {};
32+
33+
constructor(private allowedEvents_: string[]) {
34+
assert(
35+
Array.isArray(allowedEvents_) && allowedEvents_.length > 0,
36+
'Requires a non-empty array'
37+
);
38+
}
39+
40+
/**
41+
* To be overridden by derived classes in order to fire an initial event when
42+
* somebody subscribes for data.
43+
*
44+
* @returns {Array.<*>} Array of parameters to trigger initial event with.
45+
*/
46+
abstract getInitialEvent(eventType: string): unknown[];
47+
48+
/**
49+
* To be called by derived classes to trigger events.
50+
*/
51+
protected trigger(eventType: string, ...varArgs: unknown[]): void {
52+
if (Array.isArray(this.listeners_[eventType])) {
53+
// Clone the list, since callbacks could add/remove listeners.
54+
const listeners = [...this.listeners_[eventType]];
55+
56+
for (let i = 0; i < listeners.length; i++) {
57+
listeners[i].callback.apply(listeners[i].context, varArgs);
58+
}
59+
}
60+
}
61+
62+
on(
63+
eventType: string,
64+
callback: (a: unknown) => void,
65+
context: unknown
66+
): void {
67+
this.validateEventType_(eventType);
68+
this.listeners_[eventType] = this.listeners_[eventType] || [];
69+
this.listeners_[eventType].push({ callback, context });
70+
71+
const eventData = this.getInitialEvent(eventType);
72+
if (eventData) {
73+
//@ts-ignore
74+
callback.apply(context, eventData);
75+
}
76+
}
77+
78+
off(
79+
eventType: string,
80+
callback: (a: unknown) => void,
81+
context: unknown
82+
): void {
83+
this.validateEventType_(eventType);
84+
const listeners = this.listeners_[eventType] || [];
85+
for (let i = 0; i < listeners.length; i++) {
86+
if (
87+
listeners[i].callback === callback &&
88+
(!context || context === listeners[i].context)
89+
) {
90+
listeners.splice(i, 1);
91+
return;
92+
}
93+
}
94+
}
95+
96+
private validateEventType_(eventType: string): void {
97+
assert(
98+
this.allowedEvents_.find(et => {
99+
return et === eventType;
100+
}),
101+
'Unknown event: ' + eventType
102+
);
103+
}
104+
}

0 commit comments

Comments
 (0)