# DIContainer _(no submodules)_ ## 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` 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("logger"); const logger2 = container.resolve("logger"); console.log(logger1 === logger2); // true — singleton const r1 = container.resolve("requestId"); const r2 = container.resolve("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; const container = new DIContainer() .register(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(CLOCK, () => new TestClock()); const freshClock = container.resolve(CLOCK); // new TestClock instance ``` ## API Reference ### `class DIContainer` #### `register(identifier, factory, lifecycle = "singleton"): this` Register a service. Re-registering an identifier replaces the factory and discards any cached singleton instance. #### `resolve(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` — `string | symbol`. - `ServiceLifecycle` — `"singleton" | "transient"`. - `ServiceDefinition` — 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.