@jewel998/config 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -105,7 +105,7 @@ config.getFlag("feature.checkout_v2"); // Evaluated locally — no round-trip
105
105
  │ const flags = initConfig({ clientId, baseUrl }) │
106
106
  │ flags.get("feature.dark_mode") → true │
107
107
  └──────────────────────┬──────────────────────────────┘
108
- │ POST /api/getConfig
108
+ │ POST /api/v1/config
109
109
  ▼
110
110
  ┌──────────────────────────────────────────────────────┐
111
111
  │ Your Firebase Hosting (CDN cached) │
@@ -137,7 +137,7 @@ config.getFlag("feature.checkout_v2"); // Evaluated locally — no round-trip
137
137
 
138
138
  ## API Reference
139
139
 
140
- ### POST /api/getConfig
140
+ ### POST /api/v1/config
141
141
 
142
142
  The SDK calls this endpoint. Mode is determined by the API key prefix.
143
143
 
@@ -4,6 +4,11 @@ export declare class ConfigError extends Error {
4
4
  name: string;
5
5
  constructor(message: string, code: ConfigErrorCode);
6
6
  }
7
+ export declare class RateLimitError extends ConfigError {
8
+ /** Seconds to wait before retrying (from server Retry-After header) */
9
+ readonly retryAfterSeconds: number | undefined;
10
+ constructor(message: string, retryAfterSeconds?: number);
11
+ }
7
12
  export declare class InitializationError extends ConfigError {
8
13
  readonly cause?: Error | undefined;
9
14
  constructor(message: string, cause?: Error | undefined);
@@ -6,6 +6,15 @@ export class ConfigError extends Error {
6
6
  this.code = code;
7
7
  }
8
8
  }
9
+ export class RateLimitError extends ConfigError {
10
+ /** Seconds to wait before retrying (from server Retry-After header) */
11
+ retryAfterSeconds;
12
+ constructor(message, retryAfterSeconds) {
13
+ super(message, "RATE_LIMITED");
14
+ this.name = "RateLimitError";
15
+ this.retryAfterSeconds = retryAfterSeconds;
16
+ }
17
+ }
9
18
  export class InitializationError extends ConfigError {
10
19
  cause;
11
20
  constructor(message, cause) {
@@ -1,15 +1,15 @@
1
1
  export const createBatchFetcher = (transport) => ({
2
2
  async fetchAll() {
3
- const response = await transport.request("getConfig");
3
+ const response = await transport.request("v1/config");
4
4
  return response.data;
5
5
  },
6
6
  async fetchKeys(_keys) {
7
7
  // Batch mode always fetches everything
8
- const response = await transport.request("getConfig");
8
+ const response = await transport.request("v1/config");
9
9
  return response.data;
10
10
  },
11
11
  async checkVersion() {
12
- const response = await transport.request("getVersion");
12
+ const response = await transport.request("v1/version");
13
13
  return {
14
14
  version: String(response.version),
15
15
  changedKeys: response.changedKeys ?? [],
@@ -3,7 +3,7 @@ export const createProjectedFetcher = (transport) => {
3
3
  let batchPromise = null;
4
4
  return {
5
5
  async fetchAll() {
6
- const response = await transport.request("getConfig");
6
+ const response = await transport.request("v1/config");
7
7
  return response.data;
8
8
  },
9
9
  async fetchKeys(keys) {
@@ -17,7 +17,7 @@ export const createProjectedFetcher = (transport) => {
17
17
  pendingKeys = new Set();
18
18
  batchPromise = null;
19
19
  try {
20
- const response = await transport.request("getConfig", {
20
+ const response = await transport.request("v1/config", {
21
21
  keys: batchedKeys,
22
22
  });
23
23
  resolve(response.data);
@@ -39,7 +39,7 @@ export const createProjectedFetcher = (transport) => {
39
39
  return filtered;
40
40
  },
41
41
  async checkVersion() {
42
- const response = await transport.request("getVersion");
42
+ const response = await transport.request("v1/version");
43
43
  return {
44
44
  version: String(response.version),
45
45
  changedKeys: response.changedKeys ?? [],
package/dist/index.d.ts CHANGED
@@ -9,5 +9,5 @@ export { WEBHOOK_FORMATS, WEBHOOK_FORMAT_INFO, TEMPLATE_VARIABLES, WEBHOOK_EVENT
9
9
  export type { WebhookFormat, WebhookEventType, WebhookResourceCategory, } from "./webhook-formats.js";
10
10
  export { memoryStorage } from "./cache/memoryStorage.js";
11
11
  export { browserStorage } from "./cache/browserStorage.js";
12
- export { AuthenticationError, ConfigError, InitializationError, TimeoutError, } from "./errors/index.js";
12
+ export { AuthenticationError, ConfigError, InitializationError, RateLimitError, TimeoutError, } from "./errors/index.js";
13
13
  export type { ConfigErrorCode } from "./errors/index.js";
package/dist/index.js CHANGED
@@ -14,5 +14,5 @@ export { WEBHOOK_FORMATS, WEBHOOK_FORMAT_INFO, TEMPLATE_VARIABLES, WEBHOOK_EVENT
14
14
  export { memoryStorage } from "./cache/memoryStorage.js";
15
15
  export { browserStorage } from "./cache/browserStorage.js";
16
16
  // Errors
17
- export { AuthenticationError, ConfigError, InitializationError, TimeoutError, } from "./errors/index.js";
17
+ export { AuthenticationError, ConfigError, InitializationError, RateLimitError, TimeoutError, } from "./errors/index.js";
18
18
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- import type { ConfigEventCallback, ConfigEventType } from "./types.js";
1
+ import type { CacheStorage, ConfigEventCallback, ConfigEventType } from "./types.js";
2
2
  import type { EvaluationContext } from "./plugins/types.js";
3
3
  export interface InitConfigOptions {
4
4
  /** Required. API key from the portal (cid_ for client, svr_ for server). */
@@ -17,6 +17,19 @@ export interface InitConfigOptions {
17
17
  * @example "https://your-project.web.app/api"
18
18
  */
19
19
  baseUrl?: string;
20
+ /**
21
+ * Cache storage adapter. Controls where resolved config values are persisted.
22
+ *
23
+ * - `memoryStorage()` (default) — In-memory, lost on page reload
24
+ * - `browserStorage()` — localStorage, persists across page reloads and sessions
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import { browserStorage } from "@jewel998/config";
29
+ * initConfig({ storage: browserStorage({ prefix: "myapp" }) })
30
+ * ```
31
+ */
32
+ storage?: CacheStorage;
20
33
  /**
21
34
  * Polling interval in milliseconds for version checks.
22
35
  * The SDK periodically calls /api/version (lightweight ~100 bytes)
@@ -42,6 +42,7 @@ export function initConfig(options) {
42
42
  clientId: options.clientId,
43
43
  loadingStrategy: "optimistic",
44
44
  context: options.context,
45
+ storage: options.storage,
45
46
  baseUrl,
46
47
  });
47
48
  // Version-based polling: check /api/version periodically, delta-fetch on change
@@ -49,7 +50,7 @@ export function initConfig(options) {
49
50
  let pollTimer = null;
50
51
  async function checkVersion() {
51
52
  try {
52
- const res = await fetch(`${baseUrl}/getVersion?clientId=${options.clientId}`, {
53
+ const res = await fetch(`${baseUrl}/v1/version?clientId=${options.clientId}`, {
53
54
  headers: cachedVersion
54
55
  ? { "If-None-Match": `"${cachedVersion}"` }
55
56
  : {},
@@ -1,4 +1,4 @@
1
- import { AuthenticationError, ConfigError } from "../errors/index.js";
1
+ import { AuthenticationError, ConfigError, RateLimitError, } from "../errors/index.js";
2
2
  import { DEFAULT_RETRY } from "../types.js";
3
3
  export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4
4
  export const isNonRetryableError = (error) => {
@@ -20,6 +20,20 @@ export const isNonRetryableError = (error) => {
20
20
  }
21
21
  return false;
22
22
  };
23
+ /**
24
+ * Get the retry delay for an error. If the error is a RateLimitError with
25
+ * a server-specified Retry-After value, use that instead of calculated backoff.
26
+ */
27
+ const getRetryDelay = (error, attempt, config) => {
28
+ // Respect server-specified Retry-After for rate limit errors
29
+ if (error instanceof RateLimitError && error.retryAfterSeconds != null) {
30
+ return error.retryAfterSeconds * 1000;
31
+ }
32
+ // Default: exponential backoff with jitter
33
+ const delay = Math.min(config.baseDelay * Math.pow(config.multiplier, attempt), config.maxDelay);
34
+ // Add jitter (±25%) to prevent thundering herd
35
+ return delay * (0.75 + Math.random() * 0.5);
36
+ };
23
37
  export const withRetry = async (fn, options, onRetry) => {
24
38
  const config = { ...DEFAULT_RETRY, ...options };
25
39
  let lastError;
@@ -33,11 +47,9 @@ export const withRetry = async (fn, options, onRetry) => {
33
47
  throw error;
34
48
  }
35
49
  if (attempt < config.maxRetries) {
36
- const delay = Math.min(config.baseDelay * Math.pow(config.multiplier, attempt), config.maxDelay);
37
- // Add jitter (±25%) to prevent thundering herd
38
- const jitter = delay * (0.75 + Math.random() * 0.5);
39
- onRetry?.(attempt + 1, lastError, jitter);
40
- await sleep(jitter);
50
+ const delay = getRetryDelay(lastError, attempt, config);
51
+ onRetry?.(attempt + 1, lastError, delay);
52
+ await sleep(delay);
41
53
  }
42
54
  }
43
55
  }
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="dom" />
2
- import { AuthenticationError, ConfigError } from "../errors/index.js";
2
+ import { AuthenticationError, ConfigError, RateLimitError, } from "../errors/index.js";
3
3
  import { CircuitBreaker } from "./CircuitBreaker.js";
4
4
  /** Errors that should permanently stop retrying (circuit opens) */
5
5
  const FATAL_STATUS_CODES = new Set([400, 401, 403]);
@@ -68,9 +68,16 @@ export const createHttpTransport = (config) => {
68
68
  case 413:
69
69
  error = new ConfigError(message, "PAYLOAD_TOO_LARGE");
70
70
  break;
71
- case 429:
72
- error = new ConfigError(message, "RATE_LIMITED");
71
+ case 429: {
72
+ const retryAfterHeader = response.headers.get("Retry-After");
73
+ const retryAfterSeconds = retryAfterHeader
74
+ ? parseInt(retryAfterHeader, 10)
75
+ : undefined;
76
+ error = new RateLimitError(message, Number.isFinite(retryAfterSeconds)
77
+ ? retryAfterSeconds
78
+ : undefined);
73
79
  break;
80
+ }
74
81
  case 500:
75
82
  case 502:
76
83
  case 503:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jewel998/config",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,