summaryrefslogtreecommitdiffstats
path: root/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs
blob: 519cd10325f32fbdb25206316bb68dfb52b0c8e1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
/* 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.addOrSetSessionDataEntry(
        type,
        entries,
        isDocumentCreation,
        "set"
      );
    }
  }

  /**
   * @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:addOrSetSessionDataEntry": {
        const { watcherActorID, sessionContext, type, entries, updateType } =
          message.data;
        return this._addOrSetSessionDataEntry(
          watcherActorID,
          sessionContext,
          type,
          entries,
          updateType
        );
      }
      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;
  }

  _addOrSetSessionDataEntry(
    watcherActorID,
    sessionContext,
    type,
    entries,
    updateType
  ) {
    // /!\ 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.addOrSetSessionDataEntry(
      type,
      entries,
      false,
      updateType
    );
  }

  _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;
    }
  }
}