first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
# DIContainer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`DIContainer` is a small, type-safe service locator that the broader
`@suaveplan/game-server` engine uses to thread shared dependencies — a
`Clock`, a `LoggerLike`, an `EventBus`, networking adapters, plugin
registries — through component boundaries without forcing every call
site to take a deep parameter list. It is intentionally minimal: register
a factory under a string or symbol identifier, resolve to either a cached
singleton or a fresh transient instance, and inspect what is registered
for testing.
This file is the first feature migrated from the reference branch
`claude/review-game-engine-arch-Kaykr`
(`src/engine/DIContainer.ts`). The port replaces the upstream's
plain `throw new Error(...)` on unresolved identifiers with a typed
`GameServerError` carrying the stable code
`GAME_SERVER.SERVICE_NOT_REGISTERED`, so consumers can branch on
`error.code` rather than scraping message strings.
## Features
- **Singleton lifecycle** — factory invoked once; the instance is cached.
- **Transient lifecycle** — factory invoked on every `resolve`.
- **Typed identifiers** — generic parameter on `ServiceIdentifier<T>` carries through `register` and `resolve`.
- **Fluent API** — `register` returns `this` for chaining.
- **Re-register invalidates cache** — calling `register` again clears any cached singleton instance (improvement over upstream).
- **Typed error on miss** — `GameServerError(GAME_SERVER.SERVICE_NOT_REGISTERED)`.
- **Introspection** — `has`, `getRegisteredServices`, and `clear` round out the surface.
## Usage: Basic
```typescript
import { DIContainer } from "@suaveplan/game-server/engine/di-container";
const container = new DIContainer();
container.register("logger", () => new ConsoleLogger(), "singleton");
container.register("requestId", () => crypto.randomUUID(), "transient");
const logger1 = container.resolve<ConsoleLogger>("logger");
const logger2 = container.resolve<ConsoleLogger>("logger");
console.log(logger1 === logger2); // true — singleton
const r1 = container.resolve<string>("requestId");
const r2 = container.resolve<string>("requestId");
console.log(r1 === r2); // false — transient
```
## Usage: Advanced
```typescript
import { DIContainer } from "@suaveplan/game-server/engine/di-container";
import { GAME_SERVER_CODES, GameServerError } from "@suaveplan/game-server/errors";
interface IClock { now(): number }
const CLOCK = Symbol("Clock") as ServiceIdentifier<IClock>;
const container = new DIContainer()
.register<IClock>(CLOCK, () => ({ now: () => Date.now() }))
.register("events", () => new EventBus());
try {
container.resolve("nope");
} catch (err) {
if (
err instanceof GameServerError &&
err.code === GAME_SERVER_CODES.SERVICE_NOT_REGISTERED
) {
// surface a typed missing-dep diagnostic
}
}
// Re-registering invalidates the cached singleton:
container.register<IClock>(CLOCK, () => new TestClock());
const freshClock = container.resolve<IClock>(CLOCK); // new TestClock instance
```
## API Reference
### `class DIContainer`
#### `register<T>(identifier, factory, lifecycle = "singleton"): this`
Register a service. Re-registering an identifier replaces the factory
and discards any cached singleton instance.
#### `resolve<T>(identifier): T`
Resolve a registered service. Throws `GameServerError` with code
`GAME_SERVER.SERVICE_NOT_REGISTERED` if the identifier is unknown.
#### `has(identifier): boolean`
#### `unregister(identifier): boolean`
Returns `true` when the identifier was previously registered.
#### `clear(): void`
Remove every registration and cached singleton.
#### `getRegisteredServices(): ServiceIdentifier[]`
### Types
- `ServiceIdentifier<T = unknown>` — `string | symbol`.
- `ServiceLifecycle` — `"singleton" | "transient"`.
- `ServiceDefinition<T>` — internal record holding identifier, factory, singleton flag.
## Implementation Notes
Re-register-invalidates-cache is the only behavioural change versus the
upstream. The upstream kept the stale singleton after a second
`register` call, which masked test-double swaps — code that worked in
unit tests because the test installed its fake first, but failed in
integration because production startup happened to register the real
service first. Discarding the cached instance on re-register matches the
"last writer wins" semantics every other suaveplan registry uses
(`@suaveplan/events`, `@suaveplan/config`).
The unresolved-identifier path raises `GameServerError`, not a plain
`Error`, because everything in this package is expected to be
discriminable via `error.code`. `String(identifier)` is included in
both the message and the `context` bag so symbols and plain strings
surface identically in logs.