import type { FlagConfig, FlagState, FlagTransition, FeatureFlag } from './types.js'; /** Subscriber callback type for flag state changes */ export type FlagSubscriber = (flagId: string, state: FlagState, previous: FlagState) => void; /** * Manages the runtime state of all registered feature flags. * */ declare class FeatureFlagManagerImpl { /** Current state for each registered flag */ private readonly _states; /** Kill reasons stored separately to survive a disable() then re-kill cycle */ private readonly _killReasons; /** In-process ordered audit log of all state transitions */ private readonly _transitions; /** Active subscriber callbacks */ private readonly _subscribers; /** * Persisted config values for startup-gated (runtimeToggleable: false) flags * whose value differs from the current effective state. Populated by * `applyConfigState()` when a live config change targets a flag that cannot * be applied without a restart; cleared once the persisted and effective * values agree again (including at the next process start, when * `loadFromConfig()` seeds the effective state from the same value). */ private readonly _pendingRestart; constructor(); /** * Returns `true` only if the flag is in the `'enabled'` state. * A killed flag always returns `false`. * * @param flagId - The flag's kebab-case identifier. */ isEnabled(flagId: string): boolean; /** * Returns `true` if the flag has been emergency-killed. * * @param flagId - The flag's kebab-case identifier. */ isKilled(flagId: string): boolean; /** * Returns the current `FlagState` for the given flag. * Throws if the flag id is not registered. * * @param flagId - The flag's kebab-case identifier. */ getState(flagId: string): FlagState; /** * Returns a new snapshot Map of all flags with their current state. * Changes to the returned map do not affect the manager's internal state. * * `state` is the live effective state. `persistedState` is the last known * config-layer value; for a startup-gated flag with a pending restart these * two differ, which `pendingRestart` calls out explicitly rather than * leaving a caller to infer it, see `applyConfigState()`. */ getAll(): Map; /** * True when a startup-gated flag has a persisted config value that differs * from its current effective state, i.e. a process restart is required * before the persisted value takes effect. * * @param flagId - The flag's kebab-case identifier. */ hasPendingRestart(flagId: string): boolean; /** * The persisted-but-not-yet-effective state recorded for a startup-gated * flag, or `null` when no restart is pending for it. * * @param flagId - The flag's kebab-case identifier. */ getPendingRestartState(flagId: string): FlagState | null; /** * Returns the ordered audit log of all flag state transitions recorded * in this process lifetime. */ getTransitions(): readonly FlagTransition[]; /** * Enables a flag. * * Throws if: * - The flag id is not registered. * - The flag is currently `'killed'` (must call `disable()` first to un-kill). * - The flag is not `runtimeToggleable` and the process has already started. * * @param flagId - The flag's kebab-case identifier. */ enable(flagId: string): void; /** * Disables a flag (or un-kills it, resetting to `'disabled'`). * * This is the only way to move a flag out of the `'killed'` state. * * Throws if the flag is not `runtimeToggleable` and is not currently killed * (un-killing a killed flag is always permitted regardless of toggleability). * * @param flagId - The flag's kebab-case identifier. */ disable(flagId: string): void; /** * Emergency-kills a flag with a mandatory reason. * * A killed flag: * - Returns `false` from `isEnabled()`. * - Cannot be re-enabled until `disable()` is called first. * - Records the kill reason in the audit log and the `_killReasons` map. * * Calling `kill()` on an already-killed flag updates the reason and * records a new transition (idempotent on state, not on reason). * * **Note:** `kill()` intentionally bypasses `runtimeToggleable`, it is an * emergency override and must never be blocked by flag configuration. * * @param flagId - The flag's kebab-case identifier. * @param reason - Human-readable explanation of why the flag was killed. */ kill(flagId: string, reason: string): void; /** * Applies flag state overrides from the user config layer. * * - Unknown flag ids are rejected so stale configs cannot hide misspelled gates. * - Skips non-toggleable flags if the current state is already non-default * (allows startup-only flags to be seeded via config without error). * - A config-level `'killed'` state is applied without a reason string; * use `kill()` directly for operator-initiated kills with reasons. * * This is the boot-time path, called once, before the runtime event loop * begins, so it applies EVERY flag's desired state directly regardless of * `runtimeToggleable` (a startup-only flag may only be seeded this way). * For a config change arriving after boot, use `applyConfigState()` * instead, which respects `runtimeToggleable` and never fakes a live apply * for a startup-gated flag. * * @param config - Parsed flag config block from the user config file. */ loadFromConfig(config: FlagConfig): void; /** * Applies a single flag's config-layer value from a LIVE config change * (i.e. `configManager.set('featureFlags.', ...)` firing after boot, * see `bindFeatureFlagConfigBridge`), honoring `runtimeToggleable`: * * - Runtime-toggleable flags apply immediately through the same * transition path `enable()`/`disable()`/`kill()` use, so subscribers * fire exactly as they would for a direct manager toggle. * - Startup-gated flags are never faked live: the effective state is left * untouched and the persisted value is recorded as pending a restart * (see `getAll()` / `hasPendingRestart()` / `getPendingRestartState()`) * instead of silently doing nothing. * * @param flagId - The flag's kebab-case identifier. * @param persistedState - The new value read from config. */ applyConfigState(flagId: string, persistedState: FlagState): void; /** * Shared transition logic for a single flag's desired config-layer state, * used by both `loadFromConfig()` (every flag, at boot) and * `applyConfigState()` (one flag, already gated on `runtimeToggleable` by * its caller). Idempotent, and honors kill precedence: config can never * silently re-enable a killed flag. */ private _applyPersistedState; /** * Subscribes to flag state changes. * * The callback fires synchronously when a flag transitions state. * Callbacks must not throw, errors are caught and logged to stderr. * * @param callback - Called with `(flagId, newState, previousState)`. * @returns An unsubscribe function; call it to remove the subscription. */ subscribe(callback: FlagSubscriber): () => void; /** * Validates that a flag exists and returns its declaration. * Throws a clear error if the id is unknown. */ private _requireFlag; /** * Records a state transition, updates `_states`, writes the audit log entry, * and notifies all subscribers. */ private _transition; } /** * Construct a fresh feature-flag manager. * * Keeping this factory in the same module as the class avoids test-runner * namespace quirks around cross-module construction while preserving the same * runtime behavior. */ export { FeatureFlagManagerImpl as FeatureFlagManager }; export declare function createFeatureFlagManager(): FeatureFlagManagerImpl; //# sourceMappingURL=manager.d.ts.map