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 trigger onComponentCreated.
  • Duplicate registrations are ignored. Calling wakaPAC.use(myPlugin) twice with the same object reference is safe — it silently skips the second registration.
  • createPacPlugin is required. If the library doesn't have a createPacPlugin method, 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 postMessage over sendMessage. Asynchronous dispatch avoids re-entrancy issues when delivering messages during initialization or inside other event handlers.
  • Keep createPacPlugin side-effect-free where possible. Exposing message constants on pac is fine, but avoid heavy setup here — defer per-component work to onComponentCreated.
  • 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.