4.7 KiB
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<T>carries throughregisterandresolve. - Fluent API —
registerreturnsthisfor chaining. - Re-register invalidates cache — calling
registeragain clears any cached singleton instance (improvement over upstream). - Typed error on miss —
GameServerError(GAME_SERVER.SERVICE_NOT_REGISTERED). - Introspection —
has,getRegisteredServices, andclearround out the surface.
Usage: Basic
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
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.