Files
2026-05-27 23:47:53 +01:00

3.3 KiB

FeatureFlagPlugin

(no submodules)

Purpose

FeatureFlagPlugin wraps a @suaveplan/feature-flags FlagManager and exposes its evaluation surface as a lifecycle-managed game-server plugin. It folds a per-player userId into the evaluation context automatically and records every evaluation to a rotating log so callers can replay or audit flag decisions without instrumenting every callsite by hand.

The plugin is intentionally a thin adapter: it does not own the provider chain, override semantics, or persistence story — those remain in @suaveplan/feature-flags. Errors raised by the underlying manager (notably FLAG_NOT_FOUND) propagate unchanged so callers can catch the canonical FeatureFlagsError discriminator.

Features

  • Lifecycle-managed via BasePlugin — clears the evaluation log on uninstall.
  • Per-player EvaluationContext building from defaultContext + playerId.
  • isEnabled / evaluate / getString / getNumber helpers wrapping the underlying manager API.
  • Rotating evaluation log (FIFO; default cap 1 000) for downstream analytics.
  • Zod-validated maxLogEntries option.

Usage: Basic

import {
    FlagManager,
    FlagRegistry,
    Evaluator,
    MemoryProvider,
} from "@suaveplan/feature-flags";
import { FeatureFlagPlugin } from "@suaveplan/game-server/plugins/feature-flag-plugin";

const provider = new MemoryProvider({
    flags: [
        { key: "boss-rush", type: "boolean", defaultValue: false },
    ],
});
const registry = new FlagRegistry({ provider });
await registry.load();
const evaluator = new Evaluator();
const flagManager = new FlagManager({ registry, evaluator });

const plugin = new FeatureFlagPlugin({ flagManager });
plugin.isEnabled("boss-rush", "player-1");

Usage: Advanced

import { FeatureFlagPlugin } from "@suaveplan/game-server/plugins/feature-flag-plugin";

const plugin = new FeatureFlagPlugin({
    flagManager,
    defaultContext: { attributes: { region: "us-west" } },
    maxLogEntries: 50,
    clock: () => 1_700_000_000_000,
});

await plugin.onInstall(ctx);
plugin.evaluate("boss-rush", "p1");
plugin.getString("hero-skin", "p1");
plugin.getNumber("drop-rate-multiplier", "p1");
const log = plugin.getEvaluationLog();

API Reference

  • class FeatureFlagPlugin extends BasePlugin
  • constructor(options: FeatureFlagPluginOptions)
  • isEnabled(key, playerId?): boolean
  • evaluate<T>(key, playerId?): FlagEvaluationResult<T>
  • getString(key, playerId?): string
  • getNumber(key, playerId?): number
  • getEvaluationLog(): readonly EvaluationLogEntry[]
  • clearLog(): void

Implementation Notes

  • The evaluation log is bounded; once evaluationLog.length exceeds maxLogEntries, the oldest entries are pruned via slice(-max).
  • buildContext merges defaultContext with the per-call player id. userId from the default context is preserved when the caller does not supply a playerId.
  • Errors thrown by FlagManager (e.g. FLAG_NOT_FOUND) bubble up as FeatureFlagsError. The plugin does not translate them so callers retain access to the underlying error code without an extra cause unwrap.
  • The plugin holds the flagManager by reference and never disposes it. Disposal is the caller's responsibility because the manager may be shared across plugins.