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 in teardown() 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:

PatternExampleDescription
{system}:{action}tool:activateCore system events
shape:{action}shape:addedShape lifecycle events
plugin:{id}:{action}plugin:snap:calculatedPlugin-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 EventBus is synchronous. If you need async processing, handle it inside your handler.