first commit
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
# PluginManager
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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<PluginManagerEventMap> }`.
|
||||
|
||||
#### `install(plugin): Promise<void>` / `installAll(plugins): Promise<void>`
|
||||
|
||||
`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<void>`
|
||||
|
||||
Disables the plugin first if it is currently enabled, then runs
|
||||
`onUninstall`. Rejects with `PLUGIN_NOT_FOUND` on unknown name.
|
||||
|
||||
#### `enable(name): Promise<void>` / `disable(name): Promise<void>`
|
||||
|
||||
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<void>`
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user