Eventos propios: el bus User y la inversión de dependencias
Cómo publicar eventos con nvim_exec_autocmds, qué contrato define un buen nombre de evento User, cómo transportar carga útil sin acoplar módulos y qué trampas esconde un bus síncrono compartido con todos tus plugins.
Hasta ahora has escuchado eventos que emite el núcleo. El paso que convierte una configuración en una arquitectura es descubrir que tú también puedes emitirlos. El evento User es un espacio de nombres abierto donde cualquier parte de tu configuración, o cualquier plugin, publica hechos con nombre sin saber quién los escucha ni si alguien lo hace. Es el mismo mecanismo que usa lazy.nvim para anunciar que el arranque terminó, y es la razón por la que los plugins modernos se integran entre sí sin conocerse.
- Publicar eventos propios con
vim.api.nvim_exec_autocmds. - Diseñar el contrato de un evento: nombre estable y carga útil.
- Suscribirse a eventos de terceros sin crear dependencias duras.
- Anticipar las consecuencias de que el bus sea síncrono y compartido.
Publicar: nvim_exec_autocmds
User es un evento que el núcleo nunca dispara por su cuenta. Existe únicamente para que lo dispares tú, y su patrón funciona como el nombre concreto del suceso. La emisión es una sola llamada.
-- Publicar: quien emite no sabe ni le importa quien escucha
vim.api.nvim_exec_autocmds("User", {
pattern = "DiosProyectoAbierto",
data = {
raiz = vim.fn.getcwd(),
tipo = "rust",
momento = vim.uv.hrtime(),
},
})
-- Suscribirse: quien escucha no sabe ni le importa quien emite
vim.api.nvim_create_autocmd("User", {
group = vim.api.nvim_create_augroup("dios_proyecto", { clear = true }),
pattern = "DiosProyectoAbierto",
desc = "Configura la sesion segun el proyecto detectado",
callback = function(args)
local info = args.data
vim.notify(("Proyecto %s en %s"):format(info.tipo, info.raiz))
end,
})
Observa lo que ha desaparecido entre los dos fragmentos: ninguna referencia mutua. El emisor no importa el módulo del receptor, el receptor no importa el del emisor, y ninguno de los dos deja de funcionar si el otro no está cargado. Publicar sin suscriptores es una operación válida que no cuesta nada; suscribirse a algo que nadie publica tampoco es un error. Esa tolerancia es exactamente lo que hace componible una configuración grande.
args.data acepta cualquier valor Lua y se entrega tal cual, sin copia ni serialización. Es una tabla viva compartida entre emisor y todos los receptores: si un suscriptor la modifica, los siguientes ven el cambio. Puede usarse deliberadamente como canal de respuesta, pero conviene tratarla como inmutable salvo que el contrato diga lo contrario.
El contrato: nombre, carga útil y estabilidad
Un evento User es una interfaz pública. En cuanto alguien se suscribe, su nombre y la forma de su carga útil son un compromiso que no puedes romper sin avisar. Las convenciones que se han consolidado en el ecosistema no son caprichos estéticos sino soluciones a un espacio de nombres global y plano.
Prefijo de origen
El nombre empieza por el del plugin o la configuración que lo emite. LazyDone, GitSignsUpdate, DiosProyectoAbierto. Un espacio plano exige prefijos.
Hecho consumado, no orden
Un evento anuncia algo que ya ocurrió, no pide una acción. ProyectoAbierto, no AbrirProyecto. Quien escucha decide qué hacer.
Carga útil con forma
Documenta las claves de data como documentarías los parámetros de una función. Añadir claves es compatible; renombrarlas o quitarlas rompe a los suscriptores.
Idempotencia del receptor
Un evento puede emitirse más veces de las que preveías. El suscriptor debe tolerar repeticiones sin duplicar sus efectos.
El caso de estudio obligado es lazy.nvim. Su evento VeryLazy no dice “carga este plugin”: dice “el arranque interactivo ha terminado y ya puedes hacer trabajo no urgente”. Cientos de plugins se cuelgan de esa afirmación sin que lazy.nvim conozca a ninguno, y el resultado es un arranque escalonado que nadie coordinó de forma centralizada.
-- Trabajo diferido: no urgente, tras el arranque interactivo
vim.api.nvim_create_autocmd("User", {
pattern = "VeryLazy",
once = true,
desc = "Carga integraciones pesadas cuando el editor ya responde",
callback = function()
require("dios.integraciones").iniciar()
end,
})
Suscribirse sin acoplarse
El patrón de suscripción admite composición. La clave pattern acepta una lista, de modo que un solo autocomando puede escuchar varios eventos distintos, y admite comodines, lo que permite escuchar familias enteras sin enumerarlas.
local grupo = vim.api.nvim_create_augroup("dios_bus", { clear = true })
-- Varios eventos, un solo receptor
vim.api.nvim_create_autocmd("User", {
group = grupo,
pattern = { "DiosProyectoAbierto", "DiosProyectoCerrado" },
callback = function(args)
-- args.match contiene el nombre concreto que disparo
require("dios.estado").sincronizar(args.match, args.data)
end,
})
-- Una familia entera por comodin
vim.api.nvim_create_autocmd("User", {
group = grupo,
pattern = "Dios*",
desc = "Traza de todos los eventos propios",
callback = function(args)
require("dios.log").registrar(args.match, args.data)
end,
})
Ese segundo autocomando es un observador transversal: registra toda la actividad del bus sin que ninguno de los emisores haya previsto su existencia. Añadir trazabilidad, métricas o depuración a un sistema de eventos no exige tocar el sistema, y esa es precisamente la propiedad que se paga al adoptar un bus.
flowchart LR P[Modulo de deteccion de proyecto] --> BUS[Bus User] BUS --> S1[Ajustes por lenguaje] BUS --> S2[Sesion y disposicion de ventanas] BUS --> S3[Observador de trazas] S1 -.no se conocen entre si.-> S2 style BUS fill:#cba6f7,color:#11111b style P fill:#89b4fa,color:#11111b
Sincronía, orden y las trampas del bus
La comodidad tiene un precio que conviene conocer antes de construir sobre él. nvim_exec_autocmds es síncrono: la llamada no retorna hasta que todos los suscriptores han terminado. Un receptor lento bloquea al emisor, y un receptor que lanza un error interrumpe la cadena.
Si tu reacción implica leer archivos, llamar a un proceso externo o construir estructuras grandes, envuélvela en vim.schedule para devolver el control al emisor de inmediato. Y protege el cuerpo con pcall cuando el fallo de tu suscriptor no deba impedir que los demás se ejecuten. Un bus síncrono sin estas dos precauciones convierte cualquier suscriptor defectuoso en un fallo del sistema entero.
El orden de ejecución entre suscriptores es el orden de registro, que en la práctica es el orden de carga de los plugins: impredecible y sujeto a cambios cuando instalas algo nuevo. Nunca construyas lógica que dependa de que tu receptor corra antes o después de otro. Si necesitas una secuencia, exprésala explícitamente con dos eventos encadenados en lugar de confiar en el azar del cargador.
Queda una trampa temporal característica de los buses: si te suscribes después de la emisión, no te enteras. Un plugin de carga perezosa que se registra tarde en VeryLazy no verá el evento que ya pasó. La solución idiomática es que el emisor mantenga estado consultable además de publicar, de modo que un suscriptor tardío pueda preguntar en vez de esperar.
local M = { listo = false }
function M.anunciar()
M.listo = true
vim.api.nvim_exec_autocmds("User", { pattern = "DiosListo", data = { fase = "final" } })
end
-- Suscriptor robusto: actua ya si llego tarde, espera si llego pronto
function M.cuando_listo(fn)
if M.listo then
return vim.schedule(fn)
end
vim.api.nvim_create_autocmd("User", { pattern = "DiosListo", once = true, callback = fn })
end
return M
Lo que acabas de construir tiene un nombre en ingeniería de software y una historia de cincuenta años: es el principio de inversión de dependencias, y su aparición dentro de un editor de texto no es una coincidencia sino una consecuencia inevitable de la escala. Mientras una configuración cabe en un archivo, las llamadas directas son la forma más honesta de expresar una relación: este módulo necesita aquel, lo requiere y lo usa. Cuando la configuración crece a decenas de módulos y cien plugins de terceros, esa honestidad se vuelve una jaula, porque el grafo de dependencias directas fija el orden de carga, propaga los fallos, impide la carga perezosa y hace que cualquier reordenación rompa algo lejano. El bus corta ese grafo sustituyendo la relación A llama a B por dos relaciones más débiles y ambas dirigidas al mismo punto neutral: A publica un hecho y B reacciona a ese hecho. Ninguno de los dos apunta al otro, y por eso cualquiera de los dos puede faltar, cargarse tarde, duplicarse o desaparecer sin que el sistema se venga abajo. Lo que ganas no es elegancia sino tres propiedades muy concretas: puedes cargar cualquier módulo en cualquier orden, puedes añadir observadores que nadie previó, y puedes borrar un módulo entero sin buscar quién lo llamaba. Lo que pagas es igual de concreto: pierdes la trazabilidad estática —ninguna búsqueda de texto te dirá quién reacciona a qué— y ganas una clase de fallo nueva, el evento que nadie escucha y el suscriptor que llegó tarde. Por eso los eventos propios exigen la disciplina que no exigen las llamadas directas: nombres prefijados y estables, cargas útiles documentadas, receptores idempotentes y estado consultable junto a la notificación. Un bus sin contrato no es desacoplamiento, es acoplamiento invisible.
- Emite un evento
Usercon prefijo propio y carga útil, y suscríbete desde otro archivo sin que ninguno de los dos importe al otro. - Escribe un observador con patrón comodín que registre todos tus eventos propios con su nombre y su carga útil.
- Provoca deliberadamente el caso del suscriptor tardío y resuélvelo con el patrón de estado consultable más notificación.
- Añade un suscriptor que tarde un segundo, comprueba que bloquea al emisor y corrígelo con
vim.schedule. - Documenta el contrato de uno de tus eventos: nombre, cuándo se emite, claves de
datay garantías de idempotencia.