tor-browser

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

session.sys.mjs (31207B)


      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 import { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
     11  ContextDescriptorType:
     12    "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
     13  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     14  generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
     15  getWebDriverSessionById:
     16    "chrome://remote/content/shared/webdriver/Session.sys.mjs",
     17  NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs",
     18  pprint: "chrome://remote/content/shared/Format.sys.mjs",
     19  RootMessageHandler:
     20    "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
     21  TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
     22  UserContextManager:
     23    "chrome://remote/content/shared/UserContextManager.sys.mjs",
     24 });
     25 class SessionModule extends RootBiDiModule {
     26  #knownSubscriptionIds;
     27  #subscriptions;
     28 
     29  /**
     30   * An object that holds information about the subscription,
     31   * if the <var>topLevelTraversableIds</var> and the <var>userContextIds</var>
     32   * are both empty, the subscription is considered global.
     33   *
     34   * @typedef Subscription
     35   *
     36   * @property {Set} eventNames
     37   *     A set of event names related to this subscription.
     38   * @property {string} subscriptionId
     39   *     A unique subscription identifier.
     40   * @property {Set} topLevelTraversableIds
     41   *     A set of top level traversable ids related to this subscription.
     42   * @property {Set} userContextIds
     43   *     A set of user context ids related to this subscription.
     44   */
     45 
     46  constructor(messageHandler) {
     47    super(messageHandler);
     48 
     49    // Set of subscription ids.
     50    this.#knownSubscriptionIds = new Set();
     51    // List of subscription objects type Subscription.
     52    this.#subscriptions = [];
     53  }
     54 
     55  destroy() {
     56    this.#knownSubscriptionIds = null;
     57    this.#subscriptions = null;
     58  }
     59 
     60  /**
     61   * Commands
     62   */
     63 
     64  /**
     65   * End the current session.
     66   *
     67   * Session clean up will happen later in WebDriverBiDiConnection class.
     68   */
     69  async end() {
     70    const session = lazy.getWebDriverSessionById(this.messageHandler.sessionId);
     71 
     72    if (session.http) {
     73      throw new lazy.error.UnsupportedOperationError(
     74        "Ending a session started with WebDriver classic is not supported." +
     75          ' Use the WebDriver classic "Delete Session" command instead.'
     76      );
     77    }
     78  }
     79 
     80  /**
     81   * An object that holds a unique subscription identifier.
     82   *
     83   * @typedef SubscribeResult
     84   *
     85   * @property {string} subscription
     86   *     A unique subscription identifier.
     87   */
     88 
     89  /**
     90   * Enable certain events either globally, or for a list of browsing contexts.
     91   *
     92   * @param {object=} params
     93   * @param {Array<string>} params.events
     94   *     List of events to subscribe to.
     95   * @param {Array<string>=} params.contexts
     96   *     Optional list of top-level browsing context ids
     97   *     to subscribe the events for.
     98   * @param {Array<string>=} params.userContexts
     99   *     Optional list of user context ids
    100   *     to subscribe the events for.
    101   *
    102   * @returns {SubscribeResult}
    103   *     A unique subscription identifier.
    104   * @throws {InvalidArgumentError}
    105   *     If <var>events</var> or <var>contexts</var> are not valid types.
    106   */
    107  async subscribe(params = {}) {
    108    const { events, contexts: contextIds = null, userContexts = null } = params;
    109 
    110    // Check input types until we run schema validation.
    111    this.#assertNonEmptyArrayWithStrings(events, "events");
    112 
    113    if (contextIds !== null) {
    114      this.#assertNonEmptyArrayWithStrings(contextIds, "contexts");
    115    }
    116 
    117    if (userContexts !== null) {
    118      this.#assertNonEmptyArrayWithStrings(userContexts, "userContexts");
    119    }
    120 
    121    const eventNames = new Set();
    122    events.forEach(name => {
    123      this.#obtainEvents(name).forEach(event => eventNames.add(event));
    124    });
    125 
    126    const inputUserContextIds = new Set(userContexts);
    127    const inputContextIds = new Set(contextIds);
    128 
    129    if (inputUserContextIds.size > 0 && inputContextIds.size > 0) {
    130      throw new lazy.error.InvalidArgumentError(
    131        `Providing both "userContexts" and "contexts" arguments is not supported`
    132      );
    133    }
    134 
    135    let subscriptionNavigables = new Set();
    136    const topLevelTraversableContextIds = new Set();
    137    const userContextIds = new Set();
    138 
    139    if (inputContextIds.size !== 0) {
    140      const navigables = this.#getValidNavigablesByIds(inputContextIds);
    141      subscriptionNavigables = this.#getTopLevelTraversables(navigables);
    142 
    143      for (const navigable of subscriptionNavigables) {
    144        topLevelTraversableContextIds.add(
    145          lazy.NavigableManager.getIdForBrowsingContext(navigable)
    146        );
    147      }
    148    } else if (inputUserContextIds.size !== 0) {
    149      for (const userContextId of inputUserContextIds) {
    150        const internalId =
    151          lazy.UserContextManager.getInternalIdById(userContextId);
    152 
    153        if (internalId === null) {
    154          throw new lazy.error.NoSuchUserContextError(
    155            `User context with id: ${userContextId} doesn't exist`
    156          );
    157        }
    158 
    159        lazy.UserContextManager.getTabsForUserContext(internalId).forEach(
    160          item => subscriptionNavigables.add(item)
    161        );
    162 
    163        userContextIds.add(internalId);
    164      }
    165    } else {
    166      for (const tab of lazy.TabManager.allTabs) {
    167        subscriptionNavigables.add(tab);
    168      }
    169    }
    170 
    171    const subscription = {
    172      eventNames,
    173      subscriptionId: lazy.generateUUID(),
    174      topLevelTraversableIds: topLevelTraversableContextIds,
    175      userContextIds,
    176    };
    177 
    178    const subscribeStepEvents = new Map();
    179 
    180    for (const eventName of eventNames) {
    181      const existingNavigables =
    182        this.#getEnabledTopLevelTraversables(eventName);
    183 
    184      subscribeStepEvents.set(
    185        eventName,
    186        subscriptionNavigables.difference(existingNavigables)
    187      );
    188    }
    189 
    190    this.#subscriptions.push(subscription);
    191    this.#knownSubscriptionIds.add(subscription.subscriptionId);
    192 
    193    // TODO: Bug 1801284. Add subscribe priority sorting of subscribeStepEvents (step 4 to 6, and 8).
    194 
    195    const includeGlobal = this.#isSubscriptionGlobal(subscription);
    196 
    197    const listeners = this.#getListenersToSubscribe(
    198      eventNames,
    199      includeGlobal,
    200      subscribeStepEvents,
    201      userContextIds
    202    );
    203 
    204    // Subscribe to the relevant engine-internal events.
    205    await this.messageHandler.eventsDispatcher.update(listeners);
    206 
    207    return { subscription: subscription.subscriptionId };
    208  }
    209 
    210  /**
    211   * Disable certain events either globally, for a list of browsing contexts
    212   * or for a list of subscription ids.
    213   *
    214   * @param {object=} params
    215   * @param {Array<string>=} params.events
    216   *     List of events to unsubscribe from.
    217   * @param {Array<string>=} params.contexts
    218   *     Optional list of top-level browsing context ids
    219   *     to unsubscribe the events from.
    220   * @param {Array<string>=} params.subscriptions
    221   *     List of subscription identifiers to unsubscribe from.
    222   *
    223   * @throws {InvalidArgumentError}
    224   *     If <var>events</var> or <var>contexts</var> are not valid types.
    225   */
    226  async unsubscribe(params = {}) {
    227    const { events = null, contexts = null, subscriptions = null } = params;
    228 
    229    const listeners =
    230      subscriptions === null
    231        ? this.#unsubscribeByAttributes(events, contexts)
    232        : this.#unsubscribeById(subscriptions);
    233 
    234    // Unsubscribe from the relevant engine-internal events.
    235    await this.messageHandler.eventsDispatcher.update(listeners);
    236  }
    237 
    238  #assertModuleSupportsEvent(moduleName, event) {
    239    const rootModuleClass = this.#getRootModuleClass(moduleName);
    240    if (!rootModuleClass?.supportsEvent(event)) {
    241      throw new lazy.error.InvalidArgumentError(
    242        `${event} is not a valid event name`
    243      );
    244    }
    245  }
    246 
    247  #assertNonEmptyArrayWithStrings(array, variableName) {
    248    lazy.assert.isNonEmptyArray(
    249      array,
    250      `Expected "${variableName}" to be a non-empty array, ` +
    251        lazy.pprint`got ${array}`
    252    );
    253    array.forEach(item => {
    254      lazy.assert.string(
    255        item,
    256        `Expected elements of "${variableName}" to be a string, ` +
    257          lazy.pprint`got ${item}`
    258      );
    259    });
    260  }
    261 
    262  #createListener(
    263    enable,
    264    { eventName, traversableId = null, userContextId = null }
    265  ) {
    266    let contextDescriptor;
    267 
    268    if (traversableId === null && userContextId === null) {
    269      contextDescriptor = {
    270        type: lazy.ContextDescriptorType.All,
    271      };
    272    } else if (userContextId !== null) {
    273      contextDescriptor = {
    274        type: lazy.ContextDescriptorType.UserContext,
    275        id: userContextId,
    276      };
    277    } else {
    278      const traversable =
    279        lazy.NavigableManager.getBrowsingContextById(traversableId);
    280 
    281      if (traversable === null) {
    282        return null;
    283      }
    284 
    285      contextDescriptor = {
    286        type: lazy.ContextDescriptorType.TopBrowsingContext,
    287        id: traversable.browserId,
    288      };
    289    }
    290 
    291    return {
    292      event: eventName,
    293      contextDescriptor,
    294      callback: this.#onMessageHandlerEvent,
    295      enable,
    296    };
    297  }
    298 
    299  #createListenerToSubscribe(params) {
    300    return this.#createListener(true, params);
    301  }
    302 
    303  #createListenerToUnsubscribe(params) {
    304    return this.#createListener(false, params);
    305  }
    306 
    307  /**
    308   * Get a set of top-level traversables for which an event is enabled.
    309   *
    310   * @see https://w3c.github.io/webdriver-bidi/#set-of-top-level-traversables-for-which-an-event-is-enabled
    311   *
    312   * @param {string} eventName
    313   *     The name of the event.
    314   *
    315   * @returns {Array<BrowsingContext>}
    316   *     The list of top-level traversables.
    317   */
    318  #getEnabledTopLevelTraversables(eventName) {
    319    let result = new Set();
    320 
    321    for (const subscription of this.#getSubscriptionsForEvent(eventName)) {
    322      const { topLevelTraversableIds } = subscription;
    323 
    324      if (this.#isSubscriptionGlobal(subscription)) {
    325        for (const traversable of lazy.TabManager.allTabs) {
    326          result.add(traversable);
    327        }
    328 
    329        break;
    330      }
    331 
    332      result = this.#getNavigablesByIds(topLevelTraversableIds);
    333    }
    334 
    335    return result;
    336  }
    337 
    338  #getListenersToSubscribe(
    339    eventNames,
    340    includeGlobal,
    341    subscribeStepEvents,
    342    userContextIds
    343  ) {
    344    const listeners = [];
    345 
    346    for (const eventName of eventNames) {
    347      if (includeGlobal) {
    348        // Since we're going to subscribe to all top-level
    349        // traversable ids to not have duplicate subscriptions,
    350        // we have to unsubscribe from already subscribed.
    351        const alreadyEnabledTraversableIds =
    352          this.#obtainEventEnabledTraversableIds(eventName);
    353        for (const traversableId of alreadyEnabledTraversableIds) {
    354          listeners.push(
    355            this.#createListenerToUnsubscribe({
    356              eventName,
    357              traversableId,
    358            })
    359          );
    360        }
    361 
    362        // Also unsubscribe from already subscribed user contexts.
    363        const alreadyEnabledUserContextIds =
    364          this.#obtainEventEnabledUserContextIds(eventName);
    365        for (const userContextId of alreadyEnabledUserContextIds) {
    366          listeners.push(
    367            this.#createListenerToUnsubscribe({
    368              eventName,
    369              userContextId,
    370            })
    371          );
    372        }
    373 
    374        listeners.push(this.#createListenerToSubscribe({ eventName }));
    375      } else if (userContextIds.size !== 0) {
    376        for (const userContextId of userContextIds) {
    377          // Do nothing if the event has already a global subscription.
    378          if (this.#hasGlobalEventSubscription(eventName)) {
    379            continue;
    380          }
    381 
    382          // Since we're going to subscribe to all top-level
    383          // traversable ids which belongs to the certain user context
    384          // to not have duplicate subscriptions,
    385          // we have to unsubscribe from already subscribed.
    386          const alreadyEnabledTraversableIds =
    387            this.#obtainEventEnabledTraversableIds(eventName, userContextId);
    388          for (const traversableId of alreadyEnabledTraversableIds) {
    389            listeners.push(
    390              this.#createListenerToUnsubscribe({
    391                eventName,
    392                traversableId,
    393              })
    394            );
    395          }
    396 
    397          listeners.push(
    398            this.#createListenerToSubscribe({ eventName, userContextId })
    399          );
    400        }
    401      } else {
    402        for (const navigable of subscribeStepEvents.get(eventName)) {
    403          // Do nothing if the event has already a global subscription
    404          // or subscription to the associated user context.
    405          if (
    406            this.#hasGlobalEventSubscription(eventName) ||
    407            this.#hasSubscriptionByAssociatedUserContext(eventName, navigable)
    408          ) {
    409            continue;
    410          }
    411 
    412          const traversableId =
    413            lazy.NavigableManager.getIdForBrowsingContext(navigable);
    414          listeners.push(
    415            this.#createListenerToSubscribe({
    416              eventName,
    417              traversableId,
    418            })
    419          );
    420        }
    421      }
    422    }
    423 
    424    return listeners;
    425  }
    426 
    427  #getListenersToUnsubscribe(subscription) {
    428    const { eventNames, topLevelTraversableIds, userContextIds } = subscription;
    429    const listeners = [];
    430 
    431    for (const eventName of eventNames) {
    432      // Do nothing if there is a global subscription.
    433      if (this.#hasGlobalEventSubscription(eventName)) {
    434        continue;
    435      }
    436 
    437      if (this.#isSubscriptionGlobal(subscription)) {
    438        listeners.push(
    439          ...this.#getListenersToUnsubscribeFromGlobalSubscription(eventName)
    440        );
    441      } else if (userContextIds.size !== 0) {
    442        for (const userContextId of userContextIds) {
    443          listeners.push(
    444            ...this.#getListenersToUnsubscribeFromUserContext(
    445              eventName,
    446              userContextId
    447            )
    448          );
    449        }
    450      } else {
    451        for (const traversableId of topLevelTraversableIds) {
    452          listeners.push(
    453            this.#getListenersToUnsubscribeFromTraversable(
    454              eventName,
    455              traversableId
    456            )
    457          );
    458        }
    459      }
    460    }
    461 
    462    return listeners;
    463  }
    464 
    465  #getListenersToUnsubscribeFromGlobalSubscription(eventName) {
    466    // Unsubscribe from the global subscription.
    467    const listeners = [this.#createListenerToUnsubscribe({ eventName })];
    468 
    469    // Subscribe again to user contexts which have a subscription and
    470    // to traversables which have individual subscriptions,
    471    // but are not associated with subscribed user contexts.
    472    for (const item of this.#getSubscriptionsForEvent(eventName)) {
    473      for (const userContextId of item.userContextIds) {
    474        listeners.push(
    475          this.#createListenerToSubscribe({
    476            eventName,
    477            userContextId,
    478          })
    479        );
    480      }
    481 
    482      for (const traversableId of item.topLevelTraversableIds) {
    483        const traversable =
    484          lazy.NavigableManager.getBrowsingContextById(traversableId);
    485 
    486        // Do nothing if traversable doesn't exist anymore or
    487        // there is already a subscription to the associated user context.
    488        if (
    489          traversable === null ||
    490          this.#hasSubscriptionByAssociatedUserContext(eventName, traversable)
    491        ) {
    492          continue;
    493        }
    494 
    495        listeners.push(
    496          this.#createListenerToSubscribe({
    497            eventName,
    498            traversableId,
    499          })
    500        );
    501      }
    502    }
    503 
    504    return listeners;
    505  }
    506 
    507  #getListenersToUnsubscribeFromTraversable(eventName, traversableId) {
    508    // Do nothing if traversable is already closed or still has another subscription.
    509    const traversable =
    510      lazy.NavigableManager.getBrowsingContextById(traversableId);
    511 
    512    if (
    513      traversable === null ||
    514      this.#hasSubscriptionByAssociatedUserContext(eventName, traversable) ||
    515      this.#hasSubscriptionByTraversableId(eventName, traversableId)
    516    ) {
    517      return null;
    518    }
    519 
    520    return this.#createListenerToUnsubscribe({
    521      eventName,
    522      traversableId,
    523    });
    524  }
    525 
    526  #getListenersToUnsubscribeFromUserContext(eventName, userContextId) {
    527    // Do nothing if there is another subscription for this user context.
    528    if (this.#hasSubscriptionByUserContextId(eventName, userContextId)) {
    529      return [];
    530    }
    531 
    532    // Unsubscribe from the user context.
    533    const listeners = [
    534      this.#createListenerToUnsubscribe({ eventName, userContextId }),
    535    ];
    536 
    537    // Resubscribe to traversables which are associated with this user context and
    538    // have individual subscriptions.
    539    const alreadyEnabledTraversableIds = this.#obtainEventEnabledTraversableIds(
    540      eventName,
    541      userContextId
    542    );
    543    for (const traversableId of alreadyEnabledTraversableIds) {
    544      listeners.push(
    545        this.#createListenerToSubscribe({
    546          eventName,
    547          traversableId,
    548        })
    549      );
    550    }
    551 
    552    return listeners;
    553  }
    554 
    555  /**
    556   * Get a list of navigables by provided ids.
    557   *
    558   * @see https://w3c.github.io/webdriver-bidi/#get-navigables-by-ids
    559   *
    560   * @param {Set<string>} navigableIds
    561   *     The set of the navigable ids.
    562   *
    563   * @returns {Set<BrowsingContext>}
    564   *     The set of navigables.
    565   */
    566  #getNavigablesByIds(navigableIds) {
    567    const result = new Set();
    568 
    569    for (const navigableId of navigableIds) {
    570      const navigable =
    571        lazy.NavigableManager.getBrowsingContextById(navigableId);
    572 
    573      if (navigable !== null) {
    574        result.add(navigable);
    575      }
    576    }
    577 
    578    return result;
    579  }
    580 
    581  #getRootModuleClass(moduleName) {
    582    // Modules which support event subscriptions should have a root module
    583    // defining supported events.
    584    const rootDestination = { type: lazy.RootMessageHandler.type };
    585    const moduleClasses = this.messageHandler.getAllModuleClasses(
    586      moduleName,
    587      rootDestination
    588    );
    589 
    590    if (!moduleClasses.length) {
    591      throw new lazy.error.InvalidArgumentError(
    592        `Module ${moduleName} does not exist`
    593      );
    594    }
    595 
    596    return moduleClasses[0];
    597  }
    598 
    599  #getSubscriptionsForEvent(eventName) {
    600    return this.#subscriptions.filter(({ eventNames }) =>
    601      eventNames.has(eventName)
    602    );
    603  }
    604 
    605  #getTopLevelTraversableContextIds(contextIds) {
    606    const topLevelTraversableContextIds = new Set();
    607    const inputContextIds = new Set(contextIds);
    608 
    609    if (inputContextIds.size !== 0) {
    610      const navigables = this.#getValidNavigablesByIds(inputContextIds);
    611      const topLevelTraversable = this.#getTopLevelTraversables(navigables);
    612 
    613      for (const navigable of topLevelTraversable) {
    614        topLevelTraversableContextIds.add(
    615          lazy.NavigableManager.getIdForBrowsingContext(navigable)
    616        );
    617      }
    618    }
    619 
    620    return topLevelTraversableContextIds;
    621  }
    622 
    623  /**
    624   * Get a list of top-level traversables for provided navigables.
    625   *
    626   * @see https://w3c.github.io/webdriver-bidi/#get-top-level-traversables
    627   *
    628   * @param {Array<BrowsingContext>} navigables
    629   *     The list of the navigables.
    630   *
    631   * @returns {Set<BrowsingContext>}
    632   *     The set of top-level traversables.
    633   */
    634  #getTopLevelTraversables(navigables) {
    635    const result = new Set();
    636 
    637    for (const { top } of navigables) {
    638      result.add(top);
    639    }
    640 
    641    return result;
    642  }
    643 
    644  /**
    645   * Get a list of valid navigables by provided ids.
    646   *
    647   * @see https://w3c.github.io/webdriver-bidi/#get-valid-navigables-by-ids
    648   *
    649   * @param {Set<string>} navigableIds
    650   *     The set of the navigable ids.
    651   *
    652   * @returns {Set<BrowsingContext>}
    653   *     The set of navigables.
    654   * @throws {NoSuchFrameError}
    655   *     If the navigable cannot be found.
    656   */
    657  #getValidNavigablesByIds(navigableIds) {
    658    const result = new Set();
    659 
    660    for (const navigableId of navigableIds) {
    661      result.add(this._getNavigable(navigableId));
    662    }
    663 
    664    return result;
    665  }
    666 
    667  #hasGlobalEventSubscription(eventName) {
    668    for (const subscription of this.#getSubscriptionsForEvent(eventName)) {
    669      if (this.#isSubscriptionGlobal(subscription)) {
    670        return true;
    671      }
    672    }
    673 
    674    return false;
    675  }
    676 
    677  // Check if for a given event name and traversable there is
    678  // a subscription for a user context associated with this traversable.
    679  #hasSubscriptionByAssociatedUserContext(eventName, traversable) {
    680    if (traversable === null) {
    681      return false;
    682    }
    683 
    684    return this.#hasSubscriptionByUserContextId(
    685      eventName,
    686      traversable.originAttributes.userContextId
    687    );
    688  }
    689 
    690  #hasSubscriptionByTraversableId(eventName, traversableId) {
    691    for (const subscription of this.#getSubscriptionsForEvent(eventName)) {
    692      const { topLevelTraversableIds } = subscription;
    693 
    694      for (const topLevelTraversableId of topLevelTraversableIds) {
    695        if (topLevelTraversableId === traversableId) {
    696          return true;
    697        }
    698      }
    699    }
    700 
    701    return false;
    702  }
    703 
    704  #hasSubscriptionByUserContextId(eventName, userContextId) {
    705    for (const subscription of this.#getSubscriptionsForEvent(eventName)) {
    706      const { userContextIds } = subscription;
    707 
    708      if (userContextIds.has(userContextId)) {
    709        return true;
    710      }
    711    }
    712 
    713    return false;
    714  }
    715 
    716  /**
    717   * Identify if a provided subscription is global.
    718   *
    719   * @see https://w3c.github.io/webdriver-bidi/#subscription-global
    720   *
    721   * @param {Subscription} subscription
    722   *     A subscription object.
    723   *
    724   * @returns {boolean}
    725   *     Return true if the subscription is global, false otherwise.
    726   */
    727  #isSubscriptionGlobal(subscription) {
    728    return (
    729      subscription.topLevelTraversableIds.size === 0 &&
    730      subscription.userContextIds.size === 0
    731    );
    732  }
    733 
    734  /**
    735   * Obtain a list of event enabled traversable ids.
    736   *
    737   * @param {string} eventName
    738   *     The name of the event.
    739   * @param {string=} userContextId
    740   *     The user context id.
    741   *
    742   * @returns {Set<string>}
    743   *     The set of traversable ids.
    744   */
    745  #obtainEventEnabledTraversableIds(eventName, userContextId = null) {
    746    let traversableIds = new Set();
    747 
    748    for (const { topLevelTraversableIds } of this.#getSubscriptionsForEvent(
    749      eventName
    750    )) {
    751      if (topLevelTraversableIds.size === 0) {
    752        continue;
    753      }
    754 
    755      if (userContextId === null) {
    756        traversableIds = traversableIds.union(topLevelTraversableIds);
    757        continue;
    758      }
    759 
    760      for (const traversableId of topLevelTraversableIds) {
    761        const traversable =
    762          lazy.NavigableManager.getBrowsingContextById(traversableId);
    763 
    764        if (traversable === null) {
    765          continue;
    766        }
    767 
    768        if (traversable.originAttributes.userContextId === userContextId) {
    769          traversableIds.add(traversableId);
    770        }
    771      }
    772    }
    773 
    774    return traversableIds;
    775  }
    776 
    777  #obtainEventEnabledUserContextIds(eventName) {
    778    let enabledUserContextIds = new Set();
    779 
    780    for (const { userContextIds } of this.#getSubscriptionsForEvent(
    781      eventName
    782    )) {
    783      enabledUserContextIds = enabledUserContextIds.union(userContextIds);
    784    }
    785 
    786    return enabledUserContextIds;
    787  }
    788 
    789  /**
    790   * Obtain a set of events based on the given event name.
    791   *
    792   * Could contain a period for a specific event,
    793   * or just the module name for all events.
    794   *
    795   * @param {string} event
    796   *     Name of the event to process.
    797   *
    798   * @returns {Set<string>}
    799   *     A Set with the expanded events in the form of `<module>.<event>`.
    800   *
    801   * @throws {InvalidArgumentError}
    802   *     If <var>event</var> does not reference a valid event.
    803   */
    804  #obtainEvents(event) {
    805    const events = new Set();
    806 
    807    // Check if a period is present that splits the event name into the module,
    808    // and the actual event. Hereby only care about the first found instance.
    809    const index = event.indexOf(".");
    810    if (index >= 0) {
    811      const [moduleName] = event.split(".");
    812      this.#assertModuleSupportsEvent(moduleName, event);
    813      events.add(event);
    814    } else {
    815      // Interpret the name as module, and register all its available events
    816      const rootModuleClass = this.#getRootModuleClass(event);
    817      const supportedEvents = rootModuleClass?.supportedEvents;
    818 
    819      for (const eventName of supportedEvents) {
    820        events.add(eventName);
    821      }
    822    }
    823 
    824    return events;
    825  }
    826 
    827  #onMessageHandlerEvent = (name, event) => {
    828    this.messageHandler.emitProtocolEvent(name, event);
    829  };
    830 
    831  #unsubscribeByAttributes(events, contextIds) {
    832    const listeners = [];
    833 
    834    // Check input types until we run schema validation.
    835    this.#assertNonEmptyArrayWithStrings(events, "events");
    836    if (contextIds !== null) {
    837      this.#assertNonEmptyArrayWithStrings(contextIds, "contexts");
    838    }
    839 
    840    const eventNames = new Set();
    841    events.forEach(name => {
    842      this.#obtainEvents(name).forEach(event => eventNames.add(event));
    843    });
    844 
    845    const topLevelTraversableContextIds =
    846      this.#getTopLevelTraversableContextIds(contextIds);
    847 
    848    const newSubscriptions = [];
    849    const matchedEvents = new Set();
    850    const matchedContexts = new Set();
    851 
    852    for (const subscription of this.#subscriptions) {
    853      // Keep subscription if it doesn't contain any target events.
    854      if (subscription.eventNames.intersection(eventNames).size === 0) {
    855        newSubscriptions.push(subscription);
    856        continue;
    857      }
    858 
    859      // Unsubscribe globally.
    860      if (topLevelTraversableContextIds.size === 0) {
    861        // Keep subscription if verified subscription is not global.
    862        if (!this.#isSubscriptionGlobal(subscription)) {
    863          newSubscriptions.push(subscription);
    864          continue;
    865        }
    866 
    867        // Delete event names from the subscription.
    868        const subscriptionEventNames = new Set(subscription.eventNames);
    869        for (const eventName of eventNames) {
    870          if (subscriptionEventNames.has(eventName)) {
    871            matchedEvents.add(eventName);
    872            subscriptionEventNames.delete(eventName);
    873 
    874            listeners.push(this.#createListenerToUnsubscribe({ eventName }));
    875          }
    876        }
    877 
    878        // If the subscription still contains some event,
    879        // save a new partial subscription.
    880        if (subscriptionEventNames.size !== 0) {
    881          const clonedSubscription = {
    882            subscriptionId: subscription.subscriptionId,
    883            eventNames: new Set(subscriptionEventNames),
    884            topLevelTraversableIds: new Set(),
    885            userContextIds: new Set(subscription.userContextIds),
    886          };
    887          newSubscriptions.push(clonedSubscription);
    888        }
    889      }
    890      // Keep the subscription if it's global but we want to unsubscribe only from some contexts.
    891      else if (this.#isSubscriptionGlobal(subscription)) {
    892        newSubscriptions.push(subscription);
    893      } else {
    894        // Map with an event name as a key and the set of subscribed traversable ids as a value.
    895        const eventMap = new Map();
    896 
    897        // Populate the map.
    898        for (const eventName of subscription.eventNames) {
    899          eventMap.set(eventName, new Set(subscription.topLevelTraversableIds));
    900        }
    901 
    902        for (const eventName of eventNames) {
    903          // Skip if there is no subscription related to this event.
    904          if (!eventMap.has(eventName)) {
    905            continue;
    906          }
    907 
    908          for (const topLevelTraversableId of topLevelTraversableContextIds) {
    909            // Skip if there is no subscription related to this event and this traversable id.
    910            if (!eventMap.get(eventName).has(topLevelTraversableId)) {
    911              continue;
    912            }
    913 
    914            matchedContexts.add(topLevelTraversableId);
    915            matchedEvents.add(eventName);
    916            eventMap.get(eventName).delete(topLevelTraversableId);
    917 
    918            listeners.push(
    919              this.#createListenerToUnsubscribe({
    920                eventName,
    921                traversableId: topLevelTraversableId,
    922              })
    923            );
    924          }
    925 
    926          if (eventMap.get(eventName).size === 0) {
    927            eventMap.delete(eventName);
    928          }
    929        }
    930 
    931        // Build new partial subscriptions based on the remaining data in eventMap.
    932        for (const [
    933          eventName,
    934          remainingTopLevelTraversableIds,
    935        ] of eventMap.entries()) {
    936          const partialSubscription = {
    937            subscriptionId: subscription.subscriptionId,
    938            eventNames: new Set([eventName]),
    939            topLevelTraversableIds: remainingTopLevelTraversableIds,
    940            userContextIds: new Set(subscription.userContextIds),
    941          };
    942 
    943          newSubscriptions.push(partialSubscription);
    944 
    945          const traversableIdsToUnsubscribe =
    946            subscription.topLevelTraversableIds.difference(
    947              remainingTopLevelTraversableIds
    948            );
    949 
    950          for (const traversableId of traversableIdsToUnsubscribe) {
    951            listeners.push(
    952              this.#createListenerToUnsubscribe({ eventName, traversableId })
    953            );
    954          }
    955        }
    956      }
    957    }
    958 
    959    if (matchedEvents.symmetricDifference(eventNames).size > 0) {
    960      throw new lazy.error.InvalidArgumentError(
    961        `Failed to unsubscribe from events: ${Array.from(eventNames).join(", ")}`
    962      );
    963    }
    964    if (
    965      topLevelTraversableContextIds.size > 0 &&
    966      matchedContexts.symmetricDifference(topLevelTraversableContextIds).size >
    967        0
    968    ) {
    969      throw new lazy.error.InvalidArgumentError(
    970        `Failed to unsubscribe from events: ${Array.from(eventNames).join(", ")} for context ids: ${Array.from(topLevelTraversableContextIds).join(", ")}`
    971      );
    972    }
    973 
    974    this.#subscriptions = newSubscriptions;
    975 
    976    return listeners;
    977  }
    978 
    979  #unsubscribeById(subscriptionIds) {
    980    this.#assertNonEmptyArrayWithStrings(subscriptionIds, "subscriptions");
    981 
    982    const subscriptions = new Set(subscriptionIds);
    983    const unknownSubscriptionIds = subscriptions.difference(
    984      this.#knownSubscriptionIds
    985    );
    986 
    987    if (unknownSubscriptionIds.size !== 0) {
    988      throw new lazy.error.InvalidArgumentError(
    989        `Failed to unsubscribe from subscriptions with ids: ${Array.from(subscriptionIds).join(", ")} ` +
    990          `(unknown ids: ${Array.from(unknownSubscriptionIds).join(", ")})`
    991      );
    992    }
    993 
    994    const listeners = [];
    995    const subscriptionIdsToRemove = new Set();
    996    const subscriptionsToRemove = new Set();
    997 
    998    for (const subscription of this.#subscriptions) {
    999      const { subscriptionId } = subscription;
   1000 
   1001      if (!subscriptions.has(subscriptionId)) {
   1002        continue;
   1003      }
   1004 
   1005      subscriptionIdsToRemove.add(subscriptionId);
   1006      subscriptionsToRemove.add(subscription);
   1007    }
   1008 
   1009    this.#knownSubscriptionIds =
   1010      this.#knownSubscriptionIds.difference(subscriptions);
   1011    this.#subscriptions = this.#subscriptions.filter(
   1012      ({ subscriptionId }) => !subscriptionIdsToRemove.has(subscriptionId)
   1013    );
   1014 
   1015    for (const subscription of subscriptionsToRemove) {
   1016      listeners.push(...this.#getListenersToUnsubscribe(subscription));
   1017    }
   1018 
   1019    return listeners;
   1020  }
   1021 }
   1022 
   1023 // To export the class as lower-case
   1024 export const session = SessionModule;