Help on events.on Unsubscription in Panel-to-Modal Communication

Hi Forge Team,

Our team is developing a feature where a Custom UI panel on a Jira issue opens a modal for user input. We’re using the @forge/bridge/events API for the modal to communicate with the parent panel, but we’ve run into a very puzzling issue with unsubscribing from listeners that we’re hoping you can clarify.

We’ve found ourselves in a difficult spot where we seem to face a dilemma with two outcomes, neither of which allows for a robust, memory-safe implementation.

  1. If we call events.emit immediately after events.on, the communication fails. The parent panel never receives the event. Curiously, the Promise from events.on resolves immediately in this case, but it’s not helpful as no response is ever sent back.

  2. To get the communication working, we have to wrap events.emit in a setTimeout. This works perfectly! The parent gets the message and sends a response back to the modal’s listener. However, when we do this, the Promise from events.on never resolves, which leaves us with no way to call .unsubscribe() and creates a memory leak.

This brings us to our core question. We feel like we’re caught in a Catch-22. The documented method for unsubscribing seems to be unreachable in the very scenario where communication actually works.

Given this behavior, could you please provide a working example of how subscription.unsubscribe() can be successfully invoked in this specific, asynchronous panel-to-modal scenario?

Here is a simplified code snippet from our modal that demonstrates the issue:

import { events } from "@forge/bridge";

function requestDataFromParent() {
  return new Promise((resolve) => {
    const listener = (payload) => {
      console.log("Response received!");
      // This listener works, but we have no way to unsubscribe it.
      resolve(payload);
    };

    // We register the listener...
    events.on('RESPONSE_EVENT', listener)
      .then(subscription => {
        // ...but this block ONLY runs if we DON'T use setTimeout,
        // in which case the communication itself fails.
        console.log("This block is unreachable in a working scenario.");
        subscription.unsubscribe();
      });

    // We MUST use setTimeout for the parent to receive the event.
    setTimeout(() => {
        events.emit('REQUEST_EVENT', { /* ... */ });
    }, 0);
  });
}

We’d appreciate any insight or examples you can provide. Thanks for your help!

Hi everyone,

It turns out the main problem was a classic case of the debugger interfering with the code! It appears using breakpoints was disrupting the event timing, which led to some very confusing results. After testing without them, the API works as expected, with one important caveat.

The final working pattern requires wrapping the events.emit() call in a setTimeout(..., 0). This avoids a race condition and allows the Promise from events.on() to resolve correctly, so you can then use the documented .then(sub => sub.unsubscribe()) method to prevent memory leaks.

Here’s the final function that handles everything correctly:

import { events } from "@forge/bridge";

/**
 * Sends a request from a modal to its parent panel and returns a promise
 * that resolves with the response.
 */
function requestDataFromParent() {
  return new Promise((resolve, reject) => {
    let subscription = null;

    const listener = (payload) => {
      if (subscription) {
        subscription.unsubscribe();
      }
      resolve(payload);
    };

    events.on('RESPONSE_EVENT', listener)
      .then(sub => {
        subscription = sub;
      })
      .catch(reject);

    // This setTimeout is the key to preventing a race condition.
    setTimeout(() => {
        events.emit('REQUEST_EVENT', { /* ... */ });
    }, 0);
  });
}

Hope this helps anyone else facing a similar issue!