@flaggy.io/sdk-js 1.0.0 → 2.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/README.md CHANGED
@@ -1,22 +1,10 @@
1
- # Flaggy.io Feature Flag SDK
1
+ # Flaggy
2
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
3
+ A JavaScript / TypeScript SDK for managing feature flags and segments from Flaggy.io, fully compatible with both client-side (browser) and server-side (Node.js) environments.
16
4
 
17
5
  ## Requirements
18
6
 
19
- - **Node.js**: 18.0.0 or higher (required for native `fetch()` API support)
7
+ - **Node.js**: 18.0.0 or higher
20
8
  - **Browser**: Any modern browser with ES2020 support
21
9
 
22
10
  ## Installation
@@ -25,37 +13,54 @@ A JavaScript / TypeScript SDK for managing feature flags from Flaggy.io that is
25
13
  npm install @flaggy.io/sdk-js
26
14
  ```
27
15
 
28
- ## Client-Side (React)
16
+ ## Quick Start
29
17
 
30
- Create a dedicated file (e.g., `src/lib/flaggy.ts`):
18
+ ### 1. Create the client
31
19
 
32
20
  ```typescript
33
21
  import { flaggy } from "@flaggy.io/sdk-js";
34
22
 
35
23
  export const flagClient = flaggy({
36
- apiKey: import.meta.env.VITE_FLAGGY_API_KEY!,
37
- environment: import.meta.env.VITE_ENVIRONMENT,
24
+ apiKey: process.env.FLAGGY_API_KEY!,
38
25
  });
26
+ ```
27
+
28
+ ### 2. Initialize before use
29
+
30
+ ```typescript
31
+ await flagClient.initialize();
32
+ ```
39
33
 
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);
34
+ This ensures flags are loaded before you start evaluating them. If a warm cache exists (browser), it returns immediately. On Node.js it waits for the first fetch to complete.
35
+
36
+ ### 3. Check a flag
37
+
38
+ ```typescript
39
+ if (flagClient.isEnabled("new-checkout")) {
40
+ // show new checkout
46
41
  }
47
42
  ```
48
43
 
49
- Initialize and wait for flags:
44
+ That's it for simple on/off flags. Read on for environment configuration, segment targeting, and error handling.
50
45
 
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";
46
+ ---
47
+
48
+ ## Setup by Environment
49
+
50
+ ### React / Browser
58
51
 
52
+ ```typescript
53
+ // src/lib/flaggy.ts
54
+ import { flaggy } from "@flaggy.io/sdk-js";
55
+
56
+ export const flagClient = flaggy({
57
+ apiKey: import.meta.env.VITE_FLAGGY_API_KEY!,
58
+ environment: import.meta.env.VITE_ENVIRONMENT,
59
+ });
60
+ ```
61
+
62
+ ```tsx
63
+ // src/main.tsx — await before rendering to avoid flash of wrong content
59
64
  await flagClient.initialize();
60
65
 
61
66
  createRoot(document.getElementById("root")!).render(
@@ -65,268 +70,152 @@ createRoot(document.getElementById("root")!).render(
65
70
  );
66
71
  ```
67
72
 
68
- Simple usage example:
69
-
70
73
  ```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 />;
74
+ // usage anywhere in your app
75
+ if (flagClient.isEnabled("new-header")) {
76
+ return <NewHeader />;
76
77
  }
77
78
  ```
78
79
 
79
- ## Server-Side (Node.js)
80
-
81
- Create a dedicated client file (e.g., `src/lib/flaggy.ts`):
80
+ ### Node.js
82
81
 
83
82
  ```typescript
83
+ // src/lib/flaggy.ts
84
84
  import { flaggy } from "@flaggy.io/sdk-js";
85
85
 
86
86
  export const flagClient = flaggy({
87
87
  apiKey: process.env.FLAGGY_API_KEY!,
88
88
  environment: process.env.NODE_ENV,
89
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
90
  ```
99
91
 
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
92
  ```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
- }
93
+ // src/server.ts await before accepting requests
94
+ await flagClient.initialize();
124
95
 
125
- void start();
96
+ app.listen(3000);
126
97
  ```
127
98
 
128
- Use throughout your application:
129
-
130
99
  ```typescript
131
- import { featureEnabled } from "./lib/flaggy";
132
-
133
- if (featureEnabled("new-algorithm", true)) {
134
- // Defaulting to enabled
100
+ // usage in a route handler
101
+ if (flagClient.isEnabled("new-algorithm")) {
102
+ // use new algorithm
135
103
  }
136
104
  ```
137
105
 
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
106
+ ---
168
107
 
169
- The SDK uses the following hard-coded values:
108
+ ## Segment Targeting
170
109
 
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
110
+ Segments let you enable a flag only for users who match specific attributes — for example, users on the `"pro"` plan, or users in a beta programme.
175
111
 
176
- ### Feature Flag Structure
112
+ ### Passing a context
177
113
 
178
- Each feature flag has the following structure:
114
+ Instead of a simple flag name, pass a flat key-value `context` describing the current user or request:
179
115
 
180
116
  ```typescript
181
- interface FeatureFlag {
182
- key: string;
183
- enabled_production: boolean;
184
- enabled_staging: boolean;
185
- enabled_development: boolean;
117
+ const context = {
118
+ plan: "pro",
119
+ country: "US",
120
+ betaOptIn: true,
121
+ };
122
+
123
+ if (flagClient.isEnabled("new-dashboard", context)) {
124
+ // only shown to users matching an applicable segment
186
125
  }
187
126
  ```
188
127
 
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
128
+ ### How evaluation works
212
129
 
213
- **Important:** Configuration is locked after the first call. Subsequent calls with different config values will return the existing instance with the original configuration.
130
+ A segment is a set of rules defined in the Flaggy.io dashboard (e.g. `plan equals "pro"`). When you pass a context:
214
131
 
215
- ## Client API (returned by `flaggy()`)
132
+ - **All rules in a segment must match** (AND logic within a segment)
133
+ - **Any matching segment enables the flag** (OR logic across segments)
216
134
 
217
- **Caching behavior:**
135
+ | Step | Condition | Result |
136
+ | ---- | ----------------------------------- | --------------------------------------- |
137
+ | 1 | Flag not found | `defaultValue` (default: `false`) |
138
+ | 2 | Flag is disabled | `false` |
139
+ | 3 | Flag has no segments | `true` (global on/off flag) |
140
+ | 4 | Segments defined, no context passed | `false` |
141
+ | 5 | Context passed | `true` if any segment's rules all match |
218
142
 
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.
143
+ > **Note:** If a flag has segments but you don't pass a context, it always returns `false`. Always pass a context when evaluating segment-targeted flags.
222
144
 
223
- ### `isEnabled(flagName: string, defaultValue?: boolean): boolean`
145
+ ### Rule operators
224
146
 
225
- Check if a feature flag is enabled for the configured environment.
147
+ Each rule compares a context attribute to a value using one of:
226
148
 
227
- **Parameters:**
149
+ | Operator | Description |
150
+ | -------------- | ------------------------------------------- |
151
+ | `equals` | Attribute exactly matches the value |
152
+ | `not_equals` | Attribute does not match the value |
153
+ | `contains` | Attribute string contains the value |
154
+ | `not_contains` | Attribute string does not contain the value |
155
+ | `starts_with` | Attribute string starts with the value |
156
+ | `ends_with` | Attribute string ends with the value |
228
157
 
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`)
158
+ All attribute values are coerced to strings before comparison. If the attribute is missing from the context, the rule evaluates to `false`.
231
159
 
232
- **Returns:** `boolean` - Whether the flag is enabled
160
+ ---
233
161
 
234
- **Example:**
162
+ ## Configuration Options
235
163
 
236
164
  ```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
- }
165
+ const flagClient = flaggy({
166
+ apiKey: "your-api-key", // required
167
+ environment: "production", // optional "production" | "staging" | "development", defaults to "production"
168
+ onError: (err) => {
169
+ // optional — called on fetch failure, does not throw
170
+ console.warn(err);
171
+ },
172
+ });
246
173
  ```
247
174
 
248
- **Best Practice:** Always specify a safe default value that provides the correct experience for new users before flags load, or during network failures.
175
+ The `environment` field accepts raw strings like `process.env.NODE_ENV`. Any value other than `"production"`, `"staging"`, or `"development"` falls back to `"production"`.
249
176
 
250
- ### `getFlag(flagName: string): FeatureFlag | undefined`
177
+ ---
251
178
 
252
- Get the full feature flag object with all environment states.
179
+ ## Caching & Refresh
253
180
 
254
- ### `getAllFlags(): FeatureFlag[]`
181
+ The SDK caches flags locally and refreshes them automatically in the background.
255
182
 
256
- Get all cached feature flags.
183
+ - **Browser:** Cached in `localStorage`. Loaded immediately on startup; stale cache triggers a background refresh without blocking.
184
+ - **Node.js:** Cached in-memory per process. Empty on restart, so `initialize()` always fetches before resolving.
257
185
 
258
- ### `initialize(): Promise<void>`
186
+ The refresh interval starts at **60 seconds** and doubles on each failure, capped at **15 minutes**. Request timeouts (3 seconds) also count as failures — this intentionally backs clients off during API outages to aid recovery.
259
187
 
260
- Initialize the feature flag client. **This is the recommended method to call during application startup.**
188
+ ---
261
189
 
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).
190
+ ## API Reference
285
191
 
286
- **Use cases:**
192
+ ### `flaggy(config): FeatureFlagClient`
287
193
 
288
- - Manual refresh outside the automatic 60-second interval
289
- - Testing scenarios where you need deterministic flag state
194
+ Creates (or returns) the global singleton client. Safe to call across modules — subsequent calls return the same instance.
290
195
 
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.
196
+ ### `client.initialize(): Promise<void>`
292
197
 
293
- ### `clearCache(): void`
198
+ Ensures flags are ready. Call once at application startup before evaluating any flags.
294
199
 
295
- Clear all cached flags.
200
+ ### `client.isEnabled(flagName, context?, defaultValue?): boolean`
296
201
 
297
- ### `getRefreshIntervalMs(): number`
202
+ Evaluates a flag. Returns `defaultValue` (`false` by default) if the flag doesn't exist.
298
203
 
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.
204
+ ### `client.getAllFlags(): Record<string, FeatureFlag>`
300
205
 
301
- ## API Response Format
206
+ Returns a shallow copy of all currently cached flags.
302
207
 
303
- The Flaggy.io API returns feature flags in the following format:
208
+ ---
304
209
 
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:
210
+ ## Features
325
211
 
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
212
+ - Simple on/off flags and segment targeting
213
+ - Auto-environment detection (localStorage in browser, in-memory on Node.js)
214
+ - Automatic background refresh with exponential backoff
215
+ - Request deduplication no concurrent duplicate fetches
216
+ - ✅ Full TypeScript types
217
+ - ✅ Prototype-pollution-safe response handling
218
+ - ✅ Structural validation of all API responses
330
219
 
331
220
  ## License
332
221
 
package/dist/index.cjs CHANGED
@@ -51,7 +51,8 @@ var InMemoryStorageAdapter = class {
51
51
  };
52
52
  var FeatureFlagClient = class {
53
53
  constructor(config) {
54
- this.flags = [];
54
+ this.flags = {};
55
+ this.segments = {};
55
56
  this.lastFetch = 0;
56
57
  this.isFetching = false;
57
58
  this.fetchPromise = null;
@@ -74,28 +75,74 @@ var FeatureFlagClient = class {
74
75
  return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
75
76
  }
76
77
  loadFromCache() {
77
- const cached = this.storage.get("feature-flags");
78
+ const cached = this.storage.get("flaggy");
78
79
  if (cached && this.isValidCachedData(cached)) {
79
- this.flags = cached.flags;
80
+ const flags = /* @__PURE__ */ Object.create(null);
81
+ for (const [key, value] of Object.entries(cached.flags)) {
82
+ if (this.isValidFlag(value)) {
83
+ flags[key] = value;
84
+ }
85
+ }
86
+ const segments = /* @__PURE__ */ Object.create(null);
87
+ for (const [key, value] of Object.entries(cached.segments)) {
88
+ if (Array.isArray(value) && value.every(this.isValidSegmentRule)) {
89
+ segments[key] = value;
90
+ }
91
+ }
92
+ this.flags = flags;
93
+ this.segments = segments;
80
94
  this.lastFetch = cached.timestamp || 0;
81
95
  }
82
96
  }
83
97
  isValidCachedData(data) {
84
- return data && typeof data === "object" && Array.isArray(data.flags) && data.flags.every(this.isValidFeatureFlag) && typeof data.timestamp === "number";
98
+ if (!data || typeof data !== "object" || typeof data.timestamp !== "number") {
99
+ return false;
100
+ }
101
+ if (!data.flags || typeof data.flags !== "object" || Array.isArray(data.flags)) {
102
+ return false;
103
+ }
104
+ if (!data.segments || typeof data.segments !== "object" || Array.isArray(data.segments)) {
105
+ return false;
106
+ }
107
+ return Object.values(data.flags).every(this.isValidFlag) && Object.values(data.segments).every(
108
+ (rules) => Array.isArray(rules) && rules.every(this.isValidSegmentRule)
109
+ );
110
+ }
111
+ isValidFlag(flag) {
112
+ return flag !== null && typeof flag === "object" && typeof flag["enabled"] === "boolean" && Array.isArray(flag["applicable_segments"]) && flag["applicable_segments"].every((s) => typeof s === "string");
85
113
  }
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";
114
+ isValidSegmentRule(rule) {
115
+ const validOperators = [
116
+ "equals",
117
+ "not_equals",
118
+ "contains",
119
+ "not_contains",
120
+ "starts_with",
121
+ "ends_with"
122
+ ];
123
+ return rule !== null && typeof rule === "object" && typeof rule["attribute"] === "string" && typeof rule["value"] === "string" && validOperators.includes(
124
+ rule["operator"]
125
+ );
88
126
  }
89
127
  saveToCache() {
90
- this.storage.set("feature-flags", {
128
+ this.storage.set("flaggy", {
91
129
  flags: this.flags,
130
+ segments: this.segments,
92
131
  timestamp: this.lastFetch
93
132
  });
94
133
  }
95
- // Check if valid flags exist in cache
134
+ isCacheStale(timestamp) {
135
+ const age = Date.now() - timestamp;
136
+ return age > this.getRefreshIntervalMs();
137
+ }
96
138
  hasCachedFlags() {
97
- const cached = this.storage.get("feature-flags");
98
- return cached ? this.isValidCachedData(cached) : false;
139
+ const cache = this.storage.get("flaggy");
140
+ if (!cache) return false;
141
+ const isValidCache = this.isValidCachedData(cache);
142
+ if (!isValidCache) return false;
143
+ const isCacheStale = this.isCacheStale(cache.timestamp);
144
+ if (isCacheStale) return false;
145
+ return true;
99
146
  }
100
147
  // Check if cache interval has elapsed and a fetch is needed
101
148
  shouldFetch() {
@@ -125,10 +172,10 @@ var FeatureFlagClient = class {
125
172
  }
126
173
  async doFetch() {
127
174
  const controller = new AbortController();
128
- const timeoutId = setTimeout(() => controller.abort(), 2e3);
175
+ const timeoutId = setTimeout(() => controller.abort(), 3e3);
129
176
  try {
130
177
  const response = await fetch(
131
- "https://api.flaggy.io/public/feature-flags",
178
+ `https://api.flaggy.io/public/projections?environment=${this.environment}`,
132
179
  {
133
180
  method: "GET",
134
181
  headers: {
@@ -147,19 +194,34 @@ var FeatureFlagClient = class {
147
194
  if (!json || typeof json !== "object") {
148
195
  throw new Error("Invalid API response: expected JSON object");
149
196
  }
150
- if (!json.data) {
151
- throw new Error("Invalid API response: missing data property");
152
- }
153
197
  const data = json.data;
154
- if (!Array.isArray(data)) {
155
- throw new Error("Invalid API response: data must be an array");
198
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
199
+ throw new Error("Invalid API response: missing or invalid data object");
156
200
  }
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"
201
+ if (!data.flags || typeof data.flags !== "object" || Array.isArray(data.flags)) {
202
+ throw new Error(
203
+ "Invalid API response: missing or invalid flags object"
161
204
  );
162
205
  }
206
+ if (!data.segments || typeof data.segments !== "object" || Array.isArray(data.segments)) {
207
+ throw new Error(
208
+ "Invalid API response: missing or invalid segments object"
209
+ );
210
+ }
211
+ const flags = /* @__PURE__ */ Object.create(null);
212
+ for (const [key, value] of Object.entries(data.flags)) {
213
+ if (this.isValidFlag(value)) {
214
+ flags[key] = value;
215
+ }
216
+ }
217
+ const segments = /* @__PURE__ */ Object.create(null);
218
+ for (const [key, value] of Object.entries(data.segments)) {
219
+ if (Array.isArray(value) && value.every(this.isValidSegmentRule)) {
220
+ segments[key] = value;
221
+ }
222
+ }
223
+ this.flags = flags;
224
+ this.segments = segments;
163
225
  this.lastFetch = Date.now();
164
226
  this.failureCount = 0;
165
227
  this.saveToCache();
@@ -182,53 +244,49 @@ var FeatureFlagClient = class {
182
244
  if (this.hasCachedFlags()) {
183
245
  return Promise.resolve();
184
246
  }
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
- ]);
247
+ return this.fetchFlags();
195
248
  }
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;
249
+ evaluateRule(rule, context) {
250
+ const attribute = context[rule.attribute];
251
+ if (attribute === void 0) return false;
252
+ const actual = attribute.toString();
253
+ switch (rule.operator) {
254
+ case "equals":
255
+ return actual === rule.value;
256
+ case "not_equals":
257
+ return actual !== rule.value;
258
+ case "contains":
259
+ return actual.includes(rule.value);
260
+ case "not_contains":
261
+ return !actual.includes(rule.value);
262
+ case "starts_with":
263
+ return actual.startsWith(rule.value);
264
+ case "ends_with":
265
+ return actual.endsWith(rule.value);
211
266
  default:
212
- return defaultValue;
267
+ return false;
213
268
  }
214
269
  }
215
- getFlag(flagName) {
270
+ matchesSegment(rules, context) {
271
+ return rules.every((rule) => this.evaluateRule(rule, context));
272
+ }
273
+ isEnabled(flagName, context, defaultValue = false) {
216
274
  if (this.shouldFetch() && !this.isFetching) {
217
275
  this.fetchFlags();
218
276
  }
219
- return this.flags.find((f) => f.key === flagName);
277
+ const flag = this.flags[flagName];
278
+ if (!flag) return defaultValue;
279
+ const applicableSegments = flag.applicable_segments;
280
+ if (!flag.enabled) return false;
281
+ if (applicableSegments.length === 0) return true;
282
+ if (!context) return false;
283
+ return applicableSegments.some((segmentKey) => {
284
+ const rules = this.segments[segmentKey];
285
+ return rules !== void 0 && this.matchesSegment(rules, context);
286
+ });
220
287
  }
221
288
  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();
289
+ return { ...this.flags };
232
290
  }
233
291
  };
234
292
  var globalClient = null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","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"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["export interface FeatureFlag {\n enabled: boolean;\n applicable_segments: string[];\n}\n\nexport type Operator =\n | \"equals\"\n | \"not_equals\"\n | \"contains\"\n | \"not_contains\"\n | \"starts_with\"\n | \"ends_with\";\n\nexport interface SegmentRule {\n attribute: string;\n operator: Operator;\n value: string;\n}\n\nexport type Context = Record<string, string | number | boolean>;\n\nexport interface FeatureFlagConfig {\n apiKey: string;\n environment?: \"production\" | \"staging\" | \"development\";\n onError?: (error: Error) => void;\n}\n\ninterface 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: Record<string, FeatureFlag>;\n segments: Record<string, SegmentRule[]>;\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: Record<string, FeatureFlag> = {};\n private segments: Record<string, SegmentRule[]> = {};\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(\"flaggy\");\n if (cached && this.isValidCachedData(cached)) {\n const flags = Object.create(null) as Record<string, FeatureFlag>;\n for (const [key, value] of Object.entries(cached.flags)) {\n if (this.isValidFlag(value)) {\n flags[key] = value;\n }\n }\n\n const segments = Object.create(null) as Record<string, SegmentRule[]>;\n for (const [key, value] of Object.entries(cached.segments)) {\n if (Array.isArray(value) && value.every(this.isValidSegmentRule)) {\n segments[key] = value as SegmentRule[];\n }\n }\n\n this.flags = flags;\n this.segments = segments;\n this.lastFetch = cached.timestamp || 0;\n }\n }\n\n private isValidCachedData(data: any): data is CachedData {\n if (\n !data ||\n typeof data !== \"object\" ||\n typeof data.timestamp !== \"number\"\n ) {\n return false;\n }\n if (\n !data.flags ||\n typeof data.flags !== \"object\" ||\n Array.isArray(data.flags)\n ) {\n return false;\n }\n if (\n !data.segments ||\n typeof data.segments !== \"object\" ||\n Array.isArray(data.segments)\n ) {\n return false;\n }\n return (\n Object.values(data.flags).every(this.isValidFlag) &&\n Object.values(data.segments).every(\n (rules) => Array.isArray(rules) && rules.every(this.isValidSegmentRule),\n )\n );\n }\n\n private isValidFlag(flag: unknown): flag is FeatureFlag {\n return (\n flag !== null &&\n typeof flag === \"object\" &&\n typeof (flag as Record<string, unknown>)[\"enabled\"] === \"boolean\" &&\n Array.isArray((flag as Record<string, unknown>)[\"applicable_segments\"]) &&\n (\n (flag as Record<string, unknown>)[\"applicable_segments\"] as unknown[]\n ).every((s) => typeof s === \"string\")\n );\n }\n\n private isValidSegmentRule(rule: unknown): rule is SegmentRule {\n const validOperators: Operator[] = [\n \"equals\",\n \"not_equals\",\n \"contains\",\n \"not_contains\",\n \"starts_with\",\n \"ends_with\",\n ];\n return (\n rule !== null &&\n typeof rule === \"object\" &&\n typeof (rule as Record<string, unknown>)[\"attribute\"] === \"string\" &&\n typeof (rule as Record<string, unknown>)[\"value\"] === \"string\" &&\n validOperators.includes(\n (rule as Record<string, unknown>)[\"operator\"] as Operator,\n )\n );\n }\n\n private saveToCache(): void {\n this.storage.set(\"flaggy\", {\n flags: this.flags,\n segments: this.segments,\n timestamp: this.lastFetch,\n });\n }\n\n private isCacheStale(timestamp: number): boolean {\n const age = Date.now() - timestamp;\n return age > this.getRefreshIntervalMs();\n }\n\n private hasCachedFlags(): boolean {\n const cache = this.storage.get(\"flaggy\");\n\n if (!cache) return false;\n\n const isValidCache = this.isValidCachedData(cache);\n\n if (!isValidCache) return false;\n\n const isCacheStale = this.isCacheStale(cache.timestamp);\n\n if (isCacheStale) return false;\n\n return true;\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 private getRefreshIntervalMs(): number {\n const backoffMs = this.baseRefreshMs * Math.pow(2, this.failureCount);\n return Math.min(backoffMs, this.maxRefreshMs);\n }\n\n private 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(), 3000);\n\n try {\n const response = await fetch(\n `https://api.flaggy.io/public/projections?environment=${this.environment}`,\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 const data = json.data;\n if (!data || typeof data !== \"object\" || Array.isArray(data)) {\n throw new Error(\"Invalid API response: missing or invalid data object\");\n }\n\n if (\n !data.flags ||\n typeof data.flags !== \"object\" ||\n Array.isArray(data.flags)\n ) {\n throw new Error(\n \"Invalid API response: missing or invalid flags object\",\n );\n }\n\n if (\n !data.segments ||\n typeof data.segments !== \"object\" ||\n Array.isArray(data.segments)\n ) {\n throw new Error(\n \"Invalid API response: missing or invalid segments object\",\n );\n }\n\n const flags = Object.create(null) as Record<string, FeatureFlag>;\n for (const [key, value] of Object.entries(data.flags)) {\n if (this.isValidFlag(value)) {\n flags[key] = value;\n }\n }\n\n const segments = Object.create(null) as Record<string, SegmentRule[]>;\n for (const [key, value] of Object.entries(data.segments)) {\n if (Array.isArray(value) && value.every(this.isValidSegmentRule)) {\n segments[key] = value as SegmentRule[];\n }\n }\n\n this.flags = flags;\n this.segments = segments;\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 return this.fetchFlags();\n }\n\n private evaluateRule(rule: SegmentRule, context: Context): boolean {\n const attribute = context[rule.attribute];\n\n if (attribute === undefined) return false;\n\n const actual = attribute.toString();\n\n switch (rule.operator) {\n case \"equals\":\n return actual === rule.value;\n case \"not_equals\":\n return actual !== rule.value;\n case \"contains\":\n return actual.includes(rule.value);\n case \"not_contains\":\n return !actual.includes(rule.value);\n case \"starts_with\":\n return actual.startsWith(rule.value);\n case \"ends_with\":\n return actual.endsWith(rule.value);\n default:\n return false;\n }\n }\n\n private matchesSegment(rules: SegmentRule[], context: Context): boolean {\n return rules.every((rule) => this.evaluateRule(rule, context));\n }\n\n public isEnabled(\n flagName: string,\n context?: Context,\n defaultValue: boolean = false,\n ): 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[flagName];\n\n if (!flag) return defaultValue;\n\n const applicableSegments = flag.applicable_segments;\n\n if (!flag.enabled) return false;\n if (applicableSegments.length === 0) return true;\n if (!context) return false;\n\n return applicableSegments.some((segmentKey) => {\n const rules = this.segments[segmentKey];\n return rules !== undefined && this.matchesSegment(rules, context);\n });\n }\n\n public getAllFlags(): Record<string, FeatureFlag> {\n return { ...this.flags };\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/dist/index.d.cts CHANGED
@@ -1,27 +1,24 @@
1
1
  interface FeatureFlag {
2
- key: string;
3
- enabled_production: boolean;
4
- enabled_staging: boolean;
5
- enabled_development: boolean;
2
+ enabled: boolean;
3
+ applicable_segments: string[];
6
4
  }
5
+ type Operator = "equals" | "not_equals" | "contains" | "not_contains" | "starts_with" | "ends_with";
6
+ interface SegmentRule {
7
+ attribute: string;
8
+ operator: Operator;
9
+ value: string;
10
+ }
11
+ type Context = Record<string, string | number | boolean>;
7
12
  interface FeatureFlagConfig {
8
13
  apiKey: string;
9
- environment?: "production" | "staging" | "development" | string;
14
+ environment?: "production" | "staging" | "development";
10
15
  onError?: (error: Error) => void;
11
16
  }
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
17
  declare class FeatureFlagClient {
22
18
  private config;
23
19
  private storage;
24
20
  private flags;
21
+ private segments;
25
22
  private lastFetch;
26
23
  private isFetching;
27
24
  private fetchPromise;
@@ -33,20 +30,21 @@ declare class FeatureFlagClient {
33
30
  private detectEnvironment;
34
31
  private loadFromCache;
35
32
  private isValidCachedData;
36
- private isValidFeatureFlag;
33
+ private isValidFlag;
34
+ private isValidSegmentRule;
37
35
  private saveToCache;
36
+ private isCacheStale;
38
37
  private hasCachedFlags;
39
38
  private shouldFetch;
40
- getRefreshIntervalMs(): number;
41
- fetchFlags(): Promise<void>;
39
+ private getRefreshIntervalMs;
40
+ private fetchFlags;
42
41
  private doFetch;
43
42
  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;
43
+ private evaluateRule;
44
+ private matchesSegment;
45
+ isEnabled(flagName: string, context?: Context, defaultValue?: boolean): boolean;
46
+ getAllFlags(): Record<string, FeatureFlag>;
49
47
  }
50
48
  declare function flaggy(config: FeatureFlagConfig): FeatureFlagClient;
51
49
 
52
- export { type FeatureFlag, type FeatureFlagConfig, type StorageAdapter, flaggy };
50
+ export { type Context, type FeatureFlag, type FeatureFlagConfig, type Operator, type SegmentRule, flaggy };
package/dist/index.d.ts CHANGED
@@ -1,27 +1,24 @@
1
1
  interface FeatureFlag {
2
- key: string;
3
- enabled_production: boolean;
4
- enabled_staging: boolean;
5
- enabled_development: boolean;
2
+ enabled: boolean;
3
+ applicable_segments: string[];
6
4
  }
5
+ type Operator = "equals" | "not_equals" | "contains" | "not_contains" | "starts_with" | "ends_with";
6
+ interface SegmentRule {
7
+ attribute: string;
8
+ operator: Operator;
9
+ value: string;
10
+ }
11
+ type Context = Record<string, string | number | boolean>;
7
12
  interface FeatureFlagConfig {
8
13
  apiKey: string;
9
- environment?: "production" | "staging" | "development" | string;
14
+ environment?: "production" | "staging" | "development";
10
15
  onError?: (error: Error) => void;
11
16
  }
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
17
  declare class FeatureFlagClient {
22
18
  private config;
23
19
  private storage;
24
20
  private flags;
21
+ private segments;
25
22
  private lastFetch;
26
23
  private isFetching;
27
24
  private fetchPromise;
@@ -33,20 +30,21 @@ declare class FeatureFlagClient {
33
30
  private detectEnvironment;
34
31
  private loadFromCache;
35
32
  private isValidCachedData;
36
- private isValidFeatureFlag;
33
+ private isValidFlag;
34
+ private isValidSegmentRule;
37
35
  private saveToCache;
36
+ private isCacheStale;
38
37
  private hasCachedFlags;
39
38
  private shouldFetch;
40
- getRefreshIntervalMs(): number;
41
- fetchFlags(): Promise<void>;
39
+ private getRefreshIntervalMs;
40
+ private fetchFlags;
42
41
  private doFetch;
43
42
  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;
43
+ private evaluateRule;
44
+ private matchesSegment;
45
+ isEnabled(flagName: string, context?: Context, defaultValue?: boolean): boolean;
46
+ getAllFlags(): Record<string, FeatureFlag>;
49
47
  }
50
48
  declare function flaggy(config: FeatureFlagConfig): FeatureFlagClient;
51
49
 
52
- export { type FeatureFlag, type FeatureFlagConfig, type StorageAdapter, flaggy };
50
+ export { type Context, type FeatureFlag, type FeatureFlagConfig, type Operator, type SegmentRule, flaggy };
package/dist/index.js CHANGED
@@ -49,7 +49,8 @@ var InMemoryStorageAdapter = class {
49
49
  };
50
50
  var FeatureFlagClient = class {
51
51
  constructor(config) {
52
- this.flags = [];
52
+ this.flags = {};
53
+ this.segments = {};
53
54
  this.lastFetch = 0;
54
55
  this.isFetching = false;
55
56
  this.fetchPromise = null;
@@ -72,28 +73,74 @@ var FeatureFlagClient = class {
72
73
  return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
73
74
  }
74
75
  loadFromCache() {
75
- const cached = this.storage.get("feature-flags");
76
+ const cached = this.storage.get("flaggy");
76
77
  if (cached && this.isValidCachedData(cached)) {
77
- this.flags = cached.flags;
78
+ const flags = /* @__PURE__ */ Object.create(null);
79
+ for (const [key, value] of Object.entries(cached.flags)) {
80
+ if (this.isValidFlag(value)) {
81
+ flags[key] = value;
82
+ }
83
+ }
84
+ const segments = /* @__PURE__ */ Object.create(null);
85
+ for (const [key, value] of Object.entries(cached.segments)) {
86
+ if (Array.isArray(value) && value.every(this.isValidSegmentRule)) {
87
+ segments[key] = value;
88
+ }
89
+ }
90
+ this.flags = flags;
91
+ this.segments = segments;
78
92
  this.lastFetch = cached.timestamp || 0;
79
93
  }
80
94
  }
81
95
  isValidCachedData(data) {
82
- return data && typeof data === "object" && Array.isArray(data.flags) && data.flags.every(this.isValidFeatureFlag) && typeof data.timestamp === "number";
96
+ if (!data || typeof data !== "object" || typeof data.timestamp !== "number") {
97
+ return false;
98
+ }
99
+ if (!data.flags || typeof data.flags !== "object" || Array.isArray(data.flags)) {
100
+ return false;
101
+ }
102
+ if (!data.segments || typeof data.segments !== "object" || Array.isArray(data.segments)) {
103
+ return false;
104
+ }
105
+ return Object.values(data.flags).every(this.isValidFlag) && Object.values(data.segments).every(
106
+ (rules) => Array.isArray(rules) && rules.every(this.isValidSegmentRule)
107
+ );
108
+ }
109
+ isValidFlag(flag) {
110
+ return flag !== null && typeof flag === "object" && typeof flag["enabled"] === "boolean" && Array.isArray(flag["applicable_segments"]) && flag["applicable_segments"].every((s) => typeof s === "string");
83
111
  }
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";
112
+ isValidSegmentRule(rule) {
113
+ const validOperators = [
114
+ "equals",
115
+ "not_equals",
116
+ "contains",
117
+ "not_contains",
118
+ "starts_with",
119
+ "ends_with"
120
+ ];
121
+ return rule !== null && typeof rule === "object" && typeof rule["attribute"] === "string" && typeof rule["value"] === "string" && validOperators.includes(
122
+ rule["operator"]
123
+ );
86
124
  }
87
125
  saveToCache() {
88
- this.storage.set("feature-flags", {
126
+ this.storage.set("flaggy", {
89
127
  flags: this.flags,
128
+ segments: this.segments,
90
129
  timestamp: this.lastFetch
91
130
  });
92
131
  }
93
- // Check if valid flags exist in cache
132
+ isCacheStale(timestamp) {
133
+ const age = Date.now() - timestamp;
134
+ return age > this.getRefreshIntervalMs();
135
+ }
94
136
  hasCachedFlags() {
95
- const cached = this.storage.get("feature-flags");
96
- return cached ? this.isValidCachedData(cached) : false;
137
+ const cache = this.storage.get("flaggy");
138
+ if (!cache) return false;
139
+ const isValidCache = this.isValidCachedData(cache);
140
+ if (!isValidCache) return false;
141
+ const isCacheStale = this.isCacheStale(cache.timestamp);
142
+ if (isCacheStale) return false;
143
+ return true;
97
144
  }
98
145
  // Check if cache interval has elapsed and a fetch is needed
99
146
  shouldFetch() {
@@ -123,10 +170,10 @@ var FeatureFlagClient = class {
123
170
  }
124
171
  async doFetch() {
125
172
  const controller = new AbortController();
126
- const timeoutId = setTimeout(() => controller.abort(), 2e3);
173
+ const timeoutId = setTimeout(() => controller.abort(), 3e3);
127
174
  try {
128
175
  const response = await fetch(
129
- "https://api.flaggy.io/public/feature-flags",
176
+ `https://api.flaggy.io/public/projections?environment=${this.environment}`,
130
177
  {
131
178
  method: "GET",
132
179
  headers: {
@@ -145,19 +192,34 @@ var FeatureFlagClient = class {
145
192
  if (!json || typeof json !== "object") {
146
193
  throw new Error("Invalid API response: expected JSON object");
147
194
  }
148
- if (!json.data) {
149
- throw new Error("Invalid API response: missing data property");
150
- }
151
195
  const data = json.data;
152
- if (!Array.isArray(data)) {
153
- throw new Error("Invalid API response: data must be an array");
196
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
197
+ throw new Error("Invalid API response: missing or invalid data object");
154
198
  }
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"
199
+ if (!data.flags || typeof data.flags !== "object" || Array.isArray(data.flags)) {
200
+ throw new Error(
201
+ "Invalid API response: missing or invalid flags object"
159
202
  );
160
203
  }
204
+ if (!data.segments || typeof data.segments !== "object" || Array.isArray(data.segments)) {
205
+ throw new Error(
206
+ "Invalid API response: missing or invalid segments object"
207
+ );
208
+ }
209
+ const flags = /* @__PURE__ */ Object.create(null);
210
+ for (const [key, value] of Object.entries(data.flags)) {
211
+ if (this.isValidFlag(value)) {
212
+ flags[key] = value;
213
+ }
214
+ }
215
+ const segments = /* @__PURE__ */ Object.create(null);
216
+ for (const [key, value] of Object.entries(data.segments)) {
217
+ if (Array.isArray(value) && value.every(this.isValidSegmentRule)) {
218
+ segments[key] = value;
219
+ }
220
+ }
221
+ this.flags = flags;
222
+ this.segments = segments;
161
223
  this.lastFetch = Date.now();
162
224
  this.failureCount = 0;
163
225
  this.saveToCache();
@@ -180,53 +242,49 @@ var FeatureFlagClient = class {
180
242
  if (this.hasCachedFlags()) {
181
243
  return Promise.resolve();
182
244
  }
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
- ]);
245
+ return this.fetchFlags();
193
246
  }
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;
247
+ evaluateRule(rule, context) {
248
+ const attribute = context[rule.attribute];
249
+ if (attribute === void 0) return false;
250
+ const actual = attribute.toString();
251
+ switch (rule.operator) {
252
+ case "equals":
253
+ return actual === rule.value;
254
+ case "not_equals":
255
+ return actual !== rule.value;
256
+ case "contains":
257
+ return actual.includes(rule.value);
258
+ case "not_contains":
259
+ return !actual.includes(rule.value);
260
+ case "starts_with":
261
+ return actual.startsWith(rule.value);
262
+ case "ends_with":
263
+ return actual.endsWith(rule.value);
209
264
  default:
210
- return defaultValue;
265
+ return false;
211
266
  }
212
267
  }
213
- getFlag(flagName) {
268
+ matchesSegment(rules, context) {
269
+ return rules.every((rule) => this.evaluateRule(rule, context));
270
+ }
271
+ isEnabled(flagName, context, defaultValue = false) {
214
272
  if (this.shouldFetch() && !this.isFetching) {
215
273
  this.fetchFlags();
216
274
  }
217
- return this.flags.find((f) => f.key === flagName);
275
+ const flag = this.flags[flagName];
276
+ if (!flag) return defaultValue;
277
+ const applicableSegments = flag.applicable_segments;
278
+ if (!flag.enabled) return false;
279
+ if (applicableSegments.length === 0) return true;
280
+ if (!context) return false;
281
+ return applicableSegments.some((segmentKey) => {
282
+ const rules = this.segments[segmentKey];
283
+ return rules !== void 0 && this.matchesSegment(rules, context);
284
+ });
218
285
  }
219
286
  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();
287
+ return { ...this.flags };
230
288
  }
231
289
  };
232
290
  var globalClient = null;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","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"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export interface FeatureFlag {\n enabled: boolean;\n applicable_segments: string[];\n}\n\nexport type Operator =\n | \"equals\"\n | \"not_equals\"\n | \"contains\"\n | \"not_contains\"\n | \"starts_with\"\n | \"ends_with\";\n\nexport interface SegmentRule {\n attribute: string;\n operator: Operator;\n value: string;\n}\n\nexport type Context = Record<string, string | number | boolean>;\n\nexport interface FeatureFlagConfig {\n apiKey: string;\n environment?: \"production\" | \"staging\" | \"development\";\n onError?: (error: Error) => void;\n}\n\ninterface 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: Record<string, FeatureFlag>;\n segments: Record<string, SegmentRule[]>;\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: Record<string, FeatureFlag> = {};\n private segments: Record<string, SegmentRule[]> = {};\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(\"flaggy\");\n if (cached && this.isValidCachedData(cached)) {\n const flags = Object.create(null) as Record<string, FeatureFlag>;\n for (const [key, value] of Object.entries(cached.flags)) {\n if (this.isValidFlag(value)) {\n flags[key] = value;\n }\n }\n\n const segments = Object.create(null) as Record<string, SegmentRule[]>;\n for (const [key, value] of Object.entries(cached.segments)) {\n if (Array.isArray(value) && value.every(this.isValidSegmentRule)) {\n segments[key] = value as SegmentRule[];\n }\n }\n\n this.flags = flags;\n this.segments = segments;\n this.lastFetch = cached.timestamp || 0;\n }\n }\n\n private isValidCachedData(data: any): data is CachedData {\n if (\n !data ||\n typeof data !== \"object\" ||\n typeof data.timestamp !== \"number\"\n ) {\n return false;\n }\n if (\n !data.flags ||\n typeof data.flags !== \"object\" ||\n Array.isArray(data.flags)\n ) {\n return false;\n }\n if (\n !data.segments ||\n typeof data.segments !== \"object\" ||\n Array.isArray(data.segments)\n ) {\n return false;\n }\n return (\n Object.values(data.flags).every(this.isValidFlag) &&\n Object.values(data.segments).every(\n (rules) => Array.isArray(rules) && rules.every(this.isValidSegmentRule),\n )\n );\n }\n\n private isValidFlag(flag: unknown): flag is FeatureFlag {\n return (\n flag !== null &&\n typeof flag === \"object\" &&\n typeof (flag as Record<string, unknown>)[\"enabled\"] === \"boolean\" &&\n Array.isArray((flag as Record<string, unknown>)[\"applicable_segments\"]) &&\n (\n (flag as Record<string, unknown>)[\"applicable_segments\"] as unknown[]\n ).every((s) => typeof s === \"string\")\n );\n }\n\n private isValidSegmentRule(rule: unknown): rule is SegmentRule {\n const validOperators: Operator[] = [\n \"equals\",\n \"not_equals\",\n \"contains\",\n \"not_contains\",\n \"starts_with\",\n \"ends_with\",\n ];\n return (\n rule !== null &&\n typeof rule === \"object\" &&\n typeof (rule as Record<string, unknown>)[\"attribute\"] === \"string\" &&\n typeof (rule as Record<string, unknown>)[\"value\"] === \"string\" &&\n validOperators.includes(\n (rule as Record<string, unknown>)[\"operator\"] as Operator,\n )\n );\n }\n\n private saveToCache(): void {\n this.storage.set(\"flaggy\", {\n flags: this.flags,\n segments: this.segments,\n timestamp: this.lastFetch,\n });\n }\n\n private isCacheStale(timestamp: number): boolean {\n const age = Date.now() - timestamp;\n return age > this.getRefreshIntervalMs();\n }\n\n private hasCachedFlags(): boolean {\n const cache = this.storage.get(\"flaggy\");\n\n if (!cache) return false;\n\n const isValidCache = this.isValidCachedData(cache);\n\n if (!isValidCache) return false;\n\n const isCacheStale = this.isCacheStale(cache.timestamp);\n\n if (isCacheStale) return false;\n\n return true;\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 private getRefreshIntervalMs(): number {\n const backoffMs = this.baseRefreshMs * Math.pow(2, this.failureCount);\n return Math.min(backoffMs, this.maxRefreshMs);\n }\n\n private 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(), 3000);\n\n try {\n const response = await fetch(\n `https://api.flaggy.io/public/projections?environment=${this.environment}`,\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 const data = json.data;\n if (!data || typeof data !== \"object\" || Array.isArray(data)) {\n throw new Error(\"Invalid API response: missing or invalid data object\");\n }\n\n if (\n !data.flags ||\n typeof data.flags !== \"object\" ||\n Array.isArray(data.flags)\n ) {\n throw new Error(\n \"Invalid API response: missing or invalid flags object\",\n );\n }\n\n if (\n !data.segments ||\n typeof data.segments !== \"object\" ||\n Array.isArray(data.segments)\n ) {\n throw new Error(\n \"Invalid API response: missing or invalid segments object\",\n );\n }\n\n const flags = Object.create(null) as Record<string, FeatureFlag>;\n for (const [key, value] of Object.entries(data.flags)) {\n if (this.isValidFlag(value)) {\n flags[key] = value;\n }\n }\n\n const segments = Object.create(null) as Record<string, SegmentRule[]>;\n for (const [key, value] of Object.entries(data.segments)) {\n if (Array.isArray(value) && value.every(this.isValidSegmentRule)) {\n segments[key] = value as SegmentRule[];\n }\n }\n\n this.flags = flags;\n this.segments = segments;\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 return this.fetchFlags();\n }\n\n private evaluateRule(rule: SegmentRule, context: Context): boolean {\n const attribute = context[rule.attribute];\n\n if (attribute === undefined) return false;\n\n const actual = attribute.toString();\n\n switch (rule.operator) {\n case \"equals\":\n return actual === rule.value;\n case \"not_equals\":\n return actual !== rule.value;\n case \"contains\":\n return actual.includes(rule.value);\n case \"not_contains\":\n return !actual.includes(rule.value);\n case \"starts_with\":\n return actual.startsWith(rule.value);\n case \"ends_with\":\n return actual.endsWith(rule.value);\n default:\n return false;\n }\n }\n\n private matchesSegment(rules: SegmentRule[], context: Context): boolean {\n return rules.every((rule) => this.evaluateRule(rule, context));\n }\n\n public isEnabled(\n flagName: string,\n context?: Context,\n defaultValue: boolean = false,\n ): 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[flagName];\n\n if (!flag) return defaultValue;\n\n const applicableSegments = flag.applicable_segments;\n\n if (!flag.enabled) return false;\n if (applicableSegments.length === 0) return true;\n if (!context) return false;\n\n return applicableSegments.some((segmentKey) => {\n const rules = this.segments[segmentKey];\n return rules !== undefined && this.matchesSegment(rules, context);\n });\n }\n\n public getAllFlags(): Record<string, FeatureFlag> {\n return { ...this.flags };\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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flaggy.io/sdk-js",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "TypeScript SDK for Flaggy.io feature flag management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -23,6 +23,7 @@
23
23
  "keywords": [
24
24
  "feature-flags",
25
25
  "feature-toggles",
26
+ "segments",
26
27
  "flaggy",
27
28
  "typescript",
28
29
  "sdk"