summaryrefslogtreecommitdiffstats
path: root/remote/cdp/observers/ContextObserver.sys.mjs
blob: d0d6fd1f93082aa81012b10d9f7db5e4b52c7442 (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
/* 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/. */

/**
 * Helper class to coordinate Runtime and Page events.
 * Events have to be sent in the following order:
 *  - Runtime.executionContextDestroyed
 *  - Page.frameNavigated
 *  - Runtime.executionContextCreated
 *
 * This class also handles the special case of Pages going from/to the BF cache.
 * When you navigate to a new URL, the previous document may be stored in the BF Cache.
 * All its asynchronous operations are frozen (XHR, timeouts, ...) and a `pagehide` event
 * is fired for this document. We then navigate to the new URL.
 * If the user navigates back to the previous page, the page is resurected from the
 * cache. A `pageshow` event is fired and its asynchronous operations are resumed.
 *
 * When a page is in the BF Cache, we should consider it as frozen and shouldn't try
 * to execute any javascript. So that the ExecutionContext should be considered as
 * being destroyed and the document navigated.
 */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",

  executeSoon: "chrome://remote/content/shared/Sync.sys.mjs",
});

export class ContextObserver {
  constructor(chromeEventHandler) {
    this.chromeEventHandler = chromeEventHandler;
    lazy.EventEmitter.decorate(this);

    this._fissionEnabled = Services.appinfo.fissionAutostart;

    this.chromeEventHandler.addEventListener("DOMWindowCreated", this, {
      mozSystemGroup: true,
    });

    // Listen for pageshow and pagehide to track pages going in/out to/from the BF Cache
    this.chromeEventHandler.addEventListener("pageshow", this, {
      mozSystemGroup: true,
    });
    this.chromeEventHandler.addEventListener("pagehide", this, {
      mozSystemGroup: true,
    });

    Services.obs.addObserver(this, "document-element-inserted");
    Services.obs.addObserver(this, "inner-window-destroyed");

    // With Fission disabled the `DOMWindowCreated` event is fired too late.
    // Use the `webnavigation-create` notification instead.
    if (!this._fissionEnabled) {
      Services.obs.addObserver(this, "webnavigation-create");
    }
    Services.obs.addObserver(this, "webnavigation-destroy");
  }

  destructor() {
    this.chromeEventHandler.removeEventListener("DOMWindowCreated", this, {
      mozSystemGroup: true,
    });
    this.chromeEventHandler.removeEventListener("pageshow", this, {
      mozSystemGroup: true,
    });
    this.chromeEventHandler.removeEventListener("pagehide", this, {
      mozSystemGroup: true,
    });

    Services.obs.removeObserver(this, "document-element-inserted");
    Services.obs.removeObserver(this, "inner-window-destroyed");

    if (!this._fissionEnabled) {
      Services.obs.removeObserver(this, "webnavigation-create");
    }
    Services.obs.removeObserver(this, "webnavigation-destroy");
  }

  handleEvent({ type, target, persisted }) {
    const window = target.defaultView;
    const frameId = window.browsingContext.id;
    const id = window.windowGlobalChild.innerWindowId;

    switch (type) {
      case "DOMWindowCreated":
        // Do not pass `id` here as that's the new document ID instead of the old one
        // that is destroyed. Instead, pass the frameId and let the listener figure out
        // what ExecutionContext(s) to destroy.
        this.emit("context-destroyed", { frameId });

        // With Fission enabled the frame is attached early enough so that
        // expected network requests and responses are handles afterward.
        // Otherwise send the event when `webnavigation-create` is received.
        if (this._fissionEnabled) {
          this.emit("frame-attached", { frameId, window });
        }

        break;

      case "pageshow":
        // `persisted` is true when this is about a page being resurected from BF Cache
        if (!persisted) {
          return;
        }
        // XXX(ochameau) we might have to emit FrameNavigate here to properly handle BF Cache
        // scenario in Page domain events
        this.emit("context-created", { windowId: id, window });
        this.emit("script-loaded", { windowId: id, window });
        break;

      case "pagehide":
        // `persisted` is true when this is about a page being frozen into BF Cache
        if (!persisted) {
          return;
        }
        this.emit("context-destroyed", { windowId: id });
        break;
    }
  }

  observe(subject, topic, data) {
    switch (topic) {
      case "document-element-inserted":
        const window = subject.defaultView;

        // Ignore events without a window and those from other tabs
        if (
          !window ||
          window.docShell.chromeEventHandler !== this.chromeEventHandler
        ) {
          return;
        }

        // Send when the document gets attached to the window, and its location
        // is available.
        this.emit("frame-navigated", {
          frameId: window.browsingContext.id,
          window,
        });

        const id = window.windowGlobalChild.innerWindowId;
        this.emit("context-created", { windowId: id, window });
        // Delay script-loaded to allow context cleanup to happen first
        lazy.executeSoon(() => {
          this.emit("script-loaded", { windowId: id, window });
        });
        break;
      case "inner-window-destroyed":
        const windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
        this.emit("context-destroyed", { windowId });
        break;
      case "webnavigation-create":
        subject.QueryInterface(Ci.nsIDocShell);
        this.onDocShellCreated(subject);
        break;
      case "webnavigation-destroy":
        subject.QueryInterface(Ci.nsIDocShell);
        this.onDocShellDestroyed(subject);
        break;
    }
  }

  onDocShellCreated(docShell) {
    this.emit("frame-attached", {
      frameId: docShell.browsingContext.id,
      window: docShell.browsingContext.window,
    });
  }

  onDocShellDestroyed(docShell) {
    this.emit("frame-detached", {
      frameId: docShell.browsingContext.id,
    });
  }
}