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 from setup to clean up.
  • emit() synchronously calls all registered handlers for the event.
  • Known events declared in CoreEventMap (e.g. canvas:pointerdown) get type-safe emit/on overloads. Unknown string events fall back to the T = unknown signature.
  • pause() / resume() are ref-counted and let you suspend delivery. Each pause() MUST be paired with exactly one resume() (leaking a pause() silently drops every subsequent event). Use isPaused() 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:

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.