WordPress.org

Make WordPress Core

Changeset 23382


Ignore:
Timestamp:
02/03/13 07:03:27 (15 months 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.