# Understanding Internal Events for Plugin Communication in Cordis

> Master Cordis internal events for seamless plugin communication. Learn how EventsService and reserved internal/* events enable decoupled interactions with multiple dispatch strategies.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: internals
- Published: 2026-09-12

---

**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`](https://github.com/cordiverse/cordis/blob/main/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:

```typescript
// 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`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/events.ts) and include:

- `internal/plugin` – Emitted when [`packages/loader/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts) instantiates a new plugin fiber
- `internal/status` – Fired when a plugin `Fiber` transitions between states (`starting`, `running`, `stopped`)
- `internal/service` – Triggered when core services register or override via `ctx.service(name, value)`
- `internal/update` – Signals configuration updates, allowing listeners to prepend or append logic
- `internal/get` and `internal/set` – Intercept property access on context objects
- `internal/listener` – Meta-event that fires when any event listener registers
- `internal/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`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/context.ts) instantiates the event system during construction, binding it to the plugin's `Fiber` lifecycle:

```typescript
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`](https://github.com/cordiverse/cordis/blob/main/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:

```typescript
ctx.on('my-plugin/data', (payload) => {
  console.log('Received data:', payload)
})

```

Hook into the plugin load lifecycle:

```typescript
ctx.on('internal/plugin', (fiber) => {
  console.log(`Plugin loaded: ${fiber.name}`)
})

```

Intercept configuration updates with waterfall semantics:

```typescript
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:

```typescript
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.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/events.ts) serves as the centralized event bus for every `Context` instance
- **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 `Fiber` lifecycles, preventing memory leaks when plugins unload
- **Meta-events** like `internal/listener` and `internal/dispatch` enable 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`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts) and [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/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`](https://github.com/cordiverse/cordis/blob/main/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.