GeckoViewSessionStore.sys.mjs (7575B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs", 11 }); 12 13 const { debug, warn } = GeckoViewUtils.initLogging("SessionStore"); 14 const kNoIndex = Number.MAX_SAFE_INTEGER; 15 const kLastIndex = Number.MAX_SAFE_INTEGER - 1; 16 17 class SHistoryListener { 18 constructor(browsingContext) { 19 this.QueryInterface = ChromeUtils.generateQI([ 20 "nsISHistoryListener", 21 "nsISupportsWeakReference", 22 ]); 23 24 this._browserId = browsingContext.browserId; 25 this._fromIndex = kNoIndex; 26 } 27 28 unregister(permanentKey) { 29 const bc = BrowsingContext.getCurrentTopByBrowserId(this._browserId); 30 bc?.sessionHistory?.removeSHistoryListener(this); 31 GeckoViewSessionStore._browserSHistoryListener?.delete(permanentKey); 32 } 33 34 collect( 35 permanentKey, // eslint-disable-line no-shadow 36 browsingContext, // eslint-disable-line no-shadow 37 { collectFull = true, writeToCache = false } 38 ) { 39 // Don't bother doing anything if we haven't seen any navigations. 40 if (!collectFull && this._fromIndex === kNoIndex) { 41 return null; 42 } 43 44 // In a private session, when the first loaded uri is one of the about pages, the 45 // browsingContext.currentURI is null which causes a crash (see Bug 1928295). 46 // TODO: Remove this if check when Bug 1915362 gets implemented where the partial 47 // state update will be fixing this issue. See Bug 1933630. 48 if (!browsingContext.currentURI?.spec) { 49 return null; 50 } 51 52 const fromIndex = collectFull ? -1 : this._fromIndex; 53 this._fromIndex = kNoIndex; 54 55 const historychange = lazy.SessionHistory.collectFromParent( 56 browsingContext.currentURI?.spec, 57 true, // Bug 1704574 58 browsingContext.sessionHistory, 59 fromIndex 60 ); 61 62 if (writeToCache) { 63 const win = 64 browsingContext.embedderElement?.ownerGlobal || 65 browsingContext.currentWindowGlobal?.browsingContext?.window; 66 67 GeckoViewSessionStore.onTabStateUpdate(permanentKey, win, { 68 data: { historychange }, 69 }); 70 } 71 72 return historychange; 73 } 74 75 collectFrom(index) { 76 if (this._fromIndex <= index) { 77 // If we already know that we need to update history from index N we 78 // can ignore any changes that happened with an element with index 79 // larger than N. 80 // 81 // Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which 82 // means we don't ignore anything here, and in case of navigation in 83 // the history back and forth cases we use kLastIndex which ignores 84 // only the subsequent navigations, but not any new elements added. 85 return; 86 } 87 88 const bc = BrowsingContext.getCurrentTopByBrowserId(this._browserId); 89 if (bc?.embedderElement?.frameLoader) { 90 this._fromIndex = index; 91 92 // Queue a tab state update on the |browser.sessionstore.interval| 93 // timer. We'll call this.collect() when we receive the update. 94 bc.embedderElement.frameLoader.requestSHistoryUpdate(); 95 } 96 } 97 98 OnHistoryNewEntry(newURI, oldIndex) { 99 // We use oldIndex - 1 to collect the current entry as well. This makes 100 // sure to collect any changes that were made to the entry while the 101 // document was active. 102 this.collectFrom(oldIndex == -1 ? oldIndex : oldIndex - 1); 103 } 104 OnHistoryGotoIndex() { 105 this.collectFrom(kLastIndex); 106 } 107 OnHistoryPurge() { 108 this.collectFrom(-1); 109 } 110 OnHistoryReload() { 111 this.collectFrom(-1); 112 return true; 113 } 114 OnHistoryReplaceEntry() { 115 this.collectFrom(-1); 116 } 117 } 118 119 export var GeckoViewSessionStore = { 120 // For each <browser> element, records the SHistoryListener. 121 _browserSHistoryListener: new WeakMap(), 122 123 observe(aSubject, aTopic) { 124 debug`observe ${aTopic}`; 125 126 switch (aTopic) { 127 case "browsing-context-did-set-embedder": { 128 if (aSubject === aSubject.top && aSubject.isContent) { 129 const permanentKey = aSubject.embedderElement?.permanentKey; 130 if (permanentKey) { 131 this.maybeRecreateSHistoryListener(permanentKey, aSubject); 132 } 133 } 134 break; 135 } 136 case "browsing-context-discarded": { 137 const permanentKey = aSubject?.embedderElement?.permanentKey; 138 if (permanentKey) { 139 this._browserSHistoryListener 140 .get(permanentKey) 141 ?.unregister(permanentKey); 142 } 143 break; 144 } 145 } 146 }, 147 148 onTabStateUpdate(permanentKey, win, data) { 149 win.WindowEventDispatcher.sendRequest({ 150 type: "GeckoView:StateUpdated", 151 data: data.data, 152 }); 153 }, 154 155 getOrCreateSHistoryListener(permanentKey, browsingContext) { 156 if (!permanentKey || browsingContext !== browsingContext.top) { 157 return null; 158 } 159 160 const listener = this._browserSHistoryListener.get(permanentKey); 161 if (listener) { 162 return listener; 163 } 164 165 return this.createSHistoryListener(permanentKey, browsingContext, false); 166 }, 167 168 maybeRecreateSHistoryListener(permanentKey, browsingContext) { 169 const listener = this._browserSHistoryListener.get(permanentKey); 170 if (!listener || listener._browserId != browsingContext.browserId) { 171 listener?.unregister(permanentKey); 172 this.createSHistoryListener(permanentKey, browsingContext, true); 173 } 174 }, 175 176 createSHistoryListener(permanentKey, browsingContext, collectImmediately) { 177 const sessionHistory = browsingContext.sessionHistory; 178 if (!sessionHistory) { 179 return null; 180 } 181 182 const listener = new SHistoryListener(browsingContext); 183 sessionHistory.addSHistoryListener(listener); 184 this._browserSHistoryListener.set(permanentKey, listener); 185 186 if ( 187 collectImmediately && 188 (!(browsingContext.currentURI?.spec === "about:blank") || 189 sessionHistory.count !== 0) 190 ) { 191 listener.collect(permanentKey, browsingContext, { writeToCache: true }); 192 } 193 194 return listener; 195 }, 196 197 updateSessionStoreFromTabListener( 198 browser, 199 browsingContext, 200 permanentKey, 201 update, 202 forStorage = false 203 ) { 204 permanentKey = browser?.permanentKey ?? permanentKey; 205 if (!permanentKey) { 206 return; 207 } 208 209 if (browsingContext.isReplaced) { 210 return; 211 } 212 213 const listener = this.getOrCreateSHistoryListener( 214 permanentKey, 215 browsingContext 216 ); 217 218 if (listener) { 219 const historychange = 220 // If it is not the scheduled update (tab closed, window closed etc), 221 // try to store the loading non-web-controlled page opened in _blank 222 // first. 223 (forStorage && 224 lazy.SessionHistory.collectNonWebControlledBlankLoadingSession( 225 browsingContext 226 )) || 227 listener.collect(permanentKey, browsingContext, { 228 // TODO: See Bug 1915362 where partial history collection will be implemented for better 229 // performance and uncomment the line below. See Bug 1933630. 230 // collectFull: !!update.sHistoryNeeded, 231 writeToCache: false, 232 }); 233 234 if (historychange) { 235 update.data.historychange = historychange; 236 } 237 } 238 239 const win = 240 browsingContext.embedderElement?.ownerGlobal || 241 browsingContext.currentWindowGlobal?.browsingContext?.window; 242 243 this.onTabStateUpdate(permanentKey, win, update); 244 }, 245 };