matchesContext(contextInfo, contextDescriptor)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Stop listening for an event relying on SessionData and relayed by the
   * message handler.
   *
   * @param {string} event
   *     Name of the event to unsubscribe from.
   * @param {ContextDescriptor} contextDescriptor
   *     Context descriptor for this event.
   * @param {Function} callback
   *     Event listener callback.
   * @returns {Promise}
   *     Promise which resolves when the event fully unsubscribed, including
   *     propagating the necessary session data.
   */
  async off(event, contextDescriptor, callback) {
    return this.update([{ event, contextDescriptor, callback, enable: false }]);
  }

  /**
   * Listen for an event relying on SessionData and relayed by the message
   * handler.
   *
   * @param {string} event
   *     Name of the event to subscribe to.
   * @param {ContextDescriptor} contextDescriptor
   *     Context descriptor for this event.
   * @param {Function} callback
   *     Event listener callback.
   * @returns {Promise}
   *     Promise which resolves when the event fully subscribed to, including
   *     propagating the necessary session data.
   */
  async on(event, contextDescriptor, callback) {
    return this.update([{ event, contextDescriptor, callback, enable: true }]);
  }

  /**
   * An object that holds information about subscription/unsubscription
   * of an event.
   *
   * @typedef Subscription
   *
   * @param {string} event
   *     Name of the event to subscribe/unsubscribe to.
   * @param {ContextDescriptor} contextDescriptor
   *     Context descriptor for this event.
   * @param {Function} callback
   *     Event listener callback.
   * @param {boolean} enable
   *     True, if we need to subscribe to an event.
   *     Otherwise false.
   */

  /**
   * Start or stop listening to a list of events relying on SessionData
   * and relayed by the message handler.
   *
   * @param {Array<Subscription>} subscriptions
   *     The list of information to subscribe/unsubscribe to.
   *
   * @returns {Promise}
   *     Promise which resolves when the events fully subscribed/unsubscribed to,
   *     including propagating the necessary session data.
   */
  async update(subscriptions) {
    const sessionDataItemUpdates = [];
    subscriptions.forEach(subscription => {
      // Skip invalid subscriptions
      if (subscription === null) {
        return;
      }

      const { event, contextDescriptor, callback, enable } = subscription;
      if (enable) {
        // Setup listeners.
        if (!this.#listenersByEventName.has(event)) {
          this.#listenersByEventName.set(event, new Map());
          this.#messageHandler.on(event, this.#onMessageHandlerEvent);
        }

        const key = this.#getContextKey(contextDescriptor);
        const listeners = this.#listenersByEventName.get(event);
        if (listeners.has(key)) {
          const { callbacks } = listeners.get(key);
          callbacks.add(callback);
        } else {
          const callbacks = new Set([callback]);
          listeners.set(key, { callbacks, contextDescriptor });

          sessionDataItemUpdates.push({
            ...this.#getSessionDataItem(event, contextDescriptor),
            method: lazy.SessionDataMethod.Add,
          });
        }
      } else {
        // Remove listeners.
        const listeners = this.#listenersByEventName.get(event);
        if (!listeners) {
          return;
        }

        const key = this.#getContextKey(contextDescriptor);
        if (!listeners.has(key)) {
          return;
        }

        const { callbacks } = listeners.get(key);
        if (callbacks.has(callback)) {
          callbacks.delete(callback);
          if (callbacks.size === 0) {
            listeners.delete(key);
            if (listeners.size === 0) {
              this.#messageHandler.off(event, this.#onMessageHandlerEvent);
              this.#listenersByEventName.delete(event);
            }

            sessionDataItemUpdates.push({
              ...this.#getSessionDataItem(event, contextDescriptor),
              method: lazy.SessionDataMethod.Remove,
            });
          }
        }
      }
    });

    // Update all sessionData at once.
    await this.#messageHandler.updateSessionData(sessionDataItemUpdates);
  }

  #getContextKey(contextDescriptor) {
    const { id, type } = contextDescriptor;
    return `${type}-${id}`;
  }

  #getSessionDataItem(event, contextDescriptor) {
    const [moduleName] = event.split(".");
    return {
      moduleName,
      category: lazy.SessionDataCategory.Event,
      contextDescriptor,
      values: [event],
    };
  }

  #matchesContext(contextInfo, contextDescriptor) {
    const eventBrowsingContext = BrowsingContext.get(contextInfo.contextId);
    return this.#messageHandler.contextMatchesDescriptor(
      eventBrowsingContext,
      contextDescriptor
    );
  }

  #onMessageHandlerEvent = (name, event, contextInfo) => {
    const listeners = this.#listenersByEventName.get(name);
    for (const { callbacks, contextDescriptor } of listeners.values()) {
      if (!this.#matchesContext(contextInfo, contextDescriptor)) {
        continue;
      }

      for (const callback of callbacks) {
        try {
          callback(name, event);
        } catch (e) {
          lazy.logger.debug(
            `Error while executing callback for ${name}: ${e.message}`
          );
        }
      }
    }
  };
}
PK