OLD | NEW |
(Empty) | |
| 1 let {Logger} = require( "logger" ); |
| 2 |
| 3 //------------------------------------------------------- |
| 4 // Tabbed_Browser |
| 5 //------------------------------------------------------- |
| 6 /** |
| 7 * A single OS-level window of a multiple-tab Firefox browser. This is the objec
t referred to by the global 'gBrowser'. |
| 8 * |
| 9 * @param {Window} window |
| 10 * @param {Number} max_requests |
| 11 * The maximum number of simultaneous requests this object may have. |
| 12 * @constructor |
| 13 */ |
| 14 var Tabbed_Browser = function( window, max_requests ) |
| 15 { |
| 16 /** |
| 17 * Browser window through which we access the global browser object. |
| 18 * @type {Window} |
| 19 */ |
| 20 this.window = window; |
| 21 |
| 22 /** |
| 23 * A browser object that can hold multiple individual tabbed browser panes. |
| 24 */ |
| 25 this.tabbed_browser = this.window.gBrowser; |
| 26 |
| 27 /** |
| 28 * The current number of pending requests in child tabs of this object. |
| 29 * @type {Number} |
| 30 */ |
| 31 this.n_requests = 0; |
| 32 |
| 33 /** |
| 34 * The maximum number of simultaneous requests this object may have. |
| 35 * @type {Number} |
| 36 */ |
| 37 this.max_requests = max_requests; |
| 38 |
| 39 /** |
| 40 * The heart of the dispatcher for both handling progress events and trackin
g block activity is this map from |
| 41 * browser objects to Browser_Tab ones. |
| 42 * @type {Map} |
| 43 */ |
| 44 this.map_browser_to_child = new Map(); |
| 45 |
| 46 /** |
| 47 * Every object in the range of the WeakMap has this object as its prototype
. This enables map values to have |
| 48 * sane defaults. |
| 49 * |
| 50 * @type {Object} |
| 51 */ |
| 52 //this.map_range_prototype = { browser: null }; |
| 53 |
| 54 /** |
| 55 * A transient set for allocated requests that have not started their load c
ycle. |
| 56 * @type {Set} |
| 57 */ |
| 58 this.allocated_not_loaded = new Set(); |
| 59 |
| 60 this.listener = { onStateChange: this._progress.bind( this ) }; |
| 61 this.tabbed_browser.addTabsProgressListener( this.listener ); |
| 62 |
| 63 this.logger = new Logger( "Tabbed_Browser" ); |
| 64 }; |
| 65 |
| 66 /** |
| 67 * Release resources held by this object. This includes event handlers. We also
close all the child tabs, since they |
| 68 * won't work right after our progress event handler is no longer registered. |
| 69 */ |
| 70 Tabbed_Browser.prototype.close = function() |
| 71 { |
| 72 var log = this.logger.make_log( "close" ); |
| 73 log( "Tabbed_Browser.close", false ); |
| 74 if ( this.listener ) |
| 75 { |
| 76 this.tabbed_browser.removeTabsProgressListener( this.listener ); |
| 77 this.listener = null; |
| 78 } |
| 79 |
| 80 let pair = null; |
| 81 for ( pair of this.map_browser_to_child ) |
| 82 { |
| 83 let [ key, value ] = pair; |
| 84 value.child.close(); |
| 85 this.map_browser_to_child.delete( key ); |
| 86 } |
| 87 }; |
| 88 |
| 89 /** |
| 90 * Predicate "is there an open request slot?" |
| 91 */ |
| 92 Tabbed_Browser.prototype.available = function() |
| 93 { |
| 94 return this.n_requests < this.max_requests; |
| 95 }; |
| 96 |
| 97 /** |
| 98 * Predicate: "Are there no open tabs?" |
| 99 * @return {boolean} |
| 100 */ |
| 101 Tabbed_Browser.prototype.quiescent = function() |
| 102 { |
| 103 return this.n_requests == 0; |
| 104 }; |
| 105 |
| 106 /** |
| 107 * @param {Boolean} [leave_open=false] |
| 108 * Leave the tab open in the browser after closing the present object |
| 109 */ |
| 110 Tabbed_Browser.prototype.make_tab = function( leave_open ) |
| 111 { |
| 112 return new Browser_Tab( this, leave_open ); |
| 113 }; |
| 114 |
| 115 /** |
| 116 * Request an allocation of available HTTP requests. Allocates one if available. |
| 117 * <p/> |
| 118 * HAZARD: This request is made when the asynchronous action is created, which i
s strictly before it is launched. If |
| 119 * the caller does not either launch the action or close it, there will be an in
ternal resource leak here. |
| 120 * |
| 121 * @param child |
| 122 * @return {Boolean} |
| 123 */ |
| 124 Tabbed_Browser.prototype.request_load = function( child ) |
| 125 { |
| 126 if ( !this.available() ) |
| 127 { |
| 128 return false; |
| 129 } |
| 130 ++this.n_requests; |
| 131 this.allocated_not_loaded.add( child ); |
| 132 return true; |
| 133 }; |
| 134 |
| 135 /** |
| 136 * Notification that a child tab is loading a page. This constitutes a change in
the number of unallocated requests. |
| 137 * |
| 138 * @param {Browser_Tab} child |
| 139 */ |
| 140 Tabbed_Browser.prototype.notify_load_begin = function( child ) |
| 141 { |
| 142 if ( this.allocated_not_loaded.has( child ) ) |
| 143 { |
| 144 this.allocated_not_loaded.delete( child ); |
| 145 } |
| 146 else |
| 147 { |
| 148 Cu.reportError( "notice_load_begin: child not found" ); |
| 149 throw "notice_load_begin: child not found"; |
| 150 } |
| 151 let value = { child: child }; |
| 152 this.map_browser_to_child.set( child.browser, value ); |
| 153 }; |
| 154 |
| 155 /** |
| 156 * Notification that a child tab is loading a page. This constitutes a change in
the number of unallocated requests. |
| 157 * <p/> |
| 158 * The child must only call this function once, since it acts as a resource deal
locator, freeing up a request slot. |
| 159 */ |
| 160 Tabbed_Browser.prototype.notify_load_end = function() |
| 161 { |
| 162 if ( this.n_requests <= 0 ) |
| 163 { |
| 164 throw "Tabbed_Browser.notify_load_end: n_requests <= 0"; |
| 165 } |
| 166 --this.n_requests; |
| 167 }; |
| 168 |
| 169 /** |
| 170 * Notification that a child tab is closing. We leave the tab present in our map
of active children until the tab is |
| 171 * closed. This allows us to handle events that occur after the document has loa
ded, which typically arise from |
| 172 * scripts on the page. |
| 173 * |
| 174 * @param child |
| 175 */ |
| 176 Tabbed_Browser.prototype.notify_close = function( child ) |
| 177 { |
| 178 if ( this.map_browser_to_child.has( child.browser ) ) |
| 179 { |
| 180 this.map_browser_to_child.delete( child.browser ); |
| 181 } |
| 182 else |
| 183 { |
| 184 // If we're getting this notice, it really should be in our map |
| 185 Cu.reportError( "Child browser not found in map during 'notice_close()'"
); |
| 186 } |
| 187 }; |
| 188 |
| 189 //noinspection JSUnusedLocalSymbols |
| 190 /** |
| 191 * Progress event handler. It looks only for STOP states on the present tab. Whe
n that happens, it determines the |
| 192 * success status and calls the landing function. |
| 193 * |
| 194 * @param {*} browser |
| 195 * @param {nsIWebProgress} controller |
| 196 * The control object for progress monitoring that dispatches the event. |
| 197 * @param {nsIRequest} browse_request |
| 198 * The request object generated by the called to addTab(), which loads a pa
ge. |
| 199 * @param state |
| 200 * The progress state, represented as flags. |
| 201 * @param stop_status |
| 202 * Status code for success or failure if the argument state is a STOP state
. |
| 203 */ |
| 204 Tabbed_Browser.prototype._progress = function( browser, controller, browse_reque
st, state, stop_status ) |
| 205 { |
| 206 /* |
| 207 * We only care about STOP states. We're not tracking redirects, which is on
e of the progress states possible. |
| 208 * We may want to in the future, though, in case redirect behavior is involv
ed with ad delivery in some way. |
| 209 * |
| 210 * As a point of warning, traces on these messages shows that the START mess
age is delivered to the present |
| 211 * function _before_ 'notify_load_begin' is called, which seems to mean that
the JS interpreter is doing something |
| 212 * fishy, either using a second thread or dispatching during a function invo
cation or return. Regardless, this |
| 213 * event come in before it's possible that 'map_browser_to_child' has the 'b
rowser' element of a new tab as a key. |
| 214 * Thus, a warning that trapping any other progress state here should happen
only after thoroughly tracing the |
| 215 * event sequence to determine the actual behavior. |
| 216 */ |
| 217 //noinspection JSBitwiseOperatorUsage |
| 218 if ( !(state & Ci.nsIWebProgressListener.STATE_STOP) ) |
| 219 return; |
| 220 |
| 221 /* |
| 222 * This handler receives events for all the tabs present in a tabbrowser ele
ment, even ones that we didn't |
| 223 * add ourselves. It's not an error to receive such events. |
| 224 */ |
| 225 if ( !this.map_browser_to_child.has( browser ) ) |
| 226 { |
| 227 return; |
| 228 } |
| 229 |
| 230 var {child} = this.map_browser_to_child.get( browser ); |
| 231 child._stop( stop_status ); |
| 232 |
| 233 var log = this.logger.make_log( "_progress" ); |
| 234 log( "request name = " + browse_request.name, false ); |
| 235 }; |
| 236 |
| 237 //------------------------------------------------------- |
| 238 // Browser_Tab |
| 239 //------------------------------------------------------- |
| 240 /** |
| 241 * A single browser tab that can asynchronously load a web page. |
| 242 * |
| 243 * @param {Tabbed_Browser} parent |
| 244 * @param {Boolean} [leave_open=false] |
| 245 * Leave the tab open in the browser after closing the present object |
| 246 * @constructor |
| 247 */ |
| 248 var Browser_Tab = function( parent, leave_open ) |
| 249 { |
| 250 /** |
| 251 * The parent tabbed browser in whose tab set this tab is a member. |
| 252 * @type {Tabbed_Browser} |
| 253 */ |
| 254 this.parent = parent; |
| 255 |
| 256 /** |
| 257 * Leave the tab open in the browser after the crawler exits. The reason to
do this is to allow manual inspection |
| 258 * of the window as the crawler loaded it. |
| 259 * <p/> |
| 260 * It's necessary to call 'close()' on any instance of this object in order
to ensure event handlers are released. |
| 261 * This is true whether or not the tab remains open afterwards. |
| 262 * |
| 263 * @type {Boolean} |
| 264 */ |
| 265 this.leave_open = (arguments.length >= 2) ? leave_open : false; |
| 266 |
| 267 /** |
| 268 * A browser object that can hold multiple individual tabbed browser panes. |
| 269 */ |
| 270 this.tabbed_browser = this.parent.tabbed_browser; |
| 271 |
| 272 /** |
| 273 * Our tab within the tabbed browser. This is the "external" view of browser
pane, the one that allows us to |
| 274 * control loading. The tab must have a URL associated with it, so it's not
displayed at the outset |
| 275 * <p/> |
| 276 * FUTURE: Might it be useful to load the tab with a empty page but descript
ive title at construction time? |
| 277 */ |
| 278 this.tab = null; |
| 279 |
| 280 /** |
| 281 * The function to be run upon completion of an asynchronous load. |
| 282 * @type {Function} |
| 283 */ |
| 284 this.finally_f = null; |
| 285 |
| 286 /** |
| 287 * The function to be run if there's an exceptional termination to an asynch
ronous load. |
| 288 * @type {Function} |
| 289 */ |
| 290 this.catch_f = null; |
| 291 |
| 292 /** |
| 293 * |
| 294 * @type {*} |
| 295 */ |
| 296 this.browser = null; |
| 297 |
| 298 /** |
| 299 * STATE |
| 300 */ |
| 301 this.state = Browser_Tab.STATE.CREATED; |
| 302 }; |
| 303 |
| 304 Browser_Tab.STATE = { |
| 305 // Initial state |
| 306 CREATED: 0, |
| 307 // Nonterminal states |
| 308 LOADING: 1, |
| 309 // Terminal states |
| 310 DISPLAYED: 2, |
| 311 ERROR: 3, |
| 312 CLOSED: 4 |
| 313 }; |
| 314 |
| 315 /** |
| 316 * Predicate "is the Browser_Tab in an initial state for its page load?" |
| 317 * |
| 318 * @return {Boolean} |
| 319 */ |
| 320 Browser_Tab.prototype.in_initial_state = function() |
| 321 { |
| 322 return this.state == Browser_Tab.STATE.CREATED; |
| 323 }; |
| 324 |
| 325 /** |
| 326 * Predicate "is this object in a final state for its page load?" |
| 327 * <p/> |
| 328 * The CLOSED state is considered a final state, although it's present to implem
ent the moral equivalent of a |
| 329 * destructor correctly. |
| 330 * |
| 331 * @return {Boolean} |
| 332 */ |
| 333 Browser_Tab.prototype.in_final_state = function() |
| 334 { |
| 335 return this.state >= Browser_Tab.STATE.DISPLAYED; |
| 336 }; |
| 337 |
| 338 /** |
| 339 * Close function destroys our allocated host resources, such as tabs, listeners
, requests, etc. |
| 340 */ |
| 341 Browser_Tab.prototype.close = function() |
| 342 { |
| 343 if ( this.state == Browser_Tab.STATE.CLOSED ) |
| 344 return; |
| 345 |
| 346 if ( this.tab ) |
| 347 { |
| 348 if ( !this.leave_open ) |
| 349 { |
| 350 this.tabbed_browser.removeTab( this.tab ); |
| 351 } |
| 352 this.tab = null; |
| 353 /* |
| 354 * Kill the map from our associated browser to this object. This is the
point at which we can no longer |
| 355 * locate this object with a 'browser' or 'window' object. |
| 356 */ |
| 357 this.parent.notify_close( this ); |
| 358 this.browser = null; |
| 359 } |
| 360 /* |
| 361 * FUTURE: Cancel any pending page load here. |
| 362 */ |
| 363 this.state = Browser_Tab.STATE.CLOSED; |
| 364 }; |
| 365 |
| 366 /** |
| 367 * Return an asynchronous action that loads a target into a new tab. |
| 368 */ |
| 369 Browser_Tab.prototype.load = function( target ) |
| 370 { |
| 371 if ( !this.parent.request_load( this ) ) |
| 372 { |
| 373 // Should not reach. The caller should be calling available() on the Tab
bed_Browser first. |
| 374 return null; |
| 375 } |
| 376 return { |
| 377 go: function( finally_f, catch_f ) |
| 378 { |
| 379 this.finally_f = finally_f; |
| 380 this.catch_f = catch_f; |
| 381 this._show( target ); |
| 382 }.bind( this ) |
| 383 }; |
| 384 }; |
| 385 |
| 386 /** |
| 387 * Show the tab by loading a URL target into it. |
| 388 * |
| 389 * @param {String} target |
| 390 */ |
| 391 Browser_Tab.prototype._show = function( target ) |
| 392 { |
| 393 if ( !this.in_initial_state() ) |
| 394 return; |
| 395 try |
| 396 { |
| 397 this.tab = this.tabbed_browser.addTab( target ); |
| 398 this.browser = this.tabbed_browser.getBrowserForTab( this.tab ); |
| 399 this.parent.notify_load_begin( this ); |
| 400 } |
| 401 catch ( e ) |
| 402 { |
| 403 this.state = Browser_Tab.STATE.ERROR; |
| 404 Cu.reportError( "Unexpected exception in Browser_Tab.show(): " + e.toStr
ing() ); |
| 405 if ( this.catch_f ) this.catch_f( e ); |
| 406 if ( this.finally_f ) this.finally_f( false, undefined ); |
| 407 } |
| 408 }; |
| 409 |
| 410 /** |
| 411 * Stop event handler. It receives only STOP events on the present tab. When tha
t happens, it determines the |
| 412 * success status and calls the landing function. |
| 413 * |
| 414 * @param stop_status |
| 415 * Status code for success or failure if the argument state is a STOP state
. |
| 416 */ |
| 417 Browser_Tab.prototype._stop = function( stop_status ) |
| 418 { |
| 419 /* |
| 420 * This check ensures that we only call 'finally_f' once. The browser will s
end multiple STOP events when the user |
| 421 * focuses on a tab window by clicking on its tab. Since we set a final stat
e below, checking for a final state |
| 422 * ensures that we act idempotently. |
| 423 * |
| 424 * This check also forestalls a race condition where a request completes and
schedules a progress event while we are |
| 425 * closing the object. |
| 426 */ |
| 427 if ( this.in_final_state() ) |
| 428 return; |
| 429 |
| 430 /* |
| 431 * This notice back to the parent must happen after the check for being in a
final state. Since multiple STOP |
| 432 * events may arrive on a tab (they're not all for the original document), w
e send this notice just once, which |
| 433 * means that we need the state in this Browser_Tab instance. The choice is
to make a call back to the parent |
| 434 * instead of exposing this object's internal state. |
| 435 */ |
| 436 this.parent.notify_load_end(); |
| 437 |
| 438 var success = ( stop_status == 0 ); |
| 439 if ( success ) |
| 440 { |
| 441 this.state = Browser_Tab.STATE.DISPLAYED; |
| 442 } |
| 443 else |
| 444 { |
| 445 this.state = Browser_Tab.STATE.ERROR; |
| 446 /** |
| 447 * This argument is an XPCOM 'nsresult' value. It could be examined if t
he cause of the failure to load needs |
| 448 * to be diagnosed. For example, NS_ERROR_OFFLINE would be useful for su
spending operation of the crawler while |
| 449 * internet connectivity comes back. NS_ERROR_MALFORMED_URI would be use
ful for notifing the user of a typo. |
| 450 */ |
| 451 this.error_code = stop_status; |
| 452 } |
| 453 if ( this.finally_f ) this.finally_f( success, this.error_code ); |
| 454 }; |
| 455 |
| 456 exports.Tabbed_Browser = Tabbed_Browser; |
| 457 exports.Browser_Tab = Browser_Tab; |
OLD | NEW |