@flaggy.io/sdk-js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/index.cjs +243 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +241 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Flaggy.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Flaggy.io Feature Flag SDK
|
|
2
|
+
|
|
3
|
+
A JavaScript / TypeScript SDK for managing feature flags from Flaggy.io that is fully compatible for both client-side (browser) and server-side (Node.js) environments.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Safe defaults**: Optional default values for graceful handling of new users and network failures
|
|
8
|
+
- ✅ **Auto-environment detection**: Uses localStorage in browsers, in-memory storage in Node.js
|
|
9
|
+
- ✅ **Automatic caching**: Reduces API calls and improves performance
|
|
10
|
+
- ✅ **Auto-refresh**: Refreshes on a 60-second base interval with exponential backoff on failures
|
|
11
|
+
- ✅ **Request deduplication**: Prevents concurrent duplicate API requests
|
|
12
|
+
- ✅ **TypeScript support**: Full type safety
|
|
13
|
+
- ✅ **Environment-based flags**: Support for production, staging, and development environments
|
|
14
|
+
- ✅ **Simple configuration**: Only requires an API key
|
|
15
|
+
- ✅ **Input validation**: Validates API keys and response data
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- **Node.js**: 18.0.0 or higher (required for native `fetch()` API support)
|
|
20
|
+
- **Browser**: Any modern browser with ES2020 support
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @flaggy.io/sdk-js
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Client-Side (React)
|
|
29
|
+
|
|
30
|
+
Create a dedicated file (e.g., `src/lib/flaggy.ts`):
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { flaggy } from "@flaggy.io/sdk-js";
|
|
34
|
+
|
|
35
|
+
export const flagClient = flaggy({
|
|
36
|
+
apiKey: import.meta.env.VITE_FLAGGY_API_KEY!,
|
|
37
|
+
environment: import.meta.env.VITE_ENVIRONMENT,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Export a wrapper function for cleaner usage throughout your app
|
|
41
|
+
export function featureEnabled(
|
|
42
|
+
flagName: string,
|
|
43
|
+
defaultValue?: boolean,
|
|
44
|
+
): boolean {
|
|
45
|
+
return flagClient.isEnabled(flagName, defaultValue);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Initialize and wait for flags:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// src/main.tsx
|
|
53
|
+
import { StrictMode } from "react";
|
|
54
|
+
import { createRoot } from "react-dom/client";
|
|
55
|
+
import "./index.css";
|
|
56
|
+
import App from "./App";
|
|
57
|
+
import { flagClient } from "./lib/flaggy";
|
|
58
|
+
|
|
59
|
+
await flagClient.initialize();
|
|
60
|
+
|
|
61
|
+
createRoot(document.getElementById("root")!).render(
|
|
62
|
+
<StrictMode>
|
|
63
|
+
<App />
|
|
64
|
+
</StrictMode>,
|
|
65
|
+
);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Simple usage example:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { featureEnabled } from "./lib/flaggy";
|
|
72
|
+
|
|
73
|
+
export function Header() {
|
|
74
|
+
// Specify safe default for new users (before flags load)
|
|
75
|
+
return featureEnabled("new-header", false) ? <NewHeader /> : <OldHeader />;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Server-Side (Node.js)
|
|
80
|
+
|
|
81
|
+
Create a dedicated client file (e.g., `src/lib/flaggy.ts`):
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { flaggy } from "@flaggy.io/sdk-js";
|
|
85
|
+
|
|
86
|
+
export const flagClient = flaggy({
|
|
87
|
+
apiKey: process.env.FLAGGY_API_KEY!,
|
|
88
|
+
environment: process.env.NODE_ENV,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Export a wrapper function for cleaner usage
|
|
92
|
+
export function featureEnabled(
|
|
93
|
+
flagName: string,
|
|
94
|
+
defaultValue?: boolean,
|
|
95
|
+
): boolean {
|
|
96
|
+
return flagClient.isEnabled(flagName, defaultValue);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then in your server startup file (e.g., `src/server.ts` or `src/index.ts`), **await the initial flag load before starting your server**:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { featureEnabled, flagClient } from "./lib/flaggy";
|
|
104
|
+
import express, { Application, Request, Response } from "express";
|
|
105
|
+
|
|
106
|
+
const app: Application = express();
|
|
107
|
+
const port = process.env.PORT || 3000;
|
|
108
|
+
|
|
109
|
+
app.get("/", (req: Request, res: Response) => {
|
|
110
|
+
if (featureEnabled("new-service")) {
|
|
111
|
+
// Logic for the new service
|
|
112
|
+
} else {
|
|
113
|
+
// Logic for the old service
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
async function start(): Promise<void> {
|
|
118
|
+
await flagClient.initialize();
|
|
119
|
+
|
|
120
|
+
app.listen(port, () => {
|
|
121
|
+
console.log(`Server is Fire at http://localhost:${port}`);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
void start();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Use throughout your application:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { featureEnabled } from "./lib/flaggy";
|
|
132
|
+
|
|
133
|
+
if (featureEnabled("new-algorithm", true)) {
|
|
134
|
+
// Defaulting to enabled
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
To react to fetch errors without throwing, provide an `onError` callback:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const flagClient = flaggy({
|
|
142
|
+
apiKey: import.meta.env.VITE_FLAGGY_API_KEY!,
|
|
143
|
+
onError: (error) => {
|
|
144
|
+
console.warn("Flag fetch failed:", error);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Environment:** The `environment` field is optional and defaults to `"production"`. You can pass raw strings (for example `process.env.NODE_ENV` / `import.meta.env.VITE_ENVIRONMENT`), and the SDK will only use `"production"`, `"staging"`, or `"development"`—any other value falls back to `"production"`.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// Omit entirely (uses "production")
|
|
153
|
+
const flagClient = flaggy({
|
|
154
|
+
apiKey: import.meta.env.VITE_FLAGGY_API_KEY!,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Or pass env directly (SDK normalizes invalid values to "production")
|
|
158
|
+
environment: import.meta.env.VITE_ENVIRONMENT,
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Network Errors:** The SDK logs fetch errors to the console and invokes the `onError` callback if provided. If the API is unreachable, the SDK will continue to use cached flags and retry on the next refresh interval using exponential backoff (base 60 seconds, capped at 15 minutes).
|
|
162
|
+
|
|
163
|
+
**Timeout:** Requests are aborted after 2 seconds to avoid long stalls.
|
|
164
|
+
|
|
165
|
+
## API Reference
|
|
166
|
+
|
|
167
|
+
### Configuration
|
|
168
|
+
|
|
169
|
+
The SDK uses the following hard-coded values:
|
|
170
|
+
|
|
171
|
+
- **API URL**: `https://api.flaggy.io/public/feature-flags`
|
|
172
|
+
- **Storage Key**: `feature-flags`
|
|
173
|
+
- **Refresh Interval**: 60-second base with exponential backoff on failures (capped at 15 minutes)
|
|
174
|
+
- **Authentication**: `x-api-key` header
|
|
175
|
+
|
|
176
|
+
### Feature Flag Structure
|
|
177
|
+
|
|
178
|
+
Each feature flag has the following structure:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
interface FeatureFlag {
|
|
182
|
+
key: string;
|
|
183
|
+
enabled_production: boolean;
|
|
184
|
+
enabled_staging: boolean;
|
|
185
|
+
enabled_development: boolean;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
This allows the same flag to have different states across different environments.
|
|
190
|
+
|
|
191
|
+
### `flaggy(config: FeatureFlagConfig): FeatureFlagClient`
|
|
192
|
+
|
|
193
|
+
Initialize the global feature flag client with your API key and environment.
|
|
194
|
+
|
|
195
|
+
**Factory-only:** `FeatureFlagClient` is not exported, so clients must be created via `flaggy()`.
|
|
196
|
+
|
|
197
|
+
**Singleton Behavior:** Multiple calls to `flaggy()` return the same instance. This makes it safe to call in React components that re-render, and prevents duplicate API requests and race conditions.
|
|
198
|
+
|
|
199
|
+
**Auto-initialization:** Flags are automatically fetched in the background when you instantiate the client via `flaggy()`. The client loads from cache immediately if available and auto-refreshes on a 60-second base interval with exponential backoff on failures.
|
|
200
|
+
|
|
201
|
+
**React / Client-Side:** Call `await client.initialize()` before rendering your app. If cached flags exist in localStorage, `initialize()` returns immediately without blocking—preventing flash screens on page refresh. If no cache exists, it waits up to 2 seconds for the initial fetch.
|
|
202
|
+
|
|
203
|
+
**Node.js / Server Startup:** Call `await client.initialize()` before starting your server. Since the in-memory cache is empty on server restart, `initialize()` will wait up to 2 seconds for the initial fetch to ensure flags are available before accepting requests.
|
|
204
|
+
|
|
205
|
+
**Config Options:**
|
|
206
|
+
|
|
207
|
+
- `apiKey` (required): Your Flaggy.io API key for authentication
|
|
208
|
+
- `environment` (optional): Accepts any string input (e.g. `process.env.NODE_ENV`), but only `'production'`, `'staging'`, and `'development'` are used; all other values default to `'production'`.
|
|
209
|
+
- `onError` (optional): Callback invoked when a flag fetch fails. Does not throw.
|
|
210
|
+
|
|
211
|
+
**Returns:** `FeatureFlagClient` instance
|
|
212
|
+
|
|
213
|
+
**Important:** Configuration is locked after the first call. Subsequent calls with different config values will return the existing instance with the original configuration.
|
|
214
|
+
|
|
215
|
+
## Client API (returned by `flaggy()`)
|
|
216
|
+
|
|
217
|
+
**Caching behavior:**
|
|
218
|
+
|
|
219
|
+
- **Client-side (React/browser):** Flags are loaded from localStorage immediately on instantiation. If cache is stale (> 60 seconds), a background fetch updates the flags without blocking.
|
|
220
|
+
- **Server-side (Node.js):** In-memory cache is empty on server restart, so `initialize()` should be awaited to ensure flags are loaded before serving requests.
|
|
221
|
+
- All flag evaluations (`isEnabled()`, `getFlag()`) trigger automatic background refreshes when the cache interval expires.
|
|
222
|
+
|
|
223
|
+
### `isEnabled(flagName: string, defaultValue?: boolean): boolean`
|
|
224
|
+
|
|
225
|
+
Check if a feature flag is enabled for the configured environment.
|
|
226
|
+
|
|
227
|
+
**Parameters:**
|
|
228
|
+
|
|
229
|
+
- `flagName`: The key of the feature flag to check
|
|
230
|
+
- `defaultValue`: Optional default value to return if flag doesn't exist or isn't loaded yet (defaults to `false`)
|
|
231
|
+
|
|
232
|
+
**Returns:** `boolean` - Whether the flag is enabled
|
|
233
|
+
|
|
234
|
+
**Example:**
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// Safe default for new users (flag not loaded yet)
|
|
238
|
+
if (flagClient.isEnabled("new-ui", false)) {
|
|
239
|
+
// Show new UI only if flag exists and is enabled
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Default to enabled for opt-out features
|
|
243
|
+
if (flagClient.isEnabled("analytics", true)) {
|
|
244
|
+
// Analytics enabled unless explicitly disabled
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Best Practice:** Always specify a safe default value that provides the correct experience for new users before flags load, or during network failures.
|
|
249
|
+
|
|
250
|
+
### `getFlag(flagName: string): FeatureFlag | undefined`
|
|
251
|
+
|
|
252
|
+
Get the full feature flag object with all environment states.
|
|
253
|
+
|
|
254
|
+
### `getAllFlags(): FeatureFlag[]`
|
|
255
|
+
|
|
256
|
+
Get all cached feature flags.
|
|
257
|
+
|
|
258
|
+
### `initialize(): Promise<void>`
|
|
259
|
+
|
|
260
|
+
Initialize the feature flag client. **This is the recommended method to call during application startup.**
|
|
261
|
+
|
|
262
|
+
**Behavior:**
|
|
263
|
+
|
|
264
|
+
- **If cached flags exist** (React/browser with localStorage): Returns immediately without fetching or blocking
|
|
265
|
+
- **If no cache exists** (Node.js server startup or first-time browser load): Waits up to 2 seconds for the initial fetch, then resolves even if the request hasn't finished
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// React: instant return if cache exists, prevents flash screen
|
|
269
|
+
await flagClient.initialize();
|
|
270
|
+
|
|
271
|
+
// Node.js: waits for initial fetch since cache is always empty on restart
|
|
272
|
+
await flagClient.initialize();
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Use cases:**
|
|
276
|
+
|
|
277
|
+
- React/browser: Prevents blocking on page refresh when flags are cached
|
|
278
|
+
- Node.js: Ensures flags are loaded before accepting requests
|
|
279
|
+
|
|
280
|
+
### `async fetchFlags(): Promise<void>`
|
|
281
|
+
|
|
282
|
+
Manually fetch flags from the API. Use this for manual refreshes after the initial setup.
|
|
283
|
+
|
|
284
|
+
**Rate Limiting:** Respects the refresh interval. Multiple calls within the interval will return immediately without fetching. The interval uses a 60-second base and exponential backoff on failures (capped at 15 minutes).
|
|
285
|
+
|
|
286
|
+
**Use cases:**
|
|
287
|
+
|
|
288
|
+
- Manual refresh outside the automatic 60-second interval
|
|
289
|
+
- Testing scenarios where you need deterministic flag state
|
|
290
|
+
|
|
291
|
+
**Note:** `fetchFlags()` always respects the refresh interval. There is no force-refresh API; after calling `clearCache()`, the SDK will refresh on the next interval or when `initialize()` decides a fetch is needed.
|
|
292
|
+
|
|
293
|
+
### `clearCache(): void`
|
|
294
|
+
|
|
295
|
+
Clear all cached flags.
|
|
296
|
+
|
|
297
|
+
### `getRefreshIntervalMs(): number`
|
|
298
|
+
|
|
299
|
+
Returns the current refresh interval in milliseconds. The SDK uses a 60-second base interval and applies exponential backoff on failures, capped at 15 minutes.
|
|
300
|
+
|
|
301
|
+
## API Response Format
|
|
302
|
+
|
|
303
|
+
The Flaggy.io API returns feature flags in the following format:
|
|
304
|
+
|
|
305
|
+
```json
|
|
306
|
+
{
|
|
307
|
+
"data": [
|
|
308
|
+
{
|
|
309
|
+
"key": "dark-mode",
|
|
310
|
+
"enabled_production": true,
|
|
311
|
+
"enabled_staging": true,
|
|
312
|
+
"enabled_development": true
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"key": "new-feature",
|
|
316
|
+
"enabled_production": false,
|
|
317
|
+
"enabled_staging": true,
|
|
318
|
+
"enabled_development": true
|
|
319
|
+
}
|
|
320
|
+
]
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The SDK automatically:
|
|
325
|
+
|
|
326
|
+
- Validates the API response structure
|
|
327
|
+
- Filters out invalid flags
|
|
328
|
+
- Checks the appropriate `enabled_*` field based on your configured environment
|
|
329
|
+
- Sends your API key in the `x-api-key` header
|
|
330
|
+
|
|
331
|
+
## License
|
|
332
|
+
|
|
333
|
+
MIT © Flaggy.io
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var LocalStorageAdapter = class {
|
|
5
|
+
get(key) {
|
|
6
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const data = window.localStorage.getItem(key);
|
|
11
|
+
return data ? JSON.parse(data) : null;
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error("Error reading from localStorage:", error);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
set(key, value) {
|
|
18
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error("Error writing to localStorage:", error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
clear(key) {
|
|
28
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
window.localStorage.removeItem(key);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("Error clearing localStorage:", error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var InMemoryStorageAdapter = class {
|
|
39
|
+
constructor() {
|
|
40
|
+
this.store = /* @__PURE__ */ new Map();
|
|
41
|
+
}
|
|
42
|
+
get(key) {
|
|
43
|
+
return this.store.get(key) || null;
|
|
44
|
+
}
|
|
45
|
+
set(key, value) {
|
|
46
|
+
this.store.set(key, value);
|
|
47
|
+
}
|
|
48
|
+
clear(key) {
|
|
49
|
+
this.store.delete(key);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var FeatureFlagClient = class {
|
|
53
|
+
constructor(config) {
|
|
54
|
+
this.flags = [];
|
|
55
|
+
this.lastFetch = 0;
|
|
56
|
+
this.isFetching = false;
|
|
57
|
+
this.fetchPromise = null;
|
|
58
|
+
this.failureCount = 0;
|
|
59
|
+
this.baseRefreshMs = 6e4;
|
|
60
|
+
this.maxRefreshMs = 15 * 60 * 1e3;
|
|
61
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
62
|
+
throw new Error("API key is required and must be a non-empty string");
|
|
63
|
+
}
|
|
64
|
+
if (config.apiKey.trim().length === 0) {
|
|
65
|
+
throw new Error("API key cannot be empty or whitespace");
|
|
66
|
+
}
|
|
67
|
+
this.config = config;
|
|
68
|
+
this.environment = config.environment === "production" || config.environment === "staging" || config.environment === "development" ? config.environment : "production";
|
|
69
|
+
this.storage = this.detectEnvironment() ? new LocalStorageAdapter() : new InMemoryStorageAdapter();
|
|
70
|
+
this.loadFromCache();
|
|
71
|
+
this.fetchFlags();
|
|
72
|
+
}
|
|
73
|
+
detectEnvironment() {
|
|
74
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
75
|
+
}
|
|
76
|
+
loadFromCache() {
|
|
77
|
+
const cached = this.storage.get("feature-flags");
|
|
78
|
+
if (cached && this.isValidCachedData(cached)) {
|
|
79
|
+
this.flags = cached.flags;
|
|
80
|
+
this.lastFetch = cached.timestamp || 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
isValidCachedData(data) {
|
|
84
|
+
return data && typeof data === "object" && Array.isArray(data.flags) && data.flags.every(this.isValidFeatureFlag) && typeof data.timestamp === "number";
|
|
85
|
+
}
|
|
86
|
+
isValidFeatureFlag(flag) {
|
|
87
|
+
return flag && typeof flag === "object" && typeof flag.key === "string" && typeof flag.enabled_production === "boolean" && typeof flag.enabled_staging === "boolean" && typeof flag.enabled_development === "boolean";
|
|
88
|
+
}
|
|
89
|
+
saveToCache() {
|
|
90
|
+
this.storage.set("feature-flags", {
|
|
91
|
+
flags: this.flags,
|
|
92
|
+
timestamp: this.lastFetch
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Check if valid flags exist in cache
|
|
96
|
+
hasCachedFlags() {
|
|
97
|
+
const cached = this.storage.get("feature-flags");
|
|
98
|
+
return cached ? this.isValidCachedData(cached) : false;
|
|
99
|
+
}
|
|
100
|
+
// Check if cache interval has elapsed and a fetch is needed
|
|
101
|
+
shouldFetch() {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
return now - this.lastFetch > this.getRefreshIntervalMs();
|
|
104
|
+
}
|
|
105
|
+
// Exponential backoff: base * 2^failureCount (capped)
|
|
106
|
+
getRefreshIntervalMs() {
|
|
107
|
+
const backoffMs = this.baseRefreshMs * Math.pow(2, this.failureCount);
|
|
108
|
+
return Math.min(backoffMs, this.maxRefreshMs);
|
|
109
|
+
}
|
|
110
|
+
async fetchFlags() {
|
|
111
|
+
if (!this.shouldFetch()) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (this.isFetching && this.fetchPromise) {
|
|
115
|
+
return this.fetchPromise;
|
|
116
|
+
}
|
|
117
|
+
this.isFetching = true;
|
|
118
|
+
this.fetchPromise = this.doFetch();
|
|
119
|
+
try {
|
|
120
|
+
await this.fetchPromise;
|
|
121
|
+
} finally {
|
|
122
|
+
this.isFetching = false;
|
|
123
|
+
this.fetchPromise = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async doFetch() {
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const timeoutId = setTimeout(() => controller.abort(), 2e3);
|
|
129
|
+
try {
|
|
130
|
+
const response = await fetch(
|
|
131
|
+
"https://api.flaggy.io/public/feature-flags",
|
|
132
|
+
{
|
|
133
|
+
method: "GET",
|
|
134
|
+
headers: {
|
|
135
|
+
"x-api-key": this.config.apiKey,
|
|
136
|
+
"Content-Type": "application/json"
|
|
137
|
+
},
|
|
138
|
+
signal: controller.signal
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Failed to fetch feature flags: ${response.status} ${response.statusText}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const json = await response.json();
|
|
147
|
+
if (!json || typeof json !== "object") {
|
|
148
|
+
throw new Error("Invalid API response: expected JSON object");
|
|
149
|
+
}
|
|
150
|
+
if (!json.data) {
|
|
151
|
+
throw new Error("Invalid API response: missing data property");
|
|
152
|
+
}
|
|
153
|
+
const data = json.data;
|
|
154
|
+
if (!Array.isArray(data)) {
|
|
155
|
+
throw new Error("Invalid API response: data must be an array");
|
|
156
|
+
}
|
|
157
|
+
this.flags = data.filter(this.isValidFeatureFlag);
|
|
158
|
+
if (data.length > 0 && this.flags.length === 0) {
|
|
159
|
+
console.warn(
|
|
160
|
+
"All feature flags in API response were invalid and filtered out"
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
this.lastFetch = Date.now();
|
|
164
|
+
this.failureCount = 0;
|
|
165
|
+
this.saveToCache();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.failureCount += 1;
|
|
168
|
+
const err = error;
|
|
169
|
+
if (this.config.onError) {
|
|
170
|
+
this.config.onError(err);
|
|
171
|
+
}
|
|
172
|
+
if (err.name === "AbortError") {
|
|
173
|
+
console.warn("Feature flag fetch timed out");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
console.error("Error fetching feature flags:", err);
|
|
177
|
+
} finally {
|
|
178
|
+
clearTimeout(timeoutId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
initialize() {
|
|
182
|
+
if (this.hasCachedFlags()) {
|
|
183
|
+
return Promise.resolve();
|
|
184
|
+
}
|
|
185
|
+
if (!this.shouldFetch()) {
|
|
186
|
+
return Promise.resolve();
|
|
187
|
+
}
|
|
188
|
+
const timeoutMs = 2e3;
|
|
189
|
+
return Promise.race([
|
|
190
|
+
this.fetchFlags(),
|
|
191
|
+
new Promise((resolve) => {
|
|
192
|
+
setTimeout(resolve, timeoutMs);
|
|
193
|
+
})
|
|
194
|
+
]);
|
|
195
|
+
}
|
|
196
|
+
isEnabled(flagName, defaultValue = false) {
|
|
197
|
+
if (this.shouldFetch() && !this.isFetching) {
|
|
198
|
+
this.fetchFlags();
|
|
199
|
+
}
|
|
200
|
+
const flag = this.flags.find((f) => f.key === flagName);
|
|
201
|
+
if (!flag) {
|
|
202
|
+
return defaultValue;
|
|
203
|
+
}
|
|
204
|
+
switch (this.environment) {
|
|
205
|
+
case "production":
|
|
206
|
+
return flag.enabled_production;
|
|
207
|
+
case "staging":
|
|
208
|
+
return flag.enabled_staging;
|
|
209
|
+
case "development":
|
|
210
|
+
return flag.enabled_development;
|
|
211
|
+
default:
|
|
212
|
+
return defaultValue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
getFlag(flagName) {
|
|
216
|
+
if (this.shouldFetch() && !this.isFetching) {
|
|
217
|
+
this.fetchFlags();
|
|
218
|
+
}
|
|
219
|
+
return this.flags.find((f) => f.key === flagName);
|
|
220
|
+
}
|
|
221
|
+
getAllFlags() {
|
|
222
|
+
return [...this.flags];
|
|
223
|
+
}
|
|
224
|
+
clearCache() {
|
|
225
|
+
this.storage.clear("feature-flags");
|
|
226
|
+
this.flags = [];
|
|
227
|
+
}
|
|
228
|
+
// Set custom storage adapter (useful for testing or custom implementations)
|
|
229
|
+
setStorageAdapter(adapter) {
|
|
230
|
+
this.storage = adapter;
|
|
231
|
+
this.loadFromCache();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
var globalClient = null;
|
|
235
|
+
function flaggy(config) {
|
|
236
|
+
if (globalClient) return globalClient;
|
|
237
|
+
globalClient = new FeatureFlagClient(config);
|
|
238
|
+
return globalClient;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
exports.flaggy = flaggy;
|
|
242
|
+
//# sourceMappingURL=index.cjs.map
|
|
243
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
interface FeatureFlag {
|
|
2
|
+
key: string;
|
|
3
|
+
enabled_production: boolean;
|
|
4
|
+
enabled_staging: boolean;
|
|
5
|
+
enabled_development: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface FeatureFlagConfig {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
environment?: "production" | "staging" | "development" | string;
|
|
10
|
+
onError?: (error: Error) => void;
|
|
11
|
+
}
|
|
12
|
+
interface StorageAdapter {
|
|
13
|
+
get(key: string): CachedData | null;
|
|
14
|
+
set(key: string, value: CachedData): void;
|
|
15
|
+
clear(key: string): void;
|
|
16
|
+
}
|
|
17
|
+
interface CachedData {
|
|
18
|
+
flags: FeatureFlag[];
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}
|
|
21
|
+
declare class FeatureFlagClient {
|
|
22
|
+
private config;
|
|
23
|
+
private storage;
|
|
24
|
+
private flags;
|
|
25
|
+
private lastFetch;
|
|
26
|
+
private isFetching;
|
|
27
|
+
private fetchPromise;
|
|
28
|
+
private environment;
|
|
29
|
+
private failureCount;
|
|
30
|
+
private readonly baseRefreshMs;
|
|
31
|
+
private readonly maxRefreshMs;
|
|
32
|
+
constructor(config: FeatureFlagConfig);
|
|
33
|
+
private detectEnvironment;
|
|
34
|
+
private loadFromCache;
|
|
35
|
+
private isValidCachedData;
|
|
36
|
+
private isValidFeatureFlag;
|
|
37
|
+
private saveToCache;
|
|
38
|
+
private hasCachedFlags;
|
|
39
|
+
private shouldFetch;
|
|
40
|
+
getRefreshIntervalMs(): number;
|
|
41
|
+
fetchFlags(): Promise<void>;
|
|
42
|
+
private doFetch;
|
|
43
|
+
initialize(): Promise<void>;
|
|
44
|
+
isEnabled(flagName: string, defaultValue?: boolean): boolean;
|
|
45
|
+
getFlag(flagName: string): FeatureFlag | undefined;
|
|
46
|
+
getAllFlags(): FeatureFlag[];
|
|
47
|
+
clearCache(): void;
|
|
48
|
+
setStorageAdapter(adapter: StorageAdapter): void;
|
|
49
|
+
}
|
|
50
|
+
declare function flaggy(config: FeatureFlagConfig): FeatureFlagClient;
|
|
51
|
+
|
|
52
|
+
export { type FeatureFlag, type FeatureFlagConfig, type StorageAdapter, flaggy };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
interface FeatureFlag {
|
|
2
|
+
key: string;
|
|
3
|
+
enabled_production: boolean;
|
|
4
|
+
enabled_staging: boolean;
|
|
5
|
+
enabled_development: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface FeatureFlagConfig {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
environment?: "production" | "staging" | "development" | string;
|
|
10
|
+
onError?: (error: Error) => void;
|
|
11
|
+
}
|
|
12
|
+
interface StorageAdapter {
|
|
13
|
+
get(key: string): CachedData | null;
|
|
14
|
+
set(key: string, value: CachedData): void;
|
|
15
|
+
clear(key: string): void;
|
|
16
|
+
}
|
|
17
|
+
interface CachedData {
|
|
18
|
+
flags: FeatureFlag[];
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}
|
|
21
|
+
declare class FeatureFlagClient {
|
|
22
|
+
private config;
|
|
23
|
+
private storage;
|
|
24
|
+
private flags;
|
|
25
|
+
private lastFetch;
|
|
26
|
+
private isFetching;
|
|
27
|
+
private fetchPromise;
|
|
28
|
+
private environment;
|
|
29
|
+
private failureCount;
|
|
30
|
+
private readonly baseRefreshMs;
|
|
31
|
+
private readonly maxRefreshMs;
|
|
32
|
+
constructor(config: FeatureFlagConfig);
|
|
33
|
+
private detectEnvironment;
|
|
34
|
+
private loadFromCache;
|
|
35
|
+
private isValidCachedData;
|
|
36
|
+
private isValidFeatureFlag;
|
|
37
|
+
private saveToCache;
|
|
38
|
+
private hasCachedFlags;
|
|
39
|
+
private shouldFetch;
|
|
40
|
+
getRefreshIntervalMs(): number;
|
|
41
|
+
fetchFlags(): Promise<void>;
|
|
42
|
+
private doFetch;
|
|
43
|
+
initialize(): Promise<void>;
|
|
44
|
+
isEnabled(flagName: string, defaultValue?: boolean): boolean;
|
|
45
|
+
getFlag(flagName: string): FeatureFlag | undefined;
|
|
46
|
+
getAllFlags(): FeatureFlag[];
|
|
47
|
+
clearCache(): void;
|
|
48
|
+
setStorageAdapter(adapter: StorageAdapter): void;
|
|
49
|
+
}
|
|
50
|
+
declare function flaggy(config: FeatureFlagConfig): FeatureFlagClient;
|
|
51
|
+
|
|
52
|
+
export { type FeatureFlag, type FeatureFlagConfig, type StorageAdapter, flaggy };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var LocalStorageAdapter = class {
|
|
3
|
+
get(key) {
|
|
4
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
const data = window.localStorage.getItem(key);
|
|
9
|
+
return data ? JSON.parse(data) : null;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error("Error reading from localStorage:", error);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
set(key, value) {
|
|
16
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error("Error writing to localStorage:", error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
clear(key) {
|
|
26
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
window.localStorage.removeItem(key);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error("Error clearing localStorage:", error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var InMemoryStorageAdapter = class {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.store = /* @__PURE__ */ new Map();
|
|
39
|
+
}
|
|
40
|
+
get(key) {
|
|
41
|
+
return this.store.get(key) || null;
|
|
42
|
+
}
|
|
43
|
+
set(key, value) {
|
|
44
|
+
this.store.set(key, value);
|
|
45
|
+
}
|
|
46
|
+
clear(key) {
|
|
47
|
+
this.store.delete(key);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var FeatureFlagClient = class {
|
|
51
|
+
constructor(config) {
|
|
52
|
+
this.flags = [];
|
|
53
|
+
this.lastFetch = 0;
|
|
54
|
+
this.isFetching = false;
|
|
55
|
+
this.fetchPromise = null;
|
|
56
|
+
this.failureCount = 0;
|
|
57
|
+
this.baseRefreshMs = 6e4;
|
|
58
|
+
this.maxRefreshMs = 15 * 60 * 1e3;
|
|
59
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
60
|
+
throw new Error("API key is required and must be a non-empty string");
|
|
61
|
+
}
|
|
62
|
+
if (config.apiKey.trim().length === 0) {
|
|
63
|
+
throw new Error("API key cannot be empty or whitespace");
|
|
64
|
+
}
|
|
65
|
+
this.config = config;
|
|
66
|
+
this.environment = config.environment === "production" || config.environment === "staging" || config.environment === "development" ? config.environment : "production";
|
|
67
|
+
this.storage = this.detectEnvironment() ? new LocalStorageAdapter() : new InMemoryStorageAdapter();
|
|
68
|
+
this.loadFromCache();
|
|
69
|
+
this.fetchFlags();
|
|
70
|
+
}
|
|
71
|
+
detectEnvironment() {
|
|
72
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
73
|
+
}
|
|
74
|
+
loadFromCache() {
|
|
75
|
+
const cached = this.storage.get("feature-flags");
|
|
76
|
+
if (cached && this.isValidCachedData(cached)) {
|
|
77
|
+
this.flags = cached.flags;
|
|
78
|
+
this.lastFetch = cached.timestamp || 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
isValidCachedData(data) {
|
|
82
|
+
return data && typeof data === "object" && Array.isArray(data.flags) && data.flags.every(this.isValidFeatureFlag) && typeof data.timestamp === "number";
|
|
83
|
+
}
|
|
84
|
+
isValidFeatureFlag(flag) {
|
|
85
|
+
return flag && typeof flag === "object" && typeof flag.key === "string" && typeof flag.enabled_production === "boolean" && typeof flag.enabled_staging === "boolean" && typeof flag.enabled_development === "boolean";
|
|
86
|
+
}
|
|
87
|
+
saveToCache() {
|
|
88
|
+
this.storage.set("feature-flags", {
|
|
89
|
+
flags: this.flags,
|
|
90
|
+
timestamp: this.lastFetch
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Check if valid flags exist in cache
|
|
94
|
+
hasCachedFlags() {
|
|
95
|
+
const cached = this.storage.get("feature-flags");
|
|
96
|
+
return cached ? this.isValidCachedData(cached) : false;
|
|
97
|
+
}
|
|
98
|
+
// Check if cache interval has elapsed and a fetch is needed
|
|
99
|
+
shouldFetch() {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
return now - this.lastFetch > this.getRefreshIntervalMs();
|
|
102
|
+
}
|
|
103
|
+
// Exponential backoff: base * 2^failureCount (capped)
|
|
104
|
+
getRefreshIntervalMs() {
|
|
105
|
+
const backoffMs = this.baseRefreshMs * Math.pow(2, this.failureCount);
|
|
106
|
+
return Math.min(backoffMs, this.maxRefreshMs);
|
|
107
|
+
}
|
|
108
|
+
async fetchFlags() {
|
|
109
|
+
if (!this.shouldFetch()) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (this.isFetching && this.fetchPromise) {
|
|
113
|
+
return this.fetchPromise;
|
|
114
|
+
}
|
|
115
|
+
this.isFetching = true;
|
|
116
|
+
this.fetchPromise = this.doFetch();
|
|
117
|
+
try {
|
|
118
|
+
await this.fetchPromise;
|
|
119
|
+
} finally {
|
|
120
|
+
this.isFetching = false;
|
|
121
|
+
this.fetchPromise = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async doFetch() {
|
|
125
|
+
const controller = new AbortController();
|
|
126
|
+
const timeoutId = setTimeout(() => controller.abort(), 2e3);
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(
|
|
129
|
+
"https://api.flaggy.io/public/feature-flags",
|
|
130
|
+
{
|
|
131
|
+
method: "GET",
|
|
132
|
+
headers: {
|
|
133
|
+
"x-api-key": this.config.apiKey,
|
|
134
|
+
"Content-Type": "application/json"
|
|
135
|
+
},
|
|
136
|
+
signal: controller.signal
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Failed to fetch feature flags: ${response.status} ${response.statusText}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const json = await response.json();
|
|
145
|
+
if (!json || typeof json !== "object") {
|
|
146
|
+
throw new Error("Invalid API response: expected JSON object");
|
|
147
|
+
}
|
|
148
|
+
if (!json.data) {
|
|
149
|
+
throw new Error("Invalid API response: missing data property");
|
|
150
|
+
}
|
|
151
|
+
const data = json.data;
|
|
152
|
+
if (!Array.isArray(data)) {
|
|
153
|
+
throw new Error("Invalid API response: data must be an array");
|
|
154
|
+
}
|
|
155
|
+
this.flags = data.filter(this.isValidFeatureFlag);
|
|
156
|
+
if (data.length > 0 && this.flags.length === 0) {
|
|
157
|
+
console.warn(
|
|
158
|
+
"All feature flags in API response were invalid and filtered out"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
this.lastFetch = Date.now();
|
|
162
|
+
this.failureCount = 0;
|
|
163
|
+
this.saveToCache();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
this.failureCount += 1;
|
|
166
|
+
const err = error;
|
|
167
|
+
if (this.config.onError) {
|
|
168
|
+
this.config.onError(err);
|
|
169
|
+
}
|
|
170
|
+
if (err.name === "AbortError") {
|
|
171
|
+
console.warn("Feature flag fetch timed out");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
console.error("Error fetching feature flags:", err);
|
|
175
|
+
} finally {
|
|
176
|
+
clearTimeout(timeoutId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
initialize() {
|
|
180
|
+
if (this.hasCachedFlags()) {
|
|
181
|
+
return Promise.resolve();
|
|
182
|
+
}
|
|
183
|
+
if (!this.shouldFetch()) {
|
|
184
|
+
return Promise.resolve();
|
|
185
|
+
}
|
|
186
|
+
const timeoutMs = 2e3;
|
|
187
|
+
return Promise.race([
|
|
188
|
+
this.fetchFlags(),
|
|
189
|
+
new Promise((resolve) => {
|
|
190
|
+
setTimeout(resolve, timeoutMs);
|
|
191
|
+
})
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
isEnabled(flagName, defaultValue = false) {
|
|
195
|
+
if (this.shouldFetch() && !this.isFetching) {
|
|
196
|
+
this.fetchFlags();
|
|
197
|
+
}
|
|
198
|
+
const flag = this.flags.find((f) => f.key === flagName);
|
|
199
|
+
if (!flag) {
|
|
200
|
+
return defaultValue;
|
|
201
|
+
}
|
|
202
|
+
switch (this.environment) {
|
|
203
|
+
case "production":
|
|
204
|
+
return flag.enabled_production;
|
|
205
|
+
case "staging":
|
|
206
|
+
return flag.enabled_staging;
|
|
207
|
+
case "development":
|
|
208
|
+
return flag.enabled_development;
|
|
209
|
+
default:
|
|
210
|
+
return defaultValue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
getFlag(flagName) {
|
|
214
|
+
if (this.shouldFetch() && !this.isFetching) {
|
|
215
|
+
this.fetchFlags();
|
|
216
|
+
}
|
|
217
|
+
return this.flags.find((f) => f.key === flagName);
|
|
218
|
+
}
|
|
219
|
+
getAllFlags() {
|
|
220
|
+
return [...this.flags];
|
|
221
|
+
}
|
|
222
|
+
clearCache() {
|
|
223
|
+
this.storage.clear("feature-flags");
|
|
224
|
+
this.flags = [];
|
|
225
|
+
}
|
|
226
|
+
// Set custom storage adapter (useful for testing or custom implementations)
|
|
227
|
+
setStorageAdapter(adapter) {
|
|
228
|
+
this.storage = adapter;
|
|
229
|
+
this.loadFromCache();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
var globalClient = null;
|
|
233
|
+
function flaggy(config) {
|
|
234
|
+
if (globalClient) return globalClient;
|
|
235
|
+
globalClient = new FeatureFlagClient(config);
|
|
236
|
+
return globalClient;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export { flaggy };
|
|
240
|
+
//# sourceMappingURL=index.js.map
|
|
241
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flaggy.io/sdk-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TypeScript SDK for Flaggy.io feature flag management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"feature-flags",
|
|
25
|
+
"feature-toggles",
|
|
26
|
+
"flaggy",
|
|
27
|
+
"typescript",
|
|
28
|
+
"sdk"
|
|
29
|
+
],
|
|
30
|
+
"author": "Flaggy.io",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/flaggy-io/sdk-js"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://flaggy.io",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/flaggy-io/sdk-js/issues"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"tsup": "^8.0.0",
|
|
47
|
+
"typescript": "^5.0.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|