@reflag/node-sdk 1.3.0 → 1.4.1
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 +199 -58
- package/dist/package.json +4 -1
- package/dist/src/batch-buffer.js +1 -1
- package/dist/src/batch-buffer.js.map +1 -1
- package/dist/src/client.js +119 -32
- 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 +3 -2
- 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 +49 -1
- 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
|
@@ -59,10 +59,6 @@ You can also [use the HTTP API directly](https://docs.reflag.com/api/http-api)
|
|
|
59
59
|
To get started you need to obtain your secret key from the [environment settings](https://app.reflag.com/env-current/settings/app-environments)
|
|
60
60
|
in Reflag.
|
|
61
61
|
|
|
62
|
-
> [!CAUTION]
|
|
63
|
-
> Secret keys are meant for use in server side SDKs only. Secret keys offer the users the ability to obtain
|
|
64
|
-
> information that is often sensitive and thus should not be used in client-side applications.
|
|
65
|
-
|
|
66
62
|
Reflag will load settings through the various environment variables automatically (see [Configuring](#configuring) below).
|
|
67
63
|
|
|
68
64
|
1. Find the Reflag secret key for your development environment under [environment settings](https://app.reflag.com/env-current/settings/app-environments) in Reflag.
|
|
@@ -91,7 +87,7 @@ Once the client is initialized, you can obtain flags along with the `isEnabled`
|
|
|
91
87
|
status to indicate whether the flag is targeted for this user/company:
|
|
92
88
|
|
|
93
89
|
> [!IMPORTANT]
|
|
94
|
-
> If `user.id`
|
|
90
|
+
> If `user.id` is not given, the whole `user` object is ignore. Similarly, without `company.id` the `company` object is ignored.
|
|
95
91
|
|
|
96
92
|
```typescript
|
|
97
93
|
// configure the client
|
|
@@ -175,15 +171,8 @@ await client.flush();
|
|
|
175
171
|
### Rate Limiting
|
|
176
172
|
|
|
177
173
|
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
|
-
```
|
|
174
|
+
Rate limiting is applied per unique combination of flag key and evaluation context.
|
|
175
|
+
This behavior is built in and does not currently require configuration.
|
|
187
176
|
|
|
188
177
|
### Flag definitions
|
|
189
178
|
|
|
@@ -206,9 +195,176 @@ const flagDefs = await client.getFlagDefinitions();
|
|
|
206
195
|
// }]
|
|
207
196
|
```
|
|
208
197
|
|
|
198
|
+
### Fallback provider
|
|
199
|
+
|
|
200
|
+
`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.
|
|
201
|
+
|
|
202
|
+
> [!NOTE]
|
|
203
|
+
>
|
|
204
|
+
> `fallbackFlags` is deprecated. Prefer `flagsFallbackProvider` for startup fallback and outage recovery.
|
|
205
|
+
> `flagsFallbackProvider` is not used in offline mode.
|
|
206
|
+
|
|
207
|
+
#### How it works
|
|
208
|
+
|
|
209
|
+
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.
|
|
210
|
+
|
|
211
|
+
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.
|
|
212
|
+
|
|
213
|
+
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.
|
|
214
|
+
|
|
215
|
+
After successfully fetching updated flag definitions, the SDK calls `flagsFallbackProvider.save()` to keep the stored snapshot up to date.
|
|
216
|
+
|
|
217
|
+
Typical reliability flow:
|
|
218
|
+
|
|
219
|
+
1. The SDK starts and tries to fetch live flag definitions from Reflag.
|
|
220
|
+
2. If that succeeds, those definitions are used immediately and the SDK continues operating normally.
|
|
221
|
+
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.
|
|
222
|
+
4. If a future process starts while Reflag is unavailable, it can load the last saved snapshot from the fallback provider and still initialize.
|
|
223
|
+
5. Once Reflag becomes available again, the SDK resumes using live data and refreshes the fallback snapshot.
|
|
224
|
+
|
|
225
|
+
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.
|
|
226
|
+
|
|
227
|
+
> [!TIP]
|
|
228
|
+
> 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.
|
|
229
|
+
>
|
|
230
|
+
> `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.
|
|
231
|
+
>
|
|
232
|
+
> This applies to React (`getFlagsForBootstrap()` + `ReflagBootstrappedProvider`), the Browser SDK (`bootstrappedFlags`), and the Vue SDK (bootstrapped flags via the provider).
|
|
233
|
+
|
|
234
|
+
#### Built-in providers
|
|
235
|
+
|
|
236
|
+
You can access the built-in providers through the `fallbackProviders` namespace:
|
|
237
|
+
|
|
238
|
+
- `fallbackProviders.static(...)`
|
|
239
|
+
- `fallbackProviders.file(...)`
|
|
240
|
+
- `fallbackProviders.redis(...)`
|
|
241
|
+
- `fallbackProviders.s3(...)`
|
|
242
|
+
- `fallbackProviders.gcs(...)`
|
|
243
|
+
|
|
244
|
+
##### Static provider
|
|
245
|
+
|
|
246
|
+
If you just want a fixed fallback copy of simple enabled/disabled flags, you can provide a static map:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
|
|
250
|
+
|
|
251
|
+
const client = new ReflagClient({
|
|
252
|
+
secretKey: process.env.REFLAG_SECRET_KEY,
|
|
253
|
+
flagsFallbackProvider: fallbackProviders.static({
|
|
254
|
+
flags: {
|
|
255
|
+
huddle: true,
|
|
256
|
+
"smart-summaries": false,
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await client.initialize();
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
##### File provider
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
|
|
268
|
+
|
|
269
|
+
const client = new ReflagClient({
|
|
270
|
+
secretKey: process.env.REFLAG_SECRET_KEY,
|
|
271
|
+
flagsFallbackProvider: fallbackProviders.file({
|
|
272
|
+
directory: ".reflag",
|
|
273
|
+
}),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await client.initialize();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The file provider stores one snapshot file per environment in the configured
|
|
280
|
+
`directory`.
|
|
281
|
+
|
|
282
|
+
##### Redis provider
|
|
283
|
+
|
|
284
|
+
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.
|
|
285
|
+
|
|
286
|
+
Without a `keyPrefix` set, it will default to to the key `reflag:flags-fallback:${secretKeyHash}`.
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
|
|
290
|
+
|
|
291
|
+
const client = new ReflagClient({
|
|
292
|
+
secretKey: process.env.REFLAG_SECRET_KEY,
|
|
293
|
+
flagsFallbackProvider: fallbackProviders.redis(),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await client.initialize();
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
##### S3 provider
|
|
300
|
+
|
|
301
|
+
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.
|
|
302
|
+
|
|
303
|
+
Without a `keyPrefix` set, it will default to path `reflag/flags-fallback/${secretKeyHash}`.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
|
|
307
|
+
|
|
308
|
+
const client = new ReflagClient({
|
|
309
|
+
secretKey: process.env.REFLAG_SECRET_KEY,
|
|
310
|
+
flagsFallbackProvider: fallbackProviders.s3({
|
|
311
|
+
bucket: "reflag-fallback-bucket",
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await client.initialize();
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
##### GCS provider
|
|
319
|
+
|
|
320
|
+
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.
|
|
321
|
+
|
|
322
|
+
Without a `keyPrefix` set, it will default to path `reflag/flags-fallback/${secretKeyHash}`.
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
|
|
326
|
+
|
|
327
|
+
const client = new ReflagClient({
|
|
328
|
+
secretKey: process.env.REFLAG_SECRET_KEY,
|
|
329
|
+
flagsFallbackProvider: fallbackProviders.gcs({
|
|
330
|
+
bucket: "reflag-fallback-bucket",
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await client.initialize();
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### Testing fallback startup locally
|
|
338
|
+
|
|
339
|
+
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` (or set the `REFLAG_API_BASE_URL` environment variable) 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.
|
|
340
|
+
|
|
341
|
+
#### Writing a custom provider
|
|
342
|
+
|
|
343
|
+
If you just store definitions in your database or similar, a custom provider can be very small:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import type {
|
|
347
|
+
FlagsFallbackProvider,
|
|
348
|
+
FlagsFallbackSnapshot,
|
|
349
|
+
} from "@reflag/node-sdk";
|
|
350
|
+
|
|
351
|
+
export const customFallbackProvider: FlagsFallbackProvider = {
|
|
352
|
+
async load(context) {
|
|
353
|
+
// load snapshot from database
|
|
354
|
+
// optionally, look up the snapshot using the context.secretKeyHash as a key
|
|
355
|
+
return snapshot;
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
async save(context, snapshot) {
|
|
359
|
+
const serialized = JSON.stringify(snapshot);
|
|
360
|
+
// write serialized snapshot to database, optionally using context.secretKeyHash as a key
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
```
|
|
364
|
+
|
|
209
365
|
## Bootstrapping client-side applications
|
|
210
366
|
|
|
211
|
-
The `getFlagsForBootstrap()` method is
|
|
367
|
+
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
368
|
|
|
213
369
|
```typescript
|
|
214
370
|
const client = new ReflagClient();
|
|
@@ -340,7 +496,8 @@ fallback behavior:
|
|
|
340
496
|
4. **Offline Mode**:
|
|
341
497
|
|
|
342
498
|
```typescript
|
|
343
|
-
// In offline mode, the SDK uses
|
|
499
|
+
// In offline mode, the SDK uses explicit local configuration only.
|
|
500
|
+
// It does not fetch from Reflag or use flagsFallbackProvider.
|
|
344
501
|
const client = new ReflagClient({
|
|
345
502
|
offline: true,
|
|
346
503
|
flagOverrides: () => ({
|
|
@@ -400,16 +557,19 @@ a configuration file on disk or by passing options to the `ReflagClient`
|
|
|
400
557
|
constructor. By default, the SDK searches for `reflag.config.json` in the
|
|
401
558
|
current working directory.
|
|
402
559
|
|
|
403
|
-
| Option
|
|
404
|
-
|
|
|
405
|
-
| `secretKey`
|
|
406
|
-
| `logLevel`
|
|
407
|
-
| `offline`
|
|
408
|
-
| `apiBaseUrl`
|
|
409
|
-
| `flagOverrides`
|
|
410
|
-
| `
|
|
560
|
+
| Option | Type | Description | Env Var |
|
|
561
|
+
| ----------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
|
562
|
+
| `secretKey` | string | The secret key used for authentication with Reflag's servers. | REFLAG_SECRET_KEY |
|
|
563
|
+
| `logLevel` | string | The log level for the SDK (e.g., `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`). Default: `INFO` | REFLAG_LOG_LEVEL |
|
|
564
|
+
| `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 |
|
|
565
|
+
| `apiBaseUrl` | string | The base API URL for the Reflag servers. | REFLAG_API_BASE_URL |
|
|
566
|
+
| `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 |
|
|
567
|
+
| `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. | - |
|
|
568
|
+
| `configFile` | string | Load this config file from disk. Default: `reflag.config.json` | REFLAG_CONFIG_FILE |
|
|
411
569
|
|
|
412
|
-
> [!NOTE]
|
|
570
|
+
> [!NOTE]
|
|
571
|
+
>
|
|
572
|
+
> `REFLAG_FLAGS_ENABLED` and `REFLAG_FLAGS_DISABLED` are comma separated lists of flags which will be enabled or disabled respectively.
|
|
413
573
|
|
|
414
574
|
`reflag.config.json` example:
|
|
415
575
|
|
|
@@ -496,9 +656,9 @@ reflagClient.initialize().then(() => {
|
|
|
496
656
|
|
|
497
657
|

|
|
498
658
|
|
|
499
|
-
## Testing
|
|
659
|
+
## Testing with flag overrides
|
|
500
660
|
|
|
501
|
-
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
|
|
661
|
+
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:
|
|
502
662
|
|
|
503
663
|
`reflag.ts`:
|
|
504
664
|
|
|
@@ -510,7 +670,11 @@ export const reflag = new ReflagClient({
|
|
|
510
670
|
});
|
|
511
671
|
```
|
|
512
672
|
|
|
513
|
-
|
|
673
|
+
There are a few ways to programmatically manipulate the overrides which are appropriate when testing:
|
|
674
|
+
|
|
675
|
+
### Base overrides
|
|
676
|
+
|
|
677
|
+
You can set base overrides for a test run by passing `flagOverrides` in the constructor, replacing them later with `setFlagOverrides()` and clearing them with `clearFlagOverrides()`:
|
|
514
678
|
|
|
515
679
|
```typescript
|
|
516
680
|
// pass directly in the constructor
|
|
@@ -549,6 +713,8 @@ describe("API Tests", () => {
|
|
|
549
713
|
});
|
|
550
714
|
```
|
|
551
715
|
|
|
716
|
+
### Layering overrides
|
|
717
|
+
|
|
552
718
|
`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:
|
|
553
719
|
|
|
554
720
|
```typescript
|
|
@@ -585,7 +751,9 @@ The precedence is:
|
|
|
585
751
|
|
|
586
752
|
If the same flag is set in both places, the pushed override wins until its remove function is called.
|
|
587
753
|
|
|
588
|
-
|
|
754
|
+
### Context dependent overrides
|
|
755
|
+
|
|
756
|
+
`setFlagOverrides()` and `pushFlagOverrides()` also accept a function if the override depends on the evaluation context:
|
|
589
757
|
|
|
590
758
|
```typescript
|
|
591
759
|
const remove = client.pushFlagOverrides((context) => ({
|
|
@@ -597,13 +765,9 @@ const remove = client.pushFlagOverrides((context) => ({
|
|
|
597
765
|
remove();
|
|
598
766
|
```
|
|
599
767
|
|
|
600
|
-
|
|
768
|
+
### Additional ways to provide flag overrides
|
|
601
769
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
For automated tests, see the [Testing](#testing) section above.
|
|
605
|
-
|
|
606
|
-
When testing locally during development, you also have these additional ways to provide overrides:
|
|
770
|
+
You also have these additional ways to provide overrides, which can be helpful when testing out locally:
|
|
607
771
|
|
|
608
772
|
1. Through environment variables:
|
|
609
773
|
|
|
@@ -631,29 +795,6 @@ REFLAG_FLAGS_DISABLED=flag3,flag4
|
|
|
631
795
|
}
|
|
632
796
|
```
|
|
633
797
|
|
|
634
|
-
To get dynamic overrides, use a function which takes a context and returns a boolean or an object with the shape of `{isEnabled, config}`:
|
|
635
|
-
|
|
636
|
-
```typescript
|
|
637
|
-
import { ReflagClient, Context } from "@reflag/node-sdk";
|
|
638
|
-
|
|
639
|
-
const flagOverrides = (context: Context) => ({
|
|
640
|
-
"delete-todos": {
|
|
641
|
-
isEnabled: true,
|
|
642
|
-
config: {
|
|
643
|
-
key: "dev-config",
|
|
644
|
-
payload: {
|
|
645
|
-
requireConfirmation: true,
|
|
646
|
-
maxDeletionsPerDay: 5,
|
|
647
|
-
},
|
|
648
|
-
},
|
|
649
|
-
},
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
const client = new ReflagClient({
|
|
653
|
-
flagOverrides,
|
|
654
|
-
});
|
|
655
|
-
```
|
|
656
|
-
|
|
657
798
|
## Remote Flag Evaluation
|
|
658
799
|
|
|
659
800
|
In addition to local flag evaluation, Reflag supports remote evaluation using stored context. This is useful when you want to evaluate flags using user/company attributes that were previously sent to Reflag:
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reflag/node-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
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
|
}
|
package/dist/src/batch-buffer.js
CHANGED
|
@@ -73,7 +73,7 @@ class BatchBuffer {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
catch (error) {
|
|
76
|
-
(_c = this.logger) === null || _c === void 0 ? void 0 : _c.
|
|
76
|
+
(_c = this.logger) === null || _c === void 0 ? void 0 : _c.warn("flush of buffered items failed; discarding items", {
|
|
77
77
|
error,
|
|
78
78
|
count: flushingBuffer.length,
|
|
79
79
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch-buffer.js","sourceRoot":"","sources":["../../src/batch-buffer.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,qCAA6D;AAE7D,mCAAuC;AAEvC;;;GAGG;AACH,MAAqB,WAAW;IAQ9B;;;;OAIG;IACH,YAAY,OAA8B;;QAZlC,WAAM,GAAQ,EAAE,CAAC;QAKjB,UAAK,GAA0B,IAAI,CAAC;QAQ1C,IAAA,UAAE,EAAC,IAAA,gBAAQ,EAAC,OAAO,CAAC,EAAE,2BAA2B,CAAC,CAAC;QACnD,IAAA,UAAE,EACA,OAAO,OAAO,CAAC,YAAY,KAAK,UAAU,EAC1C,iCAAiC,CAClC,CAAC;QACF,IAAA,UAAE,EAAC,IAAA,gBAAQ,EAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,0BAA0B,CAAC,CAAC;QAC5E,IAAA,UAAE,EACA,CAAC,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC;YAC1D,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,EACrC,gCAAgC,CACjC,CAAC;QACF,IAAA,UAAE,EACA,CAAC,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ,IAAI,OAAO,CAAC,UAAU,IAAI,CAAC,CAAC;YACjE,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ,EACxC,+CAA+C,CAChD,CAAC;QAEF,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QACzC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,MAAA,OAAO,CAAC,OAAO,mCAAI,uBAAc,CAAC;QACjD,IAAI,CAAC,UAAU,GAAG,MAAA,OAAO,CAAC,UAAU,mCAAI,0BAAiB,CAAC;IAC5D,CAAC;IAED;;;;OAIG;IACU,GAAG,CAAC,IAAO;;YACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAEvB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACvC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACrB,CAAC;iBAAM,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;gBAC9C,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,KAAK,EAAE,CAAC;YACvE,CAAC;QACH,CAAC;KAAA;IAEY,KAAK;;;YAChB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YACpB,CAAC;YAED,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC7B,MAAA,IAAI,CAAC,MAAM,0CAAE,KAAK,CAAC,mCAAmC,CAAC,CAAC;gBACxD,OAAO;YACT,CAAC;YAED,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;YACnC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;YAEjB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC;gBAExC,MAAA,IAAI,CAAC,MAAM,0CAAE,IAAI,CAAC,wBAAwB,EAAE;oBAC1C,KAAK,EAAE,cAAc,CAAC,MAAM;iBAC7B,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAA,IAAI,CAAC,MAAM,0CAAE,
|
|
1
|
+
{"version":3,"file":"batch-buffer.js","sourceRoot":"","sources":["../../src/batch-buffer.ts"],"names":[],"mappings":"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"}
|
package/dist/src/client.js
CHANGED
|
@@ -62,6 +62,7 @@ const flag_evaluation_1 = require("@reflag/flag-evaluation");
|
|
|
62
62
|
const batch_buffer_1 = __importDefault(require("./batch-buffer"));
|
|
63
63
|
const config_1 = require("./config");
|
|
64
64
|
const fetch_http_client_1 = __importStar(require("./fetch-http-client"));
|
|
65
|
+
const flagsFallbackProvider_1 = require("./flagsFallbackProvider");
|
|
65
66
|
const flusher_1 = require("./flusher");
|
|
66
67
|
const inRequestCache_1 = __importDefault(require("./inRequestCache"));
|
|
67
68
|
const periodicallyUpdatingCache_1 = __importDefault(require("./periodicallyUpdatingCache"));
|
|
@@ -80,6 +81,54 @@ function composeFlagOverrides(baseOverrides, layers) {
|
|
|
80
81
|
}
|
|
81
82
|
return (context) => layers.reduce((acc, layer) => (Object.assign(Object.assign({}, acc), layer.overrides(context))), baseOverrides(context));
|
|
82
83
|
}
|
|
84
|
+
function compileFlagDefinitions(flags) {
|
|
85
|
+
return flags.map((flagDef) => {
|
|
86
|
+
return Object.assign(Object.assign({}, flagDef), { enabledEvaluator: (0, flag_evaluation_1.newEvaluator)(flagDef.targeting.rules.map((rule) => ({
|
|
87
|
+
filter: rule.filter,
|
|
88
|
+
value: true,
|
|
89
|
+
}))), configEvaluator: flagDef.config
|
|
90
|
+
? (0, flag_evaluation_1.newEvaluator)(flagDef.config.variants.map((variant) => ({
|
|
91
|
+
filter: variant.filter,
|
|
92
|
+
value: {
|
|
93
|
+
key: variant.key,
|
|
94
|
+
payload: variant.payload,
|
|
95
|
+
},
|
|
96
|
+
})))
|
|
97
|
+
: undefined });
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function createFlagsFallbackSnapshot(flags) {
|
|
101
|
+
return {
|
|
102
|
+
version: 1,
|
|
103
|
+
savedAt: new Date().toISOString(),
|
|
104
|
+
flags,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function formatFlagsFallbackAge(savedAt) {
|
|
108
|
+
const savedAtMs = Date.parse(savedAt);
|
|
109
|
+
if (!Number.isFinite(savedAtMs)) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const ageMs = Math.max(0, Date.now() - savedAtMs);
|
|
113
|
+
const minuteMs = 60000;
|
|
114
|
+
const hourMs = 60 * minuteMs;
|
|
115
|
+
const dayMs = 24 * hourMs;
|
|
116
|
+
if (ageMs < minuteMs) {
|
|
117
|
+
return "<1m";
|
|
118
|
+
}
|
|
119
|
+
if (ageMs < hourMs) {
|
|
120
|
+
return `${Math.floor(ageMs / minuteMs)}m`;
|
|
121
|
+
}
|
|
122
|
+
if (ageMs < dayMs) {
|
|
123
|
+
return `${Math.floor(ageMs / hourMs)}h`;
|
|
124
|
+
}
|
|
125
|
+
return `${Math.floor(ageMs / dayMs)}d`;
|
|
126
|
+
}
|
|
127
|
+
function createErrorWithCause(message, cause) {
|
|
128
|
+
const error = new Error(message);
|
|
129
|
+
error.cause = cause;
|
|
130
|
+
return error;
|
|
131
|
+
}
|
|
83
132
|
/**
|
|
84
133
|
* The SDK client.
|
|
85
134
|
*
|
|
@@ -127,6 +176,7 @@ class ReflagClient {
|
|
|
127
176
|
this.flagOverrideLayers = [];
|
|
128
177
|
this.nextFlagOverrideLayerId = 0;
|
|
129
178
|
this.initializationFinished = false;
|
|
179
|
+
this.canLoadFlagsFallbackProvider = true;
|
|
130
180
|
this._initialize = (0, utils_1.once)(() => __awaiter(this, void 0, void 0, function* () {
|
|
131
181
|
const start = Date.now();
|
|
132
182
|
if (!this._config.offline) {
|
|
@@ -146,6 +196,10 @@ class ReflagClient {
|
|
|
146
196
|
options.apiBaseUrl.length > 0), "apiBaseUrl must be a string");
|
|
147
197
|
(0, utils_1.ok)(options.logger === undefined || (0, utils_1.isObject)(options.logger), "logger must be an object");
|
|
148
198
|
(0, utils_1.ok)(options.httpClient === undefined || (0, utils_1.isObject)(options.httpClient), "httpClient must be an object");
|
|
199
|
+
(0, utils_1.ok)(options.flagsFallbackProvider === undefined ||
|
|
200
|
+
((0, utils_1.isObject)(options.flagsFallbackProvider) &&
|
|
201
|
+
typeof options.flagsFallbackProvider.load === "function" &&
|
|
202
|
+
typeof options.flagsFallbackProvider.save === "function"), "flagsFallbackProvider must be an object with load/save functions");
|
|
149
203
|
(0, utils_1.ok)(options.fallbackFlags === undefined ||
|
|
150
204
|
Array.isArray(options.fallbackFlags) ||
|
|
151
205
|
(0, utils_1.isObject)(options.fallbackFlags), "fallbackFlags must be an array or object");
|
|
@@ -217,8 +271,11 @@ class ReflagClient {
|
|
|
217
271
|
["Authorization"]: `Bearer ${config.secretKey}`,
|
|
218
272
|
},
|
|
219
273
|
refetchInterval: config_1.FLAGS_REFETCH_MS,
|
|
220
|
-
staleWarningInterval: config_1.FLAGS_REFETCH_MS * 5,
|
|
221
274
|
fallbackFlags: fallbackFlags,
|
|
275
|
+
flagsFallbackProvider: options.flagsFallbackProvider,
|
|
276
|
+
flagsFallbackProviderContext: {
|
|
277
|
+
secretKeyHash: config.secretKey ? (0, utils_1.hashString)(config.secretKey) : "",
|
|
278
|
+
},
|
|
222
279
|
flagOverrides: baseFlagOverrides,
|
|
223
280
|
flagsFetchRetries: (_f = options.flagsFetchRetries) !== null && _f !== void 0 ? _f : 3,
|
|
224
281
|
fetchTimeoutMs: (_g = options.fetchTimeoutMs) !== null && _g !== void 0 ? _g : config_1.API_TIMEOUT_MS,
|
|
@@ -234,32 +291,65 @@ class ReflagClient {
|
|
|
234
291
|
const fetchFlags = () => __awaiter(this, void 0, void 0, function* () {
|
|
235
292
|
const res = yield this.get("features", this._config.flagsFetchRetries);
|
|
236
293
|
if (!(0, utils_1.isObject)(res) || !Array.isArray(res === null || res === void 0 ? void 0 : res.features)) {
|
|
237
|
-
this.
|
|
238
|
-
return undefined;
|
|
294
|
+
return yield this.loadFlagsFallbackDefinitions();
|
|
239
295
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
filter: rule.filter,
|
|
244
|
-
value: true,
|
|
245
|
-
}))), configEvaluator: flagDef.config
|
|
246
|
-
? (0, flag_evaluation_1.newEvaluator)((_a = flagDef.config) === null || _a === void 0 ? void 0 : _a.variants.map((variant) => ({
|
|
247
|
-
filter: variant.filter,
|
|
248
|
-
value: {
|
|
249
|
-
key: variant.key,
|
|
250
|
-
payload: variant.payload,
|
|
251
|
-
},
|
|
252
|
-
})))
|
|
253
|
-
: undefined });
|
|
254
|
-
});
|
|
296
|
+
void this.saveFlagsFallbackDefinitions(res.features);
|
|
297
|
+
this.canLoadFlagsFallbackProvider = false;
|
|
298
|
+
return compileFlagDefinitions(res.features);
|
|
255
299
|
});
|
|
256
300
|
if (this._config.cacheStrategy === "periodically-update") {
|
|
257
|
-
this.flagsCache = (0, periodicallyUpdatingCache_1.default)(this._config.refetchInterval, this.
|
|
301
|
+
this.flagsCache = (0, periodicallyUpdatingCache_1.default)(this._config.refetchInterval, this.logger, fetchFlags);
|
|
258
302
|
}
|
|
259
303
|
else {
|
|
260
304
|
this.flagsCache = (0, inRequestCache_1.default)(this._config.refetchInterval, this.logger, fetchFlags);
|
|
261
305
|
}
|
|
262
306
|
}
|
|
307
|
+
loadFlagsFallbackDefinitions() {
|
|
308
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
309
|
+
if (!this.canLoadFlagsFallbackProvider) {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
this.canLoadFlagsFallbackProvider = false;
|
|
313
|
+
const provider = this._config.flagsFallbackProvider;
|
|
314
|
+
if (!provider) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const snapshot = yield provider.load(this._config.flagsFallbackProviderContext);
|
|
319
|
+
if (!snapshot) {
|
|
320
|
+
this.logger.warn("remote flags unavailable, no fallback flags found in flagsFallbackProvider");
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
if (!(0, flagsFallbackProvider_1.isFlagsFallbackSnapshot)(snapshot)) {
|
|
324
|
+
this.logger.warn("flagsFallbackProvider: invalid snapshot returned");
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
const fallbackAge = formatFlagsFallbackAge(snapshot.savedAt);
|
|
328
|
+
this.logger.warn(fallbackAge
|
|
329
|
+
? `remote flags unavailable, using fallback flags fetched ${fallbackAge} ago (${snapshot.savedAt})`
|
|
330
|
+
: `remote flags unavailable, using fallback flags (${snapshot.savedAt})`);
|
|
331
|
+
return compileFlagDefinitions(snapshot.flags);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
this.logger.error("flagsFallbackProvider: failed to load flag definitions", error);
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
saveFlagsFallbackDefinitions(flags) {
|
|
340
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
341
|
+
const provider = this._config.flagsFallbackProvider;
|
|
342
|
+
if (!provider) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
yield provider.save(this._config.flagsFallbackProviderContext, createFlagsFallbackSnapshot(flags));
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
this.logger.error("flagsFallbackProvider: failed to save flag definitions", error);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
263
353
|
/**
|
|
264
354
|
* Replaces the base flag overrides used by the client.
|
|
265
355
|
*
|
|
@@ -659,8 +749,6 @@ class ReflagClient {
|
|
|
659
749
|
* @param path - The path to send the request to.
|
|
660
750
|
* @param body - The body of the request.
|
|
661
751
|
*
|
|
662
|
-
* @returns A boolean indicating if the request was successful.
|
|
663
|
-
*
|
|
664
752
|
* @throws An error if the path or body is invalid.
|
|
665
753
|
**/
|
|
666
754
|
post(path, body) {
|
|
@@ -672,14 +760,11 @@ class ReflagClient {
|
|
|
672
760
|
const response = yield this.httpClient.post(url, this._config.headers, body);
|
|
673
761
|
this.logger.debug(`post request to "${url}"`, response);
|
|
674
762
|
if (!response.ok || !(0, utils_1.isObject)(response.body) || !response.body.success) {
|
|
675
|
-
|
|
676
|
-
return false;
|
|
763
|
+
throw createErrorWithCause(`invalid response received from server for "${url}"`, JSON.stringify(response));
|
|
677
764
|
}
|
|
678
|
-
return true;
|
|
679
765
|
}
|
|
680
766
|
catch (error) {
|
|
681
|
-
|
|
682
|
-
return false;
|
|
767
|
+
throw createErrorWithCause(`post request to "${url}" failed with error`, error);
|
|
683
768
|
}
|
|
684
769
|
});
|
|
685
770
|
}
|
|
@@ -707,12 +792,12 @@ class ReflagClient {
|
|
|
707
792
|
}
|
|
708
793
|
const _a = response.body, { success: _ } = _a, result = __rest(_a, ["success"]);
|
|
709
794
|
return result;
|
|
710
|
-
}), () => {
|
|
711
|
-
this.logger.warn("failed to fetch flags, will retry");
|
|
795
|
+
}), (error) => {
|
|
796
|
+
this.logger.warn("failed to fetch flags, will retry", error);
|
|
712
797
|
}, retries, 1000, 10000);
|
|
713
798
|
}
|
|
714
799
|
catch (error) {
|
|
715
|
-
this.logger.
|
|
800
|
+
this.logger.debug(`get request to "${path}" failed with error after ${retries} retries`, error);
|
|
716
801
|
return undefined;
|
|
717
802
|
}
|
|
718
803
|
});
|
|
@@ -727,9 +812,11 @@ class ReflagClient {
|
|
|
727
812
|
sendBulkEvents(events) {
|
|
728
813
|
return __awaiter(this, void 0, void 0, function* () {
|
|
729
814
|
(0, utils_1.ok)(Array.isArray(events) && events.length > 0, "events must be a non-empty array");
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
815
|
+
try {
|
|
816
|
+
yield this.post("bulk", events);
|
|
817
|
+
}
|
|
818
|
+
catch (error) {
|
|
819
|
+
throw createErrorWithCause("failed to send bulk events", error);
|
|
733
820
|
}
|
|
734
821
|
});
|
|
735
822
|
}
|