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 {
// Known CoreEventMap events get typed emit/on overloads
on<K extends keyof CoreEventMap>(event: K, handler: (data: CoreEventMap[K]) => void): () => void;
on<T = unknown>(event: string, handler: (data: T) => void): () => void;
emit<K extends keyof CoreEventMap>(event: K, data: CoreEventMap[K]): void;
emit<T = unknown>(event: string, data: T): void;
pause(): void; // Suspend event delivery (ref-counted; nests safely)
resume(): void; // Release one pause; delivery resumes when the count hits zero
isPaused(): boolean;
}
on()returns an unsubscribe function. Return it fromsetupto clean up.emit()synchronously calls all registered handlers for the event.- Known events declared in
CoreEventMap(e.g.canvas:pointerdown) get type-safeemit/onoverloads. Unknown string events fall back to theT = unknownsignature. pause()/resume()are ref-counted and let you suspend delivery. Eachpause()MUST be paired with exactly oneresume()(leaking apause()silently drops every subsequent event). UseisPaused()to check the current state.
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
const snapPlugin: UsketchPlugin = {
id: "usketch-plugin-snap",
name: "Snap",
setup(ctx: PluginContext) {
// Return the unsubscribe from on() directly as setup's teardown
return ctx.events.on("tool:drag", (event) => {
const snapped = calculateSnap(event.point, event.shapes);
updateSnapGuides(snapped);
});
},
};
To unsubscribe from multiple events, return a closure that calls each unsubscribe:
const multiPlugin: UsketchPlugin = {
id: "usketch-plugin-multi",
name: "Multi",
setup(ctx: PluginContext) {
const offDrag = ctx.events.on("tool:drag", handleDrag);
const offDrop = ctx.events.on("canvas:drop", handleDrop);
return () => {
offDrag();
offDrop();
};
},
};
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.