# PluginManager _(no submodules)_ ## Purpose `PluginManager` is the typed plugin registry for `@suaveplan/game-server`. It owns the install/uninstall/enable/disable lifecycle, drives plugin dependency ordering via topological sort, and wraps every plugin lifecycle hook in a single discriminable error code (`PLUGIN_LIFECYCLE_FAILED`) so consumers can centralise their error handling without scraping cause chains. The manager carries no global state — plugins are stored under their declared `name`, and the install order is captured separately so `disposeAll()` can tear plugins down in reverse. Every state-changing call (`install`, `uninstall`, `enable`, `disable`) emits both a structured log line and a typed event onto the injected `EventBusLike`, and bumps the `game_server.plugin.lifecycle_total` OTel counter with a low-cardinality `op`/`outcome` attribute pair. ## Features - Typed `EnginePlugin` contract with optional `onInstall` / `onUninstall` / `onEnable` / `onDisable` hooks. - Zod-validated plugin shape on every registration path. - `DUPLICATE_PLUGIN` on second install with the same name; `PLUGIN_NOT_FOUND` on every unknown-name lookup; `PLUGIN_LIFECYCLE_FAILED` wrapping hook errors, missing deps, and cycles. - `installAll(plugins)` accepts an unordered batch and resolves dependencies via topological sort. - `disposeAll()` tears plugins down in reverse install order; `isEnabled` is checked before `onDisable` fires (so callers can call uninstall on a still-enabled plugin without an explicit disable first). - Structured logging + typed event emission on every state change. - OTel `game_server.plugin.lifecycle_total` counter incremented with `op`/`outcome` attributes. ## Usage: Basic ```typescript import { PluginManager } from "@suaveplan/game-server/engine/plugin-manager"; const plugins = new PluginManager({ logger, events }); await plugins.install({ name: "metrics", version: "1.0.0", async onInstall(ctx) { ctx.logger.info("metrics plugin online"); }, }); await plugins.enable("metrics"); ``` ## Usage: Advanced ```typescript import { PluginManager } from "@suaveplan/game-server/engine/plugin-manager"; const plugins = new PluginManager({ logger, events }); await plugins.installAll([ { name: "metrics", version: "1.0.0" }, { name: "leaderboard", version: "0.3.0", dependencies: ["metrics"], async onInstall(ctx) { ctx.events.emit("plugin:installed", { name: "leaderboard", version: "0.3.0", }); }, }, ]); // `metrics` is installed first because `leaderboard.dependencies = ["metrics"]`. await plugins.enable("leaderboard"); await plugins.enable("metrics"); // Errors carry the offending plugin + hook name: try { await plugins.install({ name: "broken", version: "0.1.0", async onInstall() { throw new Error("boom"); }, }); } catch (err) { if (err.code === "GAME_SERVER.PLUGIN_LIFECYCLE_FAILED") { console.log(err.context); // { name: "broken", hook: "onInstall" } } } ``` ## API Reference ### `class PluginManager` #### `new PluginManager(deps)` `deps`: `{ logger: LoggerLike; events: EventBusLike }`. #### `install(plugin): Promise` / `installAll(plugins): Promise` `install` rejects with `DUPLICATE_PLUGIN` on a re-install. `installAll` topologically orders the batch; a missing dependency or a cycle rejects with `PLUGIN_LIFECYCLE_FAILED` whose `context` carries either `{ missing, requestedBy }` or `{ cycle }`. #### `uninstall(name): Promise` Disables the plugin first if it is currently enabled, then runs `onUninstall`. Rejects with `PLUGIN_NOT_FOUND` on unknown name. #### `enable(name): Promise` / `disable(name): Promise` Idempotent — calling `enable` on an already-enabled plugin is a no-op that does not run the hook again. #### `get(name): EnginePlugin` / `has(name): boolean` / `isEnabled(name): boolean` `get` and `isEnabled` throw `PLUGIN_NOT_FOUND` on unknown name; `has` returns a boolean. #### `list(): readonly string[]` / `getStats(): PluginManagerStats` `list()` returns the install order; `getStats()` adds `enabled`/`disabled` counts. #### `disposeAll(): Promise` Uninstall every plugin in reverse install order. ### Types `EnginePlugin`, `EnginePluginContext`, `PluginManagerStats`, `PluginManagerEventMap`, `PluginManagerDeps`. ## Implementation Notes Topological sort uses the standard three-set Tarjan algorithm (`visited` / `visiting` plus a `path` accumulator) so cycles surface with the offending edge sequence rather than a generic message. The error carries the cycle string in `context.cycle` so log scrapers can group by the participating plugins without parsing free text. The decision to fold "schema validation" failures into the `PLUGIN_LIFECYCLE_FAILED` code rather than a dedicated `INVALID_PLUGIN` code keeps the error surface narrow — every plugin-related failure has the same discriminable code, with the specific failure mode visible in `context.hook` or `context.missing` or `context.cycle`. Consumers that want to branch on the precise failure mode read those fields; the `code` field is enough for a catch-all log emit. `disposeAll` walks the install order in reverse so plugins with declared dependencies tear down after their dependents. This mirrors the upstream contract: a logging plugin installed first must be the last to go, because every other plugin's `onUninstall` may still need to log.