Skip to content

Commit b7680cc

Browse files
committed
feat(node-sdk): add static fallback provider
1 parent a35e254 commit b7680cc

4 files changed

Lines changed: 122 additions & 3 deletions

File tree

packages/node-sdk/README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,32 @@ The file provider stores one snapshot file per environment in the configured
256256
You can also access the built-in providers through the `fallbackProviders`
257257
namespace:
258258

259+
- `fallbackProviders.static(...)`
259260
- `fallbackProviders.file(...)`
260261
- `fallbackProviders.redis(...)`
261262
- `fallbackProviders.s3(...)`
262263
- `fallbackProviders.gcs(...)`
263264

265+
#### Built-in static provider
266+
267+
If you just want a fixed fallback copy of simple enabled/disabled flags, you can provide a static map:
268+
269+
```typescript
270+
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
271+
272+
const client = new ReflagClient({
273+
secretKey: process.env.REFLAG_SECRET_KEY,
274+
flagsFallbackProvider: fallbackProviders.static({
275+
flags: {
276+
huddle: true,
277+
"smart-summaries": false,
278+
},
279+
}),
280+
});
281+
282+
await client.initialize();
283+
```
284+
264285
#### Built-in Redis provider
265286

266287
The built-in Redis provider creates a Redis client automatically when omitted and uses `REDIS_URL` from the environment.
@@ -348,12 +369,14 @@ export const staticFallbackProvider: FlagsFallbackProvider = {
348369
};
349370
```
350371

351-
> [!NOTE] > `fallbackFlags` is deprecated. Prefer `flagsFallbackProvider` for startup fallback and outage recovery.
372+
> [!NOTE]
373+
>
374+
> `fallbackFlags` is deprecated. Prefer `flagsFallbackProvider` for startup fallback and outage recovery.
352375
> `flagsFallbackProvider` is not used in offline mode.
353376
354377
## Bootstrapping client-side applications
355378

356-
The `getFlagsForBootstrap()` method is designed for server-side rendering (SSR) scenarios where you need to pass flag data to client-side applications. This method returns raw flag data without wrapper functions, making it suitable for serialization and client-side hydration.
379+
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.
357380

358381
```typescript
359382
const client = new ReflagClient();
@@ -556,7 +579,9 @@ current working directory.
556579
| `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. | - |
557580
| `configFile` | string | Load this config file from disk. Default: `reflag.config.json` | REFLAG_CONFIG_FILE |
558581

559-
> [!NOTE] > `REFLAG_FLAGS_ENABLED` and `REFLAG_FLAGS_DISABLED` are comma separated lists of flags which will be enabled or disabled respectively.
582+
> [!NOTE]
583+
>
584+
> `REFLAG_FLAGS_ENABLED` and `REFLAG_FLAGS_DISABLED` are comma separated lists of flags which will be enabled or disabled respectively.
560585
561586
`reflag.config.json` example:
562587

packages/node-sdk/src/flagsFallbackProvider.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ export type RedisFallbackProviderOptions = {
8383
keyPrefix?: string;
8484
};
8585

86+
export type StaticFallbackProviderOptions = {
87+
/**
88+
* Static fallback flags keyed by flag key.
89+
*/
90+
flags: Record<string, boolean>;
91+
};
92+
8693
function defaultSnapshotName(secretKeyHash: string) {
8794
return `flags-fallback-${secretKeyHash.slice(0, 16)}.json`;
8895
}
@@ -162,6 +169,24 @@ function parseSnapshot(raw: string) {
162169
return isFlagsFallbackSnapshot(parsed) ? parsed : undefined;
163170
}
164171

172+
function staticFlagApiResponse(key: string, isEnabled: boolean): FlagAPIResponse {
173+
return {
174+
key,
175+
description: null,
176+
targeting: {
177+
version: 1,
178+
rules: [
179+
{
180+
filter: {
181+
type: "constant",
182+
value: isEnabled,
183+
},
184+
},
185+
],
186+
},
187+
};
188+
}
189+
165190
async function createDefaultS3Client() {
166191
const { S3Client } = await import("@aws-sdk/client-s3");
167192
return new S3Client({});
@@ -172,6 +197,26 @@ async function createDefaultGCSClient() {
172197
return new Storage();
173198
}
174199

200+
export function createStaticFallbackProvider({
201+
flags,
202+
}: StaticFallbackProviderOptions): FlagsFallbackProvider {
203+
return {
204+
async load() {
205+
return {
206+
version: 1,
207+
savedAt: new Date().toISOString(),
208+
flags: Object.entries(flags).map(([key, isEnabled]) =>
209+
staticFlagApiResponse(key, isEnabled),
210+
),
211+
};
212+
},
213+
214+
async save() {
215+
// no-op
216+
},
217+
};
218+
}
219+
175220
export function createFileFallbackProvider({
176221
directory,
177222
}: FileFallbackProviderOptions = {}): FlagsFallbackProvider {

packages/node-sdk/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
createGCSFallbackProvider,
44
createRedisFallbackProvider,
55
createS3FallbackProvider,
6+
createStaticFallbackProvider,
67
} from "./flagsFallbackProvider";
78

89
export { BoundReflagClient, ReflagClient } from "./client";
@@ -12,9 +13,11 @@ export type {
1213
GCSFallbackProviderOptions,
1314
RedisFallbackProviderOptions,
1415
S3FallbackProviderOptions,
16+
StaticFallbackProviderOptions,
1517
} from "./flagsFallbackProvider";
1618

1719
export const fallbackProviders = {
20+
static: createStaticFallbackProvider,
1821
file: createFileFallbackProvider,
1922
redis: createRedisFallbackProvider,
2023
s3: createS3FallbackProvider,

packages/node-sdk/test/flagsFallbackProvider.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,52 @@ describe("flagsFallbackProvider", () => {
3636
}
3737
});
3838

39+
it("loads static snapshots from a flag map", async () => {
40+
const provider = fallbackProviders.static({
41+
flags: {
42+
"flag-1": true,
43+
"flag-2": false,
44+
},
45+
});
46+
47+
await expect(provider.load(context)).resolves.toEqual({
48+
version: 1,
49+
savedAt: expect.any(String),
50+
flags: [
51+
{
52+
key: "flag-1",
53+
description: null,
54+
targeting: {
55+
version: 1,
56+
rules: [
57+
{
58+
filter: {
59+
type: "constant",
60+
value: true,
61+
},
62+
},
63+
],
64+
},
65+
},
66+
{
67+
key: "flag-2",
68+
description: null,
69+
targeting: {
70+
version: 1,
71+
rules: [
72+
{
73+
filter: {
74+
type: "constant",
75+
value: false,
76+
},
77+
},
78+
],
79+
},
80+
},
81+
],
82+
});
83+
});
84+
3985
it("loads undefined when a file snapshot does not exist", async () => {
4086
tempDir = await mkdtemp(path.join(os.tmpdir(), "reflag-node-sdk-"));
4187
const provider = fallbackProviders.file({ directory: tempDir });

0 commit comments

Comments
 (0)