first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user