Eventos propios: el bus User y la inversión de dependencias
Cómo publicar hechos con nvim_exec_autocmds y transportar carga útil en data sin acoplar módulos, qué contrato define un buen evento User, el caso VeryLazy y los PackChanged de vim.pack, y las trampas de un bus síncrono compartido: orden, errores y suscriptores tardíos.
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 mecanismo con el que lazy.nvim anuncia que el arranque terminó, y la razón por la que los plugins modernos se integran entre sí sin conocerse.
- Publicar eventos propios con
vim.api.nvim_exec_autocmdsy su clavedata. - Diseñar el contrato de un evento: nombre prefijado y estable, carga útil documentada.
- Suscribirse a eventos de terceros —
VeryLazyy compañía— sin crear dependencias duras. - Anticipar las tres trampas del bus: sincronía, orden de registro y suscriptor tardío.
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 escuchavim.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 emitevim.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 hace require del módulo del receptor, el receptor no hace require del emisor, y ninguno 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.
Tres detalles de la llamada. Primero: nvim_exec_autocmds no es exclusiva de User — dispara cualquier evento, y por eso es también una herramienta de depuración en la lección 9.5 (su pariente de línea de comandos es :doautocmd). Segundo: admite group y buffer para acotar a quién despiertas. Tercero: args.data acepta cualquier valor de Lua y se entrega tal cual, sin copia — es una tabla viva compartida entre el emisor y todos los receptores, así que trátala como inmutable salvo que el contrato diga otra cosa.
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 del 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 configuración que lo emite: LazyDone, GitSignsUpdate, DiosProyectoAbierto. Un espacio plano exige prefijos — el mismo argumento que el de los grupos en la lección 9.2.
Hecho consumado, no orden
Un evento anuncia algo que ya ocurrió, no pide una acción: ProyectoAbierto, no AbrirProyecto. Quien escucha decide qué hacer con el hecho.
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.
Receptor idempotente
Un evento puede emitirse más veces de las que preveías. El suscriptor debe tolerar repeticiones sin duplicar sus efectos — y tolerar data vacío, porque un :doautocmd manual no lo trae.
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ó centralmente.
-- Trabajo diferido: no urgente, tras el arranque interactivovim.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,})Y en 0.12 el propio núcleo firma un contrato de este estilo: vim.pack emite PackChangedPre y PackChanged cuando instala, actualiza o borra un plugin, con la información del cambio en args.data — qué plugin, qué tipo de operación, su especificación. No son eventos User (son eventos con nombre propio, del núcleo), pero el diseño es idéntico al que estás aprendiendo: un hecho consumado, con prefijo, con carga útil documentada, del que cualquiera puede colgarse para lanzar un build tras actualizar sin que vim.pack sepa que existes.
Suscribirse sin acoplarse
La clave pattern acepta una lista, de modo que un solo autocomando puede escuchar varios sucesos, 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 receptorvim.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 comodinvim.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 ningún emisor haya previsto su existencia. Añadir trazabilidad, métricas o depuración a un sistema de eventos no exige tocar el sistema — esa es precisamente la propiedad por la que se paga el precio de adoptar un bus. Veamos el precio.
Sincronía, orden y las trampas del bus
nvim_exec_autocmds es síncrona: 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.
Nunca hagas trabajo pesado dentro de un suscriptor
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 — la mecánica exacta de por qué esto funciona es el Nivel 12. Y protege el cuerpo con pcall (Nivel 7) cuando tu fallo 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 con dos eventos encadenados en lugar de confiar en el azar del cargador.
Queda la trampa temporal característica de todos los buses: si te suscribes después de la emisión, no te enteras.
La solución idiomática es que el emisor mantenga estado consultable además de publicar, de modo que el 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 prontofunction 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 })endreturn MEl bus User es inversión de dependencias dentro de tu editor
Lo que acabas de construir tiene nombre en ingeniería de software y medio siglo de historia: es el principio de inversión de dependencias, y su aparición dentro de un editor no es coincidencia sino consecuencia 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 dirigidas al mismo punto neutral: A publica un hecho y B reacciona a ese hecho. Ninguno 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 son tres propiedades concretas: cargar cualquier módulo en cualquier orden, añadir observadores que nadie previó, y 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 las llamadas directas no exigen: 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.
El pariente interactivo de nvim_exec_autocmds. Ojo: así no viaja data — tu suscriptor recibirá args.data a nil, y si eso lo rompe, acabas de descubrir que tu contrato no toleraba emisiones manuales.
Todos los suscriptores de eventos User de la sesión, con sus patrones. Los tuyos con prefijo salen agrupados y con desc; los de tus plugins te enseñan cuánto ecosistema vive ya de este mecanismo.
La próxima vez que vim.pack instale o actualice algo, verás la carga útil real del evento: el hecho, documentado, con datos. Un contrato del núcleo, idéntico en diseño a los tuyos.
Construye tu propio bus
- Emite un evento
Usercon prefijo propio y carga útil, y suscríbete desde otro archivo sin que ninguno de los dos hagarequiredel otro. - Escribe el observador con patrón comodín que registre todos tus eventos con nombre y carga útil, y verifica que ve un evento nuevo sin tocar una línea del observador.
- Provoca 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 (
vim.wait(1000)dentro del callback), comprueba que bloquea al emisor, y corrígelo convim.schedule. - Documenta el contrato de uno de tus eventos: nombre, cuándo se emite, claves de
data, y qué garantiza ante repeticiones y ante un:doautocmdsin datos.