Skip to content

Core API Reference

Hand-written reference for everything @useoptimus/core exports today (packages/core/src/index.ts). Generating this from TSDoc via TypeDoc is planned — see the docs-site README — but not wired up yet.

interface FlagDefinition<T = boolean> {
key: string;
kind: FlagKind;
valueType: "boolean" | "variant" | "value";
defaultValue: T;
variants?: FlagVariant<T>[];
failureMode: FailureMode;
sticky: boolean;
emitsExposure: boolean;
dependsOn?: string[];
schedule?: { startAt?: string; endAt?: string };
description?: string;
owners?: string[];
}

The code-defined schema half of a flag. See Flag Taxonomy for what each field does.

type FlagKind =
| "release" | "killSwitch" | "experiment" | "migration"
| "progressiveDeploy" | "entitlement" | "circuitBreaker"
| "dynamicConfig" | "custom";
type FailureMode = "closed" | "open" | "lastKnown";
interface FlagVariant<T> {
key: string;
value: T;
weight: number;
}
interface FlagRemoteState {
key: string;
enabled?: boolean;
rolloutPercentage?: number;
bucketingSeed?: string;
targetingRules?: TargetingRule[];
variantOverrides?: { key: string; weight: number }[];
valueOverride?: unknown;
updatedAt: string;
}

The live/remote half of a flag. Any field left unset falls back to the FlagDefinition’s code-defined default — this fallback is the kill-switch fail-safe path.

interface EvaluationContext {
bucketingKey?: string;
userId?: string;
deviceId?: string;
sessionId?: string;
anonymousId?: string;
attributes?: Record<string, string | number | boolean>;
environment?: string;
}

See Bucketing & Salting for how bucketingKey is resolved. Identity aliasing (anonymous → identified re-bucketing) is out of scope for v1 — if the resolved key changes between calls for what is conceptually “the same user” (e.g. pre- and post-login), bucket assignment may change.

interface EvaluatedFlag<T = boolean> {
key: string;
value: T;
variantKey?: string;
reason: EvaluationReason;
ruleMatched?: string;
stale: boolean;
}
type EvaluationReason =
| "default" | "targetingMatch" | "rollout"
| "override" | "fallbackError" | "dependencyNotMet";

fallbackError is never produced by the pure evaluate() function itself — it’s returned by FlagsClient.evaluate()/evaluateAll() when a 'closed'-failureMode flag falls back after a provider fetch failure. See FlagsClient below and Flag Taxonomy.

type TargetingRule =
| { type: "attributeEquals"; attribute: string; value: string | number | boolean }
| { type: "attributeIn"; attribute: string; values: (string | number)[] }
| { type: "percentageRollout"; percentage: number; bucketingSeed?: string }
| { type: "semverRange"; attribute: string; range: string }
| { type: "dateRange"; startAt?: string; endAt?: string }
| { type: "and"; rules: TargetingRule[] }
| { type: "or"; rules: TargetingRule[] };
interface FlagProvider {
name: string;
init(): Promise<void>;
getRemoteState(keys?: string[]): Promise<FlagRemoteState[]>;
subscribe?(onUpdate: (state: FlagRemoteState[]) => void): () => void;
}

subscribe is optional — implement it for push-based providers (SSE/WS); polling providers can omit it.

interface ResolvedFlagConfig<T> {
key: string;
defaultValue: T;
failureMode: FailureMode;
sticky: boolean;
emitsExposure: boolean;
dependsOn?: string[];
schedule?: { startAt?: string; endAt?: string };
enabled?: boolean;
rolloutPercentage?: number;
bucketingSeed?: string;
targetingRules: TargetingRule[];
variants?: FlagVariant<T>[];
valueOverride?: unknown;
}

The result of merging a FlagDefinition with its FlagRemoteState — the return type of mergeDefinitionWithRemoteState.

evaluate(definition, remoteState, context, dependencies?)

Section titled “evaluate(definition, remoteState, context, dependencies?)”
function evaluate<T>(
definition: FlagDefinition<T>,
remoteState: FlagRemoteState | undefined,
context: EvaluationContext,
dependencies?: Record<string, EvaluatedFlag>,
): EvaluatedFlag<T>

Pure and deterministic: identical inputs always produce an identical EvaluatedFlag, which is what makes SSR snapshot/hydrate parity possible. dependencies supports only the dependsOn trait’s simple truthy-parent check — a parent flag resolving to a specific variant is not supported.

function resolveBucketingKey(context: EvaluationContext): string | undefined

Applies the default identity resolution chain. See Bucketing & Salting.

computeBucket(bucketingKey, flagKey, seed?)

Section titled “computeBucket(bucketingKey, flagKey, seed?)”
function computeBucket(bucketingKey: string, flagKey: string, seed?: string): number

Returns a bucket value in [0, 10000).

function isInRollout(bucketValue: number, percentage: number): boolean

percentage is 0–100.

function matchRule(rule: TargetingRule, context: EvaluationContext, flagKey: string): boolean

Evaluates a single TargetingRule against a context.

function evaluateRules(
rules: TargetingRule[],
context: EvaluationContext,
flagKey: string,
): { matched: boolean; rule?: TargetingRule }

First-match-wins over the rule list.

mergeDefinitionWithRemoteState(definition, remoteState?)

Section titled “mergeDefinitionWithRemoteState(definition, remoteState?)”
function mergeDefinitionWithRemoteState<T>(
definition: FlagDefinition<T>,
remoteState?: FlagRemoteState,
): ResolvedFlagConfig<T>

Field-by-field merge of code schema and remote state. Used internally by evaluate(); exported for testing merge behavior in isolation.

function fnv1a(input: string): number

FNV-1a 32-bit hash. Not cryptographic — used purely for bucketing distribution. computeBucket runs this through an additional avalanche step before reducing it; see Bucketing & Salting for why.

class LocalProvider implements FlagProvider {
name: "local";
constructor(state: FlagRemoteState[]);
init(): Promise<void>;
getRemoteState(keys?: string[]): Promise<FlagRemoteState[]>;
}

Static/in-memory provider, no network calls. Useful for tests and local development. Does not implement subscribe.

interface HttpPollingProviderOptions {
url: string;
intervalMs?: number; // steady-state poll interval, default 30_000
fetchImpl?: FetchLike;
headers?: Record<string, string>;
now?: () => number; // test seam
random?: () => number; // jitter test seam, default Math.random
}
class HttpPollingProvider implements FlagProvider {
name: "http-polling";
constructor(options: HttpPollingProviderOptions);
}

Polls url on intervalMs, backing off on failure with Equal Jitter exponential backoff (50%–100% of the doubling delay, capped at 5 minutes) so a retry never degenerates to a near-immediate hammering on an unlucky jitter roll.

interface SseProviderOptions {
url: string;
eventSourceFactory?: () => EventSourceLike;
}
class SseProvider implements FlagProvider {
name: "sse";
constructor(options: SseProviderOptions);
}

getRemoteState() returns the last-known pushed state ([] before the first message) rather than performing a network fetch. No self-implemented reconnect — both the browser’s native EventSource and the eventsource npm package already auto-reconnect per the SSE spec.

Orchestration layer around the pure evaluate() engine: owns a flag registry, a FlagProvider, cached remote state, dependsOn resolution across multiple flags, failureMode semantics, and exposure events. Use this instead of calling evaluate() directly for anything beyond a single one-off evaluation.

interface FlagsClientOptions {
definitions: FlagDefinition<unknown>[]; // immutable for the client's lifetime
provider: FlagProvider;
context?: EvaluationContext;
cache?: FlagStateCache; // defaults to an in-memory Map; see TtlFlagStateCache below
onEvaluate?: OnEvaluateHandler;
now?: () => number; // clock seam for tests, default Date.now
}
class FlagsClient {
constructor(options: FlagsClientOptions);
init(): Promise<void>;
refresh(keys?: string[]): Promise<RefreshResult>;
setContext(context: EvaluationContext): void;
getContext(): EvaluationContext;
evaluate<T = boolean>(key: string, context?: EvaluationContext): EvaluatedFlag<T>;
evaluateAll(context?: EvaluationContext): Record<string, EvaluatedFlag>;
onEvaluate(handler: OnEvaluateHandler): Unsubscribe;
subscribe(listener: ClientUpdateListener): Unsubscribe;
setOverrides(overrides: Record<string, FlagOverride>): void;
clearOverrides(keys?: string[]): void;
dispose(): void;
}
type Unsubscribe = () => void;
type OnEvaluateHandler = (evaluated: EvaluatedFlag, definition: FlagDefinition<unknown>) => void;
type ClientUpdateListener = (changedKeys: string[]) => void;
interface FlagOverride {
value: unknown;
variantKey?: string;
}
interface RefreshResult {
succeededKeys: string[];
failedKeys: string[];
errors: Record<string, unknown>;
}
interface FlagStateEntry {
remoteState: FlagRemoteState | undefined; // last state successfully returned for this key
fetchedAt: number | undefined; // epoch ms of last successful fetch; undefined = never fetched
lastError: unknown | undefined; // set if the most recent fetch attempt failed
}
interface FlagStateCache {
get(key: string): FlagStateEntry | undefined;
set(key: string, entry: FlagStateEntry): void;
}

FlagStateCache is the pluggable storage abstraction behind FlagsClientOptions.cacheTtlFlagStateCache (below) is the built-in implementation; a plain Map-backed one is used by default.

evaluate/evaluateAll are synchronous cache reads — they never call the provider, so a broken provider can only degrade individual flags (via failureMode), never throw out of an evaluation call. The one exception is evaluate() on a key that was never registered, which throws synchronously by design (a caller bug, not a runtime condition).

Not thread-safe across concurrent contexts: setContext/getContext mutate shared instance state. Either instantiate one client per request (e.g. per SSR request), or always pass context explicitly to evaluate/evaluateAll and never call setContext on a shared instance.

setOverrides() forces a flag’s resolved value/variantKey regardless of failureMode, dependsOn, targeting, or remote state — reason is always "override", and overridden reads never fire onEvaluate handlers even when emitsExposure is true. See the DevTools adapter, which builds on this.

interface TtlFlagStateCacheOptions {
ttlMs?: number; // default 30_000
staleWhileRevalidateMs?: number; // default 300_000
retriggerCooldownMs?: number; // default = ttlMs
now?: () => number; // clock seam for tests
onStale?: (key: string) => void;
}
class TtlFlagStateCache implements FlagStateCache {
constructor(options?: TtlFlagStateCacheOptions);
get(key: string): FlagStateEntry | undefined;
set(key: string, entry: FlagStateEntry): void;
isStale(key: string): boolean;
getStaleness(key: string): CacheStaleness; // "fresh" | "stale" | "unknown"
setOnStale(handler: ((key: string) => void) | undefined): void;
}
function wireAutoRevalidation(
cache: TtlFlagStateCache,
client: Pick<FlagsClient, "refresh">,
): Unsubscribe;

get()/set() are pure passthroughs on top of the default in-memory cache — TTL age never changes what’s returned or evicts data; the only effect is the onStale side-channel. wireAutoRevalidation wires that side-channel to call client.refresh([...keys]) automatically, batching every key that goes stale in the same tick into one call. It only needs a refresh() method, not a full FlagsClient — pass an instance as FlagsClientOptions.cache.

Eight factory functions, each a pre-filled trait bundle over the same FlagDefinition shape — not a separate evaluation path per “kind”:

interface CommonFlagOptions {
schedule?: { startAt?: string; endAt?: string };
description?: string;
owners?: string[];
dependsOn?: string[];
}
interface DefineBooleanFlagOptions extends CommonFlagOptions {
key: string;
defaultValue: boolean;
}
interface DefineExperimentOptions<T> extends CommonFlagOptions {
key: string;
defaultValue: T; // fallback used by evaluate() when no bucketing key resolves
variants: FlagVariant<T>[];
}
interface DefineDynamicConfigOptions<T> extends CommonFlagOptions {
key: string;
defaultValue: T;
}
function defineReleaseFlag(options: DefineBooleanFlagOptions): FlagDefinition<boolean>;
function defineKillSwitch(options: DefineBooleanFlagOptions): FlagDefinition<boolean>;
function defineExperiment<T>(options: DefineExperimentOptions<T>): FlagDefinition<T>;
function defineMigrationFlag(options: DefineBooleanFlagOptions): FlagDefinition<boolean>;
function defineProgressiveDeploy(options: DefineBooleanFlagOptions): FlagDefinition<boolean>;
function defineEntitlementFlag(options: DefineBooleanFlagOptions): FlagDefinition<boolean>;
function defineCircuitBreaker(options: DefineBooleanFlagOptions): FlagDefinition<boolean>;
function defineDynamicConfig<T>(options: DefineDynamicConfigOptions<T>): FlagDefinition<T>;

defaultValue is always required — there are no hidden defaults. See Flag Taxonomy.

function runShadow<T>(
oldImpl: () => T,
newImpl: () => T,
onResult: ShadowResultHandler<T>,
): T;
function runShadowAsync<T>(
oldImpl: () => Promise<T>,
newImpl: () => Promise<T>,
onResult: ShadowResultHandler<T>,
): Promise<T>;
function deepEqual(a: unknown, b: unknown): boolean;

Standalone utilities for comparing an old and new code path during a migration — decoupled from FlagDefinition/FlagsClient so evaluate() stays pure. Always returns the old implementation’s result; a throwing newImpl is caught and reported via onResult, never propagated.