@reflag/node-sdk 1.2.0 → 1.4.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 +264 -40
- package/dist/package.json +4 -1
- package/dist/src/client.js +170 -37
- package/dist/src/client.js.map +1 -1
- package/dist/src/flagsFallbackProvider.js +307 -0
- package/dist/src/flagsFallbackProvider.js.map +1 -0
- package/dist/src/index.js +9 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/periodicallyUpdatingCache.js +1 -11
- package/dist/src/periodicallyUpdatingCache.js.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils.js +11 -0
- package/dist/src/utils.js.map +1 -1
- package/dist/types/src/client.d.ts +45 -9
- package/dist/types/src/client.d.ts.map +1 -1
- package/dist/types/src/flagsFallbackProvider.d.ts +81 -0
- package/dist/types/src/flagsFallbackProvider.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +10 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/periodicallyUpdatingCache.d.ts +1 -3
- package/dist/types/src/periodicallyUpdatingCache.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +51 -7
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/utils.d.ts +7 -0
- package/dist/types/src/utils.d.ts.map +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -175,15 +175,8 @@ await client.flush();
|
|
|
175
175
|
### Rate Limiting
|
|
176
176
|
|
|
177
177
|
The SDK includes automatic rate limiting for flag events to prevent overwhelming the API.
|
|
178
|
-
Rate limiting is applied per unique combination of flag key and context.
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
const client = new ReflagClient({
|
|
182
|
-
rateLimiterOptions: {
|
|
183
|
-
windowSizeMs: 60000, // Rate limiting window size in milliseconds
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
```
|
|
178
|
+
Rate limiting is applied per unique combination of flag key and evaluation context.
|
|
179
|
+
This behavior is built in and does not currently require configuration.
|
|
187
180
|
|
|
188
181
|
### Flag definitions
|
|
189
182
|
|
|
@@ -206,9 +199,182 @@ const flagDefs = await client.getFlagDefinitions();
|
|
|
206
199
|
// }]
|
|
207
200
|
```
|
|
208
201
|
|
|
202
|
+
### Fallback provider
|
|
203
|
+
|
|
204
|
+
`flagsFallbackProvider` is a reliability feature that lets the SDK persist the latest successfully fetched raw flag definitions to fallback storage such as a local file, Redis, S3, GCS, or a custom backend.
|
|
205
|
+
|
|
206
|
+
#### How it works
|
|
207
|
+
|
|
208
|
+
Reflag servers remain the primary source of truth. On `initialize()`, the SDK always tries to fetch a live copy of the flag definitions first, and it continues refreshing those definitions from the Reflag servers over time.
|
|
209
|
+
|
|
210
|
+
If that initial live fetch fails, the SDK can call `flagsFallbackProvider.load()` and start with the last saved snapshot instead. This is mainly useful for cold starts in the exceedingly rare case that Reflag has an outage.
|
|
211
|
+
|
|
212
|
+
If Reflag becomes unavailable after the SDK has already initialized successfully, the SDK keeps using the last successfully fetched definitions it already has in memory. In other words, the fallback provider is mainly what helps future processes start, not what keeps an already running process alive.
|
|
213
|
+
|
|
214
|
+
After successfully fetching updated flag definitions, the SDK calls `flagsFallbackProvider.save()` to keep the stored snapshot up to date.
|
|
215
|
+
|
|
216
|
+
Typical reliability flow:
|
|
217
|
+
|
|
218
|
+
1. The SDK starts and tries to fetch live flag definitions from Reflag.
|
|
219
|
+
2. If that succeeds, those definitions are used immediately and the SDK continues operating normally.
|
|
220
|
+
3. After successfully fetching updated flag definitions, the SDK saves the latest snapshot through the fallback provider so a recent copy is available if needed later.
|
|
221
|
+
4. If a future process starts while Reflag is unavailable, it can load the last saved snapshot from the fallback provider and still initialize.
|
|
222
|
+
5. Once Reflag becomes available again, the SDK resumes using live data and refreshes the fallback snapshot.
|
|
223
|
+
|
|
224
|
+
Most deployments run multiple SDK processes, so more than one process may save identical flag definitions to the fallback storage at roughly the same time. This is expected and generally harmless for backends like a local file, Redis, S3, or GCS because the operation is cheap. In practice, this only becomes worth thinking about once you have many thousands of SDK processes writing to the same fallback storage.
|
|
225
|
+
|
|
226
|
+
> [!TIP]
|
|
227
|
+
> If you are building a web or client-side application and want the most resilient setup, combine `flagsFallbackProvider` on the server with bootstrapped flags on the client.
|
|
228
|
+
>
|
|
229
|
+
> `flagsFallbackProvider` helps new server processes start if they cannot reach Reflag during initialization. Bootstrapping helps clients render from server-provided flags instead of depending on an initial client-side fetch from the Reflag servers.
|
|
230
|
+
>
|
|
231
|
+
> This applies to React (`getFlagsForBootstrap()` + `ReflagBootstrappedProvider`), the Browser SDK (`bootstrappedFlags`), and the Vue SDK (bootstrapped flags via the provider).
|
|
232
|
+
|
|
233
|
+
#### Built-in providers
|
|
234
|
+
|
|
235
|
+
You can access the built-in providers through the `fallbackProviders` namespace:
|
|
236
|
+
|
|
237
|
+
- `fallbackProviders.static(...)`
|
|
238
|
+
- `fallbackProviders.file(...)`
|
|
239
|
+
- `fallbackProviders.redis(...)`
|
|
240
|
+
- `fallbackProviders.s3(...)`
|
|
241
|
+
- `fallbackProviders.gcs(...)`
|
|
242
|
+
|
|
243
|
+
##### File provider
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
|
|
247
|
+
|
|
248
|
+
const client = new ReflagClient({
|
|
249
|
+
secretKey: process.env.REFLAG_SECRET_KEY,
|
|
250
|
+
flagsFallbackProvider: fallbackProviders.file({
|
|
251
|
+
directory: ".reflag",
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await client.initialize();
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The file provider stores one snapshot file per environment in the configured
|
|
259
|
+
`directory`.
|
|
260
|
+
|
|
261
|
+
##### Static provider
|
|
262
|
+
|
|
263
|
+
If you just want a fixed fallback copy of simple enabled/disabled flags, you can provide a static map:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
|
|
267
|
+
|
|
268
|
+
const client = new ReflagClient({
|
|
269
|
+
secretKey: process.env.REFLAG_SECRET_KEY,
|
|
270
|
+
flagsFallbackProvider: fallbackProviders.static({
|
|
271
|
+
flags: {
|
|
272
|
+
huddle: true,
|
|
273
|
+
"smart-summaries": false,
|
|
274
|
+
},
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await client.initialize();
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
##### Redis provider
|
|
282
|
+
|
|
283
|
+
The built-in Redis provider creates a Redis client automatically when omitted and uses `REDIS_URL` from the environment. It stores snapshots under the configured `keyPrefix` and uses the first 16 characters of the secret key hash in the Redis key.
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
|
|
287
|
+
|
|
288
|
+
const client = new ReflagClient({
|
|
289
|
+
secretKey: process.env.REFLAG_SECRET_KEY,
|
|
290
|
+
flagsFallbackProvider: fallbackProviders.redis(),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await client.initialize();
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
##### S3 provider
|
|
297
|
+
|
|
298
|
+
The built-in S3 provider works out of the box using the AWS SDK's default credential chain and region resolution. It stores the snapshot object under the configured `keyPrefix` and uses a hash of the secret key in the object name.
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
|
|
302
|
+
|
|
303
|
+
const client = new ReflagClient({
|
|
304
|
+
secretKey: process.env.REFLAG_SECRET_KEY,
|
|
305
|
+
flagsFallbackProvider: fallbackProviders.s3({
|
|
306
|
+
bucket: process.env.REFLAG_SNAPSHOT_BUCKET!,
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await client.initialize();
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
##### GCS provider
|
|
314
|
+
|
|
315
|
+
The built-in GCS provider works out of the box using Google Cloud's default application credentials. It stores the snapshot object under the configured `keyPrefix` and uses a hash of the secret key in the object name.
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
|
|
319
|
+
|
|
320
|
+
const client = new ReflagClient({
|
|
321
|
+
secretKey: process.env.REFLAG_SECRET_KEY,
|
|
322
|
+
flagsFallbackProvider: fallbackProviders.gcs({
|
|
323
|
+
bucket: process.env.REFLAG_SNAPSHOT_BUCKET!,
|
|
324
|
+
}),
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await client.initialize();
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### Testing fallback startup locally
|
|
331
|
+
|
|
332
|
+
To test fallback startup in your own app, first run it once with a working Reflag connection so a snapshot is saved. Then restart it with the same secret key and fallback provider configuration, but set `apiBaseUrl` to `http://127.0.0.1:65535`. That forces the live fetch to fail and lets you verify that the SDK initializes from the saved snapshot instead.
|
|
333
|
+
|
|
334
|
+
#### Writing a custom provider
|
|
335
|
+
|
|
336
|
+
If you just want a fixed fallback copy of the flag definitions, a custom provider can be very small:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import type {
|
|
340
|
+
FlagsFallbackProvider,
|
|
341
|
+
FlagsFallbackSnapshot,
|
|
342
|
+
} from "@reflag/node-sdk";
|
|
343
|
+
|
|
344
|
+
const fallbackSnapshot: FlagsFallbackSnapshot = {
|
|
345
|
+
version: 1,
|
|
346
|
+
savedAt: "2026-03-10T00:00:00.000Z",
|
|
347
|
+
flags: [
|
|
348
|
+
{
|
|
349
|
+
key: "huddle",
|
|
350
|
+
description: "Fallback example",
|
|
351
|
+
targeting: {
|
|
352
|
+
version: 1,
|
|
353
|
+
rules: [],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
export const staticFallbackProvider: FlagsFallbackProvider = {
|
|
360
|
+
async load() {
|
|
361
|
+
return fallbackSnapshot;
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
async save() {
|
|
365
|
+
// no-op
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
> [!NOTE]
|
|
371
|
+
>
|
|
372
|
+
> `fallbackFlags` is deprecated. Prefer `flagsFallbackProvider` for startup fallback and outage recovery.
|
|
373
|
+
> `flagsFallbackProvider` is not used in offline mode.
|
|
374
|
+
|
|
209
375
|
## Bootstrapping client-side applications
|
|
210
376
|
|
|
211
|
-
The `getFlagsForBootstrap()` method is
|
|
377
|
+
The `getFlagsForBootstrap()` method is useful whenever you need to pass flag data to another runtime or serialize it without wrapper functions. Server-side rendering (SSR) is a common example, but it is also useful for other bootstrapping and hydration flows.
|
|
212
378
|
|
|
213
379
|
```typescript
|
|
214
380
|
const client = new ReflagClient();
|
|
@@ -340,7 +506,8 @@ fallback behavior:
|
|
|
340
506
|
4. **Offline Mode**:
|
|
341
507
|
|
|
342
508
|
```typescript
|
|
343
|
-
// In offline mode, the SDK uses
|
|
509
|
+
// In offline mode, the SDK uses explicit local configuration only.
|
|
510
|
+
// It does not fetch from Reflag or use flagsFallbackProvider.
|
|
344
511
|
const client = new ReflagClient({
|
|
345
512
|
offline: true,
|
|
346
513
|
flagOverrides: () => ({
|
|
@@ -400,16 +567,19 @@ a configuration file on disk or by passing options to the `ReflagClient`
|
|
|
400
567
|
constructor. By default, the SDK searches for `reflag.config.json` in the
|
|
401
568
|
current working directory.
|
|
402
569
|
|
|
403
|
-
| Option
|
|
404
|
-
|
|
|
405
|
-
| `secretKey`
|
|
406
|
-
| `logLevel`
|
|
407
|
-
| `offline`
|
|
408
|
-
| `apiBaseUrl`
|
|
409
|
-
| `flagOverrides`
|
|
410
|
-
| `
|
|
570
|
+
| Option | Type | Description | Env Var |
|
|
571
|
+
| ----------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
|
572
|
+
| `secretKey` | string | The secret key used for authentication with Reflag's servers. | REFLAG_SECRET_KEY |
|
|
573
|
+
| `logLevel` | string | The log level for the SDK (e.g., `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`). Default: `INFO` | REFLAG_LOG_LEVEL |
|
|
574
|
+
| `offline` | boolean | Operate in offline mode. Default: `false`, except in tests it will default to `true` based off of the `TEST` env. var. In offline mode the SDK does not fetch from Reflag and does not use `flagsFallbackProvider`. | REFLAG_OFFLINE |
|
|
575
|
+
| `apiBaseUrl` | string | The base API URL for the Reflag servers. | REFLAG_API_BASE_URL |
|
|
576
|
+
| `flagOverrides` | Record<string, boolean> | An object specifying flag overrides for testing or local development. See [examples/express/app.test.ts](https://github.com/reflagcom/javascript/tree/main/packages/node-sdk/examples/express/app.test.ts) for how to use `flagOverrides` in tests. | REFLAG_FLAGS_ENABLED, REFLAG_FLAGS_DISABLED |
|
|
577
|
+
| `flagsFallbackProvider` | `FlagsFallbackProvider` | Optional provider used to load and save raw flag definitions for fallback startup when the initial live fetch fails. Available only through the constructor. Ignored in offline mode. | - |
|
|
578
|
+
| `configFile` | string | Load this config file from disk. Default: `reflag.config.json` | REFLAG_CONFIG_FILE |
|
|
411
579
|
|
|
412
|
-
> [!NOTE]
|
|
580
|
+
> [!NOTE]
|
|
581
|
+
>
|
|
582
|
+
> `REFLAG_FLAGS_ENABLED` and `REFLAG_FLAGS_DISABLED` are comma separated lists of flags which will be enabled or disabled respectively.
|
|
413
583
|
|
|
414
584
|
`reflag.config.json` example:
|
|
415
585
|
|
|
@@ -498,14 +668,32 @@ reflagClient.initialize().then(() => {
|
|
|
498
668
|
|
|
499
669
|
## Testing
|
|
500
670
|
|
|
501
|
-
When writing tests that cover code with flags, you can toggle flags on/off programmatically to test
|
|
671
|
+
When writing tests that cover code with flags, you can toggle flags on/off programmatically to test different behavior. For tests, you will often want to run the client in offline mode and provide flag overrides directly through the client options.
|
|
502
672
|
|
|
503
673
|
`reflag.ts`:
|
|
504
674
|
|
|
505
675
|
```typescript
|
|
506
676
|
import { ReflagClient } from "@reflag/node-sdk";
|
|
507
677
|
|
|
508
|
-
export const reflag = new ReflagClient(
|
|
678
|
+
export const reflag = new ReflagClient({
|
|
679
|
+
offline: true,
|
|
680
|
+
});
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
You can then set base overrides for a test run by passing `flagOverrides` in the constructor, replacing them later with `setFlagOverrides()`, or clearing them with `clearFlagOverrides()`:
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
// pass directly in the constructor
|
|
687
|
+
const client = new ReflagClient({
|
|
688
|
+
offline: true,
|
|
689
|
+
flagOverrides: { myFlag: true },
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// or replace the base overrides at a later time
|
|
693
|
+
client.setFlagOverrides({ myFlag: false });
|
|
694
|
+
|
|
695
|
+
// clear only the base overrides
|
|
696
|
+
client.clearFlagOverrides();
|
|
509
697
|
```
|
|
510
698
|
|
|
511
699
|
`app.test.ts`:
|
|
@@ -520,9 +708,9 @@ afterEach(() => {
|
|
|
520
708
|
|
|
521
709
|
describe("API Tests", () => {
|
|
522
710
|
it("should return 200 for the root endpoint", async () => {
|
|
523
|
-
reflag.
|
|
711
|
+
reflag.setFlagOverrides({
|
|
524
712
|
"show-todo": true,
|
|
525
|
-
};
|
|
713
|
+
});
|
|
526
714
|
|
|
527
715
|
const response = await request(app).get("/");
|
|
528
716
|
expect(response.status).toBe(200);
|
|
@@ -531,11 +719,61 @@ describe("API Tests", () => {
|
|
|
531
719
|
});
|
|
532
720
|
```
|
|
533
721
|
|
|
534
|
-
|
|
722
|
+
`pushFlagOverrides()` serves a different purpose: it adds a temporary layer on top of the base overrides and returns a remove function that removes only that layer. This is useful for nested tests:
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
export const flag = function (name: string, enabled: boolean): void {
|
|
726
|
+
let remove: (() => void) | undefined;
|
|
727
|
+
|
|
728
|
+
beforeEach(function () {
|
|
729
|
+
remove = reflagClient.pushFlagOverrides({ [name]: enabled });
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
afterEach(function () {
|
|
733
|
+
remove?.();
|
|
734
|
+
remove = undefined;
|
|
735
|
+
});
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
describe("foo", () => {
|
|
739
|
+
describe("with new search ranking enabled", () => {
|
|
740
|
+
flag("search-ranking-v2", true);
|
|
741
|
+
|
|
742
|
+
describe("with summaries enabled", () => {
|
|
743
|
+
flag("smart-summaries", true);
|
|
744
|
+
|
|
745
|
+
// ...
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
The precedence is:
|
|
752
|
+
|
|
753
|
+
1. Base overrides from the constructor or `setFlagOverrides()`
|
|
754
|
+
2. Temporary layers added by `pushFlagOverrides()`
|
|
755
|
+
|
|
756
|
+
If the same flag is set in both places, the pushed override wins until its remove function is called.
|
|
757
|
+
|
|
758
|
+
`pushFlagOverrides()` also accepts a function if the temporary override depends on the evaluation context:
|
|
759
|
+
|
|
760
|
+
```typescript
|
|
761
|
+
const remove = client.pushFlagOverrides((context) => ({
|
|
762
|
+
"smart-summaries": context.user?.id === "qa-user",
|
|
763
|
+
}));
|
|
764
|
+
|
|
765
|
+
// ...
|
|
766
|
+
|
|
767
|
+
remove();
|
|
768
|
+
```
|
|
535
769
|
|
|
536
770
|
## Flag Overrides
|
|
537
771
|
|
|
538
|
-
Flag overrides allow you to override flags and their configurations locally. This is particularly useful for
|
|
772
|
+
Flag overrides allow you to override flags and their configurations locally. This is particularly useful when testing changes locally, for example when running your app and clicking around to verify behavior before deploying your changes.
|
|
773
|
+
|
|
774
|
+
For automated tests, see the [Testing](#testing) section above.
|
|
775
|
+
|
|
776
|
+
When testing locally during development, you also have these additional ways to provide overrides:
|
|
539
777
|
|
|
540
778
|
1. Through environment variables:
|
|
541
779
|
|
|
@@ -563,20 +801,6 @@ REFLAG_FLAGS_DISABLED=flag3,flag4
|
|
|
563
801
|
}
|
|
564
802
|
```
|
|
565
803
|
|
|
566
|
-
1. Programmatically through the client options:
|
|
567
|
-
|
|
568
|
-
You can use a simple `Record<string, boolean>` and pass it either in the constructor or by setting `client.flagOverrides`:
|
|
569
|
-
|
|
570
|
-
```typescript
|
|
571
|
-
// pass directly in the constructor
|
|
572
|
-
const client = new ReflagClient({ flagOverrides: { myFlag: true } });
|
|
573
|
-
// or set on the client at a later time
|
|
574
|
-
client.flagOverrides = { myFlag: false };
|
|
575
|
-
|
|
576
|
-
// clear flag overrides. Same as setting to {}.
|
|
577
|
-
client.clearFlagOverrides();
|
|
578
|
-
```
|
|
579
|
-
|
|
580
804
|
To get dynamic overrides, use a function which takes a context and returns a boolean or an object with the shape of `{isEnabled, config}`:
|
|
581
805
|
|
|
582
806
|
```typescript
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reflag/node-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
"vitest": "~1.6.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@aws-sdk/client-s3": "^3.888.0",
|
|
49
|
+
"@google-cloud/storage": "^7.19.0",
|
|
50
|
+
"@redis/client": "^5.11.0",
|
|
48
51
|
"@reflag/flag-evaluation": "1.0.0"
|
|
49
52
|
}
|
|
50
53
|
}
|