Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
# FeatureFlagPlugin
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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.