Understanding Internal Events for Plugin Communication in Cordis
Cordis implements a lightweight, type-safe event system centered on EventsService that enables decoupled plugin-to-plugin communication through reserved internal/* namespace events and multiple dispatch strategies.
The cordiverse/cordis framework relies on a central event hub to coordinate interactions between independently loaded plugins. Every Context instance owns an events object that serves as the backbone for message passing, lifecycle hooks, and service orchestration across the entire application.
The EventsService Architecture
The heart of Cordis communication lies in EventsService, defined in packages/core/src/events.ts. Each Context instance holds an events property that references this service, creating a dedicated event bus for every plugin scope. The service maintains an internal _hooks map that stores registered callbacks and exposes methods for binding, dispatching, and managing event listeners throughout the plugin lifecycle.
Registering Event Listeners
Plugins register interest in events through ctx.on(name, listener, options?) or ctx.once(name, listener). These methods delegate to EventsService.on, which performs context binding before storage. The registration flow executes the 'internal/listener' hook first, allowing other plugins to intercept or modify listener attachment:
// packages/core/src/events.ts – registration flow
this.ctx.fiber.assertActive()
listener = this.ctx.reflect.bind(listener) // bind context utilities
const result = this.bail(this.ctx, 'internal/listener', name, listener, options)
if (result) return result
const label = `ctx.on(${JSON.stringify(name)})`
return this.register(label, name, listener, options)
Dispatch Modes and Execution Strategies
Cordis supports five distinct dispatch strategies through the _resolve method, each encoded as a string literal argument. The mode determines how listeners execute and whether results propagate:
- emit – Fire all listeners concurrently and discard return values
- parallel – Execute listeners concurrently while aggregating errors
- serial – Invoke listeners sequentially until a bail value returns
- bail – Stop at the first non-null/false/undefined result
- waterfall – Chain listeners sequentially, passing each output to the next input
After resolution in _resolve, the appropriate execution method (parallel, serial, bail, or waterfall) processes the callback queue.
The Internal Event Namespace
Cordis reserves the internal/* namespace for framework-level communication that drives plugin lifecycles. These events are defined in the Events interface at the bottom of packages/core/src/events.ts and include:
internal/plugin– Emitted whenpackages/loader/src/index.tsinstantiates a new plugin fiberinternal/status– Fired when a pluginFibertransitions between states (starting,running,stopped)internal/service– Triggered when core services register or override viactx.service(name, value)internal/update– Signals configuration updates, allowing listeners to prepend or append logicinternal/getandinternal/set– Intercept property access on context objectsinternal/listener– Meta-event that fires when any event listener registersinternal/dispatch– Hooks into the dispatch process for logging or modification
Cross-Plugin Communication Patterns
Plugins communicate by publishing custom events while optionally tapping into internal lifecycle hooks. A plugin emits public events using ctx.emit('my-plugin/ready') or ctx.parallel('my-plugin/ready'), while consumers bind with ctx.on('my-plugin/ready', callback). For deeper integration, plugins monitor internal/plugin to react to loading events or internal/status to track state changes in sibling components.
Context Integration and Lifecycle Wiring
The Context class in packages/core/src/context.ts instantiates the event system during construction, binding it to the plugin's Fiber lifecycle:
this.fiber = new Fiber(self, …)
this.reflect = new ReflectService(self)
this.registry = new RegistryService(self)
this.events = new EventsService(self) // ← creates the event hub
this.logger = new LoggerService(self)
This initialization ties EventsService to the current fiber, ensuring that all registered listeners automatically dispose when the plugin stops. The integration between packages/core/src/fiber.ts and EventsService enables the 'internal/status' emissions that track plugin health.
Practical Implementation Examples
Listen to custom events from other plugins:
ctx.on('my-plugin/data', (payload) => {
console.log('Received data:', payload)
})
Hook into the plugin load lifecycle:
ctx.on('internal/plugin', (fiber) => {
console.log(`Plugin loaded: ${fiber.name}`)
})
Intercept configuration updates with waterfall semantics:
ctx.on('internal/update', (config, noSave, next) => {
console.log('Config about to change:', config)
return next()
}, { global: true, prepend: true })
Execute serial computation with bail semantics:
ctx.serial('my-plugin/compute', (input) => {
if (input < 0) return 'negative' // bail with result
// fall through to next listener
})
Summary
- EventsService in
packages/core/src/events.tsserves as the centralized event bus for everyContextinstance - Five dispatch modes (
emit,parallel,serial,bail,waterfall) control how listeners execute and return values propagate - Internal namespace events (
internal/*) provide hooks for plugin lifecycle, service registration, and configuration updates - Automatic disposal ties event registration to
Fiberlifecycles, preventing memory leaks when plugins unload - Meta-events like
internal/listenerandinternal/dispatchenable plugins to observe and modify the event system itself
Frequently Asked Questions
What is the difference between ctx.emit and ctx.parallel in Cordis?
ctx.emit fires all matching listeners concurrently and ignores their return values, making it ideal for notifications that require no response. ctx.parallel also executes listeners concurrently but aggregates thrown errors into a single rejection, ensuring that all listeners attempt execution even if some fail. Both methods route through EventsService but use different error-handling strategies in the underlying dispatcher.
How do I listen to internal plugin lifecycle events?
Register listeners for internal/plugin to detect when new plugins load, or internal/status to monitor state transitions like starting, running, and stopped. These events fire from packages/loader/src/index.ts and packages/core/src/fiber.ts respectively, allowing your plugin to react to the overall system state without direct coupling to other components.
Can I intercept event listener registration in Cordis?
Yes. The internal/listener event fires whenever any code calls ctx.on or ctx.once, passing the event name, bound listener function, and options. By listening to this meta-event with ctx.bail or ctx.on, you can block registration by returning a value, modify the listener before storage, or log registration attempts for debugging purposes.
Where are internal event types defined?
The TypeScript interfaces for internal events reside at the bottom of packages/core/src/events.ts, where the Events interface declares type signatures for internal/plugin, internal/status, internal/service, and other framework-level events. These definitions provide type safety when using ctx.on or ctx.emit with internal namespace strings.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →