DEV Community

Signor_P
Signor_P

Posted on

Nexcord@0.0.3

Nexcord 0.0.3 — Release Notes

🔄 Circular Dependency Resolution: Container Layer Added on Top of Proxy

The Lazy Proxy mechanism from 0.0.2 hasn't been replaced — a new container layer now sits underneath it, controlling the dependency resolution process. Circular dependency detection and instance bookkeeping are now handled centrally through DependencyContainer and ServiceContainer, while the Proxy remains active at runtime for safe method-call access.

Why this was needed: in the previous version, circular resolution logic was scattered and it was hard to trace when and in what order service instances were being created. With 0.0.3:

  • DependencyContainer — holds a class's declared dependencies (including those marked with @Inject()) and resolves them recursively via addDeps().
  • ServiceContainer — holds each service's singleton instance, its circular flag, and its resolution state. Instance creation is now fully managed through addInstanceDeps().

When a circular dependency is detected, the flow now works as follows: DependencyContainer first checks whether the relevant service is already registered in ServiceContainer; if it is and carries the circular flag, the service is re-flagged as circular: false and its instance is rebuilt to break the cycle. This means work that was previously left entirely to the Proxy is now tracked and observable at the container level.

// Behavior stays the same from the developer's side — nothing changes here
@Service()
export class AService {
    constructor(@Inject(() => BService) private readonly bService: InjectType<BService>) { }

    a() {
        this.bService.b() // now resolved more reliably thanks to the container layer
    }
}
Enter fullscreen mode Exit fullscreen mode

Note: this release also fixes a logic bug in ServiceContainer.get() where the slient (silent mode) flag was inverted — silent mode no longer throws, allowing the circular resolution flow to correctly fall through to undefined as intended.


🧱 New Architecture: Isolated Per-Feature Containers

The biggest structural change in 0.0.3. Every decorator — @Service(), @Guard(), @SlashCommand(), @Listener() — now has its own dedicated container class. In the previous version all of these class types were loosely managed through a single shared structure; in 0.0.3 each feature lives in its own container and is fully isolated from the others.

What this means in practice:

  • How a @Guard() class is registered, which routes it owns, and how it's instantiated is now entirely the responsibility of its own container — with no overlap with the @Service() or @SlashCommand() containers.
  • Each container has its own add() / get() contract, but all of them ultimately hand off to ServiceContainer for dependency injection. In other words, every feature gains its "value" through its own container, while instance management and DI resolution remain centralized.
  • Thanks to this isolation, a change in one container (e.g. an update to Guard route-matching logic) can no longer leak into the behavior of other containers — something that was possible in the previous version.

In short: the registration/definition layer is now split per feature, while the instantiation and dependency resolution layer remains centralized.


🛡️ Type-Safe Event Handler Parameters: DcEvents<T> Re-enabled

This feature isn't new — it existed at the core level before but had been disabled. 0.0.3 both updates it and turns it back on.

@On(eventName) and @Once(eventName) decorators now check, at compile time, whether your method's parameters match the correct type for the given event name. The DcEvents<'eventName'> helper type automatically resolves the correct parameter signature for the discord.js event in question.

@On('clientReady')
async ready(...[client]: DcEvents<'clientReady'>) {
    this.loggerService.log(`${client.user?.username} active`)
}
Enter fullscreen mode Exit fullscreen mode

If you mistakenly annotate the client parameter here as string, TypeScript will fail at compile time — because DcEvents<'clientReady'> is bound to the real parameter signature ([Client<true>]) of the 'clientReady' event on the discord.js side, and the decorator enforces it.

Something to keep in mind: this check only works based on the literal event name string you pass to @On / @Once — the more specific the eventName, the more precise the type inference. With generic or dynamic event names (@On(someVariable)), this type narrowing won't kick in.


📋 Summary Table

Change Scope Impact
Container-based circular dependency resolution Core (DependencyContainer, ServiceContainer) Added on top of the Proxy mechanism, not a replacement
slient flag logic bug fix ServiceContainer.get() Improves reliability of the circular resolution flow
Isolated per-feature container architecture All decorators (@Service, @Guard, @SlashCommand, @Listener) Registration/definition layer split apart, DI stays centralized
DcEvents<T> type checking @On / @Once decorators Existed before, was disabled — updated and re-enabled in this release

Top comments (0)