Appearance
ADR-007: Tab-Scoped Job Bus via window.BautistaJobBus for Legacy PHP Bridge
Fecha: 2026-02-23 Estado: Aceptado Deciders: Architecture Team, Frontend Team
Contexto y Problema
Sistema Bautista está en migración progresiva desde vistas PHP legacy hacia React. Las vistas PHP que están fuera del árbol React necesitan feedback inmediato cuando los background jobs completan (por ejemplo, notificaciones toast). El JobNotificationsContext existente usa polling de 30 segundos, lo cual es demasiado lento para flujos legacy interactivos donde el usuario espera feedback inmediato después de disparar un job.
Escenario de riesgo sin solución:
- Usuario en vista PHP legacy dispara facturación masiva
- Job completa en ~10 segundos
JobNotificationsContextnotifica recién en el próximo ciclo de polling (hasta 30s después)- Usuario no recibe feedback oportuno y puede re-disparar el job o navegar creyendo que falló
Restricción arquitectónica clave: Las vistas PHP legacy no montan React ni tienen acceso al árbol de componentes. No pueden consumir Context ni hooks de React directamente.
Timing crítico: El script listener legacy se ejecuta en DOMContentLoaded. El bus debe estar disponible en window antes de ese evento.
Opciones Consideradas
Opción A: window.BautistaJobBus — Pub/Sub tab-scoped (SELECCIONADA)
Descripción:
JobBusse crea enmain.tsxa nivel de módulo (antes de que monte cualquier componente React)- Asignado a
window.BautistaJobBusen scope de módulo, garantiza disponibilidad antes deDOMContentLoaded - Script vanilla IIFE
job-status-listener.jscargado en cada página autenticada se suscribe al bus enDOMContentLoaded - El ID de usuario se lee desde
<meta name="bautista-user-id">para filtrar eventos del usuario actual JobBusAdapterconecta el bus con el sistema de polling React (JobNotificationsContext) como puente
Garantía de timing:
main.tsx (module scope) → window.BautistaJobBus = createJobBus()
React mount → JobBusAdapter conecta polling → bus
DOMContentLoaded → job-status-listener.js llama window.BautistaJobBus.on(...)Pros:
- Disponible globalmente sin imports ni bundler en el lado PHP
- Timing garantizado (module scope ejecuta antes de DOMContentLoaded)
- Cero dependencias externas (pub/sub trivial en memoria)
- Compatible con entornos sin HTTPS
- No requiere procesos separados ni workers del navegador
- Fácil de testear (objeto en memoria, suscripciones directas)
Contras:
- Alcance tab-scoped únicamente (sin notificación cross-tab por diseño)
- Introduce
windowglobal (punto de acoplamiento) - Requiere que
showAlertToastesté disponible globalmente en páginas legacy
Opción B: BroadcastChannel
Descripción: API nativa del navegador para comunicación cross-tab/cross-context same-origin.
Rechazada: No funciona de forma confiable en algunos contextos de iframe PHP legacy de esta aplicación. El enforcement same-origin puede fallar en configuraciones con subdominios mixtos presentes en el entorno de desarrollo. Además, no aporta beneficio real dado que el caso de uso es exclusivamente tab-scoped.
Opción C: SharedWorker
Descripción: Worker compartido entre todas las pestañas del mismo origen, con canal de mensajes dedicado.
Rechazada:
- Requiere HTTPS (no todas las páginas legacy corren sobre HTTPS en desarrollo)
- Agrega un proceso separado del navegador y complejidad operacional desproporcionada
- No aporta valor adicional para el caso de uso actual (migración progresiva tab-scoped)
- El overhead de implementación supera el beneficio en la etapa actual de migración
Opción D: Polling-only via JobNotificationsContext (30 segundos)
Descripción: No agregar bus; mantener el polling existente de 30 segundos para notificaciones legacy.
Rechazada: Latencia de 30 segundos no es aceptable para flujos interactivos legacy donde el usuario espera feedback inmediato. Degradaría la experiencia de usuario sin justificación técnica.
Decisión
Seleccionamos Opción A: window.BautistaJobBus tab-scoped.
Justificación:
- Resuelve el problema de latencia sin introducir dependencias externas ni requisitos de infraestructura
- El timing garantizado (module scope en
main.tsx) elimina race conditions con el listener legacy - Compatible con la realidad del entorno de desarrollo (HTTP sin HTTPS obligatorio)
- Apropiado para la etapa actual de migración progresiva: solución simple y reversible
Consecuencias
Positivas
- Latencia near real-time para notificaciones legacy (tan rápido como el polling actual, pero sin esperar el intervalo completo)
- Bus garantizado disponible en
DOMContentLoaded— sin race condition con el listener script - Páginas PHP que no cargan React no tienen bus — el listener guarda con
typeof window.BautistaJobBus === 'undefined' - Filtrado por user ID previene mostrar notificaciones de otro usuario cuando múltiples usuarios comparten sesión de navegador
- Alcance cross-tab no es necesario en este caso de uso y se maneja por separado via polling de
JobNotificationsContext
Negativas
- Cross-tab notification no es provista por este bus (el polling de 30s de
JobNotificationsContextcubre ese caso) showAlertToastdebe estar disponible globalmente en páginas legacy — el listener guarda contypeof showAlertToast === 'function'- Si
main.tsxno está cargado en una página (sin React), el bus no existe — comportamiento esperado y documentado
Invariantes obligatorios
- El bus se asigna a
window.BautistaJobBusANTES de cualquier mount de componente React - El listener legacy SIEMPRE verifica existencia del bus antes de suscribirse
- El ID de usuario proviene ÚNICAMENTE del
<meta name="bautista-user-id">— no de localStorage ni cookies JobBusAdapteres el único punto que publica eventos al bus desde React
Archivos Afectados
| Archivo | Tipo | Descripción |
|---|---|---|
public/ts/core/jobs/JobBus.ts | Nuevo | Implementación pub/sub tab-scoped |
public/ts/core/jobs/JobBusAdapter.ts | Nuevo | Puente entre polling React y el bus |
public/ts/main.tsx | Modificado | Asignación window.BautistaJobBus en scope de módulo |
public/js/core/job-status-listener.js | Nuevo | IIFE vanilla que suscribe al bus en páginas legacy |
public/php/components/navbar.php | Modificado | Incluye <meta name="bautista-user-id"> y carga el listener |
Referencias
- ADR-003: Polling HTTP → SSE (Fases) — estrategia de notificaciones frontend
- Architecture: Background Jobs
- Backend: Background Jobs System