# Event
An **Event** is the core message type in EDRI. Everything in the system communicates through events.
---
## What is an Event?
* Events are **transport-agnostic** — the same event model works for REST, WebSocket, HTML, scheduled tasks, and internal communication.
* All events **inherit from** `Event` (a dataclass base class).
* Events are routed by the **Router** to subscribed **Managers** based on their Python type and direction — the Router keeps separate subscription maps for requests and responses, so a Manager can subscribe to a given event type as a request, as a response, or both.
* Events can carry a **Response** (for request/reply patterns) or be fire-and-forget (notifications).
```mermaid
flowchart LR
C["Component (Producer)
put(Event)"] -->|dispatch by type| R[Router] --> M["Manager (Subscriber)
@request"]
```
---
## Defining events and responses
Events and responses are registered using decorators:
| Decorator | Purpose |
|-------------|--------------------------------|
| `@event` | Register a class as an Event |
| `@response` | Register a class as a Response |
### Minimal example
```python
@response
class Pong(Response):
pass
@event
class Ping(Event):
response: Pong
```
### Event with payload
```python
@response
class SearchResponse(Response):
results: list[dict]
total: int
@event
class Search(Event):
query: str
limit: int = 10
response: SearchResponse
```
### Fire-and-forget event (no response)
```python
@event
class LogMessage(Event):
level: str
message: str
source: str
```
---
## Fields
Event fields are standard Python dataclass fields:
```python
from dataclasses import field
from enum import Enum, auto
from uuid import UUID, uuid4
from edri.dataclass.event import Event, event
class Priority(Enum):
LOW = auto()
NORMAL = auto()
HIGH = auto()
@event
class Task(Event):
name: str # required
id: UUID = field(default_factory=uuid4) # generated per instance
priority: Priority = Priority.NORMAL
tags: list[str] | None = None # optional
metadata: dict = field(default_factory=dict) # mutable default
```
### Keyword-only fields
Framework-supplied fields (headers, cookies, scope) are typically declared as `kw_only=True`:
```python
from dataclasses import dataclass, field
@dataclass
class AuthenticatedEvent(Event):
user_id: int
bearer_token: list[str] | None = field(
default=None,
kw_only=True,
)
```
This prevents positional-argument ordering problems with dataclass inheritance.
---
## The `response` field
When an event declares a `response` field:
1. `@event` validates the type is a subclass of `Response`.
2. The `response` field is made `init=False` (not passed to the constructor).
3. It is auto-instantiated via `default_factory`.
```python
@event
class MyEvent(Event):
response: MyResponse
# Usage:
event = MyEvent(other_field="value")
event.response.result = "done" # auto-instantiated, no need to create it
```
---
## Event lifecycle
### Simple model (request/response)
```mermaid
flowchart TD
S1["1. Component creates Event"] --> S2["2. Event put on Router Queue"]
S2 --> S3["3. Router looks up
subscribers by Event type"]
S3 --> S4["4. Router delivers Event
to subscribed Managers"]
S4 --> S5["5. Manager selects @request handler method"]
S5 --> S6["6. Handler runs
read fields
set response
return"]
S6 --> S7["7. Manager auto-sends
modified response"]
S7 --> S8["8. Router delivers response back to origin"]
```
### Fire-and-forget model
```mermaid
flowchart TD
F1["1. Component creates Event
(no response field)"] --> F2["2. Event put on Router Queue"]
F2 --> F3["3. Router delivers to subscribed Managers"]
F3 --> F4["4. Handler runs"]
F4 --> F5["5. No response
event is discarded"]
```
(nested-event-model)=
### Nested event model
```mermaid
sequenceDiagram
participant Caller
participant MA as ManagerA
participant R as Router
participant MB as ManagerB
Caller->>MA: EventA
Note over MA: creates EventB, embeds EventA inside
MA->>R: put EventB
R->>MB: deliver EventB
Note over MB: processes EventB, sets response
MB->>R: EventB response
R->>MA: EventB response
Note over MA: completes EventA using EventB's response
MA->>R: put completed EventA
R->>Caller: EventA response
```
---
## Shareable events
By default, events stay within a single EDRI instance. To allow an event to cross the network to another router (via the Switch), mark it as **shareable**:
```python
@event(shareable=True)
class CrossNodeTask(Event):
data: str
response: CrossNodeResponse
```
Only shareable events are forwarded through the Connector to the Switch.
---
## Transport metadata
Every event carries up to two optional internal metadata objects that track per-hop transport state:
### `SwitchInfo`
Populated on every ``shareable`` event. Tracks the current hop through the Switch:
| Attribute | Type | Description |
|-------------|------------------|--------------------------------------------------------------------------|
| `router_id` | `UUID \| None` | The UUID of the Router that sent the event on the current hop. |
| `received` | `bool` | ``True`` when the event arrived from the Switch, so the local Router does not echo it back through the Connector. |
Both attributes are re-stamped on every send/receive and cleared when the event is turned into a response. The event's stable identity (used for cache replay and Switch resume markers) is ``Event._id`` — every event carries one, regardless of shareability.
### `ApiInfo`
Populated when an event is dispatched through the API layer (REST, WebSocket, HTML):
| Attribute | Type | Description |
|-----------|-----------|------------------------------|
| `key` | `str` | The API route key. |
| `type` | `ApiType` | Enum value (REST, WebSocket, HTML, etc.). |
Both `SwitchInfo` and `ApiInfo` are concealed (prefixed with ``_``) from JSON output and managed automatically by the framework.
---
## Event handling types (`EventHandlingType`)
`EventHandlingType` is an :class:`~enum.IntFlag` that controls how events are dispatched to clients by the API broker. Multiple modes can be combined with bitwise operations.
| Value | Name | Behaviour | Example APIs |
|-------|--------------|-------------------------------------------------------------------|----------------------------------|
| 1 | `SPECIFIC` | 1:1 response to the client that requested the event. | REST, HTML |
| 2 | `SUBSCRIBED` | Sent only to clients subscribed to this event type. | WebSocket, MQTT |
| 4 | `ALL` | Broadcast to every async-capable client, regardless of subscription. | WebSocket, MQTT |
**Combinations:**
| Value | Combination | Use case |
|-------|-------------------------|-----------------------------------------------------------------|
| 3 | `SPECIFIC \| SUBSCRIBED` | Direct response or subscriber broadcast, depending on context. |
| 5 | `SPECIFIC \| ALL` | Direct response or global broadcast, depending on context. |
---
## Serialization
Events cross process boundaries via `multiprocessing.Queue`, which uses `pickle` for serialization.
### Rules
* All event field values must be **picklable**.
* Standard Python types (`int`, `str`, `list`, `dict`, `None`, `UUID`, `datetime`, `Enum`) are safe.
* Custom objects must implement `__getstate__` / `__setstate__` or be pickle-compatible.
* Lambdas, generators, file handles, and most function references are **not** picklable.
### Cross-node serialization
For events that leave the local appliance (shareable events), the same pickle mechanism is used over TCP between the two Connectors — the Switch itself never unpickles, it forwards bytes. Two things must line up:
* **Pickle protocol.** EDRI calls `pickle.dumps` with the default protocol — 5 on every supported Python (3.12+), stable since 3.8. The receiver's `pickle.HIGHEST_PROTOCOL` must be at least what the sender emits, so a node may not run a Python far *behind* its peers. The Python versions themselves need not match.
* **Event classes.** Every shared event class must resolve to the **same import path** on both nodes — `pickle` keys on `module.qualname` and the Connector matches demands by class identity, so `myapp.events.Task` and `other.events.Task` are unrelated types that will not route.
When the producer and consumer are **separate deployments** (in practice, separate repositories), the shared event and response definitions cannot just be copied into each — they must come from a **single package both sides depend on**, installed at the same import path and released in lockstep. Adding, removing or renaming a field on a shareable event is a breaking change for every peer still on the old definition. Keep that package free of `@api` events: importing it would register HTTP routes on nodes that only need the wire type.
(concealed-fields-api-output)=
### Concealed fields (API output)
When an event or its response is serialized for an API client, any field whose name starts with an underscore (`_`) is **concealed** — omitted from the output. The framework relies on this for its own internal fields (`_timing`, `_switch`, `_worker`, …), but the same convention is available to application code: name a field `_something` to keep it out of client payloads.
Concealment is **not** uniform across transports — it depends on which renderer handles the response:
| Transport | Concealed (`_`) fields |
|---|---|
| REST (JSON) | **stripped** |
| WebSocket (JSON) | **stripped** |
| HTML template | **kept** — passed to the Jinja template |
This asymmetry is deliberate and useful: a field like `_db_row` or `_internal_score` can be set by a Manager, rendered into an HTML page by a template, yet never leak into the JSON/WebSocket representation of the same event. It is purely a name-prefix convention — there is no decorator or annotation involved.
> Note: concealment is independent of pickle serialization above. Concealed fields **do** cross process and node boundaries normally (the Router, Workers, and the Switch all see them); they are only filtered at the final API-rendering step.
---
## Inheritance and shared patterns
### Marker base classes
A common pattern is to create marker base classes that express requirements:
```python
@dataclass
class AuthenticatedEvent(Event):
"""Requires authentication — middleware checks this."""
pass
@dataclass
class RateLimitedEvent(Event):
"""Subject to rate limiting."""
pass
@event
class DeleteAccount(AuthenticatedEvent, RateLimitedEvent):
user_id: int
response: DeleteAccountResponse
```
### Shared input fields
```python
@dataclass
class PaginatedEvent(Event):
page: int = 1
page_size: int = 20
@event
class ListUsers(PaginatedEvent):
response: ListUsersResponse
@event
class ListOrders(PaginatedEvent):
status: str | None = None
response: ListOrdersResponse
```
---
## Built-in events
EDRI includes several built-in event families:
| Module | Purpose |
|---------------------------------------------|--------------------------------------|
| `edri.events.edri.manager` | Manager control (Restart, WorkerQuit) |
| `edri.events.edri.router` | Router control |
| `edri.events.edri.scheduler` | Scheduler control (Set, Update, SetUpdate, Cancel) |
| `edri.events.edri.store` | Store control (Set, Get, Delete, GetCallback) |
| `edri.events.edri.group` | Marker base classes for event classification (`Manager`, `Router`, `Scheduler`, `Store`, `Switch`, `Test`) |
| `edri.events.api.client` | Client management events |
| `edri.events.api.group` | API group events |
| `edri.events.api.manage` | API management events |
---
## Gotchas & Notes
* Events are **not** inherently API events. API exposure is added via `@api(...)` — see {doc}`/api_event`.
* The response field is **optional**. Many events (notifications, scheduled tasks) do not declare one.
* If a response is declared, its field name must be `response` (lowercase).
* `@event` validates the response type at class definition time.
* `@event(shareable=True)` allows the event to cross router boundaries.
* Events must be picklable for inter-process communication.
* Event creation can happen anywhere in the system (Managers, Workers, API, Middleware).
* Nested events are powerful for multi-step workflows — keep ownership and lifecycle clear.
* The `method` field on events is **legacy/deprecated** and will be removed. Use `method=...` on `@api(...)` instead.
---
## See also
* {doc}`/api_event` — exposing events through the API layer
* {doc}`/response` — response status, directives, lifecycle
* {doc}`/router` — how events are dispatched
* {doc}`/manager` — how managers handle events
* {doc}`/validation` — field validation with `inject(...)`