@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 +121 -232
- package/dist/index.cjs +117 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -23
- package/dist/index.d.ts +21 -23
- package/dist/index.js +117 -59
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
|
-
# Flaggy
|
|
1
|
+
# Flaggy
|
|
2
2
|
|
|
3
|
-
A JavaScript / TypeScript SDK for managing feature flags from Flaggy.io
|
|
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
|
|
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
|
-
##
|
|
16
|
+
## Quick Start
|
|
29
17
|
|
|
30
|
-
|
|
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:
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
44
|
+
That's it for simple on/off flags. Read on for environment configuration, segment targeting, and error handling.
|
|
50
45
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
96
|
+
app.listen(3000);
|
|
126
97
|
```
|
|
127
98
|
|
|
128
|
-
Use throughout your application:
|
|
129
|
-
|
|
130
99
|
```typescript
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
## Segment Targeting
|
|
170
109
|
|
|
171
|
-
|
|
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
|
-
###
|
|
112
|
+
### Passing a context
|
|
177
113
|
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
145
|
+
### Rule operators
|
|
224
146
|
|
|
225
|
-
|
|
147
|
+
Each rule compares a context attribute to a value using one of:
|
|
226
148
|
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
+
---
|
|
233
161
|
|
|
234
|
-
|
|
162
|
+
## Configuration Options
|
|
235
163
|
|
|
236
164
|
```typescript
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
+
---
|
|
251
178
|
|
|
252
|
-
|
|
179
|
+
## Caching & Refresh
|
|
253
180
|
|
|
254
|
-
|
|
181
|
+
The SDK caches flags locally and refreshes them automatically in the background.
|
|
255
182
|
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
+
---
|
|
261
189
|
|
|
262
|
-
|
|
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
|
-
|
|
192
|
+
### `flaggy(config): FeatureFlagClient`
|
|
287
193
|
|
|
288
|
-
|
|
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
|
-
|
|
196
|
+
### `client.initialize(): Promise<void>`
|
|
292
197
|
|
|
293
|
-
|
|
198
|
+
Ensures flags are ready. Call once at application startup before evaluating any flags.
|
|
294
199
|
|
|
295
|
-
|
|
200
|
+
### `client.isEnabled(flagName, context?, defaultValue?): boolean`
|
|
296
201
|
|
|
297
|
-
|
|
202
|
+
Evaluates a flag. Returns `defaultValue` (`false` by default) if the flag doesn't exist.
|
|
298
203
|
|
|
299
|
-
|
|
204
|
+
### `client.getAllFlags(): Record<string, FeatureFlag>`
|
|
300
205
|
|
|
301
|
-
|
|
206
|
+
Returns a shallow copy of all currently cached flags.
|
|
302
207
|
|
|
303
|
-
|
|
208
|
+
---
|
|
304
209
|
|
|
305
|
-
|
|
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
|
-
-
|
|
327
|
-
-
|
|
328
|
-
-
|
|
329
|
-
-
|
|
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("
|
|
78
|
+
const cached = this.storage.get("flaggy");
|
|
78
79
|
if (cached && this.isValidCachedData(cached)) {
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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("
|
|
128
|
+
this.storage.set("flaggy", {
|
|
91
129
|
flags: this.flags,
|
|
130
|
+
segments: this.segments,
|
|
92
131
|
timestamp: this.lastFetch
|
|
93
132
|
});
|
|
94
133
|
}
|
|
95
|
-
|
|
134
|
+
isCacheStale(timestamp) {
|
|
135
|
+
const age = Date.now() - timestamp;
|
|
136
|
+
return age > this.getRefreshIntervalMs();
|
|
137
|
+
}
|
|
96
138
|
hasCachedFlags() {
|
|
97
|
-
const
|
|
98
|
-
|
|
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(),
|
|
175
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
129
176
|
try {
|
|
130
177
|
const response = await fetch(
|
|
131
|
-
|
|
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:
|
|
198
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
199
|
+
throw new Error("Invalid API response: missing or invalid data object");
|
|
156
200
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
case "
|
|
206
|
-
return
|
|
207
|
-
case "
|
|
208
|
-
return
|
|
209
|
-
case "
|
|
210
|
-
return
|
|
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
|
|
267
|
+
return false;
|
|
213
268
|
}
|
|
214
269
|
}
|
|
215
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
-
|
|
3
|
-
|
|
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"
|
|
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
|
|
33
|
+
private isValidFlag;
|
|
34
|
+
private isValidSegmentRule;
|
|
37
35
|
private saveToCache;
|
|
36
|
+
private isCacheStale;
|
|
38
37
|
private hasCachedFlags;
|
|
39
38
|
private shouldFetch;
|
|
40
|
-
getRefreshIntervalMs
|
|
41
|
-
fetchFlags
|
|
39
|
+
private getRefreshIntervalMs;
|
|
40
|
+
private fetchFlags;
|
|
42
41
|
private doFetch;
|
|
43
42
|
initialize(): Promise<void>;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
3
|
-
|
|
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"
|
|
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
|
|
33
|
+
private isValidFlag;
|
|
34
|
+
private isValidSegmentRule;
|
|
37
35
|
private saveToCache;
|
|
36
|
+
private isCacheStale;
|
|
38
37
|
private hasCachedFlags;
|
|
39
38
|
private shouldFetch;
|
|
40
|
-
getRefreshIntervalMs
|
|
41
|
-
fetchFlags
|
|
39
|
+
private getRefreshIntervalMs;
|
|
40
|
+
private fetchFlags;
|
|
42
41
|
private doFetch;
|
|
43
42
|
initialize(): Promise<void>;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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("
|
|
76
|
+
const cached = this.storage.get("flaggy");
|
|
76
77
|
if (cached && this.isValidCachedData(cached)) {
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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("
|
|
126
|
+
this.storage.set("flaggy", {
|
|
89
127
|
flags: this.flags,
|
|
128
|
+
segments: this.segments,
|
|
90
129
|
timestamp: this.lastFetch
|
|
91
130
|
});
|
|
92
131
|
}
|
|
93
|
-
|
|
132
|
+
isCacheStale(timestamp) {
|
|
133
|
+
const age = Date.now() - timestamp;
|
|
134
|
+
return age > this.getRefreshIntervalMs();
|
|
135
|
+
}
|
|
94
136
|
hasCachedFlags() {
|
|
95
|
-
const
|
|
96
|
-
|
|
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(),
|
|
173
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
127
174
|
try {
|
|
128
175
|
const response = await fetch(
|
|
129
|
-
|
|
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:
|
|
196
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
197
|
+
throw new Error("Invalid API response: missing or invalid data object");
|
|
154
198
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
case "
|
|
204
|
-
return
|
|
205
|
-
case "
|
|
206
|
-
return
|
|
207
|
-
case "
|
|
208
|
-
return
|
|
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
|
|
265
|
+
return false;
|
|
211
266
|
}
|
|
212
267
|
}
|
|
213
|
-
|
|
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
|
-
|
|
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
|
|
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": "
|
|
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"
|