@jewel998/config 0.2.6 → 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.
@@ -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<Record<string, unknown>>;
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<Record<string, unknown>>;
32
+ fetchAll(): Promise<FetchResult>;
33
33
  /** Check the remote version (passthrough). */
34
34
  checkVersion(): ReturnType<ConfigFetcher["checkVersion"]>;
35
35
  /**
@@ -37,7 +37,7 @@ 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<Record<string, unknown>>;
40
+ refetchKeys(keys: string[]): Promise<FetchResult>;
41
41
  /**
42
42
  * Record keys as fetched under a tier WITHOUT performing a network fetch.
43
43
  * Used when values are hydrated from persistent storage (the boot gate),
@@ -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);
@@ -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");
@@ -180,12 +180,12 @@ export function initConfig(options) {
180
180
  cachedVersion = remote;
181
181
  }
182
182
  const result = await withRetry(() => tierFetcher.fetchTier(initPrefetch, "prefetch"), retry);
183
- const fetched = applyResult(result);
184
- // Record the version these values correspond to.
185
- const v = await fetchRemoteVersion();
186
- if (v !== null)
187
- persistVersion(v);
188
- tierFetcher.notifyWaiters(result);
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);
189
189
  emitUpdated(fetched, "prefetch");
190
190
  }
191
191
  catch (err) {
@@ -214,11 +214,10 @@ export function initConfig(options) {
214
214
  cachedVersion = remote;
215
215
  }
216
216
  const result = await tierFetcher.fetchAll();
217
- const fetched = applyResult(result);
218
- const v = await fetchRemoteVersion();
219
- if (v !== null)
220
- persistVersion(v);
221
- tierFetcher.notifyWaiters(result);
217
+ const fetched = applyResult(result.data);
218
+ if (result.version !== null)
219
+ persistVersion(result.version);
220
+ tierFetcher.notifyWaiters(result.data);
222
221
  emitUpdated(fetched, "idle");
223
222
  }
224
223
  catch {
@@ -261,18 +260,22 @@ export function initConfig(options) {
261
260
  if (pf.length > 0) {
262
261
  refetches.push(withRetry(() => tierFetcher.refetchKeys(pf), retry)
263
262
  .then((r) => {
264
- applyResult(r);
265
- tierFetcher.notifyWaiters(r);
266
- allFetched.push(...Object.keys(r));
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));
267
268
  })
268
269
  .catch(() => { }));
269
270
  }
270
271
  if (page.length > 0) {
271
272
  refetches.push(withRetry(() => tierFetcher.refetchKeys(page), retry)
272
273
  .then((r) => {
273
- applyResult(r);
274
- tierFetcher.notifyWaiters(r);
275
- allFetched.push(...Object.keys(r));
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));
276
279
  })
277
280
  .catch(() => { }));
278
281
  }
@@ -280,9 +283,11 @@ export function initConfig(options) {
280
283
  refetches.push(tierFetcher
281
284
  .fetchAll()
282
285
  .then((r) => {
283
- applyResult(r);
284
- tierFetcher.notifyWaiters(r);
285
- allFetched.push(...Object.keys(r));
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));
286
291
  })
287
292
  .catch(() => { }));
288
293
  }
@@ -300,8 +305,10 @@ export function initConfig(options) {
300
305
  void (async () => {
301
306
  try {
302
307
  const result = await withRetry(() => tierFetcher.fetchTier(keys, "page"), retry);
303
- const fetched = applyResult(result);
304
- tierFetcher.notifyWaiters(result);
308
+ const fetched = applyResult(result.data);
309
+ if (result.version !== null)
310
+ persistVersion(result.version);
311
+ tierFetcher.notifyWaiters(result.data);
305
312
  emitUpdated(fetched, "page");
306
313
  }
307
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<Record<string, unknown>>;
153
+ fetchAll(): Promise<FetchResult>;
143
154
  /** Fetch specific keys only */
144
- fetchKeys(keys: string[]): Promise<Record<string, unknown>>;
155
+ fetchKeys(keys: string[]): Promise<FetchResult>;
145
156
  /** Check current config version (lightweight — single Firestore read) */
146
157
  checkVersion(): Promise<{
147
158
  version: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jewel998/config",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "private": false,
5
5
  "description": "Free, self-hosted feature flags and remote configuration SDK",
6
6
  "keywords": [