Hooks, plugins, timeline, and AI
Runtime extensions and deferred subsystems with precise limits on execution, registration, and authority.
Lifecycle middleware
Hooks receive (context, next). Calling next() continues the chain; omitting it cancels the chain. Higher priority runs first. Registration returns an unsubscribe function that you must retain if middleware has a shorter lifecycle than the shell. The TypeScript declaration of use says void; the runtime does provide this 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();
The phases are beforeInit, afterInit, beforeLoad, afterLoad, beforeMount, afterMount, beforeUpdate, afterUpdate, beforeUnmount, afterUnmount, beforeDestroy, and afterDestroy. Context depends on the phase; not every hook receives lifecycle, sandbox, or props.
Middleware throwing before next cancels by default; failOpen: true allows continuation after that failure. Exported helpers createSimpleHook, createConditionalHook, createGuardHook, createTransformHook, and createTimedHook build middleware. The timed helper logs its error and calls next: it is unsuitable for a control that must deny access on timeout.
Plugin with explicit grants
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 accepts an object or factory, awaits installation, and validates permissions. ready() lets you await pending installations; init already waits for this barrier. The default plugin-hook timeout is 5000 ms.
Factories receive options, not the core. Host grants are copied before calling the factory so it cannot expand its options and authorize itself. However, a plugin executes in the host: the filtered API does not isolate closures or global access that code already has.
Timeline and loading time
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() loads the chunk before starting. If you call record() for the first time and immediately write, the write may occur before deferred recording is armed. The facade preserves record/stop intent while the module loads; call ready first to capture the next write unambiguously.
The journal records store writes, events, and lifecycle/props passing through WU. Its default limit is 5000 entries, with snapshots every 64. seek, stepBack, stepForward, live, export, import, entries, and status let you inspect that record.
Going back rematerializes projections; it does not rewind apps’ internal timers, HTTP requests, or infrastructure. While you are in the past, new writes are not added to the journal, and returning to live discards them. There is no history branching or implicit durable audit.
Optional AI
The main import installs a lazy proxy. Chainable methods such as provider, action, trigger, capability, and workflow can queue configuration while the chunk loads; value-returning operations require waiting for the real module. aiReady() obtains it explicitly.
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);
Registering and executing this local action does not require sending data to a model. Conversation uses providers, send, stream, json, agent, triggers, and workflows, with their own permissions and configuration. Do not put a private service key in a public bundle.
Actions validate parameters, check executeActions, consult additional/conditional permissions, and may request confirmation. dangerous: true requires confirmation in the current code. Execution results use an envelope { success, result? , reason? }: permission rejection may be a negative result rather than an exception.
Defaults allow readStore and emitEvents; writeStore, readDOM, modifyDOM, readDiagnostics, navigate, executeActions, network, and allowDirectKey start disabled. These are AI-module flags, independent of app grants and server authorization.
MCP and automation
wu.mcp.connect(url, options), disconnect(), and isConnected() expose the deferred client bridge. The package also includes the wu-mcp-server binary and its subpath. The bridge can inspect and mutate state/interface according to module controls; it does not automatically add binding dry runs, idempotency, domain authorization, rollback, or permanent records.
For an agent operating real resources, use a service that receives validated commands and returns receipts. The store can project its progress; an action should not become a closure executing privileges without revalidation.
Implementation references
Hooks: src/core/wu-hooks.js; plugins: src/core/wu-plugin.js; timeline facade: src/index.js; journal: src/core/wu-timeline.js; AI: 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. Tests: 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.