/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
import * as Loader from "resource://devtools/shared/loader/Loader.sys.mjs";

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  isWindowGlobalPartOfContext:
    "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
  releaseDistinctSystemPrincipalLoader:
    "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
  TargetActorRegistry:
    "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
  useDistinctSystemPrincipalLoader:
    "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
  WindowGlobalLogger:
    "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs",
});

const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
  "devtools.every-frame-target.enabled",
  false
);

// Name of the attribute into which we save data in `sharedData` object.
const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";

// If true, log info about WindowGlobal's being created.
const DEBUG = false;
function logWindowGlobal(windowGlobal, message) {
  if (!DEBUG) {
    return;
  }
  lazy.WindowGlobalLogger.logWindowGlobal(windowGlobal, message);
}

export class DevToolsFrameChild extends JSWindowActorChild {
  constructor() {
    super();

    // The map is indexed by the Watcher Actor ID.
    // The values are objects containing the following properties:
    // - connection: the DevToolsServerConnection itself
    // - actor: the WindowGlobalTargetActor instance
    this._connections = new Map();

    EventEmitter.decorate(this);

    // Set the following preference on the constructor, so that we can easily
    // toggle these preferences on and off from tests and have the new value being picked up.

    // bfcache-in-parent changes significantly how navigation behaves.
    // We may start reusing previously existing WindowGlobal and so reuse
    // previous set of JSWindowActor pairs (i.e. DevToolsFrameParent/DevToolsFrameChild).
    // When enabled, regular navigations may also change and spawn new BrowsingContexts.
    // If the page we navigate from supports being stored in bfcache,
    // the navigation will use a new BrowsingContext. And so force spawning
    // a new top-level target.
    ChromeUtils.defineLazyGetter(
      this,
      "isBfcacheInParentEnabled",
      () =>
        Services.appinfo.sessionHistoryInParent &&
        Services.prefs.getBoolPref("fission.bfcacheInParent", false)
    );
  }

  /**
   * Try to instantiate new target actors for the current WindowGlobal, and that,
   * for all the currently registered Watcher actors.
   *
   * Read the `sharedData` to get metadata about all registered watcher actors.
   * If these watcher actors are interested in the current WindowGlobal,
   * instantiate a new dedicated target actor for each of the watchers.
   *
   * @param Object options
   * @param Boolean options.isBFCache
   *        True, if the request to instantiate a new target comes from a bfcache navigation.
   *        i.e. when we receive a pageshow event with persisted=true.
   *        This will be true regardless of bfcacheInParent being enabled or disabled.
   * @param Boolean options.ignoreIfExisting
   *        By default to false. If true is passed, we avoid instantiating a target actor
   *        if one already exists for this windowGlobal.
   */
  instantiate({ isBFCache = false, ignoreIfExisting = false } = {}) {
    const { sharedData } = Services.cpmm;
    const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
    if (!sessionDataByWatcherActor) {
      throw new Error(
        "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets"
      );
    }

    // Create one Target actor for each prefix/client which listen to frames
    for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
      const { connectionPrefix, sessionContext } = sessionData;
      // For bfcache navigations, we only create new targets when bfcacheInParent is enabled,
      // as this would be the only case where new DocShells will be created. This requires us to spawn a
      // new WindowGlobalTargetActor as one such actor is bound to a unique DocShell.
      const forceAcceptTopLevelTarget =
        isBFCache && this.isBfcacheInParentEnabled;
      if (
        sessionData.targets?.includes("frame") &&
        lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, {
          forceAcceptTopLevelTarget,
        })
      ) {
        // If this was triggered because of a navigation, we want to retrieve the existing
        // target we were debugging so we can destroy it before creating the new target.
        // This is important because we had cases where the destruction of an old target
        // was unsetting a flag on the **new** target document, breaking the toolbox (See Bug 1721398).

        // We're checking for an existing target given a watcherActorID + browserId + browsingContext
        // Note that a target switch might create a new browsing context, so we wouldn't
        // retrieve the existing target here. We are okay with this as:
        // - this shouldn't happen much
        // - in such case we weren't seeing the issue of Bug 1721398 (the old target can't access the new document)
        const existingTarget = this._findTargetActor({
          watcherActorID,
          sessionContext,
          browsingContextId: this.manager.browsingContext.id,
        });

        // See comment in handleEvent(DOMDocElementInserted) to know why we try to
        // create targets if none already exists
        if (existingTarget && ignoreIfExisting) {
          continue;
        }

        // Bail if there is already an existing WindowGlobalTargetActor which wasn't
        // created from a JSWIndowActor.
        // This means we are reloading or navigating (same-process) a Target
        // which has not been created using the Watcher, but from the client (most likely
        // the initial target of a local-tab toolbox).
        // However, we force overriding the first message manager based target in case of
        // BFCache navigations.
        if (
          existingTarget &&
          !existingTarget.createdFromJsWindowActor &&
          !isBFCache
        ) {
          continue;
        }

        // If we decide to instantiate a new target and there was one before,
        // first destroy the previous one.
        // Otherwise its destroy sequence will be executed *after* the new one
        // is being initialized and may easily revert changes made against platform API.
        // (typically toggle platform boolean attributes back to default…)
        if (existingTarget) {
          existingTarget.destroy({ isTargetSwitching: true });
        }

        this._createTargetActor({
          watcherActorID,
          parentConnectionPrefix: connectionPrefix,
          sessionData,
          isDocumentCreation: true,
        });
      }
    }
  }

  /**
   * Instantiate a new WindowGlobalTarget for the given connection.
   *
   * @param Object options
   * @param String options.watcherActorID
   *        The ID of the WatcherActor who requested to observe and create these target actors.
   * @param String options.parentConnectionPrefix
   *        The prefix of the DevToolsServerConnection of the Watcher Actor.
   *        This is used to compute a unique ID for the target actor.
   * @param Object options.sessionData
   *        All data managed by the Watcher Actor and WatcherRegistry.jsm, containing
   *        target types, resources types to be listened as well as breakpoints and any
   *        other data meant to be shared across processes and threads.
   * @param Boolean options.isDocumentCreation
   *        Set to true if the function is called from `instantiate`, i.e. when we're
   *        handling a new document being created.
   * @param Boolean options.fromInstantiateAlreadyAvailable
   *        Set to true if the function is called from handling `DevToolsFrameParent:instantiate-already-available`
   *        query.
   */
  _createTargetActor({
    watcherActorID,
    parentConnectionPrefix,
    sessionData,
    isDocumentCreation,
    fromInstantiateAlreadyAvailable,
  }) {
    if (this._connections.get(watcherActorID)) {
      // If this function is called as a result of a `DevToolsFrameParent:instantiate-already-available`
      // message, we might have a legitimate race condition:
      // In frame-helper, we want to create the initial targets for a given browser element.
      // It might happen that the `DevToolsFrameParent:instantiate-already-available` is
      // aborted if the page navigates (and the document is destroyed) while the query is still pending.
      // In such case, frame-helper will try to send a new message. In the meantime,
      // the DevToolsFrameChild `DOMWindowCreated` handler may already have been called and
      // the new target already created.
      // We don't want to throw in such case, as our end-goal, having a target for the document,
      // is achieved.
      if (fromInstantiateAlreadyAvailable) {
        return;
      }
      throw new Error(
        "DevToolsFrameChild _createTargetActor was called more than once" +
          ` for the same Watcher (Actor ID: "${watcherActorID}")`
      );
    }

    // Compute a unique prefix, just for this WindowGlobal,
    // which will be used to create a JSWindowActorTransport pair between content and parent processes.
    // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`,
    // but here, we can't have access to any DevTools connection as we are really early in the content process startup
    // XXX: WindowGlobal's innerWindowId should be unique across processes, I think. So that should be safe?
    // (this.manager == WindowGlobalChild interface)
    const forwardingPrefix =
      parentConnectionPrefix + "windowGlobal" + this.manager.innerWindowId;

    logWindowGlobal(
      this.manager,
      "Instantiate WindowGlobalTarget with prefix: " + forwardingPrefix
    );

    const { connection, targetActor } = this._createConnectionAndActor(
      forwardingPrefix,
      sessionData
    );
    const form = targetActor.form();
    // Ensure unregistering and destroying the related DevToolsServerConnection+Transport
    // on both content and parent process JSWindowActors.
    targetActor.once("destroyed", options => {
      // This will destroy the content process one
      this._destroyTargetActor(watcherActorID, options);
      // And this will destroy the parent process one
      try {
        this.sendAsyncMessage("DevToolsFrameChild:destroy", {
          actors: [
            {
              watcherActorID,
              form,
            },
          ],
          options,
        });
      } catch (e) {
        // Ignore exception when the JSWindowActorChild has already been destroyed.
        // We often try to emit this message while the WindowGlobal is in the process of being
        // destroyed. We eagerly destroy the target actor during reloads,
        // just before the WindowGlobal starts destroying, but sendAsyncMessage
        // doesn't have time to complete and throws.
        if (
          !e.message.includes("JSWindowActorChild cannot send at the moment")
        ) {
          throw e;
        }
      }
    });
    this._connections.set(watcherActorID, {
      connection,
      actor: targetActor,
    });

    // Immediately queue a message for the parent process,
    // in order to ensure that the JSWindowActorTransport is instantiated
    // before any packet is sent from the content process.
    // As the order of messages is guaranteed to be delivered in the order they
    // were queued, we don't have to wait for anything around this sendAsyncMessage call.
    // In theory, the WindowGlobalTargetActor may emit events in its constructor.
    // If it does, such RDP packets may be lost.
    // The important point here is to send this message before processing the sessionData,
    // which will start the Watcher and start emitting resources on the target actor.
    this.sendAsyncMessage("DevToolsFrameChild:connectFromContent", {
      watcherActorID,
      forwardingPrefix,
      actor: targetActor.form(),
    });

    // Pass initialization data to the target actor
    for (const type in sessionData) {
      // `sessionData` will also contain `browserId` as well as entries with empty arrays,
      // which shouldn't be processed.
      const entries = sessionData[type];
      if (!Array.isArray(entries) || !entries.length) {
        continue;
      }
      targetActor.addSessionDataEntry(type, entries, isDocumentCreation);
    }
  }

  /**
   * @param {string} watcherActorID
   * @param {object} options
   * @param {boolean} options.isModeSwitching
   *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
   */
  _destroyTargetActor(watcherActorID, options) {
    const connectionInfo = this._connections.get(watcherActorID);
    // This connection has already been cleaned?
    if (!connectionInfo) {
      throw new Error(
        `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
      );
    }
    connectionInfo.connection.close(options);
    this._connections.delete(watcherActorID);
    if (this._connections.size == 0) {
      this.didDestroy(options);
    }
  }

  _createConnectionAndActor(forwardingPrefix, sessionData) {
    this.useCustomLoader = this.document.nodePrincipal.isSystemPrincipal;

    if (!this.loader) {
      // When debugging chrome pages, use a new dedicated loader, using a distinct chrome compartment.
      this.loader = this.useCustomLoader
        ? lazy.useDistinctSystemPrincipalLoader(this)
        : Loader;
    }
    const { DevToolsServer } = this.loader.require(
      "resource://devtools/server/devtools-server.js"
    );

    const { WindowGlobalTargetActor } = this.loader.require(
      "resource://devtools/server/actors/targets/window-global.js"
    );

    DevToolsServer.init();

    // We want a special server without any root actor and only target-scoped actors.
    // We are going to spawn a WindowGlobalTargetActor instance in the next few lines,
    // it is going to act like a root actor without being one.
    DevToolsServer.registerActors({ target: true });

    const connection = DevToolsServer.connectToParentWindowActor(
      this,
      forwardingPrefix
    );

    // In the case of the browser toolbox, tab's BrowsingContext don't have
    // any parent BC and shouldn't be considered as top-level.
    // This is why we check for browserId's.
    const browsingContext = this.manager.browsingContext;
    const isTopLevelTarget =
      !browsingContext.parent &&
      browsingContext.browserId == sessionData.sessionContext.browserId;

    // Create the actual target actor.
    const targetActor = new WindowGlobalTargetActor(connection, {
      docShell: this.docShell,
      // Targets created from the server side, via Watcher actor and DevToolsFrame JSWindow
      // actor pair are following WindowGlobal lifecycle. i.e. will be destroyed on any
      // type of navigation/reload.
      followWindowGlobalLifeCycle: true,
      isTopLevelTarget,
      ignoreSubFrames: isEveryFrameTargetEnabled,
      sessionContext: sessionData.sessionContext,
    });
    targetActor.manage(targetActor);
    targetActor.createdFromJsWindowActor = true;

    return { connection, targetActor };
  }

  /**
   * Supported Queries
   */

  sendPacket(packet, prefix) {
    this.sendAsyncMessage("DevToolsFrameChild:packet", { packet, prefix });
  }

  /**
   * JsWindowActor API
   */

  async sendQuery(msg, args) {
    try {
      const res = await super.sendQuery(msg, args);
      return res;
    } catch (e) {
      console.error("Failed to sendQuery in DevToolsFrameChild", msg);
      console.error(e.toString());
      throw e;
    }
  }

  receiveMessage(message) {
    // Assert that the message is intended for this window global,
    // except for "packet" messages which use a dedicated routing
    if (
      message.name != "DevToolsFrameParent:packet" &&
      message.data.sessionContext.type == "browser-element"
    ) {
      const { browserId } = message.data.sessionContext;
      // Re-check here, just to ensure that both parent and content processes agree
      // on what should or should not be watched.
      if (
        this.manager.browsingContext.browserId != browserId &&
        !lazy.isWindowGlobalPartOfContext(
          this.manager,
          message.data.sessionContext,
          {
            forceAcceptTopLevelTarget: true,
          }
        )
      ) {
        throw new Error(
          "Mismatch between DevToolsFrameParent and DevToolsFrameChild " +
            (this.manager.browsingContext.browserId == browserId
              ? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)"
              : `expected browsing context with browserId ${browserId}, but got ${this.manager.browsingContext.browserId}`)
        );
      }
    }
    switch (message.name) {
      case "DevToolsFrameParent:instantiate-already-available": {
        const { watcherActorID, connectionPrefix, sessionData } = message.data;

        return this._createTargetActor({
          watcherActorID,
          parentConnectionPrefix: connectionPrefix,
          sessionData,
          fromInstantiateAlreadyAvailable: true,
        });
      }
      case "DevToolsFrameParent:destroy": {
        const { watcherActorID, options } = message.data;
        return this._destroyTargetActor(watcherActorID, options);
      }
      case "DevToolsFrameParent:addSessionDataEntry": {
        const { watcherActorID, sessionContext, type, entries } = message.data;
        return this._addSessionDataEntry(
          watcherActorID,
          sessionContext,
          type,
          entries
        );
      }
      case "DevToolsFrameParent:removeSessionDataEntry": {
        const { watcherActorID, sessionContext, type, entries } = message.data;
        return this._removeSessionDataEntry(
          watcherActorID,
          sessionContext,
          type,
          entries
        );
      }
      case "DevToolsFrameParent:packet":
        return this.emit("packet-received", message);
      default:
        throw new Error(
          "Unsupported message in DevToolsFrameParent: " + message.name
        );
    }
  }

  /**
   * Return an existing target given a WatcherActor id, a browserId and an optional
   * browsing context id.
   * /!\ Note that we may have multiple targets for a given (watcherActorId, browserId) couple,
   * for example if we have 2 remote iframes sharing the same origin, which is why you
   * might want to pass a specific browsing context id to filter the list down.
   *
   * @param {Object} options
   * @param {Object} options.watcherActorID
   * @param {Object} options.sessionContext
   * @param {Object} options.browsingContextId: Optional browsing context id to narrow the
   *                 search to a specific browsing context.
   *
   * @returns {WindowGlobalTargetActor|null}
   */
  _findTargetActor({ watcherActorID, sessionContext, browsingContextId }) {
    // First let's check if a target was created for this watcher actor in this specific
    // DevToolsFrameChild instance.
    const connectionInfo = this._connections.get(watcherActorID);
    const targetActor = connectionInfo ? connectionInfo.actor : null;
    if (targetActor) {
      return targetActor;
    }

    // If we couldn't find such target, we want to see if a target was created for this
    // (watcherActorId,browserId, {browsingContextId}) in another DevToolsFrameChild instance.
    // This might be the case if we're navigating to a new page with server side target
    // enabled and we want to retrieve the target of the page we're navigating from.
    if (
      lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, {
        forceAcceptTopLevelTarget: true,
      })
    ) {
      // Ensure retrieving the one target actor related to this connection.
      // This allows to distinguish actors created for various toolboxes.
      // For ex, regular toolbox versus browser console versus browser toolbox
      const connectionPrefix = watcherActorID.replace(/watcher\d+$/, "");
      const targetActors = lazy.TargetActorRegistry.getTargetActors(
        sessionContext,
        connectionPrefix
      );

      if (!browsingContextId) {
        return targetActors[0] || null;
      }
      return targetActors.find(
        actor => actor.browsingContextID === browsingContextId
      );
    }
    return null;
  }

  _addSessionDataEntry(watcherActorID, sessionContext, type, entries) {
    // /!\ We may have an issue here as there could be multiple targets for a given
    // (watcherActorID,browserId) pair.
    // This should be clarified as part of Bug 1725623.
    const targetActor = this._findTargetActor({
      watcherActorID,
      sessionContext,
    });

    if (!targetActor) {
      throw new Error(
        `No target actor for this Watcher Actor ID:"${watcherActorID}" / BrowserId:${sessionContext.browserId}`
      );
    }
    return targetActor.addSessionDataEntry(type, entries);
  }

  _removeSessionDataEntry(watcherActorID, sessionContext, type, entries) {
    // /!\ We may have an issue here as there could be multiple targets for a given
    // (watcherActorID,browserId) pair.
    // This should be clarified as part of Bug 1725623.
    const targetActor = this._findTargetActor({
      watcherActorID,
      sessionContext,
    });
    // By the time we are calling this, the target may already have been destroyed.
    if (targetActor) {
      return targetActor.removeSessionDataEntry(type, entries);
    }
    return null;
  }

  handleEvent({ type, persisted, target }) {
    // Ignore any event that may fire for children WindowGlobals/documents
    if (target != this.document) {
      return;
    }

    // DOMWindowCreated is registered from FrameWatcher via `ActorManagerParent.addJSWindowActors`
    // as a DOM event to be listened to and so is fired by JS Window Actor code platform code.
    if (type == "DOMWindowCreated") {
      this.instantiate();
      return;
    }
    // We might have ignored the DOMWindowCreated event because it was the initial about:blank document.
    // But when loading same-process iframes, we reuse the WindowGlobal of the about:bank document
    // to load the actual URL loaded in the iframe. This means we won't have a new DOMWindowCreated
    // for the actual document. There is a DOMDocElementInserted fired just after, that we can catch
    // to create a target for same-process iframes.
    // This means that we still do not create any target for the initial documents.
    // It is complex to instantiate targets for initial documents because:
    // - it would mean spawning two targets for the same WindowGlobal and sharing the same innerWindowId
    // - or have WindowGlobalTargets to handle more than one document (it would mean reusing will-navigate/window-ready events
    // both on client and server)
    if (type == "DOMDocElementInserted") {
      this.instantiate({ ignoreIfExisting: true });
      return;
    }

    // If persisted=true, this is a BFCache navigation.
    //
    // With Fission enabled and bfcacheInParent, BFCache navigations will spawn a new DocShell
    // in the same process:
    // * the previous page won't be destroyed, its JSWindowActor will stay alive (`didDestroy` won't be called)
    //   and a 'pagehide' with persisted=true will be emitted on it.
    // * the new page page won't emit any DOMWindowCreated, but instead a pageshow with persisted=true
    //   will be emitted.

    if (type === "pageshow" && persisted) {
      // Notify all bfcache navigations, even the one for which we don't create a new target
      // as that's being useful for parent process storage resource watchers.
      this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pageshow");

      // Here we are going to re-instantiate a target that got destroyed before while processing a pagehide event.
      // We force instantiating a new top level target, within `instantiate()` even if server targets are disabled.
      // But we only do that if bfcacheInParent is enabled. Otherwise for same-process, same-docshell bfcache navigation,
      // we don't want to spawn new targets.
      this.instantiate({
        isBFCache: true,
      });
      return;
    }

    if (type === "pagehide" && persisted) {
      // Notify all bfcache navigations, even the one for which we don't create a new target
      // as that's being useful for parent process storage resource watchers.
      this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pagehide");

      // We might navigate away for the first top level target,
      // which isn't using JSWindowActor (it still uses messages manager and is created by the client, via TabDescriptor.getTarget).
      // We have to unregister it from the TargetActorRegistry, otherwise,
      // if we navigate back to it, the next DOMWindowCreated won't create a new target for it.
      const { sharedData } = Services.cpmm;
      const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
      if (!sessionDataByWatcherActor) {
        throw new Error(
          "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets"
        );
      }

      const actors = [];
      // A flag to know if the following for loop ended up destroying all the actors.
      // It may not be the case if one Watcher isn't having server target switching enabled.
      let allActorsAreDestroyed = true;
      for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
        const { sessionContext } = sessionData;

        // /!\ We may have an issue here as there could be multiple targets for a given
        // (watcherActorID,browserId) pair.
        // This should be clarified as part of Bug 1725623.
        const existingTarget = this._findTargetActor({
          watcherActorID,
          sessionContext,
        });

        if (!existingTarget) {
          continue;
        }

        // Use `originalWindow` as `window` can be set when a document was selected from
        // the iframe picker in the toolbox toolbar.
        if (existingTarget.originalWindow.document != target) {
          throw new Error("Existing target actor is for a distinct document");
        }
        // Do not do anything if both bfcache in parent and server targets are disabled
        // As history navigations will be handled within the same DocShell and by the
        // same WindowGlobalTargetActor. The actor will listen to pageshow/pagehide by itself.
        // We should not destroy any target.
        if (
          !this.isBfcacheInParentEnabled &&
          !sessionContext.isServerTargetSwitchingEnabled
        ) {
          allActorsAreDestroyed = false;
          continue;
        }

        actors.push({
          watcherActorID,
          form: existingTarget.form(),
        });
        existingTarget.destroy();
      }

      if (actors.length) {
        // The most important is to unregister the actor from TargetActorRegistry,
        // so that it is no longer present in the list when new DOMWindowCreated fires.
        // This will also help notify the client that the target has been destroyed.
        // And if we navigate back to this target, the client will receive the same target actor ID,
        // so that it is really important to destroy it correctly on both server and client.
        this.sendAsyncMessage("DevToolsFrameChild:destroy", { actors });
      }

      if (allActorsAreDestroyed) {
        // Completely clear this JSWindow Actor.
        // Do this after having called _findTargetActor,
        // as it would clear the registered target actors.
        this.didDestroy();
      }
    }
  }

  didDestroy(options) {
    logWindowGlobal(this.manager, "Destroy WindowGlobalTarget");
    for (const [, connectionInfo] of this._connections) {
      connectionInfo.connection.close(options);
    }
    this._connections.clear();

    if (this.loader) {
      if (this.useCustomLoader) {
        lazy.releaseDistinctSystemPrincipalLoader(this);
      }
      this.loader = null;
    }
  }
}
