feature-manager-wrapper
v3.3.3
Published
Abstraction layer for configuration / feature management systems and services
Downloads
37
Maintainers
Readme
feature-manager-wrapper
A wrapper / abstraction around configuration / feature manager libraries.
Use cases:
- Shift to using
LaunchDarkly
fromprocess.env
(or vice versa)- Also support for other feature managers such as
Configurity
- Also support for other feature managers such as
- Have a common interface for interacting with feature managers so if you need to swap out the underlying implementation, minimal changes would be required(*).
- (*) Feature managers use
context
data differently (or not at all) for custom features and may require adjustments.
- (*) Feature managers use
// See examples in documentation for working examples
// This example is for illustrative purposes only
import { AsyncFeatureManager, LaunchDarklyServerDriver } from 'feature-manager-wrapper';
// By default, use process.env to maintain our feature flags
let driver = new EnvironmentDriver();
if (process.env.USE_LAUNCH_DARKLY) {
// Use LaunchDarkly to maintain our feature flags
driver = new LaunchDarklyServerDriver(...);
}
// Note: EnvironmentDriver supports both sync and async operations, but LaunchDarklyServerDriver only supports async operations, so you'll want to only use non-async methods.
const featureManager = new AsyncFeatureManager(driver);
// Get a feature flag
const myFeatureValue = await featureManager.getValue('featureFlag', {
// optional context to pass to LaunchDarkly
context: {},
// optional default value to return if the feature flag is not found
defaultValue: true,
})
Table of contents
- Installation
- Getting started
- API
Installation
$ npm install feature-manager-wrapper
Getting started
(For Typescript users) Map out your feature flags
Create an interface that maps out the available feature flags. They will be used for IDE autocompletion when working with the feature manager.
interface FeatureFlags {
featureFlag: boolean;
anotherFeatureFlag: string;
featureFlaggedObject: {
featureFlaggedProperty: number;
};
}
Select the feature manager service driver to use
feature-management-wrapper
currently supports the following feature managers:
LaunchDarkly
(server):LaunchDarklyServerDriver
LaunchDarkly
(client):LaunchDarklyClientDriver
LaunchDarkly
(Electron client):LaunchDarklyElectronClientDriver
process.env
:EnvironmentDriver
configurity
:ConfigurityDriver
- key / value (where you have a KV mapping from an external source):
SimpleKeyValueDriver
Create a feature manager
Select the feature manager driver to use
Determine if the feature manager you use is async or sync-based:
- If the APIs you call require
await
to get your feature flags, then it would be async-based. - If not, then sync-based.
Drivers and their (a)sync type:
LaunchDarklyServerDriver
: asyncLaunchDarklyClientDriver
: sync (+ async supported)LaunchDarklyElectronClientDriver
: sync (+async supported)EnvironmentDriver
: sync (+ async supported)ConfigurityDriver
: sync (+ async supported)SimpleKeyValueDriver
: sync (+ async supported)
Create an instance of the feature manager
A feature manager uses one of the above drivers to interact with the specific feature manager service.
- async (+ sync) drivers:
AsyncFeatureManager
- Only exposes async operations to fetch feature flags from remote endpoints
- Can use both sync and async drivers (since sync drivers wrap sync operations in a promise for async operations), but operations are limited to async
- sync drivers:
SyncFeatureManager
- Exposes both sync and async operations to fetch feature flags locally
You can switch from SyncFeatureManager
to AsyncFeatureManager
if you need to use an async driver, but
not the other way around as conversion to async methods would be necessary.
Example: LaunchDarkly (server-side node SDK)
The LaunchDarkly Server SDK is async-based since we call remote endpoints to fetch feature flags.
Because it's async-based, we use the AsyncFeatureManager
to create our own feature manager.
import { AsyncFeatureManager, LaunchDarklyServerDriver } from 'feature-manager-wrapper';
import { LDClient, LDContext } from "@launchdarkly/node-server-sdk";
interface FeatureFlags {
featureFlag: boolean;
anotherFeatureFlag: string;
featureFlaggedObject: {
featureFlaggedProperty: number;
};
}
// Create your LaunchDarkly client
const launchDarklyClient = LDClient.initialize('sdk-key')
const driver = new LaunchDarklyServerDriver<FeatureFlags, LDContext>(launchDarklyClient, defaultContext)
const featureManager = new AsyncFeatureManager<FeatureFlags, LDContext>(driver);
// Get a feature flag
const myFeatureValue = await featureManager.getValue('featureFlag')
// Get a feature flag with context
const myFeatureValueFromContext = await featureManager.getValue('featureFlag', {
context: {
'kind': 'user',
'key': 'user-key-123abc',
'name': 'Sandy'
},
// optional default value
defaultValue: true,
})
// Close the connection to the LaunchDarkly service
await featureManager.close()
Example: LaunchDarkly (client-side JS SDK)
The client javascript SDK for LaunchDarkly is sync-based since the flags are all fetched on client initialization.
It does not have the ability to use context data for fetching flags.
// Can also use AsyncFeatureManager
import { SyncFeatureManager, LaunchDarklyClientDriver } from 'feature-manager-wrapper';
import * as LDClient from 'launchdarkly-js-client-sdk';
interface FeatureFlags {
featureFlag: boolean;
anotherFeatureFlag: string;
featureFlaggedObject: {
featureFlaggedProperty: number;
};
}
const context = {
kind: 'user',
key: 'context-key-123abc'
};
// Create your LaunchDarkly client
const launchDarklyClient = LDClient.initialize('client-side-id-123abc', context);
const driver = new LaunchDarklyClientDriver<FeatureFlags>(launchDarklyClient)
const featureManager = new SyncFeatureManager<FeatureFlags>(driver);
// Get a feature flag
const myFeatureValue = featureManager.getValueSync('featureFlag')
// Close the connection to the LaunchDarkly service
await featureManager.close()
Example: LaunchDarkly (client-side Electron JS SDK)
The electron client javascript SDK for LaunchDarkly is sync-based since the flags are all fetched on client initialization.
It does not have the ability to use context data for fetching flags.
// Can also use AsyncFeatureManager
import { SyncFeatureManager, LaunchDarklyClientDriver } from 'feature-manager-wrapper';
import * as LDElectron from 'launchdarkly-electron-client-sdk';
interface FeatureFlags {
featureFlag: boolean;
anotherFeatureFlag: string;
featureFlaggedObject: {
featureFlaggedProperty: number;
};
}
const context = {
kind: 'user',
key: 'context-key-123abc'
};
// Create your LaunchDarkly client
const launchDarklyClient = LDElectron.initializeInMain('client-side-id-123abc', context);
const driver = new LaunchDarklyElectronClientDriver<FeatureFlags>(launchDarklyClient)
const featureManager = new SyncFeatureManager<FeatureFlags>(driver);
// Get a feature flag
const myFeatureValue = featureManager.getValueSync('featureFlag')
// Close the connection to the LaunchDarkly service
await featureManager.close()
Example: process.env
process.env
is sync-based since we access the environment variables synchronously.
Because it's sync-based, we use the SyncFeatureManager
to create our own feature manager.
It does not have the ability to use context data for fetching flags.
// Can also use AsyncFeatureManager
import { SyncFeatureManager, EnvironmentDriver } from 'feature-manager-wrapper';
// maps to process.env variables
interface FeatureFlags {
FEATURE_FLAG: boolean;
ANOTHER_FEATURE_FLAG: string;
FEATURE_FLAGGED_OBJECT: {
featureFlaggedProperty: number;
};
}
const driver = new EnvironmentDriver<FeatureFlags>()
const featureManager = new SyncFeatureManager<FeatureFlags>(driver);
// Get a feature flag
const myFeatureValue = await featureManager.getValue('FEATURE_FLAG')
// sync version
const myFeatureValueSync = featureManager.getValueSync('FEATURE_FLAG', {
// optional default value
defaultValue: true
})
Example: Key / Value
Key / Value is sync-based since we access the key / value mapping synchronously.
Because it's sync-based, we use the SyncFeatureManager
to create our own feature manager.
- It does not have the ability to use context data for fetching flags.
- It does have the ability to set flags from the driver via
setValueSync
andsetValuesSync
methods.
// Can also use AsyncFeatureManager
import { SyncFeatureManager, SimpleKeyValueDriver } from 'feature-manager-wrapper';
interface FeatureFlags {
featureFlag: boolean;
anotherFeatureFlag: string;
featureFlaggedObject: {
featureFlaggedProperty: number;
};
}
const featureFlags: FeatureFlags = {
featureFlag: true,
anotherFeatureFlag: 'hello',
featureFlaggedObject: {
featureFlaggedProperty: 123
}
}
const driver = new SimpleKeyValueDriver<FeatureFlags>(featureFlags)
const featureManager = new SyncFeatureManager<FeatureFlags>(driver);
// Get a feature flag
const myFeatureValue = await featureManager.getValue('featureFlag')
const myFeatureValueSync = featureManager.getValueSync('featureFlag')
// Specific to this driver: set a feature flag(s)
// const driver = manager.getDriver() as SimpleKeyValueDriver<Flags>
driver.setValueSync('featureFlag', false)
driver.setValuesSync({
featureFlag: false,
anotherFeatureFlag: 'goodbye',
featureFlaggedObject: {
featureFlaggedProperty: 456
}
})
Example: Configurity
Configurity
is sync-based since we access the config synchronously.
Because it's sync-based, we use the SyncFeatureManager
to create our own feature manager.
// Can also use AsyncFeatureManager
import { SyncFeatureManager, ConfigurityDriver } from 'feature-manager-wrapper';
import { loadConfigParser } from 'configurity'
interface FeatureFlags {
featureFlag: boolean;
anotherFeatureFlag: string;
featureFlaggedObject: {
featureFlaggedProperty: number;
};
}
// Your custom context definition to access custom feature flags
interface ConfigurityContext {
environment?: string
}
// Load the config file
const YML_PATH = path.join(__dirname, '..', '__fixtures__', 'configurity.yml')
// Get the configurity config parser
const configParser = loadConfigParser<FeatureFlags>(YML_PATH)
const driver = new ConfigurityDriver<FeatureFlags, ConfigurityContext>(configParser)
const featureManager = new SyncFeatureManager<FeatureFlags, ConfigurityContext>();
// Get a feature flag
const myFeatureValue = await featureManager.getValue('featureFlag')
const myFeatureValueSync = featureManager.getValueSync('featureFlag', {
// optional context (see Configurity documentation)
context: {
environment: 'production'
},
// optional default value
defaultValue: true
})
Feature Managers with async initialization
You may want to extend a feature manager to support extended functionality, and sometimes you need to initialize the feature manager asynchronously.
You can do this using a DummyDriver
:
import { SyncFeatureManager, DummyDriver } from 'feature-manager-wrapper';
class MyFeatureManager extends SyncFeatureManager {
constructor() {
// We have to pass a driver to the constructor, so we pass a dummy driver
super(new DummyDriver())
}
async init() {
// Do some async init stuff
// Assign the actual driver here
this.setDriver(new MyActualDriver())
}
}
API
Interface: 'CommonValueParams'
Most of the API methods in the feature manager has an optional params
parameter that can be passed to the method.
/**
* Common optional parameters for retrieving a flag.
*/
type CommonValueParams<Flags, K extends keyof Flags> = {
/**
* The default value to use if the flag is not found.
*/
defaultValue?: Flags[K]
/**
* The context to use when retrieving the flag.
*/
context?: any
}
Class: AsyncFeatureManager
Use this class to ensure that your feature API calls are only async-based. This is useful if you want to ensure that your codebase is consistent with async operations.
If you are switching to a driver that uses sync operations, you will need to update your feature manager to use the SyncFeatureManager
class instead.
/**
* Feature manager that supports async and sync drivers.
* Acts as a facade for the underlying driver, and only exposes async operations.
*/
class AsyncFeatureManager<
Flags extends Record<string, any>,
Context extends Record<string, any> = Record<string, any>
Generic types:
Flags
(optional): the interface that maps out the available feature flagsContext
(optional): the interface that maps out the context data that can be passed when fetching feature flags. Must be supported by the underlying driver.
constructor()
/**
* @param driver The driver to use for interacting with the feature manager service.
*/
constructor(driver: AsyncFeatureManagerDriver<Flags, Context>)
assertGetValue()
/**
* Asynchronously asserts and retrieves the value of a feature flag based on its key.
*
* - Throws an error if the value is null, undefined, or empty string.
* - Attempts to convert the value based on its probable type (number, boolean, string, object).
*
* Examples:
*
* - null => null
* - undefined => undefined
* - "true" => true
* - "123" => 123
* - "{ "foo": "bar" }" => { foo: "bar" }
*
* @param key The key of the feature flag.
* @param params Optional parameters including default value and context.
* @throws FeatureManagerAssertionError if the value is null, undefined, or empty string.
* @returns A Promise resolving to the value of the flag.
*/
assertGetValue<K extends string & keyof Flags>(
key: K,
params?: CommonValueParams<Flags, K>
): Promise<Flags[K]>
getValue()
/**
* Asynchronously retrieves the value of a feature flag based on its key.
*
* - Returns null if the value is null or undefined.
* - Attempts to convert the value based on its probable type (number, boolean, string, object).
*
* Examples:
*
* - null => null
* - undefined => undefined
* - "true" => true
* - "123" => 123
* - "{ "foo": "bar" }" => { foo: "bar" }
*
* @param key The key of the feature flag.
* @param params Optional parameters including default value and context.
* @returns A Promise resolving to the value of the flag, or null if not found.
*/
getValue<K extends string & keyof Flags>(
key: K,
params?: CommonValueParams<Flags, K>
): Promise<Flags[K]>
assertGetRawValue()
/**
* Asynchronously asserts and retrieves the raw value of a feature flag (no conversions applied) based on its key.
* Throws an error if the value is null, undefined, or empty string.
*
* @param key The key of the feature flag.
* @param params Optional parameters including default value and context.
* @throws FeatureManagerAssertionError if the value is null, undefined, or empty string.
* @returns A Promise resolving to the raw value of the flag.
*/
assertGetRawValue<K extends string & keyof Flags>(
key: K,
params?: CommonValueParams<Flags, K>
): Promise<Flags[K]>
getRawValue()
/**
* Asynchronously retrieves the raw value of a feature flag (no conversions applied) based on its key.
*
* @param key The key of the feature flag.
* @param params Optional parameters including default value and context.
* @returns A Promise resolving to the raw value of the flag, or null if not found.
*/
getRawValue<K extends string & keyof Flags>(
key: K,
params?: CommonValueParams<Flags, K>
): Promise<Flags[K] | null>
getAllValues()
/**
* Asynchronously retrieves all feature flag values.
* - Attempts to convert the value based on its probable type (number, boolean, string, object).
*
* Examples:
*
* - null => null
* - undefined => undefined
* - "true" => true
* - "123" => 123
* - "{ "foo": "bar" }" => { foo: "bar" }
*
* @param params Optional parameters including context.
* @returns A Promise resolving to an object with all flag values.
*/
getAllValues(params?: { context?: Context }): Promise<Flags>
getAllRawValues()
/**
* Asynchronously retrieves all feature flag raw values (no conversions applied).
*
* @param params Optional parameters including context.
* @returns A Promise resolving to an object with all flag raw values.
*/
getAllRawValues(params?: { context?: Context }): Promise<Flags>
close()
/**
* Asynchronously closes the connection to the feature manager service.
*
* @returns A Promise that resolves when the connection is closed.
*/
close(): Promise<void>
Class: SyncFeatureManager
Also includes the methods in AsyncFeatureManager
.
/**
* Feature manager that only supports sync drivers. Exposes both sync and async operations since async operations are just sync operations wrapped in a promise.
* Acts as a facade for the underlying driver.
*/
class SyncFeatureManager<
Flags extends Record<string, any>,
Context,
>
Generic types:
Flags
(optional): the interface that maps out the available feature flagsContext
(optional): the interface that maps out the context data that can be passed when fetching feature flags. Must be supported by the underlying driver.
constructor()
/**
* @param driver The driver to use for interacting with the feature manager service.
*/
constructor(driver: SyncFeatureManagerDriver<Flags, Context>)
assertGetValueSync()
/**
* Synchronously asserts and retrieves the value of a feature flag based on its key.
*
* - Throws an error if the value is null, undefined, or empty string.
* - Attempts to convert the value based on its probable type (number, boolean, string, object).
*
* Examples:
*
* - null => null
* - undefined => undefined
* - "true" => true
* - "123" => 123
* - "{ "foo": "bar" }" => { foo: "bar" }
*
* @param key The key of the feature flag.
* @param params Optional parameters including default value and context.
* @throws FeatureManagerAssertionError if the value is null, undefined, or empty string.
* @returns The value of the flag.
*/
assertGetValueSync<K extends string & keyof Flags>(
key: K,
params?: CommonValueParams<Flags, K>
): Flags[K]
getValueSync()
/**
* Synchronously retrieves the value of a feature flag based on its key.
*
* - Returns null if the value is null or undefined.
* - Attempts to convert the value based on its probable type (number, boolean, string, object).
*
* Examples:
*
* - null => null
* - undefined => undefined
* - "true" => true
* - "123" => 123
* - "{ "foo": "bar" }" => { foo: "bar" }
*
* @param key The key of the feature flag.
* @param params Optional parameters including default value and context.
* @returns The value of the flag, or null if not found.
*/
getValueSync<K extends string & keyof Flags>(
key: K,
params?: CommonValueParams<Flags, K>
): Flags[K] | null
assertGetRawValueSync()
/**
* Synchronously asserts and retrieves the raw value of a feature flag (no conversions applied) based on its key.
*
* Throws an error if the value is null, undefined, or empty string.
*
* @param key The key of the feature flag.
* @param params Optional parameters including default value and context.
* @throws FeatureManagerAssertionError if the value is null, undefined, or empty string.
* @returns The raw value of the flag.
*/
assertGetRawValueSync<K extends string & keyof Flags>(
key: K,
params?: CommonValueParams<Flags, K>
): Flags[K]
getRawValueSync()
/**
* Synchronously retrieves the raw value of a feature flag (no conversions applied) based on its key.
*
* @param key The key of the feature flag.
* @param params Optional parameters including default value and context.
* @returns The raw value of the flag, or null if not found.
*/
getRawValueSync<K extends string & keyof Flags>(
key: K,
params?: CommonValueParams<Flags, K>
): Flags[K] | null
getAllValuesSync()
/**
* Synchronously retrieves all feature flag values.
*
* - Returns null if the value is null or undefined.
* - Attempts to convert the value based on its probable type (number, boolean, string, object).
*
* Examples:
*
* - null => null
* - undefined => undefined
* - "true" => true
* - "123" => 123
* - "{ "foo": "bar" }" => { foo: "bar" }
*
* @param params Optional parameters including context.
* @returns An object with all flag values.
*/
getAllValuesSync(params?: { context?: Context }): Flags
getAllRawValuesSync()
/**
* Synchronously retrieves all feature flag raw values (no conversions applied).
*
* @param params Optional parameters including context.
* @returns An object with all flag raw values.
*/
getAllRawValuesSync(params?: { context?: Context }): Flags
closeSync()
/**
* Closes the connection to the config manager.
* @returns A Promise that resolves when the connection is closed.
*/
closeSync(): void