From ba10ab197b8ef94fd1d338d30790eda8887632e4 Mon Sep 17 00:00:00 2001 From: Andrew Ozz Date: Mon, 10 Nov 2014 01:47:24 +0000 Subject: [PATCH] Heartbeat: - Use the page visibility API (when available) and document.hasFocus() instead of window.onfocus/onblur. Improves speeding up/slowing down the interval and works for iframes by default. - Add a setting for minimal interval. Maximum value is 10 min. This overrides all other intervals and cannot be changed after setting it at initialization. Can be used to reduce the frequency of requests on hosts that have low limits for used CPU time, etc. - Extend the setting of interval to support 120 sec. (60 sec, is still the default). - Always suspend after one hour of keyboard/mouse/touch inactivity. Fixes #29779. Built from https://develop.svn.wordpress.org/trunk@30293 git-svn-id: http://core.svn.wordpress.org/trunk@30292 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/js/heartbeat.js | 210 +++++++++++++++++--------------- wp-includes/js/heartbeat.min.js | 2 +- wp-includes/version.php | 2 +- 3 files changed, 113 insertions(+), 101 deletions(-) diff --git a/wp-includes/js/heartbeat.js b/wp-includes/js/heartbeat.js index 63ee4f523c..b4c8316780 100644 --- a/wp-includes/js/heartbeat.js +++ b/wp-includes/js/heartbeat.js @@ -57,6 +57,9 @@ // Used when the interval is reset originalInterval: 0, + // Used to limit the number of AJAX requests. + minimalInterval: 0, + // Used together with tempInterval countdown: 0, @@ -81,10 +84,8 @@ // Flags whether events tracking user activity were set userActivityEvents: false, - // References to various timeouts - beatTimer: 0, - winBlurTimer: 0, - frameBlurTimer: 0 + checkFocusTimer: 0, + beatTimer: 0 }; /** @@ -95,6 +96,8 @@ * @return void */ function initialize() { + var options, hidden, visibilityState, visibilitychange; + if ( typeof window.pagenow === 'string' ) { settings.screenId = window.pagenow; } @@ -105,24 +108,39 @@ // Pull in options passed from PHP if ( typeof window.heartbeatSettings === 'object' ) { - var options = window.heartbeatSettings; + options = window.heartbeatSettings; // The XHR URL can be passed as option when window.ajaxurl is not set if ( ! settings.url && options.ajaxurl ) { settings.url = options.ajaxurl; } - // The interval can be from 15 to 60 sec. and can be set temporarily to 5 sec. + // The interval can be from 15 to 120 sec. and can be set temporarily to 5 sec. + // It can be set in the initial options or changed later from JS and/or from PHP. if ( options.interval ) { settings.mainInterval = options.interval; if ( settings.mainInterval < 15 ) { settings.mainInterval = 15; - } else if ( settings.mainInterval > 60 ) { - settings.mainInterval = 60; + } else if ( settings.mainInterval > 120 ) { + settings.mainInterval = 120; } } + // Used to limit the number of AJAX requests. Overrides all other intervals if they are shorter. + // Needed for some hosts that cannot handle frequent requests and the user may exceed the allocated server CPU time, etc. + // The minimal interval can be up to 600 sec. however setting it to longer than 120 sec. will limit or disable + // some of the functionality (like post locks). + // Once set at initialization, minimalInterval cannot be changed/overriden. + if ( options.minimalInterval ) { + options.minimalInterval = parseInt( options.minimalInterval, 10 ); + settings.minimalInterval = options.minimalInterval > 0 && options.minimalInterval <= 600 ? options.minimalInterval * 1000 : 0; + } + + if ( settings.minimalInterval && settings.mainInterval < settings.minimalInterval ) { + settings.mainInterval = settings.minimalInterval; + } + // 'screenId' can be added from settings on the front-end where the JS global 'pagenow' is not set if ( ! settings.screenId ) { settings.screenId = options.screenId || 'front'; @@ -137,16 +155,47 @@ settings.mainInterval = settings.mainInterval * 1000; settings.originalInterval = settings.mainInterval; - // Set focus/blur events on the window - $(window).on( 'blur.wp-heartbeat-focus', function() { - setFrameFocusEvents(); - // We don't know why the 'blur' was fired. Either the user clicked in an iframe or outside the browser. - // Running blurred() after some timeout lets us cancel it if the user clicked in an iframe. - settings.winBlurTimer = window.setTimeout( function(){ blurred(); }, 500 ); - }).on( 'focus.wp-heartbeat-focus', function() { - removeFrameFocusEvents(); - focused(); - }).on( 'unload.wp-heartbeat', function() { + // Switch the interval to 120 sec. by using the Page Visibility API. + // If the browser doesn't support it (Safari < 7, Android < 4.4, IE < 10), the interval + // will be increased to 120 sec. after 5 min. of mouse and keyboard inactivity. + if ( typeof document.hidden !== 'undefined' ) { + hidden = 'hidden'; + visibilitychange = 'visibilitychange'; + visibilityState = 'visibilityState'; + } else if ( typeof document.msHidden !== 'undefined' ) { // IE10 + hidden = 'msHidden'; + visibilitychange = 'msvisibilitychange'; + visibilityState = 'msVisibilityState'; + } else if ( typeof document.webkitHidden !== 'undefined' ) { // Android + hidden = 'webkitHidden'; + visibilitychange = 'webkitvisibilitychange'; + visibilityState = 'webkitVisibilityState'; + } + + if ( hidden ) { + if ( document[hidden] ) { + settings.hasFocus = false; + } + + $document.on( visibilitychange + '.wp-heartbeat', function() { + if ( document[visibilityState] === 'hidden' ) { + blurred(); + window.clearInterval( settings.checkFocusTimer ); + } else { + focused(); + if ( document.hasFocus ) { + settings.checkFocusTimer = window.setInterval( checkFocus, 10000 ); + } + } + }); + } + + // Use document.hasFocus() if available. + if ( document.hasFocus ) { + settings.checkFocusTimer = window.setInterval( checkFocus, 10000 ); + } + + $(window).on( 'unload.wp-heartbeat', function() { // Don't connect any more settings.suspend = true; @@ -157,7 +206,7 @@ }); // Check for user activity every 30 seconds. - window.setInterval( function(){ checkUserActivity(); }, 30000 ); + window.setInterval( checkUserActivity, 30000 ); // Start one tick after DOM ready $document.ready( function() { @@ -206,6 +255,21 @@ return false; } + /** + * Check if the document's focus has changed + * + * @access private + * + * @return void + */ + function checkFocus() { + if ( settings.hasFocus && ! document.hasFocus() ) { + blurred(); + } else if ( ! settings.hasFocus && document.hasFocus() ) { + focused(); + } + } + /** * Set error state and fire an event on XHR errors or timeout * @@ -374,12 +438,16 @@ } } + if ( settings.minimalInterval && interval < settings.minimalInterval ) { + interval = settings.minimalInterval; + } + window.clearTimeout( settings.beatTimer ); if ( delta < interval ) { settings.beatTimer = window.setTimeout( function() { - connect(); + connect(); }, interval - delta ); @@ -389,26 +457,24 @@ } /** - * Set the internal state when the browser window looses focus + * Set the internal state when the browser window becomes hidden or loses focus * * @access private * * @return void */ function blurred() { - clearFocusTimers(); settings.hasFocus = false; } /** - * Set the internal state when the browser window is focused + * Set the internal state when the browser window becomes visible or is in focus * * @access private * * @return void */ function focused() { - clearFocusTimers(); settings.userActivity = time(); // Resume if suspended @@ -420,68 +486,6 @@ } } - /** - * Add focus/blur events to all local iframes - * - * Used to detect when focus is moved from the main window to an iframe - * - * @access private - * - * @return void - */ - function setFrameFocusEvents() { - $('iframe').each( function( i, frame ) { - if ( ! isLocalFrame( frame ) ) { - return; - } - - if ( $.data( frame, 'wp-heartbeat-focus' ) ) { - return; - } - - $.data( frame, 'wp-heartbeat-focus', 1 ); - - $( frame.contentWindow ).on( 'focus.wp-heartbeat-focus', function() { - focused(); - }).on('blur.wp-heartbeat-focus', function() { - setFrameFocusEvents(); - // We don't know why 'blur' was fired. Either the user clicked in the main window or outside the browser. - // Running blurred() after some timeout lets us cancel it if the user clicked in the main window. - settings.frameBlurTimer = window.setTimeout( function(){ blurred(); }, 500 ); - }); - }); - } - - /** - * Remove the focus/blur events to all local iframes - * - * @access private - * - * @return void - */ - function removeFrameFocusEvents() { - $('iframe').each( function( i, frame ) { - if ( ! isLocalFrame( frame ) ) { - return; - } - - $.removeData( frame, 'wp-heartbeat-focus' ); - $( frame.contentWindow ).off( '.wp-heartbeat-focus' ); - }); - } - - /** - * Clear the reset timers for focus/blur events on the window and iframes - * - * @access private - * - * @return void - */ - function clearFocusTimers() { - window.clearTimeout( settings.winBlurTimer ); - window.clearTimeout( settings.frameBlurTimer ); - } - /** * Runs when the user becomes active after a period of inactivity * @@ -494,11 +498,9 @@ $document.off( '.wp-heartbeat-active' ); $('iframe').each( function( i, frame ) { - if ( ! isLocalFrame( frame ) ) { - return; + if ( isLocalFrame( frame ) ) { + $( frame.contentWindow ).off( '.wp-heartbeat-active' ); } - - $( frame.contentWindow ).off( '.wp-heartbeat-active' ); }); focused(); @@ -519,25 +521,28 @@ function checkUserActivity() { var lastActive = settings.userActivity ? time() - settings.userActivity : 0; + // Throttle down when no mouse or keyboard activity for 5 min. if ( lastActive > 300000 && settings.hasFocus ) { - // Throttle down when no mouse or keyboard activity for 5 min blurred(); } - if ( settings.suspendEnabled && lastActive > 1200000 ) { - // Suspend after 20 min. of inactivity + // Suspend after 10 min. of inactivity when suspending is enabled. + // Always suspend after 60 min. of inactivity. This will release the post lock, etc. + if ( ( settings.suspendEnabled && lastActive > 600000 ) || lastActive > 3600000 ) { settings.suspend = true; } if ( ! settings.userActivityEvents ) { - $document.on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } ); + $document.on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active touchend.wp-heartbeat-active', function() { + userIsActive(); + }); $('iframe').each( function( i, frame ) { - if ( ! isLocalFrame( frame ) ) { - return; + if ( isLocalFrame( frame ) ) { + $( frame.contentWindow ).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active touchend.wp-heartbeat-active', function() { + userIsActive(); + }); } - - $( frame.contentWindow ).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } ); }); settings.userActivityEvents = true; @@ -597,7 +602,7 @@ * In this case the number of 'ticks' can be passed as second argument. * If the window doesn't have focus, the interval slows down to 2 min. * - * @param mixed speed Interval: 'fast' or 5, 15, 30, 60 + * @param mixed speed Interval: 'fast' or 5, 15, 30, 60, 120 * @param string ticks Used with speed = 'fast' or 5, how many ticks before the interval reverts back * @return int Current interval in seconds */ @@ -620,6 +625,9 @@ case 60: newInterval = 60000; break; + case 120: + newInterval = 120000; + break; case 'long-polling': // Allow long polling, (experimental) settings.mainInterval = 0; @@ -628,6 +636,10 @@ newInterval = settings.originalInterval; } + if ( settings.minimalInterval && newInterval < settings.minimalInterval ) { + newInterval = settings.minimalInterval; + } + if ( 5000 === newInterval ) { ticks = parseInt( ticks, 10 ) || 30; ticks = ticks < 1 || ticks > 30 ? 30 : ticks; diff --git a/wp-includes/js/heartbeat.min.js b/wp-includes/js/heartbeat.min.js index d2f17f3ba4..29d0b4a0fa 100644 --- a/wp-includes/js/heartbeat.min.js +++ b/wp-includes/js/heartbeat.min.js @@ -1 +1 @@ -!function(a,b,c){var d=function(){function d(){if("string"==typeof b.pagenow&&(B.screenId=b.pagenow),"string"==typeof b.ajaxurl&&(B.url=b.ajaxurl),"object"==typeof b.heartbeatSettings){var c=b.heartbeatSettings;!B.url&&c.ajaxurl&&(B.url=c.ajaxurl),c.interval&&(B.mainInterval=c.interval,B.mainInterval<15?B.mainInterval=15:B.mainInterval>60&&(B.mainInterval=60)),B.screenId||(B.screenId=c.screenId||"front"),"disable"===c.suspension&&(B.suspendEnabled=!1)}B.mainInterval=1e3*B.mainInterval,B.originalInterval=B.mainInterval,a(b).on("blur.wp-heartbeat-focus",function(){m(),B.winBlurTimer=b.setTimeout(function(){k()},500)}).on("focus.wp-heartbeat-focus",function(){n(),l()}).on("unload.wp-heartbeat",function(){B.suspend=!0,B.xhr&&4!==B.xhr.readyState&&B.xhr.abort()}),b.setInterval(function(){q()},3e4),A.ready(function(){B.lastTick=e(),j()})}function e(){return(new Date).getTime()}function f(a){var c,d=a.src;if(d&&/^https?:\/\//.test(d)&&(c=b.location.origin?b.location.origin:b.location.protocol+"//"+b.location.host,0!==d.indexOf(c)))return!1;try{if(a.contentWindow.document)return!0}catch(e){}return!1}function g(a,b){var c;if(a){switch(a){case"abort":break;case"timeout":c=!0;break;case"error":if(503===b&&B.hasConnected){c=!0;break}case"parsererror":case"empty":case"unknown":B.errorcount++,B.errorcount>2&&B.hasConnected&&(c=!0)}c&&!s()&&(B.connectionError=!0,A.trigger("heartbeat-connection-lost",[a,b]))}}function h(){B.hasConnected=!0,s()&&(B.errorcount=0,B.connectionError=!1,A.trigger("heartbeat-connection-restored"))}function i(){var c,d;B.connecting||B.suspend||(B.lastTick=e(),d=a.extend({},B.queue),B.queue={},A.trigger("heartbeat-send",[d]),c={data:d,interval:B.tempInterval?B.tempInterval/1e3:B.mainInterval/1e3,_nonce:"object"==typeof b.heartbeatSettings?b.heartbeatSettings.nonce:"",action:"heartbeat",screen_id:B.screenId,has_focus:B.hasFocus},B.connecting=!0,B.xhr=a.ajax({url:B.url,type:"post",timeout:3e4,data:c,dataType:"json"}).always(function(){B.connecting=!1,j()}).done(function(a,b,c){var d;return a?(h(),a.nonces_expired?void A.trigger("heartbeat-nonces-expired"):(a.heartbeat_interval&&(d=a.heartbeat_interval,delete a.heartbeat_interval),A.trigger("heartbeat-tick",[a,b,c]),void(d&&v(d)))):void g("empty")}).fail(function(a,b,c){g(b||"unknown",a.status),A.trigger("heartbeat-error",[a,b,c])}))}function j(){var a=e()-B.lastTick,c=B.mainInterval;B.suspend||(B.hasFocus?B.countdown>0&&B.tempInterval&&(c=B.tempInterval,B.countdown--,B.countdown<1&&(B.tempInterval=0)):c=12e4,b.clearTimeout(B.beatTimer),c>a?B.beatTimer=b.setTimeout(function(){i()},c-a):i())}function k(){o(),B.hasFocus=!1}function l(){o(),B.userActivity=e(),B.suspend=!1,B.hasFocus||(B.hasFocus=!0,j())}function m(){a("iframe").each(function(c,d){f(d)&&(a.data(d,"wp-heartbeat-focus")||(a.data(d,"wp-heartbeat-focus",1),a(d.contentWindow).on("focus.wp-heartbeat-focus",function(){l()}).on("blur.wp-heartbeat-focus",function(){m(),B.frameBlurTimer=b.setTimeout(function(){k()},500)})))})}function n(){a("iframe").each(function(b,c){f(c)&&(a.removeData(c,"wp-heartbeat-focus"),a(c.contentWindow).off(".wp-heartbeat-focus"))})}function o(){b.clearTimeout(B.winBlurTimer),b.clearTimeout(B.frameBlurTimer)}function p(){B.userActivityEvents=!1,A.off(".wp-heartbeat-active"),a("iframe").each(function(b,c){f(c)&&a(c.contentWindow).off(".wp-heartbeat-active")}),l()}function q(){var b=B.userActivity?e()-B.userActivity:0;b>3e5&&B.hasFocus&&k(),B.suspendEnabled&&b>12e5&&(B.suspend=!0),B.userActivityEvents||(A.on("mouseover.wp-heartbeat-active keyup.wp-heartbeat-active",function(){p()}),a("iframe").each(function(b,c){f(c)&&a(c.contentWindow).on("mouseover.wp-heartbeat-active keyup.wp-heartbeat-active",function(){p()})}),B.userActivityEvents=!0)}function r(){return B.hasFocus}function s(){return B.connectionError}function t(){B.lastTick=0,j()}function u(){B.suspendEnabled=!1}function v(a,b){var c,d=B.tempInterval?B.tempInterval:B.mainInterval;if(a){switch(a){case"fast":case 5:c=5e3;break;case 15:c=15e3;break;case 30:c=3e4;break;case 60:c=6e4;break;case"long-polling":return B.mainInterval=0,0;default:c=B.originalInterval}5e3===c?(b=parseInt(b,10)||30,b=1>b||b>30?30:b,B.countdown=b,B.tempInterval=c):(B.countdown=0,B.tempInterval=0,B.mainInterval=c),c!==d&&j()}return B.tempInterval?B.tempInterval/1e3:B.mainInterval/1e3}function w(a,b,c){return a?c&&this.isQueued(a)?!1:(B.queue[a]=b,!0):!1}function x(a){return a?B.queue.hasOwnProperty(a):void 0}function y(a){a&&delete B.queue[a]}function z(a){return a?this.isQueued(a)?B.queue[a]:c:void 0}var A=a(document),B={suspend:!1,suspendEnabled:!0,screenId:"",url:"",lastTick:0,queue:{},mainInterval:60,tempInterval:0,originalInterval:0,countdown:0,connecting:!1,connectionError:!1,errorcount:0,hasConnected:!1,hasFocus:!0,userActivity:0,userActivityEvents:!1,beatTimer:0,winBlurTimer:0,frameBlurTimer:0};return d(),{hasFocus:r,connectNow:t,disableSuspend:u,interval:v,hasConnectionError:s,enqueue:w,dequeue:y,isQueued:x,getQueuedItem:z}};b.wp=b.wp||{},b.wp.heartbeat=new d}(jQuery,window); \ No newline at end of file +!function(a,b,c){var d=function(){function d(){var c,d,f,h;"string"==typeof b.pagenow&&(z.screenId=b.pagenow),"string"==typeof b.ajaxurl&&(z.url=b.ajaxurl),"object"==typeof b.heartbeatSettings&&(c=b.heartbeatSettings,!z.url&&c.ajaxurl&&(z.url=c.ajaxurl),c.interval&&(z.mainInterval=c.interval,z.mainInterval<15?z.mainInterval=15:z.mainInterval>120&&(z.mainInterval=120)),c.minimalInterval&&(c.minimalInterval=parseInt(c.minimalInterval,10),z.minimalInterval=c.minimalInterval>0&&c.minimalInterval<=600?1e3*c.minimalInterval:0),z.minimalInterval&&z.mainInterval2&&z.hasConnected&&(c=!0)}c&&!q()&&(z.connectionError=!0,y.trigger("heartbeat-connection-lost",[a,b]))}}function i(){z.hasConnected=!0,q()&&(z.errorcount=0,z.connectionError=!1,y.trigger("heartbeat-connection-restored"))}function j(){var c,d;z.connecting||z.suspend||(z.lastTick=e(),d=a.extend({},z.queue),z.queue={},y.trigger("heartbeat-send",[d]),c={data:d,interval:z.tempInterval?z.tempInterval/1e3:z.mainInterval/1e3,_nonce:"object"==typeof b.heartbeatSettings?b.heartbeatSettings.nonce:"",action:"heartbeat",screen_id:z.screenId,has_focus:z.hasFocus},z.connecting=!0,z.xhr=a.ajax({url:z.url,type:"post",timeout:3e4,data:c,dataType:"json"}).always(function(){z.connecting=!1,k()}).done(function(a,b,c){var d;return a?(i(),a.nonces_expired?void y.trigger("heartbeat-nonces-expired"):(a.heartbeat_interval&&(d=a.heartbeat_interval,delete a.heartbeat_interval),y.trigger("heartbeat-tick",[a,b,c]),void(d&&t(d)))):void h("empty")}).fail(function(a,b,c){h(b||"unknown",a.status),y.trigger("heartbeat-error",[a,b,c])}))}function k(){var a=e()-z.lastTick,c=z.mainInterval;z.suspend||(z.hasFocus?z.countdown>0&&z.tempInterval&&(c=z.tempInterval,z.countdown--,z.countdown<1&&(z.tempInterval=0)):c=12e4,z.minimalInterval&&ca?z.beatTimer=b.setTimeout(function(){j()},c-a):j())}function l(){z.hasFocus=!1}function m(){z.userActivity=e(),z.suspend=!1,z.hasFocus||(z.hasFocus=!0,k())}function n(){z.userActivityEvents=!1,y.off(".wp-heartbeat-active"),a("iframe").each(function(b,c){f(c)&&a(c.contentWindow).off(".wp-heartbeat-active")}),m()}function o(){var b=z.userActivity?e()-z.userActivity:0;b>3e5&&z.hasFocus&&l(),(z.suspendEnabled&&b>6e5||b>36e5)&&(z.suspend=!0),z.userActivityEvents||(y.on("mouseover.wp-heartbeat-active keyup.wp-heartbeat-active touchend.wp-heartbeat-active",function(){n()}),a("iframe").each(function(b,c){f(c)&&a(c.contentWindow).on("mouseover.wp-heartbeat-active keyup.wp-heartbeat-active touchend.wp-heartbeat-active",function(){n()})}),z.userActivityEvents=!0)}function p(){return z.hasFocus}function q(){return z.connectionError}function r(){z.lastTick=0,k()}function s(){z.suspendEnabled=!1}function t(a,b){var c,d=z.tempInterval?z.tempInterval:z.mainInterval;if(a){switch(a){case"fast":case 5:c=5e3;break;case 15:c=15e3;break;case 30:c=3e4;break;case 60:c=6e4;break;case 120:c=12e4;break;case"long-polling":return z.mainInterval=0,0;default:c=z.originalInterval}z.minimalInterval&&cb||b>30?30:b,z.countdown=b,z.tempInterval=c):(z.countdown=0,z.tempInterval=0,z.mainInterval=c),c!==d&&k()}return z.tempInterval?z.tempInterval/1e3:z.mainInterval/1e3}function u(a,b,c){return a?c&&this.isQueued(a)?!1:(z.queue[a]=b,!0):!1}function v(a){return a?z.queue.hasOwnProperty(a):void 0}function w(a){a&&delete z.queue[a]}function x(a){return a?this.isQueued(a)?z.queue[a]:c:void 0}var y=a(document),z={suspend:!1,suspendEnabled:!0,screenId:"",url:"",lastTick:0,queue:{},mainInterval:60,tempInterval:0,originalInterval:0,minimalInterval:0,countdown:0,connecting:!1,connectionError:!1,errorcount:0,hasConnected:!1,hasFocus:!0,userActivity:0,userActivityEvents:!1,checkFocusTimer:0,beatTimer:0};return d(),{hasFocus:p,connectNow:r,disableSuspend:s,interval:t,hasConnectionError:q,enqueue:u,dequeue:w,isQueued:v,getQueuedItem:x}};b.wp=b.wp||{},b.wp.heartbeat=new d}(jQuery,window); \ No newline at end of file diff --git a/wp-includes/version.php b/wp-includes/version.php index 2025ce6e5c..3a4ee6177f 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -4,7 +4,7 @@ * * @global string $wp_version */ -$wp_version = '4.1-alpha-30292'; +$wp_version = '4.1-alpha-30293'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.