Skip to content

Events ​

EventAdmin is a built-in Pandino service for topic-based publish-subscribe messaging. It lets bundles communicate without direct dependencies -- publishers send events to named topics, and subscribers receive them by declaring interest in those topics.

Core Concepts ​

The event system has three parts:

  1. Event -- A message with a topic string and a properties map.
  2. EventAdmin -- The service used to send events.
  3. EventHandler -- The interface subscribers implement to receive events.

The Event Class ​

An Event carries a topic and optional properties:

typescript
import { Event } from '@pandino/pandino';

const event = new Event('user/login', {
  userId: '12345',
  timestamp: Date.now(),
  source: 'auth-service',
});

console.log(event.getTopic());             // 'user/login'
console.log(event.getProperty('userId'));   // '12345'
console.log(event.getPropertyNames());      // ['userId', 'timestamp', 'source']
console.log(event.containsProperty('ip'));  // false

Topics use a path-like structure (e.g., 'user/login', 'order/created', 'system/shutdown'). This hierarchical naming enables wildcard subscriptions.

Sending Events ​

First, obtain the EventAdmin service:

typescript
import type { BundleContext } from '@pandino/pandino';
import type { EventAdmin } from '@pandino/pandino';
import { Event } from '@pandino/pandino';

async start(context: BundleContext): Promise<void> {
  const ref = context.getServiceReference<EventAdmin>('EventAdmin');
  const eventAdmin = context.getService(ref!);
}

Then send events using one of two methods:

Synchronous: sendEvent() ​

sendEvent() delivers the event to all matching handlers synchronously and blocks until all handlers have finished processing:

typescript
eventAdmin.sendEvent(new Event('order/created', {
  orderId: 'ORD-001',
  total: 99.99,
}));
// All handlers have finished by this point

Use sendEvent() when you need to ensure all handlers have processed the event before continuing (e.g., validation, pre-commit checks).

Asynchronous: postEvent() ​

postEvent() delivers the event asynchronously -- it returns immediately and handlers are invoked later:

typescript
eventAdmin.postEvent(new Event('user/login', {
  userId: '12345',
  timestamp: Date.now(),
}));
// Returns immediately; handlers run asynchronously

Use postEvent() for fire-and-forget notifications where you do not need to wait for handler completion.

Subscribing to Events ​

To receive events, implement the EventHandler interface and register it as a service with the event.topics property:

typescript
import type {
  BundleActivator,
  BundleContext,
  ServiceRegistration,
} from '@pandino/pandino';
import type { EventHandler } from '@pandino/pandino';
import type { Event } from '@pandino/pandino';

class UserLoginHandler implements EventHandler {
  handleEvent(event: Event): void {
    const userId = event.getProperty('userId');
    const timestamp = event.getProperty('timestamp');
    console.log(`User ${userId} logged in at ${new Date(timestamp).toISOString()}`);
  }
}

export default class Activator implements BundleActivator {
  private registration?: ServiceRegistration<EventHandler>;

  async start(context: BundleContext): Promise<void> {
    this.registration = context.registerService(
      'EventHandler',
      new UserLoginHandler(),
      { 'event.topics': 'user/login' },
    );
  }

  async stop(context: BundleContext): Promise<void> {
    this.registration?.unregister();
  }
}

The event.topics property tells EventAdmin which topics this handler cares about.

Topic Wildcards ​

Use * as the last segment to subscribe to all events under a topic prefix:

PatternMatches
'user/login'Only 'user/login'
'user/*''user/login', 'user/logout', 'user/registered', etc.
'order/*''order/created', 'order/shipped', 'order/cancelled', etc.
'*'All events on all topics
typescript
// Subscribe to all user-related events
context.registerService('EventHandler', new UserEventHandler(), {
  'event.topics': 'user/*',
});

Using Events with Declarative Services ​

You can use SCR decorators to create event handlers without a manual BundleActivator. Use @Component, @Service, and @Property to declare the handler:

typescript
import { Component, Service, Property } from '@pandino/decorators';
import type { EventHandler } from '@pandino/pandino';
import type { Event } from '@pandino/pandino';

@Component({ name: 'com.example.order-audit', immediate: true })
@Service({ interfaces: ['EventHandler'] })
@Property('event.topics', 'order/*')
class OrderAuditHandler implements EventHandler {
  handleEvent(event: Event): void {
    const topic = event.getTopic();
    const orderId = event.getProperty('orderId');
    console.log(`[Audit] ${topic}: order ${orderId}`);
  }
}

This is equivalent to manually registering an EventHandler service with the event.topics property, but the SCR manages the lifecycle for you.

Practical Example: System Notifications ​

Here is a complete example showing a notification dispatcher and multiple subscribers:

Publisher -- sends events when system actions occur:

typescript
import { Component, Service, Activate } from '@pandino/decorators';
import type { ComponentContext } from '@pandino/pandino';
import type { EventAdmin } from '@pandino/pandino';
import { Event } from '@pandino/pandino';

@Component({ name: 'com.example.user-service', immediate: true })
@Service({ interfaces: ['UserService'] })
class UserServiceComponent implements UserService {
  private eventAdmin?: EventAdmin;

  @Activate
  activate(context: ComponentContext): void {
    const ref = context.getBundleContext().getServiceReference<EventAdmin>('EventAdmin');
    if (ref) {
      this.eventAdmin = context.getBundleContext().getService(ref) ?? undefined;
    }
  }

  login(userId: string): void {
    // ... perform login logic ...

    this.eventAdmin?.postEvent(new Event('user/login', {
      userId,
      timestamp: Date.now(),
    }));
  }

  logout(userId: string): void {
    // ... perform logout logic ...

    this.eventAdmin?.postEvent(new Event('user/logout', {
      userId,
      timestamp: Date.now(),
    }));
  }
}

Subscriber -- logs all user activity:

typescript
import { Component, Service, Property } from '@pandino/decorators';
import type { EventHandler } from '@pandino/pandino';
import type { Event } from '@pandino/pandino';

@Component({ name: 'com.example.user-activity-logger', immediate: true })
@Service({ interfaces: ['EventHandler'] })
@Property('event.topics', 'user/*')
class UserActivityLogger implements EventHandler {
  handleEvent(event: Event): void {
    console.log(`[Activity] ${event.getTopic()} - user: ${event.getProperty('userId')}`);
  }
}

Subscriber -- tracks login metrics:

typescript
import { Component, Service, Property } from '@pandino/decorators';
import type { EventHandler } from '@pandino/pandino';
import type { Event } from '@pandino/pandino';

@Component({ name: 'com.example.login-metrics', immediate: true })
@Service({ interfaces: ['EventHandler'] })
@Property('event.topics', 'user/login')
class LoginMetricsHandler implements EventHandler {
  private loginCount = 0;

  handleEvent(event: Event): void {
    this.loginCount++;
    console.log(`Total logins: ${this.loginCount}`);
  }
}

The publisher does not know about either subscriber. New subscribers can be added or removed at any time without modifying the publisher.

Next Steps ​

Released under the Eclipse Public License 2.0.