# 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(...)`