Skip to Content
FeatureAPI Reference

API reference

Not on npm yet

npm install @evanion/feature does not resolve. The package is private: true, so a release run versions and tags it without publishing: npm cannot configure a trusted publisher for a package that does not exist on the registry, and the first version has to go up by hand.

Two entry points. @evanion/feature is the core; @evanion/feature/react is the adapter.

createFeatures(definitions)

function createFeatures<F extends FeatureKey>( definitions: readonly FeatureDefinition<F>[], ): Features<F>;

Validates the dependency graph, deep-clones and freezes the configuration, and returns the store.

Throws FeatureCycleError, UnknownDependencyError or DuplicateFeatureError. All three are configuration errors and all three are raised here rather than at evaluation.

Features<F>

MemberReturnsNotes
keysreadonly F[]every key, in configuration order
configreadonly FeatureDefinition<F>[]the stored intent, deeply frozen
definition(key)FeatureDefinition<F> | undefined
dependants(key)readonly F[]transitive, in dependency order
resolve(context?)Decisions<F>a decision per feature; writes nothing
isEnabled(key, context?)booleanfalse for an unknown key
plan(context?)Plan<F>build-time partition
toggle(key, enabled, context?)ToggleResult<F>the only writer; reports willDisable

resolve, plan and toggle all default context.now to new Date() at the call.

Core exports

import { createFeatures, bucketOf, inRollout, murmur3, evaluateCondition, DEFAULT_ROLLOUT_FIELD, FeatureConfigError, FeatureCycleError, UnknownDependencyError, DuplicateFeatureError, } from '@evanion/feature';
ExportWhat it is
bucketOf(value, seed)the bucket in [0, 1), for snapshot tooling and tests
inRollout(value, percent, seed)whether that bucket is inside the rollout
murmur3(input, seed?)the hash itself, unsigned 32-bit
evaluateCondition(cond, context)one condition, for the same reason
DEFAULT_ROLLOUT_FIELD'targetingKey'

Types

FeatureDefinition<F>

interface FeatureDefinition<F extends FeatureKey = string> { key: F; enabled: boolean; dependsOn?: readonly F[]; rules?: readonly Rule[]; seed?: string; freezeTimeAtBuild?: boolean; }

Rule and RolloutSpec

interface Rule { id?: string; // defaults to '#0', '#1', … by index when?: readonly Condition[]; rollout?: RolloutSpec; } interface RolloutSpec { percent: number; by?: string; // defaults to 'targetingKey' seed?: string; // defaults to the feature's seed, then the key }

Condition

type Condition = WindowCondition | DayOfWeekCondition | AttributeCondition; interface WindowCondition { field: 'now'; op: 'before' | 'after'; value: Instant; // ISO string, epoch ms, or Date } interface DayOfWeekCondition { field: 'now'; op: 'day-of-week'; zone: string; // IANA; required value: readonly Weekday[]; } interface AttributeCondition { field: string; op: 'eq' | 'ne' | 'in' | 'not-in' | 'contains'; value: unknown; }

Weekday is 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'.

EvaluationContext

interface EvaluationContext { now?: Date; targetingKey?: string; [field: string]: unknown; }

Decision<F> and Decisions<F>

interface Decision<F extends FeatureKey = string> { key: F; enabled: boolean; reason: Reason; rule?: string; rules?: readonly RuleOutcome[]; blockedBy?: F; cause?: Cause<F>; } type Decisions<F extends FeatureKey = string> = Record<F, Decision<F>>;

Reason is 'default-on' | 'rule-match' | 'explicitly-off' | 'no-rule-matched' | 'dependency-off'.

RuleOutcome

interface RuleOutcome { rule: string; matched: boolean; failed?: Condition; // the first condition that failed rollout?: { percent: number; by: string; bucket?: number; // absent when the context carried no bucketing value member: boolean; }; }

Cause<F>

interface Cause<F extends FeatureKey = string> { key: F; reason: Reason; rule?: string; }

PlanEntry<F> and Plan<F>

interface PlanEntry<F extends FeatureKey = string> { key: F; resolved: boolean | 'deferred'; needs: readonly string[]; decision?: Decision<F>; } type Plan<F extends FeatureKey = string> = Record<F, PlanEntry<F>>;

ToggleResult<F>

type ToggleResult<F extends FeatureKey = string> = | { ok: true; key: F; enabled: boolean; willDisable: readonly F[] } | { ok: false; key: F; error: 'unknown-feature' };

FeatureKey and Instant

FeatureKey is string | number rather than string, so a consumer can key features on a numeric enum and still get exhaustiveness from Decisions<F>.

Instant is string | number | Date: a string is parsed as ISO 8601, a number as epoch milliseconds.

Errors

ClassExtendsCarries
FeatureConfigErrorError
FeatureCycleErrorFeatureConfigErrorpath, the closed walk
UnknownDependencyErrorFeatureConfigErrorkey, dependency
DuplicateFeatureErrorFeatureConfigErrorkey

All of them are raised by createFeatures, so a store a caller holds cannot fail mid-evaluation.

React exports

import { FeatureProvider, useFeatures, useFeature, useFeatureEnabled, } from '@evanion/feature/react';
ExportSignature
FeatureProvider{ features, context?, decisions?, children? }
useFeatures<F>()Decisions<F>
useFeature<F>(key)Decision<F>; throws for an unconfigured key
useFeatureEnabled<F>(key)boolean

FeatureProviderProps<F> is exported as a type. See React for the memoisation rule on context.

Last updated on