@flaggy.io/sdk-js 1.0.0

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flaggy.io
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.
package/README.md ADDED
@@ -0,0 +1,333 @@
1
+ # Flaggy.io Feature Flag SDK
2
+
3
+ A JavaScript / TypeScript SDK for managing feature flags from Flaggy.io that is fully compatible for both client-side (browser) and server-side (Node.js) environments.
4
+
5
+ ## Features
6
+
7
+ - ✅ **Safe defaults**: Optional default values for graceful handling of new users and network failures
8
+ - ✅ **Auto-environment detection**: Uses localStorage in browsers, in-memory storage in Node.js
9
+ - ✅ **Automatic caching**: Reduces API calls and improves performance
10
+ - ✅ **Auto-refresh**: Refreshes on a 60-second base interval with exponential backoff on failures
11
+ - ✅ **Request deduplication**: Prevents concurrent duplicate API requests
12
+ - ✅ **TypeScript support**: Full type safety
13
+ - ✅ **Environment-based flags**: Support for production, staging, and development environments
14
+ - ✅ **Simple configuration**: Only requires an API key
15
+ - ✅ **Input validation**: Validates API keys and response data
16
+
17
+ ## Requirements
18
+
19
+ - **Node.js**: 18.0.0 or higher (required for native `fetch()` API support)
20
+ - **Browser**: Any modern browser with ES2020 support
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @flaggy.io/sdk-js
26
+ ```
27
+
28
+ ## Client-Side (React)
29
+
30
+ Create a dedicated file (e.g., `src/lib/flaggy.ts`):
31
+
32
+ ```typescript
33
+ import { flaggy } from "@flaggy.io/sdk-js";
34
+
35
+ export const flagClient = flaggy({
36
+ apiKey: import.meta.env.VITE_FLAGGY_API_KEY!,
37
+ environment: import.meta.env.VITE_ENVIRONMENT,
38
+ });
39
+
40
+ // Export a wrapper function for cleaner usage throughout your app
41
+ export function featureEnabled(
42
+ flagName: string,
43
+ defaultValue?: boolean,
44
+ ): boolean {
45
+ return flagClient.isEnabled(flagName, defaultValue);
46
+ }
47
+ ```
48
+
49
+ Initialize and wait for flags:
50
+
51
+ ```tsx
52
+ // src/main.tsx
53
+ import { StrictMode } from "react";
54
+ import { createRoot } from "react-dom/client";
55
+ import "./index.css";
56
+ import App from "./App";
57
+ import { flagClient } from "./lib/flaggy";
58
+
59
+ await flagClient.initialize();
60
+
61
+ createRoot(document.getElementById("root")!).render(
62
+ <StrictMode>
63
+ <App />
64
+ </StrictMode>,
65
+ );
66
+ ```
67
+
68
+ Simple usage example:
69
+
70
+ ```tsx
71
+ import { featureEnabled } from "./lib/flaggy";
72
+
73
+ export function Header() {
74
+ // Specify safe default for new users (before flags load)
75
+ return featureEnabled("new-header", false) ? <NewHeader /> : <OldHeader />;
76
+ }
77
+ ```
78
+
79
+ ## Server-Side (Node.js)
80
+
81
+ Create a dedicated client file (e.g., `src/lib/flaggy.ts`):
82
+
83
+ ```typescript
84
+ import { flaggy } from "@flaggy.io/sdk-js";
85
+
86
+ export const flagClient = flaggy({
87
+ apiKey: process.env.FLAGGY_API_KEY!,
88
+ environment: process.env.NODE_ENV,
89
+ });
90
+
91
+ // Export a wrapper function for cleaner usage
92
+ export function featureEnabled(
93
+ flagName: string,
94
+ defaultValue?: boolean,
95
+ ): boolean {
96
+ return flagClient.isEnabled(flagName, defaultValue);
97
+ }
98
+ ```
99
+
100
+ Then in your server startup file (e.g., `src/server.ts` or `src/index.ts`), **await the initial flag load before starting your server**:
101
+
102
+ ```typescript
103
+ import { featureEnabled, flagClient } from "./lib/flaggy";
104
+ import express, { Application, Request, Response } from "express";
105
+
106
+ const app: Application = express();
107
+ const port = process.env.PORT || 3000;
108
+
109
+ app.get("/", (req: Request, res: Response) => {
110
+ if (featureEnabled("new-service")) {
111
+ // Logic for the new service
112
+ } else {
113
+ // Logic for the old service
114
+ }
115
+ });
116
+
117
+ async function start(): Promise<void> {
118
+ await flagClient.initialize();
119
+
120
+ app.listen(port, () => {
121
+ console.log(`Server is Fire at http://localhost:${port}`);
122
+ });
123
+ }
124
+
125
+ void start();
126
+ ```
127
+
128
+ Use throughout your application:
129
+
130
+ ```typescript
131
+ import { featureEnabled } from "./lib/flaggy";
132
+
133
+ if (featureEnabled("new-algorithm", true)) {
134
+ // Defaulting to enabled
135
+ }
136
+ ```
137
+
138
+ To react to fetch errors without throwing, provide an `onError` callback:
139
+
140
+ ```typescript
141
+ const flagClient = flaggy({
142
+ apiKey: import.meta.env.VITE_FLAGGY_API_KEY!,
143
+ onError: (error) => {
144
+ console.warn("Flag fetch failed:", error);
145
+ },
146
+ });
147
+ ```
148
+
149
+ **Environment:** The `environment` field is optional and defaults to `"production"`. You can pass raw strings (for example `process.env.NODE_ENV` / `import.meta.env.VITE_ENVIRONMENT`), and the SDK will only use `"production"`, `"staging"`, or `"development"`—any other value falls back to `"production"`.
150
+
151
+ ```typescript
152
+ // Omit entirely (uses "production")
153
+ const flagClient = flaggy({
154
+ apiKey: import.meta.env.VITE_FLAGGY_API_KEY!,
155
+ });
156
+
157
+ // Or pass env directly (SDK normalizes invalid values to "production")
158
+ environment: import.meta.env.VITE_ENVIRONMENT,
159
+ ```
160
+
161
+ **Network Errors:** The SDK logs fetch errors to the console and invokes the `onError` callback if provided. If the API is unreachable, the SDK will continue to use cached flags and retry on the next refresh interval using exponential backoff (base 60 seconds, capped at 15 minutes).
162
+
163
+ **Timeout:** Requests are aborted after 2 seconds to avoid long stalls.
164
+
165
+ ## API Reference
166
+
167
+ ### Configuration
168
+
169
+ The SDK uses the following hard-coded values:
170
+
171
+ - **API URL**: `https://api.flaggy.io/public/feature-flags`
172
+ - **Storage Key**: `feature-flags`
173
+ - **Refresh Interval**: 60-second base with exponential backoff on failures (capped at 15 minutes)
174
+ - **Authentication**: `x-api-key` header
175
+
176
+ ### Feature Flag Structure
177
+
178
+ Each feature flag has the following structure:
179
+
180
+ ```typescript
181
+ interface FeatureFlag {
182
+ key: string;
183
+ enabled_production: boolean;
184
+ enabled_staging: boolean;
185
+ enabled_development: boolean;
186
+ }
187
+ ```
188
+
189
+ This allows the same flag to have different states across different environments.
190
+
191
+ ### `flaggy(config: FeatureFlagConfig): FeatureFlagClient`
192
+
193
+ Initialize the global feature flag client with your API key and environment.
194
+
195
+ **Factory-only:** `FeatureFlagClient` is not exported, so clients must be created via `flaggy()`.
196
+
197
+ **Singleton Behavior:** Multiple calls to `flaggy()` return the same instance. This makes it safe to call in React components that re-render, and prevents duplicate API requests and race conditions.
198
+
199
+ **Auto-initialization:** Flags are automatically fetched in the background when you instantiate the client via `flaggy()`. The client loads from cache immediately if available and auto-refreshes on a 60-second base interval with exponential backoff on failures.
200
+
201
+ **React / Client-Side:** Call `await client.initialize()` before rendering your app. If cached flags exist in localStorage, `initialize()` returns immediately without blocking—preventing flash screens on page refresh. If no cache exists, it waits up to 2 seconds for the initial fetch.
202
+
203
+ **Node.js / Server Startup:** Call `await client.initialize()` before starting your server. Since the in-memory cache is empty on server restart, `initialize()` will wait up to 2 seconds for the initial fetch to ensure flags are available before accepting requests.
204
+
205
+ **Config Options:**
206
+
207
+ - `apiKey` (required): Your Flaggy.io API key for authentication
208
+ - `environment` (optional): Accepts any string input (e.g. `process.env.NODE_ENV`), but only `'production'`, `'staging'`, and `'development'` are used; all other values default to `'production'`.
209
+ - `onError` (optional): Callback invoked when a flag fetch fails. Does not throw.
210
+
211
+ **Returns:** `FeatureFlagClient` instance
212
+
213
+ **Important:** Configuration is locked after the first call. Subsequent calls with different config values will return the existing instance with the original configuration.
214
+
215
+ ## Client API (returned by `flaggy()`)
216
+
217
+ **Caching behavior:**
218
+
219
+ - **Client-side (React/browser):** Flags are loaded from localStorage immediately on instantiation. If cache is stale (> 60 seconds), a background fetch updates the flags without blocking.
220
+ - **Server-side (Node.js):** In-memory cache is empty on server restart, so `initialize()` should be awaited to ensure flags are loaded before serving requests.
221
+ - All flag evaluations (`isEnabled()`, `getFlag()`) trigger automatic background refreshes when the cache interval expires.
222
+
223
+ ### `isEnabled(flagName: string, defaultValue?: boolean): boolean`
224
+
225
+ Check if a feature flag is enabled for the configured environment.
226
+
227
+ **Parameters:**
228
+
229
+ - `flagName`: The key of the feature flag to check
230
+ - `defaultValue`: Optional default value to return if flag doesn't exist or isn't loaded yet (defaults to `false`)
231
+
232
+ **Returns:** `boolean` - Whether the flag is enabled
233
+
234
+ **Example:**
235
+
236
+ ```typescript
237
+ // Safe default for new users (flag not loaded yet)
238
+ if (flagClient.isEnabled("new-ui", false)) {
239
+ // Show new UI only if flag exists and is enabled
240
+ }
241
+
242
+ // Default to enabled for opt-out features
243
+ if (flagClient.isEnabled("analytics", true)) {
244
+ // Analytics enabled unless explicitly disabled
245
+ }
246
+ ```
247
+
248
+ **Best Practice:** Always specify a safe default value that provides the correct experience for new users before flags load, or during network failures.
249
+
250
+ ### `getFlag(flagName: string): FeatureFlag | undefined`
251
+
252
+ Get the full feature flag object with all environment states.
253
+
254
+ ### `getAllFlags(): FeatureFlag[]`
255
+
256
+ Get all cached feature flags.
257
+
258
+ ### `initialize(): Promise<void>`
259
+
260
+ Initialize the feature flag client. **This is the recommended method to call during application startup.**
261
+
262
+ **Behavior:**
263
+
264
+ - **If cached flags exist** (React/browser with localStorage): Returns immediately without fetching or blocking
265
+ - **If no cache exists** (Node.js server startup or first-time browser load): Waits up to 2 seconds for the initial fetch, then resolves even if the request hasn't finished
266
+
267
+ ```typescript
268
+ // React: instant return if cache exists, prevents flash screen
269
+ await flagClient.initialize();
270
+
271
+ // Node.js: waits for initial fetch since cache is always empty on restart
272
+ await flagClient.initialize();
273
+ ```
274
+
275
+ **Use cases:**
276
+
277
+ - React/browser: Prevents blocking on page refresh when flags are cached
278
+ - Node.js: Ensures flags are loaded before accepting requests
279
+
280
+ ### `async fetchFlags(): Promise<void>`
281
+
282
+ Manually fetch flags from the API. Use this for manual refreshes after the initial setup.
283
+
284
+ **Rate Limiting:** Respects the refresh interval. Multiple calls within the interval will return immediately without fetching. The interval uses a 60-second base and exponential backoff on failures (capped at 15 minutes).
285
+
286
+ **Use cases:**
287
+
288
+ - Manual refresh outside the automatic 60-second interval
289
+ - Testing scenarios where you need deterministic flag state
290
+
291
+ **Note:** `fetchFlags()` always respects the refresh interval. There is no force-refresh API; after calling `clearCache()`, the SDK will refresh on the next interval or when `initialize()` decides a fetch is needed.
292
+
293
+ ### `clearCache(): void`
294
+
295
+ Clear all cached flags.
296
+
297
+ ### `getRefreshIntervalMs(): number`
298
+
299
+ Returns the current refresh interval in milliseconds. The SDK uses a 60-second base interval and applies exponential backoff on failures, capped at 15 minutes.
300
+
301
+ ## API Response Format
302
+
303
+ The Flaggy.io API returns feature flags in the following format:
304
+
305
+ ```json
306
+ {
307
+ "data": [
308
+ {
309
+ "key": "dark-mode",
310
+ "enabled_production": true,
311
+ "enabled_staging": true,
312
+ "enabled_development": true
313
+ },
314
+ {
315
+ "key": "new-feature",
316
+ "enabled_production": false,
317
+ "enabled_staging": true,
318
+ "enabled_development": true
319
+ }
320
+ ]
321
+ }
322
+ ```
323
+
324
+ The SDK automatically:
325
+
326
+ - Validates the API response structure
327
+ - Filters out invalid flags
328
+ - Checks the appropriate `enabled_*` field based on your configured environment
329
+ - Sends your API key in the `x-api-key` header
330
+
331
+ ## License
332
+
333
+ MIT © Flaggy.io
package/dist/index.cjs ADDED
@@ -0,0 +1,243 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var LocalStorageAdapter = class {
5
+ get(key) {
6
+ if (typeof window === "undefined" || !window.localStorage) {
7
+ return null;
8
+ }
9
+ try {
10
+ const data = window.localStorage.getItem(key);
11
+ return data ? JSON.parse(data) : null;
12
+ } catch (error) {
13
+ console.error("Error reading from localStorage:", error);
14
+ return null;
15
+ }
16
+ }
17
+ set(key, value) {
18
+ if (typeof window === "undefined" || !window.localStorage) {
19
+ return;
20
+ }
21
+ try {
22
+ window.localStorage.setItem(key, JSON.stringify(value));
23
+ } catch (error) {
24
+ console.error("Error writing to localStorage:", error);
25
+ }
26
+ }
27
+ clear(key) {
28
+ if (typeof window === "undefined" || !window.localStorage) {
29
+ return;
30
+ }
31
+ try {
32
+ window.localStorage.removeItem(key);
33
+ } catch (error) {
34
+ console.error("Error clearing localStorage:", error);
35
+ }
36
+ }
37
+ };
38
+ var InMemoryStorageAdapter = class {
39
+ constructor() {
40
+ this.store = /* @__PURE__ */ new Map();
41
+ }
42
+ get(key) {
43
+ return this.store.get(key) || null;
44
+ }
45
+ set(key, value) {
46
+ this.store.set(key, value);
47
+ }
48
+ clear(key) {
49
+ this.store.delete(key);
50
+ }
51
+ };
52
+ var FeatureFlagClient = class {
53
+ constructor(config) {
54
+ this.flags = [];
55
+ this.lastFetch = 0;
56
+ this.isFetching = false;
57
+ this.fetchPromise = null;
58
+ this.failureCount = 0;
59
+ this.baseRefreshMs = 6e4;
60
+ this.maxRefreshMs = 15 * 60 * 1e3;
61
+ if (!config.apiKey || typeof config.apiKey !== "string") {
62
+ throw new Error("API key is required and must be a non-empty string");
63
+ }
64
+ if (config.apiKey.trim().length === 0) {
65
+ throw new Error("API key cannot be empty or whitespace");
66
+ }
67
+ this.config = config;
68
+ this.environment = config.environment === "production" || config.environment === "staging" || config.environment === "development" ? config.environment : "production";
69
+ this.storage = this.detectEnvironment() ? new LocalStorageAdapter() : new InMemoryStorageAdapter();
70
+ this.loadFromCache();
71
+ this.fetchFlags();
72
+ }
73
+ detectEnvironment() {
74
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
75
+ }
76
+ loadFromCache() {
77
+ const cached = this.storage.get("feature-flags");
78
+ if (cached && this.isValidCachedData(cached)) {
79
+ this.flags = cached.flags;
80
+ this.lastFetch = cached.timestamp || 0;
81
+ }
82
+ }
83
+ isValidCachedData(data) {
84
+ return data && typeof data === "object" && Array.isArray(data.flags) && data.flags.every(this.isValidFeatureFlag) && typeof data.timestamp === "number";
85
+ }
86
+ isValidFeatureFlag(flag) {
87
+ return flag && typeof flag === "object" && typeof flag.key === "string" && typeof flag.enabled_production === "boolean" && typeof flag.enabled_staging === "boolean" && typeof flag.enabled_development === "boolean";
88
+ }
89
+ saveToCache() {
90
+ this.storage.set("feature-flags", {
91
+ flags: this.flags,
92
+ timestamp: this.lastFetch
93
+ });
94
+ }
95
+ // Check if valid flags exist in cache
96
+ hasCachedFlags() {
97
+ const cached = this.storage.get("feature-flags");
98
+ return cached ? this.isValidCachedData(cached) : false;
99
+ }
100
+ // Check if cache interval has elapsed and a fetch is needed
101
+ shouldFetch() {
102
+ const now = Date.now();
103
+ return now - this.lastFetch > this.getRefreshIntervalMs();
104
+ }
105
+ // Exponential backoff: base * 2^failureCount (capped)
106
+ getRefreshIntervalMs() {
107
+ const backoffMs = this.baseRefreshMs * Math.pow(2, this.failureCount);
108
+ return Math.min(backoffMs, this.maxRefreshMs);
109
+ }
110
+ async fetchFlags() {
111
+ if (!this.shouldFetch()) {
112
+ return;
113
+ }
114
+ if (this.isFetching && this.fetchPromise) {
115
+ return this.fetchPromise;
116
+ }
117
+ this.isFetching = true;
118
+ this.fetchPromise = this.doFetch();
119
+ try {
120
+ await this.fetchPromise;
121
+ } finally {
122
+ this.isFetching = false;
123
+ this.fetchPromise = null;
124
+ }
125
+ }
126
+ async doFetch() {
127
+ const controller = new AbortController();
128
+ const timeoutId = setTimeout(() => controller.abort(), 2e3);
129
+ try {
130
+ const response = await fetch(
131
+ "https://api.flaggy.io/public/feature-flags",
132
+ {
133
+ method: "GET",
134
+ headers: {
135
+ "x-api-key": this.config.apiKey,
136
+ "Content-Type": "application/json"
137
+ },
138
+ signal: controller.signal
139
+ }
140
+ );
141
+ if (!response.ok) {
142
+ throw new Error(
143
+ `Failed to fetch feature flags: ${response.status} ${response.statusText}`
144
+ );
145
+ }
146
+ const json = await response.json();
147
+ if (!json || typeof json !== "object") {
148
+ throw new Error("Invalid API response: expected JSON object");
149
+ }
150
+ if (!json.data) {
151
+ throw new Error("Invalid API response: missing data property");
152
+ }
153
+ const data = json.data;
154
+ if (!Array.isArray(data)) {
155
+ throw new Error("Invalid API response: data must be an array");
156
+ }
157
+ this.flags = data.filter(this.isValidFeatureFlag);
158
+ if (data.length > 0 && this.flags.length === 0) {
159
+ console.warn(
160
+ "All feature flags in API response were invalid and filtered out"
161
+ );
162
+ }
163
+ this.lastFetch = Date.now();
164
+ this.failureCount = 0;
165
+ this.saveToCache();
166
+ } catch (error) {
167
+ this.failureCount += 1;
168
+ const err = error;
169
+ if (this.config.onError) {
170
+ this.config.onError(err);
171
+ }
172
+ if (err.name === "AbortError") {
173
+ console.warn("Feature flag fetch timed out");
174
+ return;
175
+ }
176
+ console.error("Error fetching feature flags:", err);
177
+ } finally {
178
+ clearTimeout(timeoutId);
179
+ }
180
+ }
181
+ initialize() {
182
+ if (this.hasCachedFlags()) {
183
+ return Promise.resolve();
184
+ }
185
+ if (!this.shouldFetch()) {
186
+ return Promise.resolve();
187
+ }
188
+ const timeoutMs = 2e3;
189
+ return Promise.race([
190
+ this.fetchFlags(),
191
+ new Promise((resolve) => {
192
+ setTimeout(resolve, timeoutMs);
193
+ })
194
+ ]);
195
+ }
196
+ isEnabled(flagName, defaultValue = false) {
197
+ if (this.shouldFetch() && !this.isFetching) {
198
+ this.fetchFlags();
199
+ }
200
+ const flag = this.flags.find((f) => f.key === flagName);
201
+ if (!flag) {
202
+ return defaultValue;
203
+ }
204
+ switch (this.environment) {
205
+ case "production":
206
+ return flag.enabled_production;
207
+ case "staging":
208
+ return flag.enabled_staging;
209
+ case "development":
210
+ return flag.enabled_development;
211
+ default:
212
+ return defaultValue;
213
+ }
214
+ }
215
+ getFlag(flagName) {
216
+ if (this.shouldFetch() && !this.isFetching) {
217
+ this.fetchFlags();
218
+ }
219
+ return this.flags.find((f) => f.key === flagName);
220
+ }
221
+ getAllFlags() {
222
+ return [...this.flags];
223
+ }
224
+ clearCache() {
225
+ this.storage.clear("feature-flags");
226
+ this.flags = [];
227
+ }
228
+ // Set custom storage adapter (useful for testing or custom implementations)
229
+ setStorageAdapter(adapter) {
230
+ this.storage = adapter;
231
+ this.loadFromCache();
232
+ }
233
+ };
234
+ var globalClient = null;
235
+ function flaggy(config) {
236
+ if (globalClient) return globalClient;
237
+ globalClient = new FeatureFlagClient(config);
238
+ return globalClient;
239
+ }
240
+
241
+ exports.flaggy = flaggy;
242
+ //# sourceMappingURL=index.cjs.map
243
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AA2BA,IAAM,sBAAN,MAAoD;AAAA,EAClD,IAAI,GAAA,EAAgC;AAClC,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,OAAO,YAAA,EAAc;AACzD,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAO,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,GAAG,CAAA;AAC5C,MAAA,OAAO,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AAAA,IACnC,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,oCAAoC,KAAK,CAAA;AACvD,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,GAAA,CAAI,KAAa,KAAA,EAAyB;AACxC,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,OAAO,YAAA,EAAc;AACzD,MAAA;AAAA,IACF;AACA,IAAA,IAAI;AACF,MAAA,MAAA,CAAO,aAAa,OAAA,CAAQ,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC,CAAA;AAAA,IACxD,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,kCAAkC,KAAK,CAAA;AAAA,IACvD;AAAA,EACF;AAAA,EAEA,MAAM,GAAA,EAAmB;AACvB,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,OAAO,YAAA,EAAc;AACzD,MAAA;AAAA,IACF;AACA,IAAA,IAAI;AACF,MAAA,MAAA,CAAO,YAAA,CAAa,WAAW,GAAG,CAAA;AAAA,IACpC,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,gCAAgC,KAAK,CAAA;AAAA,IACrD;AAAA,EACF;AACF,CAAA;AAGA,IAAM,yBAAN,MAAuD;AAAA,EAAvD,WAAA,GAAA;AACE,IAAA,IAAA,CAAQ,KAAA,uBAAqC,GAAA,EAAI;AAAA,EAAA;AAAA,EAEjD,IAAI,GAAA,EAAgC;AAClC,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA,IAAK,IAAA;AAAA,EAChC;AAAA,EAEA,GAAA,CAAI,KAAa,KAAA,EAAyB;AACxC,IAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAAA,EAC3B;AAAA,EAEA,MAAM,GAAA,EAAmB;AACvB,IAAA,IAAA,CAAK,KAAA,CAAM,OAAO,GAAG,CAAA;AAAA,EACvB;AACF,CAAA;AAEA,IAAM,oBAAN,MAAwB;AAAA,EAYtB,YAAY,MAAA,EAA2B;AATvC,IAAA,IAAA,CAAQ,QAAuB,EAAC;AAChC,IAAA,IAAA,CAAQ,SAAA,GAAoB,CAAA;AAC5B,IAAA,IAAA,CAAQ,UAAA,GAAsB,KAAA;AAC9B,IAAA,IAAA,CAAQ,YAAA,GAAqC,IAAA;AAE7C,IAAA,IAAA,CAAQ,YAAA,GAAuB,CAAA;AAC/B,IAAA,IAAA,CAAiB,aAAA,GAAwB,GAAA;AACzC,IAAA,IAAA,CAAiB,YAAA,GAAuB,KAAK,EAAA,GAAK,GAAA;AAIhD,IAAA,IAAI,CAAC,MAAA,CAAO,MAAA,IAAU,OAAO,MAAA,CAAO,WAAW,QAAA,EAAU;AACvD,MAAA,MAAM,IAAI,MAAM,oDAAoD,CAAA;AAAA,IACtE;AAEA,IAAA,IAAI,MAAA,CAAO,MAAA,CAAO,IAAA,EAAK,CAAE,WAAW,CAAA,EAAG;AACrC,MAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,IACzD;AAEA,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,WAAA,GACH,MAAA,CAAO,WAAA,KAAgB,YAAA,IACvB,MAAA,CAAO,WAAA,KAAgB,SAAA,IACvB,MAAA,CAAO,WAAA,KAAgB,aAAA,GACnB,MAAA,CAAO,WAAA,GACP,YAAA;AAGN,IAAA,IAAA,CAAK,OAAA,GAAU,KAAK,iBAAA,EAAkB,GAClC,IAAI,mBAAA,EAAoB,GACxB,IAAI,sBAAA,EAAuB;AAG/B,IAAA,IAAA,CAAK,aAAA,EAAc;AAGnB,IAAA,IAAA,CAAK,UAAA,EAAW;AAAA,EAClB;AAAA,EAEQ,iBAAA,GAA6B;AACnC,IAAA,OACE,OAAO,MAAA,KAAW,WAAA,IAClB,OAAO,OAAO,YAAA,KAAiB,WAAA;AAAA,EAEnC;AAAA,EAEQ,aAAA,GAAsB;AAC5B,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA;AAC/C,IAAA,IAAI,MAAA,IAAU,IAAA,CAAK,iBAAA,CAAkB,MAAM,CAAA,EAAG;AAC5C,MAAA,IAAA,CAAK,QAAQ,MAAA,CAAO,KAAA;AACpB,MAAA,IAAA,CAAK,SAAA,GAAY,OAAO,SAAA,IAAa,CAAA;AAAA,IACvC;AAAA,EACF;AAAA,EAEQ,kBAAkB,IAAA,EAA+B;AACvD,IAAA,OACE,QACA,OAAO,IAAA,KAAS,QAAA,IAChB,KAAA,CAAM,QAAQ,IAAA,CAAK,KAAK,CAAA,IACxB,IAAA,CAAK,MAAM,KAAA,CAAM,IAAA,CAAK,kBAAkB,CAAA,IACxC,OAAO,KAAK,SAAA,KAAc,QAAA;AAAA,EAE9B;AAAA,EAEQ,mBAAmB,IAAA,EAAgC;AACzD,IAAA,OACE,QACA,OAAO,IAAA,KAAS,YAChB,OAAO,IAAA,CAAK,QAAQ,QAAA,IACpB,OAAO,IAAA,CAAK,kBAAA,KAAuB,aACnC,OAAO,IAAA,CAAK,oBAAoB,SAAA,IAChC,OAAO,KAAK,mBAAA,KAAwB,SAAA;AAAA,EAExC;AAAA,EAEQ,WAAA,GAAoB;AAC1B,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAI,eAAA,EAAiB;AAAA,MAChC,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,WAAW,IAAA,CAAK;AAAA,KACjB,CAAA;AAAA,EACH;AAAA;AAAA,EAGQ,cAAA,GAA0B;AAChC,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA;AAC/C,IAAA,OAAO,MAAA,GAAS,IAAA,CAAK,iBAAA,CAAkB,MAAM,CAAA,GAAI,KAAA;AAAA,EACnD;AAAA;AAAA,EAGQ,WAAA,GAAuB;AAC7B,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,OAAO,GAAA,GAAM,IAAA,CAAK,SAAA,GAAY,IAAA,CAAK,oBAAA,EAAqB;AAAA,EAC1D;AAAA;AAAA,EAGA,oBAAA,GAA+B;AAC7B,IAAA,MAAM,YAAY,IAAA,CAAK,aAAA,GAAgB,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,YAAY,CAAA;AACpE,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,SAAA,EAAW,IAAA,CAAK,YAAY,CAAA;AAAA,EAC9C;AAAA,EAEA,MAAM,UAAA,GAA4B;AAChC,IAAA,IAAI,CAAC,IAAA,CAAK,WAAA,EAAY,EAAG;AACvB,MAAA;AAAA,IACF;AAGA,IAAA,IAAI,IAAA,CAAK,UAAA,IAAc,IAAA,CAAK,YAAA,EAAc;AACxC,MAAA,OAAO,IAAA,CAAK,YAAA;AAAA,IACd;AAEA,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,YAAA,GAAe,KAAK,OAAA,EAAQ;AAEjC,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK,YAAA;AAAA,IACb,CAAA,SAAE;AACA,MAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,MAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAc,OAAA,GAAyB;AACrC,IAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,IAAA,MAAM,YAAY,UAAA,CAAW,MAAM,UAAA,CAAW,KAAA,IAAS,GAAI,CAAA;AAE3D,IAAA,IAAI;AACF,MAAA,MAAM,WAAW,MAAM,KAAA;AAAA,QACrB,4CAAA;AAAA,QACA;AAAA,UACE,MAAA,EAAQ,KAAA;AAAA,UACR,OAAA,EAAS;AAAA,YACP,WAAA,EAAa,KAAK,MAAA,CAAO,MAAA;AAAA,YACzB,cAAA,EAAgB;AAAA,WAClB;AAAA,UACA,QAAQ,UAAA,CAAW;AAAA;AACrB,OACF;AAEA,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,+BAAA,EAAkC,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,SAAS,UAAU,CAAA;AAAA,SAC1E;AAAA,MACF;AAEA,MAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAGjC,MAAA,IAAI,CAAC,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,EAAU;AACrC,QAAA,MAAM,IAAI,MAAM,4CAA4C,CAAA;AAAA,MAC9D;AAEA,MAAA,IAAI,CAAC,KAAK,IAAA,EAAM;AACd,QAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,MAC/D;AAEA,MAAA,MAAM,OAAO,IAAA,CAAK,IAAA;AAGlB,MAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,EAAG;AACxB,QAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,MAC/D;AAGA,MAAA,IAAA,CAAK,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,kBAAkB,CAAA;AAEhD,MAAA,IAAI,KAAK,MAAA,GAAS,CAAA,IAAK,IAAA,CAAK,KAAA,CAAM,WAAW,CAAA,EAAG;AAC9C,QAAA,OAAA,CAAQ,IAAA;AAAA,UACN;AAAA,SACF;AAAA,MACF;AAEA,MAAA,IAAA,CAAK,SAAA,GAAY,KAAK,GAAA,EAAI;AAC1B,MAAA,IAAA,CAAK,YAAA,GAAe,CAAA;AACpB,MAAA,IAAA,CAAK,WAAA,EAAY;AAAA,IACnB,SAAS,KAAA,EAAO;AACd,MAAA,IAAA,CAAK,YAAA,IAAgB,CAAA;AACrB,MAAA,MAAM,GAAA,GAAM,KAAA;AACZ,MAAA,IAAI,IAAA,CAAK,OAAO,OAAA,EAAS;AACvB,QAAA,IAAA,CAAK,MAAA,CAAO,QAAQ,GAAG,CAAA;AAAA,MACzB;AACA,MAAA,IAAI,GAAA,CAAI,SAAS,YAAA,EAAc;AAC7B,QAAA,OAAA,CAAQ,KAAK,8BAA8B,CAAA;AAC3C,QAAA;AAAA,MACF;AACA,MAAA,OAAA,CAAQ,KAAA,CAAM,iCAAiC,GAAG,CAAA;AAAA,IACpD,CAAA,SAAE;AACA,MAAA,YAAA,CAAa,SAAS,CAAA;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,UAAA,GAA4B;AAC1B,IAAA,IAAI,IAAA,CAAK,gBAAe,EAAG;AACzB,MAAA,OAAO,QAAQ,OAAA,EAAQ;AAAA,IACzB;AAEA,IAAA,IAAI,CAAC,IAAA,CAAK,WAAA,EAAY,EAAG;AACvB,MAAA,OAAO,QAAQ,OAAA,EAAQ;AAAA,IACzB;AAEA,IAAA,MAAM,SAAA,GAAY,GAAA;AAClB,IAAA,OAAO,QAAQ,IAAA,CAAK;AAAA,MAClB,KAAK,UAAA,EAAW;AAAA,MAChB,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AAC7B,QAAA,UAAA,CAAW,SAAS,SAAS,CAAA;AAAA,MAC/B,CAAC;AAAA,KACF,CAAA;AAAA,EACH;AAAA,EAEA,SAAA,CAAU,QAAA,EAAkB,YAAA,GAAwB,KAAA,EAAgB;AAElE,IAAA,IAAI,IAAA,CAAK,WAAA,EAAY,IAAK,CAAC,KAAK,UAAA,EAAY;AAC1C,MAAA,IAAA,CAAK,UAAA,EAAW;AAAA,IAClB;AAEA,IAAA,MAAM,IAAA,GAAO,KAAK,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,QAAQ,QAAQ,CAAA;AACtD,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAO,YAAA;AAAA,IACT;AAGA,IAAA,QAAQ,KAAK,WAAA;AAAa,MACxB,KAAK,YAAA;AACH,QAAA,OAAO,IAAA,CAAK,kBAAA;AAAA,MACd,KAAK,SAAA;AACH,QAAA,OAAO,IAAA,CAAK,eAAA;AAAA,MACd,KAAK,aAAA;AACH,QAAA,OAAO,IAAA,CAAK,mBAAA;AAAA,MACd;AACE,QAAA,OAAO,YAAA;AAAA;AACX,EACF;AAAA,EAEA,QAAQ,QAAA,EAA2C;AAEjD,IAAA,IAAI,IAAA,CAAK,WAAA,EAAY,IAAK,CAAC,KAAK,UAAA,EAAY;AAC1C,MAAA,IAAA,CAAK,UAAA,EAAW;AAAA,IAClB;AAEA,IAAA,OAAO,KAAK,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,QAAQ,QAAQ,CAAA;AAAA,EAClD;AAAA,EAEA,WAAA,GAA6B;AAC3B,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,KAAK,CAAA;AAAA,EACvB;AAAA,EAEA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,OAAA,CAAQ,MAAM,eAAe,CAAA;AAClC,IAAA,IAAA,CAAK,QAAQ,EAAC;AAAA,EAChB;AAAA;AAAA,EAGA,kBAAkB,OAAA,EAA+B;AAC/C,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AACf,IAAA,IAAA,CAAK,aAAA,EAAc;AAAA,EACrB;AACF,CAAA;AAEA,IAAI,YAAA,GAAyC,IAAA;AAEtC,SAAS,OAAO,MAAA,EAA8C;AACnE,EAAA,IAAI,cAAc,OAAO,YAAA;AAEzB,EAAA,YAAA,GAAe,IAAI,kBAAkB,MAAM,CAAA;AAC3C,EAAA,OAAO,YAAA;AACT","file":"index.cjs","sourcesContent":["export interface FeatureFlag {\n key: string;\n enabled_production: boolean;\n enabled_staging: boolean;\n enabled_development: boolean;\n}\n\nexport interface FeatureFlagConfig {\n apiKey: string;\n environment?: \"production\" | \"staging\" | \"development\" | string;\n onError?: (error: Error) => void;\n}\n\n// Storage adapter interface for custom implementations\nexport interface StorageAdapter {\n get(key: string): CachedData | null;\n set(key: string, value: CachedData): void;\n clear(key: string): void;\n}\n\n// Storage adapter interface\ninterface CachedData {\n flags: FeatureFlag[];\n timestamp: number;\n}\n\n// LocalStorage adapter for browser\nclass LocalStorageAdapter implements StorageAdapter {\n get(key: string): CachedData | null {\n if (typeof window === \"undefined\" || !window.localStorage) {\n return null;\n }\n try {\n const data = window.localStorage.getItem(key);\n return data ? JSON.parse(data) : null;\n } catch (error) {\n console.error(\"Error reading from localStorage:\", error);\n return null;\n }\n }\n\n set(key: string, value: CachedData): void {\n if (typeof window === \"undefined\" || !window.localStorage) {\n return;\n }\n try {\n window.localStorage.setItem(key, JSON.stringify(value));\n } catch (error) {\n console.error(\"Error writing to localStorage:\", error);\n }\n }\n\n clear(key: string): void {\n if (typeof window === \"undefined\" || !window.localStorage) {\n return;\n }\n try {\n window.localStorage.removeItem(key);\n } catch (error) {\n console.error(\"Error clearing localStorage:\", error);\n }\n }\n}\n\n// In-memory adapter for server-side\nclass InMemoryStorageAdapter implements StorageAdapter {\n private store: Map<string, CachedData> = new Map();\n\n get(key: string): CachedData | null {\n return this.store.get(key) || null;\n }\n\n set(key: string, value: CachedData): void {\n this.store.set(key, value);\n }\n\n clear(key: string): void {\n this.store.delete(key);\n }\n}\n\nclass FeatureFlagClient {\n private config: FeatureFlagConfig;\n private storage: StorageAdapter;\n private flags: FeatureFlag[] = [];\n private lastFetch: number = 0;\n private isFetching: boolean = false;\n private fetchPromise: Promise<void> | null = null;\n private environment: \"production\" | \"staging\" | \"development\";\n private failureCount: number = 0;\n private readonly baseRefreshMs: number = 60000;\n private readonly maxRefreshMs: number = 15 * 60 * 1000;\n\n constructor(config: FeatureFlagConfig) {\n // Validate API key\n if (!config.apiKey || typeof config.apiKey !== \"string\") {\n throw new Error(\"API key is required and must be a non-empty string\");\n }\n\n if (config.apiKey.trim().length === 0) {\n throw new Error(\"API key cannot be empty or whitespace\");\n }\n\n this.config = config;\n this.environment =\n config.environment === \"production\" ||\n config.environment === \"staging\" ||\n config.environment === \"development\"\n ? config.environment\n : \"production\";\n\n // Auto-detect environment and use appropriate storage\n this.storage = this.detectEnvironment()\n ? new LocalStorageAdapter()\n : new InMemoryStorageAdapter();\n\n // Load cached flags on initialization\n this.loadFromCache();\n\n // Auto-fetch flags in background (fire and forget)\n this.fetchFlags();\n }\n\n private detectEnvironment(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof window.localStorage !== \"undefined\"\n );\n }\n\n private loadFromCache(): void {\n const cached = this.storage.get(\"feature-flags\");\n if (cached && this.isValidCachedData(cached)) {\n this.flags = cached.flags;\n this.lastFetch = cached.timestamp || 0;\n }\n }\n\n private isValidCachedData(data: any): data is CachedData {\n return (\n data &&\n typeof data === \"object\" &&\n Array.isArray(data.flags) &&\n data.flags.every(this.isValidFeatureFlag) &&\n typeof data.timestamp === \"number\"\n );\n }\n\n private isValidFeatureFlag(flag: any): flag is FeatureFlag {\n return (\n flag &&\n typeof flag === \"object\" &&\n typeof flag.key === \"string\" &&\n typeof flag.enabled_production === \"boolean\" &&\n typeof flag.enabled_staging === \"boolean\" &&\n typeof flag.enabled_development === \"boolean\"\n );\n }\n\n private saveToCache(): void {\n this.storage.set(\"feature-flags\", {\n flags: this.flags,\n timestamp: this.lastFetch,\n });\n }\n\n // Check if valid flags exist in cache\n private hasCachedFlags(): boolean {\n const cached = this.storage.get(\"feature-flags\");\n return cached ? this.isValidCachedData(cached) : false;\n }\n\n // Check if cache interval has elapsed and a fetch is needed\n private shouldFetch(): boolean {\n const now = Date.now();\n return now - this.lastFetch > this.getRefreshIntervalMs();\n }\n\n // Exponential backoff: base * 2^failureCount (capped)\n getRefreshIntervalMs(): number {\n const backoffMs = this.baseRefreshMs * Math.pow(2, this.failureCount);\n return Math.min(backoffMs, this.maxRefreshMs);\n }\n\n async fetchFlags(): Promise<void> {\n if (!this.shouldFetch()) {\n return;\n }\n\n // If already fetching, return the existing promise\n if (this.isFetching && this.fetchPromise) {\n return this.fetchPromise;\n }\n\n this.isFetching = true;\n this.fetchPromise = this.doFetch();\n\n try {\n await this.fetchPromise;\n } finally {\n this.isFetching = false;\n this.fetchPromise = null;\n }\n }\n\n private async doFetch(): Promise<void> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 2000);\n\n try {\n const response = await fetch(\n \"https://api.flaggy.io/public/feature-flags\",\n {\n method: \"GET\",\n headers: {\n \"x-api-key\": this.config.apiKey,\n \"Content-Type\": \"application/json\",\n },\n signal: controller.signal,\n },\n );\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch feature flags: ${response.status} ${response.statusText}`,\n );\n }\n\n const json = await response.json();\n\n // Validate response structure\n if (!json || typeof json !== \"object\") {\n throw new Error(\"Invalid API response: expected JSON object\");\n }\n\n if (!json.data) {\n throw new Error(\"Invalid API response: missing data property\");\n }\n\n const data = json.data;\n\n // Validate and filter feature flags\n if (!Array.isArray(data)) {\n throw new Error(\"Invalid API response: data must be an array\");\n }\n\n // Filter out invalid flags and keep only valid ones\n this.flags = data.filter(this.isValidFeatureFlag);\n\n if (data.length > 0 && this.flags.length === 0) {\n console.warn(\n \"All feature flags in API response were invalid and filtered out\",\n );\n }\n\n this.lastFetch = Date.now();\n this.failureCount = 0;\n this.saveToCache();\n } catch (error) {\n this.failureCount += 1;\n const err = error as Error;\n if (this.config.onError) {\n this.config.onError(err);\n }\n if (err.name === \"AbortError\") {\n console.warn(\"Feature flag fetch timed out\");\n return;\n }\n console.error(\"Error fetching feature flags:\", err);\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n initialize(): Promise<void> {\n if (this.hasCachedFlags()) {\n return Promise.resolve();\n }\n\n if (!this.shouldFetch()) {\n return Promise.resolve();\n }\n\n const timeoutMs = 2000;\n return Promise.race([\n this.fetchFlags(),\n new Promise<void>((resolve) => {\n setTimeout(resolve, timeoutMs);\n }),\n ]);\n }\n\n isEnabled(flagName: string, defaultValue: boolean = false): boolean {\n // Auto-refresh if needed\n if (this.shouldFetch() && !this.isFetching) {\n this.fetchFlags(); // Fire and forget\n }\n\n const flag = this.flags.find((f) => f.key === flagName);\n if (!flag) {\n return defaultValue; // Return provided default for unknown flags\n }\n\n // Check the environment-specific enabled field\n switch (this.environment) {\n case \"production\":\n return flag.enabled_production;\n case \"staging\":\n return flag.enabled_staging;\n case \"development\":\n return flag.enabled_development;\n default:\n return defaultValue;\n }\n }\n\n getFlag(flagName: string): FeatureFlag | undefined {\n // Auto-refresh if needed\n if (this.shouldFetch() && !this.isFetching) {\n this.fetchFlags(); // Fire and forget\n }\n\n return this.flags.find((f) => f.key === flagName);\n }\n\n getAllFlags(): FeatureFlag[] {\n return [...this.flags];\n }\n\n clearCache(): void {\n this.storage.clear(\"feature-flags\");\n this.flags = [];\n }\n\n // Set custom storage adapter (useful for testing or custom implementations)\n setStorageAdapter(adapter: StorageAdapter): void {\n this.storage = adapter;\n this.loadFromCache();\n }\n}\n\nlet globalClient: FeatureFlagClient | null = null;\n\nexport function flaggy(config: FeatureFlagConfig): FeatureFlagClient {\n if (globalClient) return globalClient;\n\n globalClient = new FeatureFlagClient(config);\n return globalClient;\n}\n"]}
@@ -0,0 +1,52 @@
1
+ interface FeatureFlag {
2
+ key: string;
3
+ enabled_production: boolean;
4
+ enabled_staging: boolean;
5
+ enabled_development: boolean;
6
+ }
7
+ interface FeatureFlagConfig {
8
+ apiKey: string;
9
+ environment?: "production" | "staging" | "development" | string;
10
+ onError?: (error: Error) => void;
11
+ }
12
+ interface StorageAdapter {
13
+ get(key: string): CachedData | null;
14
+ set(key: string, value: CachedData): void;
15
+ clear(key: string): void;
16
+ }
17
+ interface CachedData {
18
+ flags: FeatureFlag[];
19
+ timestamp: number;
20
+ }
21
+ declare class FeatureFlagClient {
22
+ private config;
23
+ private storage;
24
+ private flags;
25
+ private lastFetch;
26
+ private isFetching;
27
+ private fetchPromise;
28
+ private environment;
29
+ private failureCount;
30
+ private readonly baseRefreshMs;
31
+ private readonly maxRefreshMs;
32
+ constructor(config: FeatureFlagConfig);
33
+ private detectEnvironment;
34
+ private loadFromCache;
35
+ private isValidCachedData;
36
+ private isValidFeatureFlag;
37
+ private saveToCache;
38
+ private hasCachedFlags;
39
+ private shouldFetch;
40
+ getRefreshIntervalMs(): number;
41
+ fetchFlags(): Promise<void>;
42
+ private doFetch;
43
+ initialize(): Promise<void>;
44
+ isEnabled(flagName: string, defaultValue?: boolean): boolean;
45
+ getFlag(flagName: string): FeatureFlag | undefined;
46
+ getAllFlags(): FeatureFlag[];
47
+ clearCache(): void;
48
+ setStorageAdapter(adapter: StorageAdapter): void;
49
+ }
50
+ declare function flaggy(config: FeatureFlagConfig): FeatureFlagClient;
51
+
52
+ export { type FeatureFlag, type FeatureFlagConfig, type StorageAdapter, flaggy };
@@ -0,0 +1,52 @@
1
+ interface FeatureFlag {
2
+ key: string;
3
+ enabled_production: boolean;
4
+ enabled_staging: boolean;
5
+ enabled_development: boolean;
6
+ }
7
+ interface FeatureFlagConfig {
8
+ apiKey: string;
9
+ environment?: "production" | "staging" | "development" | string;
10
+ onError?: (error: Error) => void;
11
+ }
12
+ interface StorageAdapter {
13
+ get(key: string): CachedData | null;
14
+ set(key: string, value: CachedData): void;
15
+ clear(key: string): void;
16
+ }
17
+ interface CachedData {
18
+ flags: FeatureFlag[];
19
+ timestamp: number;
20
+ }
21
+ declare class FeatureFlagClient {
22
+ private config;
23
+ private storage;
24
+ private flags;
25
+ private lastFetch;
26
+ private isFetching;
27
+ private fetchPromise;
28
+ private environment;
29
+ private failureCount;
30
+ private readonly baseRefreshMs;
31
+ private readonly maxRefreshMs;
32
+ constructor(config: FeatureFlagConfig);
33
+ private detectEnvironment;
34
+ private loadFromCache;
35
+ private isValidCachedData;
36
+ private isValidFeatureFlag;
37
+ private saveToCache;
38
+ private hasCachedFlags;
39
+ private shouldFetch;
40
+ getRefreshIntervalMs(): number;
41
+ fetchFlags(): Promise<void>;
42
+ private doFetch;
43
+ initialize(): Promise<void>;
44
+ isEnabled(flagName: string, defaultValue?: boolean): boolean;
45
+ getFlag(flagName: string): FeatureFlag | undefined;
46
+ getAllFlags(): FeatureFlag[];
47
+ clearCache(): void;
48
+ setStorageAdapter(adapter: StorageAdapter): void;
49
+ }
50
+ declare function flaggy(config: FeatureFlagConfig): FeatureFlagClient;
51
+
52
+ export { type FeatureFlag, type FeatureFlagConfig, type StorageAdapter, flaggy };
package/dist/index.js ADDED
@@ -0,0 +1,241 @@
1
+ // src/index.ts
2
+ var LocalStorageAdapter = class {
3
+ get(key) {
4
+ if (typeof window === "undefined" || !window.localStorage) {
5
+ return null;
6
+ }
7
+ try {
8
+ const data = window.localStorage.getItem(key);
9
+ return data ? JSON.parse(data) : null;
10
+ } catch (error) {
11
+ console.error("Error reading from localStorage:", error);
12
+ return null;
13
+ }
14
+ }
15
+ set(key, value) {
16
+ if (typeof window === "undefined" || !window.localStorage) {
17
+ return;
18
+ }
19
+ try {
20
+ window.localStorage.setItem(key, JSON.stringify(value));
21
+ } catch (error) {
22
+ console.error("Error writing to localStorage:", error);
23
+ }
24
+ }
25
+ clear(key) {
26
+ if (typeof window === "undefined" || !window.localStorage) {
27
+ return;
28
+ }
29
+ try {
30
+ window.localStorage.removeItem(key);
31
+ } catch (error) {
32
+ console.error("Error clearing localStorage:", error);
33
+ }
34
+ }
35
+ };
36
+ var InMemoryStorageAdapter = class {
37
+ constructor() {
38
+ this.store = /* @__PURE__ */ new Map();
39
+ }
40
+ get(key) {
41
+ return this.store.get(key) || null;
42
+ }
43
+ set(key, value) {
44
+ this.store.set(key, value);
45
+ }
46
+ clear(key) {
47
+ this.store.delete(key);
48
+ }
49
+ };
50
+ var FeatureFlagClient = class {
51
+ constructor(config) {
52
+ this.flags = [];
53
+ this.lastFetch = 0;
54
+ this.isFetching = false;
55
+ this.fetchPromise = null;
56
+ this.failureCount = 0;
57
+ this.baseRefreshMs = 6e4;
58
+ this.maxRefreshMs = 15 * 60 * 1e3;
59
+ if (!config.apiKey || typeof config.apiKey !== "string") {
60
+ throw new Error("API key is required and must be a non-empty string");
61
+ }
62
+ if (config.apiKey.trim().length === 0) {
63
+ throw new Error("API key cannot be empty or whitespace");
64
+ }
65
+ this.config = config;
66
+ this.environment = config.environment === "production" || config.environment === "staging" || config.environment === "development" ? config.environment : "production";
67
+ this.storage = this.detectEnvironment() ? new LocalStorageAdapter() : new InMemoryStorageAdapter();
68
+ this.loadFromCache();
69
+ this.fetchFlags();
70
+ }
71
+ detectEnvironment() {
72
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
73
+ }
74
+ loadFromCache() {
75
+ const cached = this.storage.get("feature-flags");
76
+ if (cached && this.isValidCachedData(cached)) {
77
+ this.flags = cached.flags;
78
+ this.lastFetch = cached.timestamp || 0;
79
+ }
80
+ }
81
+ isValidCachedData(data) {
82
+ return data && typeof data === "object" && Array.isArray(data.flags) && data.flags.every(this.isValidFeatureFlag) && typeof data.timestamp === "number";
83
+ }
84
+ isValidFeatureFlag(flag) {
85
+ return flag && typeof flag === "object" && typeof flag.key === "string" && typeof flag.enabled_production === "boolean" && typeof flag.enabled_staging === "boolean" && typeof flag.enabled_development === "boolean";
86
+ }
87
+ saveToCache() {
88
+ this.storage.set("feature-flags", {
89
+ flags: this.flags,
90
+ timestamp: this.lastFetch
91
+ });
92
+ }
93
+ // Check if valid flags exist in cache
94
+ hasCachedFlags() {
95
+ const cached = this.storage.get("feature-flags");
96
+ return cached ? this.isValidCachedData(cached) : false;
97
+ }
98
+ // Check if cache interval has elapsed and a fetch is needed
99
+ shouldFetch() {
100
+ const now = Date.now();
101
+ return now - this.lastFetch > this.getRefreshIntervalMs();
102
+ }
103
+ // Exponential backoff: base * 2^failureCount (capped)
104
+ getRefreshIntervalMs() {
105
+ const backoffMs = this.baseRefreshMs * Math.pow(2, this.failureCount);
106
+ return Math.min(backoffMs, this.maxRefreshMs);
107
+ }
108
+ async fetchFlags() {
109
+ if (!this.shouldFetch()) {
110
+ return;
111
+ }
112
+ if (this.isFetching && this.fetchPromise) {
113
+ return this.fetchPromise;
114
+ }
115
+ this.isFetching = true;
116
+ this.fetchPromise = this.doFetch();
117
+ try {
118
+ await this.fetchPromise;
119
+ } finally {
120
+ this.isFetching = false;
121
+ this.fetchPromise = null;
122
+ }
123
+ }
124
+ async doFetch() {
125
+ const controller = new AbortController();
126
+ const timeoutId = setTimeout(() => controller.abort(), 2e3);
127
+ try {
128
+ const response = await fetch(
129
+ "https://api.flaggy.io/public/feature-flags",
130
+ {
131
+ method: "GET",
132
+ headers: {
133
+ "x-api-key": this.config.apiKey,
134
+ "Content-Type": "application/json"
135
+ },
136
+ signal: controller.signal
137
+ }
138
+ );
139
+ if (!response.ok) {
140
+ throw new Error(
141
+ `Failed to fetch feature flags: ${response.status} ${response.statusText}`
142
+ );
143
+ }
144
+ const json = await response.json();
145
+ if (!json || typeof json !== "object") {
146
+ throw new Error("Invalid API response: expected JSON object");
147
+ }
148
+ if (!json.data) {
149
+ throw new Error("Invalid API response: missing data property");
150
+ }
151
+ const data = json.data;
152
+ if (!Array.isArray(data)) {
153
+ throw new Error("Invalid API response: data must be an array");
154
+ }
155
+ this.flags = data.filter(this.isValidFeatureFlag);
156
+ if (data.length > 0 && this.flags.length === 0) {
157
+ console.warn(
158
+ "All feature flags in API response were invalid and filtered out"
159
+ );
160
+ }
161
+ this.lastFetch = Date.now();
162
+ this.failureCount = 0;
163
+ this.saveToCache();
164
+ } catch (error) {
165
+ this.failureCount += 1;
166
+ const err = error;
167
+ if (this.config.onError) {
168
+ this.config.onError(err);
169
+ }
170
+ if (err.name === "AbortError") {
171
+ console.warn("Feature flag fetch timed out");
172
+ return;
173
+ }
174
+ console.error("Error fetching feature flags:", err);
175
+ } finally {
176
+ clearTimeout(timeoutId);
177
+ }
178
+ }
179
+ initialize() {
180
+ if (this.hasCachedFlags()) {
181
+ return Promise.resolve();
182
+ }
183
+ if (!this.shouldFetch()) {
184
+ return Promise.resolve();
185
+ }
186
+ const timeoutMs = 2e3;
187
+ return Promise.race([
188
+ this.fetchFlags(),
189
+ new Promise((resolve) => {
190
+ setTimeout(resolve, timeoutMs);
191
+ })
192
+ ]);
193
+ }
194
+ isEnabled(flagName, defaultValue = false) {
195
+ if (this.shouldFetch() && !this.isFetching) {
196
+ this.fetchFlags();
197
+ }
198
+ const flag = this.flags.find((f) => f.key === flagName);
199
+ if (!flag) {
200
+ return defaultValue;
201
+ }
202
+ switch (this.environment) {
203
+ case "production":
204
+ return flag.enabled_production;
205
+ case "staging":
206
+ return flag.enabled_staging;
207
+ case "development":
208
+ return flag.enabled_development;
209
+ default:
210
+ return defaultValue;
211
+ }
212
+ }
213
+ getFlag(flagName) {
214
+ if (this.shouldFetch() && !this.isFetching) {
215
+ this.fetchFlags();
216
+ }
217
+ return this.flags.find((f) => f.key === flagName);
218
+ }
219
+ getAllFlags() {
220
+ return [...this.flags];
221
+ }
222
+ clearCache() {
223
+ this.storage.clear("feature-flags");
224
+ this.flags = [];
225
+ }
226
+ // Set custom storage adapter (useful for testing or custom implementations)
227
+ setStorageAdapter(adapter) {
228
+ this.storage = adapter;
229
+ this.loadFromCache();
230
+ }
231
+ };
232
+ var globalClient = null;
233
+ function flaggy(config) {
234
+ if (globalClient) return globalClient;
235
+ globalClient = new FeatureFlagClient(config);
236
+ return globalClient;
237
+ }
238
+
239
+ export { flaggy };
240
+ //# sourceMappingURL=index.js.map
241
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AA2BA,IAAM,sBAAN,MAAoD;AAAA,EAClD,IAAI,GAAA,EAAgC;AAClC,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,OAAO,YAAA,EAAc;AACzD,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAO,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,GAAG,CAAA;AAC5C,MAAA,OAAO,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AAAA,IACnC,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,oCAAoC,KAAK,CAAA;AACvD,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,GAAA,CAAI,KAAa,KAAA,EAAyB;AACxC,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,OAAO,YAAA,EAAc;AACzD,MAAA;AAAA,IACF;AACA,IAAA,IAAI;AACF,MAAA,MAAA,CAAO,aAAa,OAAA,CAAQ,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC,CAAA;AAAA,IACxD,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,kCAAkC,KAAK,CAAA;AAAA,IACvD;AAAA,EACF;AAAA,EAEA,MAAM,GAAA,EAAmB;AACvB,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,OAAO,YAAA,EAAc;AACzD,MAAA;AAAA,IACF;AACA,IAAA,IAAI;AACF,MAAA,MAAA,CAAO,YAAA,CAAa,WAAW,GAAG,CAAA;AAAA,IACpC,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,gCAAgC,KAAK,CAAA;AAAA,IACrD;AAAA,EACF;AACF,CAAA;AAGA,IAAM,yBAAN,MAAuD;AAAA,EAAvD,WAAA,GAAA;AACE,IAAA,IAAA,CAAQ,KAAA,uBAAqC,GAAA,EAAI;AAAA,EAAA;AAAA,EAEjD,IAAI,GAAA,EAAgC;AAClC,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA,IAAK,IAAA;AAAA,EAChC;AAAA,EAEA,GAAA,CAAI,KAAa,KAAA,EAAyB;AACxC,IAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAAA,EAC3B;AAAA,EAEA,MAAM,GAAA,EAAmB;AACvB,IAAA,IAAA,CAAK,KAAA,CAAM,OAAO,GAAG,CAAA;AAAA,EACvB;AACF,CAAA;AAEA,IAAM,oBAAN,MAAwB;AAAA,EAYtB,YAAY,MAAA,EAA2B;AATvC,IAAA,IAAA,CAAQ,QAAuB,EAAC;AAChC,IAAA,IAAA,CAAQ,SAAA,GAAoB,CAAA;AAC5B,IAAA,IAAA,CAAQ,UAAA,GAAsB,KAAA;AAC9B,IAAA,IAAA,CAAQ,YAAA,GAAqC,IAAA;AAE7C,IAAA,IAAA,CAAQ,YAAA,GAAuB,CAAA;AAC/B,IAAA,IAAA,CAAiB,aAAA,GAAwB,GAAA;AACzC,IAAA,IAAA,CAAiB,YAAA,GAAuB,KAAK,EAAA,GAAK,GAAA;AAIhD,IAAA,IAAI,CAAC,MAAA,CAAO,MAAA,IAAU,OAAO,MAAA,CAAO,WAAW,QAAA,EAAU;AACvD,MAAA,MAAM,IAAI,MAAM,oDAAoD,CAAA;AAAA,IACtE;AAEA,IAAA,IAAI,MAAA,CAAO,MAAA,CAAO,IAAA,EAAK,CAAE,WAAW,CAAA,EAAG;AACrC,MAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,IACzD;AAEA,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,WAAA,GACH,MAAA,CAAO,WAAA,KAAgB,YAAA,IACvB,MAAA,CAAO,WAAA,KAAgB,SAAA,IACvB,MAAA,CAAO,WAAA,KAAgB,aAAA,GACnB,MAAA,CAAO,WAAA,GACP,YAAA;AAGN,IAAA,IAAA,CAAK,OAAA,GAAU,KAAK,iBAAA,EAAkB,GAClC,IAAI,mBAAA,EAAoB,GACxB,IAAI,sBAAA,EAAuB;AAG/B,IAAA,IAAA,CAAK,aAAA,EAAc;AAGnB,IAAA,IAAA,CAAK,UAAA,EAAW;AAAA,EAClB;AAAA,EAEQ,iBAAA,GAA6B;AACnC,IAAA,OACE,OAAO,MAAA,KAAW,WAAA,IAClB,OAAO,OAAO,YAAA,KAAiB,WAAA;AAAA,EAEnC;AAAA,EAEQ,aAAA,GAAsB;AAC5B,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA;AAC/C,IAAA,IAAI,MAAA,IAAU,IAAA,CAAK,iBAAA,CAAkB,MAAM,CAAA,EAAG;AAC5C,MAAA,IAAA,CAAK,QAAQ,MAAA,CAAO,KAAA;AACpB,MAAA,IAAA,CAAK,SAAA,GAAY,OAAO,SAAA,IAAa,CAAA;AAAA,IACvC;AAAA,EACF;AAAA,EAEQ,kBAAkB,IAAA,EAA+B;AACvD,IAAA,OACE,QACA,OAAO,IAAA,KAAS,QAAA,IAChB,KAAA,CAAM,QAAQ,IAAA,CAAK,KAAK,CAAA,IACxB,IAAA,CAAK,MAAM,KAAA,CAAM,IAAA,CAAK,kBAAkB,CAAA,IACxC,OAAO,KAAK,SAAA,KAAc,QAAA;AAAA,EAE9B;AAAA,EAEQ,mBAAmB,IAAA,EAAgC;AACzD,IAAA,OACE,QACA,OAAO,IAAA,KAAS,YAChB,OAAO,IAAA,CAAK,QAAQ,QAAA,IACpB,OAAO,IAAA,CAAK,kBAAA,KAAuB,aACnC,OAAO,IAAA,CAAK,oBAAoB,SAAA,IAChC,OAAO,KAAK,mBAAA,KAAwB,SAAA;AAAA,EAExC;AAAA,EAEQ,WAAA,GAAoB;AAC1B,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAI,eAAA,EAAiB;AAAA,MAChC,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,WAAW,IAAA,CAAK;AAAA,KACjB,CAAA;AAAA,EACH;AAAA;AAAA,EAGQ,cAAA,GAA0B;AAChC,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA;AAC/C,IAAA,OAAO,MAAA,GAAS,IAAA,CAAK,iBAAA,CAAkB,MAAM,CAAA,GAAI,KAAA;AAAA,EACnD;AAAA;AAAA,EAGQ,WAAA,GAAuB;AAC7B,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,OAAO,GAAA,GAAM,IAAA,CAAK,SAAA,GAAY,IAAA,CAAK,oBAAA,EAAqB;AAAA,EAC1D;AAAA;AAAA,EAGA,oBAAA,GAA+B;AAC7B,IAAA,MAAM,YAAY,IAAA,CAAK,aAAA,GAAgB,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,YAAY,CAAA;AACpE,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,SAAA,EAAW,IAAA,CAAK,YAAY,CAAA;AAAA,EAC9C;AAAA,EAEA,MAAM,UAAA,GAA4B;AAChC,IAAA,IAAI,CAAC,IAAA,CAAK,WAAA,EAAY,EAAG;AACvB,MAAA;AAAA,IACF;AAGA,IAAA,IAAI,IAAA,CAAK,UAAA,IAAc,IAAA,CAAK,YAAA,EAAc;AACxC,MAAA,OAAO,IAAA,CAAK,YAAA;AAAA,IACd;AAEA,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,YAAA,GAAe,KAAK,OAAA,EAAQ;AAEjC,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK,YAAA;AAAA,IACb,CAAA,SAAE;AACA,MAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,MAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAc,OAAA,GAAyB;AACrC,IAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,IAAA,MAAM,YAAY,UAAA,CAAW,MAAM,UAAA,CAAW,KAAA,IAAS,GAAI,CAAA;AAE3D,IAAA,IAAI;AACF,MAAA,MAAM,WAAW,MAAM,KAAA;AAAA,QACrB,4CAAA;AAAA,QACA;AAAA,UACE,MAAA,EAAQ,KAAA;AAAA,UACR,OAAA,EAAS;AAAA,YACP,WAAA,EAAa,KAAK,MAAA,CAAO,MAAA;AAAA,YACzB,cAAA,EAAgB;AAAA,WAClB;AAAA,UACA,QAAQ,UAAA,CAAW;AAAA;AACrB,OACF;AAEA,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,+BAAA,EAAkC,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,SAAS,UAAU,CAAA;AAAA,SAC1E;AAAA,MACF;AAEA,MAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAGjC,MAAA,IAAI,CAAC,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,EAAU;AACrC,QAAA,MAAM,IAAI,MAAM,4CAA4C,CAAA;AAAA,MAC9D;AAEA,MAAA,IAAI,CAAC,KAAK,IAAA,EAAM;AACd,QAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,MAC/D;AAEA,MAAA,MAAM,OAAO,IAAA,CAAK,IAAA;AAGlB,MAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,EAAG;AACxB,QAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,MAC/D;AAGA,MAAA,IAAA,CAAK,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,kBAAkB,CAAA;AAEhD,MAAA,IAAI,KAAK,MAAA,GAAS,CAAA,IAAK,IAAA,CAAK,KAAA,CAAM,WAAW,CAAA,EAAG;AAC9C,QAAA,OAAA,CAAQ,IAAA;AAAA,UACN;AAAA,SACF;AAAA,MACF;AAEA,MAAA,IAAA,CAAK,SAAA,GAAY,KAAK,GAAA,EAAI;AAC1B,MAAA,IAAA,CAAK,YAAA,GAAe,CAAA;AACpB,MAAA,IAAA,CAAK,WAAA,EAAY;AAAA,IACnB,SAAS,KAAA,EAAO;AACd,MAAA,IAAA,CAAK,YAAA,IAAgB,CAAA;AACrB,MAAA,MAAM,GAAA,GAAM,KAAA;AACZ,MAAA,IAAI,IAAA,CAAK,OAAO,OAAA,EAAS;AACvB,QAAA,IAAA,CAAK,MAAA,CAAO,QAAQ,GAAG,CAAA;AAAA,MACzB;AACA,MAAA,IAAI,GAAA,CAAI,SAAS,YAAA,EAAc;AAC7B,QAAA,OAAA,CAAQ,KAAK,8BAA8B,CAAA;AAC3C,QAAA;AAAA,MACF;AACA,MAAA,OAAA,CAAQ,KAAA,CAAM,iCAAiC,GAAG,CAAA;AAAA,IACpD,CAAA,SAAE;AACA,MAAA,YAAA,CAAa,SAAS,CAAA;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,UAAA,GAA4B;AAC1B,IAAA,IAAI,IAAA,CAAK,gBAAe,EAAG;AACzB,MAAA,OAAO,QAAQ,OAAA,EAAQ;AAAA,IACzB;AAEA,IAAA,IAAI,CAAC,IAAA,CAAK,WAAA,EAAY,EAAG;AACvB,MAAA,OAAO,QAAQ,OAAA,EAAQ;AAAA,IACzB;AAEA,IAAA,MAAM,SAAA,GAAY,GAAA;AAClB,IAAA,OAAO,QAAQ,IAAA,CAAK;AAAA,MAClB,KAAK,UAAA,EAAW;AAAA,MAChB,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AAC7B,QAAA,UAAA,CAAW,SAAS,SAAS,CAAA;AAAA,MAC/B,CAAC;AAAA,KACF,CAAA;AAAA,EACH;AAAA,EAEA,SAAA,CAAU,QAAA,EAAkB,YAAA,GAAwB,KAAA,EAAgB;AAElE,IAAA,IAAI,IAAA,CAAK,WAAA,EAAY,IAAK,CAAC,KAAK,UAAA,EAAY;AAC1C,MAAA,IAAA,CAAK,UAAA,EAAW;AAAA,IAClB;AAEA,IAAA,MAAM,IAAA,GAAO,KAAK,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,QAAQ,QAAQ,CAAA;AACtD,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAO,YAAA;AAAA,IACT;AAGA,IAAA,QAAQ,KAAK,WAAA;AAAa,MACxB,KAAK,YAAA;AACH,QAAA,OAAO,IAAA,CAAK,kBAAA;AAAA,MACd,KAAK,SAAA;AACH,QAAA,OAAO,IAAA,CAAK,eAAA;AAAA,MACd,KAAK,aAAA;AACH,QAAA,OAAO,IAAA,CAAK,mBAAA;AAAA,MACd;AACE,QAAA,OAAO,YAAA;AAAA;AACX,EACF;AAAA,EAEA,QAAQ,QAAA,EAA2C;AAEjD,IAAA,IAAI,IAAA,CAAK,WAAA,EAAY,IAAK,CAAC,KAAK,UAAA,EAAY;AAC1C,MAAA,IAAA,CAAK,UAAA,EAAW;AAAA,IAClB;AAEA,IAAA,OAAO,KAAK,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,QAAQ,QAAQ,CAAA;AAAA,EAClD;AAAA,EAEA,WAAA,GAA6B;AAC3B,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,KAAK,CAAA;AAAA,EACvB;AAAA,EAEA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,OAAA,CAAQ,MAAM,eAAe,CAAA;AAClC,IAAA,IAAA,CAAK,QAAQ,EAAC;AAAA,EAChB;AAAA;AAAA,EAGA,kBAAkB,OAAA,EAA+B;AAC/C,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AACf,IAAA,IAAA,CAAK,aAAA,EAAc;AAAA,EACrB;AACF,CAAA;AAEA,IAAI,YAAA,GAAyC,IAAA;AAEtC,SAAS,OAAO,MAAA,EAA8C;AACnE,EAAA,IAAI,cAAc,OAAO,YAAA;AAEzB,EAAA,YAAA,GAAe,IAAI,kBAAkB,MAAM,CAAA;AAC3C,EAAA,OAAO,YAAA;AACT","file":"index.js","sourcesContent":["export interface FeatureFlag {\n key: string;\n enabled_production: boolean;\n enabled_staging: boolean;\n enabled_development: boolean;\n}\n\nexport interface FeatureFlagConfig {\n apiKey: string;\n environment?: \"production\" | \"staging\" | \"development\" | string;\n onError?: (error: Error) => void;\n}\n\n// Storage adapter interface for custom implementations\nexport interface StorageAdapter {\n get(key: string): CachedData | null;\n set(key: string, value: CachedData): void;\n clear(key: string): void;\n}\n\n// Storage adapter interface\ninterface CachedData {\n flags: FeatureFlag[];\n timestamp: number;\n}\n\n// LocalStorage adapter for browser\nclass LocalStorageAdapter implements StorageAdapter {\n get(key: string): CachedData | null {\n if (typeof window === \"undefined\" || !window.localStorage) {\n return null;\n }\n try {\n const data = window.localStorage.getItem(key);\n return data ? JSON.parse(data) : null;\n } catch (error) {\n console.error(\"Error reading from localStorage:\", error);\n return null;\n }\n }\n\n set(key: string, value: CachedData): void {\n if (typeof window === \"undefined\" || !window.localStorage) {\n return;\n }\n try {\n window.localStorage.setItem(key, JSON.stringify(value));\n } catch (error) {\n console.error(\"Error writing to localStorage:\", error);\n }\n }\n\n clear(key: string): void {\n if (typeof window === \"undefined\" || !window.localStorage) {\n return;\n }\n try {\n window.localStorage.removeItem(key);\n } catch (error) {\n console.error(\"Error clearing localStorage:\", error);\n }\n }\n}\n\n// In-memory adapter for server-side\nclass InMemoryStorageAdapter implements StorageAdapter {\n private store: Map<string, CachedData> = new Map();\n\n get(key: string): CachedData | null {\n return this.store.get(key) || null;\n }\n\n set(key: string, value: CachedData): void {\n this.store.set(key, value);\n }\n\n clear(key: string): void {\n this.store.delete(key);\n }\n}\n\nclass FeatureFlagClient {\n private config: FeatureFlagConfig;\n private storage: StorageAdapter;\n private flags: FeatureFlag[] = [];\n private lastFetch: number = 0;\n private isFetching: boolean = false;\n private fetchPromise: Promise<void> | null = null;\n private environment: \"production\" | \"staging\" | \"development\";\n private failureCount: number = 0;\n private readonly baseRefreshMs: number = 60000;\n private readonly maxRefreshMs: number = 15 * 60 * 1000;\n\n constructor(config: FeatureFlagConfig) {\n // Validate API key\n if (!config.apiKey || typeof config.apiKey !== \"string\") {\n throw new Error(\"API key is required and must be a non-empty string\");\n }\n\n if (config.apiKey.trim().length === 0) {\n throw new Error(\"API key cannot be empty or whitespace\");\n }\n\n this.config = config;\n this.environment =\n config.environment === \"production\" ||\n config.environment === \"staging\" ||\n config.environment === \"development\"\n ? config.environment\n : \"production\";\n\n // Auto-detect environment and use appropriate storage\n this.storage = this.detectEnvironment()\n ? new LocalStorageAdapter()\n : new InMemoryStorageAdapter();\n\n // Load cached flags on initialization\n this.loadFromCache();\n\n // Auto-fetch flags in background (fire and forget)\n this.fetchFlags();\n }\n\n private detectEnvironment(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof window.localStorage !== \"undefined\"\n );\n }\n\n private loadFromCache(): void {\n const cached = this.storage.get(\"feature-flags\");\n if (cached && this.isValidCachedData(cached)) {\n this.flags = cached.flags;\n this.lastFetch = cached.timestamp || 0;\n }\n }\n\n private isValidCachedData(data: any): data is CachedData {\n return (\n data &&\n typeof data === \"object\" &&\n Array.isArray(data.flags) &&\n data.flags.every(this.isValidFeatureFlag) &&\n typeof data.timestamp === \"number\"\n );\n }\n\n private isValidFeatureFlag(flag: any): flag is FeatureFlag {\n return (\n flag &&\n typeof flag === \"object\" &&\n typeof flag.key === \"string\" &&\n typeof flag.enabled_production === \"boolean\" &&\n typeof flag.enabled_staging === \"boolean\" &&\n typeof flag.enabled_development === \"boolean\"\n );\n }\n\n private saveToCache(): void {\n this.storage.set(\"feature-flags\", {\n flags: this.flags,\n timestamp: this.lastFetch,\n });\n }\n\n // Check if valid flags exist in cache\n private hasCachedFlags(): boolean {\n const cached = this.storage.get(\"feature-flags\");\n return cached ? this.isValidCachedData(cached) : false;\n }\n\n // Check if cache interval has elapsed and a fetch is needed\n private shouldFetch(): boolean {\n const now = Date.now();\n return now - this.lastFetch > this.getRefreshIntervalMs();\n }\n\n // Exponential backoff: base * 2^failureCount (capped)\n getRefreshIntervalMs(): number {\n const backoffMs = this.baseRefreshMs * Math.pow(2, this.failureCount);\n return Math.min(backoffMs, this.maxRefreshMs);\n }\n\n async fetchFlags(): Promise<void> {\n if (!this.shouldFetch()) {\n return;\n }\n\n // If already fetching, return the existing promise\n if (this.isFetching && this.fetchPromise) {\n return this.fetchPromise;\n }\n\n this.isFetching = true;\n this.fetchPromise = this.doFetch();\n\n try {\n await this.fetchPromise;\n } finally {\n this.isFetching = false;\n this.fetchPromise = null;\n }\n }\n\n private async doFetch(): Promise<void> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 2000);\n\n try {\n const response = await fetch(\n \"https://api.flaggy.io/public/feature-flags\",\n {\n method: \"GET\",\n headers: {\n \"x-api-key\": this.config.apiKey,\n \"Content-Type\": \"application/json\",\n },\n signal: controller.signal,\n },\n );\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch feature flags: ${response.status} ${response.statusText}`,\n );\n }\n\n const json = await response.json();\n\n // Validate response structure\n if (!json || typeof json !== \"object\") {\n throw new Error(\"Invalid API response: expected JSON object\");\n }\n\n if (!json.data) {\n throw new Error(\"Invalid API response: missing data property\");\n }\n\n const data = json.data;\n\n // Validate and filter feature flags\n if (!Array.isArray(data)) {\n throw new Error(\"Invalid API response: data must be an array\");\n }\n\n // Filter out invalid flags and keep only valid ones\n this.flags = data.filter(this.isValidFeatureFlag);\n\n if (data.length > 0 && this.flags.length === 0) {\n console.warn(\n \"All feature flags in API response were invalid and filtered out\",\n );\n }\n\n this.lastFetch = Date.now();\n this.failureCount = 0;\n this.saveToCache();\n } catch (error) {\n this.failureCount += 1;\n const err = error as Error;\n if (this.config.onError) {\n this.config.onError(err);\n }\n if (err.name === \"AbortError\") {\n console.warn(\"Feature flag fetch timed out\");\n return;\n }\n console.error(\"Error fetching feature flags:\", err);\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n initialize(): Promise<void> {\n if (this.hasCachedFlags()) {\n return Promise.resolve();\n }\n\n if (!this.shouldFetch()) {\n return Promise.resolve();\n }\n\n const timeoutMs = 2000;\n return Promise.race([\n this.fetchFlags(),\n new Promise<void>((resolve) => {\n setTimeout(resolve, timeoutMs);\n }),\n ]);\n }\n\n isEnabled(flagName: string, defaultValue: boolean = false): boolean {\n // Auto-refresh if needed\n if (this.shouldFetch() && !this.isFetching) {\n this.fetchFlags(); // Fire and forget\n }\n\n const flag = this.flags.find((f) => f.key === flagName);\n if (!flag) {\n return defaultValue; // Return provided default for unknown flags\n }\n\n // Check the environment-specific enabled field\n switch (this.environment) {\n case \"production\":\n return flag.enabled_production;\n case \"staging\":\n return flag.enabled_staging;\n case \"development\":\n return flag.enabled_development;\n default:\n return defaultValue;\n }\n }\n\n getFlag(flagName: string): FeatureFlag | undefined {\n // Auto-refresh if needed\n if (this.shouldFetch() && !this.isFetching) {\n this.fetchFlags(); // Fire and forget\n }\n\n return this.flags.find((f) => f.key === flagName);\n }\n\n getAllFlags(): FeatureFlag[] {\n return [...this.flags];\n }\n\n clearCache(): void {\n this.storage.clear(\"feature-flags\");\n this.flags = [];\n }\n\n // Set custom storage adapter (useful for testing or custom implementations)\n setStorageAdapter(adapter: StorageAdapter): void {\n this.storage = adapter;\n this.loadFromCache();\n }\n}\n\nlet globalClient: FeatureFlagClient | null = null;\n\nexport function flaggy(config: FeatureFlagConfig): FeatureFlagClient {\n if (globalClient) return globalClient;\n\n globalClient = new FeatureFlagClient(config);\n return globalClient;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@flaggy.io/sdk-js",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript SDK for Flaggy.io feature flag management",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "sideEffects": false,
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "dev": "tsup --watch",
20
+ "prepublishOnly": "npm run build",
21
+ "test": "echo \"Error: no test specified\" && exit 1"
22
+ },
23
+ "keywords": [
24
+ "feature-flags",
25
+ "feature-toggles",
26
+ "flaggy",
27
+ "typescript",
28
+ "sdk"
29
+ ],
30
+ "author": "Flaggy.io",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/flaggy-io/sdk-js"
35
+ },
36
+ "homepage": "https://flaggy.io",
37
+ "bugs": {
38
+ "url": "https://github.com/flaggy-io/sdk-js/issues"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ "devDependencies": {
46
+ "tsup": "^8.0.0",
47
+ "typescript": "^5.0.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=18.0.0"
51
+ }
52
+ }