Decisions
@evanion/feature 0.1.0 and its patch releases up to 0.1.1, the version on npm. Values on this page were produced by running the source at e53b2d8, a later commit than the 0.1.0 tag, and are not re-executed.
resolve() answers every feature for one context, and each answer is a decision that says why the feature is on or off. toggle() is the one call that changes what the next decision reads.
Resolving Features
To check if a feature is active, you use the resolution methods provided by @evanion/feature. Depending on whether you need a single answer or a full audit, you have two primary ways to check a feature’s state:
features.isEnabled(key, context): Returns a simple boolean. It returnsfalsefor a key the store was not built with.features.resolve(context): Returns a complete record of every feature in the store and its current decision.
Both are pure functions of the configuration and the context, and both write nothing. context is optional, and a context with no now field reads the clock at the call.
Because isEnabled() evaluates the whole dependency graph on every call, a request that reads several flags should call resolve() once and keep the record.
Example: Resolving a Request
To see how this works in practice, consider a store containing the new checkout and express pickup from Getting Started, utilizing the after-launch rule and its 25% rollout:
const = .({ : 'cust-0101', });
['new-checkout']; // -> { key: 'new-checkout', enabled: true, reason: 'rule-match', rule: 'after-launch' }
['express-pickup']; // -> { key: 'express-pickup', enabled: true, reason: 'default-on' }Anatomy of a Decision
When you use resolve(), @evanion/feature returns a Decision object that provides a detailed audit trail for every feature. While enabled is the only field your application needs, the other fields are designed for operators.
| Field | Meaning |
|---|---|
enabled | The final answer (true/false). |
reason | Why the feature resolved this way (e.g., rule-match, dependency-off). |
rule | The specific rule that matched (used when reason is rule-match). |
rules | One outcome per rule (used when reason is no-rule-matched). |
blockedBy | The immediate parent that forced this feature off. |
cause | The first ancestor that is off for a reason of its own. |
variant | The assigned variant (if the feature declares variants and resolved on). |
value | The value associated with the assigned variant. |
assignment | Details on how the variant was assigned (e.g., weighted, pinned). |
Every field except enabled is output only. Nothing in the library reads them back to decide anything.
Understanding the Reason
To determine exactly which part of the logic decided the outcome, you inspect the reason field:
reason | Meaning |
|---|---|
default-on | Enabled because it has no rules. |
rule-match | Enabled because a specific rule matched. |
explicitly-off | Disabled because enabled was set to false. |
no-rule-matched | enabled is true and rules exist, but none matched; carries a breakdown. |
dependency-off | Disabled because a parent dependency is off. |
Reading the Dependency Cascade
When a parent feature is disabled, @evanion/feature cascades that state to all dependants. In the example below, the new checkout is turned off in the chain Getting Started toggles, and the decisions for both dependants report reason: 'dependency-off'.
.('new-checkout', false);
const = .();
['express-pickup']; // -> { key: 'express-pickup', enabled: false, reason: 'dependency-off', blockedBy: 'new-checkout', cause: { key: 'new-checkout', reason: 'explicitly-off' } }
['demo-night-booking']; // -> { key: 'demo-night-booking', enabled: false, reason: 'dependency-off', blockedBy: 'express-pickup', cause: { key: 'new-checkout', reason: 'explicitly-off' } }You can use this cascade to trace failures without manually walking the dependency graph:
blockedByidentifies the immediate edge (e.g., demo night booking is blocked by express pickup).causewalks the chain to the root failure (e.g., demo night booking is ultimately off because the new checkout isexplicitly-off).
An operator reading demo night booking’s cause finds the new checkout immediately.
Why a Rule Did Not Match
To debug why a feature is disabled despite being generally enabled, you can examine the no-rule-matched reason. When a feature resolves to false for this reason, the @evanion/feature decision includes a detailed breakdown of every rule and why it failed.
import { } from '@evanion/feature';
const = ([
{
: 'new-checkout',
: true,
: [
{ : [{ : 'role', : 'eq', : 'bookseller' }] },
{ : 'a-share', : { : 10 } },
],
},
]);
const = { : 'customer', : 'cust-0042' };
.()['new-checkout']; // -> { key: 'new-checkout', enabled: false, reason: 'no-rule-matched', rules: [{ rule: 'rule-d69c7f2b', matched: false, failed: { field: 'role', op: 'eq', value: 'bookseller' } }, { rule: 'a-share', matched: false, rollout: { percent: 10, by: 'targetingKey', member: false } }] }If a condition failed, the failed object names the first condition that did not hold: its field, operator, and value. If a rollout left the customer out, the rollout object gives the percentage, the field it bucketed on, and member: false. Note that since the first rule above declares no id, its outcome carries the hash-derived name rule-d69c7f2b.
Managing State with toggle
To modify the stored intent of a feature in @evanion/feature, you use the toggle() call. This is the only way to change state; it sets the enabled property, applies the change at once, and returns a report of which dependants went off with it.
The store below demonstrates this using the same three-flag chain before any toggle is applied:
.('express-pickup', false); // -> { ok: true, key: 'express-pickup', enabled: false, willDisable: ['demo-night-booking'] }
.('express-pickup', true); // -> { ok: true, key: 'express-pickup', enabled: true, willDisable: [] }
// @ts-expect-error -- the store's keys are checked at compile time too
.('express-delivery', false); // -> { ok: false, key: 'express-delivery', error: 'unknown-feature' }The willDisable property holds only the dependants that resolved on before the write and off after it. Both sides are evaluated against one context—provided as the optional third argument of toggle(key, enabled, context)—and with none, both read an empty context at the current time.
Because willDisable is empty when enabling, a UI can use this to report the full effect of a change: “Turning off Express Pickup also disabled Demo Night Booking.” A key the store was not built with returns ok: false and never throws.
Inspecting the Store
To audit the configuration of an @evanion/feature store, you can use these inspection properties:
features.keys: Every configured key, in configuration order.features.config: The deeply frozen configuration array.features.definition(key): The specific definition for one feature, orundefined.features.dependants(key): Every feature that depends on the given key, transitively, in dependency order.
Using the same three-flag chain:
.; // -> ['new-checkout', 'express-pickup', 'demo-night-booking']
.('new-checkout'); // -> ['express-pickup', 'demo-night-booking']
.('express-pickup'); // -> { key: 'express-pickup', enabled: true, dependsOn: ['new-checkout'] }
.(.); // -> trueTo learn more about the 25% rollout carried by the new checkout’s after-launch rule, see Rollouts.