Comunicación
Las apps nunca se importan entre sí. Todo cruza por un sustrato que vive en la ventana del shell y se comporta igual en todos los modos de sandbox.
┌─ App A (React) ───┐ ┌─ App B (Vue) ─────┐ │ wu.emit('cart:*') │──── bus ────►│ wu.on('cart:*') │ │ wu.request('c:t') │──── canal ──►│ wu.handle('c:t') │ │ ◄──────────│─ respuesta ──│────────── │ │ wu.store.set(…) │──── store ──►│ wu.store.on(…) │ │ wu.provide('cart')│── contrato ─►│ wu.consume('cart')│ └───────────────────┘ └───────────────────┘ ▲ ▲ └──── wu.update(name, props) ─────┘ (enviadas por el shell)Qué falta deliberadamente
Sección titulada «Qué falta deliberadamente»No hay negociación de versiones para los módulos de framework. Conviene ser exacto aquí, porque se suele contar mal en las dos direcciones.
Wu sí reutiliza el runtime del framework si está en el global. Seis adapters —React, Vue, Preact, Solid, Angular y Alpine— lo comprueban antes de importar nada:
if (typeof window !== 'undefined' && window.React && window.ReactDOM) { adapterState.React = window.React; // reutiliza el del shell ... return true;}// solo si no está, importa el suyoconst [React, ReactDOMClient] = await Promise.all([ import('react'), import('react-dom/client'),]);Así que si el shell expone window.React y las apps marcan React como
external en su build, hay una sola copia en la página. La afirmación de
que “N apps son N Reacts” solo es cierta cuando nadie expone el global.
Lo que Wu no tiene es la otra mitad: nada compara versiones. La condición
es window.React && window.ReactDOM y ya está. Si el shell publica React 18 y
una app se compiló contra React 19, esa app se monta contra el 18 sin un solo
aviso, y el fallo aparece después — al tocar una API que en esa versión no
existe. El requiredVersion de Module Federation existe precisamente para
cazar eso.
| Wu | Module Federation | |
|---|---|---|
| Reutilizar el runtime | Sí, si el shell lo expone | Sí, automático |
| Cómo se configura | Global + external en cada build |
Declarativo en el config |
| Versiones distintas | No se detecta, rompe en runtime | Negocia o avisa |
| Sin nadie exponiendo | Cada app trae la suya | Igual |
A veces se confunden los contratos de capacidades con un sustituto de todo
esto. No lo son: comparten objetos vivos, no módulos. wu.consume('cart', '^2.0') te da un proxy a un objeto que creó la app del carrito — no te da la
instancia de react de esa app, y no te ahorra ningún byte. Lo que sí hace es
negociar semver de verdad, pero para capacidades entre apps, que es otro
problema.
1. Bus de eventos — “ha pasado algo”
Sección titulada «1. Bus de eventos — “ha pasado algo”»wu.emit('cart:item-added', { sku: 'SKU-42' });
const off = wu.on('cart:*', (event) => { event.name; // 'cart:item-added' event.data; // { sku: 'SKU-42' } event.appName; // quién lo emitió event.verified; // ¿estaba registrado el emisor? event.timestamp;});off();| Método | Notas |
|---|---|
emit(name, data, opts) |
Devuelve false si se rechaza, true si se despacha |
on(name, cb) |
Devuelve una función para desuscribirse |
off(name, cb) |
|
once(name, cb) |
Devuelve una función para desuscribirse |
eventBus.replay(pattern, cb) |
Ejecuta cb por cada evento pasado que coincida |
eventBus.tap(fn) |
Manguera: todos los eventos, sin comodines ni autorización |
eventBus.clearHistory(pattern) |
Sin argumento borra todo |
Opciones de emit: { appName, timestamp, meta, token, history }. Pasar
history: false despacha a los listeners sin registrar el evento.
Comodines
Sección titulada «Comodines»* coincide con cualquier carácter, incluidos : y .:
wu.on('cart:*', cb); // cart:added, cart:removed, cart:sync:donewu.on('*', cb); // todoUn patrón sin * nunca pasa por la vía de los comodines, así que un listener
exacto nunca se dispara dos veces.
Fíjate en la diferencia con el store, donde * coincide con un solo
segmento. Son dos comparadores distintos con semánticas distintas.
El historial está limitado a 100 eventos y solo se registra si el replay está
habilitado (lo está por defecto) y el emit no pasó history: false.
wu.eventBus.replay('user:*', (event) => { // se llama una vez por cada evento coincidente ya en el historial});Útil para una app que se monta tarde y necesita ponerse al día con el estado que se perdió.
strictMode y autorización
Sección titulada «strictMode y autorización»strictMode está desactivado por defecto, siempre. No hay ninguna
comprobación de NODE_ENV ni heurística por nombre de host: un bundle de
navegador no puede deducir el entorno de forma fiable en tiempo de ejecución, así
que el bus no lo intenta. Se pide explícitamente, en el init o sobre el bus:
await wu.init({ strictEvents: true, apps: [ /* … */ ] });
wu.eventBus.enableStrictMode();wu.eventBus.disableStrictMode();wu.eventBus.configure({ strictMode: true, validateOrigin: true });Cuando está desactivado, el primer emit() registra un aviso único indicándolo.
Con strictMode activo, emit() se valida:
- Los eventos de sistema siempre pasan: los nombres que empiezan por
wu:,system:oapp:. - Los subsistemas internos (
wu-core,wu-ai,wu-mcp-bridge,plugin,wu-event-bus) deben presentar su token interno. Reclamar uno de esos nombres sin el token se rechaza: los nombres no se pueden suplantar. - Todo lo demás debe ser una app registrada, con un token coincidente si se proporcionó uno, y un permiso que cubra el nombre del evento.
const token = wu.eventBus.registerApp('cart'); // los permisos son ['*'] por defectowu.eventBus.registerApp('cart', { permissions: ['cart:*'] });wu.eventBus.unregisterApp('cart');Un emit rechazado no lanza error. Incrementa un contador, registra
Event rejected: <name> from <app> (unauthorized) y devuelve false. No se
añade nada al historial, no se disparan taps, no se ejecuta ningún listener.
Comprueba el valor de retorno si necesitas saberlo.
Los tokens se generan con WebCrypto: crypto.randomUUID(), con respaldo en
crypto.getRandomValues (el escalón que importa en despliegues internos por HTTP
plano, donde randomUUID no existe), y solo se llega a Math.random() si
WebCrypto falta por completo — con un aviso cuando ocurre.
2. Petición-respuesta — “necesito una respuesta”
Sección titulada «2. Petición-respuesta — “necesito una respuesta”»// Quien atiende: un solo responsable por canalconst baja = wu.handle('users:page', async ({ page }) => { const res = await fetch(`/api/users?page=${page}`); return res.json(); // esto es lo que recibe quien preguntó});
// Quien preguntaconst usuarios = await wu.request('users:page', { page: 3 });
wu.hasHandler('users:page'); // ¿hay alguien atendiendo?baja();| Método | Notas |
|---|---|
handle(canal, fn) |
Devuelve una función de baja. Un segundo handle en el mismo canal sustituye al primero y avisa |
request(canal, data, opts) |
Promise con la respuesta. opts.timeout en ms; sin él, la espera no tiene límite |
hasHandler(canal) |
boolean |
No tienen import nombrado: se usan como wu.request / wu.handle /
wu.hasHandler.
El evento avisa; el canal pregunta. Un evento sin oyente no da error — la vista se queda cargando y no hay nada en la consola. Un canal que nadie atiende revienta al primer intento y te dice qué canales sí están abiertos:
[WuEventBus] nobody is handling 'users:page'. Open channels: cart:total.Did the app that calls handle() mount?El handler se invoca directamente, en la misma pila de llamadas, no dando la
vuelta por el bus. Las apps comparten una única instancia de wu en la página, así
que no hay nada que serializar — y si el que responde falla, el error llega con
sus propios frames dentro en vez de un { ok: false } sin origen. Se emiten
igual <canal>:request y <canal>:response como traza para devtools y la línea
de tiempo, pero la respuesta no viaja por ahí.
Los handlers no están atados al ciclo de vida. A diferencia de las
capacidades, nadie los revoca cuando la app que llamó a handle() se desmonta:
guarda la función de baja y llámala en tu unmount.
Mira Eventos → handle() / request().
3. Store — “lee esto más tarde”
Sección titulada «3. Store — “lee esto más tarde”»wu.store.set('user.name', 'Luis'); // → número de secuenciawu.store.get('user.name'); // 'Luis'wu.store.get(); // el objeto de estado completo
const off = wu.store.on('user.*', ({ path, value }) => { … });wu.store.batch({ 'a.b': 1, 'c': 2 }); // → array de números de secuenciaRutas con puntos. Los niveles intermedios que falten se crean al escribir y
se acceden con encadenamiento opcional al leer, así que get('a.b.c') sobre un
store vacío devuelve undefined en vez de lanzar un error.
La notificación es asíncrona: los suscriptores se llaman en una microtarea
después de la escritura, no de forma síncrona dentro de set(). Los listeners de
ruta exacta reciben (value, path); la escritura además burbujea hacia los
ancestros, así que escribir user.profile.name también notifica a
user.profile y a user. Los listeners de patrón reciben un único objeto
{ path, value }.
Los comodines coinciden con un solo segmento. user.* coincide con
user.name pero no con user.profile.name. Es lo contrario que en el bus de
eventos. '*' a secas coincide con todo.
set() devuelve un número de secuencia monótono de un búfer circular de 256
huecos (ese búfer es de donde leen el timeline y la sincronización CRDT). Los
huecos están preasignados y se reutilizan, así que las escrituras no reservan
memoria.
La contaminación de prototipos lanza error. __proto__, constructor y
prototype se rechazan como segmentos de ruta:
wu.store.set('__proto__.polluted', 1);// Error: [WuStore] Unsafe key in path: "__proto__" (path: "__proto__.polluted")get() deliberadamente no lanza error con esas mismas claves: devuelve
undefined, ya que una lectura no puede contaminar nada.
Sincronización entre pestañas y multiusuario
Sección titulada «Sincronización entre pestañas y multiusuario»const handle = wu.store.sync({ transport: 'broadcast', room: 'default' });await handle.ready();handle.status(); // { connected, site, lamport, peers, tracked, sent, received, … }handle.stop();Transportes: 'broadcast' (BroadcastChannel, entre pestañas), una instancia de
WebSocket o una URL wss://, o el tuyo propio con { send, onMessage, close }.
La resolución de conflictos es el último que escribe gana, por ruta, ordenada por un reloj de Lamport con el id de sitio como desempate, de modo que las réplicas convergen sin importar el orden de entrega. Quien se une tarde intercambia una instantánea. De los valores no serializables (BigInt, Function, Symbol) se avisa y se contabilizan como descartados, nunca se tragan en silencio. Las escrituras remotas pasan por la misma protección contra contaminación de prototipos y una operación rechazada se cuenta como ignorada.
Solo puede haber una sincronización activa por store; una segunda llamada a
sync() avisa y devuelve el handle existente.
4. Contratos de capacidades — “llama a esta función”
Sección titulada «4. Contratos de capacidades — “llama a esta función”»Los eventos son de disparar y olvidar; el store es estado. Cuando una app necesita llamar a otra, usa un contrato.
// Proveedorconst handle = wu.provide('cart', cartApi, { version: '2.1.0', shape: { add: 'function', remove: 'function' }, app: 'cart', // se revoca automáticamente cuando esta app se desmonta});
// Consumidorconst cart = wu.consume('cart', '^2.0');cart.add('SKU-42'); // se vuelve a resolver en vivo en cada accesoprovide() lanza un error si la implementación no cumple la shape declarada.
La presencia se comprueba recorriendo la cadena de prototipos de la propia
implementación — así que una instancia de clase cuenta —, pero el recorrido se
detiene antes de Object.prototype, de modo que un {} vacío no puede satisfacer
un contrato heredando toString.
consume() devuelve un proxy vivo, no una instantánea. Cada acceso a una
propiedad vuelve a resolver el proveedor, así que cambiar de proveedor es
invisible para los consumidores. Lanza el error en el acceso, no en
consume():
[WuContracts] no provider for capability 'cart'. Did the providing app mountand call wu.provide('cart', …)?
[WuContracts] capability 'cart@1.4.0' does not satisfy '^2.0'.Detalles que conviene conocer:
- La identidad de los métodos es estable.
proxy.fn === proxy.fn, así que los métodos enlazados funcionan como dependencias de efectos de React y como referencias paraaddEventListener/removeEventListener. Un cambio de proveedor produce un enlace nuevo. - El proxy es de solo lectura. Las escrituras se ignoran con un aviso.
- Nunca es thenable.
proxy.thensiempre devuelveundefined, así queawait consume(...)no puede quedarse colgado por accidente. Un método de la capacidad que se llame literalmentethenes inaccesible a través del proxy. - Sondas que no lanzan error:
proxy.__wuAvailable,proxy.__wuCapability,proxy.__wuRangeywu.contracts.has(name, range).
Los consumidores tardíos pueden esperar:
const cart = await wu.consume('cart', '^2.0', { wait: true, timeout: 5000 });La revocación al desmontar la dirige el core con el nombre de app verificado
por el framework, no el evento de bus app:unmounted — porque los eventos
app: son de confianza del sistema y, si no, una app rival podría falsificarlos
para revocar las capacidades de una competidora.
Eventos emitidos: wu:capability:provided y wu:capability:revoked. Las
capacidades vivas aparecen en wu.inspect().capabilities.
5. Props en vivo — del shell a la app
Sección titulada «5. Props en vivo — del shell a la app»await wu.update('cart', { currency: 'EUR' }); // → booleanEnvía props a una app montada sin remontarla, llamando al slot opcional
update(container, props) de la app. Los adaptadores lo exponen solo cuando su
framework puede volver a renderizar en el sitio.
Honestamente no hace nada cuando no puede funcionar: si la app no está
montada, está a mitad de desmontaje o su adaptador nunca anunció un slot
update, registra un aviso y devuelve false. Nunca lanza un error y nunca
finge. Comprueba el valor de retorno si te importa.
Mira Ciclo de vida.
Elegir un canal
Sección titulada «Elegir un canal»| Lo que necesitas | Usa |
|---|---|
| Anunciar que ha pasado algo | Evento |
| Pedir un dato y esperar la respuesta | wu.request() / wu.handle() |
| Compartir estado que otros leen después | Store |
| Dejar que otra app llame a tus funciones | Contrato de capacidad |
| Enviar props nuevas a una app montada | wu.update() |
| Mantener el estado sincronizado entre pestañas | wu.store.sync() |
| Ponerte al día con eventos que te perdiste | eventBus.replay() |
Guía aproximada: eventos para verbos, canales para preguntas, store para sustantivos, contratos para servicios.
