browser/components/sessionstore/content/content-sessionStore.js

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

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

mercurial