Hooks, plugins, timeline e IA
Extensiones y subsistemas diferidos del runtime con límites precisos de ejecución, registro y autoridad.
Middleware de lifecycle
Los hooks reciben (context, next). Llamar a next() permite continuar; omitirlo cancela la cadena. Una prioridad mayor se ejecuta antes. El registro devuelve una función de baja que debes conservar si el middleware tiene un lifecycle más corto que el shell. La declaración TypeScript de use indica void; el runtime sí proporciona ese cleanup.
const remove = wu.hooks.use('beforeMount', async (context, next) => {
console.log('Montando', context.appName);
await next();
}, { name: 'mount-observer', priority: 10 });
// Después de observar el escenario:
remove();
Las fases son beforeInit, afterInit, beforeLoad, afterLoad, beforeMount, afterMount, beforeUpdate, afterUpdate, beforeUnmount, afterUnmount, beforeDestroy y afterDestroy. El contexto depende de la fase; no todos los hooks reciben lifecycle, sandbox o props.
Un middleware que lanza antes de next cancela por defecto; failOpen: true permite continuar ante ese fallo. Los helpers exportados createSimpleHook, createConditionalHook, createGuardHook, createTransformHook y createTimedHook construyen middleware. El helper timed registra su error y llama a next: no es apropiado como control que deba cerrar acceso ante timeout.
Plugin con grants explícitos
let stop = () => {};
await wu.pluginSystem.use({
name: 'catalog-observer',
permissions: ['events'],
install(api) {
stop = api.on('catalog:selected', (event) => {
console.info('Selección observada', event.data);
});
},
uninstall() { stop(); },
}, { grants: ['events'] });
// Al retirarlo:
await wu.pluginSystem.uninstall('catalog-observer');
use admite objeto o factory, espera instalación y valida permisos. ready() permite esperar instalaciones pendientes; init ya espera esa barrera. El timeout por defecto de hooks de plugins es 5000 ms.
Las factories reciben opciones, no el core. Los grants host se copian antes de llamar a la factory para que esta no amplíe sus opciones y se autorice a sí misma. Aun así, un plugin es código ejecutado en el host: la API filtrada no aísla closures ni acceso global que ya tenga ese código.
Timeline y tiempo de carga
await wu.timelineReady();
wu.timeline.record({ snapshotEvery: 64, maxEntries: 5000 });
wu.store.set('demo.counter', 1);
wu.store.set('demo.counter', 2);
await wu.timeline.stepBack();
console.log(wu.store.get('demo.counter'));
await wu.timeline.live();
const recording = await wu.timeline.export();
wu.timeline.stop();
timelineReady() carga el chunk antes de empezar. Si llamas record() por primera vez y escribes inmediatamente, la escritura puede ocurrir antes de armar el registro diferido. El facade conserva la intención de record/stop mientras llega el módulo; para capturar la siguiente escritura sin ambigüedad usa ready primero.
El journal registra escrituras del store, eventos y lifecycle/props que pasan por WU. Tiene límite por defecto de 5000 entradas y snapshots cada 64. seek, stepBack, stepForward, live, export, import, entries y status permiten inspeccionar ese registro.
Retroceder rematerializa proyecciones; no rebobina timers internos de apps, peticiones HTTP ni infraestructura. Cuando estás en el pasado, nuevas escrituras no se agregan al journal y volver a live las descarta. No existe branching del historial ni auditoría durable implícita.
IA optativa
El import principal instala un proxy lazy. Métodos chainables como provider, action, trigger, capability y workflow pueden encolar configuración mientras llega el chunk; operaciones de valor requieren esperar al módulo real. aiReady() es la forma explícita de obtenerlo.
const ai = await wu.aiReady();
ai.init({ permissions: { executeActions: true } });
ai.action('sum_local', {
description: 'Suma dos valores de demostración sin efectos externos',
parameters: {
type: 'object',
properties: {
a: { type: 'number' },
b: { type: 'number' },
},
required: ['a', 'b'],
},
handler: async ({ a, b }) => ({ total: a + b }),
});
const result = await ai.execute('sum_local', { a: 2, b: 3 });
console.log(result);
Registrar y ejecutar esta action local no requiere enviar datos a un modelo. Para conversación existen providers, send, stream, json, agent, triggers y workflows, con sus propios permisos y configuración. No uses una key de servicio privada en un bundle público.
Actions validan parámetros, verifican executeActions, consultan permisos adicionales/condicionales y pueden pedir confirmación. dangerous: true obliga confirmación en el código actual. Los resultados de ejecución usan un envelope { success, result? , reason? }: un rechazo de permisos puede ser un resultado negativo y no una excepción.
Los defaults permiten readStore y emitEvents; comienzan desactivados writeStore, readDOM, modifyDOM, readDiagnostics, navigate, executeActions, network y allowDirectKey. Son flags del módulo IA, independientes de grants app y autorización servidor.
MCP y automatización
wu.mcp.connect(url, options), disconnect() e isConnected() exponen el bridge cliente diferido. El paquete incluye también el binario wu-mcp-server y su subpath. El bridge puede inspeccionar y mutar estado/interfaz según controles del módulo; no agrega por sí mismo dry-run vinculante, idempotencia, autorización de dominio, rollback o un registro permanente.
Para un agente que opere recursos reales, usa un servicio que reciba comandos validados y devuelva receipts. El store puede proyectar su progreso; la action no debe convertirse en un closure que ejecuta privilegios sin revalidación.
Referencia de implementación
Hooks: src/core/wu-hooks.js; plugins: src/core/wu-plugin.js; facade timeline: src/index.js; journal: src/core/wu-timeline.js; IA: src/ai/wu-ai.js, src/ai/wu-ai-actions.js, src/ai/wu-ai-actions.js; defaults: src/ai/wu-ai-permissions.js; MCP: src/index.js. Pruebas: tests/unit/plugin-security.test.js, tests/unit/timeline-facade.test.js, tests/unit/lazy-ai-proxy.test.js, tests/unit/ai-actions.test.js, tests/unit/mcp-bridge-security.test.js.