Purpose

The purpose of this document is to introduce messaging infrastructure available at IntelliJ IDEA to the product's developers and plugin writers. It is intended to answer why, when and how to use it.

Table of contents

Rationale

So, what is messaging in IntelliJ IDEA and why do we need it? Basically, it's implementation of Observer pattern that provides additional features like 'broadcasting on hierarchy' and special 'nested events' processing ('nested event' here is a situation when new event is fired (directly or indirectly) from the callback of another event).

Design

Here are the main components of the messaging API.

Topic

This class serves as an endpoint at the messaging infrastructure. I.e. clients are allowed to subscribe to the topic within particular bus and to send messages to particular topic within particular bus.

Message bus

Is the core of the messaging system. Is used at the following scenarios:

Connection

Manages all subscriptions for particular client within particular bus.

Putting altogether

Defining business interface and topic

public interface ChangeActionNotifier {

    Topic<ChangeActionNotifier> CHANGE_ACTION_TOPIC = Topic.create("custom name", ChangeActionNotifier.class)

    void beforeAction(Context context);
    void afterAction(Context context);
}

Subscribing

public void init(MessageBus bus) {
    bus.connect().subscribe(ActionTopics.CHANGE_ACTION_TOPIC, new ChangeActionNotifier() {
        @Override
        public void beforeAction(Context context) {
            // Process 'before action' event.
        }
        @Override
        public void afterAction(Context context) {
            // Process 'after action' event.
        }
    });
}

Publishing

public void doChange(Context context) {
    ChangeActionNotifier publisher = myBus.syncPublisher(ActionTopics.CHANGE_ACTION_TOPIC);
    publisher.beforeAction(context);
    try {
        // Do action
        // ...
    } finally {
        publisher.afterAction(context)
    }    
}

Existing resources

Broadcasting

Message buses can be organised into hierarchies. Moreover, IntelliJ IDEA has them already:

That allows to notify subscribers registered in one message bus on messages sent to another message bus.

Example:

Here we have a simple hierarchy ('application bus' is a parent of 'project bus') with three subscribers for the same topic.

We get the following if 'topic1' defines broadcast direction as 'TO_CHILDREN':

  1. A message is sent to 'topic1' via 'application bus';
  2. 'handler1' is notified about the message;
  3. The message is delivered to the subscribers of the same topic within 'project bus' ('handler2' and 'handler3');

Benefits

We don't need to bother with memory management of subscribers that are bound to child buses but interested in parent bus-level events.

Consider the example above - we may want to have project-specific functionality that reacts to the application-level events. All we need to do is to subscribe to the target topic within the 'project bus'. No hard reference to the project-level subscriber will be stored at application-level then, i.e. we just avoided memory leak on project re-opening.

Options

Broadcast configuration is defined per-topic. Following options are available:

Nested messages

'Nested message' is a message sent (directly or indirectly) during another message processing. Messaging infrastructure of IntelliJ IDEA guarantees that all messages sent to particular topic will be delivered at the sending order.

Example:

Suppose we have the following configuration:

Let's see what happens if someone sends a message to the target topic:

Tips'n'tricks

Relief listeners management

Messaging infrastructure is very light-weight, so, it's possible to reuse it at local sub-systems in order to relief Observers construction. Let's see what is necessary to do then:

  1. Define business interface to work with;
  2. Create shared message bus and topic that uses the interface above ('shared' here means that either 'subject' or 'observers' know about them);

Let's compare that with a manual implementation:

  1. Define listener interface (business interface);
  2. Provide reference to the 'subject' to all interested listeners;
  3. Add listeners storage and listeners management methods (add/remove) to the 'subject';
  4. Manually iterate all listeners and call target callback in all places where new event is fired;

Avoid shared data modification from subscribers

We had a problem in a situation when two subscribers tried to modify the same document (IDEA-71701). The thing is that every document change is performed by the following scenario:

  1. 'before change' event is sent to all document listeners and some of them publish new messages during that;
  2. actual change is performed;
  3. 'after change' event is sent to all document listeners;

We had the following then:

  1. message1 is sent to the topic with two subscribers;
  2. message1 is queued for both subscribers;
  3. message1 delivery starts;
  4. subscriber1 receives message1;
  5. subscriber1 issues document modification request at particular range (e.g. 'document.delete(startOffset, endOffset)');
  6. 'before change' notification is sent to the document listeners;
  7. message2 is sent by one of the standard document listeners to another topic within the same message bus during 'before change' processing;
  8. the bus tries to deliver all pending messages before queuing message2;
  9. subscriber2 receives message1 and also modifies a document;
  10. the call stack is unwinded and 'actual change' phase of document modification operation requested by subscriber1 begins;

The problem is that document range used by subscriber1 for initial modification request is invalid if subscriber2 has changed document's range before it.