Writing Plugins
WakaPAC's plugin system lets external libraries hook into the component lifecycle. Plugins can inject per-component state, define new message types, and clean up resources automatically — all without either side depending on the other's internals.
Plugin Structure
A plugin is any object with a createPacPlugin(pac) method. WakaPAC calls this method once during wakaPAC.use(),
passing itself as the pac argument. The method returns a plugin instance — an object that implements
lifecycle hooks.
const myPlugin = {
createPacPlugin(pac) {
// Called once at registration time.
// 'pac' is the wakaPAC runtime object.
return {
onComponentCreated(abstraction, pacId, config) {
// Called for every new component.
},
onComponentDestroyed(pacId) {
// Called when a component's DOM element is removed.
}
};
}
};
wakaPAC.use(myPlugin);
Both lifecycle hooks are optional — implement only what you need.
Lifecycle Hooks
| Hook | Arguments | Timing |
|---|---|---|
onComponentCreated |
abstraction, pacId, config |
After the component is registered in PACRegistry, before init() runs and
before the pac:component-ready event fires.
|
onComponentDestroyed |
pacId |
When the component's DOM element is removed, before the component's internal destroy()
runs.
|
The abstraction argument is the component's reactive proxy — the same this the component
author works with. The config argument is the merged options object (the third argument to wakaPAC()),
which includes any plugin-specific options the component author may have provided.
Defining Custom Messages
Plugins communicate with components through msgProc using custom message types. Derive your message
constants from pac.MSG_USER to avoid collisions with built-in messages and other plugins:
createPacPlugin(pac) {
const MSG_WS_OPEN = pac.MSG_USER + 0x100;
const MSG_WS_MESSAGE = pac.MSG_USER + 0x101;
const MSG_WS_CLOSE = pac.MSG_USER + 0x102;
const MSG_WS_ERROR = pac.MSG_USER + 0x103;
// Expose on wakaPAC so components can reference them
pac.MSG_WS_OPEN = MSG_WS_OPEN;
pac.MSG_WS_MESSAGE = MSG_WS_MESSAGE;
pac.MSG_WS_CLOSE = MSG_WS_CLOSE;
pac.MSG_WS_ERROR = MSG_WS_ERROR;
// ...
}
Components then handle these in msgProc like any other message:
msgProc(event) {
switch (event.message) {
case wakaPAC.MSG_WS_MESSAGE:
this.lastMessage = event.detail.data;
break;
}
}
Delivering Messages
Use pac.sendMessage() or pac.postMessage() to deliver messages to a specific component.
Both take the same arguments; sendMessage dispatches synchronously, while postMessage
dispatches asynchronously via setTimeout.
// Synchronous — dispatched immediately
pac.sendMessage(pacId, MSG_WS_MESSAGE, wParam, lParam, detail);
// Asynchronous — dispatched on the next tick
pac.postMessage(pacId, MSG_WS_MESSAGE, wParam, lParam, detail);
Use pac.broadcastMessage() to send a message to every active component at once:
pac.broadcastMessage(MSG_WS_CLOSE, wParam, lParam, detail);
Prefer postMessage in most cases. Synchronous dispatch during initialization or inside event handlers
can cause unexpected re-entrancy.
Injecting Per-Component State
The most common plugin pattern is attaching a scoped handle to each component's abstraction in onComponentCreated,
then cleaning it up in onComponentDestroyed. Use a Map keyed by pacId to
track per-component resources:
const myPlugin = {
createPacPlugin(pac) {
const connections = new Map();
return {
onComponentCreated(abstraction, pacId, config) {
const conn = new WebSocket(config.wsUrl || '/ws');
conn.onmessage = function(e) {
pac.postMessage(pacId, pac.MSG_WS_MESSAGE, 0, 0, {
data: JSON.parse(e.data)
});
};
connections.set(pacId, conn);
// Inject a handle the component can use
abstraction._ws = {
send(data) {
conn.send(JSON.stringify(data));
}
};
},
onComponentDestroyed(pacId) {
const conn = connections.get(pacId);
if (conn) {
conn.close();
connections.delete(pacId);
}
}
};
}
};
The component author then uses this._ws.send() naturally, without knowing about the plugin's internals:
wakaPAC('#chat', {
draft: '',
msgProc(event) {
switch (event.message) {
case wakaPAC.MSG_WS_MESSAGE:
this.messages.push(event.detail.data);
break;
}
},
sendMessage() {
this._ws.send({ text: this.draft });
this.draft = '';
}
}, { wsUrl: 'wss://chat.example.com' });
Reading Component Options
The config argument passed to onComponentCreated is the merged options object from wakaPAC()'s
third argument. Use it to accept per-component configuration for your plugin. Choose a single top-level key to
namespace your options:
// Component opts in to plugin behavior via config
wakaPAC('#dashboard', {
// ...
}, {
ws: { url: 'wss://live.example.com', reconnect: true }
});
// Plugin reads config.ws in onComponentCreated
onComponentCreated(abstraction, pacId, config) {
const opts = config.ws || {};
const url = opts.url || '/ws';
const reconnect = opts.reconnect !== false;
// ...
}
Registration Rules
A few things to be aware of when registering plugins:
-
Register before creating components. Plugins only see components created after
wakaPAC.use()is called. Components created earlier will not triggeronComponentCreated. -
Duplicate registrations are ignored. Calling
wakaPAC.use(myPlugin)twice with the same object reference is safe — it silently skips the second registration. -
createPacPluginis required. If the library doesn't have acreatePacPluginmethod,wakaPAC.use()throws an error. - Order matters between plugins. Plugins are called in registration order. If plugin B depends on state injected by plugin A, register A first.
Best Practices
-
Namespace injected properties. Prefix injected properties with an underscore (e.g.
this._ws,this._log) to avoid collisions with the component author's own properties. -
Namespace your message range. Pick a fixed offset from
MSG_USER(e.g.+ 0x100,+ 0x200) and document it, so other plugins can avoid the same range. -
Clean up in
onComponentDestroyed. Close connections, clear intervals, delete Map entries. The component's DOM element is already gone at this point — don't try to access it. -
Prefer
postMessageoversendMessage. Asynchronous dispatch avoids re-entrancy issues when delivering messages during initialization or inside other event handlers. -
Keep
createPacPluginside-effect-free where possible. Exposing message constants onpacis fine, but avoid heavy setup here — defer per-component work toonComponentCreated. -
Respect component config. Accept a namespaced key in the config object (e.g.
config.ws,config.logging) rather than requiring the component to call your plugin directly. This keeps the component author's interface clean. -
Guard against missing components. In
onComponentDestroyed, always check that your per-component state exists before accessing it — a component may have been created before the plugin was registered.