You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1382 lines
33 KiB

# 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<AppBusEvents>();
```
No usar:
```ts
createEngineBuss()
EngineBuss
```
Tampoco usar dentro de un módulo:
```ts
const Bus = createEngineBus<MyModuleEvents>(); // 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<TType extends string = string, TPayload = unknown> {
readonly id: string;
readonly type: TType;
readonly payload: TPayload;
readonly at: number;
readonly source: string;
readonly correlationId?: string;
readonly causationId?: string;
readonly context?: Readonly<Record<string, unknown>>;
readonly tags?: readonly string[];
}
export interface BusListenerFailure {
readonly listenerId?: string;
readonly type: string;
readonly error: unknown;
}
export interface BusPublishResult<TType extends string = string, TPayload = unknown> {
readonly envelope: BusEnvelope<TType, TPayload>;
readonly errors: readonly BusListenerFailure[];
}
export type BusListener<TPayload> = (
event: BusEnvelope<string, TPayload>,
context: BusListenerContext
) => void | Promise<void>;
export interface BusListenerContext {
readonly signal: AbortSignal;
readonly logger?: Logger;
}
export interface EngineBus<TEvents extends BusEventMap = BusEventMap> {
publish<TType extends keyof TEvents & string>(
type: TType,
payload: TEvents[TType],
options?: BusPublishOptions
): BusPublishResult<TType, TEvents[TType]>;
publishAsync<TType extends keyof TEvents & string>(
type: TType,
payload: TEvents[TType],
options?: BusPublishOptions
): Promise<BusPublishResult<TType, TEvents[TType]>>;
on<TType extends keyof TEvents & string>(
type: TType,
listener: BusListener<TEvents[TType]>,
options?: BusListenOptions
): BusSubscription;
onAny(listener: BusAnyListener<TEvents>, options?: BusListenOptions): BusSubscription;
once<TType extends keyof TEvents & string>(
type: TType,
listener: BusListener<TEvents[TType]>,
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<Record<string, unknown>>;
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<AppBusEvents>({
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<TEvents extends BusEventMap> {
publish<TType extends keyof TEvents & string>(
type: TType,
payload: TEvents[TType],
options?: BusPublishOptions
): BusPublishResult<TType, TEvents[TType]>;
}
```
## 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<AppBusEvents & SessEventMap>): () => 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.

Powered by TurnKey Linux.