tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

EventsDispatcher.sys.mjs (8002B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  Log: "chrome://remote/content/shared/Log.sys.mjs",
      9  SessionDataCategory:
     10    "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs",
     11  SessionDataMethod:
     12    "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs",
     13 });
     14 
     15 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
     16 
     17 /**
     18 * Helper to listen to events which rely on SessionData.
     19 * In order to support the EventsDispatcher, a module emitting events should
     20 * subscribe and unsubscribe to those events based on SessionData updates
     21 * and should use the "event" SessionData category.
     22 */
     23 export class EventsDispatcher {
     24  // The MessageHandler owning this EventsDispatcher.
     25  #messageHandler;
     26 
     27  /**
     28   * @typedef {object} EventListenerInfo
     29   * @property {ContextDescriptor} contextDescriptor
     30   *     The ContextDescriptor to which those callbacks are associated
     31   * @property {Set<Function>} callbacks
     32   *     The callbacks to trigger when an event matching the ContextDescriptor
     33   *     is received.
     34   */
     35 
     36  // Map from event name to map of strings (context keys) to EventListenerInfo.
     37  #listenersByEventName;
     38 
     39  /**
     40   * Create a new EventsDispatcher instance.
     41   *
     42   * @param {MessageHandler} messageHandler
     43   *     The MessageHandler owning this EventsDispatcher.
     44   */
     45  constructor(messageHandler) {
     46    this.#messageHandler = messageHandler;
     47 
     48    this.#listenersByEventName = new Map();
     49  }
     50 
     51  destroy() {
     52    for (const event of this.#listenersByEventName.keys()) {
     53      this.#messageHandler.off(event, this.#onMessageHandlerEvent);
     54    }
     55 
     56    this.#listenersByEventName = null;
     57  }
     58 
     59  /**
     60   * Check for existing listeners for a given event name and a given context.
     61   *
     62   * @param {string} name
     63   *     Name of the event to check.
     64   * @param {ContextInfo} contextInfo
     65   *     ContextInfo identifying the context to check.
     66   *
     67   * @returns {boolean}
     68   *     True if there is a registered listener matching the provided arguments.
     69   */
     70  hasListener(name, contextInfo) {
     71    if (!this.#listenersByEventName.has(name)) {
     72      return false;
     73    }
     74 
     75    const listeners = this.#listenersByEventName.get(name);
     76    for (const { contextDescriptor } of listeners.values()) {
     77      if (this.#matchesContext(contextInfo, contextDescriptor)) {
     78        return true;
     79      }
     80    }
     81    return false;
     82  }
     83 
     84  /**
     85   * Stop listening for an event relying on SessionData and relayed by the
     86   * message handler.
     87   *
     88   * @param {string} event
     89   *     Name of the event to unsubscribe from.
     90   * @param {ContextDescriptor} contextDescriptor
     91   *     Context descriptor for this event.
     92   * @param {Function} callback
     93   *     Event listener callback.
     94   * @returns {Promise}
     95   *     Promise which resolves when the event fully unsubscribed, including
     96   *     propagating the necessary session data.
     97   */
     98  async off(event, contextDescriptor, callback) {
     99    return this.update([{ event, contextDescriptor, callback, enable: false }]);
    100  }
    101 
    102  /**
    103   * Listen for an event relying on SessionData and relayed by the message
    104   * handler.
    105   *
    106   * @param {string} event
    107   *     Name of the event to subscribe to.
    108   * @param {ContextDescriptor} contextDescriptor
    109   *     Context descriptor for this event.
    110   * @param {Function} callback
    111   *     Event listener callback.
    112   * @returns {Promise}
    113   *     Promise which resolves when the event fully subscribed to, including
    114   *     propagating the necessary session data.
    115   */
    116  async on(event, contextDescriptor, callback) {
    117    return this.update([{ event, contextDescriptor, callback, enable: true }]);
    118  }
    119 
    120  /**
    121   * An object that holds information about subscription/unsubscription
    122   * of an event.
    123   *
    124   * @typedef Subscription
    125   *
    126   * @param {string} event
    127   *     Name of the event to subscribe/unsubscribe to.
    128   * @param {ContextDescriptor} contextDescriptor
    129   *     Context descriptor for this event.
    130   * @param {Function} callback
    131   *     Event listener callback.
    132   * @param {boolean} enable
    133   *     True, if we need to subscribe to an event.
    134   *     Otherwise false.
    135   */
    136 
    137  /**
    138   * Start or stop listening to a list of events relying on SessionData
    139   * and relayed by the message handler.
    140   *
    141   * @param {Array<Subscription>} subscriptions
    142   *     The list of information to subscribe/unsubscribe to.
    143   *
    144   * @returns {Promise}
    145   *     Promise which resolves when the events fully subscribed/unsubscribed to,
    146   *     including propagating the necessary session data.
    147   */
    148  async update(subscriptions) {
    149    const sessionDataItemUpdates = [];
    150    subscriptions.forEach(subscription => {
    151      // Skip invalid subscriptions
    152      if (subscription === null) {
    153        return;
    154      }
    155 
    156      const { event, contextDescriptor, callback, enable } = subscription;
    157      if (enable) {
    158        // Setup listeners.
    159        if (!this.#listenersByEventName.has(event)) {
    160          this.#listenersByEventName.set(event, new Map());
    161          this.#messageHandler.on(event, this.#onMessageHandlerEvent);
    162        }
    163 
    164        const key = this.#getContextKey(contextDescriptor);
    165        const listeners = this.#listenersByEventName.get(event);
    166        if (listeners.has(key)) {
    167          const { callbacks } = listeners.get(key);
    168          callbacks.add(callback);
    169        } else {
    170          const callbacks = new Set([callback]);
    171          listeners.set(key, { callbacks, contextDescriptor });
    172 
    173          sessionDataItemUpdates.push({
    174            ...this.#getSessionDataItem(event, contextDescriptor),
    175            method: lazy.SessionDataMethod.Add,
    176          });
    177        }
    178      } else {
    179        // Remove listeners.
    180        const listeners = this.#listenersByEventName.get(event);
    181        if (!listeners) {
    182          return;
    183        }
    184 
    185        const key = this.#getContextKey(contextDescriptor);
    186        if (!listeners.has(key)) {
    187          return;
    188        }
    189 
    190        const { callbacks } = listeners.get(key);
    191        if (callbacks.has(callback)) {
    192          callbacks.delete(callback);
    193          if (callbacks.size === 0) {
    194            listeners.delete(key);
    195            if (listeners.size === 0) {
    196              this.#messageHandler.off(event, this.#onMessageHandlerEvent);
    197              this.#listenersByEventName.delete(event);
    198            }
    199 
    200            sessionDataItemUpdates.push({
    201              ...this.#getSessionDataItem(event, contextDescriptor),
    202              method: lazy.SessionDataMethod.Remove,
    203            });
    204          }
    205        }
    206      }
    207    });
    208 
    209    // Update all sessionData at once.
    210    await this.#messageHandler.updateSessionData(sessionDataItemUpdates);
    211  }
    212 
    213  #getContextKey(contextDescriptor) {
    214    const { id, type } = contextDescriptor;
    215    return `${type}-${id}`;
    216  }
    217 
    218  #getSessionDataItem(event, contextDescriptor) {
    219    const [moduleName] = event.split(".");
    220    return {
    221      moduleName,
    222      category: lazy.SessionDataCategory.Event,
    223      contextDescriptor,
    224      values: [event],
    225    };
    226  }
    227 
    228  #matchesContext(contextInfo, contextDescriptor) {
    229    const eventBrowsingContext = BrowsingContext.get(contextInfo.contextId);
    230    return this.#messageHandler.contextMatchesDescriptor(
    231      eventBrowsingContext,
    232      contextDescriptor
    233    );
    234  }
    235 
    236  #onMessageHandlerEvent = (name, event, contextInfo) => {
    237    const listeners = this.#listenersByEventName.get(name);
    238    for (const { callbacks, contextDescriptor } of listeners.values()) {
    239      if (!this.#matchesContext(contextInfo, contextDescriptor)) {
    240        continue;
    241      }
    242 
    243      for (const callback of callbacks) {
    244        try {
    245          callback(name, event);
    246        } catch (e) {
    247          lazy.logger.debug(
    248            `Error while executing callback for ${name}: ${e.message}`
    249          );
    250        }
    251      }
    252    }
    253  };
    254 }