@hot-updater/plugin-remote-config 0.0.0-stage → 1.0.0-rc.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,33 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Sungyu Kang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ ADDITIONAL DISCLAIMER
26
+
27
+ The authors of this software shall not be held responsible for any legal issues,
28
+ regulatory violations, policy violations, or disputes arising from the use,
29
+ distribution, or integration of this software.
30
+
31
+ Users are solely responsible for ensuring compliance with all applicable laws,
32
+ regulations, and third-party policies (including but not limited to app store
33
+ policies).
package/README.md CHANGED
@@ -1,4 +1,24 @@
1
- # Temporary Holding Version
1
+ # @hot-updater/plugin-remote-config
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
4
- If no other versions are published within 30 days, this package and version will be deleted.
3
+ Hot Updater's Remote Config plugin. The server plugin stores versioned
4
+ templates of parameters and conditions, and answers each device with the
5
+ values its conditions pick; the client plugin fetches those values in the app
6
+ and reads them synchronously, over in-app defaults.
7
+
8
+ You don't install it on its own. `@hot-updater/server` and
9
+ `@hot-updater/react-native` depend on it and re-export it:
10
+
11
+ ```ts
12
+ // The server definition
13
+ import { remoteConfig } from "@hot-updater/server/plugins/remote-config";
14
+
15
+ // The app
16
+ import { HotUpdater, remoteConfig } from "@hot-updater/react-native";
17
+ ```
18
+
19
+ | Entry | Re-exported as |
20
+ | ---------- | ----------------------------------------------------------------------- |
21
+ | `./server` | `@hot-updater/server/plugins/remote-config` |
22
+ | `./client` | `remoteConfig` and its types from the `@hot-updater/react-native` root |
23
+
24
+ See [Remote Config](https://hot-updater.dev/docs/guides/remote-config).
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@hot-updater/plugin-remote-config/client",
3
+ "private": true,
4
+ "main": "../dist/client/index.cjs",
5
+ "module": "../dist/client/index.mjs",
6
+ "types": "../dist/client/index.d.cts"
7
+ }
@@ -0,0 +1,208 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_ownEntry = require("../shared/ownEntry.cjs");
3
+ const require_wire = require("../shared/wire.cjs");
4
+ let _hot_updater_protocol = require("@hot-updater/protocol");
5
+ //#region src/client/index.ts
6
+ const DEFAULT_MINIMUM_FETCH_INTERVAL_MS = 432e5;
7
+ const BOOLEAN_TRUTHY_VALUES = /* @__PURE__ */ new Set([
8
+ "1",
9
+ "true",
10
+ "t",
11
+ "yes",
12
+ "y",
13
+ "on"
14
+ ]);
15
+ const FETCHED_KEY = "fetched";
16
+ const ACTIVE_KEY = "active";
17
+ const LAST_FETCH_STATUS_KEY = "lastFetchStatus";
18
+ const createValue = (text, source) => Object.freeze({
19
+ asString: () => text,
20
+ asNumber: () => {
21
+ const number = Number(text);
22
+ return Number.isNaN(number) ? 0 : number;
23
+ },
24
+ asBoolean: () => BOOLEAN_TRUTHY_VALUES.has(text.toLowerCase()),
25
+ getSource: () => source
26
+ });
27
+ const parseStored = (text, extra) => {
28
+ if (text === null) return null;
29
+ try {
30
+ const value = JSON.parse(text);
31
+ if (typeof value !== "object" || value === null) return null;
32
+ const record = value;
33
+ if (require_wire.parseRemoteConfigFetchResponse(record) === null || record.etag !== null && typeof record.etag !== "string" || !extra(record)) return null;
34
+ return record;
35
+ } catch {
36
+ return null;
37
+ }
38
+ };
39
+ const isFetchStatus = (value) => value === "success" || value === "failure";
40
+ const deviceContext = (context) => ({
41
+ platform: context.platform,
42
+ appVersion: context.appVersion,
43
+ channel: context.getChannel(),
44
+ cohort: context.getCohort(),
45
+ fingerprintHash: context.getFingerprintHash()
46
+ });
47
+ /**
48
+ * Remote Config for the app: in-app defaults, values a server running the
49
+ * `remoteConfig()` plugin picks for this device, and a fetch and activate
50
+ * step between them. Add it to `HotUpdater.init`'s `plugins`, and read it
51
+ * from the instance `init` returns, as `hotUpdater.remoteConfig`. It fetches
52
+ * from the `baseURL`, request headers, and timeout configured there.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { HotUpdater, remoteConfig } from "@hot-updater/react-native";
57
+ *
58
+ * export const hotUpdater = HotUpdater.init({
59
+ * baseURL,
60
+ * plugins: [
61
+ * remoteConfig({ defaults: { welcome_message: "Welcome", max_items: 20 } }),
62
+ * ],
63
+ * });
64
+ *
65
+ * hotUpdater.remoteConfig.fetchAndActivate().catch(() => {});
66
+ * hotUpdater.remoteConfig.getString("welcome_message");
67
+ * ```
68
+ */
69
+ const remoteConfig = (options = {}) => {
70
+ const minimumFetchIntervalMs = options.minimumFetchIntervalMs ?? DEFAULT_MINIMUM_FETCH_INTERVAL_MS;
71
+ if (typeof minimumFetchIntervalMs !== "number" || !Number.isFinite(minimumFetchIntervalMs) || minimumFetchIntervalMs < 0) throw new TypeError("[HotUpdater] remoteConfig({ minimumFetchIntervalMs }) must be a non-negative number.");
72
+ const defaults = Object.fromEntries(Object.entries(options.defaults ?? {}).map(([key, value]) => [key, String(value)]));
73
+ return (0, _hot_updater_protocol.defineClientPlugin)({
74
+ id: "remoteConfig",
75
+ setup: (context) => ({ api: createRemoteConfigClient(context, {
76
+ defaults,
77
+ minimumFetchIntervalMs
78
+ }) })
79
+ });
80
+ };
81
+ /** The client a plugin's `setup` creates, over the values stored on the device. */
82
+ const createRemoteConfigClient = (context, { defaults, minimumFetchIntervalMs }) => {
83
+ let active = parseStored(context.storage.get(ACTIVE_KEY), () => true);
84
+ let fetched = parseStored(context.storage.get(FETCHED_KEY), (record) => typeof record.context === "string" && typeof record.fetchedAtMs === "number");
85
+ const storedStatus = context.storage.get(LAST_FETCH_STATUS_KEY);
86
+ let lastFetchStatus = isFetchStatus(storedStatus) ? storedStatus : fetched === null ? "no-fetch-yet" : "success";
87
+ let inFlight = null;
88
+ let snapshot = null;
89
+ const listeners = /* @__PURE__ */ new Set();
90
+ const notify = () => {
91
+ snapshot = null;
92
+ for (const listener of listeners) try {
93
+ listener();
94
+ } catch (error) {
95
+ console.warn("[HotUpdater] A remoteConfig listener threw.", error);
96
+ }
97
+ };
98
+ const store = (key, value) => {
99
+ try {
100
+ context.storage.set(key, value);
101
+ } catch (error) {
102
+ console.warn(`[HotUpdater] remoteConfig could not store its ${key} values.`, error);
103
+ }
104
+ };
105
+ const setStatus = (status) => {
106
+ lastFetchStatus = status;
107
+ store(LAST_FETCH_STATUS_KEY, status);
108
+ };
109
+ const getValue = (key) => {
110
+ const remote = require_ownEntry.ownEntry(active?.values, key);
111
+ if (remote !== void 0) return createValue(remote, "remote");
112
+ const fallback = require_ownEntry.ownEntry(defaults, key);
113
+ return fallback === void 0 ? null : createValue(fallback, "default");
114
+ };
115
+ const fetchFromServer = async (device) => {
116
+ const contextKey = JSON.stringify(device);
117
+ let response;
118
+ try {
119
+ response = await context.fetch(require_wire.remoteConfigRequestPath(device), { headers: fetched === null || fetched.etag === null ? {} : { "if-none-match": fetched.etag } });
120
+ } catch (error) {
121
+ setStatus("failure");
122
+ throw new Error("[HotUpdater] Remote Config could not be fetched.", { cause: error });
123
+ }
124
+ const fetchedAtMs = context.now();
125
+ if (response.status === 304 && fetched !== null) {
126
+ fetched = {
127
+ ...fetched,
128
+ context: contextKey,
129
+ fetchedAtMs
130
+ };
131
+ store(FETCHED_KEY, JSON.stringify(fetched));
132
+ setStatus("success");
133
+ return;
134
+ }
135
+ if (!response.ok) {
136
+ setStatus("failure");
137
+ throw new Error(response.status === 404 ? "[HotUpdater] The server answered 404 to GET /remote-config: it runs without the remoteConfig() plugin." : `[HotUpdater] Remote Config fetch failed with HTTP ${response.status}.`);
138
+ }
139
+ const body = require_wire.parseRemoteConfigFetchResponse(await response.json().catch(() => null));
140
+ if (body === null) {
141
+ setStatus("failure");
142
+ throw new Error("[HotUpdater] The server's GET /remote-config answer is not Remote Config values.");
143
+ }
144
+ fetched = {
145
+ version: body.version,
146
+ values: body.values,
147
+ etag: response.headers.get("etag"),
148
+ context: contextKey,
149
+ fetchedAtMs
150
+ };
151
+ store(FETCHED_KEY, JSON.stringify(fetched));
152
+ setStatus("success");
153
+ };
154
+ const fetchValues = (options) => {
155
+ const device = deviceContext(context);
156
+ if (options?.force !== true && inFlight === null && fetched !== null && fetched.context === JSON.stringify(device) && context.now() - fetched.fetchedAtMs < minimumFetchIntervalMs) return Promise.resolve();
157
+ inFlight ??= fetchFromServer(device).finally(() => {
158
+ inFlight = null;
159
+ });
160
+ return inFlight;
161
+ };
162
+ const activate = async () => {
163
+ if (fetched === null) return false;
164
+ const next = {
165
+ version: fetched.version,
166
+ values: fetched.values,
167
+ etag: fetched.etag
168
+ };
169
+ if (active !== null && active.version === next.version && JSON.stringify(active.values) === JSON.stringify(next.values)) return false;
170
+ active = next;
171
+ store(ACTIVE_KEY, JSON.stringify(active));
172
+ notify();
173
+ return true;
174
+ };
175
+ return Object.freeze({
176
+ getValue,
177
+ getString: (key) => getValue(key)?.asString() ?? null,
178
+ getNumber: (key) => getValue(key)?.asNumber() ?? null,
179
+ getBoolean: (key) => getValue(key)?.asBoolean() ?? null,
180
+ getAll: () => {
181
+ snapshot ??= Object.freeze(Object.fromEntries([.../* @__PURE__ */ new Set([...Object.keys(defaults), ...Object.keys(active?.values ?? {})])].map((key) => [key, getValue(key)])));
182
+ return snapshot;
183
+ },
184
+ fetch: (options) => fetchValues(options),
185
+ activate,
186
+ fetchAndActivate: async (options) => {
187
+ await fetchValues(options);
188
+ return activate();
189
+ },
190
+ get lastFetchStatus() {
191
+ return lastFetchStatus;
192
+ },
193
+ get fetchedAtMs() {
194
+ return fetched?.fetchedAtMs ?? null;
195
+ },
196
+ get activeVersion() {
197
+ return active?.version ?? 0;
198
+ },
199
+ subscribe: (listener) => {
200
+ listeners.add(listener);
201
+ return () => {
202
+ listeners.delete(listener);
203
+ };
204
+ }
205
+ });
206
+ };
207
+ //#endregion
208
+ exports.remoteConfig = remoteConfig;
@@ -0,0 +1,123 @@
1
+ import { HotUpdaterClientPlugin } from "@hot-updater/protocol";
2
+ //#region src/client/index.d.ts
3
+ /** An in-app default value: text, a number, or a boolean. */
4
+ type RemoteConfigDefaultValue = string | number | boolean;
5
+ type RemoteConfigDefaults = Readonly<Record<string, RemoteConfigDefaultValue>>;
6
+ /**
7
+ * Where a value comes from: `remote` from the activated values, `default`
8
+ * from the in-app defaults. A key neither has reads as `null`.
9
+ */
10
+ type RemoteConfigValueSource = "default" | "remote";
11
+ /** How the last fetch ended: `failure` for any error, a `429` included. */
12
+ type RemoteConfigFetchStatus = "no-fetch-yet" | "success" | "failure";
13
+ /** A parameter's value, read as the type the app expects. */
14
+ interface RemoteConfigValue {
15
+ /** The text, `""` included. */
16
+ asString(): string;
17
+ /** The text as a number, `0` included; 0 for text that is not a number. */
18
+ asNumber(): number;
19
+ /** Whether the text is `1`, `true`, `t`, `yes`, `y`, or `on`, in any case. */
20
+ asBoolean(): boolean;
21
+ getSource(): RemoteConfigValueSource;
22
+ }
23
+ /** No defaults: every key may read as `null`. */
24
+ type NoDefaults = Record<never, never>;
25
+ interface RemoteConfigOptions<TDefaults extends RemoteConfigDefaults = NoDefaults> {
26
+ /** Values the app uses until it activates fetched ones, and for keys the template leaves to it. */
27
+ readonly defaults?: TDefaults;
28
+ /**
29
+ * The shortest time between fetches that reach the server; a fetch
30
+ * sooner keeps the values fetched last. Defaults to 12 hours. A fetch
31
+ * always reaches the server after the app's channel, version, cohort, or
32
+ * fingerprint changes.
33
+ */
34
+ readonly minimumFetchIntervalMs?: number;
35
+ }
36
+ interface RemoteConfigFetchOptions {
37
+ /**
38
+ * Asks the server even within `minimumFetchIntervalMs`, for a moment that
39
+ * needs fresh values, such as pull to refresh. Each one is a request (a
40
+ * `304` when nothing changed).
41
+ */
42
+ readonly force?: boolean;
43
+ }
44
+ /** A defaults key, with any other key still accepted. */
45
+ type RemoteConfigKey<TDefaults> = (keyof TDefaults & string) | (string & {});
46
+ /**
47
+ * What a read of `TKey` returns: `T` for a key `defaults` declares, which
48
+ * always has a value, and `T | null` for any other key. Defaults typed as
49
+ * a map of any string, such as `RemoteConfigDefaults`, declare no key.
50
+ */
51
+ type RemoteConfigRead<TDefaults, TKey extends string, T> = string extends keyof TDefaults ? T | null : TKey extends keyof TDefaults ? T : T | null;
52
+ /**
53
+ * Remote Config on the instance `HotUpdater.init` returns, as
54
+ * `hotUpdater.remoteConfig`.
55
+ */
56
+ interface RemoteConfigClient<TDefaults extends RemoteConfigDefaults = NoDefaults> {
57
+ /**
58
+ * A parameter's value: the active remote value, else the in-app default,
59
+ * else `null`. Reads are synchronous; `init` loads the values the app
60
+ * activated last from the device. `false`, `0`, and `""` are values, not
61
+ * missing ones.
62
+ */
63
+ getValue<TKey extends RemoteConfigKey<TDefaults>>(key: TKey): RemoteConfigRead<TDefaults, TKey, RemoteConfigValue>;
64
+ getString<TKey extends RemoteConfigKey<TDefaults>>(key: TKey): RemoteConfigRead<TDefaults, TKey, string>;
65
+ getNumber<TKey extends RemoteConfigKey<TDefaults>>(key: TKey): RemoteConfigRead<TDefaults, TKey, number>;
66
+ getBoolean<TKey extends RemoteConfigKey<TDefaults>>(key: TKey): RemoteConfigRead<TDefaults, TKey, boolean>;
67
+ /**
68
+ * Every key the defaults or the active values have. The object stays the
69
+ * same until the values change, so it suits `useSyncExternalStore`.
70
+ */
71
+ getAll(): Readonly<Record<string, RemoteConfigValue>>;
72
+ /**
73
+ * Fetches this device's values from `GET /remote-config` on the
74
+ * `baseURL` the app configured, with its request headers, and keeps them
75
+ * for `activate`; active values do not change. Within
76
+ * `minimumFetchIntervalMs` of the last successful fetch it keeps those
77
+ * values without a request, unless `force` is set. Rejects when the server
78
+ * cannot be reached or runs without the `remoteConfig()` plugin.
79
+ */
80
+ fetch(options?: RemoteConfigFetchOptions): Promise<void>;
81
+ /** Makes the fetched values active; true when they replaced other values. */
82
+ activate(): Promise<boolean>;
83
+ /**
84
+ * `fetch(options)`, then `activate()`, for an app that applies new values
85
+ * as soon as they arrive. Resolves as `activate` does; rejects as `fetch`
86
+ * does, and the active values stay.
87
+ */
88
+ fetchAndActivate(options?: RemoteConfigFetchOptions): Promise<boolean>;
89
+ readonly lastFetchStatus: RemoteConfigFetchStatus;
90
+ /** When the last successful fetch ended, in ms since the epoch; `null` before one. */
91
+ readonly fetchedAtMs: number | null;
92
+ /** The template version the active values come from; 0 before any. */
93
+ readonly activeVersion: number;
94
+ /** Calls `listener` whenever the active values change; returns the unsubscribe. */
95
+ subscribe(listener: () => void): () => void;
96
+ }
97
+ /** The `remoteConfig()` plugin: it adds `hotUpdater.remoteConfig`. */
98
+ type RemoteConfigPlugin<TDefaults extends RemoteConfigDefaults = NoDefaults> = HotUpdaterClientPlugin<"remoteConfig", RemoteConfigClient<TDefaults>>;
99
+ /**
100
+ * Remote Config for the app: in-app defaults, values a server running the
101
+ * `remoteConfig()` plugin picks for this device, and a fetch and activate
102
+ * step between them. Add it to `HotUpdater.init`'s `plugins`, and read it
103
+ * from the instance `init` returns, as `hotUpdater.remoteConfig`. It fetches
104
+ * from the `baseURL`, request headers, and timeout configured there.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * import { HotUpdater, remoteConfig } from "@hot-updater/react-native";
109
+ *
110
+ * export const hotUpdater = HotUpdater.init({
111
+ * baseURL,
112
+ * plugins: [
113
+ * remoteConfig({ defaults: { welcome_message: "Welcome", max_items: 20 } }),
114
+ * ],
115
+ * });
116
+ *
117
+ * hotUpdater.remoteConfig.fetchAndActivate().catch(() => {});
118
+ * hotUpdater.remoteConfig.getString("welcome_message");
119
+ * ```
120
+ */
121
+ declare const remoteConfig: <const TDefaults extends RemoteConfigDefaults = NoDefaults>(options?: RemoteConfigOptions<TDefaults>) => RemoteConfigPlugin<TDefaults>;
122
+ //#endregion
123
+ export { RemoteConfigClient, RemoteConfigDefaultValue, RemoteConfigDefaults, RemoteConfigFetchOptions, RemoteConfigFetchStatus, RemoteConfigKey, RemoteConfigOptions, RemoteConfigPlugin, RemoteConfigRead, RemoteConfigValue, RemoteConfigValueSource, remoteConfig };
@@ -0,0 +1,123 @@
1
+ import { HotUpdaterClientPlugin } from "@hot-updater/protocol";
2
+ //#region src/client/index.d.ts
3
+ /** An in-app default value: text, a number, or a boolean. */
4
+ type RemoteConfigDefaultValue = string | number | boolean;
5
+ type RemoteConfigDefaults = Readonly<Record<string, RemoteConfigDefaultValue>>;
6
+ /**
7
+ * Where a value comes from: `remote` from the activated values, `default`
8
+ * from the in-app defaults. A key neither has reads as `null`.
9
+ */
10
+ type RemoteConfigValueSource = "default" | "remote";
11
+ /** How the last fetch ended: `failure` for any error, a `429` included. */
12
+ type RemoteConfigFetchStatus = "no-fetch-yet" | "success" | "failure";
13
+ /** A parameter's value, read as the type the app expects. */
14
+ interface RemoteConfigValue {
15
+ /** The text, `""` included. */
16
+ asString(): string;
17
+ /** The text as a number, `0` included; 0 for text that is not a number. */
18
+ asNumber(): number;
19
+ /** Whether the text is `1`, `true`, `t`, `yes`, `y`, or `on`, in any case. */
20
+ asBoolean(): boolean;
21
+ getSource(): RemoteConfigValueSource;
22
+ }
23
+ /** No defaults: every key may read as `null`. */
24
+ type NoDefaults = Record<never, never>;
25
+ interface RemoteConfigOptions<TDefaults extends RemoteConfigDefaults = NoDefaults> {
26
+ /** Values the app uses until it activates fetched ones, and for keys the template leaves to it. */
27
+ readonly defaults?: TDefaults;
28
+ /**
29
+ * The shortest time between fetches that reach the server; a fetch
30
+ * sooner keeps the values fetched last. Defaults to 12 hours. A fetch
31
+ * always reaches the server after the app's channel, version, cohort, or
32
+ * fingerprint changes.
33
+ */
34
+ readonly minimumFetchIntervalMs?: number;
35
+ }
36
+ interface RemoteConfigFetchOptions {
37
+ /**
38
+ * Asks the server even within `minimumFetchIntervalMs`, for a moment that
39
+ * needs fresh values, such as pull to refresh. Each one is a request (a
40
+ * `304` when nothing changed).
41
+ */
42
+ readonly force?: boolean;
43
+ }
44
+ /** A defaults key, with any other key still accepted. */
45
+ type RemoteConfigKey<TDefaults> = (keyof TDefaults & string) | (string & {});
46
+ /**
47
+ * What a read of `TKey` returns: `T` for a key `defaults` declares, which
48
+ * always has a value, and `T | null` for any other key. Defaults typed as
49
+ * a map of any string, such as `RemoteConfigDefaults`, declare no key.
50
+ */
51
+ type RemoteConfigRead<TDefaults, TKey extends string, T> = string extends keyof TDefaults ? T | null : TKey extends keyof TDefaults ? T : T | null;
52
+ /**
53
+ * Remote Config on the instance `HotUpdater.init` returns, as
54
+ * `hotUpdater.remoteConfig`.
55
+ */
56
+ interface RemoteConfigClient<TDefaults extends RemoteConfigDefaults = NoDefaults> {
57
+ /**
58
+ * A parameter's value: the active remote value, else the in-app default,
59
+ * else `null`. Reads are synchronous; `init` loads the values the app
60
+ * activated last from the device. `false`, `0`, and `""` are values, not
61
+ * missing ones.
62
+ */
63
+ getValue<TKey extends RemoteConfigKey<TDefaults>>(key: TKey): RemoteConfigRead<TDefaults, TKey, RemoteConfigValue>;
64
+ getString<TKey extends RemoteConfigKey<TDefaults>>(key: TKey): RemoteConfigRead<TDefaults, TKey, string>;
65
+ getNumber<TKey extends RemoteConfigKey<TDefaults>>(key: TKey): RemoteConfigRead<TDefaults, TKey, number>;
66
+ getBoolean<TKey extends RemoteConfigKey<TDefaults>>(key: TKey): RemoteConfigRead<TDefaults, TKey, boolean>;
67
+ /**
68
+ * Every key the defaults or the active values have. The object stays the
69
+ * same until the values change, so it suits `useSyncExternalStore`.
70
+ */
71
+ getAll(): Readonly<Record<string, RemoteConfigValue>>;
72
+ /**
73
+ * Fetches this device's values from `GET /remote-config` on the
74
+ * `baseURL` the app configured, with its request headers, and keeps them
75
+ * for `activate`; active values do not change. Within
76
+ * `minimumFetchIntervalMs` of the last successful fetch it keeps those
77
+ * values without a request, unless `force` is set. Rejects when the server
78
+ * cannot be reached or runs without the `remoteConfig()` plugin.
79
+ */
80
+ fetch(options?: RemoteConfigFetchOptions): Promise<void>;
81
+ /** Makes the fetched values active; true when they replaced other values. */
82
+ activate(): Promise<boolean>;
83
+ /**
84
+ * `fetch(options)`, then `activate()`, for an app that applies new values
85
+ * as soon as they arrive. Resolves as `activate` does; rejects as `fetch`
86
+ * does, and the active values stay.
87
+ */
88
+ fetchAndActivate(options?: RemoteConfigFetchOptions): Promise<boolean>;
89
+ readonly lastFetchStatus: RemoteConfigFetchStatus;
90
+ /** When the last successful fetch ended, in ms since the epoch; `null` before one. */
91
+ readonly fetchedAtMs: number | null;
92
+ /** The template version the active values come from; 0 before any. */
93
+ readonly activeVersion: number;
94
+ /** Calls `listener` whenever the active values change; returns the unsubscribe. */
95
+ subscribe(listener: () => void): () => void;
96
+ }
97
+ /** The `remoteConfig()` plugin: it adds `hotUpdater.remoteConfig`. */
98
+ type RemoteConfigPlugin<TDefaults extends RemoteConfigDefaults = NoDefaults> = HotUpdaterClientPlugin<"remoteConfig", RemoteConfigClient<TDefaults>>;
99
+ /**
100
+ * Remote Config for the app: in-app defaults, values a server running the
101
+ * `remoteConfig()` plugin picks for this device, and a fetch and activate
102
+ * step between them. Add it to `HotUpdater.init`'s `plugins`, and read it
103
+ * from the instance `init` returns, as `hotUpdater.remoteConfig`. It fetches
104
+ * from the `baseURL`, request headers, and timeout configured there.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * import { HotUpdater, remoteConfig } from "@hot-updater/react-native";
109
+ *
110
+ * export const hotUpdater = HotUpdater.init({
111
+ * baseURL,
112
+ * plugins: [
113
+ * remoteConfig({ defaults: { welcome_message: "Welcome", max_items: 20 } }),
114
+ * ],
115
+ * });
116
+ *
117
+ * hotUpdater.remoteConfig.fetchAndActivate().catch(() => {});
118
+ * hotUpdater.remoteConfig.getString("welcome_message");
119
+ * ```
120
+ */
121
+ declare const remoteConfig: <const TDefaults extends RemoteConfigDefaults = NoDefaults>(options?: RemoteConfigOptions<TDefaults>) => RemoteConfigPlugin<TDefaults>;
122
+ //#endregion
123
+ export { RemoteConfigClient, RemoteConfigDefaultValue, RemoteConfigDefaults, RemoteConfigFetchOptions, RemoteConfigFetchStatus, RemoteConfigKey, RemoteConfigOptions, RemoteConfigPlugin, RemoteConfigRead, RemoteConfigValue, RemoteConfigValueSource, remoteConfig };