featurefly 0.1.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 +687 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +22 -0
- package/dist/react/index.d.ts +54 -0
- package/dist/react/index.js +126 -0
- package/dist/shared/cache.d.ts +40 -0
- package/dist/shared/cache.js +80 -0
- package/dist/shared/circuit-breaker.d.ts +46 -0
- package/dist/shared/circuit-breaker.js +90 -0
- package/dist/shared/client.d.ts +153 -0
- package/dist/shared/client.js +560 -0
- package/dist/shared/edge-evaluator.d.ts +35 -0
- package/dist/shared/edge-evaluator.js +127 -0
- package/dist/shared/event-emitter.d.ts +29 -0
- package/dist/shared/event-emitter.js +68 -0
- package/dist/shared/experiment.d.ts +9 -0
- package/dist/shared/experiment.js +51 -0
- package/dist/shared/index.d.ts +7 -0
- package/dist/shared/index.js +7 -0
- package/dist/shared/logger.d.ts +14 -0
- package/dist/shared/logger.js +37 -0
- package/dist/shared/metrics.d.ts +79 -0
- package/dist/shared/metrics.js +147 -0
- package/dist/shared/retry.d.ts +10 -0
- package/dist/shared/retry.js +39 -0
- package/dist/shared/rollout.d.ts +14 -0
- package/dist/shared/rollout.js +77 -0
- package/dist/shared/streaming.d.ts +35 -0
- package/dist/shared/streaming.js +117 -0
- package/dist/shared/targeting.d.ts +10 -0
- package/dist/shared/targeting.js +133 -0
- package/dist/shared/types.d.ts +248 -0
- package/dist/shared/types.js +4 -0
- package/dist/vue/index.d.ts +60 -0
- package/dist/vue/index.js +136 -0
- package/package.json +97 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arrua Platform Team
|
|
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,687 @@
|
|
|
1
|
+
# FeatureFly π
|
|
2
|
+
|
|
3
|
+
**Lightweight, universal Feature Flags SDK for Node.js and the browser.**
|
|
4
|
+
One package. Backend and frontend. Zero config to start.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/featurefly)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](#-feature-comparison)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## π Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Installation](#-installation)
|
|
16
|
+
- [Quick Start](#-quick-start)
|
|
17
|
+
- [Usage by Environment](#-usage-by-environment)
|
|
18
|
+
- [Backend (Node.js / NestJS / Express)](#-backend-nodejs--nestjs--express)
|
|
19
|
+
- [Frontend (Vanilla JS / Any bundler)](#-frontend-vanilla-js--any-bundler)
|
|
20
|
+
- [React](#%EF%B8%8F-react)
|
|
21
|
+
- [Vue 3](#-vue-3)
|
|
22
|
+
- [Configuration](#-configuration)
|
|
23
|
+
- [API Reference](#-api-reference)
|
|
24
|
+
- [Flag Evaluation](#flag-evaluation)
|
|
25
|
+
- [Flag Management (CRUD)](#flag-management-crud)
|
|
26
|
+
- [Workspace Flags](#workspace-flags)
|
|
27
|
+
- [Real-time Streaming (SSE)](#real-time-streaming-sse)
|
|
28
|
+
- [Edge Evaluation (Offline)](#edge-evaluation-offline-mode)
|
|
29
|
+
- [Impact Metrics](#impact-metrics)
|
|
30
|
+
- [Event System](#event-system)
|
|
31
|
+
- [Local Overrides](#local-overrides)
|
|
32
|
+
- [Utilities](#utilities)
|
|
33
|
+
- [Considerations](#-considerations)
|
|
34
|
+
- [Evaluation Flow](#-evaluation-flow)
|
|
35
|
+
- [Resilience](#-resilience)
|
|
36
|
+
- [Feature Comparison](#-feature-comparison)
|
|
37
|
+
- [Roadmap](#-roadmap)
|
|
38
|
+
- [Contributing](#-contributing)
|
|
39
|
+
- [License](#-license)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## π¦ Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install featurefly
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> **Peer dependencies (optional):** If you plan to use the React hooks or Vue composables, install the corresponding framework alongside:
|
|
50
|
+
>
|
|
51
|
+
> ```bash
|
|
52
|
+
> # React projects
|
|
53
|
+
> npm install featurefly react
|
|
54
|
+
>
|
|
55
|
+
> # Vue 3 projects
|
|
56
|
+
> npm install featurefly vue
|
|
57
|
+
> ```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## π Quick Start
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { FeatureFlagsClient } from "featurefly";
|
|
65
|
+
|
|
66
|
+
const client = new FeatureFlagsClient({
|
|
67
|
+
baseUrl: "https://your-api.com",
|
|
68
|
+
apiKey: "your-api-key",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const isEnabled = await client.evaluateFlag("new-checkout-flow");
|
|
72
|
+
|
|
73
|
+
if (isEnabled) {
|
|
74
|
+
// New checkout
|
|
75
|
+
} else {
|
|
76
|
+
// Legacy checkout
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Always dispose when done (servers: on shutdown, SPAs: on unmount)
|
|
80
|
+
client.dispose();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
That's it. The same code works in a NestJS service, an Express middleware, a Vite frontend, or a Next.js API route.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## π Usage by Environment
|
|
88
|
+
|
|
89
|
+
FeatureFly is **universal** β the same npm package runs on the server and in the browser. The only difference is _how_ you integrate it.
|
|
90
|
+
|
|
91
|
+
### π₯οΈ Backend (Node.js / NestJS / Express)
|
|
92
|
+
|
|
93
|
+
Use the client directly to evaluate flags on the server side, typically in middleware, guards, or services.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// services/feature-flags.service.ts
|
|
97
|
+
import { FeatureFlagsClient } from "featurefly";
|
|
98
|
+
|
|
99
|
+
const client = new FeatureFlagsClient({
|
|
100
|
+
baseUrl: process.env.FEATURE_FLAGS_API_URL,
|
|
101
|
+
apiKey: process.env.FEATURE_FLAGS_API_KEY,
|
|
102
|
+
cacheTtlMs: 30_000, // Cache flags for 30s on the server
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export async function isFeatureEnabled(
|
|
106
|
+
slug: string,
|
|
107
|
+
userId?: string,
|
|
108
|
+
workspaceId?: string,
|
|
109
|
+
): Promise<boolean> {
|
|
110
|
+
return client.evaluateFlag(slug, { userId, workspaceId });
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Example: Express middleware
|
|
116
|
+
app.get("/dashboard", async (req, res) => {
|
|
117
|
+
const showNewDashboard = await isFeatureEnabled(
|
|
118
|
+
"new-dashboard",
|
|
119
|
+
req.user.id,
|
|
120
|
+
req.user.workspaceId,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (showNewDashboard) {
|
|
124
|
+
return res.render("dashboard-v2");
|
|
125
|
+
}
|
|
126
|
+
return res.render("dashboard");
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// Example: NestJS guard
|
|
132
|
+
@Injectable()
|
|
133
|
+
export class FeatureFlagGuard implements CanActivate {
|
|
134
|
+
constructor(private readonly flags: FeatureFlagsClient) {}
|
|
135
|
+
|
|
136
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
137
|
+
const request = context.switchToHttp().getRequest();
|
|
138
|
+
return this.flags.evaluateFlag("beta-api", {
|
|
139
|
+
userId: request.user.id,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> π‘ **Tip:** On the server, create a **single instance** of `FeatureFlagsClient` and reuse it across requests. Call `client.dispose()` during graceful shutdown.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### π Frontend (Vanilla JS / Any bundler)
|
|
150
|
+
|
|
151
|
+
Works with any bundler (Vite, Webpack, esbuild, Rollup) or even a plain `<script>` tag.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { FeatureFlagsClient } from "featurefly";
|
|
155
|
+
|
|
156
|
+
const client = new FeatureFlagsClient({
|
|
157
|
+
baseUrl: "https://your-api.com",
|
|
158
|
+
apiKey: "pk_live_xxx", // Use a public/client key
|
|
159
|
+
streaming: true, // Auto-receive flag updates via SSE
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Evaluate and render
|
|
163
|
+
const showBanner = await client.evaluateFlag("promo-banner", {
|
|
164
|
+
userId: currentUser.id,
|
|
165
|
+
attributes: { plan: currentUser.plan, country: "AR" },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (showBanner) {
|
|
169
|
+
document.getElementById("promo")!.style.display = "block";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// React to live flag changes
|
|
173
|
+
client.on("flagsUpdated", async () => {
|
|
174
|
+
const updated = await client.evaluateFlag("promo-banner");
|
|
175
|
+
document.getElementById("promo")!.style.display = updated ? "block" : "none";
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### βοΈ React
|
|
182
|
+
|
|
183
|
+
Import from `featurefly/react`. Hooks auto-re-evaluate when flags change via streaming.
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { FeatureFlagsClient } from "featurefly";
|
|
187
|
+
import {
|
|
188
|
+
FeatureFlyProvider,
|
|
189
|
+
useFeatureFlag,
|
|
190
|
+
useAllFlags,
|
|
191
|
+
} from "featurefly/react";
|
|
192
|
+
|
|
193
|
+
// Create your client (once, outside the component tree)
|
|
194
|
+
const client = new FeatureFlagsClient({
|
|
195
|
+
baseUrl: "https://your-api.com",
|
|
196
|
+
apiKey: "pk_live_xxx",
|
|
197
|
+
streaming: true,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// 1. Wrap your app
|
|
201
|
+
function App() {
|
|
202
|
+
return (
|
|
203
|
+
<FeatureFlyProvider client={client}>
|
|
204
|
+
<MyComponent />
|
|
205
|
+
</FeatureFlyProvider>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 2. Use hooks
|
|
210
|
+
function MyComponent() {
|
|
211
|
+
const { value: darkMode, loading } = useFeatureFlag("dark-mode", false);
|
|
212
|
+
const { flags } = useAllFlags({ workspaceId: "ws-123" });
|
|
213
|
+
|
|
214
|
+
if (loading) return <Spinner />;
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className={darkMode ? "dark" : "light"}>
|
|
218
|
+
{flags["new-feature"] && <NewFeature />}
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
| Hook | Returns | Description |
|
|
225
|
+
| ---------------------------------------------- | -------------------- | ----------------------------------------------- |
|
|
226
|
+
| `useFeatureFlag(slug, defaultValue, context?)` | `{ value, loading }` | Evaluates a single flag. Re-renders on changes. |
|
|
227
|
+
| `useAllFlags(context?)` | `{ flags, loading }` | Returns all flags as a key-value object. |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
### π Vue 3
|
|
232
|
+
|
|
233
|
+
Import from `featurefly/vue`. Composables return reactive `Ref` values that update automatically.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// main.ts
|
|
237
|
+
import { createApp } from "vue";
|
|
238
|
+
import { FeatureFlagsClient } from "featurefly";
|
|
239
|
+
import { FeatureFlyPlugin } from "featurefly/vue";
|
|
240
|
+
|
|
241
|
+
const client = new FeatureFlagsClient({
|
|
242
|
+
baseUrl: "https://your-api.com",
|
|
243
|
+
apiKey: "pk_live_xxx",
|
|
244
|
+
streaming: true,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const app = createApp(App);
|
|
248
|
+
app.use(FeatureFlyPlugin, { client });
|
|
249
|
+
app.mount("#app");
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```vue
|
|
253
|
+
<!-- MyComponent.vue -->
|
|
254
|
+
<script setup>
|
|
255
|
+
import { useFeatureFlag, useAllFlags } from "featurefly/vue";
|
|
256
|
+
|
|
257
|
+
const darkMode = useFeatureFlag("dark-mode", false);
|
|
258
|
+
const flags = useAllFlags({ workspaceId: "ws-123" });
|
|
259
|
+
</script>
|
|
260
|
+
|
|
261
|
+
<template>
|
|
262
|
+
<div :class="{ dark: darkMode }">
|
|
263
|
+
<NewFeature v-if="flags['new-feature']" />
|
|
264
|
+
</div>
|
|
265
|
+
</template>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
| Composable | Returns | Description |
|
|
269
|
+
| ---------------------------------------------- | -------------------------------- | ------------------------------------------ |
|
|
270
|
+
| `useFeatureFlag(slug, defaultValue, context?)` | `Ref<T>` | Reactive ref that updates on flag changes. |
|
|
271
|
+
| `useAllFlags(context?)` | `Ref<Record<string, FlagValue>>` | Reactive ref with all flags. |
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## π§ Configuration
|
|
276
|
+
|
|
277
|
+
All options are optional except `baseUrl`.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
const client = new FeatureFlagsClient({
|
|
281
|
+
// Required β your feature flags API endpoint
|
|
282
|
+
baseUrl: "https://your-api.com",
|
|
283
|
+
|
|
284
|
+
// Authentication
|
|
285
|
+
apiKey: "your-api-key",
|
|
286
|
+
|
|
287
|
+
// HTTP timeout (default: 10000ms)
|
|
288
|
+
timeout: 10_000,
|
|
289
|
+
|
|
290
|
+
// Cache (default: enabled, 60s TTL)
|
|
291
|
+
cacheEnabled: true,
|
|
292
|
+
cacheTtlMs: 60_000,
|
|
293
|
+
|
|
294
|
+
// Retry (default: 3 attempts, 1s base delay)
|
|
295
|
+
retry: {
|
|
296
|
+
maxAttempts: 3,
|
|
297
|
+
baseDelayMs: 1_000,
|
|
298
|
+
maxDelayMs: 10_000,
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
// Circuit Breaker (default: 5 failures, 30s reset)
|
|
302
|
+
circuitBreaker: {
|
|
303
|
+
failureThreshold: 5,
|
|
304
|
+
resetTimeoutMs: 30_000,
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
// Logging β 'debug' | 'info' | 'warn' | 'error' | 'silent' (default: 'warn')
|
|
308
|
+
logLevel: "warn",
|
|
309
|
+
|
|
310
|
+
// Or provide your own logger (pino, winston, etc.)
|
|
311
|
+
logger: myLogger,
|
|
312
|
+
|
|
313
|
+
// Local overrides β flags evaluated instantly without HTTP
|
|
314
|
+
localOverrides: {
|
|
315
|
+
"feature-x": true,
|
|
316
|
+
"variant-test": "blue",
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
// Fallback defaults when the API is unreachable
|
|
320
|
+
fallbackDefaults: {
|
|
321
|
+
"critical-feature": false,
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
// Real-time updates via SSE (default: false)
|
|
325
|
+
streaming: true, // or { reconnectDelayMs: 2000 }
|
|
326
|
+
|
|
327
|
+
// A/B testing callback
|
|
328
|
+
trackingCallback: (assignment) => {
|
|
329
|
+
analytics.track("Experiment Viewed", assignment);
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
// Edge evaluation β pass a pre-fetched document for fully offline mode
|
|
333
|
+
// edgeDocument: myPreFetchedDoc,
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## π API Reference
|
|
340
|
+
|
|
341
|
+
### Flag Evaluation
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// Boolean flag
|
|
345
|
+
const isEnabled = await client.evaluateFlag("my-flag");
|
|
346
|
+
|
|
347
|
+
// With user context (targeting, rollout, experiments)
|
|
348
|
+
const isEnabled = await client.evaluateFlag("my-flag", {
|
|
349
|
+
workspaceId: "ws-123",
|
|
350
|
+
userId: "user-456",
|
|
351
|
+
attributes: { plan: "pro", country: "AR" },
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Typed flag values
|
|
355
|
+
const variant = await client.evaluateFlag<string>("ab-test");
|
|
356
|
+
const limit = await client.evaluateFlag<number>("rate-limit");
|
|
357
|
+
const config = await client.evaluateFlag<Record<string, unknown>>("ui-config");
|
|
358
|
+
|
|
359
|
+
// All flags at once (single HTTP request)
|
|
360
|
+
const allFlags = await client.evaluateAllFlags({ workspaceId: "ws-123" });
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Flag Management (CRUD)
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
// Create
|
|
367
|
+
const flag = await client.createFlag({
|
|
368
|
+
slug: "new-feature",
|
|
369
|
+
name: "New Feature",
|
|
370
|
+
category: "both",
|
|
371
|
+
valueType: "boolean",
|
|
372
|
+
defaultValue: false,
|
|
373
|
+
tags: ["v2", "beta"],
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Read
|
|
377
|
+
const allFlags = await client.getAllFlags();
|
|
378
|
+
const byId = await client.getFlagById("flag-id");
|
|
379
|
+
const bySlug = await client.getFlagBySlug("my-flag");
|
|
380
|
+
|
|
381
|
+
// Update
|
|
382
|
+
await client.updateFlag("flag-id", { name: "Updated Name" });
|
|
383
|
+
|
|
384
|
+
// Delete
|
|
385
|
+
await client.deleteFlag("flag-id");
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Workspace Flags
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// Set a workspace-level override
|
|
392
|
+
await client.setWorkspaceFlag("feature-x", "workspace-123", true);
|
|
393
|
+
|
|
394
|
+
// Get all flags for a workspace
|
|
395
|
+
const flags = await client.getWorkspaceFlags("workspace-123");
|
|
396
|
+
|
|
397
|
+
// Remove an override
|
|
398
|
+
await client.removeWorkspaceFlag("feature-x", "workspace-123");
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Real-time Streaming (SSE)
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// Option A: Auto-start via config
|
|
405
|
+
const client = new FeatureFlagsClient({
|
|
406
|
+
baseUrl: "https://your-api.com",
|
|
407
|
+
streaming: true,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Option B: Manual control
|
|
411
|
+
client.startStreaming();
|
|
412
|
+
client.stopStreaming();
|
|
413
|
+
|
|
414
|
+
// React to flag updates (cache is auto-invalidated)
|
|
415
|
+
client.on("flagsUpdated", () => {
|
|
416
|
+
console.log("Flags refreshed from server!");
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Edge Evaluation (Offline Mode)
|
|
421
|
+
|
|
422
|
+
Evaluate flags with 0ms latency by pre-fetching the entire flag document.
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
// 1. Fetch the document (e.g., at server startup or on app boot)
|
|
426
|
+
const doc = await fetch("https://your-api.com/feature-flags/document").then(
|
|
427
|
+
(r) => r.json(),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// 2. Create a fully offline client
|
|
431
|
+
const edgeClient = new FeatureFlagsClient({
|
|
432
|
+
baseUrl: "https://your-api.com",
|
|
433
|
+
edgeDocument: doc,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// 3. Evaluate β zero network calls, zero latency
|
|
437
|
+
const value = await edgeClient.evaluateFlag("my-flag", {
|
|
438
|
+
userId: "user-123",
|
|
439
|
+
});
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Impact Metrics
|
|
443
|
+
|
|
444
|
+
Client-side telemetry collected passively, no external calls.
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
const metrics = client.getImpactMetrics();
|
|
448
|
+
|
|
449
|
+
console.log(metrics.totalEvaluations); // 1523
|
|
450
|
+
console.log(metrics.cacheHitRate); // 0.87
|
|
451
|
+
console.log(metrics.latency.p50); // 2ms
|
|
452
|
+
console.log(metrics.latency.p95); // 12ms
|
|
453
|
+
console.log(metrics.latency.p99); // 45ms
|
|
454
|
+
|
|
455
|
+
// Per-flag detail
|
|
456
|
+
console.log(metrics.flags["my-flag"].evaluations); // 42
|
|
457
|
+
|
|
458
|
+
// Experiment exposures
|
|
459
|
+
console.log(metrics.experiments["checkout-exp"].exposures); // 300
|
|
460
|
+
|
|
461
|
+
// Reset all counters
|
|
462
|
+
client.resetMetrics();
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Event System
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
// Flag evaluated (with timing)
|
|
469
|
+
client.on("flagEvaluated", ({ slug, value, reason, durationMs }) => {
|
|
470
|
+
analytics.track("flag_check", { slug, value, reason, durationMs });
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Flag value changed (detect drift)
|
|
474
|
+
client.on("flagChanged", ({ slug, previousValue, newValue }) => {
|
|
475
|
+
console.log(`${slug}: ${previousValue} β ${newValue}`);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Circuit breaker
|
|
479
|
+
client.on("circuitOpen", ({ state, failures }) => {
|
|
480
|
+
alerting.send(`Circuit opened after ${failures} failures`);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Cache
|
|
484
|
+
client.on("cacheHit", ({ key }) => monitor.increment("cache.hit"));
|
|
485
|
+
client.on("cacheMiss", ({ key }) => monitor.increment("cache.miss"));
|
|
486
|
+
|
|
487
|
+
// Streaming
|
|
488
|
+
client.on("streamConnected", () => console.log("SSE connected"));
|
|
489
|
+
client.on("streamDisconnected", () => console.log("SSE lost"));
|
|
490
|
+
|
|
491
|
+
// Unsubscribe
|
|
492
|
+
const unsubscribe = client.on("flagEvaluated", handler);
|
|
493
|
+
unsubscribe();
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Local Overrides
|
|
497
|
+
|
|
498
|
+
Useful for development and testing β evaluated instantly, no HTTP.
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
client.setLocalOverride("experimental-ui", true);
|
|
502
|
+
client.setLocalOverride("theme", "dark");
|
|
503
|
+
|
|
504
|
+
const overrides = client.getLocalOverrides();
|
|
505
|
+
|
|
506
|
+
client.removeLocalOverride("experimental-ui");
|
|
507
|
+
client.clearLocalOverrides();
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Utilities
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
// Cache
|
|
514
|
+
client.clearCache();
|
|
515
|
+
client.getCacheStats(); // { size, keys, enabled }
|
|
516
|
+
|
|
517
|
+
// Circuit breaker
|
|
518
|
+
client.getCircuitBreakerState(); // { state, failures }
|
|
519
|
+
client.resetCircuitBreaker();
|
|
520
|
+
|
|
521
|
+
// Lifecycle
|
|
522
|
+
client.dispose(); // Release timers, listeners, metrics, SSE
|
|
523
|
+
client.isDisposed(); // true
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## β οΈ Considerations
|
|
529
|
+
|
|
530
|
+
### Universal Package (Backend + Frontend)
|
|
531
|
+
|
|
532
|
+
FeatureFly ships as a **single universal package** that works on both the server and the browser:
|
|
533
|
+
|
|
534
|
+
| Concern | How it's handled |
|
|
535
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
536
|
+
| **HTTP client** | Uses `axios`, which auto-detects the environment (Node.js `http` module vs browser `XMLHttpRequest`). |
|
|
537
|
+
| **Hashing (MurmurHash3)** | Implemented in pure TypeScript β no dependency on Node.js `crypto`. |
|
|
538
|
+
| **SSE Streaming** | Uses the native browser `EventSource` API. On Node.js < 22, streaming is disabled unless you provide a polyfill. Node.js 22+ includes native `EventSource`. |
|
|
539
|
+
| **Build format** | Ships both CJS (`require()`) and ESM (`import`). Your bundler picks the right one. |
|
|
540
|
+
|
|
541
|
+
### API Key Security
|
|
542
|
+
|
|
543
|
+
- On the **backend**, use a secret API key stored in environment variables.
|
|
544
|
+
- On the **frontend**, use a **public/read-only** key that only has permission to evaluate flags, not manage them. Never expose your secret key in client-side code.
|
|
545
|
+
|
|
546
|
+
### Single Instance Pattern
|
|
547
|
+
|
|
548
|
+
Create **one** `FeatureFlagsClient` instance and share it:
|
|
549
|
+
|
|
550
|
+
- **Backend:** Create at app startup, dispose on `SIGTERM` / `SIGINT`.
|
|
551
|
+
- **React:** Create outside the component tree, pass via `<FeatureFlyProvider>`.
|
|
552
|
+
- **Vue:** Create before `app.mount()`, install via `app.use(FeatureFlyPlugin, { client })`.
|
|
553
|
+
|
|
554
|
+
### Cache Strategy
|
|
555
|
+
|
|
556
|
+
| Environment | Recommended `cacheTtlMs` | Why |
|
|
557
|
+
| ----------------- | ------------------------ | -------------------------------------------------------------------------- |
|
|
558
|
+
| Backend (API) | `30_000` β `60_000` | Flags change infrequently; caching reduces load on the flags API. |
|
|
559
|
+
| Frontend (SPA) | `60_000` β `120_000` | Combined with `streaming: true`, the cache is auto-invalidated on changes. |
|
|
560
|
+
| Edge / Serverless | `0` (disabled) | Use `edgeDocument` for fully offline evaluation instead. |
|
|
561
|
+
|
|
562
|
+
### Disposing Resources
|
|
563
|
+
|
|
564
|
+
Always call `client.dispose()` when you're done. This cleans up:
|
|
565
|
+
|
|
566
|
+
- SSE connections
|
|
567
|
+
- Cache timers
|
|
568
|
+
- Metrics collectors
|
|
569
|
+
- Event listeners
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
// Express / NestJS
|
|
573
|
+
process.on("SIGTERM", () => client.dispose());
|
|
574
|
+
|
|
575
|
+
// React
|
|
576
|
+
useEffect(() => () => client.dispose(), []);
|
|
577
|
+
|
|
578
|
+
// Vue
|
|
579
|
+
onUnmounted(() => client.dispose());
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Node.js Version
|
|
583
|
+
|
|
584
|
+
Requires **Node.js >= 18**. For SSE streaming on the server, Node.js 22+ is recommended (native `EventSource`), or install a polyfill like `eventsource` for older versions.
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## βοΈ Evaluation Flow
|
|
589
|
+
|
|
590
|
+
When you call `evaluateFlag()`, the SDK follows this priority chain:
|
|
591
|
+
|
|
592
|
+
```
|
|
593
|
+
evaluateFlag('slug', context)
|
|
594
|
+
β
|
|
595
|
+
ββ 1. Local Overrides β instant return (no HTTP)
|
|
596
|
+
ββ 2. Edge Evaluator β offline return if document loaded
|
|
597
|
+
ββ 3. Cache hit β instant return (no HTTP)
|
|
598
|
+
ββ 4. Circuit Breaker β reject if circuit is open
|
|
599
|
+
ββ 5. Retry w/ Backoff β exponential backoff + jitter
|
|
600
|
+
ββ 6. HTTP Request β GET /feature-flags/:slug/evaluate
|
|
601
|
+
ββ 7. Cache store β persist result with TTL
|
|
602
|
+
ββ 8. Fallback β predefined defaults if everything fails
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## π Resilience
|
|
608
|
+
|
|
609
|
+
Built-in, zero-config resilience. No plugins needed.
|
|
610
|
+
|
|
611
|
+
| Layer | What it does |
|
|
612
|
+
| --------------------- | ----------------------------------------------------------------------- |
|
|
613
|
+
| **Retry** | Retries failed requests with exponential backoff + jitter |
|
|
614
|
+
| **Circuit Breaker** | Stops calling a failing API after N consecutive failures, auto-recovers |
|
|
615
|
+
| **Cache** | Serves stale data while the API is down |
|
|
616
|
+
| **Fallback Defaults** | Returns predefined safe values when nothing else works |
|
|
617
|
+
| **Local Overrides** | Flags work completely offline |
|
|
618
|
+
| **Edge Evaluator** | Full offline evaluation using a pre-fetched document |
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## π Feature Comparison
|
|
623
|
+
|
|
624
|
+
| Capability | FeatureFly | LaunchDarkly | Unleash | GrowthBook | Flagsmith |
|
|
625
|
+
| ---------------------------------------- | :--------: | :-----------: | :-----------: | :--------: | :-------: |
|
|
626
|
+
| Boolean flags | β
| β
| β
| β
| β
|
|
|
627
|
+
| Multi-type values (string, number, JSON) | β
| β
| β
| β
| β
|
|
|
628
|
+
| In-memory cache | β
| β
| β
| β
| β
|
|
|
629
|
+
| Retry with backoff | β
| β
| β
| β
| β
|
|
|
630
|
+
| Circuit breaker | β
| β
| β | β | β |
|
|
631
|
+
| Typed event system | β
| β οΈ | β | β | β |
|
|
632
|
+
| Local overrides | β
| β οΈ | β
| β
| β |
|
|
633
|
+
| Fallback defaults | β
| β
| β
| β
| β
|
|
|
634
|
+
| Injectable logger | β
| β
| β
| β | β |
|
|
635
|
+
| Dispose / cleanup | β
| β
| β
| β | β |
|
|
636
|
+
| Workspace-level overrides | β
| β οΈ | β | β | β
|
|
|
637
|
+
| Streaming (SSE) | β
| β
| β
| β
| β
|
|
|
638
|
+
| Targeting / segmentation | β
| β
| β
| β
| β
|
|
|
639
|
+
| Percentage rollout | β
| β
| β
| β
| β
|
|
|
640
|
+
| A/B testing | β
| β
| β | β
| β
|
|
|
641
|
+
| Edge evaluation (offline) | β
| β
| β
| β
| β |
|
|
642
|
+
| Impact metrics | β
| β
| β
| β
| β |
|
|
643
|
+
| React hooks | β
| β
| β
| β
| β
|
|
|
644
|
+
| Vue composables | β
| β | β | β | β |
|
|
645
|
+
| Self-hosted | β
| β | β
| β
| β
|
|
|
646
|
+
| TypeScript first | β
| β
| β οΈ | β
| β οΈ |
|
|
647
|
+
| Open source | β
MIT | β
Apache 2.0 | β
Apache 2.0 | β
MIT | β
BSD-3 |
|
|
648
|
+
| | | | | | |
|
|
649
|
+
| **Bundle size (gzipped)** | **~21 KB** | ~100 KB+ | ~336 KB | ~9 KB | ~50 KB+ |
|
|
650
|
+
| **Runtime dependencies** | **1** | 3+ | 5+ | **0** | 2+ |
|
|
651
|
+
| **Pricing** | **Free** | Paid | Free/Paid | **Free** | Free/Paid |
|
|
652
|
+
|
|
653
|
+
> β
Supported Β· β οΈ Partial Β· β Not available
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## πΊοΈ Roadmap
|
|
658
|
+
|
|
659
|
+
| Feature | Status | Description |
|
|
660
|
+
| ------------------- | ------------ | -------------------------------------------- |
|
|
661
|
+
| Multi-language SDKs | π Planned | Go, Python, Ruby, PHP server-side SDKs |
|
|
662
|
+
| Encrypted payloads | π‘ Exploring | End-to-end encryption of flag configurations |
|
|
663
|
+
| Audit log | π‘ Exploring | Track who changed what and when |
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## π§ͺ Testing
|
|
668
|
+
|
|
669
|
+
```bash
|
|
670
|
+
npm test # Run tests with coverage
|
|
671
|
+
npm run test:watch # Watch mode
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
## π€ Contributing
|
|
677
|
+
|
|
678
|
+
1. Clone the repo
|
|
679
|
+
2. `npm install`
|
|
680
|
+
3. `npm test`
|
|
681
|
+
4. `npm run build`
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
## π License
|
|
686
|
+
|
|
687
|
+
MIT Β© Arrua Platform Team
|