|
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 "use strict"; |
|
6 |
|
7 function debug(msg) { |
|
8 Services.console.logStringMessage("SessionStoreContent: " + msg); |
|
9 } |
|
10 |
|
11 let Cu = Components.utils; |
|
12 let Cc = Components.classes; |
|
13 let Ci = Components.interfaces; |
|
14 let Cr = Components.results; |
|
15 |
|
16 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); |
|
17 Cu.import("resource://gre/modules/Timer.jsm", this); |
|
18 |
|
19 XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities", |
|
20 "resource:///modules/sessionstore/DocShellCapabilities.jsm"); |
|
21 XPCOMUtils.defineLazyModuleGetter(this, "FormData", |
|
22 "resource://gre/modules/FormData.jsm"); |
|
23 XPCOMUtils.defineLazyModuleGetter(this, "PageStyle", |
|
24 "resource:///modules/sessionstore/PageStyle.jsm"); |
|
25 XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", |
|
26 "resource://gre/modules/ScrollPosition.jsm"); |
|
27 XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory", |
|
28 "resource:///modules/sessionstore/SessionHistory.jsm"); |
|
29 XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", |
|
30 "resource:///modules/sessionstore/SessionStorage.jsm"); |
|
31 |
|
32 Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this); |
|
33 let gFrameTree = new FrameTree(this); |
|
34 |
|
35 Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this); |
|
36 XPCOMUtils.defineLazyGetter(this, 'gContentRestore', |
|
37 () => { return new ContentRestore(this) }); |
|
38 |
|
39 /** |
|
40 * Returns a lazy function that will evaluate the given |
|
41 * function |fn| only once and cache its return value. |
|
42 */ |
|
43 function createLazy(fn) { |
|
44 let cached = false; |
|
45 let cachedValue = null; |
|
46 |
|
47 return function lazy() { |
|
48 if (!cached) { |
|
49 cachedValue = fn(); |
|
50 cached = true; |
|
51 } |
|
52 |
|
53 return cachedValue; |
|
54 }; |
|
55 } |
|
56 |
|
57 /** |
|
58 * Determines whether the given storage event was triggered by changes |
|
59 * to the sessionStorage object and not the local or globalStorage. |
|
60 */ |
|
61 function isSessionStorageEvent(event) { |
|
62 try { |
|
63 return event.storageArea == content.sessionStorage; |
|
64 } catch (ex if ex instanceof Ci.nsIException && ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { |
|
65 // This page does not have a DOMSessionStorage |
|
66 // (this is typically the case for about: pages) |
|
67 return false; |
|
68 } |
|
69 } |
|
70 |
|
71 /** |
|
72 * Listens for and handles content events that we need for the |
|
73 * session store service to be notified of state changes in content. |
|
74 */ |
|
75 let EventListener = { |
|
76 |
|
77 init: function () { |
|
78 addEventListener("load", this, true); |
|
79 }, |
|
80 |
|
81 handleEvent: function (event) { |
|
82 // Ignore load events from subframes. |
|
83 if (event.target != content.document) { |
|
84 return; |
|
85 } |
|
86 |
|
87 // If we're in the process of restoring, this load may signal |
|
88 // the end of the restoration. |
|
89 let epoch = gContentRestore.getRestoreEpoch(); |
|
90 if (!epoch) { |
|
91 return; |
|
92 } |
|
93 |
|
94 // Restore the form data and scroll position. |
|
95 gContentRestore.restoreDocument(); |
|
96 |
|
97 // Ask SessionStore.jsm to trigger SSTabRestored. |
|
98 sendAsyncMessage("SessionStore:restoreDocumentComplete", {epoch: epoch}); |
|
99 } |
|
100 }; |
|
101 |
|
102 /** |
|
103 * Listens for and handles messages sent by the session store service. |
|
104 */ |
|
105 let MessageListener = { |
|
106 |
|
107 MESSAGES: [ |
|
108 "SessionStore:restoreHistory", |
|
109 "SessionStore:restoreTabContent", |
|
110 "SessionStore:resetRestore", |
|
111 ], |
|
112 |
|
113 init: function () { |
|
114 this.MESSAGES.forEach(m => addMessageListener(m, this)); |
|
115 }, |
|
116 |
|
117 receiveMessage: function ({name, data}) { |
|
118 switch (name) { |
|
119 case "SessionStore:restoreHistory": |
|
120 let reloadCallback = () => { |
|
121 // Inform SessionStore.jsm about the reload. It will send |
|
122 // restoreTabContent in response. |
|
123 sendAsyncMessage("SessionStore:reloadPendingTab", {epoch: data.epoch}); |
|
124 }; |
|
125 gContentRestore.restoreHistory(data.epoch, data.tabData, reloadCallback); |
|
126 |
|
127 // When restoreHistory finishes, we send a synchronous message to |
|
128 // SessionStore.jsm so that it can run SSTabRestoring. Users of |
|
129 // SSTabRestoring seem to get confused if chrome and content are out of |
|
130 // sync about the state of the restore (particularly regarding |
|
131 // docShell.currentURI). Using a synchronous message is the easiest way |
|
132 // to temporarily synchronize them. |
|
133 sendSyncMessage("SessionStore:restoreHistoryComplete", {epoch: data.epoch}); |
|
134 break; |
|
135 case "SessionStore:restoreTabContent": |
|
136 let epoch = gContentRestore.getRestoreEpoch(); |
|
137 let finishCallback = () => { |
|
138 // Tell SessionStore.jsm that it may want to restore some more tabs, |
|
139 // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. |
|
140 sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch: epoch}); |
|
141 }; |
|
142 |
|
143 // We need to pass the value of didStartLoad back to SessionStore.jsm. |
|
144 let didStartLoad = gContentRestore.restoreTabContent(finishCallback); |
|
145 |
|
146 sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch: epoch}); |
|
147 |
|
148 if (!didStartLoad) { |
|
149 // Pretend that the load succeeded so that event handlers fire correctly. |
|
150 sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch: epoch}); |
|
151 sendAsyncMessage("SessionStore:restoreDocumentComplete", {epoch: epoch}); |
|
152 } |
|
153 break; |
|
154 case "SessionStore:resetRestore": |
|
155 gContentRestore.resetRestore(); |
|
156 break; |
|
157 default: |
|
158 debug("received unknown message '" + name + "'"); |
|
159 break; |
|
160 } |
|
161 } |
|
162 }; |
|
163 |
|
164 /** |
|
165 * On initialization, this handler gets sent to the parent process as a CPOW. |
|
166 * The parent will use it only to flush pending data from the frame script |
|
167 * when needed, i.e. when closing a tab, closing a window, shutting down, etc. |
|
168 * |
|
169 * This will hopefully not be needed in the future once we have async APIs for |
|
170 * closing windows and tabs. |
|
171 */ |
|
172 let SyncHandler = { |
|
173 init: function () { |
|
174 // Send this object as a CPOW to chrome. In single-process mode, |
|
175 // the synchronous send ensures that the handler object is |
|
176 // available in SessionStore.jsm immediately upon loading |
|
177 // content-sessionStore.js. |
|
178 sendSyncMessage("SessionStore:setupSyncHandler", {}, {handler: this}); |
|
179 }, |
|
180 |
|
181 /** |
|
182 * This function is used to make the tab process flush all data that |
|
183 * hasn't been sent to the parent process, yet. |
|
184 * |
|
185 * @param id (int) |
|
186 * A unique id that represents the last message received by the chrome |
|
187 * process before flushing. We will use this to determine data that |
|
188 * would be lost when data has been sent asynchronously shortly |
|
189 * before flushing synchronously. |
|
190 */ |
|
191 flush: function (id) { |
|
192 MessageQueue.flush(id); |
|
193 }, |
|
194 |
|
195 /** |
|
196 * DO NOT USE - DEBUGGING / TESTING ONLY |
|
197 * |
|
198 * This function is used to simulate certain situations where race conditions |
|
199 * can occur by sending data shortly before flushing synchronously. |
|
200 */ |
|
201 flushAsync: function () { |
|
202 MessageQueue.flushAsync(); |
|
203 } |
|
204 }; |
|
205 |
|
206 /** |
|
207 * Listens for changes to the session history. Whenever the user navigates |
|
208 * we will collect URLs and everything belonging to session history. |
|
209 * |
|
210 * Causes a SessionStore:update message to be sent that contains the current |
|
211 * session history. |
|
212 * |
|
213 * Example: |
|
214 * {entries: [{url: "about:mozilla", ...}, ...], index: 1} |
|
215 */ |
|
216 let SessionHistoryListener = { |
|
217 init: function () { |
|
218 // The frame tree observer is needed to handle navigating away from |
|
219 // an about page. Currently nsISHistoryListener does not have |
|
220 // OnHistoryNewEntry() called for about pages because the history entry is |
|
221 // modified to point at the new page. Once Bug 981900 lands the frame tree |
|
222 // observer can be removed. |
|
223 gFrameTree.addObserver(this); |
|
224 |
|
225 // By adding the SHistoryListener immediately, we will unfortunately be |
|
226 // notified of every history entry as the tab is restored. We don't bother |
|
227 // waiting to add the listener later because these notifications are cheap. |
|
228 // We will likely only collect once since we are batching collection on |
|
229 // a delay. |
|
230 docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory. |
|
231 addSHistoryListener(this); |
|
232 |
|
233 // Collect data if we start with a non-empty shistory. |
|
234 if (!SessionHistory.isEmpty(docShell)) { |
|
235 this.collect(); |
|
236 } |
|
237 }, |
|
238 |
|
239 uninit: function () { |
|
240 let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; |
|
241 if (sessionHistory) { |
|
242 sessionHistory.removeSHistoryListener(this); |
|
243 } |
|
244 }, |
|
245 |
|
246 collect: function () { |
|
247 if (docShell) { |
|
248 MessageQueue.push("history", () => SessionHistory.collect(docShell)); |
|
249 } |
|
250 }, |
|
251 |
|
252 onFrameTreeCollected: function () { |
|
253 this.collect(); |
|
254 }, |
|
255 |
|
256 onFrameTreeReset: function () { |
|
257 this.collect(); |
|
258 }, |
|
259 |
|
260 OnHistoryNewEntry: function (newURI) { |
|
261 this.collect(); |
|
262 }, |
|
263 |
|
264 OnHistoryGoBack: function (backURI) { |
|
265 this.collect(); |
|
266 return true; |
|
267 }, |
|
268 |
|
269 OnHistoryGoForward: function (forwardURI) { |
|
270 this.collect(); |
|
271 return true; |
|
272 }, |
|
273 |
|
274 OnHistoryGotoIndex: function (index, gotoURI) { |
|
275 this.collect(); |
|
276 return true; |
|
277 }, |
|
278 |
|
279 OnHistoryPurge: function (numEntries) { |
|
280 this.collect(); |
|
281 return true; |
|
282 }, |
|
283 |
|
284 OnHistoryReload: function (reloadURI, reloadFlags) { |
|
285 this.collect(); |
|
286 return true; |
|
287 }, |
|
288 |
|
289 OnHistoryReplaceEntry: function (index) { |
|
290 this.collect(); |
|
291 }, |
|
292 |
|
293 QueryInterface: XPCOMUtils.generateQI([ |
|
294 Ci.nsISHistoryListener, |
|
295 Ci.nsISupportsWeakReference |
|
296 ]) |
|
297 }; |
|
298 |
|
299 /** |
|
300 * Listens for scroll position changes. Whenever the user scrolls the top-most |
|
301 * frame we update the scroll position and will restore it when requested. |
|
302 * |
|
303 * Causes a SessionStore:update message to be sent that contains the current |
|
304 * scroll positions as a tree of strings. If no frame of the whole frame tree |
|
305 * is scrolled this will return null so that we don't tack a property onto |
|
306 * the tabData object in the parent process. |
|
307 * |
|
308 * Example: |
|
309 * {scroll: "100,100", children: [null, null, {scroll: "200,200"}]} |
|
310 */ |
|
311 let ScrollPositionListener = { |
|
312 init: function () { |
|
313 addEventListener("scroll", this); |
|
314 gFrameTree.addObserver(this); |
|
315 }, |
|
316 |
|
317 handleEvent: function (event) { |
|
318 let frame = event.target && event.target.defaultView; |
|
319 |
|
320 // Don't collect scroll data for frames created at or after the load event |
|
321 // as SessionStore can't restore scroll data for those. |
|
322 if (frame && gFrameTree.contains(frame)) { |
|
323 MessageQueue.push("scroll", () => this.collect()); |
|
324 } |
|
325 }, |
|
326 |
|
327 onFrameTreeCollected: function () { |
|
328 MessageQueue.push("scroll", () => this.collect()); |
|
329 }, |
|
330 |
|
331 onFrameTreeReset: function () { |
|
332 MessageQueue.push("scroll", () => null); |
|
333 }, |
|
334 |
|
335 collect: function () { |
|
336 return gFrameTree.map(ScrollPosition.collect); |
|
337 } |
|
338 }; |
|
339 |
|
340 /** |
|
341 * Listens for changes to input elements. Whenever the value of an input |
|
342 * element changes we will re-collect data for the current frame tree and send |
|
343 * a message to the parent process. |
|
344 * |
|
345 * Causes a SessionStore:update message to be sent that contains the form data |
|
346 * for all reachable frames. |
|
347 * |
|
348 * Example: |
|
349 * { |
|
350 * formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}}, |
|
351 * children: [ |
|
352 * null, |
|
353 * {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}} |
|
354 * ] |
|
355 * } |
|
356 */ |
|
357 let FormDataListener = { |
|
358 init: function () { |
|
359 addEventListener("input", this, true); |
|
360 addEventListener("change", this, true); |
|
361 gFrameTree.addObserver(this); |
|
362 }, |
|
363 |
|
364 handleEvent: function (event) { |
|
365 let frame = event.target && |
|
366 event.target.ownerDocument && |
|
367 event.target.ownerDocument.defaultView; |
|
368 |
|
369 // Don't collect form data for frames created at or after the load event |
|
370 // as SessionStore can't restore form data for those. |
|
371 if (frame && gFrameTree.contains(frame)) { |
|
372 MessageQueue.push("formdata", () => this.collect()); |
|
373 } |
|
374 }, |
|
375 |
|
376 onFrameTreeReset: function () { |
|
377 MessageQueue.push("formdata", () => null); |
|
378 }, |
|
379 |
|
380 collect: function () { |
|
381 return gFrameTree.map(FormData.collect); |
|
382 } |
|
383 }; |
|
384 |
|
385 /** |
|
386 * Listens for changes to the page style. Whenever a different page style is |
|
387 * selected or author styles are enabled/disabled we send a message with the |
|
388 * currently applied style to the chrome process. |
|
389 * |
|
390 * Causes a SessionStore:update message to be sent that contains the currently |
|
391 * selected pageStyle for all reachable frames. |
|
392 * |
|
393 * Example: |
|
394 * {pageStyle: "Dusk", children: [null, {pageStyle: "Mozilla"}]} |
|
395 */ |
|
396 let PageStyleListener = { |
|
397 init: function () { |
|
398 Services.obs.addObserver(this, "author-style-disabled-changed", false); |
|
399 Services.obs.addObserver(this, "style-sheet-applicable-state-changed", false); |
|
400 gFrameTree.addObserver(this); |
|
401 }, |
|
402 |
|
403 uninit: function () { |
|
404 Services.obs.removeObserver(this, "author-style-disabled-changed"); |
|
405 Services.obs.removeObserver(this, "style-sheet-applicable-state-changed"); |
|
406 }, |
|
407 |
|
408 observe: function (subject, topic) { |
|
409 let frame = subject.defaultView; |
|
410 |
|
411 if (frame && gFrameTree.contains(frame)) { |
|
412 MessageQueue.push("pageStyle", () => this.collect()); |
|
413 } |
|
414 }, |
|
415 |
|
416 collect: function () { |
|
417 return PageStyle.collect(docShell, gFrameTree); |
|
418 }, |
|
419 |
|
420 onFrameTreeCollected: function () { |
|
421 MessageQueue.push("pageStyle", () => this.collect()); |
|
422 }, |
|
423 |
|
424 onFrameTreeReset: function () { |
|
425 MessageQueue.push("pageStyle", () => null); |
|
426 } |
|
427 }; |
|
428 |
|
429 /** |
|
430 * Listens for changes to docShell capabilities. Whenever a new load is started |
|
431 * we need to re-check the list of capabilities and send message when it has |
|
432 * changed. |
|
433 * |
|
434 * Causes a SessionStore:update message to be sent that contains the currently |
|
435 * disabled docShell capabilities (all nsIDocShell.allow* properties set to |
|
436 * false) as a string - i.e. capability names separate by commas. |
|
437 */ |
|
438 let DocShellCapabilitiesListener = { |
|
439 /** |
|
440 * This field is used to compare the last docShell capabilities to the ones |
|
441 * that have just been collected. If nothing changed we won't send a message. |
|
442 */ |
|
443 _latestCapabilities: "", |
|
444 |
|
445 init: function () { |
|
446 gFrameTree.addObserver(this); |
|
447 }, |
|
448 |
|
449 /** |
|
450 * onFrameTreeReset() is called as soon as we start loading a page. |
|
451 */ |
|
452 onFrameTreeReset: function() { |
|
453 // The order of docShell capabilities cannot change while we're running |
|
454 // so calling join() without sorting before is totally sufficient. |
|
455 let caps = DocShellCapabilities.collect(docShell).join(","); |
|
456 |
|
457 // Send new data only when the capability list changes. |
|
458 if (caps != this._latestCapabilities) { |
|
459 this._latestCapabilities = caps; |
|
460 MessageQueue.push("disallow", () => caps || null); |
|
461 } |
|
462 } |
|
463 }; |
|
464 |
|
465 /** |
|
466 * Listens for changes to the DOMSessionStorage. Whenever new keys are added, |
|
467 * existing ones removed or changed, or the storage is cleared we will send a |
|
468 * message to the parent process containing up-to-date sessionStorage data. |
|
469 * |
|
470 * Causes a SessionStore:update message to be sent that contains the current |
|
471 * DOMSessionStorage contents. The data is a nested object using host names |
|
472 * as keys and per-host DOMSessionStorage data as values. |
|
473 */ |
|
474 let SessionStorageListener = { |
|
475 init: function () { |
|
476 addEventListener("MozStorageChanged", this); |
|
477 Services.obs.addObserver(this, "browser:purge-domain-data", false); |
|
478 gFrameTree.addObserver(this); |
|
479 }, |
|
480 |
|
481 uninit: function () { |
|
482 Services.obs.removeObserver(this, "browser:purge-domain-data"); |
|
483 }, |
|
484 |
|
485 handleEvent: function (event) { |
|
486 // Ignore events triggered by localStorage or globalStorage changes. |
|
487 if (gFrameTree.contains(event.target) && isSessionStorageEvent(event)) { |
|
488 this.collect(); |
|
489 } |
|
490 }, |
|
491 |
|
492 observe: function () { |
|
493 // Collect data on the next tick so that any other observer |
|
494 // that needs to purge data can do its work first. |
|
495 setTimeout(() => this.collect(), 0); |
|
496 }, |
|
497 |
|
498 collect: function () { |
|
499 if (docShell) { |
|
500 MessageQueue.push("storage", () => SessionStorage.collect(docShell, gFrameTree)); |
|
501 } |
|
502 }, |
|
503 |
|
504 onFrameTreeCollected: function () { |
|
505 this.collect(); |
|
506 }, |
|
507 |
|
508 onFrameTreeReset: function () { |
|
509 this.collect(); |
|
510 } |
|
511 }; |
|
512 |
|
513 /** |
|
514 * Listen for changes to the privacy status of the tab. |
|
515 * By definition, tabs start in non-private mode. |
|
516 * |
|
517 * Causes a SessionStore:update message to be sent for |
|
518 * field "isPrivate". This message contains |
|
519 * |true| if the tab is now private |
|
520 * |null| if the tab is now public - the field is therefore |
|
521 * not saved. |
|
522 */ |
|
523 let PrivacyListener = { |
|
524 init: function() { |
|
525 docShell.addWeakPrivacyTransitionObserver(this); |
|
526 |
|
527 // Check that value at startup as it might have |
|
528 // been set before the frame script was loaded. |
|
529 if (docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing) { |
|
530 MessageQueue.push("isPrivate", () => true); |
|
531 } |
|
532 }, |
|
533 |
|
534 // Ci.nsIPrivacyTransitionObserver |
|
535 privateModeChanged: function(enabled) { |
|
536 MessageQueue.push("isPrivate", () => enabled || null); |
|
537 }, |
|
538 |
|
539 QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivacyTransitionObserver, |
|
540 Ci.nsISupportsWeakReference]) |
|
541 }; |
|
542 |
|
543 /** |
|
544 * A message queue that takes collected data and will take care of sending it |
|
545 * to the chrome process. It allows flushing using synchronous messages and |
|
546 * takes care of any race conditions that might occur because of that. Changes |
|
547 * will be batched if they're pushed in quick succession to avoid a message |
|
548 * flood. |
|
549 */ |
|
550 let MessageQueue = { |
|
551 /** |
|
552 * A unique, monotonically increasing ID used for outgoing messages. This is |
|
553 * important to make it possible to reuse tabs and allow sync flushes before |
|
554 * data could be destroyed. |
|
555 */ |
|
556 _id: 1, |
|
557 |
|
558 /** |
|
559 * A map (string -> lazy fn) holding lazy closures of all queued data |
|
560 * collection routines. These functions will return data collected from the |
|
561 * docShell. |
|
562 */ |
|
563 _data: new Map(), |
|
564 |
|
565 /** |
|
566 * A map holding the |this._id| value for every type of data back when it |
|
567 * was pushed onto the queue. We will use those IDs to find the data to send |
|
568 * and flush. |
|
569 */ |
|
570 _lastUpdated: new Map(), |
|
571 |
|
572 /** |
|
573 * The delay (in ms) used to delay sending changes after data has been |
|
574 * invalidated. |
|
575 */ |
|
576 BATCH_DELAY_MS: 1000, |
|
577 |
|
578 /** |
|
579 * The current timeout ID, null if there is no queue data. We use timeouts |
|
580 * to damp a flood of data changes and send lots of changes as one batch. |
|
581 */ |
|
582 _timeout: null, |
|
583 |
|
584 /** |
|
585 * Pushes a given |value| onto the queue. The given |key| represents the type |
|
586 * of data that is stored and can override data that has been queued before |
|
587 * but has not been sent to the parent process, yet. |
|
588 * |
|
589 * @param key (string) |
|
590 * A unique identifier specific to the type of data this is passed. |
|
591 * @param fn (function) |
|
592 * A function that returns the value that will be sent to the parent |
|
593 * process. |
|
594 */ |
|
595 push: function (key, fn) { |
|
596 this._data.set(key, createLazy(fn)); |
|
597 this._lastUpdated.set(key, this._id); |
|
598 |
|
599 if (!this._timeout) { |
|
600 // Wait a little before sending the message to batch multiple changes. |
|
601 this._timeout = setTimeout(() => this.send(), this.BATCH_DELAY_MS); |
|
602 } |
|
603 }, |
|
604 |
|
605 /** |
|
606 * Sends queued data to the chrome process. |
|
607 * |
|
608 * @param options (object) |
|
609 * {id: 123} to override the update ID used to accumulate data to send. |
|
610 * {sync: true} to send data to the parent process synchronously. |
|
611 */ |
|
612 send: function (options = {}) { |
|
613 // Looks like we have been called off a timeout after the tab has been |
|
614 // closed. The docShell is gone now and we can just return here as there |
|
615 // is nothing to do. |
|
616 if (!docShell) { |
|
617 return; |
|
618 } |
|
619 |
|
620 if (this._timeout) { |
|
621 clearTimeout(this._timeout); |
|
622 this._timeout = null; |
|
623 } |
|
624 |
|
625 let sync = options && options.sync; |
|
626 let startID = (options && options.id) || this._id; |
|
627 |
|
628 // We use sendRpcMessage in the sync case because we may have been called |
|
629 // through a CPOW. RPC messages are the only synchronous messages that the |
|
630 // child is allowed to send to the parent while it is handling a CPOW |
|
631 // request. |
|
632 let sendMessage = sync ? sendRpcMessage : sendAsyncMessage; |
|
633 |
|
634 let durationMs = Date.now(); |
|
635 |
|
636 let data = {}; |
|
637 for (let [key, id] of this._lastUpdated) { |
|
638 // There is no data for the given key anymore because |
|
639 // the parent process already marked it as received. |
|
640 if (!this._data.has(key)) { |
|
641 continue; |
|
642 } |
|
643 |
|
644 if (startID > id) { |
|
645 // If the |id| passed by the parent process is higher than the one |
|
646 // stored in |_lastUpdated| for the given key we know that the parent |
|
647 // received all necessary data and we can remove it from the map. |
|
648 this._data.delete(key); |
|
649 continue; |
|
650 } |
|
651 |
|
652 data[key] = this._data.get(key)(); |
|
653 } |
|
654 |
|
655 durationMs = Date.now() - durationMs; |
|
656 let telemetry = { |
|
657 FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS: durationMs |
|
658 } |
|
659 |
|
660 // Send all data to the parent process. |
|
661 sendMessage("SessionStore:update", { |
|
662 id: this._id, |
|
663 data: data, |
|
664 telemetry: telemetry |
|
665 }); |
|
666 |
|
667 // Increase our unique message ID. |
|
668 this._id++; |
|
669 }, |
|
670 |
|
671 /** |
|
672 * This function is used to make the message queue flush all queue data that |
|
673 * hasn't been sent to the parent process, yet. |
|
674 * |
|
675 * @param id (int) |
|
676 * A unique id that represents the latest message received by the |
|
677 * chrome process. We can use this to determine which messages have not |
|
678 * yet been received because they are still stuck in the event queue. |
|
679 */ |
|
680 flush: function (id) { |
|
681 // It's important to always send data, even if there is nothing to flush. |
|
682 // The update message will be received by the parent process that can then |
|
683 // update its last received update ID to ignore stale messages. |
|
684 this.send({id: id + 1, sync: true}); |
|
685 |
|
686 this._data.clear(); |
|
687 this._lastUpdated.clear(); |
|
688 }, |
|
689 |
|
690 /** |
|
691 * DO NOT USE - DEBUGGING / TESTING ONLY |
|
692 * |
|
693 * This function is used to simulate certain situations where race conditions |
|
694 * can occur by sending data shortly before flushing synchronously. |
|
695 */ |
|
696 flushAsync: function () { |
|
697 if (!Services.prefs.getBoolPref("browser.sessionstore.debug")) { |
|
698 throw new Error("flushAsync() must be used for testing, only."); |
|
699 } |
|
700 |
|
701 this.send(); |
|
702 } |
|
703 }; |
|
704 |
|
705 EventListener.init(); |
|
706 MessageListener.init(); |
|
707 FormDataListener.init(); |
|
708 SyncHandler.init(); |
|
709 PageStyleListener.init(); |
|
710 SessionHistoryListener.init(); |
|
711 SessionStorageListener.init(); |
|
712 ScrollPositionListener.init(); |
|
713 DocShellCapabilitiesListener.init(); |
|
714 PrivacyListener.init(); |
|
715 |
|
716 addEventListener("unload", () => { |
|
717 // Remove all registered nsIObservers. |
|
718 PageStyleListener.uninit(); |
|
719 SessionStorageListener.uninit(); |
|
720 SessionHistoryListener.uninit(); |
|
721 |
|
722 // We don't need to take care of any gFrameTree observers as the gFrameTree |
|
723 // will die with the content script. The same goes for the privacy transition |
|
724 // observer that will die with the docShell when the tab is closed. |
|
725 }); |