@jewel998/config 0.2.4 → 0.2.7
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/dist/client/ConfigClient.js +2 -2
- package/dist/client/RefreshManager.js +5 -1
- package/dist/fetch/TierFetcher.d.ts +10 -4
- package/dist/fetch/TierFetcher.js +11 -3
- package/dist/fetch/batchFetcher.js +2 -2
- package/dist/fetch/projectedFetcher.js +5 -5
- package/dist/initConfig.d.ts +7 -1
- package/dist/initConfig.js +143 -39
- package/dist/loading/optimistic.js +1 -1
- package/dist/loading/pessimistic.js +1 -1
- package/dist/types.d.ts +13 -2
- package/package.json +1 -1
|
@@ -32,7 +32,7 @@ export const buildConfigClient = (internals) => {
|
|
|
32
32
|
batchFetchTriggered = true;
|
|
33
33
|
void (async () => {
|
|
34
34
|
try {
|
|
35
|
-
const result = await withRetry(() => fetcher.fetchAll(), retry);
|
|
35
|
+
const { data: result } = await withRetry(() => fetcher.fetchAll(), retry);
|
|
36
36
|
cache.set("__all__", result, DEFAULT_CACHE_TTL);
|
|
37
37
|
for (const [key, value] of Object.entries(result)) {
|
|
38
38
|
cache.set(key, value, DEFAULT_CACHE_TTL);
|
|
@@ -58,7 +58,7 @@ export const buildConfigClient = (internals) => {
|
|
|
58
58
|
return;
|
|
59
59
|
void (async () => {
|
|
60
60
|
try {
|
|
61
|
-
const result = await fetcher.fetchKeys([key]);
|
|
61
|
+
const { data: result } = await fetcher.fetchKeys([key]);
|
|
62
62
|
for (const [k, v] of Object.entries(result)) {
|
|
63
63
|
cache.set(k, v, DEFAULT_CACHE_TTL);
|
|
64
64
|
data[k] = v;
|
|
@@ -59,7 +59,11 @@ export class RefreshManager {
|
|
|
59
59
|
catch {
|
|
60
60
|
// If version check fails, proceed with full fetch as fallback
|
|
61
61
|
}
|
|
62
|
-
const result = await withRetry(() => fetcher.fetchAll(), retry);
|
|
62
|
+
const { data: result, version: responseVersion } = await withRetry(() => fetcher.fetchAll(), retry);
|
|
63
|
+
// Sync the version from the config response itself — avoids drifting
|
|
64
|
+
// from the value learned by the earlier checkVersion() probe.
|
|
65
|
+
if (responseVersion !== null)
|
|
66
|
+
this.cachedVersion = responseVersion;
|
|
63
67
|
cache.set("__all__", result, DEFAULT_CACHE_TTL);
|
|
64
68
|
for (const [key, value] of Object.entries(result)) {
|
|
65
69
|
cache.set(key, value, DEFAULT_CACHE_TTL);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConfigFetcher } from "../types.js";
|
|
1
|
+
import type { ConfigFetcher, FetchResult } from "../types.js";
|
|
2
2
|
export type FetchTier = "prefetch" | "page" | "idle";
|
|
3
3
|
/**
|
|
4
4
|
* TierFetcher — wraps a ConfigFetcher with three responsibilities:
|
|
@@ -24,12 +24,12 @@ export declare class TierFetcher {
|
|
|
24
24
|
* Fetch a specific set of keys under a given tier.
|
|
25
25
|
* Keys already tracked under any tier are skipped (deduplication).
|
|
26
26
|
*/
|
|
27
|
-
fetchTier(keys: string[], tier: FetchTier): Promise<
|
|
27
|
+
fetchTier(keys: string[], tier: FetchTier): Promise<FetchResult>;
|
|
28
28
|
/**
|
|
29
29
|
* Fetch all remaining keys (idle tier).
|
|
30
30
|
* Only keys not already tracked are added to the idle set.
|
|
31
31
|
*/
|
|
32
|
-
fetchAll(): Promise<
|
|
32
|
+
fetchAll(): Promise<FetchResult>;
|
|
33
33
|
/** Check the remote version (passthrough). */
|
|
34
34
|
checkVersion(): ReturnType<ConfigFetcher["checkVersion"]>;
|
|
35
35
|
/**
|
|
@@ -37,7 +37,13 @@ export declare class TierFetcher {
|
|
|
37
37
|
* Used by doRefresh() — we're intentionally re-fetching already-fetched keys
|
|
38
38
|
* with a new context, so the allFetched deduplication should be bypassed.
|
|
39
39
|
*/
|
|
40
|
-
refetchKeys(keys: string[]): Promise<
|
|
40
|
+
refetchKeys(keys: string[]): Promise<FetchResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Record keys as fetched under a tier WITHOUT performing a network fetch.
|
|
43
|
+
* Used when values are hydrated from persistent storage (the boot gate),
|
|
44
|
+
* so later context re-fetches correctly scope to these keys.
|
|
45
|
+
*/
|
|
46
|
+
markFetched(keys: string[], tier: FetchTier): void;
|
|
41
47
|
/**
|
|
42
48
|
* Register a waiter for a key that hasn't been fetched yet.
|
|
43
49
|
* Returns a Promise that resolves when the key arrives, or rejects on error.
|
|
@@ -36,9 +36,9 @@ export class TierFetcher {
|
|
|
36
36
|
async fetchTier(keys, tier) {
|
|
37
37
|
const toFetch = keys.filter((k) => !this.allFetched.has(k));
|
|
38
38
|
if (toFetch.length === 0)
|
|
39
|
-
return {};
|
|
39
|
+
return { data: {}, version: null };
|
|
40
40
|
const result = await this.fetcher.fetchKeys(toFetch);
|
|
41
|
-
this.recordKeys(Object.keys(result), tier);
|
|
41
|
+
this.recordKeys(Object.keys(result.data), tier);
|
|
42
42
|
return result;
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
@@ -47,7 +47,7 @@ export class TierFetcher {
|
|
|
47
47
|
*/
|
|
48
48
|
async fetchAll() {
|
|
49
49
|
const result = await this.fetcher.fetchAll();
|
|
50
|
-
for (const key of Object.keys(result)) {
|
|
50
|
+
for (const key of Object.keys(result.data)) {
|
|
51
51
|
if (!this.allFetched.has(key)) {
|
|
52
52
|
this.fetchedKeys.get("idle").add(key);
|
|
53
53
|
this.allFetched.add(key);
|
|
@@ -67,6 +67,14 @@ export class TierFetcher {
|
|
|
67
67
|
async refetchKeys(keys) {
|
|
68
68
|
return this.fetcher.fetchKeys(keys);
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Record keys as fetched under a tier WITHOUT performing a network fetch.
|
|
72
|
+
* Used when values are hydrated from persistent storage (the boot gate),
|
|
73
|
+
* so later context re-fetches correctly scope to these keys.
|
|
74
|
+
*/
|
|
75
|
+
markFetched(keys, tier) {
|
|
76
|
+
this.recordKeys(keys, tier);
|
|
77
|
+
}
|
|
70
78
|
// ─── Waiter management ───────────────────────────────────────
|
|
71
79
|
/**
|
|
72
80
|
* Register a waiter for a key that hasn't been fetched yet.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export const createBatchFetcher = (transport) => ({
|
|
2
2
|
async fetchAll() {
|
|
3
3
|
const response = await transport.request("v1/config");
|
|
4
|
-
return response.data;
|
|
4
|
+
return { data: response.data, version: response.version ?? null };
|
|
5
5
|
},
|
|
6
6
|
async fetchKeys(_keys) {
|
|
7
7
|
// Batch mode always fetches everything
|
|
8
8
|
const response = await transport.request("v1/config");
|
|
9
|
-
return response.data;
|
|
9
|
+
return { data: response.data, version: response.version ?? null };
|
|
10
10
|
},
|
|
11
11
|
async checkVersion() {
|
|
12
12
|
const response = await transport.request("v1/version");
|
|
@@ -4,7 +4,7 @@ export const createProjectedFetcher = (transport) => {
|
|
|
4
4
|
return {
|
|
5
5
|
async fetchAll() {
|
|
6
6
|
const response = await transport.request("v1/config");
|
|
7
|
-
return response.data;
|
|
7
|
+
return { data: response.data, version: response.version ?? null };
|
|
8
8
|
},
|
|
9
9
|
async fetchKeys(keys) {
|
|
10
10
|
for (const key of keys) {
|
|
@@ -20,7 +20,7 @@ export const createProjectedFetcher = (transport) => {
|
|
|
20
20
|
const response = await transport.request("v1/config", {
|
|
21
21
|
keys: batchedKeys,
|
|
22
22
|
});
|
|
23
|
-
resolve(response.data);
|
|
23
|
+
resolve({ data: response.data, version: response.version ?? null });
|
|
24
24
|
}
|
|
25
25
|
catch (err) {
|
|
26
26
|
reject(err);
|
|
@@ -32,11 +32,11 @@ export const createProjectedFetcher = (transport) => {
|
|
|
32
32
|
// Return only the keys this caller requested
|
|
33
33
|
const filtered = {};
|
|
34
34
|
for (const key of keys) {
|
|
35
|
-
if (key in result) {
|
|
36
|
-
filtered[key] = result[key];
|
|
35
|
+
if (key in result.data) {
|
|
36
|
+
filtered[key] = result.data[key];
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
return filtered;
|
|
39
|
+
return { data: filtered, version: result.version };
|
|
40
40
|
},
|
|
41
41
|
async checkVersion() {
|
|
42
42
|
const response = await transport.request("v1/version");
|
package/dist/initConfig.d.ts
CHANGED
|
@@ -22,7 +22,13 @@ export interface InitConfigOptions {
|
|
|
22
22
|
* Default: https://jewel998-config.web.app/api (demo instance)
|
|
23
23
|
*/
|
|
24
24
|
baseUrl?: string;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Cache storage adapter. Default: `browserStorage()` (localStorage), which
|
|
27
|
+
* persists values and the config version across page loads so a reload or
|
|
28
|
+
* return visit skips the `/config` fetch when nothing changed. Falls back to
|
|
29
|
+
* in-memory automatically when `localStorage` is unavailable (SSR / Node /
|
|
30
|
+
* server keys). Pass `memoryStorage()` explicitly to opt out of persistence.
|
|
31
|
+
*/
|
|
26
32
|
storage?: CacheStorage;
|
|
27
33
|
/**
|
|
28
34
|
* Version polling interval in milliseconds.
|
package/dist/initConfig.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// setContext() is fire-and-forget (debounced 100ms).
|
|
27
27
|
// Re-fetches only already-fetched keys in tier order.
|
|
28
28
|
// ═══════════════════════════════════════════════════════════════
|
|
29
|
-
import {
|
|
29
|
+
import { browserStorage } from "./cache/browserStorage.js";
|
|
30
30
|
import { SdkError, ConfigError } from "./errors/index.js";
|
|
31
31
|
import { TypedEventEmitter } from "./events/EventEmitter.js";
|
|
32
32
|
import { createProjectedFetcher } from "./fetch/projectedFetcher.js";
|
|
@@ -38,6 +38,34 @@ const DEFAULT_BASE_URL = "https://jewel998-config.web.app/api";
|
|
|
38
38
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
39
39
|
const CONTEXT_DEBOUNCE_MS = 100;
|
|
40
40
|
const MIN_REFETCH_INTERVAL_MS = 30_000;
|
|
41
|
+
/** Reserved storage keys for persisted gating metadata. */
|
|
42
|
+
const VERSION_STORAGE_KEY = "__version__";
|
|
43
|
+
const CONTEXT_FINGERPRINT_STORAGE_KEY = "__ctx__";
|
|
44
|
+
/**
|
|
45
|
+
* Stable string fingerprint of an evaluation context. Keys are sorted so the
|
|
46
|
+
* same context always produces the same fingerprint regardless of insertion
|
|
47
|
+
* order. Used to detect when context changed across loads (which invalidates
|
|
48
|
+
* persisted values even if the config version is unchanged).
|
|
49
|
+
*/
|
|
50
|
+
function contextFingerprint(ctx) {
|
|
51
|
+
const stable = (value) => {
|
|
52
|
+
if (value === null || typeof value !== "object")
|
|
53
|
+
return value;
|
|
54
|
+
if (Array.isArray(value))
|
|
55
|
+
return value.map(stable);
|
|
56
|
+
const obj = value;
|
|
57
|
+
const sorted = {};
|
|
58
|
+
for (const k of Object.keys(obj).sort())
|
|
59
|
+
sorted[k] = stable(obj[k]);
|
|
60
|
+
return sorted;
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
return JSON.stringify(stable(ctx ?? {}));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
41
69
|
// ─── Implementation ────────────────────────────────────────────
|
|
42
70
|
export function initConfig(options) {
|
|
43
71
|
if (!options.clientId) {
|
|
@@ -49,7 +77,7 @@ export function initConfig(options) {
|
|
|
49
77
|
const defaults = options.defaults ?? {};
|
|
50
78
|
const pollInterval = options.pollInterval ?? 300_000;
|
|
51
79
|
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
52
|
-
const storage = options.storage ??
|
|
80
|
+
const storage = options.storage ?? browserStorage();
|
|
53
81
|
const retry = { ...DEFAULT_RETRY };
|
|
54
82
|
const timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
55
83
|
const onError = options.onError;
|
|
@@ -67,6 +95,22 @@ export function initConfig(options) {
|
|
|
67
95
|
const tierFetcher = new TierFetcher(baseFetcher);
|
|
68
96
|
// ── In-memory data store ─────────────────────────────────────
|
|
69
97
|
const data = {};
|
|
98
|
+
// Version known to the SDK. Hydrated from persistent storage on init so a
|
|
99
|
+
// reload can skip the config fetch when the version is unchanged.
|
|
100
|
+
let cachedVersion = storage.get(VERSION_STORAGE_KEY) ?? null;
|
|
101
|
+
const currentFingerprint = contextFingerprint(currentContext);
|
|
102
|
+
// If the persisted context fingerprint differs from this init's context, any
|
|
103
|
+
// persisted per-user values are stale — drop the persisted version so the
|
|
104
|
+
// boot gate re-fetches rather than serving values evaluated for old context.
|
|
105
|
+
if (storage.get(CONTEXT_FINGERPRINT_STORAGE_KEY) !== currentFingerprint) {
|
|
106
|
+
cachedVersion = null;
|
|
107
|
+
}
|
|
108
|
+
/** Persist the current version + context fingerprint next to the values. */
|
|
109
|
+
function persistVersion(version) {
|
|
110
|
+
cachedVersion = version;
|
|
111
|
+
storage.set(VERSION_STORAGE_KEY, version, DEFAULT_CACHE_TTL);
|
|
112
|
+
storage.set(CONTEXT_FINGERPRINT_STORAGE_KEY, currentFingerprint, DEFAULT_CACHE_TTL);
|
|
113
|
+
}
|
|
70
114
|
function applyResult(result) {
|
|
71
115
|
const keys = Object.keys(result);
|
|
72
116
|
for (const key of keys) {
|
|
@@ -83,13 +127,65 @@ export function initConfig(options) {
|
|
|
83
127
|
return;
|
|
84
128
|
events.emit("updated", { keys, source });
|
|
85
129
|
}
|
|
86
|
-
// ──
|
|
130
|
+
// ── Remote version check (shared by boot gate + polling) ─────
|
|
131
|
+
async function fetchRemoteVersion() {
|
|
132
|
+
try {
|
|
133
|
+
const res = await fetch(`${baseUrl}/v1/version?clientId=${options.clientId}`, {
|
|
134
|
+
headers: cachedVersion ? { "If-None-Match": `"${cachedVersion}"` } : {},
|
|
135
|
+
});
|
|
136
|
+
// 304 => unchanged from the version we sent in If-None-Match.
|
|
137
|
+
if (res.status === 304)
|
|
138
|
+
return cachedVersion;
|
|
139
|
+
if (!res.ok)
|
|
140
|
+
return null;
|
|
141
|
+
const { version } = (await res.json());
|
|
142
|
+
return version ?? null;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** True when every key is already present in persistent storage. */
|
|
149
|
+
function allKeysCached(keys) {
|
|
150
|
+
return keys.every((k) => storage.get(k) !== undefined);
|
|
151
|
+
}
|
|
152
|
+
function hydrateFromStorage(keys) {
|
|
153
|
+
const hydrated = [];
|
|
154
|
+
for (const key of keys) {
|
|
155
|
+
const cached = storage.get(key);
|
|
156
|
+
if (cached !== undefined) {
|
|
157
|
+
data[key] = cached;
|
|
158
|
+
hydrated.push(key);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return hydrated;
|
|
162
|
+
}
|
|
163
|
+
// ── Tier 1: init prefetch (version-gated against persisted cache) ─
|
|
87
164
|
const readyPromise = initPrefetch.length > 0
|
|
88
165
|
? (async () => {
|
|
89
166
|
try {
|
|
167
|
+
// Boot gate: if we have a persisted version and values, confirm the
|
|
168
|
+
// remote version matches before spending a /config call. On match,
|
|
169
|
+
// serve from storage and skip the fetch entirely.
|
|
170
|
+
if (cachedVersion !== null && allKeysCached(initPrefetch)) {
|
|
171
|
+
const remote = await fetchRemoteVersion();
|
|
172
|
+
if (remote !== null && remote === cachedVersion) {
|
|
173
|
+
const hydrated = hydrateFromStorage(initPrefetch);
|
|
174
|
+
tierFetcher.markFetched(initPrefetch, "prefetch");
|
|
175
|
+
emitUpdated(hydrated, "prefetch");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Version advanced (or unknown) — fall through to a real fetch.
|
|
179
|
+
if (remote !== null)
|
|
180
|
+
cachedVersion = remote;
|
|
181
|
+
}
|
|
90
182
|
const result = await withRetry(() => tierFetcher.fetchTier(initPrefetch, "prefetch"), retry);
|
|
91
|
-
const fetched = applyResult(result);
|
|
92
|
-
|
|
183
|
+
const fetched = applyResult(result.data);
|
|
184
|
+
// Persist the version delivered alongside these values — no extra
|
|
185
|
+
// /version round trip needed; the /config response carries it.
|
|
186
|
+
if (result.version !== null)
|
|
187
|
+
persistVersion(result.version);
|
|
188
|
+
tierFetcher.notifyWaiters(result.data);
|
|
93
189
|
emitUpdated(fetched, "prefetch");
|
|
94
190
|
}
|
|
95
191
|
catch (err) {
|
|
@@ -104,13 +200,24 @@ export function initConfig(options) {
|
|
|
104
200
|
}
|
|
105
201
|
})()
|
|
106
202
|
: Promise.resolve();
|
|
107
|
-
// ── Tier 3: idle fetch
|
|
203
|
+
// ── Tier 3: idle fetch (skipped when the full snapshot is fresh) ──
|
|
108
204
|
readyPromise.then(() => {
|
|
109
205
|
scheduleIdleFetch(async () => {
|
|
110
206
|
try {
|
|
207
|
+
// If we already hold the full snapshot for the current version,
|
|
208
|
+
// there is nothing to fill in — skip the idle /config call.
|
|
209
|
+
if (cachedVersion !== null && storage.get("__all__") !== undefined) {
|
|
210
|
+
const remote = await fetchRemoteVersion();
|
|
211
|
+
if (remote !== null && remote === cachedVersion)
|
|
212
|
+
return;
|
|
213
|
+
if (remote !== null)
|
|
214
|
+
cachedVersion = remote;
|
|
215
|
+
}
|
|
111
216
|
const result = await tierFetcher.fetchAll();
|
|
112
|
-
const fetched = applyResult(result);
|
|
113
|
-
|
|
217
|
+
const fetched = applyResult(result.data);
|
|
218
|
+
if (result.version !== null)
|
|
219
|
+
persistVersion(result.version);
|
|
220
|
+
tierFetcher.notifyWaiters(result.data);
|
|
114
221
|
emitUpdated(fetched, "idle");
|
|
115
222
|
}
|
|
116
223
|
catch {
|
|
@@ -120,28 +227,17 @@ export function initConfig(options) {
|
|
|
120
227
|
});
|
|
121
228
|
});
|
|
122
229
|
// ── Version polling ──────────────────────────────────────────
|
|
123
|
-
let cachedVersion = null;
|
|
124
230
|
async function checkVersion() {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!res.ok)
|
|
132
|
-
return;
|
|
133
|
-
const { version } = (await res.json());
|
|
134
|
-
if (cachedVersion === null) {
|
|
135
|
-
cachedVersion = version;
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (version !== cachedVersion) {
|
|
139
|
-
cachedVersion = version;
|
|
140
|
-
await doRefresh();
|
|
141
|
-
}
|
|
231
|
+
const version = await fetchRemoteVersion();
|
|
232
|
+
if (version === null)
|
|
233
|
+
return;
|
|
234
|
+
if (cachedVersion === null) {
|
|
235
|
+
persistVersion(version);
|
|
236
|
+
return;
|
|
142
237
|
}
|
|
143
|
-
|
|
144
|
-
|
|
238
|
+
if (version !== cachedVersion) {
|
|
239
|
+
persistVersion(version);
|
|
240
|
+
await doRefresh();
|
|
145
241
|
}
|
|
146
242
|
}
|
|
147
243
|
if (pollInterval > 0 && typeof window !== "undefined") {
|
|
@@ -164,18 +260,22 @@ export function initConfig(options) {
|
|
|
164
260
|
if (pf.length > 0) {
|
|
165
261
|
refetches.push(withRetry(() => tierFetcher.refetchKeys(pf), retry)
|
|
166
262
|
.then((r) => {
|
|
167
|
-
applyResult(r);
|
|
168
|
-
|
|
169
|
-
|
|
263
|
+
applyResult(r.data);
|
|
264
|
+
if (r.version !== null)
|
|
265
|
+
persistVersion(r.version);
|
|
266
|
+
tierFetcher.notifyWaiters(r.data);
|
|
267
|
+
allFetched.push(...Object.keys(r.data));
|
|
170
268
|
})
|
|
171
269
|
.catch(() => { }));
|
|
172
270
|
}
|
|
173
271
|
if (page.length > 0) {
|
|
174
272
|
refetches.push(withRetry(() => tierFetcher.refetchKeys(page), retry)
|
|
175
273
|
.then((r) => {
|
|
176
|
-
applyResult(r);
|
|
177
|
-
|
|
178
|
-
|
|
274
|
+
applyResult(r.data);
|
|
275
|
+
if (r.version !== null)
|
|
276
|
+
persistVersion(r.version);
|
|
277
|
+
tierFetcher.notifyWaiters(r.data);
|
|
278
|
+
allFetched.push(...Object.keys(r.data));
|
|
179
279
|
})
|
|
180
280
|
.catch(() => { }));
|
|
181
281
|
}
|
|
@@ -183,9 +283,11 @@ export function initConfig(options) {
|
|
|
183
283
|
refetches.push(tierFetcher
|
|
184
284
|
.fetchAll()
|
|
185
285
|
.then((r) => {
|
|
186
|
-
applyResult(r);
|
|
187
|
-
|
|
188
|
-
|
|
286
|
+
applyResult(r.data);
|
|
287
|
+
if (r.version !== null)
|
|
288
|
+
persistVersion(r.version);
|
|
289
|
+
tierFetcher.notifyWaiters(r.data);
|
|
290
|
+
allFetched.push(...Object.keys(r.data));
|
|
189
291
|
})
|
|
190
292
|
.catch(() => { }));
|
|
191
293
|
}
|
|
@@ -203,8 +305,10 @@ export function initConfig(options) {
|
|
|
203
305
|
void (async () => {
|
|
204
306
|
try {
|
|
205
307
|
const result = await withRetry(() => tierFetcher.fetchTier(keys, "page"), retry);
|
|
206
|
-
const fetched = applyResult(result);
|
|
207
|
-
|
|
308
|
+
const fetched = applyResult(result.data);
|
|
309
|
+
if (result.version !== null)
|
|
310
|
+
persistVersion(result.version);
|
|
311
|
+
tierFetcher.notifyWaiters(result.data);
|
|
208
312
|
emitUpdated(fetched, "page");
|
|
209
313
|
}
|
|
210
314
|
catch (err) {
|
|
@@ -3,7 +3,7 @@ import { DEFAULT_CACHE_TTL } from "../types.js";
|
|
|
3
3
|
const scheduleBackgroundFetch = (ctx) => {
|
|
4
4
|
const doFetch = async () => {
|
|
5
5
|
try {
|
|
6
|
-
const data = await withRetry(() => ctx.fetcher.fetchAll(), ctx.retry);
|
|
6
|
+
const { data } = await withRetry(() => ctx.fetcher.fetchAll(), ctx.retry);
|
|
7
7
|
ctx.cache.set("__all__", data, DEFAULT_CACHE_TTL);
|
|
8
8
|
for (const [key, value] of Object.entries(data)) {
|
|
9
9
|
ctx.cache.set(key, value, DEFAULT_CACHE_TTL);
|
|
@@ -7,7 +7,7 @@ export const executePessimistic = async (ctx) => {
|
|
|
7
7
|
setTimeout(() => reject(new TimeoutError(ctx.timeout)), ctx.timeout);
|
|
8
8
|
});
|
|
9
9
|
try {
|
|
10
|
-
const data = await Promise.race([fetchPromise, timeoutPromise]);
|
|
10
|
+
const { data } = await Promise.race([fetchPromise, timeoutPromise]);
|
|
11
11
|
ctx.cache.set("__all__", data, DEFAULT_CACHE_TTL);
|
|
12
12
|
for (const [key, value] of Object.entries(data)) {
|
|
13
13
|
ctx.cache.set(key, value, DEFAULT_CACHE_TTL);
|
package/dist/types.d.ts
CHANGED
|
@@ -137,11 +137,22 @@ export interface LoadingResult {
|
|
|
137
137
|
export interface LoadingStrategyHandler {
|
|
138
138
|
execute(context: LoadingContext): LoadingResult | Promise<LoadingResult>;
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Result of a config fetch — the resolved values plus the config version
|
|
142
|
+
* they correspond to. The version comes from the same `/config` response
|
|
143
|
+
* that delivered the data, so the SDK can persist it without a second
|
|
144
|
+
* `/version` round trip.
|
|
145
|
+
*/
|
|
146
|
+
export interface FetchResult {
|
|
147
|
+
data: Record<string, unknown>;
|
|
148
|
+
/** Config version from the response, or null if the response omitted it. */
|
|
149
|
+
version: string | null;
|
|
150
|
+
}
|
|
140
151
|
export interface ConfigFetcher {
|
|
141
152
|
/** Fetch all configs for the bound project+environment */
|
|
142
|
-
fetchAll(): Promise<
|
|
153
|
+
fetchAll(): Promise<FetchResult>;
|
|
143
154
|
/** Fetch specific keys only */
|
|
144
|
-
fetchKeys(keys: string[]): Promise<
|
|
155
|
+
fetchKeys(keys: string[]): Promise<FetchResult>;
|
|
145
156
|
/** Check current config version (lightweight — single Firestore read) */
|
|
146
157
|
checkVersion(): Promise<{
|
|
147
158
|
version: string;
|