Skip to content

Commit 7b4417b

Browse files
committed
allow bootstrapping feature definitions
1 parent cd9c6c1 commit 7b4417b

File tree

4 files changed

+69
-32
lines changed

4 files changed

+69
-32
lines changed

packages/node-sdk/README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ const client = new BucketClient({
146146
});
147147
```
148148

149-
### Caching
149+
### Feature definitions
150150

151-
Feature definitions are automatically cached and refreshed in the background. The cache behavior is configurable:
151+
Feature definitions include the rules needed to determine which features should be enabled and which config values should be applied to any given user/company. Feature definitions are automatically fetched, cached and refreshed in the background. The cache behavior is configurable:
152152

153153
```typescript
154154
const client = new BucketClient({
@@ -157,6 +157,23 @@ const client = new BucketClient({
157157
});
158158
```
159159

160+
It's also possible to load the feature definitions from a local file or similar:
161+
162+
```typescript
163+
import fs from "fs";
164+
165+
const client = new BucketClient({
166+
fetchFeatures: false,
167+
});
168+
169+
const featureDefs = fs.readFileSync("featureDefs.json");
170+
171+
client.bootstrapFeatureDefinitions(featureDefs);
172+
client.initialize().then(() => {
173+
console.log("Bootstrapped feature definitions");
174+
});
175+
```
176+
160177
## Error Handling
161178

162179
The SDK is designed to fail gracefully and never throw exceptions to the caller. Instead, it logs errors and provides

packages/node-sdk/src/cache.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,10 @@ export default function cache<T>(
6767
return get();
6868
};
6969

70-
return { get, refresh };
70+
const set = (value: T) => {
71+
cachedValue = value;
72+
lastUpdate = Date.now();
73+
};
74+
75+
return { get, refresh, set };
7176
}

packages/node-sdk/src/client.ts

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,18 @@ export class BucketClient {
111111
staleWarningInterval: number;
112112
headers: Record<string, string>;
113113
fallbackFeatures?: Record<keyof TypedFeatures, RawFeature>;
114-
featuresCache?: Cache<FeaturesAPIResponse>;
114+
featuresCache: Cache<FeaturesAPIResponse>;
115115
batchBuffer: BatchBuffer<BulkEvent>;
116116
featureOverrides: FeatureOverridesFn;
117117
rateLimiter: ReturnType<typeof newRateLimiter>;
118118
offline: boolean;
119+
fetchFeatures: boolean;
119120
configFile?: string;
120121
};
121122

122123
private _initialize = once(async () => {
123-
if (!this._config.offline) {
124-
await this.getFeaturesCache().refresh();
124+
if (!this._config.offline && this._config.fetchFeatures) {
125+
await this._config.featuresCache.refresh();
125126
}
126127
this._config.logger?.info("Bucket initialized");
127128
});
@@ -256,6 +257,7 @@ export class BucketClient {
256257
rateLimiter: newRateLimiter(FEATURE_EVENT_RATE_LIMITER_WINDOW_SIZE_MS),
257258
httpClient: options.httpClient || fetchClient,
258259
refetchInterval: FEATURES_REFETCH_MS,
260+
fetchFeatures: options.fetchFeatures ?? true,
259261
staleWarningInterval: FEATURES_REFETCH_MS * 5,
260262
fallbackFeatures: fallbackFeatures,
261263
batchBuffer: new BatchBuffer<BulkEvent>({
@@ -276,6 +278,21 @@ export class BucketClient {
276278
if (!new URL(this._config.apiBaseUrl).pathname.endsWith("/")) {
277279
this._config.apiBaseUrl += "/";
278280
}
281+
282+
this._config.featuresCache = cache<FeaturesAPIResponse>(
283+
this._config.refetchInterval,
284+
this._config.staleWarningInterval,
285+
this._config.logger,
286+
async () => {
287+
const res = await this.get<FeaturesAPIResponse>("features");
288+
289+
if (!isObject(res) || !Array.isArray(res?.features)) {
290+
return undefined;
291+
}
292+
293+
return res;
294+
},
295+
);
279296
}
280297

281298
/**
@@ -454,6 +471,18 @@ export class BucketClient {
454471
});
455472
}
456473

474+
/**
475+
* Updates the feature definitions cache.
476+
*
477+
* @param features - The features to cache.
478+
*
479+
* @remarks
480+
* Useful when loading feature definitions from a file or other source.
481+
**/
482+
public bootstrapFeatureDefinitions(features: FeaturesAPIResponse) {
483+
this._config.featuresCache.set(features);
484+
}
485+
457486
/**
458487
* Initializes the client by caching the features definitions.
459488
*
@@ -854,32 +883,6 @@ export class BucketClient {
854883
}
855884
}
856885

857-
/**
858-
* Gets the features cache.
859-
*
860-
* @returns The features cache.
861-
**/
862-
private getFeaturesCache() {
863-
if (!this._config.featuresCache) {
864-
this._config.featuresCache = cache<FeaturesAPIResponse>(
865-
this._config.refetchInterval,
866-
this._config.staleWarningInterval,
867-
this._config.logger,
868-
async () => {
869-
const res = await this.get<FeaturesAPIResponse>("features");
870-
871-
if (!isObject(res) || !Array.isArray(res?.features)) {
872-
return undefined;
873-
}
874-
875-
return res;
876-
},
877-
);
878-
}
879-
880-
return this._config.featuresCache;
881-
}
882-
883886
/**
884887
* Warns if any features have targeting rules that require context fields that are missing.
885888
*

packages/node-sdk/src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,11 @@ export type Cache<T> = {
499499
* @returns The value or `undefined` if the value is not available.
500500
**/
501501
refresh: () => Promise<T | undefined>;
502+
503+
/**
504+
* Set the value
505+
**/
506+
set: (value: T) => void;
502507
};
503508

504509
/**
@@ -610,6 +615,13 @@ export type ClientOptions = {
610615
*/
611616
offline?: boolean;
612617

618+
/**
619+
* If set to false, feature definitions will not be fetched from the API when the client is initialized.
620+
* Useful if you want to manage feature definitions manually through `bootstrapFeatureDefinitions`.
621+
* Defaults to true.
622+
*/
623+
fetchFeatures?: boolean;
624+
613625
/**
614626
* The path to the config file. If supplied, the config file will be loaded.
615627
* Defaults to `bucket.json` when NODE_ENV is not production. Can also be

0 commit comments

Comments
 (0)