WordPress.org

Make WordPress Core

Ticket #23216: 23216-heartbeat-cleanup.diff

File 23216-heartbeat-cleanup.diff, 38.6 KB (added by carldanley, 9 months ago)
  • wp-includes/js/heartbeat.js

     
    1 /** 
    2  * Heartbeat API 
    3  * 
    4  * Heartbeat is a simple server polling API that sends XHR requests to 
    5  * the server every 15 seconds and triggers events (or callbacks) upon 
    6  * receiving data. Currently these 'ticks' handle transports for post locking, 
    7  * login-expiration warnings, and related tasks while a user is logged in. 
    8  * 
    9  * Available filters in ajax-actions.php: 
    10  * - heartbeat_received 
    11  * - heartbeat_send 
    12  * - heartbeat_tick 
    13  * - heartbeat_nopriv_received 
    14  * - heartbeat_nopriv_send 
    15  * - heartbeat_nopriv_tick 
    16  * @see wp_ajax_nopriv_heartbeat(), wp_ajax_heartbeat() 
    17  * 
    18  * @since 3.6.0 
    19  */ 
     1( function( window, $, undefined ) { 
    202 
    21  // Ensure the global `wp` object exists. 
    22 window.wp = window.wp || {}; 
     3        // pull the correct document into scope 
     4        var document = window.document; 
    235 
    24 (function($){ 
    25         var Heartbeat = function() { 
    26                 var self = this, 
    27                         running, 
    28                         beat, 
    29                         screenId = typeof pagenow != 'undefined' ? pagenow : '', 
    30                         url = typeof ajaxurl != 'undefined' ? ajaxurl : '', 
    31                         settings, 
    32                         tick = 0, 
    33                         queue = {}, 
    34                         interval, 
    35                         connecting, 
    36                         countdown = 0, 
    37                         errorcount = 0, 
    38                         tempInterval, 
    39                         hasFocus = true, 
    40                         isUserActive, 
    41                         userActiveEvents, 
    42                         winBlurTimeout, 
    43                         frameBlurTimeout = -1; 
     6        /** 
     7         * Heartbeat API 
     8         * 
     9         * Heartbeat is a simple server polling API that sends XHR requests to the server every 15 seconds and triggers events 
     10         * (or callbacks) upon receiving data. Currently these 'ticks' handle transports for post locking, login-expiration 
     11         * warnings, and related tasks while a user is logged in. 
     12         * 
     13         * todo: if this code is run multiple times in different iframes, there could be a problem with iframes becoming 
     14         * inactive but activity occurring in the main window which is really weird... To fix this, we need to check if the 
     15         * current window is the top window just like ads do. This could either be a major bug OR something that's intended 
     16         * but either way should be confirmed ASAP. 
     17         * 
     18         * Available filters in ajax-actions.php: 
     19         *              heartbeat_received 
     20         *              heartbeat_send 
     21         *              heartbeat_tick 
     22         *              heartbeat_nopriv_received 
     23         *              heartbeat_nopriv_send 
     24         *              heartbeat_nopriv_tick 
     25         * @see wp_ajax_nopriv_heartbeat(), wp_ajax_heartbeat() 
     26         * 
     27         * @since 3.6.0 
     28         * @type {*|{}} 
     29         */ 
     30        function Heartbeat() { 
    4431 
    45                 this.autostart = true; 
    46                 this.connectionLost = false; 
     32                /** 
     33                 * Container for all of the cached objects that we'll need references to later 
     34                 * 
     35                 * @type {{$document: null, $window: null, windowBlurTimer: null, frameBlurTimer: null, heartbeatTimer: null, ajaxRequest: null}} 
     36                 * @private 
     37                 */ 
     38                var _Cache = { 
     39                        $document : null, 
     40                        $window : null, 
     41                        windowBlurTimer : null, 
     42                        frameBlurTimer : null, 
     43                        heartbeatTimer : null, 
     44                        ajaxRequest : null 
     45                }; 
    4746 
    48                 if ( typeof( window.heartbeatSettings ) == 'object' ) { 
    49                         settings = $.extend( {}, window.heartbeatSettings ); 
    5047 
    51                         // Add private vars 
    52                         url = settings.ajaxurl || url; 
    53                         delete settings.ajaxurl; 
    54                         delete settings.nonce; 
     48                /** 
     49                 * Container for all of the settings that this module needs to track 
     50                 * 
     51                 * @type {{shouldAutoStart: boolean, hasActiveConnection: boolean, errorCountSinceConnected: number, ajaxURL: string, nonce: string, heartbeatInterval: number, temporaryHeartbeatInterval: number, screenId: string, hasFocus: boolean, userIsActive: boolean, lastUserActivityTimestamp: number, isConnecting: boolean, isRunning: boolean, userActivityCheckInterval: number, lastHeartbeatTick: number, countDown: number, ajaxRequestTimeout: number}} 
     52                 * @private 
     53                 */ 
     54                var _Settings = { 
     55                        shouldAutoStart : true, 
     56                        hasActiveConnection : false, 
     57                        errorCountSinceConnected : 0, 
     58                        ajaxURL : "", 
     59                        nonce : "", 
     60                        heartbeatInterval : 15, 
     61                        temporaryHeartbeatInterval : 0, 
     62                        screenId : "", 
     63                        hasFocus : false, 
     64                        userIsActive : false, 
     65                        lastUserActivityTimestamp : _getUnixTimestamp(), 
     66                        isConnecting : false, 
     67                        isRunning : false, 
     68                        userActivityCheckInterval : 30000, 
     69                        lastHeartbeatTick : 0, 
     70                        countDown : 0, 
     71                        ajaxRequestTimeout : 30000 
     72                }; 
    5573 
    56                         interval = settings.interval || 15; // default interval 
    57                         delete settings.interval; 
    58                         // The interval can be from 15 to 60 sec. and can be set temporarily to 5 sec. 
    59                         if ( interval < 15 ) 
    60                                 interval = 15; 
    61                         else if ( interval > 60 ) 
    62                                 interval = 60; 
     74                /** 
     75                 * Container for any AJAX data that will sent through the next heartbeat pulse 
     76                 * 
     77                 * @type {{}} 
     78                 * @private 
     79                 */ 
     80                var _QueuedAjaxData = {}; 
    6381 
    64                         interval = interval * 1000; 
     82                /** 
     83                 * Handles setting up this object so that things are properly setup before it's used anywhere 
     84                 * 
     85                 * @private 
     86                 */ 
     87                function _initialize() { 
     88                        _buildSettings(); 
     89                        _cacheObjects(); 
     90                        _startCheckingIfTheUserIsActive(); 
     91                        _bindWindowEvents(); 
    6592 
    66                         // 'screenId' can be added from settings on the front-end where the JS global 'pagenow' is not set 
    67                         screenId = screenId || settings.screenId || 'front'; 
    68                         delete settings.screenId; 
     93                        // check to see if we need to automatically start this module 
     94                        if( _Settings.shouldAutoStart === true ) { 
     95                                _Cache.$document.ready( _startAutomaticTick ); 
     96                        } 
     97                } 
    6998 
    70                         // Add or overwrite public vars 
    71                         $.extend( this, settings ); 
     99                /** 
     100                 * Starts the automatic tick if the application should automatically start. Should only be called from `_initialize()` 
     101                 * 
     102                 * @private 
     103                 */ 
     104                function _startAutomaticTick() { 
     105                        _Settings.isRunning = true; 
     106                        _Settings.lastHeartbeatTick = _getUnixTimestamp(); 
     107                        _nextTick(); 
    72108                } 
    73109 
    74                 function time(s) { 
    75                         if ( s ) 
    76                                 return parseInt( (new Date()).getTime() / 1000 ); 
    77  
    78                         return (new Date()).getTime(); 
     110                /** 
     111                 * Starts the interval timer for checking if the user is currently active or not 
     112                 * 
     113                 * @private 
     114                 */ 
     115                function _startCheckingIfTheUserIsActive() { 
     116                        setInterval( _checkIfUserIsStillActive, _Settings.userActivityCheckInterval ); 
    79117                } 
    80118 
    81                 function isLocalFrame( frame ) { 
    82                         var origin, src = frame.src; 
     119                /** 
     120                 * Handles standardizing our internal settings based on settings that might have been localized from the server. 
     121                 * We didn't extend the global window `heartbeatSettings` object because we don't want to make use of `delete` 
     122                 * and have to manage everything we don't care about. 
     123                 * 
     124                 * @private 
     125                 */ 
     126                function _buildSettings() { 
     127                        if( typeof window.heartbeatSettings !== "object" ) { 
     128                                return; 
     129                        } 
    83130 
    84                         if ( src && /^https?:\/\//.test( src ) ) { 
    85                                 origin = window.location.origin ? window.location.origin : window.location.protocol + '//' + window.location.host; 
     131                        // temporarily cache the window heartbeatSettings for easier access 
     132                        var tempSettings = window.heartbeatSettings; 
    86133 
    87                                 if ( src.indexOf( origin ) !== 0 ) 
    88                                         return false; 
     134                        // setup the ajax URL 
     135                        _Settings.ajaxURL = window.ajaxurl || ""; 
     136                        _Settings.ajaxURL = tempSettings.ajaxurl || _Settings.ajaxURL; 
     137 
     138                        // setup the nonce 
     139                        _Settings.nonce = tempSettings.nonce || ""; 
     140 
     141                        // setup the heartbeat interval - force it to an integer. If the value of tempSettings.interval is incorrect 
     142                        // and cannot be parsed, we still default to 10 
     143                        _Settings.heartbeatInterval = parseInt( tempSettings.interval, 10 ) || _Settings.heartbeatInterval; 
     144 
     145                        // keep the heartbeat interval within bounds 
     146                        if( _Settings.heartbeatInterval < 15 ) { 
     147                                _Settings.heartbeatInterval = 15; 
    89148                        } 
     149                        else if( _Settings.heartbeatInterval > 60 ) { 
     150                                _Settings.heartbeatInterval = 60; 
     151                        } 
    90152 
    91                         try { 
    92                                 if ( frame.contentWindow.document ) 
    93                                         return true; 
    94                         } catch(e) {} 
     153                        // make sure the interval is in milliseconds now 
     154                        _Settings.heartbeatInterval *= 1000; 
    95155 
    96                         return false; 
     156                        // setup the screenID now 
     157                        // screenId can be added from settings on the front-end where the JS global `pagenow` is not set 
     158                        _Settings.screenId = window.pagenow || ( settings.screenId || "" ); 
    97159                } 
    98160 
    99                 // Set error state and fire an event on XHR errors or timeout 
    100                 function errorstate( error ) { 
    101                         var trigger; 
     161                /** 
     162                 * Caches any objects we might be using during the lifetime of this module 
     163                 * 
     164                 * @private 
     165                 */ 
     166                function _cacheObjects() { 
     167                        _Cache.$document = $( document ); 
     168                        _Cache.$window = $( window ); 
     169                } 
    102170 
    103                         if ( error ) { 
    104                                 switch ( error ) { 
    105                                         case 'abort': 
    106                                                 // do nothing 
    107                                                 break; 
    108                                         case 'timeout': 
    109                                                 // no response for 30 sec. 
    110                                                 trigger = true; 
    111                                                 break; 
    112                                         case 'parsererror': 
    113                                         case 'error': 
    114                                         case 'empty': 
    115                                         case 'unknown': 
    116                                                 errorcount++; 
     171                /** 
     172                 * Gets the current unix timestamp 
     173                 * 
     174                 * @returns {number} 
     175                 */ 
     176                function _getUnixTimestamp() { 
     177                        return ( new Date() ).getTime(); 
     178                } 
    117179 
    118                                                 if ( errorcount > 2 ) 
    119                                                         trigger = true; 
     180                /** 
     181                 * Handles enqueuing data to be sent along with the next heartbeat pulse. We require a key to be set. Note that 
     182                 * a value is not required in the event that a user might want to dequeue a key from being passed along. 
     183                 * 
     184                 * As the data is sent later, this function doesn't return the XHR response. 
     185                 * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example: 
     186                 *              $(document).on( 'heartbeat-tick.myname', function( event, data, textStatus, jqXHR ) { 
     187                 *                      // code 
     188                 *              }); 
     189                 * If the same `key` is used more than once, the data is not overwritten when the third argument is `true`. 
     190                 * Use wp.heartbeat.isQueued( 'key' ) to see if any data is already queued for that key. 
     191                 * 
     192                 * @param key Unique string for the data. The handle is used in PHP to receive the data. 
     193                 * @param value The data to send. 
     194                 * @param overwriteExistingData Whether to overwrite existing data in the queue. 
     195                 * @returns {boolean} Whether the data was queued or not. 
     196                 * @private 
     197                 */ 
     198                function _enqueueData( key, value, overwriteExistingData ) { 
     199                        overwriteExistingData = ( typeof overwriteExistingData === "boolean" ) ? overwriteExistingData : false; 
    120200 
    121                                                 break; 
     201                        if( key !== undefined ) { 
     202                                if( _QueuedAjaxData.hasOwnProperty( key ) === true && overwriteExistingData === false ) { 
     203                                        return false; 
    122204                                } 
    123205 
    124                                 if ( trigger && ! self.connectionLost ) { 
    125                                         self.connectionLost = true; 
    126                                         $(document).trigger( 'heartbeat-connection-lost', [error] ); 
    127                                 } 
    128                         } else if ( self.connectionLost ) { 
    129                                 errorcount = 0; 
    130                                 self.connectionLost = false; 
    131                                 $(document).trigger( 'heartbeat-connection-restored' ); 
     206                                _QueuedAjaxData[ key ] = value; 
     207                                return true; 
    132208                        } 
     209                        return false; 
    133210                } 
    134211 
    135                 function connect() { 
    136                         var send = {}, data, i, empty = true, 
    137                         nonce = typeof window.heartbeatSettings == 'object' ? window.heartbeatSettings.nonce : ''; 
    138                         tick = time(); 
     212                /** 
     213                 * Check if data with a particular handle is queued. 
     214                 * 
     215                 * @param key The handle for the data 
     216                 * @returns {*} The data queued with that handle or null 
     217                 * @private 
     218                 */ 
     219                function _dataIsQueued( key ) { 
     220                        return _QueuedAjaxData[ key ]; 
     221                } 
    139222 
    140                         data = $.extend( {}, queue ); 
    141                         // Clear the data queue, anything added after this point will be send on the next tick 
    142                         queue = {}; 
     223                /** 
     224                 * If a value is specified, this function will set the internal value of the `shouldAutoStart` setting. Always 
     225                 * returns the value of `shouldAutoStart` 
     226                 * 
     227                 * @param value 
     228                 * @returns boolean 
     229                 * @private 
     230                 */ 
     231                function _shouldAutoStart( value ) { 
     232                        if( typeof value === "boolean" ) { 
     233                                return _Settings.shouldAutoStart = value; 
     234                        } 
    143235 
    144                         $(document).trigger( 'heartbeat-send', [data] ); 
     236                        return _Settings.shouldAutoStart; 
     237                } 
    145238 
    146                         for ( i in data ) { 
    147                                 if ( data.hasOwnProperty( i ) ) { 
    148                                         empty = false; 
    149                                         break; 
    150                                 } 
     239                /** 
     240                 * Determines if the iframe passed to this function is indeed an iframe and whether or not the src of that iframe 
     241                 * is local to us. The iframe will be classified as `local` if and only if it has either: 
     242                 *      1. the same domain name and protocol as the currently open window 
     243                 *      2. the document object can be accessed on the frame which means that our browser recognizes the iframe src 
     244                 *              as one of our own. 
     245                 * 
     246                 * @param iframe 
     247                 * @returns {boolean} 
     248                 * @private 
     249                 */ 
     250                function _isLocalFrame( iframe ) { 
     251                        if( iframe.nodeName !== "IFRAME" ) { 
     252                                return false; 
    151253                        } 
    152254 
    153                         // If nothing to send (nothing is expecting a response), 
    154                         // schedule the next tick and bail 
    155                         if ( empty && ! self.connectionLost ) { 
    156                                 connecting = false; 
    157                                 next(); 
    158                                 return; 
     255                        var origin = window.location.origin ? window.location.origin : window.location.protocol + "//" + window.location.host; 
     256                        var src = iframe.getAttribute( "src" ); 
     257 
     258                        if( /^https?:\/\//.test( src ) === true && src.indexOf( origin ) !== 0 ) { 
     259                                return false; 
    159260                        } 
    160261 
    161                         send.data = data; 
    162                         send.interval = interval / 1000; 
    163                         send._nonce = nonce; 
    164                         send.action = 'heartbeat'; 
    165                         send.screen_id = screenId; 
    166                         send.has_focus = hasFocus; 
     262                        if( iframe.contentWindow !== undefined && iframe.contentWindow.document !== undefined ) { 
     263                                return true; 
     264                        } 
    167265 
    168                         connecting = true; 
    169                         self.xhr = $.ajax({ 
    170                                 url: url, 
    171                                 type: 'post', 
    172                                 timeout: 30000, // throw an error if not completed after 30 sec. 
    173                                 data: send, 
    174                                 dataType: 'json' 
    175                         }).done( function( response, textStatus, jqXHR ) { 
    176                                 var new_interval; 
     266                        return false; 
     267                } 
    177268 
    178                                 if ( ! response ) 
    179                                         return errorstate( 'empty' ); 
     269                /** 
     270                 * If a value is specified, this function will set the internal value of the `hasActiveConnection` setting. 
     271                 * Always returns the value of `hasActiveConnection` 
     272                 * 
     273                 * @param value (optional) 
     274                 * @returns boolean 
     275                 * @private 
     276                 */ 
     277                function _hasActiveConnection( value ) { 
     278                        if( typeof value === "boolean" ) { 
     279                                _Settings.hasActiveConnection = value; 
     280                                return value; 
     281                        } 
    180282 
    181                                 // Clear error state 
    182                                 if ( self.connectionLost ) 
    183                                         errorstate(); 
     283                        return _Settings.hasActiveConnection; 
     284                } 
    184285 
    185                                 if ( response.nonces_expired ) { 
    186                                         $(document).trigger( 'heartbeat-nonces-expired' ); 
    187                                         return; 
    188                                 } 
     286                /** 
     287                 * Checks the error passed to this function and takes the appropriate action necessary. This function mainly 
     288                 * catches the transitions from an active connection to that of a non-active connection. 
     289                 * 
     290                 * @param error 
     291                 * @private 
     292                 */ 
     293                function _triggerError( error ) { 
     294                        var trigger = false; 
     295                        error = error || false; 
    189296 
    190                                 // Change the interval from PHP 
    191                                 if ( response.heartbeat_interval ) { 
    192                                         new_interval = response.heartbeat_interval; 
    193                                         delete response.heartbeat_interval; 
     297                        // if the error was aborted, don't do anything 
     298                        if( error === "abort" ) { 
     299                                return; 
     300                        } 
     301                        else if( error === "timeout" ) { 
     302                                trigger = true; 
     303                        } 
     304                        else if( error === "unknown" || error === "parsererror" || error === "error" || error === "empty" ) { 
     305                                _Settings.errorCountSinceConnected++; 
     306                                if( _Settings.errorCountSinceConnected > 2 ) { 
     307                                        trigger = true; 
    194308                                } 
     309                        } 
    195310 
    196                                 self.tick( response, textStatus, jqXHR ); 
     311                        // take action if we need to trigger things 
     312                        if( trigger === true && _hasActiveConnection() === true ) { 
     313                                _hasActiveConnection( false ); 
     314                                _Cache.$document.trigger( "heartbeat-connection-lost", [ error ] ); 
     315                        } 
     316                        else if( _hasActiveConnection() === false ) { 
     317                                _Settings.errorCountSinceConnected = 0; 
     318                                _hasActiveConnection( true ); 
     319                                _Cache.$document.trigger( "heartbeat-connection-restored" ); 
     320                        } 
     321                } 
    197322 
    198                                 // do this last, can trigger the next XHR if connection time > 5 sec. and new_interval == 'fast' 
    199                                 if ( new_interval ) 
    200                                         self.interval.call( self, new_interval ); 
    201                         }).always( function() { 
    202                                 connecting = false; 
    203                                 next(); 
    204                         }).fail( function( jqXHR, textStatus, error ) { 
    205                                 errorstate( textStatus || 'unknown' ); 
    206                                 self.error( jqXHR, textStatus, error ); 
    207                         }); 
    208                 }; 
     323                /** 
     324                 * Handles connecting to the server-side heartbeat API and sending any queued data along. 
     325                 * 
     326                 * @private 
     327                 */ 
     328                function _connect() { 
     329                        // get the data we need to send 
     330                        var dataToSend = $.extend( {}, _QueuedAjaxData ); 
     331                        _clearAjaxDataQueue(); 
    209332 
    210                 function next() { 
    211                         var delta = time() - tick, t = interval; 
     333                        // trigger an event with the data we're sending 
     334                        _Cache.$document.trigger( "heartbeat-send", [ dataToSend ] ); 
    212335 
    213                         if ( ! running ) 
     336                        // make sure there is data to send and that we have an active connection. If this criteria is met, bail from 
     337                        // this function 
     338                        if( _objectIsEmpty( dataToSend ) === true && _hasActiveConnection() === true ) { 
     339                                _Settings.isConnecting = false; 
     340                                _nextTick(); 
    214341                                return; 
     342                        } 
    215343 
    216                         if ( ! hasFocus ) { 
    217                                 t = 120000; // 2 min 
    218                         } else if ( countdown > 0 && tempInterval ) { 
    219                                 t = tempInterval; 
    220                                 countdown--; 
     344                        // build the ajax data we need to send now 
     345                        var ajaxData = _buildAjaxData( dataToSend ); 
     346 
     347                        // keep track of when we're connecting for this ajax call 
     348                        _Settings.isConnecting = true; 
     349 
     350                        // initiate a new ajax request 
     351                        _Cache.ajaxRequest = $.ajax( { 
     352                                url : _Settings.ajaxURL, 
     353                                type : "post", 
     354                                timeout : _Settings.ajaxRequestTimeout, 
     355                                data : ajaxData, 
     356                                dataType : "json" 
     357                        } ); 
     358 
     359                        // setup our promises 
     360                        _Cache.ajaxRequest.done( _onAjaxDone ); 
     361                        _Cache.ajaxRequest.always( _onAjaxAlwaysPromise ); 
     362                        _Cache.ajaxRequest.fail( _onAjaxFailed ); 
     363                } 
     364 
     365                /** 
     366                 * Handles taking action when the heartbeat pulse AJAX call is finished, whether fail or succeed 
     367                 * 
     368                 * @param response 
     369                 * @param textStatus 
     370                 * @param jqXHR 
     371                 * @private 
     372                 */ 
     373                function _onAjaxDone( response, textStatus, jqXHR  ) { 
     374                        var cachedHeartbeatInterval = null; 
     375 
     376                        // make sure we got a response 
     377                        if( ! response ) { 
     378                                _triggerError( "empty" ); 
     379                                return; 
    221380                        } 
    222381 
    223                         window.clearTimeout(beat); 
     382                        // clear the error state if the connection was lost already 
     383                        if( _Settings.hasActiveConnection() === false ) { 
     384                                _triggerError(); 
     385                        } 
    224386 
    225                         if ( delta < t ) { 
    226                                 beat = window.setTimeout( 
    227                                         function(){ 
    228                                                 if ( running ) 
    229                                                         connect(); 
    230                                         }, 
    231                                         t - delta 
    232                                 ); 
    233                         } else { 
    234                                 connect(); 
     387                        // check to see if the nonce has expired 
     388                        // todo: this could use a strict check 
     389                        if( response.nonces_expired ) { 
     390                                _Cache.$document.trigger( "heartbeat-nonces-expired" ); 
    235391                        } 
     392 
     393                        // check to see if a new interval needs to be sent 
     394                        // todo: this could use a strict check 
     395                        if( response.heartbeat_interval ) { 
     396                                cachedHeartbeatInterval = response.heartbeat_interval; 
     397 
     398                                // todo: what's the purpose of deleting this? 
     399                                delete response.heartbeat_interval; 
     400                        } 
     401 
     402                        // emit an event with the data we need 
     403                        _Cache.$document.trigger( "heartbeat-tick", [ response, textStatus, jqXHR ] ); 
     404 
     405                        // check to see if we set a value 
     406                        // do this last, can trigger the next XHR if connection time > 5 sec. and new_interval == 'fast' 
     407                        if( cachedHeartbeatInterval !== null ) { 
     408                                _interval( cachedHeartbeatInterval ); 
     409                        } 
    236410                } 
    237411 
    238                 function blurred() { 
    239                         window.clearTimeout(winBlurTimeout); 
    240                         window.clearTimeout(frameBlurTimeout); 
    241                         winBlurTimeout = frameBlurTimeout = 0; 
     412                /** 
     413                 * Handles taking action when the heartbeat pulse is completed and has failed 
     414                 * 
     415                 * @param jqXHR 
     416                 * @param textStatus 
     417                 * @param error 
     418                 * @private 
     419                 */ 
     420                function _onAjaxFailed( jqXHR, textStatus, error ) { 
     421                        _triggerError( textStatus || "unknown" ); 
     422                        _Cache.$document.trigger( "heartbeat-error", [ jqXHR, textStatus, error ] ); 
     423                } 
    242424 
    243                         hasFocus = false; 
     425                /** 
     426                 * Handles any actions to take for the always part of the Ajax promise 
     427                 * 
     428                 * @private 
     429                 */ 
     430                function _onAjaxAlwaysPromise() { 
     431                        _Settings.isConnecting = false; 
     432                        _nextTick(); 
    244433                } 
    245434 
    246                 function focused() { 
    247                         window.clearTimeout(winBlurTimeout); 
    248                         window.clearTimeout(frameBlurTimeout); 
    249                         winBlurTimeout = frameBlurTimeout = 0; 
     435                /** 
     436                 * Builds the data object literal that we'll use for initiating an AJAX request for each heartbeat pulse 
     437                 * 
     438                 * @param queuedData 
     439                 * @returns {{}} 
     440                 * @private 
     441                 */ 
     442                function _buildAjaxData( queuedData ) { 
     443                        return { 
     444                                data : queuedData, 
     445                                interval : ( _Settings.heartbeatInterval / 1000 ), 
     446                                _nonce : _Settings.nonce, 
     447                                action : "heartbeat", 
     448                                screen_id : _Settings.screenId, 
     449                                has_focus : _Settings.hasFocus 
     450                        }; 
     451                } 
    250452 
    251                         isUserActive = time(); 
     453                /** 
     454                 * Checks to see if the object literal passed to this function is currently empty or not. 
     455                 * 
     456                 * @param object 
     457                 * @returns {boolean} 
     458                 * @private 
     459                 */ 
     460                function _objectIsEmpty( object ) { 
     461                        for( var key in object ) { 
     462                                if( object.hasOwnProperty( key ) ) { 
     463                                        return false; 
     464                                } 
     465                        } 
    252466 
    253                         if ( hasFocus ) 
    254                                 return; 
     467                        return true; 
     468                } 
    255469 
    256                         hasFocus = true; 
    257                         window.clearTimeout(beat); 
     470                /** 
     471                 * Handles clearing the current AJAX data queue. This was moved to a function for clarity moving forward along 
     472                 * with the idea that we could possibly trigger events here or do additional things. 
     473                 * 
     474                 * @private 
     475                 */ 
     476                function _clearAjaxDataQueue() { 
     477                        _QueuedAjaxData = {}; 
     478                } 
    258479 
    259                         if ( ! connecting ) 
    260                                 next(); 
     480                /** 
     481                 * Handles binding any events 
     482                 * @private 
     483                 */ 
     484                function _bindWindowEvents() { 
     485                        _Cache.$window.on( "blur.wp-heartbeat-focus", _onWindowBlur ); 
     486                        _Cache.$window.on( "focus.wp-heartbeat-focus", _onWindowFocus ); 
    261487                } 
    262488 
    263                 function setFrameEvents() { 
    264                         $('iframe').each( function( i, frame ){ 
    265                                 if ( ! isLocalFrame( frame ) ) 
     489                /** 
     490                 * Handles performing anything we expect to happen when the window is blurred 
     491                 * 
     492                 * @param event 
     493                 * @private 
     494                 */ 
     495                function _onWindowBlur( event ) { 
     496                        // bind our iframe events 
     497                        _bindIframeEvents(); 
     498 
     499                        // reset our window blur timer 
     500                        _clearWindowBlurTimer(); 
     501 
     502                        // todo: not really sure why this was 500 in the first place, seems hacky? (there wasn't any commenting as to why) 
     503                        _Cache.windowBlurTimer = setTimeout( _userHasBecomeInactive, 500 ); 
     504                } 
     505 
     506                /** 
     507                 * Handles performing anything we expect to happen when the window is focused 
     508                 * 
     509                 * @param event 
     510                 * @private 
     511                 */ 
     512                function _onWindowFocus( event ) { 
     513                        _unbindIframeEvents(); 
     514                        _userHasBecomeActive(); 
     515                } 
     516 
     517                /** 
     518                 * Handles binding any iframe events that are needed for this API to work effectively 
     519                 * 
     520                 * @private 
     521                 */ 
     522                function _bindIframeEvents() { 
     523                        $( document.querySelectorAll( "iframe" ) ).each( function( i, iframe ) { 
     524                                if( _isLocalFrame( iframe ) === false ) { 
    266525                                        return; 
     526                                } 
    267527 
    268                                 if ( $.data( frame, 'wp-heartbeat-focus' ) ) 
     528                                // if we already set data for this iframe, skip it 
     529                                if( $.data( iframe, "wp-heartbeat-focus" ) ) { 
    269530                                        return; 
     531                                } 
    270532 
    271                                 $.data( frame, 'wp-heartbeat-focus', 1 ); 
     533                                // set data for this frame now so we know not to set it again next time this is called 
     534                                $.data( iframe, "wp-heartbeat-focus", true ); 
    272535 
    273                                 $( frame.contentWindow ).on( 'focus.wp-heartbeat-focus', function(e) { 
    274                                         focused(); 
    275                                 }).on('blur.wp-heartbeat-focus', function(e) { 
    276                                         setFrameEvents(); 
    277                                         frameBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 ); 
    278                                 }); 
    279                         }); 
     536                                // cache a reference to the jquery object for this iframe window object 
     537                                var $iframeWindow = $( iframe.contentWindow ); 
     538 
     539                                // now setup a focus event for this iframe 
     540                                $iframeWindow.on( "focus.wp-heartbeat-focus", _userHasBecomeActive ); 
     541                                $iframeWindow.on( "blur.wp-heartbeat-focus", _onIframeBlur ); 
     542 
     543                        } ); 
    280544                } 
    281545 
    282                 $(window).on( 'blur.wp-heartbeat-focus', function(e) { 
    283                         setFrameEvents(); 
    284                         winBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 ); 
    285                 }).on( 'focus.wp-heartbeat-focus', function() { 
    286                         $('iframe').each( function( i, frame ) { 
    287                                 if ( !isLocalFrame( frame ) ) 
     546                /** 
     547                 * Callback for when an iframe becomes blurred on the page 
     548                 * 
     549                 * @param event 
     550                 * @private 
     551                 */ 
     552                function _onIframeBlur( event ) { 
     553                        _bindIframeEvents(); 
     554 
     555                        //clear the existing frame blur timer 
     556                        _clearFrameBlurTimer(); 
     557 
     558                        // todo: not really sure why this was 500 in the first place, seems hacky? (there wasn't any commenting as to why) 
     559                        _Cache.frameBlurTimer = setTimeout( _userHasBecomeInactive, 500 ); 
     560                } 
     561 
     562                /** 
     563                 * Unbinds any previously bound heartbeat focus events from all iframes on the page (if they belong to us) 
     564                 * 
     565                 * @private 
     566                 */ 
     567                function _unbindIframeEvents() { 
     568                        $( document.querySelectorAll( "iframe" ) ).each( function( i, iframe ) { 
     569                                if( _isLocalFrame( iframe ) === false ) { 
    288570                                        return; 
     571                                } 
    289572 
    290                                 $.removeData( frame, 'wp-heartbeat-focus' ); 
    291                                 $( frame.contentWindow ).off( '.wp-heartbeat-focus' ); 
    292                         }); 
     573                                $.removeData( iframe, "wp-heartbeat-focus" ); 
     574                                $( iframe.contentWindow ).off( ".wp-heartbeat-focus" ); 
     575                        } ); 
     576                } 
    293577 
    294                         focused(); 
    295                 }); 
     578                /** 
     579                 * Performs the next tick of the timer for our heartbeat. Verifies that we're supposed to continue before trying 
     580                 * to start the next tick. 
     581                 * 
     582                 * @private 
     583                 */ 
     584                function _nextTick() { 
     585                        var timeSinceLastTick = _getUnixTimestamp() - _Settings.lastHeartbeatTick; 
     586                        var timeLimit = _Settings.heartbeatInterval; 
    296587 
    297                 function userIsActive() { 
    298                         userActiveEvents = false; 
    299                         $(document).off( '.wp-heartbeat-active' ); 
    300                         $('iframe').each( function( i, frame ) { 
    301                                 if ( ! isLocalFrame( frame ) ) 
     588                        // make sure we're running before doing anything 
     589                        if( _Settings.isRunning === false ) { 
     590                                return; 
     591                        } 
     592 
     593                        // normalize the timeLimit 
     594                        if( _hasFocus() === false ) { 
     595                                // set the time limit to 2 minutes because we don't have focus 
     596                                timeLimit = 120000; 
     597                        } 
     598                        else if( _Settings.countDown > 0 && _Settings.temporaryHeartbeatInterval > 0 ) { 
     599                                timeLimit = _Settings.temporaryHeartbeatInterval; 
     600                        } 
     601 
     602                        // clear the existing heartbeat timer 
     603                        _clearHeartbeatTimer(); 
     604 
     605                        // check to see if we need to connect now or later and then set things up to do just that 
     606                        if( timeSinceLastTick < timeLimit ) { 
     607                                _Cache.heartbeatTimer = setTimeout( _connect, timeLimit - timeSinceLastTick ); 
     608                        } 
     609                        else { 
     610                                _connect(); 
     611                        } 
     612                } 
     613 
     614                /** 
     615                 * Handles unbinding any events that were previously bound through `_bindUserActivityEvents()` 
     616                 * 
     617                 * @private 
     618                 */ 
     619                function _unbindUserActivityEvents() { 
     620                        // remove the mouseover event for the document 
     621                        _Cache.$document.off( ".wp-heartbeat-active" ); 
     622 
     623                        // loop through all iframes on the page and if they are local iframes, remove the previously attached events 
     624                        $( document.querySelectorAll( "iframe" ) ).each( function( i, iframe ) { 
     625 
     626                                // make sure this frame is one of ours before trying to access it's contentWindow 
     627                                if( _isLocalFrame( iframe ) === false ) { 
    302628                                        return; 
     629                                } 
    303630 
    304                                 $( frame.contentWindow ).off( '.wp-heartbeat-active' ); 
    305                         }); 
     631                                // bind a mouseover event to this iframe's window object 
     632                                $( iframe.contentWindow ).off( ".wp-heartbeat-active" ); 
    306633 
    307                         focused(); 
     634                        } ); 
    308635                } 
    309636 
    310                 // Set 'hasFocus = true' if user is active and the window is in the background. 
    311                 // Set 'hasFocus = false' if the user has been inactive (no mouse or keyboard activity) for 5 min. even when the window has focus. 
    312                 function checkUserActive() { 
    313                         var lastActive = isUserActive ? time() - isUserActive : 0; 
     637                /** 
     638                 * Handles binding any events necessary to mark the user as active again 
     639                 * 
     640                 * @private 
     641                 */ 
     642                function _bindUserActivityEvents() { 
     643                        // when the user moves their mouse, mark the the user as becoming active again 
     644                        _Cache.$document.on( "mouseover.wp-heartbeat-active keyup.wp-heartbeat-active", _userHasBecomeActive ); 
    314645 
    315                         // Throttle down when no mouse or keyboard activity for 5 min 
    316                         if ( lastActive > 300000 && hasFocus ) 
    317                                  blurred(); 
     646                        // loop through all iframes on the page and if they are local iframes, 
     647                        $( document.querySelectorAll( "iframe" ) ).each( function( i, iframe ) { 
    318648 
    319                         if ( ! userActiveEvents ) { 
    320                                 $(document).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } ); 
     649                                // make sure this frame is one of ours before trying to access it's contentWindow 
     650                                if( _isLocalFrame( iframe ) === false ) { 
     651                                        return; 
     652                                } 
    321653 
    322                                 $('iframe').each( function( i, frame ) { 
    323                                         if ( ! isLocalFrame( frame ) ) 
    324                                                 return; 
     654                                // bind a mouseover event to this iframe's window object 
     655                                $( iframe.contentWindow ).on( "mouseover.wp-heartbeat-active keyup.wp-heartbeat-active", _userHasBecomeActive ); 
    325656 
    326                                         $( frame.contentWindow ).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } ); 
    327                                 }); 
     657                        } ); 
     658                } 
    328659 
    329                                 userActiveEvents = true; 
     660                /** 
     661                 * Periodically called to ensure that the user is still active. If the user becomes inactive, we take action. 
     662                 * 
     663                 * @private 
     664                 */ 
     665                function _checkIfUserIsStillActive() { 
     666                        var timeSinceUserWasLastActive = 0; 
     667                        if( _Settings.userIsActive === true ) { 
     668                                timeSinceUserWasLastActive = _getUnixTimestamp() - _Settings.lastUserActivityTimestamp; 
    330669                        } 
     670 
     671                        // check to see if the user has become inactive or not ( no activity in 5 minutes ) 
     672                        if( timeSinceUserWasLastActive > 300000 && _hasFocus() === true ) { 
     673                                _userHasBecomeInactive(); 
     674                                _bindUserActivityEvents(); 
     675                        } 
    331676                } 
    332677 
    333                 // Check for user activity every 30 seconds. 
    334                 window.setInterval( function(){ checkUserActive(); }, 30000 ); 
     678                /** 
     679                 * Starts the heartbeat pulse if it isn't already running 
     680                 * todo: maybe add a trigger() here for other applications that need to be informed of when heartbeat starts 
     681                 * 
     682                 * @returns {boolean} 
     683                 * @private 
     684                 */ 
     685                function _start() { 
     686                        if( _Settings.isRunning === true ) { 
     687                                return false; 
     688                        } 
    335689 
    336                 if ( this.autostart ) { 
    337                         $(document).ready( function() { 
    338                                 // Start one tick (15 sec) after DOM ready 
    339                                 running = true; 
    340                                 tick = time(); 
    341                                 next(); 
    342                         }); 
     690                        _connect(); 
     691 
     692                        _Settings.isRunning = true; 
     693                        return true; 
    343694                } 
    344695 
    345                 this.hasFocus = function() { 
    346                         return hasFocus; 
     696                /** 
     697                 * Stops the heartbeat pulse if isn't already running 
     698                 * todo: should this be clearing timers as well? ( _clearWindowBlurTimer(), _clearFrameBlurTimer() and _clearHeartbeatTimer() ) 
     699                 * 
     700                 * @returns {boolean} 
     701                 * @private 
     702                 */ 
     703                function _stop() { 
     704                        if( _Cache.ajaxRequest !== null && _Cache.ajaxRequest.readyState !== 4 ) { 
     705                                _Cache.ajaxRequest.abort(); 
     706                                _Cache.ajaxRequest = null; 
     707                        } 
     708 
     709                        // fire an error 
     710                        // todo: this wasn't passing anything previously. I feel like it should be passing 'abort' 
     711                        _triggerError(); 
     712 
     713                        // update our internal value for whether or not we are currently running heartbeat 
     714                        _Settings.isRunning = false; 
     715 
     716                        return true; 
    347717                } 
    348718 
    349719                /** 
    350                  * Get/Set the interval 
     720                 * Gets or sets the current heartbeat interval based on speed and ticks passed to it. If no speed is passed, the 
     721                 * function simply returns the current interval. 
    351722                 * 
    352723                 * When setting to 'fast', the interval is 5 sec. for the next 30 ticks (for 2 min and 30 sec). 
    353724                 * If the window doesn't have focus, the interval slows down to 2 min. 
    354725                 * 
    355                  * @param string speed Interval speed: 'fast' (5sec), 'standard' (15sec) default, 'slow' (60sec) 
    356                  * @param string ticks Used with speed = 'fast', how many ticks before the speed reverts back 
    357                  * @return int Current interval in seconds 
     726                 * @param speed Interval speed: 'fast' (5sec), 'standard' (15sec) default, 'slow' (60sec) 
     727                 * @param ticks Used with speed = 'fast', how many ticks before the speed reverts back 
     728                 * @returns {number} interval in seconds 
     729                 * @private 
    358730                 */ 
    359                 this.interval = function( speed, ticks ) { 
    360                         var reset, seconds; 
    361                         ticks = parseInt( ticks, 10 ) || 30; 
    362                         ticks = ticks < 1 || ticks > 30 ? 30 : ticks; 
     731                function _interval( speed, ticks ) { 
     732                        var reset, seconds = 15; 
    363733 
    364                         if ( speed ) { 
    365                                 switch ( speed ) { 
    366                                         case 'fast': 
    367                                                 seconds = 5; 
    368                                                 countdown = ticks; 
    369                                                 break; 
    370                                         case 'slow': 
    371                                                 seconds = 60; 
    372                                                 countdown = 0; 
    373                                                 break; 
    374                                         case 'long-polling': 
    375                                                 // Allow long polling, (experimental) 
    376                                                 interval = 0; 
    377                                                 return 0; 
    378                                                 break; 
    379                                         default: 
    380                                                 seconds = 15; 
    381                                                 countdown = 0; 
     734                        if( speed === undefined ) { 
     735                                if( _hasFocus() === false ) { 
     736                                        return 120; 
    382737                                } 
    383738 
    384                                 // Reset when the new interval value is lower than the current one 
    385                                 reset = seconds * 1000 < interval; 
     739                                // return the existing values 
     740                                return _Settings.temporaryHeartbeatInterval ? ( _Settings.temporaryHeartbeatInterval / 1000 ) : ( _Settings.heartbeatInterval * 1000 ); 
     741                        } 
    386742 
    387                                 if ( countdown > 0 ) { 
    388                                         tempInterval = seconds * 1000; 
    389                                 } else { 
    390                                         interval = seconds * 1000; 
    391                                         tempInterval = 0; 
    392                                 } 
     743                        // normalize ticks before continuing 
     744                        ticks = parseInt( ticks, 10 ) || 30; 
    393745 
    394                                 if ( reset ) 
    395                                         next(); 
     746                        // make sure `ticks` is within bounds 
     747                        if( ticks < 1 || ticks > 30 ) { 
     748                                ticks = 30; 
    396749                        } 
    397750 
    398                         if ( ! hasFocus ) 
     751                        // convert speed from a string if necessary 
     752                        if( speed === "fast" ) { 
     753                                seconds = 5; 
     754                                _Settings.countDown = ticks; 
     755                        } 
     756                        else if( speed === "slow" ) { 
     757                                seconds = 60; 
     758                                _Settings.countDown = 0; 
     759                        } 
     760                        // long-polling is currently experimental 
     761                        else if( speed === "long-polling" ) { 
     762                                return _Settings.heartbeatInterval = 0; 
     763                        } 
     764 
     765                        // determine whether or not we should reset based on whether the new interval value is lower than the current 
     766                        // one or not 
     767                        reset = seconds * 1000 < _Settings.heartbeatInterval; 
     768 
     769                        if( _Settings.countDown > 0 ) { 
     770                                _Settings.temporaryHeartbeatInterval = seconds * 1000; 
     771                        } 
     772                        else { 
     773                                _Settings.heartbeatInterval = seconds * 1000; 
     774                                _Settings.temporaryHeartbeatInterval = 0; 
     775                        } 
     776 
     777                        // check if we need to reset or not 
     778                        if( reset === true ) { 
     779                                _nextTick(); 
     780                        } 
     781 
     782                        // check to see if we have focus or not 
     783                        if( _hasFocus() === false ) { 
    399784                                return 120; 
     785                        } 
    400786 
    401                         return tempInterval ? tempInterval / 1000 : interval / 1000; 
    402                 }; 
     787                        // return the new values 
     788                        return _Settings.temporaryHeartbeatInterval ? ( _Settings.temporaryHeartbeatInterval / 1000 ) : ( _Settings.heartbeatInterval * 1000 ); 
     789                } 
    403790 
    404                 // Start. Has no effect if heartbeat is already running 
    405                 this.start = function() { 
    406                         if ( running ) 
    407                                 return false; 
     791                /** 
     792                 * Returns a boolean that indicates whether or not heartbeat has focus at the moment 
     793                 * 
     794                 * @returns boolean 
     795                 * @private 
     796                 */ 
     797                function _hasFocus() { 
     798                        return _Settings.hasFocus; 
     799                } 
    408800 
    409                         running = true; 
    410                         connect(); 
    411                         return true; 
    412                 }; 
     801                /** 
     802                 * Handles the actions to take when the user has become inactive 
     803                 * todo: possibly add a $document.trigger() here to notify other applications? 
     804                 * 
     805                 * @private 
     806                 */ 
     807                function _userHasBecomeInactive() { 
     808                        _clearWindowBlurTimer(); 
     809                        _clearFrameBlurTimer(); 
    413810 
    414                 // Stop. If a XHR is in progress, abort it 
    415                 this.stop = function() { 
    416                         if ( self.xhr && self.xhr.readyState != 4 ) 
    417                                 self.xhr.abort(); 
     811                        // set the value of hasFocus to false 
     812                        _Settings.hasFocus = false; 
     813                } 
    418814 
    419                         // Reset the error state 
    420                         errorstate(); 
    421                         running = false; 
    422                         return true; 
     815                /** 
     816                 * Handles the actions to take when the heartbeat window becomes focused. 
     817                 * todo: possibly add a $document.trigger() here to notify other applications? 
     818                 * 
     819                 * @private 
     820                 */ 
     821                function _userHasBecomeActive() { 
     822                        _clearWindowBlurTimer(); 
     823                        _clearFrameBlurTimer(); 
     824                        _unbindUserActivityEvents(); 
     825                        _updateUsersLastActivityTime(); 
     826 
     827                        if( _hasFocus() === true ) { 
     828                                return; 
     829                        } 
     830 
     831                        // set the the focus to true and clear the heartbeat pulse 
     832                        _Settings.hasFocus = true; 
     833                        _clearHeartbeatTimer(); 
     834 
     835                        if( _Settings.isConnecting === false ) { 
     836                                _nextTick(); 
     837                        } 
    423838                } 
    424839 
    425840                /** 
    426                  * Enqueue data to send with the next XHR 
     841                 * Updates the user's last activity timestamp 
     842                 * todo: perhaps trigger an event to notify other applications? 
    427843                 * 
    428                  * As the data is sent later, this function doesn't return the XHR response. 
    429                  * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example: 
    430                  *              $(document).on( 'heartbeat-tick.myname', function( event, data, textStatus, jqXHR ) { 
    431                  *                      // code 
    432                  *              }); 
    433                  * If the same 'handle' is used more than once, the data is not overwritten when the third argument is 'true'. 
    434                  * Use wp.heartbeat.isQueued('handle') to see if any data is already queued for that handle. 
     844                 * @private 
     845                 */ 
     846                function _updateUsersLastActivityTime() { 
     847                        _Settings.lastUserActivityTimestamp = _getUnixTimestamp(); 
     848                } 
     849 
     850                /** 
     851                 * Clears the window blur timer if it exists 
    435852                 * 
    436                  * $param string handle Unique handle for the data. The handle is used in PHP to receive the data. 
    437                  * $param mixed data The data to send. 
    438                  * $param bool dont_overwrite Whether to overwrite existing data in the queue. 
    439                  * $return bool Whether the data was queued or not. 
     853                 * @private 
    440854                 */ 
    441                 this.enqueue = function( handle, data, dont_overwrite ) { 
    442                         if ( handle ) { 
    443                                 if ( queue.hasOwnProperty( handle ) && dont_overwrite ) 
    444                                         return false; 
     855                function _clearWindowBlurTimer() { 
     856                        if( _Cache.windowBlurTimer !== null ) { 
     857                                clearTimeout( _Cache.windowBlurTimer ); 
     858                                _Cache.windowBlurTimer = null; 
     859                        } 
     860                } 
    445861 
    446                                 queue[handle] = data; 
    447                                 return true; 
     862                /** 
     863                 * Clears the frame blur timer if it exists 
     864                 * 
     865                 * @private 
     866                 */ 
     867                function _clearFrameBlurTimer() { 
     868                        if( _Cache.frameBlurTimer !== null ) { 
     869                                clearTimeout( _Cache.frameBlurTimer ); 
     870                                _Cache.frameBlurTimer = null; 
    448871                        } 
    449                         return false; 
    450872                } 
    451873 
    452874                /** 
    453                  * Check if data with a particular handle is queued 
     875                 * Clears the heartbeat timer 
    454876                 * 
    455                  * $param string handle The handle for the data 
    456                  * $return mixed The data queued with that handle or null 
     877                 * @private 
    457878                 */ 
    458                 this.isQueued = function( handle ) { 
    459                         return queue[handle]; 
     879                function _clearHeartbeatTimer() { 
     880                        if( _Cache.heartbeatTimer !== null ) { 
     881                                clearTimeout( _Cache.heartbeatTimer ); 
     882                                _Cache.heartbeatTimer = null; 
     883                        } 
    460884                } 
     885 
     886                /** 
     887                 * Call our object initializer to make sure things get setup properly before this object is used 
     888                 */ 
     889                _initialize(); 
     890 
     891                /** 
     892                 * Explicitly expose any methods we want to be available to the public scope 
     893                 */ 
     894                return { 
     895                        shouldAutoStart : _shouldAutoStart, 
     896                        hasActiveConnection : _hasActiveConnection, 
     897                        isLocalFrame : _isLocalFrame, 
     898                        enqueueData : _enqueueData, 
     899                        dataIsQueued : _dataIsQueued, 
     900                        start : _start, 
     901                        stop : _stop, 
     902                        interval : _interval, 
     903                        hasFocus : _hasFocus 
     904                }; 
    461905        } 
    462906 
    463         $.extend( Heartbeat.prototype, { 
    464                 tick: function( data, textStatus, jqXHR ) { 
    465                         $(document).trigger( 'heartbeat-tick', [data, textStatus, jqXHR] ); 
    466                 }, 
    467                 error: function( jqXHR, textStatus, error ) { 
    468                         $(document).trigger( 'heartbeat-error', [jqXHR, textStatus, error] ); 
    469                 } 
    470         }); 
     907        // ensure our global `wp` object exists 
     908        window.wp = window.wp || {}; 
    471909 
    472         wp.heartbeat = new Heartbeat(); 
     910        // create our new heartbeat object and expose it to the public 
     911        window.wp.heartbeat = new Heartbeat(); 
    473912 
    474 }(jQuery)); 
     913} )( window, jQuery ); 
     914 No newline at end of file