Contratos de capacidad
El bus de eventos es dispara-y-olvida y el store es una pizarra compartida. Ninguno de los dos permite que una app le pida a otra que haga algo y reciba una respuesta. Los contratos de capacidad sí: una app publica una implementación bajo un nombre y una versión semver, y otras apps la consumen contra un rango de versiones.
La decisión de diseño que los hace usables es que consume() nunca bloquea y
nunca lanza. Devuelve un proxy de inmediato, y el proxy resuelve el proveedor
en cada acceso. Consume antes de que el proveedor monte, y la capacidad
empieza a funcionar en el momento en que lo haga.
import { wu, provide, consume } from 'wu-framework';wu.provide()
Sección titulada «wu.provide()»provide(name: string, impl: object | Function, opts?: { version?: string; shape?: string[] | Record<string, string>; app?: string;}): { revoke: () => boolean }Publica una implementación bajo name.
| Opción | Tipo | Por defecto | Significado |
|---|---|---|---|
version |
string |
'0.0.0' |
Versión semver contra la que los consumidores comparan su rango. |
shape |
string[] o Record<string, string> |
null |
Comprobación en tiempo de ejecución de que la implementación tiene realmente esos miembros, verificada en el momento del provide(). |
app |
string |
null |
App propietaria. La capacidad se revoca automáticamente cuando esa app se desmonta. |
wu.provide('auth', { getUser: () => currentUser, hasRole: (role) => currentUser?.roles.includes(role), logout: async () => { await api.logout(); currentUser = null; },}, { version: '2.1.0', app: 'auth-app', shape: { getUser: 'function', hasRole: 'function', logout: 'function' },});La forma de array de shape solo comprueba la presencia; la forma de objeto
asocia cada clave a una cadena de typeof y también comprueba eso.
Casos límite. provide() lanza en tres casos: un name ausente o que no
sea cadena, un impl que no sea ni objeto ni función (null y los primitivos se
rechazan — ojo, los arrays pasan, ya que typeof [] === 'object'), y una
violación de la forma, cuyo mensaje enumera todos los problemas
(getUser (missing), hasRole (expected function, got string)).
Hay exactamente un proveedor por nombre. El registro se indexa solo por
nombre, así que un segundo provide('auth', ...) sustituye al primero
silenciosamente: gana la última escritura, sin error, sin multiplexado de
versiones. Si la versión nueva ya no satisface el rango de un consumidor vivo,
ese consumidor empieza a lanzar en su siguiente acceso a una propiedad. Esto es
deliberado (la re-resolución sigue viva), pero significa que no puedes ejecutar
dos versiones de una capacidad en paralelo.
El handle devuelto está acotado por identidad: revoke() devuelve false y no
hace nada si alguien ha vuelto a proveer ese nombre desde entonces, de modo que
un handle obsoleto no puede matar al propietario actual.
wu.consume()
Sección titulada «wu.consume()»consume<T>(name: string, range?: string, opts?: { wait?: false; timeout?: number }): Tconsume<T>(name: string, range: string, opts: { wait: true; timeout?: number }): Promise<T>Devuelve un proxy vivo para una capacidad. range es '*' por defecto
(cualquier versión).
| Opción | Tipo | Por defecto | Significado |
|---|---|---|---|
wait |
boolean |
false |
Devolver una Promise que se resuelve cuando aparezca un proveedor que satisfaga el rango, en lugar de un proxy. |
timeout |
number (ms) |
ninguno | Solo con wait: true. Se rechaza pasado ese tiempo. 0 cuenta como “sin timeout”. |
const auth = wu.consume('auth', '^2.0.0');
// Todavía no ha pasado nada. Sin proveedor necesario, sin error.// La búsqueda ocurre aquí, en el acceso:if (auth.__wuAvailable) { const user = auth.getUser();}Casos límite — consume() en sí nunca lanza. El proxy sí, en el primer
acceso a una propiedad, con uno de estos dos mensajes:
| Situación | Se lanza en el acceso |
|---|---|
| No hay proveedor registrado con ese nombre | [WuContracts] no provider for capability '<name>'. Did the providing app mount and call wu.provide('<name>', …)? |
| El proveedor existe pero su versión está fuera de tu rango | [WuContracts] capability '<name>@<version>' does not satisfy '<range>'. |
Ninguno de los dos errores lleva una propiedad code.
Las reglas del proxy
Sección titulada «Las reglas del proxy»Importan más que los dos métodos de arriba, porque el proxy es lo que realmente tienes en la mano.
No es thenable. proxy.then devuelve undefined, siempre. Sin eso, un
await consume('auth') le entregaría a await una función llamada then y se
colgaría para siempre. El coste es que un método de capacidad literalmente
llamado then queda inalcanzable a través del proxy — un intercambio aceptable,
y algo que conviene saber.
Los métodos están vinculados y memoizados. proxy.fn === proxy.fn se
cumple, que es lo que hace funcionar las idas y venidas de
addEventListener/removeEventListener, las claves de Set y los arrays de
dependencias de React. La caché se indexa por la función fuente sin vincular,
así que cambiar de proveedor produce identidades nuevas y la re-resolución sigue
viva.
Las escrituras son un no-op. Las capacidades consumidas son de solo lectura.
Una escritura registra
[WuContracts] ignored write to '<prop>' on consumed capability '<name>': consumed capabilities are read-only.
y se descarta. La trampa devuelve true, así que no lanza ni siquiera en modo
estricto — un no-op honesto, mejor que dejar que un consumidor mute la
implementación compartida del proveedor (o le reapunte el prototipo vía
__proto__).
El proxy no enumera. No hay trampa ownKeys, así que Object.keys(proxy) es
[], { ...proxy } es {} y JSON.stringify(proxy) es {}. Hacer spread de
una capacidad consumida pierde todo silenciosamente. Llama a los miembros
directamente.
Los símbolos nunca lanzan. Las sondas de Symbol.iterator,
Symbol.toStringTag y util.inspect devuelven undefined cuando no hay
proveedor que satisfaga el rango, así que registrar en el log o desestructurar
una capacidad ausente no revienta en un sitio inesperado.
Tres claves de introspección siempre son seguras de leer:
| Clave | Devuelve |
|---|---|
__wuAvailable |
boolean — ¿hay ahora mismo un proveedor que satisfaga el rango? |
__wuCapability |
El nombre de la capacidad. |
__wuRange |
El rango con el que se creó este proxy. |
const analytics = wu.consume('analytics', '^1.0.0');
// La sonda segura: no hace falta try/catch.if (analytics.__wuAvailable) { analytics.track('checkout', { total });}Esperar a un proveedor
Sección titulada «Esperar a un proveedor»try { const auth = await wu.consume('auth', '^2.0.0', { wait: true, timeout: 5000 }); render(auth.getUser());} catch (err) { // [WuContracts] timed out after 5000ms waiting for 'auth' satisfying '^2.0.0'.}Casos límite. Con wait: true y sin timeout, la promesa puede no
resolverse nunca: lo único que la rechazará es wu.destroy(), con
[WuContracts] framework destroyed. Pasa siempre un timeout en rutas de
producción. Si ya existe un proveedor que satisface el rango, la promesa se
resuelve de inmediato con el mismo proxy que habría devuelto la forma síncrona.
wu.contracts.has()
Sección titulada «wu.contracts.has()»has(name: string, range?: string): booleanComprobación de disponibilidad que no lanza, equivalente a leer __wuAvailable
en un proxy.
if (wu.contracts.has('payments', '>=3.0.0')) { showCheckoutButton();}Revocar
Sección titulada «Revocar»revoke(name: string): booleanrevokeByApp(app: string): voidrevoke() elimina la capacidad y emite wu:capability:revoked. Devuelve false
si no había nada registrado con ese nombre — un no-op, no un error.
const handle = wu.provide('auth', impl, { version: '2.1.0', app: 'auth-app' });handle.revoke(); // acotado: solo revoca si sigue siendo el propietario actualwu.contracts.revoke('auth'); // sin acotar: revoca a quien tenga el nombre ahorarevokeByApp(app) revoca todas las capacidades cuya opción app coincida.
Normalmente no lo llamas tú: el framework lo llama durante el desmontaje real,
usando el nombre de app que verificó él mismo en lugar de uno leído de un evento
del bus, que es por lo que el sistema de contratos deliberadamente no se
suscribe a app:unmounted (un evento falsificado podría, si no, revocar las
capacidades de otra app).
wu.contracts.list()
Sección titulada «wu.contracts.list()»list(): Array<{ name: string; version: string; app: string | null }>Todo lo que se provee ahora mismo. Fíjate en que omite impl deliberadamente:
esto alimenta a wu.inspect() y al overlay de devtools, no es una forma de
esquivar el proxy.
console.table(wu.contracts.list());// name version app// auth 2.1.0 auth-app// analytics 1.4.2 nullRangos de versiones
Sección titulada «Rangos de versiones»La comparación usa una implementación pequeña de semver sin dependencias, no
node-semver. Sintaxis soportada:
| Forma | Ejemplo | Coincide con |
|---|---|---|
| Cualquiera | *, '', omitido |
Todo |
| Exacta | 2.1.0 |
Solo esa versión |
| Rango X | 1, 1.2, 1.x |
Cualquier parche/minor bajo el prefijo dado |
| Caret | ^2.1.0 |
>=2.1.0 <3.0.0 |
| Tilde | ~2.1.0 |
>=2.1.0 <2.2.0 |
| Comparadores | >=2.0.0, <3.0.0, >1.0.0 |
Tal cual se escriben |
| AND | >=2.0.0 <3.0.0 |
Separados por espacios, deben cumplirse todos |
| OR | ^1.0.0 || ^2.0.0 |
Cualquiera de los dos lados |
Los metadatos de prerelease y de build se eliminan antes de comparar:
2.1.0-rc.1 se trata como 2.1.0. Si dependes de la semántica de exclusión de
prereleases de node-semver, aquí no la vas a tener. Un rango mal formado (un
operador colgando) no coincide con nada.
La dirección es siempre: la versión del proveedor contrastada con el rango del consumidor.
Eventos
Sección titulada «Eventos»| Evento | Payload |
|---|---|
wu:capability:provided |
{ name, version, app } |
wu:capability:revoked |
{ name } |
Ambos se emiten con appName: 'wu-core' y pasan el modo estricto.
Limitaciones honestas
Sección titulada «Limitaciones honestas»- Un proveedor por nombre. Sin versiones en paralelo, sin cadena de fallback. Un re-provide con una versión incompatible rompe a los consumidores vivos en su siguiente acceso, no en el momento del provide.
- Sin enumeración. Todo lo que dependa de iterar las claves del objeto
—spread,
JSON.stringify, la mayoría de utilidades de igualdad profunda— ve un objeto vacío. thenes inalcanzable como nombre de miembro de una capacidad.- La comprobación de forma es superficial. Verifica presencia y
typeof, y solo en el momento del provide. No puede verificar tipos de argumentos ni de retorno, y un valor deshapeque no sea ni array ni objeto pasa de forma vacua. - Esto no es una frontera de seguridad. Cualquier código de la página puede
llamar a
wu.consume(). Los contratos te dan versionado y fallo ruidoso, no control de acceso.
