Cordis and Plugin Architecture in TypeScript Without the Headache
If you've ever written an extensible application in Node.js or TypeScript, you've probably hit the same pitfalls. A user or system connects a plugin. The plugin hangs five event listeners, starts a couple of timers, registers its routes, and injects a service. And then the plugin is disabled or updated on the fly.
What happens next? Right, memory leaks. Listeners keep hanging, timers tick in the background, context references prevent the garbage collector from cleaning up memory. In Node.js, managing the lifecycle of dependencies often turns into manual grunt work.
Some time ago, the developers of the Koishi chatbot framework faced exactly this problem. They needed to create a core where hundreds of third-party plugins could start, isolate, replace each other, and unload without restarting the process. That's how the Cordis framework was born.
What Even Is Cordis
The creators call their project a "meta-framework of spatiotemporal composability". It sounds smart and pretentious, but the essence is actually down-to-earth.
Cordis combines a dependency injection container (IoC), an event bus, and a hierarchical context tree. Each plugin or service lives within its own context. If that context is destroyed, Cordis automatically cleans up absolutely all resources for it: removes event handlers, kills timers, and deletes created services.
There's no magic here, but there is clear discipline: if a plugin uses the methods [object Object], [object Object], or [object Object], the framework takes care of cleaning up side effects on its own.
How the Context Model Works
The central concept in the library is [object Object]. It's not just a flat object with settings, but a branching tree.
When you call [object Object], the framework spawns a child context (fork). The child context inherits the parent's services but stores its own references to registered resources.
If you disable Plugin A, its child context collapses. The [object Object] handler is removed from the shared event bus, while the Database Service and Plugin B continue to work calmly.
Services and Typing in TypeScript
Services in Cordis are declared through inheritance of the base class [object Object]. This makes them accessible directly through context properties while maintaining strict typing:
The [object Object] construct solves the problem of module loading order. If the database service initializes asynchronously or connects later, the dependent plugin will wait for its readiness and activate itself.
Fine-Tuning Scope Visibility
In real programs, modules often shouldn't react to everything. For example, one handler is needed only for messages from a specific channel or requests with a particular header.
Cordis introduces the concept of filters through the [object Object] call and context properties. You can limit a service's visibility to a specific branch of the tree or set a predicate that filters out unwanted events:
What Tasks Is This Approach Good For
The library was created for a specific class of applications. You shouldn't drag it into a regular CRUD API on Fastify or Express, where it would create an extra layer of abstraction.
But Cordis fits perfectly into the following scenarios:
- Modular CLI utilities and generators. When users can deliver npm packages that extend commands or build pipelines.
- Desktop applications on Electron/Tauri. For organizing an addon system and themes that can be enabled and disabled on the fly without reloading the window.
- Bots and integration hubs. If a service communicates with a dozen different platforms (Telegram, Discord, Slack), and each adapter needs to live an isolated life.
- Automation tools. Where processes are configured by users dynamically through a web interface or YAML files.
Pitfalls and Drawbacks
There are no perfect tools, and Cordis has plenty of specific nuances:
- Steep learning curve. The documentation is written in dry language with an abundance of specific terms. To get through the concepts of scope merging and side effects, you'll need to carefully read the source code.
- Unfamiliar API. Binding services to the context object through TypeScript's type merging module can be confusing at first for those used to classic NestJS or InversifyJS with their decorators.
- Tied to a mental model. If your project's architecture doesn't involve frequent dynamic code unloading, the benefits of the built-in effect manager are negated by code complexity.
Is It Worth Trying
Cordis is an interesting engineering project focused on component lifecycle management. It elegantly solves the problem of resource leaks when creating extensible systems.
If you're designing a system with a developed ecosystem of pluggable plugins and want a reliable mechanism for tracking side effects out of the box, fork the repository [object Object] and study the examples in the tests. It's a great example of how you can build a microkernel architecture in pure TypeScript.
Related projects