Saltar a contenido

Eventos de dominio

Reacciona a hechos de negocio entre plugins sin importar el paquete del otro: middleware en el borde, observers en tus datos, events y listeners en la plataforma, notifications y broadcasting para avisar.

Beta

API en cortex_framework.io (events, observers, notifications, broadcasting, middleware). Decisiones: ADR 021 y ADR 010. Adopción incremental: booking emite booking.confirmed; payments escucha con listener registrado en bootstrap.

Cuándo aplica

  • Un plugin debe reaccionar cuando otro confirma un hecho (entity.confirmed, factura emitida, …).
  • Quieres side-effects locales al guardar tus entidades sin repetir código en cada endpoint.
  • Necesitas separar “pasó el hecho”, “avisar a alguien” y “actualizar la UI en vivo”.

Mapa de conceptos

Pieza Pregunta Dónde vive
Middleware ¿La petición puede entrar? Borde HTTP (apirest, guards)
Observers ¿Qué hago en mi plugin al mutar mis datos? Plugin dueño del modelo (ADR 017)
Events ¿Qué hecho público ocurrió? Contrato de plataforma (io.events)
Listeners ¿Quién reacciona a ese hecho? Plugins activos que se registran en bootstrap
Notifications ¿A quién aviso y por qué canal? Canales de entrega (io.notifications)
Broadcasting ¿Qué clientes en vivo se enteran? Fachada sobre WebSocket (io.ws)

Listeners son el lado receptor de events, no un mecanismo aparte.

Fuente del diagrama: docs/diagrams/guias-plugins-eventos-dominio-01.mermaid.

flowchart LR
  OBS[observers] -->|ciclo de vida modelo| EV[events]
  EV --> L[listeners]
  EV --> NT[notifications]
  EV --> BC[broadcasting]
  BC --> WS[io.ws]
  MW[middleware borde HTTP]

Leyenda: Middleware es borde HTTP (aparte). Observers emiten events; listeners, notifications y broadcasting reaccionan. Estado: Implementado (beta).

Middleware

Pipeline del request (auth, rate limit, plugin habilitado, errores de transporte).

  • Corre antes de tu handler REST o del borde MCP.
  • No emite hechos de dominio ni crea pagos.
  • Piezas actuales viven junto a la API FastAPI; el diseño las agrupa bajo IO (ADR 010).

Observers

Gancho al ciclo de vida de tus entidades (crear, actualizar, borrar).

  • Solo el plugin que posee la tabla o el store registra observers sobre esos datos.
  • Sirven para lógica local (auditoría del plugin, normalización) y, sobre todo, para emitir un event de plataforma de forma centralizada.
  • No llaman a otro cortex_plugin_* ni observan modelos ajenos.

Si no usas observer, el servicio puede emitir el event a mano; el observer evita olvidarlo en cada ruta.

Events

Hecho de dominio público, con nombre estable y payload acordado.

  • Ejemplo: helloworld.item_created tras crear un ítem de demo en el plugin helloworld.
  • El emisor no conoce a los listeners.
  • Cero listeners: el emit no falla (no-op).
  • No envía email ni empuja al panel por sí solo; puede disparar notifications o broadcasting en listeners dedicados.

Listeners

Funciones (o handlers) que se registran en bootstrap del plugin activo y se asocian a un nombre de event.

  • Solo se cargan listeners de plugins activos en el state store.
  • Si el plugin consumidor no está activo, no hay listener: el emit sigue siendo válido.
  • Un listener hace trabajo de su dominio, no observa la base de otro plugin.

Hook: register_listeners(registry)registry.listen("helloworld.item_created", handler).

Notifications

Entrega de un aviso por uno o más canales (correo, webhook externo, registro interno, …).

  • Separado del event: el event es “reserva confirmada”; la notification es “avisar al cliente”.
  • Los canales pluggables viven en el framework; el plugin de dominio no implementa SMTP.
  • Los webhooks hacia URLs externas (ADR 010) se modelan como canal de notification, no como bus entre plugins.

Broadcasting

Push en tiempo real a clientes conectados (panel React).

  • Transporte base: WebSocket ya implementado (WS /api/v1/ws). Ver MCP y WebSocket.
  • Broadcasting es la fachada de dominio (“publica este hecho a la UI”), no un segundo protocolo.
  • Si no hay clientes conectados, no debe tumbar la operación de negocio.

Flujo ejemplo: ítem creado (plugin helloworld)

  1. Middleware valida la petición a POST /api/v1/samples/todo.
  2. El servicio del plugin helloworld persiste el ítem.
  3. Un observer (register_observers) en created emite helloworld.item_created vía event_dispatcher.emit.
  4. Un listener de otro plugin activo reacciona al hecho (p. ej. actualizar un índice).
  5. Una notification (notification_manager.send) avisa si aplica.
  6. Broadcasting (broadcaster.broadcast) publica al canal de UI.

Si el plugin consumidor no está activo, no hay listener y el emit no falla.

Emitir desde un servicio

await event_dispatcher.emit("helloworld.item_created", {"id": item_id})

Registrar un listener

async def on_item_created(payload: dict) -> None:
    ...

def register_listeners(registry) -> None:
    registry.listen("helloworld.item_created", on_item_created)

Qué no mezclar

Incorrecto Correcto
Middleware crea el side-effect del otro plugin Listener del plugin consumidor reacciona al event
Observer del plugin B lee tablas del plugin A Listener escucha el event con payload acordado
Event “envía el email” como única responsabilidad Event + notification por canal
Plugin A importa cortex_plugin_b Event/listener o REST sin import cruzado
Broadcasting como forma de que un plugin lea el modelo de otro Event con payload o REST al plugin activo

Integración en paralelo

  • Events/listeners para relevo desacoplado (esta guía).
  • REST sin imports cruzados cuando el llamador conoce la ruta (contexto del proyecto).
  • MCP para orquestación por agentes (MCP y WebSocket).

Fuera de esta guía

  • Bus de datos y hooks de puertos entre capacidades de plugins (entrada/salida de datos como cables explícitos). Vendrán en una decisión posterior; no forman parte de este diseño.
  • Colas asíncronas obligatorias (se evaluarán con Redis / ADR 009).

Errores frecuentes

Síntoma Causa probable
Listener no corre Plugin no exporta register_listeners o no está activo en /control/plugins
Plugin B importa plugin A Prohibido; usa event/listener o REST
Observer del plugin B sobre modelo del plugin A Ownership incorrecto (ADR 017)
Confundir notification con event Event = hecho; notification = aviso

Siguiente paso