# buss — Event Bus y coordinación del ecosistema ## Tesis `buss` debe ser un bus de eventos tipado, no un simple `EventEmitter` genérico y tampoco un contenedor de reglas de negocio. Su objetivo es transportar hechos con envelope, orden, errores y observabilidad. La coordinación del ecosistema se construye encima: ```txt artifact events -> translators in aapp -> app events -> consumer reactions ``` Ejemplo: ```txt Sess emite: sess.changed aapp traduce: app.user.identity.changed Cach decide: si autoInvalidateOn incluye identity -> clear() Perm decide: si autoInvalidateOn incluye identity -> invalidate() Conn decide: si autoReauthOn + connection.session + connection.auth -> reauth/close ``` La regla principal: ```txt Los módulos no deben conocerse entre sí. ``` `sess` no debe importar `conn`, `cach` ni `perm`. `conn` no debe saber de `auth`. `cach` no debe saber de `sess`. La traducción vive en `aapp`; las reacciones viven en la factory del consumidor que las ejecuta. Además, los módulos no crean buses propios. El bus transversal del cliente es `App.Bus`, creado una vez por `aapp`. Un artefacto recibe una referencia inyectada (`EngineBus`, `AppEventBus` o `EventPublisher`) y nunca llama `createEngineBus()` para abrir una isla privada. ## Modelo Cerrado Este es el contrato que se debe implementar antes de seguir ampliando el bus. ### 1. Bus Engine `arts/buss` es un artefacto raíz porque expone `createEngineBus()` y ciclo de vida. Pero su dominio es **mecánico**, no de aplicación: ```txt Sí pertenece a buss: - EngineBus - EventPublisher - BusEnvelope - BusSubscription - publish / publishAsync - on / once / onAny - listenerErrorMode - diagnostics del propio bus - BUS_EVENT_ALL No pertenece a buss: - APP_EVENT_* - SESS_EVENT_* - AUTH_EVENT_* - CACH_EVENT_* - PERM_EVENT_* - CONN_EVENT_* - reglas de cache/permisos/conexiones ``` `buss` no debe conocer `sess`, `auth`, `cach`, `perm`, `conn`, `tenant`, `actor`, `permissions`, `cache` ni `credentials`. ### 2. Module Events Cada artefacto puede publicar eventos propios si recibe un `EventPublisher`. Esos eventos pertenecen al artefacto que los emite: ```txt arts/sess/consts.ts -> SESS_EVENT_* arts/auth/consts.ts -> AUTH_EVENT_* arts/cach/consts.ts -> CACH_EVENT_* arts/perm/consts.ts -> PERM_EVENT_* arts/conn/consts.ts -> CONN_EVENT_* ``` Los module events son hechos internos del ecosistema. No son el contrato público estable para plugins. Los consume `aapp` mediante traductores. Regla: ```txt Un artefacto puede publicar sus propios eventos. Un artefacto no debe suscribirse a eventos privados de otro artefacto. Un artefacto no crea su propio bus; recibe el bus central o un publisher inyectado. El código publica/escucha constantes de evento, no strings inline. ``` ### 3. App Events Los eventos públicos y estables de aplicación no pertenecen a `buss`; pertenecen al contrato común de aplicación. Ubicación propuesta: ```txt src/libs/aapp/events.ts ``` Ahí viven: ```txt APP_EVENT_USER_IDENTITY_CHANGED APP_EVENT_TENANT_SWITCHED APP_EVENT_PERMISSIONS_REFRESH_REQUESTED APP_EVENT_CONNECTIVITY_CHANGED APP_EVENT_CACHE_INVALIDATE_REQUESTED APP_EVENT_DISPOSE_STARTING ``` También viven ahí sus payloads y helpers: ```ts publishAppUserIdentityChanged(bus, payload) onAppUserIdentityChanged(bus, listener) ``` `aapp` y los consumidores importan este contrato desde `libs`, no desde otro artefacto. ### 4. Translators `aapp` no ejecuta side-effects destructivos. `aapp` solo traduce eventos de módulo a eventos de aplicación: ```txt SESS_EVENT_CHANGED -> APP_EVENT_USER_IDENTITY_CHANGED AUTH_EVENT_SIGNED_IN -> APP_EVENT_USER_IDENTITY_CHANGED AUTH_EVENT_SIGNED_OUT -> APP_EVENT_USER_IDENTITY_CHANGED PERM_EVENT_POLICY_DIRTY -> APP_EVENT_PERMISSIONS_REFRESH_REQUESTED ``` Estos traductores viven en: ```txt src/arts/aapp/integrations/*-translator.ts ``` No se llaman `*-orchestrator.ts` en el modelo final si solo traducen. La palabra orquestador se reserva para una pieza que coordina flujo; aquí queremos piezas pequeñas: ```txt module event in -> app event out ``` ### 5. Consumer Reactions Las reacciones automáticas viven en el consumidor que muta su propio estado: ```ts createActiveCache({ autoInvalidateOn: 'standard' }); createActivePermissions({ autoInvalidateOn: 'standard' }); createActiveConnections({ autoReauthOn: 'standard' }); ``` `aapp` nunca configura `invalidateCache`, `invalidatePermissions` ni `reauthenticateConnections` porque eso mezcla traducción con side-effects. ### 6. Defaults Los defaults quedan cerrados así: | Capa | Default | Motivo | | --- | --- | --- | | `App.Bus` | siempre presente | superficie uniforme | | Module events | se publican si el artefacto recibe bus | publicar hechos no muta otros módulos | | `aapp` translators | `standard` por defecto | traducir a `app.*` no es destructivo | | Consumer reactions | `none` por defecto | limpiar cache/permisos o reautenticar sockets sí es destructivo | | `orchestration: 'silent'` | sin traductores automáticos | tests que publican `app.*` manualmente | ### 7. Payload Safety Los `APP_EVENT_*` nunca contienen credenciales, tokens, passwords, headers de authorization, secretos ni hashes sensibles. Si un consumidor necesita contexto sensible, recibe `correlationId` y resuelve contra su propio estado o backend. ## Frame Estratégico `buss` no es solo un refactor interno para quitar puentes directos. Es la fundación del sistema de extensión del framework. Hoy el ecosistema tiene artefactos fijos. Mañana un módulo externo como `feature flags`, `analytics`, `queue`, `billing` o `audit trail` debe poder: - escuchar hechos del runtime sin tocar el core; - emitir hechos propios sin que el core lo conozca; - integrarse desde `aapp` mediante translators; - participar en trazas y diagnósticos con el mismo envelope. Por eso `buss` debe tratarse como API pública desde el primer commit. Los nombres de eventos canónicos, el shape del envelope y la semántica de `publish` son contrato externo. ## Problema Que Resuelve El framework ya tiene módulos potentes, pero los casos reales no ocurren en aislado: - Usuario conectado a un chat con un token. - Cambia la identidad activa. - Las conexiones deben reautenticarse o cerrarse. - La cache actor-scoped o pública contaminada debe invalidarse. - Los permisos cacheados deben vaciarse. - Los logs deben contar qué política se ejecutó. - El test debe verificar toda la cadena. Sin bus, esto acaba en puentes directos: ```txt sess -> conn auth -> cach auth -> perm perm -> cach ``` Ese patrón escala mal porque convierte la composición en una malla invisible. Con `buss`, cada módulo publica hechos, `aapp` los traduce a un vocabulario público de aplicación, y cada consumidor decide sus efectos opt-in. ## Frontera De Uso `buss` es para hechos transversales entre artefactos. No reemplaza los eventos privados de cada módulo. Se quedan internos: - `Session.onChange(...)` - `Connection.onState(...)` - `Connection.onAny(...)` - `Cache.on(CACHE_EVENT_ALL, ...)` - snapshots y listeners internos de `ActiveEngine` Van a `buss`: - cambio de identidad; - sign-in/sign-out; - sesión expirada/revocada; - cambio de permisos; - invalidación transversal de cache; - reautenticación de conexiones por identidad; - eventos públicos que extensiones externas puedan consumir. Regla: ```txt Si el evento solo interesa al módulo que lo emite, no va al bus. Si el evento cambia política entre módulos, sí va al bus. ``` ## Referentes ### Node EventEmitter Bueno: - API conocida: `on`, `emit`, `off`. - Ejecución síncrona y determinista. - Muy probado. Malo: - Tipado débil. - `error` tiene semántica especial. - Listener leaks si no se controlan. - Poca estructura para metadatos, correlación o trazabilidad. Qué copiar: - Emisión síncrona por defecto para eventos internos. - `once`. - Limpieza explícita. Qué evitar: - API basada en `this`. - Strings libres repartidos. - Eventos especiales mágicos. ### mitt Bueno: - Muy pequeño. - Sin dependencias. - Funcional, sin `this`. - Wildcard `*`. - Tipable con mapa de eventos. Malo: - No tiene envelope. - No tiene políticas de error. - No tiene async orchestration. - No tiene replay, prioridad, cola ni metadatos. Qué copiar: - Simplicidad. - `on(type, handler)`, `off`, `emit`. - `on('*')` para observabilidad. Qué mejorar: - Envelopes con `id`, `at`, `source`, `correlationId`. - Constantes obligatorias. - Integración con `Logger`. ### Emittery Bueno: - Async-first. - `onAny`. - Async iterators. - `AbortSignal`. - Limpieza cómoda. - Tipado fuerte. Malo: - Async por defecto puede ocultar orden y carreras. - Más superficie de la necesaria para el core. Qué copiar: - `AbortSignal` para cancelar listeners. - `onAny`. - `publishAsync` separado de `publish`. - Limpieza idempotente. Qué evitar: - Hacer que todos los eventos internos sean async por defecto. ### NestJS Events Bueno: - Usa eventos para desacoplar módulos. - Listeners registrados en composition root. - Buen modelo mental para backend. Malo: - Decorators/DI pesados. - Depende de `eventemitter2`. - Wildcards potentes pero fáciles de abusar. Qué copiar: - Registro centralizado. - Eventos como contrato entre módulos. Qué evitar: - Decorators. - Dependencia externa. - Magia de bootstrap. ### Redux Toolkit Listener Middleware Bueno: - No es solo pub/sub: es orquestación. - Permite efectos al reaccionar a acciones. - Tiene cancelación, `condition`, `take`, `delay`. - Separa evento de efecto. Malo: - Está unido a store/actions Redux. - Puede volverse demasiado flexible. Qué copiar: - Concepto de listener/orchestrator. - Cancelación con `AbortSignal`. - Helpers para workflows complejos en una fase posterior. ### Effect PubSub Bueno: - Capacidad bounded/unbounded. - Estrategias: backpressure, dropping, sliding. - Buen modelo para sistemas concurrentes. Malo: - Demasiado pesado para el core actual. - Requiere adoptar modelo Effect. Qué copiar a futuro: - Cola bounded. - Políticas de saturación. - Replay opcional. ### XState Actors Bueno: - Eventos como mensajes tipados. - Estado privado. - Comunicación explícita. Malo: - Demasiado grande para un bus común. Qué copiar: - Los eventos describen hechos, no instrucciones. ## Diseño Propuesto Directorio: ```txt src/arts/buss/ index.ts consts.ts types.ts errors.ts engine-bus.ts matching.ts diagnostics.ts README.md test/ ``` Si necesitamos capa reactiva: ```txt src/arts/buss/ index.ts active-bus.svelte.ts types.ts README.md test/ ``` La primera versión vive en `arts/buss` aunque sea pura y no use Svelte, porque expone `createEngineBus()` y ciclo de vida. `libs` queda reservado para primitivas compartidas sin factory raíz. ## Naming Decisión: ```txt Directorio: buss Nombre público: Bus Factory: createEngineBus() Tipo raíz: EngineBus ``` Claude propone `bus/` porque `buss` puede leerse como typo. La objeción es válida, pero la decisión recomendada para este framework es mantener `buss` como directorio por coherencia con los artefactos de cuatro letras y exponer siempre `Bus` en la superficie pública. El usuario no debería escribir `Buss` salvo al importar desde la ruta interna. ```ts import { createEngineBus } from '$buss'; const Bus = createEngineBus(); ``` No usar: ```ts createEngineBuss() EngineBuss ``` Tampoco usar dentro de un módulo: ```ts const Bus = createEngineBus(); // mal en un artefacto normal ``` La creación directa de `createEngineBus()` queda para: ```txt - createActiveApp(), que crea App.Bus; - tests unitarios aislados; - servicios o herramientas que no viven dentro de una App activa. ``` ## Contrato Base ```ts export type BusEventMap = object; export interface BusEnvelope { readonly id: string; readonly type: TType; readonly payload: TPayload; readonly at: number; readonly source: string; readonly correlationId?: string; readonly causationId?: string; readonly context?: Readonly>; readonly tags?: readonly string[]; } export interface BusListenerFailure { readonly listenerId?: string; readonly type: string; readonly error: unknown; } export interface BusPublishResult { readonly envelope: BusEnvelope; readonly errors: readonly BusListenerFailure[]; } export type BusListener = ( event: BusEnvelope, context: BusListenerContext ) => void | Promise; export interface BusListenerContext { readonly signal: AbortSignal; readonly logger?: Logger; } export interface EngineBus { publish( type: TType, payload: TEvents[TType], options?: BusPublishOptions ): BusPublishResult; publishAsync( type: TType, payload: TEvents[TType], options?: BusPublishOptions ): Promise>; on( type: TType, listener: BusListener, options?: BusListenOptions ): BusSubscription; onAny(listener: BusAnyListener, options?: BusListenOptions): BusSubscription; once( type: TType, listener: BusListener, options?: BusListenOptions ): BusSubscription; listenerCount(type?: keyof TEvents & string): number; _clearForTesting(type?: keyof TEvents & string): void; dispose(): void; } ``` ### Por Qué `context` Y No `tenantId` / `actorId` El envelope pertenece a `arts/buss`, por tanto no debe acoplarse a un modelo concreto de tenancy, sesión o usuario. `tenantId`, `actorId`, `locale`, `permissionHash` y similares son claves convencionales dentro de `context`. Ejemplo: ```ts Bus.publish(APP_EVENT_USER_IDENTITY_CHANGED, payload, { context: { tenantId, previousActorId, nextActorId, permissionHash } }); ``` ## Opciones ```ts export interface BusPublishOptions { readonly source?: string; readonly correlationId?: string; readonly causationId?: string; readonly context?: Readonly>; readonly tags?: readonly string[]; readonly listenerErrorMode?: BusListenerErrorMode; } export interface BusListenOptions { readonly signal?: AbortSignal; readonly once?: boolean; readonly id?: string; } export type BusListenerErrorMode = 'log-and-continue' | 'throw' | 'collect'; export interface EngineBusOptions { readonly logger?: Logger; readonly clock?: { now(): number }; readonly idFactory?: () => string; readonly maxListenersPerEvent?: number; readonly listenerErrorMode?: BusListenerErrorMode; } ``` Decisiones v0: - Sin `priority` en `BusListenOptions`. - Orden por registro. - `maxListenersPerEvent` default: `32`. - `_clearForTesting()` existe para tests, no como API normal de producción. ## Eventos Como Hechos Correcto: ```ts Bus.publish(SESS_EVENT_IDENTITY_CHANGED, { previousActorId: 'actor-ada', nextActorId: 'actor-linus' }); ``` Incorrecto: ```ts Bus.publish('cache.clear.now', {}); Bus.publish('connection.reauthenticate.now', {}); ``` Los eventos deben decir qué ha pasado, no qué otro módulo tiene que hacer. ## Constantes No debe haber magic strings: ```ts // arts/buss/consts.ts export const BUS_EVENT_ALL = '*'; export const BUS_LISTENER_ERROR_MODE_COLLECT = 'collect'; // libs/aapp/events.ts export const APP_EVENT_USER_IDENTITY_CHANGED = 'app.user.identity.changed'; // arts/sess/consts.ts export const SESS_EVENT_CHANGED = 'sess.changed'; export const SESS_EVENT_REVOKED = 'sess.revoked'; ``` Convención obligatoria: ```txt El símbolo exportado es UPPER_SNAKE_CASE y describe el dueño. El valor puede ser dot-case porque es el nombre serializable del evento. ``` Correcto: ```ts Bus.publish(APP_EVENT_USER_IDENTITY_CHANGED, payload); Bus.on(SESS_EVENT_CHANGED, listener); ``` Incorrecto: ```ts Bus.publish('app.user.identity.changed', payload); Bus.on('sess.changed', listener); ``` Las constantes se centralizan por dueño, no todas dentro del bus: ```txt BUS_* -> contrato mecánico del bus APP_EVENT_* -> contrato público de aplicación SESS_EVENT_* -> contrato privado de sess AUTH_EVENT_* -> contrato privado de auth ... ``` ## onAny `onAny` es una herramienta de observabilidad, testing y devtools. Uso correcto: ```ts Bus.onAny((event) => { App.Logger.debug('buss', event.type, { context: { envelope: event } }); }); ``` Uso incorrecto: ```ts Bus.onAny((event) => { if (event.type === APP_EVENT_USER_IDENTITY_CHANGED) { App.Cache.clear(); } }); ``` La lógica de negocio debe estar en listeners concretos del consumidor o en servicios explícitos, no en `onAny`. ## Integración Con aapp `aapp` debería crear un bus por aplicación: ```ts const Bus = createEngineBus({ logger: Logger, clock: Timers.clock }); ``` Y exponerlo: ```ts App.Bus ``` Los artefactos reciben el bus opcionalmente: ```ts createActiveSession({ logger, bus: Bus }); createActiveConnections({ logger, bus: Bus }); createActiveCache({ logger, bus: Bus }); ``` La regla es centralización: una app, un `App.Bus`. Si un módulo necesita emitir hechos, recibe un `EventPublisher`; si necesita reaccionar, recibe un `AppEventBus` o se configura desde su factory. Regla de implementación para `sess`: ```txt createEngineSession({ bus }) publica sess.* desde el engine puro. createActiveSession({ bus }) publica sess.* desde el wrapper activo, después de sincronizar current/generation con $state. ``` Ese matiz evita que consumidores reactivos como `conn` reciban `app.user.identity.changed` y lean todavía el snapshot anterior de `ActiveSession.current` dentro del mismo tick. Pero los módulos no deben depender de `aapp`. Solo aceptan una interfaz mínima: ```ts interface EventPublisher { publish( type: TType, payload: TEvents[TType], options?: BusPublishOptions ): BusPublishResult; } ``` ## Modelo De Coordinación: Traducción vs Reacción El bus por sí mismo reduce acoplamiento porque convierte dependencias directas entre módulos en hechos públicos. Pero un translator default demasiado prescriptivo puede reintroducir acoplamiento desde `aapp`. Ejemplo peligroso: ```txt aapp escucha sess.changed aapp ejecuta Permissions.invalidate() + Cache.clear() + Conn.reauthenticateAll() ``` Aunque `sess` no conozca `cache`, `permissions` ni `conn`, `aapp` estaría asumiendo cómo deben comportarse todos los consumidores. Eso no escala: - una app puede no tener sesión; - una app puede no crear `Permissions`; - una app puede querer conservar cache pública; - una app puede tener cache segmentada que no depende de identidad; - una app puede querer cerrar conexiones en vez de reautenticarlas; - un plugin externo puede querer reaccionar sin tocar `aapp`. Por tanto hay dos capas separadas: ```txt Capa 1: traducción aapp traduce eventos de módulo -> app.* Capa 2: reacción cada consumidor decide si escucha app.* y qué side-effect ejecuta ``` ### Config De App: Translators `createActiveApp().orchestration` solo decide qué traductores automáticos corren. No decide qué hace cache, permissions o connections. ```ts createActiveApp({ orchestration: 'standard' }); ``` ```ts createActiveApp({ orchestration: 'silent' }); ``` ```ts createActiveApp({ orchestration: ['identity', 'tenant-switched'] }); ``` Tipos aproximados: ```ts export type ActiveAppOrchestrationPreset = false | 'silent' | 'standard'; export type ActiveAppOrchestrationTranslator = | 'identity' | 'permissions-refresh' | 'tenant-switched' | 'connectivity' | 'dispose'; export type ActiveAppOrchestrationOptions = | ActiveAppOrchestrationPreset | readonly ActiveAppOrchestrationTranslator[]; ``` `createActiveApp()` equivale a translators `standard`: publica eventos `app.*` útiles por defecto, pero no provoca side-effects destructivos porque los consumidores no reaccionan automáticamente salvo que se configure su propio `auto*On`. ### Config De Consumidores: Reacciones Los efectos destructivos viven en el consumidor: ```ts const App = createActiveApp({ orchestration: 'standard', cache: { autoInvalidateOn: 'standard' }, permissions: { endpoint: '/api/permissions', autoInvalidateOn: 'standard' }, connections: { autoReauthOn: 'standard' } }); ``` Y también se pueden activar por factory: ```ts const Permissions = App.createActivePermissions({ endpoint: '/api/permissions', autoInvalidateOn: ['userIdentityChange'] }); const Connections = App.createActiveConnections({ autoReauthOn: 'standard' }); ``` ### Modos De App | Modo | Traduce a `app.*` | Side-effects | Uso | | --- | --- | --- | --- | | `createActiveApp()` | sí, preset `standard` | ninguno por sí mismo | Apps que quieren hechos públicos sin reacciones automáticas | | `orchestration: 'standard'` | sí, preset cerrado | ninguno por sí mismo | Apps normales, getting started | | `orchestration: ['identity']` | solo los traductores indicados | ninguno por sí mismo | Tests/producción con control fino | | `orchestration: 'silent'` | no | ninguno | Tests que publican `app.*` manualmente | ### Translators `standard` `'standard'` debe estar documentado como lista cerrada. No puede ser magia. | Translator | Publica | | --- | --- | | `identity` | `app.user.identity.changed` desde eventos de `sess`/`auth` | | `permissions-refresh` | `app.permissions.refresh` | | `tenant-switched` | `app.tenant.switched` | | `connectivity` | `app.connectivity.changed` | | `dispose` | `app.dispose.starting` | ### Reacciones `standard` Por Consumidor | Consumidor | Config | Reacciona a | | --- | --- | --- | | `cach` | `autoInvalidateOn: 'standard'` | `userIdentityChange`, `tenantSwitched` | | `perm` | `autoInvalidateOn: 'standard'` | `userIdentityChange`, `permissionsRefresh`, `tenantSwitched` | | `conn` | `autoReauthOn: 'standard'` | `userIdentityChange` | `conn.autoReauthOn` es opt-in porque puede cerrar o reautenticar sockets, perder mensajes en vuelo si el transporte no bufferiza, producir un blink en chat/realtime o disparar reconexiones masivas. Para `conn`, `autoReauthOn` no basta por sí solo. La registry escucha el evento de identidad, pero cada conexión debe activar `session.enabled` y definir `auth` para que exista una credencial nueva que enviar. Sin `auth`, el evento queda como señal observable o debe resolverse con reconnect/disconnect manual. ### App Events Sin Credenciales Los eventos de aplicación son contrato público. Un día pueden acabar en logs, devtools, tracing, outbox o dumps de auditoría. Por tanto: ```txt App events nunca contienen credenciales. ``` Protecciones recomendadas: - tipos de payload cerrados: `AppUserIdentityChangePayload` solo puede contener `previousActorId`, `nextActorId`, `tenantId`, `cause`, `generation`, etc.; - lint/check en DEV antes de publicar: advertir si aparecen keys como `token`, `secret`, `password`, `authorization`, o valores con forma de JWT; - documentación explícita: si un consumidor necesita algo sensible, recibe `correlationId` y resuelve contra `App.Sess` o contra su propio backend. ### Decisión Adoptada Para v0.1 - `createActiveApp()` crea `App.Bus` siempre. - `createActiveApp()` publica/traduce eventos de aplicación con preset `standard`. - `orchestration: 'silent'` desactiva traductores automáticos. - Los side-effects destructivos son opt-in en cada consumidor. - Los consumidores escuchan `app.*`, no eventos internos de otros módulos. - Los payloads `app.*` no contienen credenciales. - `APP_EVENT_*` vive fuera de `arts/buss`; `buss` no conoce eventos de dominio. ## Orden De Bootstrap Para no perder eventos tempranos: ```txt 1. crear Logger 2. crear Timers 3. crear Bus 4. crear roots always-present 5. cablear translators 6. exponer factories que pueden emitir eventos ``` Los translators deben estar registrados antes de que se creen sesiones, auth clients o conexiones que puedan publicar eventos. ## Orden De Teardown En `App.dispose()`: ```txt 1. desactivar factories/active modules que puedan emitir 2. desmontar translators 3. disponer Bus 4. disponer roots always-present ``` Debe haber test específico que verifique que un evento emitido durante dispose no dispara listeners contra servicios ya disposed. ## Translators Los traductores viven en integraciones de `aapp`: ```txt src/arts/aapp/integrations/session-translator.ts src/arts/aapp/integrations/auth-translator.ts src/arts/aapp/integrations/permission-translator.ts ``` Ejemplo: ```ts export function wireSessionTranslator(bus: EngineBus): () => void { const subscription = bus.on(SESS_EVENT_CHANGED, (event) => { bus.publish(APP_EVENT_USER_IDENTITY_CHANGED, { ...event.payload, cause: mapSessEventToCause(event.payload.event) }); }); return () => subscription.unsubscribe(); } ``` Un translator debe ser fino: ```txt X pasó -> publico app.Y ``` No debe convertirse en servicio de negocio: ```txt X pasó -> leo cinco estados -> decido reglas de negocio -> mutaciones complejas ``` Si aparece lógica compleja, debe moverse a un servicio o engine específico. ## Política De Errores Semántica cerrada: ```txt publish: - ejecuta listeners síncronos en orden - captura errores - devuelve { envelope, errors } - si listenerErrorMode === 'throw', lanza BusAggregateListenerError publishAsync: - ejecuta listeners en orden - espera listeners async - devuelve { envelope, errors } - si listenerErrorMode === 'throw', lanza BusAggregateListenerError ``` Modos: ```ts type BusListenerErrorMode = 'log-and-continue' | 'throw' | 'collect'; ``` Comportamiento: ```txt log-and-continue: - loggea cada error - continúa - devuelve errors collect: - no loggea por defecto - continúa - devuelve errors throw: - acumula errores - detiene según política de implementación - lanza BusAggregateListenerError ``` Default recomendado: ```txt runtime: log-and-continue tests: throw ``` `listenerErrorMode` puede sobrescribirse por publicación: ```ts const result = Bus.publish(SESS_EVENT_CHANGED, payload, { listenerErrorMode: 'collect' }); ``` Esto permite que el bus tenga un default operativo (`log-and-continue`) y que tests o flujos puntuales inspeccionen errores sin producir logs ni lanzar. ## Orden El orden debe ser determinista: ```txt 1. listeners específicos en orden de registro 2. once se elimina antes de invocar 3. onAny después de listeners específicos ``` No hay `priority` en v0. Las prioridades numéricas son un footgun porque crean acoplamiento invisible entre listeners. Si v1 necesita orden especial, debe ser con fases nombradas, no con números libres. Ejemplo futuro aceptable: ```ts phase: 'before-default' | 'default' | 'after-default' ``` ## Síncrono vs Async Dos métodos separados: ```ts Bus.publish(...); await Bus.publishAsync(...); ``` `publish()` se usa para estado interno inmediato. `publishAsync()` se usa para workflows que pueden esperar: - auditoría remota; - outbox; - persistencia server-side; - handlers externos; - integraciones de plugins. No mezclar ambas semánticas en un único `emit`. ## Observabilidad El bus debe poder producir diagnósticos: ```txt buss.event.published buss.listener.started buss.listener.completed buss.listener.failed buss.listener.cancelled buss.listener.leak_warning ``` Con constantes full-prefix: ```ts export const BUS_DIAGNOSTIC_EVENTS = { EVENT_PUBLISHED: 'buss.event.published', LISTENER_STARTED: 'buss.listener.started', LISTENER_COMPLETED: 'buss.listener.completed', LISTENER_FAILED: 'buss.listener.failed', LISTENER_CANCELLED: 'buss.listener.cancelled', LISTENER_LEAK_WARNING: 'buss.listener.leak_warning' } as const; ``` Debe usar `Logger` desde `libs/logr`, nunca acoplarse a `arts/logr`. El envelope debe poder serializarse 1:1 en un log: ```ts Logger.info(BUSS_LOG_CATEGORY, envelope.type, { context: { envelope } }); ``` ## API Surface Stability Desde `0.1.x` se considera API pública estable: - `EngineBus` - `BusEnvelope` - `BusPublishResult` - `BusEventMap` - `BusListener` - `BusSubscription` - semántica de `publish` / `publishAsync` - orden de ejecución documentado - política de errores documentada Puede iterar sin romper: - diagnósticos internos; - opciones nuevas opcionales; - helpers de testing; - adapters futuros; - active wrapper. Los `APP_EVENT_*` tienen su propia estabilidad en `libs/aapp/events.ts`. Rompe compatibilidad: - renombrar eventos canónicos; - quitar campos del envelope; - cambiar el orden de ejecución; - cambiar la semántica de errores; - convertir `publish` síncrono en async. ## Bundle Budget `buss` será always-present si lo crea `aapp`, así que su core debe ser pequeño. Target: ```txt arts/buss core <= 3 KB gzip ``` Este límite aplica solo al core `arts/buss`, no a translators de `aapp`, tests, páginas demo ni documentación. Debe añadirse al smoke test de bundle cuando se implemente. ## Relación Con SemanticEngine / Taxis `SemanticEngine` y `buss` pueden parecer registry + dispatch, pero pertenecen a capas distintas. | Eje | SemanticEngine | buss | | --- | --- | --- | | Capa | Perceptiva / UI primitives | Dominio / runtime artifacts | | Granularidad | Componente e interacción | Hecho de aplicación | | Canales | Visual, sound, vibra, motion | Listeners y translators | | Bloqueo | Visual hold / transition | `publish` sync, async opcional | | Vocabulario | Verbos perceptivos | Hechos de dominio | | Vida del evento | Transiente | Envelope/loggable | No deben fusionarse. Sí pueden compartir ideas: - `id` - `at` - `source` - correlación Pero `SemanticEngine` sirve a la experiencia perceptiva y `buss` a la composición de artefactos. ## Riesgos ### God Object Si todo evento del runtime pasa por `buss`, deja de ser bus y se vuelve estado central. La frontera inter/intra módulo debe aplicarse de forma estricta. ### Translators Con Lógica De Negocio Los translators deben traducir hechos, no absorber decisiones de dominio. ### Drift Entre Envelope Y Logger `buss` y `logr` deben alinearse. El envelope no debe inventar otra taxonomía paralela de observabilidad. ### onAny Como Middleware Oculto `onAny` solo observabilidad/devtools/testing. No policy. ### clear Destructivo No exponer `clear(type?)` como API normal. Usar `_clearForTesting`. ## Test Compuesto Objetivo Caso de regresión principal: ```txt 1. Usuario Ada inicia sesión. 2. Chat abre conexión con token Ada. 3. Cache guarda presencia/proyecto para Ada. 4. Perm cachea decisión de Ada. 5. Se adopta sesión Linus. 6. Bus emite `sess.changed`. 7. Translator de App emite `app.user.identity.changed`. 8. Consumidores opt-in reaccionan: - `perm.autoInvalidateOn` limpia decisiones - `cach.autoInvalidateOn` limpia cache - `conn.autoReauthOn` reautentica conexiones con `session.enabled` y `auth` 9. Chat envía auth frame con token Linus. 10. Ningún frame posterior contiene token Ada. 11. Cache refetchea para Linus. ``` Este test debe verificar ambas capas: traducción `sess.* -> app.*` y reacción opt-in por consumidor. ## Roadmap ### Estado actual - `arts/buss` existe como engine mecánico. - `App.Bus` se crea siempre en `createActiveApp()`. - `APP_EVENT_*` vive en `src/libs/aapp/events.ts`. - `SESS_EVENT_*` vive en `src/arts/sess/consts.ts`. - `sess` publica `sess.*` cuando recibe bus. - `aapp` traduce `sess.changed -> app.user.identity.changed` con `wireSessionTranslator`. - `cach`, `perm` y `conn` reaccionan por opt-in desde sus propias factories. - El test compuesto de cambio de usuario/chat/cache/permisos cubre la cadena completa y evita credenciales antiguas después del cambio de actor. ### v0 - `arts/buss` - `createEngineBus` - `publish` - `publishAsync` - `on` - `once` - `onAny` - `listenerCount` - `_clearForTesting` - `dispose` - `AbortSignal` - `Logger` - `BusPublishResult` - `BusAggregateListenerError` - tests unitarios ### v0.1 - Integración en `aapp` - Translator `sess.* -> app.user.identity.changed` - Reacciones opt-in en `cach`, `perm` y `conn` - Translator `auth.* -> app.user.identity.changed` cuando auth publique eventos propios - Mover `APP_EVENT_*` a `src/libs/aapp/events.ts` - Mover `SESS_EVENT_*` a `arts/sess`, fuera de `arts/buss` - Orden de bootstrap documentado y testeado - Orden de teardown documentado y testeado - Test de dispose: evento emitido durante teardown no dispara contra servicios ya disposed - Test compuesto cambio de usuario/chat/cache - Bundle smoke para `buss <= 3 KB gzip` ### v0.2 - Active wrapper solo si hace falta: - `lastEvent` - `eventCount` - `listenerCount` - `onChange` - Página `/test/buss` - Devtools/debug panel ### v1 - `waitFor` - `condition` - `debounce` - `takeLatest` - fases nombradas si hay necesidad real - replay opcional - bounded queue opcional - adapter server/outbox ## Qué No Meter Al Principio - No decorators. - No dependencia externa. - No RxJS. - No Effect. - No wildcards complejos tipo `**` en v0. - No bus distribuido. - No persistence/outbox en v0. - No reemplazar `Logger`. - No convertir todos los logs en eventos. - No eventos privados intra-módulo en el bus. - No `priority` numérico. - No lógica de negocio compleja en translators. ## Decisión Recomendada Implementar `buss` como núcleo pequeño: ```txt Core pequeño, eventos tipados, envelopes ricos, ejecución determinista, translators en aapp, reacciones en consumidores y cero acoplamiento entre artefactos. ``` Esto corrige el problema real del ecosistema: los módulos ya funcionan, pero las transiciones entre módulos necesitan una gramática común, estable y testeable.