Make WordPress Core

Ticket #23216: 23216-heartbeat-cleanup.diff

File 23216-heartbeat-cleanup.diff, 38.6 KB (added by carldanley, 11 years 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