@numaris-microfront/contracts
v0.2.7
Published
Eventos de dominio, contrato de montaje y manifiesto de widget de la plataforma de micro-frontends Numaris.
Downloads
922
Maintainers
Readme
@numaris-microfront/contracts
El contrato entre el Shell (el host) y un widget (un remoto de Module Federation) de la plataforma de rastreo Numaris. Si estas construyendo un widget nuevo para esta plataforma, este es el paquete que necesitas leer entero antes de escribir codigo.
Trae tres cosas:
- Los eventos de dominio con los que tu widget se comunica con los demas, sin importarlos y sin conocerlos.
- El contrato de montaje: que te pasa el Shell cuando monta tu widget.
- El esquema del manifiesto: el archivo que declara, antes de escribir codigo, que eventos tu widget provee y cuales consume.
Instalacion
npm install @numaris-microfront/contractsDepende de @numaris-microfront/event-bus (el bus subyacente) y zod (validacion de
esquemas). No depende de React ni de ninguna libreria de UI.
El modelo mental: nunca importes otro widget
La regla de arquitectura de esta plataforma es una sola: un widget nunca importa codigo de
otro widget, y nunca asume que otro widget esta presente. Toda comunicacion entre widgets
pasa por el Event Bus, con eventos de dominio (trackable.selected), nunca de UI
(tableRowClicked). Si tu widget necesita saber algo que otro widget podria proveer, te
suscribis al evento correspondiente — y si el evento nunca llega (porque ese otro widget esta
desactivado, o no existe en este despliegue), tu widget tiene que seguir funcionando con un
fallback propio. Esa es la regla 1.3, y el esquema del manifiesto (mas abajo) la hace cumplir
en validacion.
1. Los eventos de dominio (numarisBus)
import { numarisBus } from '@numaris-microfront/contracts';
// Suscribirse. Por default recibis el ultimo valor emitido, si existe (ver
// @numaris-microfront/event-bus para el detalle de la retencion).
const unsubscribe = numarisBus.on('trackable.selected', (payload, meta) => {
console.log(payload.trackableId); // string | null
});
// Emitir.
numarisBus.emit('trackable.selected', { trackableId: 'trk-42' });
unsubscribe();numarisBus es la MISMA instancia que expone @numaris-microfront/event-bus, solo que
tipada con NumarisEventMap — el mapa completo de eventos de esta plataforma. Autocompletado
y chequeo de tipos vienen gratis: emitir un evento con un payload mal formado no compila.
Catalogo de eventos (NumarisEventMap)
| Evento | Payload | Quien lo provee | Que significa |
|---|---|---|---|
| trackable.list.updated | { trackables: TrackableSummary[] } | trackable-list | El catalogo visible de la cuenta, completo. Se emite en CADA carga (inicial y cualquier refetch) — no uses esto para saber si algo cambio, solo para leer el estado actual. |
| trackable.catalog.changed | { change: 'created' \| 'updated' \| 'deleted', trackableId: string } | trackable-list | SEÑAL de que una unidad se dio de alta, se edito o se dio de baja, confirmado por el servidor. Se emite UNA vez por mutacion, nunca en cada carga. Quien lo recibe relee su propia fuente de datos — el payload no trae el catalogo. |
| trackable.selected | { trackableId: string \| null } | Cualquier widget que permita seleccionar una unidad | Que unidad esta seleccionada ahora. null es un valor legitimo: significa "ninguna". |
| trackable.focus.requested | { trackableId: string } | Cualquier widget que pueda pedir mirar de cerca una unidad | SEÑAL: "acercate a esta unidad". El mapa centra la camara y cambia el zoom. NO es lo mismo que trackable.selected y no cambia la seleccion: si querés las dos cosas, emiti los dos eventos. Sin null —no existe pedir foco sobre nada— y sin nivel de zoom en el payload: el zoom lo decide quien tiene el mapa. Consumilo con replayLast: false, o un pedido viejo te mueve la camara sola al montar. |
| poi.selected | { poiId: string \| null } | Cualquier widget que permita seleccionar un punto de interes | Que punto de interes esta seleccionado ahora. null significa "ninguno". Es HERMANO de trackable.selected y no el mismo evento: son dos espacios de ids distintos. |
| poi.catalog.changed | { change: 'created' \| 'updated' \| 'deleted', poiId: string } | poi-list | SEÑAL de que un punto de interes se dio de alta, se edito o se dio de baja. Mismo patron que trackable.catalog.changed: quien lo recibe relee su propia fuente. |
| organization.branding.updated | { organizationId: string } | El Shell | La marca de la organizacion (colores, tipografia) se guardo y ya esta aplicada. SEÑAL, no dato — los valores reales viajan por CSS variables en el documento, no en el payload. |
| command.dispatched | { deviceId: string, commandId: string, dispatchedAt: number } | commands | Un comando se envio de verdad al equipo, confirmado por el servidor (no la intencion optimista con la que un widget puede pintar antes de llamar a la red). |
TrackableSummary (la forma de cada fila en trackable.list.updated):
interface TrackableSummary {
id: string;
name: string;
icon: TrackableIcon; // union cerrada, ver TRACKABLE_ICONS mas abajo
lastSeen: number | null; // epoch en SEGUNDOS de la hora de recepcion, o null si nunca reporto
deviceId: string | null;
}Reglas para consumir un evento
- Todo consumo es opcional. Tu widget tiene que andar sin ese evento — otro widget que lo provee puede estar desactivado en este despliegue, o directamente no existir. Diseñá un fallback (normalmente: tu widget resuelve el dato con su propio fetch).
- Tu widget puede no estar montado cuando el evento se emite. La plataforma anfitriona puede tener varias pantallas y montar sólo la que se está viendo, así que el widget que provee lo que consumís puede estar vivo, contratado, y simplemente en otra. No es el caso raro: es uno de los dos normales. Un evento retenido te llega igual al montar; una señal emitida mientras no estabas, no — por eso el fallback no es opcional.
- El ultimo valor se retiene por tipo, asi que el orden en que se montan los widgets no
cambia lo que cada uno ve. Si NO queres el replay (por ejemplo, para reaccionar solo a
cambios que pasen DESPUES de montar, no al estado que ya tenias), suscribite con
{ replayLast: false }. - No inventes un evento nuevo sin agregarlo a este paquete. El mapa de eventos es el contrato compartido — un evento que solo vive en tu widget no lo puede escuchar nadie mas, y si le pones un nombre que choca con uno existente pero con otra forma de payload, rompe en runtime sin error de compilacion.
2. El contrato de montaje
Lo que el Shell le pasa a tu widget cuando lo monta, por props — la unica via de datos Shell-a-widget (nunca al reves, y nunca widget-a-widget):
import type { WidgetMountProps, PocSession } from '@numaris-microfront/contracts';
interface PocSession {
getToken(): Promise<string>; // PEDILO en el momento de usarlo, no lo guardes en una variable:
// el token expira y una copia vieja capturada en un render queda muerta
organizationId: string; // el tenant activo, ya resuelto — no lo derives de localStorage ni de la URL
userLabel: string;
}
interface WidgetMountProps {
session: PocSession;
mountMode: 'shell' | 'standalone'; // tu widget tiene que comportarse igual en los dos casos
}mountMode existe solo para diagnostico — si tu widget necesita ramificar comportamiento real
segun si esta dentro del Shell o standalone (mas alla de, por ejemplo, mostrar un header propio
en modo standalone), probablemente estas acoplando tu widget al host de una forma que no deberias.
Esas props no llegan a un componente: llegan a una funcion. Lo que tu bundle expone como
./Widget no es un componente de React, es un mount:
import type { WidgetMount, WidgetModule } from '@numaris-microfront/contracts';
type WidgetUnmount = () => void;
type WidgetMount = (container: HTMLElement, props: WidgetMountProps) => WidgetUnmount;
interface WidgetModule {
mount: WidgetMount; // lo que el Shell busca en tu modulo expuesto
}Por eso tu widget puede estar escrito con cualquier cosa que sepa pintar dentro de un
HTMLElement: lo que cruza la frontera es un nodo del DOM y datos planos, no un arbol de React.
Tres cosas que hay que respetar, y que el tipo no puede exigir:
mountes sincrono. El Shell necesita tu desmontaje en el mismo tick en que monta. Todo lo asincrono —pedir tus datos— va adentro, despues de montar. Unmountque devuelva una promesa deja tu widget vivo sin nadie que lo apague la primera vez que el host lo desmonta rapido.- El desmontaje saca tu widget de la pantalla antes de volver. Lo que hagas despues con tu framework puede esperar un microtask, y en React tiene que esperarlo.
- El error boundary es tuyo. Tu widget corre en su propia raiz, y el host no ve lo que pasa
adentro: sin un boundary propio, un error de render deja tu hueco vacio y nadie se entera. Lo
que el host todavia caza es el montaje —que tu bundle no cargue, que no exponga
mount, quemountexplote—.
Si tu widget es de React no escribas nada de esto a mano:
npm create @numaris-microfront/widget te genera el src/mount.tsx con las tres cosas ya
resueltas —el nodo propio, el desmontaje diferido y el boundary— y tu widget.tsx sigue siendo
un componente normal con estas props. Ese archivo se genera y no se instala: no hay paquete
publicado del adaptador, asi que el codigo es tuyo y lo podes leer entero.
3. El manifiesto (widgetManifestSchema)
Antes de escribir el codigo de tu widget, declarás un manifiesto — un JSON que dice qué eventos provee tu widget y cuáles consume, con su fallback. Se valida con este esquema:
import { validateWidgetManifest } from '@numaris-microfront/contracts';
const result = validateWidgetManifest({
id: 'my-widget',
title: 'Mi widget',
version: '1.0.0',
provides: [
{
event: 'my-widget.item.selected',
description: 'Se eligio un item en la lista',
payload: { itemId: 'id del item elegido, o null si se deselecciono' },
},
],
consumes: [
{
event: 'trackable.selected',
description: 'Resaltar el item correspondiente si existe',
required: false,
fallback: 'No resalta nada hasta que el usuario elija manualmente',
},
],
});
if (result.ok) {
console.log(result.manifest);
} else {
console.error(result.errors); // string[], uno por cada problema
}Reglas que el esquema hace cumplir (no son solo convencion, son validacion real):
- El nombre de un evento tiene que ser de dominio y punteado, en minuscula:
sustantivo.accion(ej.trackable.selected). Un nombre de evento de UI comotableRowClickedno pasa la validacion. - Todo evento consumido con
required: false(el default) tiene que declararfallback: que hace tu widget cuando ese evento nunca llega. required: truees la excepcion, no la norma, y el esquema te obliga a ponerrequiredApprovedByAdr— una referencia a la decision que aprobó depender obligatoriamente de otro widget. Sin esa referencia, la validación falla.
findUnprovidedConsumptions(manifests) compara una lista de manifiestos entre si y reporta
(sin fallar) los eventos que alguien consume pero nadie de esa lista provee — útil para un
panel de diagnóstico que muestre, en un despliegue dado, qué integraciones están "sueltas".
Otras piezas del paquete
layoutSchema/validateLayout/LAYOUT_SLOTS: si tu plataforma anfitriona organiza widgets en zonas (top,left,center,right,bottom), este es el esquema de esa estructura. Puede no aplicar a tu integración si el host no usa este layout.validateWorkspaces/createWorkspaceSet/wrapLayout: un host puede tener varios acomodos por organización —espacios de trabajo, cada uno con nombre, icono y color— y esto es el esquema de esa colección. Dos cosas que conviene saber si vas a leer o escribir ese dato: unWorkspaceSettambién valida comoLayout, porque lleva un espejo del primer espacio para que un cliente que sólo entiende el modelo viejo lo siga leyendo; yvalidateWorkspacesacepta las dos formas, envolviendo unLayoutsuelto en un único espacio. Al escribir, armá siempre el conjunto concreateWorkspaceSet: es lo único que mantiene ese espejo al día.widgetCatalogSchema/validateWidgetCatalog: el esquema del catálogo que declara qué widgets existen y dónde vive el bundle de cada uno (usado por el host, no por el widget).MANUFACTURERS/modelsFor(manufacturer): catálogo fijo de fabricantes y modelos de dispositivos GPS soportados. Vocabulario compartido entre widgets que necesitan ofrecer las mismas opciones (por ejemplo, un formulario de alta de unidad), no una fuente de verdad de catálogo real.TRACKABLE_ICONS/TrackableIcon: la lista cerrada de íconos que puede tener una unidad. Es una unión de TypeScript, no unstring, a propósito — un ícono que no está en la lista no compila.
Peer package
@numaris-microfront/contracts no importa React ni ninguna librería de UI. Si además querés
componentes visuales consistentes con el resto de la plataforma, mirá
@numaris-microfront/ui-kit.
