Event Bus
Decoupled inter-plugin communication.
Overview
The EventBus enables plugins to communicate without direct dependencies. One plugin emits an event; any number of plugins can listen.
API
interface EventBus {
on<T = unknown>(event: string, handler: (data: T) => void): () => void;
emit<T = unknown>(event: string, data: T): void;
}
on()returns an unsubscribe function. Call it inteardown()to clean up.emit()synchronously calls all registered handlers for the event.
Store Mutation Bridge
The core automatically bridges BoardStore mutations to the EventBus. When a shape is added, updated, or deleted through the store, the corresponding mutation event is emitted on the bus.
// This happens automatically in core:
store.onMutation((event) => {
events.emit(event.type, event.payload);
});
This means plugins can listen for store changes without directly subscribing to the store.
Usage Example: Snap Plugin
// Keep unsubscribe references in a closure, not on the plugin object
let unsub: (() => void) | null = null;
const snapPlugin: UsketchPlugin = {
id: "usketch-plugin-snap",
name: "Snap",
setup(ctx: PluginContext) {
unsub = ctx.events.on("tool:drag", (event) => {
const snapped = calculateSnap(event.point, event.shapes);
updateSnapGuides(snapped);
});
},
teardown() {
unsub?.();
unsub = null;
},
};
Event Naming Conventions
While the event bus is fully dynamic (any string works), we recommend these patterns:
| Pattern | Example | Description |
|---|---|---|
{system}:{action} | tool:activate | Core system events |
shape:{action} | shape:added | Shape lifecycle events |
plugin:{id}:{action} | plugin:snap:calculated | Plugin-specific events |
Tips
- Keep event payloads serializable when possible — this helps with debugging and future sync scenarios.
- Prefer events over direct cross-plugin imports. If plugin A needs data from plugin B, have B emit an event rather than exposing an internal API.
- The
EventBusis synchronous. If you need async processing, handle it inside your handler.