Make WordPress Core

Changeset 23382


Ignore:
Timestamp:
02/03/2013 07:03:27 AM (12 years ago)
Author:
azaozz
Message:

Heartbeat API: throttle down when the window looses focus or when the user is inactive, always send 'screen_id', change the interval settings to 'fast' (5sec), 'standard' (15sec) and 'slow' (60sec), the interval can be changed from PHP, see #23216

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/wp-admin/includes/ajax-actions.php

    r23355 r23382  
    20752075function wp_ajax_heartbeat() {
    20762076    check_ajax_referer( 'heartbeat-nonce', '_nonce' );
    2077     $response = array( 'pagenow' => '' );
    2078 
    2079     if ( ! empty($_POST['pagenow']) )
    2080         $response['pagenow'] = sanitize_key($_POST['pagenow']);
     2077    $response = array();
     2078
     2079    // screenid is the same as $current_screen->id and the JS global 'pagenow'
     2080    if ( ! empty($_POST['screenid']) )
     2081        $screen_id = sanitize_key($_POST['screenid']);
     2082    else
     2083        $screen_id = 'site';
    20812084   
    20822085    if ( ! empty($_POST['data']) ) {
     
    20882091        // todo: separate filters: 'heartbeat_[action]' so we call different callbacks only when there is data for them,
    20892092        // or all callbacks listen to one filter and run when there is something for them in $data?
    2090         $response = apply_filters( 'heartbeat_received', $response, $data );
    2091     }
    2092 
    2093     $response = apply_filters( 'heartbeat_send', $response );
     2093        $response = apply_filters( 'heartbeat_received', $response, $data, $screen_id );
     2094    }
     2095
     2096    $response = apply_filters( 'heartbeat_send', $response, $screen_id );
    20942097
    20952098    // Allow the transport to be replaced with long-polling easily
    2096     do_action( 'heartbeat_tick', $response );
    2097 
    2098     // always send the current time acording to the server
    2099     $response['time'] = time();
     2099    do_action( 'heartbeat_tick', $response, $screen_id );
     2100
     2101    // send the current time acording to the server
     2102    $response['servertime'] = time();
     2103
     2104    // Change the interval, format: array( speed, ticks )
     2105    if ( isset($response['heartbeat_interval']) )
     2106        $response['heartbeat_interval'] = (array) $response['heartbeat_interval'];
    21002107
    21012108    wp_send_json($response);
  • trunk/wp-includes/js/heartbeat.js

    r23355 r23382  
    1010        var self = this,
    1111            running,
    12             timeout,
     12            beat,
    1313            nonce,
    14             screen = typeof pagenow != 'undefined' ? pagenow : '',
     14            screenid = typeof pagenow != 'undefined' ? pagenow : '',
    1515            settings,
    1616            tick = 0,
    1717            queue = {},
    1818            interval,
    19             lastconnect = 0;
     19            lastconnect = 0,
     20            connecting,
     21            countdown,
     22            tempInterval,
     23            hasFocus = true,
     24            isUserActive,
     25            userActiveEvents,
     26            winBlurTimeout,
     27            frameBlurTimeout = -1;
    2028
    2129        this.url = typeof ajaxurl != 'undefined' ? ajaxurl : 'wp-admin/admin-ajax.php';
     
    3038            delete settings.nonce;
    3139
    32             interval = settings.interval || 15000; // default interval
     40            interval = settings.interval || 15; // default interval
    3341            delete settings.interval;
    34            
     42            // The interval can be from 5 to 60 sec.
     43            if ( interval < 5 )
     44                interval = 5;
     45            else if ( interval > 60 )
     46                interval = 60;
     47
     48            interval = interval * 1000;
     49
    3550            // todo: needed?
    36             // 'pagenow' can be added from settings if not already defined
    37             screen = screen || settings.pagenow;
    38             delete settings.pagenow;
    39 
    40             // Add public vars
     51            // 'screenid' can be added from settings on the front-end where the JS global 'pagenow' is not set
     52            screenid = screenid || settings.screenid || 'site';
     53            delete settings.screenid;
     54
     55            // Add or overwrite public vars
    4156            $.extend( this, settings );
    4257        }
     
    4964        }
    5065
    51         // Set error state and fire an event if it persists for over 3 min
     66        // Set error state and fire an event if errors persist for over 2 min when the window has focus
     67        // or 6 min when the window is in the background
    5268        function errorstate() {
    5369            var since;
    5470
    5571            if ( lastconnect ) {
    56                 since = time() - lastconnect;
    57 
    58                 if ( since > 180000 ) {
     72                since = time() - lastconnect, duration = hasFocus ? 120000 : 360000;
     73
     74                if ( since > duration ) {
    5975                    self.connectionLost = true;
    6076                    $(document).trigger( 'heartbeat-connection-lost', parseInt(since / 1000) );
     
    7187
    7288            data.data = $.extend( {}, queue );
    73             queue = {};
    7489
    7590            data.interval = interval / 1000;
    7691            data._nonce = nonce;
    7792            data.action = 'heartbeat';
    78             data.pagenow = screen;
    79 
    80             self.xhr = $.post( self.url, data, function(r){
     93            data.screenid = screenid;
     94            data.has_focus = hasFocus;
     95
     96            connecting = true;
     97            self.xhr = $.post( self.url, data, 'json' )
     98            .done( function( data, textStatus, jqXHR ) {
     99                var interval;
     100
     101                // Clear the data queue
     102                queue = {};
     103
     104                // Clear error state
    81105                lastconnect = time();
    82                 // Clear error state
    83106                if ( self.connectionLost )
    84107                    errorstate();
    85                
    86                 self.tick(r);
    87             }, 'json' ).always( function(){
     108
     109                // Change the interval from PHP
     110                interval = data.heartbeat_interval;
     111                delete data.heartbeat_interval;
     112
     113                self.tick( data, textStatus, jqXHR );
     114
     115                // do this last, can trigger the next XHR
     116                if ( interval )
     117                    self.interval.apply( self, data.heartbeat_interval );
     118            }).always( function(){
     119                connecting = false;
    88120                next();
    89             }).fail( function(r){
     121            }).fail( function( jqXHR, textStatus, error ){
    90122                errorstate();
    91                 self.error(r);
     123                self.error( jqXHR, textStatus, error );
    92124            });
    93125        };
    94126
    95127        function next() {
    96             var delta = time() - tick;
     128            var delta = time() - tick, t = interval;
    97129
    98130            if ( !running )
    99131                return;
    100132
    101             if ( delta < interval ) {
    102                 timeout = window.setTimeout(
     133            if ( !hasFocus ) {
     134                t = 120000; // 2 min
     135            } else if ( countdown && tempInterval ) {
     136                t = tempInterval;
     137                countdown--;
     138            }
     139
     140            window.clearTimeout(beat);
     141
     142            if ( delta < t ) {
     143                beat = window.setTimeout(
    103144                    function(){
    104145                        if ( running )
    105146                            connect();
    106147                    },
    107                     interval - delta
     148                    t - delta
    108149                );
    109150            } else {
    110                 window.clearTimeout(timeout); // this has already expired?
    111151                connect();
    112152            }
    113         };
    114 
    115         this.interval = function(seconds) {
    116             if ( seconds ) {
    117                 // Limit
    118                 if ( 5 > seconds || seconds > 60 )
    119                     return false;
    120 
    121                 interval = seconds * 1000;
    122             } else if ( seconds === 0 ) {
    123                 // Allow long polling to be turned on
    124                 interval = 0;
    125             }
    126             return interval / 1000;
    127         };
    128 
    129         this.start = function() {
    130             // start only once
    131             if ( running )
    132                 return false;
    133 
    134             running = true;
    135             connect();
    136 
    137             return true;
    138         };
    139 
    140         this.stop = function() {
    141             if ( !running )
    142                 return false;
    143 
    144             if ( self.xhr )
    145                 self.xhr.abort();
    146 
    147             running = false;
    148             return true;
    149         }
    150 
    151         this.send = function(action, data) {
    152             if ( action )
    153                 queue[action] = data;
    154         }
     153        }
     154
     155        function blurred() {
     156            window.clearTimeout(winBlurTimeout);
     157            window.clearTimeout(frameBlurTimeout);
     158            winBlurTimeout = frameBlurTimeout = 0;
     159
     160            hasFocus = false;
     161
     162            // temp debug
     163            if ( self.debug )
     164                console.log('### blurred(), slow down...')
     165        }
     166
     167        function focused() {
     168            window.clearTimeout(winBlurTimeout);
     169            window.clearTimeout(frameBlurTimeout);
     170            winBlurTimeout = frameBlurTimeout = 0;
     171
     172            isUserActive = time();
     173
     174            if ( hasFocus )
     175                return;
     176
     177            hasFocus = true;
     178            window.clearTimeout(beat);
     179
     180            if ( !connecting )
     181                next();
     182
     183            // temp debug
     184            if ( self.debug )
     185                console.log('### focused(), speed up... ')
     186        }
     187
     188        function setFrameEvents() {
     189            $('iframe').each( function(i, frame){
     190                if ( $.data(frame, 'wp-heartbeat-focus') )
     191                    return;
     192
     193                $.data(frame, 'wp-heartbeat-focus', 1);
     194
     195                $(frame.contentWindow).on('focus.wp-heartbeat-focus', function(e){
     196                    focused();
     197                }).on('blur.wp-heartbeat-focus', function(e){
     198                    setFrameEvents();
     199                    frameBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 );
     200                });
     201            });
     202        }
     203
     204        $(window).on('blur.wp-heartbeat-focus', function(e){
     205            setFrameEvents();
     206            winBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 );
     207        }).on('focus.wp-heartbeat-focus', function(){
     208            $('iframe').each( function(i, frame){
     209                $.removeData(frame, 'wp-heartbeat-focus');
     210                $(frame.contentWindow).off('.wp-heartbeat-focus');
     211            });
     212
     213            focused();
     214        });
     215
     216        function userIsActive() {
     217            userActiveEvents = false;
     218            $(document).off('.wp-heartbeat-active');
     219            $('iframe').each( function(i, frame){
     220                $(frame.contentWindow).off('.wp-heartbeat-active');
     221            });
     222
     223            focused();
     224
     225            // temp debug
     226            if ( self.debug )
     227                console.log( 'userIsActive()' );
     228        }
     229
     230        // Set 'hasFocus = true' if user is active and the window is in the background.
     231        // Set 'hasFocus = false' if the user has been inactive (no mouse or keyboard activity) for 5 min. even when the window has focus.
     232        function checkUserActive() {
     233            var lastActive = isUserActive ? time() - isUserActive : 0;
     234
     235            // temp debug
     236            if ( self.debug )
     237                console.log( 'checkUserActive(), lastActive = %s seconds ago', parseInt(lastActive / 1000) || 'null' );
     238
     239            // Throttle down when no mouse or keyboard activity for 5 min
     240            if ( lastActive > 300000 && hasFocus )
     241                 blurred();
     242
     243            if ( !userActiveEvents ) {
     244                $(document).on('mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); });
     245                $('iframe').each( function(i, frame){
     246                    $(frame.contentWindow).on('mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); });
     247                });
     248                userActiveEvents = true;
     249            }
     250        }
     251
     252        // Check for user activity every 30 seconds.
     253        window.setInterval( function(){ checkUserActive(); }, 30000 );
    155254
    156255        if ( this.autostart ) {
     
    162261            });
    163262        }
    164            
     263
     264        this.winHasFocus = function() {
     265            return hasFocus;
     266        }
     267
     268        /**
     269         * Get/Set the interval
     270         *
     271         * When setting the interval to 'fast', the number of ticks is specified wiht the second argument, default 30.
     272         * If the window doesn't have focus, the interval is overridden to 2 min. In this case setting the 'ticks'
     273         * will start counting after the window gets focus.
     274         *
     275         * @param string speed Interval speed: 'fast' (5sec), 'standard' (15sec) default, 'slow' (60sec)
     276         * @param int ticks Number of ticks for the changed interval, optional when setting 'standard' or 'slow'
     277         * @return int Current interval in seconds
     278         */
     279        this.interval = function(speed, ticks) {
     280            var reset, seconds;
     281
     282            if ( speed ) {
     283                switch ( speed ) {
     284                    case 'fast':
     285                        seconds = 5;
     286                        countdown = parseInt(ticks) || 30;
     287                        break;
     288                    case 'slow':
     289                        seconds = 60;
     290                        countdown = parseInt(ticks) || 0;
     291                        break;
     292                    case 'long-polling':
     293                        // Allow long polling, (experimental)
     294                        interval = 0;
     295                        return 0;
     296                        break;
     297                    default:
     298                        seconds = 15;
     299                        countdown = 0;
     300                }
     301
     302                // Reset when the new interval value is lower than the current one
     303                reset = seconds * 1000 < interval;
     304
     305                if ( countdown ) {
     306                    tempInterval = seconds * 1000;
     307                } else {
     308                    interval = seconds * 1000;
     309                    tempInterval = 0;
     310                }
     311
     312                if ( reset )
     313                    next();
     314            }
     315
     316            if ( !hasFocus )
     317                return 120;
     318
     319            return tempInterval ? tempInterval / 1000 : interval / 1000;
     320        };
     321
     322        // Start. Has no effect if heartbeat is already running
     323        this.start = function() {
     324            if ( running )
     325                return false;
     326
     327            running = true;
     328            connect();
     329            return true;
     330        };
     331
     332        // Stop. If a XHR is in progress, abort it
     333        this.stop = function() {
     334            if ( self.xhr && self.xhr.readyState != 4 )
     335                self.xhr.abort();
     336
     337            running = false;
     338            return true;
     339        }
     340
     341        /**
     342         * Send data with the next XHR
     343         *
     344         * As the data is sent later, this function doesn't return the XHR response.
     345         * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example:
     346         *      $(document).on('heartbeat-tick.myname', function(data, textStatus, jqXHR) {
     347         *          // code
     348         *      });
     349         * If the same 'handle' is used more than once, the data is overwritten when the third argument is 'true'.
     350         * Use wp.heartbeat.isQueued('handle') to see if any data is already queued for that handle.
     351         *
     352         * $param string handle Unique handle for the data. The handle is used in PHP to receive the data
     353         * $param mixed data The data to be sent
     354         * $param bool overwrite Whether to overwrite existing data in the queue
     355         * $return bool Whether the data was queued or not
     356         */
     357        this.send = function(handle, data, overwrite) {
     358            if ( handle ) {
     359                if ( queue.hasOwnProperty(handle) && !overwrite )
     360                    return false;
     361
     362                queue[handle] = data;
     363                return true;
     364            }
     365            return false;
     366        }
     367
     368        /**
     369         * Check if data with a particular handle is queued
     370         *
     371         * $param string handle The handle for the data
     372         * $return mixed The data queued with that handle or null
     373         */
     374        this.isQueued = function(handle) {
     375            return queue[handle];
     376        }
    165377    }
    166378
    167379    $.extend( Heartbeat.prototype, {
    168         tick: function(r) {
    169             $(document).trigger( 'heartbeat-tick', r );
     380        tick: function(data, textStatus, jqXHR) {
     381            $(document).trigger( 'heartbeat-tick', [data, textStatus, jqXHR] );
    170382        },
    171         error: function(r) {
    172             $(document).trigger( 'heartbeat-error', r );
     383        error: function(jqXHR, textStatus, error) {
     384            $(document).trigger( 'heartbeat-error', [jqXHR, textStatus, error] );
    173385        }
    174386    });
Note: See TracChangeset for help on using the changeset viewer.